Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 40 additions & 2 deletions internal/orchestrator/orchestrator.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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 {
Expand All @@ -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
Expand All @@ -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.
Expand All @@ -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 {
Expand Down
110 changes: 110 additions & 0 deletions internal/orchestrator/orchestrator_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{
Expand Down
Loading