diff --git a/internal/orchestrator/orchestrator.go b/internal/orchestrator/orchestrator.go index 6141c95..4f82922 100644 --- a/internal/orchestrator/orchestrator.go +++ b/internal/orchestrator/orchestrator.go @@ -716,6 +716,7 @@ func (o *Orchestrator) buildPlanPrompt(task *tasks.Task) string { if o.runMeta != nil && o.runMeta.Branch != "" { branchInstruction = fmt.Sprintf("\n Create your feature branch from `%s`.", o.runMeta.Branch) } + taskGuidance := o.buildTaskSpecificPlanGuidance(task) return fmt.Sprintf(`You are a planning agent. Create a detailed execution plan for this task. @@ -733,7 +734,7 @@ Description: %s Nightshift-Ref: https://github.com/marcus/nightshift 4. Analyze the task requirements 5. Identify files that need to be modified -6. Create step-by-step implementation plan +6. Create step-by-step implementation plan%s 7. Output only valid JSON (no markdown, no extra text). The output is read by a machine. Use this schema: { @@ -741,7 +742,7 @@ Description: %s "files": ["file1.go", "file2.go", ...], "description": "overall approach" } -`, task.ID, task.Title, task.Description, branchInstruction, task.Type) +`, task.ID, task.Title, task.Description, branchInstruction, task.Type, taskGuidance) } func (o *Orchestrator) buildImplementPrompt(task *tasks.Task, plan *PlanOutput, iteration int) string { @@ -754,6 +755,7 @@ func (o *Orchestrator) buildImplementPrompt(task *tasks.Task, plan *PlanOutput, if o.runMeta != nil && o.runMeta.Branch != "" { branchInstruction = fmt.Sprintf("\n Checkout `%s` before creating your feature branch.", o.runMeta.Branch) } + taskGuidance := o.buildTaskSpecificImplementGuidance(task) return fmt.Sprintf(`You are an implementation agent. Execute the plan for this task. @@ -776,14 +778,51 @@ Description: %s Nightshift-Ref: https://github.com/marcus/nightshift 2. Implement the plan step by step 3. Make all necessary code changes -4. Ensure tests pass +4. Ensure tests pass%s 5. Output a summary as JSON: { "files_modified": ["file1.go", ...], "summary": "what was done" } -`, task.ID, task.Title, task.Description, plan.Description, plan.Steps, iterationNote, branchInstruction, task.Type) +`, task.ID, task.Title, task.Description, plan.Description, plan.Steps, iterationNote, branchInstruction, task.Type, taskGuidance) +} + +// buildTaskSpecificPlanGuidance appends extra planning instructions for tasks +// whose safe default behavior needs more precision than the generic template. +func (o *Orchestrator) buildTaskSpecificPlanGuidance(task *tasks.Task) string { + switch task.Type { + case tasks.TaskCommitNormalize: + return ` + +## Task-Specific Guidance +- Treat commit-normalize as a low-risk, prospective cleanup for new work and lightweight repo guidance. +- Inspect recent commits first and prefer the repository's existing commit style when it is clear. +- If the repository has no consistent style, use a concise conventional format for new commit messages. +- Preserve required trailers and any project-specific footer lines. +- Keep subjects concise and avoid plans that rewrite published or shared history.` + default: + return "" + } +} + +// buildTaskSpecificImplementGuidance keeps built-in low-risk tasks scoped to +// safe actions. commit-normalize should standardize future work, not rewrite +// existing shared history. +func (o *Orchestrator) buildTaskSpecificImplementGuidance(task *tasks.Task) string { + switch task.Type { + case tasks.TaskCommitNormalize: + return ` + +## Task-Specific Guidance +- Prefer the repository's established commit message style when present; otherwise use a clear conventional format. +- Preserve required trailers and any project-specific footer lines on new commits you create. +- Keep commit messages concise. +- Standardize prospectively via new commits and lightweight repo guidance; do not rewrite published history. +- Do not rebase, force-push, or otherwise rewrite shared branch history.` + default: + return "" + } } func (o *Orchestrator) buildReviewPrompt(task *tasks.Task, impl *ImplementOutput) string { diff --git a/internal/orchestrator/orchestrator_test.go b/internal/orchestrator/orchestrator_test.go index 45bff5c..40332ea 100644 --- a/internal/orchestrator/orchestrator_test.go +++ b/internal/orchestrator/orchestrator_test.go @@ -468,6 +468,80 @@ func TestBuildPrompts(t *testing.T) { } } +func TestBuildPlanPrompt_CommitNormalizeIncludesTaskSpecificGuidance(t *testing.T) { + o := New() + task := &tasks.Task{ + ID: "commit-normalize:/repo", + Title: "Commit Message Normalizer", + Description: "Standardize commit message format safely", + Type: tasks.TaskCommitNormalize, + } + + prompt := o.buildPlanPrompt(task) + for _, want := range []string{ + "Treat commit-normalize as a low-risk, prospective cleanup", + "prefer the repository's existing commit style", + "use a concise conventional format", + "Preserve required trailers", + "avoid plans that rewrite published or shared history", + } { + if !strings.Contains(prompt, want) { + t.Errorf("plan prompt missing %q\nGot:\n%s", want, prompt) + } + } +} + +func TestBuildImplementPrompt_CommitNormalizeIncludesTaskSpecificGuidance(t *testing.T) { + o := New() + task := &tasks.Task{ + ID: "commit-normalize:/repo", + Title: "Commit Message Normalizer", + Description: "Standardize commit message format safely", + Type: tasks.TaskCommitNormalize, + } + plan := &PlanOutput{ + Steps: []string{"Inspect recent commits", "Update prompt guidance"}, + Description: "Keep the task prospective and non-destructive.", + } + + prompt := o.buildImplementPrompt(task, plan, 1) + for _, want := range []string{ + "Prefer the repository's established commit message style when present", + "otherwise use a clear conventional format", + "Preserve required trailers", + "Keep commit messages concise", + "do not rewrite published history", + "Do not rebase, force-push, or otherwise rewrite shared branch history.", + } { + if !strings.Contains(prompt, want) { + t.Errorf("implement prompt missing %q\nGot:\n%s", want, prompt) + } + } +} + +func TestBuildPrompts_GenericTaskDoesNotIncludeCommitNormalizeGuidance(t *testing.T) { + o := New() + task := &tasks.Task{ + ID: "lint-fix:/repo", + Title: "Lint Fixes", + Description: "Automatically fix linting errors and style issues", + Type: tasks.TaskLintFix, + } + plan := &PlanOutput{ + Steps: []string{"Run linter"}, + Description: "Generic prompt path.", + } + + planPrompt := o.buildPlanPrompt(task) + implPrompt := o.buildImplementPrompt(task, plan, 1) + + for _, got := range []string{planPrompt, implPrompt} { + if strings.Contains(got, "Do not rebase, force-push, or otherwise rewrite shared branch history.") { + t.Errorf("generic task prompt unexpectedly included commit-normalize guidance\nGot:\n%s", got) + } + } +} + func TestExtractPRURL(t *testing.T) { tests := []struct { name string diff --git a/internal/tasks/tasks.go b/internal/tasks/tasks.go index 2c7dabb..4490768 100644 --- a/internal/tasks/tasks.go +++ b/internal/tasks/tasks.go @@ -332,7 +332,7 @@ Apply safe updates directly, and leave concise follow-ups for anything uncertain Type: TaskCommitNormalize, Category: CategoryPR, Name: "Commit Message Normalizer", - Description: "Standardize commit message format", + Description: "Standardize commit message conventions for new work without rewriting shared history", CostTier: CostLow, RiskLevel: RiskLow, DefaultInterval: 24 * time.Hour, diff --git a/internal/tasks/tasks_test.go b/internal/tasks/tasks_test.go index 03cdf18..516c9bb 100644 --- a/internal/tasks/tasks_test.go +++ b/internal/tasks/tasks_test.go @@ -489,3 +489,26 @@ func TestSpecificDefaultIntervalOverrides(t *testing.T) { } } } + +func TestCommitNormalizeDefinition(t *testing.T) { + def, err := GetDefinition(TaskCommitNormalize) + if err != nil { + t.Fatalf("GetDefinition(%q) error: %v", TaskCommitNormalize, err) + } + + if def.Category != CategoryPR { + t.Errorf("Category = %d, want %d", def.Category, CategoryPR) + } + if def.CostTier != CostLow { + t.Errorf("CostTier = %d, want %d", def.CostTier, CostLow) + } + if def.RiskLevel != RiskLow { + t.Errorf("RiskLevel = %d, want %d", def.RiskLevel, RiskLow) + } + if def.DefaultInterval != 24*time.Hour { + t.Errorf("DefaultInterval = %v, want %v", def.DefaultInterval, 24*time.Hour) + } + if def.Description != "Standardize commit message conventions for new work without rewriting shared history" { + t.Errorf("Description = %q", def.Description) + } +} diff --git a/website/docs/task-reference.md b/website/docs/task-reference.md index cb241df..5edf181 100644 --- a/website/docs/task-reference.md +++ b/website/docs/task-reference.md @@ -23,7 +23,7 @@ Fully formed, review-ready artifacts. These tasks create branches and open pull | `backward-compat` | Backward-Compatibility Checks | Check and ensure backward compatibility | Medium | Low | 7d | | `build-optimize` | Build Time Optimization | Optimize build configuration for faster builds | High | Medium | 7d | | `docs-backfill` | Documentation Backfiller | Generate missing documentation | Low | Low | 7d | -| `commit-normalize` | Commit Message Normalizer | Standardize commit message format | Low | Low | 24h | +| `commit-normalize` | Commit Message Normalizer | Standardize commit message conventions for new work without rewriting shared history | Low | Low | 24h | | `changelog-synth` | Changelog Synthesizer | Generate changelog from commits | Low | Low | 7d | | `release-notes` | Release Note Drafter | Draft release notes from changes | Low | Low | 7d | | `adr-draft` | ADR Drafter | Draft Architecture Decision Records | Medium | Low | 7d |