From f29eaa13f11a343410008353af86e9970f601410 Mon Sep 17 00:00:00 2001 From: Marcus Vorwaller Date: Sun, 19 Apr 2026 02:48:50 -0700 Subject: [PATCH] fix: normalize commit task prompts Nightshift-Task: commit-normalize Nightshift-Ref: https://github.com/marcus/nightshift --- internal/orchestrator/orchestrator.go | 42 +++++++- internal/orchestrator/orchestrator_test.go | 110 +++++++++++++++++++++ 2 files changed, 150 insertions(+), 2 deletions(-) diff --git a/internal/orchestrator/orchestrator.go b/internal/orchestrator/orchestrator.go index 6141c95..f2394a7 100644 --- a/internal/orchestrator/orchestrator.go +++ b/internal/orchestrator/orchestrator.go @@ -711,18 +711,53 @@ func (o *Orchestrator) PlanPrompt(task *tasks.Task) string { return o.buildPlanPrompt(task) } +func taskSpecificPlanGuidance(task *tasks.Task) string { + switch task.Type { + case tasks.TaskCommitNormalize: + return ` +## Task-Specific Guidance +- Inspect recent commit subjects on the repo and active work branch or PR before deciding what to normalize. +- If the project does not document a commit-message standard, infer the local convention from recent commits and state that assumption. +- Keep the task scoped to the current work branch or PR. Do not plan git hooks, broad repository rewrites, or history changes outside the relevant commit range. +- Preserve existing Nightshift trailers when commit messages are reworded. +- Prefer small, explainable actions such as rewording the relevant commits or documenting the exact normalization steps when the agent cannot safely rewrite commits. +` + default: + return "" + } +} + +func taskSpecificImplementGuidance(task *tasks.Task) string { + switch task.Type { + case tasks.TaskCommitNormalize: + return ` +## Task-Specific Guidance +- Inspect recent commit subjects on the repo and active work branch or PR before normalizing commit messages. +- Infer the local convention from recent commits when no documented standard exists, and report that assumption clearly. +- Normalize only the commit messages relevant to the active branch or PR. Do not install git hooks or rewrite unrelated history. +- Keep Nightshift trailers intact on any rewritten or newly created commits. +- If the exact commit range is ambiguous, state the assumption and choose the narrowest safe scope. +` + default: + return "" + } +} + func (o *Orchestrator) buildPlanPrompt(task *tasks.Task) string { branchInstruction := "" if o.runMeta != nil && o.runMeta.Branch != "" { branchInstruction = fmt.Sprintf("\n Create your feature branch from `%s`.", o.runMeta.Branch) } + taskGuidance := taskSpecificPlanGuidance(task) + return fmt.Sprintf(`You are a planning agent. Create a detailed execution plan for this task. ## Task ID: %s Title: %s Description: %s +%s ## Instructions 0. You are running autonomously. If the task is broad or ambiguous, choose a concrete, minimal scope that delivers value and state any assumptions in the description. @@ -741,7 +776,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, taskGuidance, branchInstruction, task.Type) } func (o *Orchestrator) buildImplementPrompt(task *tasks.Task, plan *PlanOutput, iteration int) string { @@ -755,6 +790,8 @@ func (o *Orchestrator) buildImplementPrompt(task *tasks.Task, plan *PlanOutput, branchInstruction = fmt.Sprintf("\n Checkout `%s` before creating your feature branch.", o.runMeta.Branch) } + taskGuidance := taskSpecificImplementGuidance(task) + return fmt.Sprintf(`You are an implementation agent. Execute the plan for this task. ## Task @@ -768,6 +805,7 @@ Description: %s ## Steps %v %s +%s ## Instructions 0. Before creating your branch, record the current branch name. Create and work on a new branch. Never modify or commit directly to the primary branch.%s When finished, open a PR. After the PR is submitted, switch back to the original branch. If you cannot open a PR, leave the branch and explain next steps. @@ -783,7 +821,7 @@ Description: %s "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, taskGuidance, branchInstruction, task.Type) } 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..cf6f88e 100644 --- a/internal/orchestrator/orchestrator_test.go +++ b/internal/orchestrator/orchestrator_test.go @@ -809,6 +809,116 @@ func TestBuildImplementPrompt_WithoutBranch(t *testing.T) { } } +func TestBuildPlanPrompt_CommitNormalizeIncludesRepoAwareGuidance(t *testing.T) { + o := New() + + task := &tasks.Task{ + ID: "commit-normalize:/tmp/nightshift", + Title: "Commit Message Normalizer", + Description: "Standardize commit message format", + Type: tasks.TaskCommitNormalize, + } + + prompt := o.buildPlanPrompt(task) + + expected := []string{ + "## Task-Specific Guidance", + "Inspect recent commit subjects on the repo and active work branch or PR", + "infer the local convention from recent commits", + "Do not plan git hooks, broad repository rewrites, or history changes outside the relevant commit range", + "Preserve existing Nightshift trailers", + "rewording the relevant commits or documenting the exact normalization steps", + } + + for _, want := range expected { + if !strings.Contains(prompt, want) { + t.Errorf("plan prompt missing %q\nGot:\n%s", want, prompt) + } + } + + if strings.Count(prompt, "\n6. ") != 1 { + t.Errorf("plan prompt should contain exactly one step 6\nGot:\n%s", prompt) + } + if strings.Count(prompt, "\n7. ") != 1 { + t.Errorf("plan prompt should contain exactly one step 7\nGot:\n%s", prompt) + } +} + +func TestBuildImplementPrompt_CommitNormalizeIncludesRepoAwareGuidance(t *testing.T) { + o := New() + + task := &tasks.Task{ + ID: "commit-normalize:/tmp/nightshift", + Title: "Commit Message Normalizer", + Description: "Standardize commit message format", + Type: tasks.TaskCommitNormalize, + } + plan := &PlanOutput{ + Steps: []string{"Inspect recent commits", "Normalize relevant branch commits"}, + Description: "Normalize commit messages for the current work branch.", + } + + prompt := o.buildImplementPrompt(task, plan, 1) + + expected := []string{ + "## Task-Specific Guidance", + "Inspect recent commit subjects on the repo and active work branch or PR", + "Infer the local convention from recent commits when no documented standard exists", + "Normalize only the commit messages relevant to the active branch or PR", + "Do not install git hooks or rewrite unrelated history", + "Keep Nightshift trailers intact", + "If the exact commit range is ambiguous, state the assumption and choose the narrowest safe scope", + } + + for _, want := range expected { + if !strings.Contains(prompt, want) { + t.Errorf("implement prompt missing %q\nGot:\n%s", want, prompt) + } + } + + if strings.Count(prompt, "\n4. Ensure tests pass") != 1 { + t.Errorf("implement prompt should contain exactly one step 4\nGot:\n%s", prompt) + } + if strings.Count(prompt, "\n5. Output a summary as JSON:") != 1 { + t.Errorf("implement prompt should contain exactly one step 5\nGot:\n%s", prompt) + } +} + +func TestBuildPrompts_GenericTasksDoNotReceiveCommitNormalizeGuidance(t *testing.T) { + o := New() + + task := &tasks.Task{ + ID: "lint-fix:/tmp/nightshift", + Title: "Linter Fixes", + Description: "Automatically fix linting errors and style issues", + Type: tasks.TaskLintFix, + } + plan := &PlanOutput{ + Steps: []string{"Run linters", "Fix issues"}, + Description: "Apply lint fixes.", + } + + planPrompt := o.buildPlanPrompt(task) + implPrompt := o.buildImplementPrompt(task, plan, 1) + + unexpected := []string{ + "active work branch or PR", + "git hooks", + "Nightshift trailers", + "exact normalization steps", + "exact commit range is ambiguous", + } + + for _, notWant := range unexpected { + if strings.Contains(planPrompt, notWant) { + t.Errorf("generic plan prompt should not contain %q\nGot:\n%s", notWant, planPrompt) + } + if strings.Contains(implPrompt, notWant) { + t.Errorf("generic implement prompt should not contain %q\nGot:\n%s", notWant, implPrompt) + } + } +} + func TestBuildMetadataBlock_WithBranch(t *testing.T) { o := New() o.SetRunMetadata(&RunMetadata{