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
41 changes: 37 additions & 4 deletions internal/orchestrator/orchestrator.go
Original file line number Diff line number Diff line change
Expand Up @@ -711,8 +711,23 @@ func (o *Orchestrator) PlanPrompt(task *tasks.Task) string {
return o.buildPlanPrompt(task)
}

func taskSpecificPlanGuidance(task *tasks.Task) string {
func changelogComparisonBaseGuidance(baseBranch string) string {
if baseBranch != "" {
return fmt.Sprintf("- Nightshift provided `%s` as the comparison base; derive the commit range from `git merge-base %s HEAD` through `HEAD`, and exclude merge commits so the draft is deterministic.\n", baseBranch, baseBranch)
}

return "- Determine the intended comparison base for this repo or PR flow; derive the commit range from `git merge-base <base> HEAD` through `HEAD`, exclude merge commits so the draft stays deterministic, and state the assumption if the base branch is unclear.\n"
}

func taskSpecificPlanGuidance(task *tasks.Task, baseBranch string) string {
switch task.Type {
case tasks.TaskChangelogSynth:
return "\n\n## Task-Specific Guidance\n" +
"- Inspect the repo's current changelog and release signals before deciding the scope: CHANGELOG.md, release workflow files if present (for example .github/workflows/release.yml), and recent git tags/commits.\n" +
changelogComparisonBaseGuidance(baseBranch) +
"- If the release boundary or target changelog section is unclear, state the assumptions you made.\n" +
"- Prefer updating the existing changelog artifact and structure instead of inventing a new format, and preserve prior changelog history.\n" +
"- Plan for stable Markdown sections such as Added, Changed, Fixed, Docs, Refactor, Tests, Chore, and Other. Omit empty sections when appropriate.\n"
case tasks.TaskReleaseNotes:
return "\n\n## Task-Specific Guidance\n" +
"- Inspect the repo's existing release signals before deciding the scope: CHANGELOG.md, .github/workflows/release.yml, and recent git tags/commits.\n" +
Expand All @@ -724,8 +739,16 @@ func taskSpecificPlanGuidance(task *tasks.Task) string {
}
}

func taskSpecificImplementGuidance(task *tasks.Task) string {
func taskSpecificImplementGuidance(task *tasks.Task, baseBranch string) string {
switch task.Type {
case tasks.TaskChangelogSynth:
return "\n\n## Task-Specific Guidance\n" +
"- Inspect the repo's current changelog and release signals before drafting: CHANGELOG.md, release workflow files if present (for example .github/workflows/release.yml), and recent git tags/commits.\n" +
changelogComparisonBaseGuidance(baseBranch) +
"- Synthesize only the newest changelog section from supported commits, note assumptions if the release boundary or base branch is unclear, and do not invent entries.\n" +
"- Preserve prior changelog sections and structure; prefer updating the existing changelog artifact over creating a new format.\n" +
"- Organize the update into stable Markdown sections such as Added, Changed, Fixed, Docs, Refactor, Tests, Chore, and Other. Omit empty sections when appropriate.\n" +
"- Emit Markdown-ready changelog content only.\n"
case tasks.TaskReleaseNotes:
return "\n\n## Task-Specific Guidance\n" +
"- Inspect the repo's existing release signals before drafting: CHANGELOG.md, .github/workflows/release.yml, and recent git tags/commits.\n" +
Expand All @@ -743,7 +766,12 @@ func (o *Orchestrator) buildPlanPrompt(task *tasks.Task) string {
branchInstruction = fmt.Sprintf("\n Create your feature branch from `%s`.", o.runMeta.Branch)
}

taskGuidance := taskSpecificPlanGuidance(task)
baseBranch := ""
if o.runMeta != nil {
baseBranch = o.runMeta.Branch
}

taskGuidance := taskSpecificPlanGuidance(task, baseBranch)

return fmt.Sprintf(`You are a planning agent. Create a detailed execution plan for this task.

Expand Down Expand Up @@ -783,7 +811,12 @@ 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)
baseBranch := ""
if o.runMeta != nil {
baseBranch = o.runMeta.Branch
}

taskGuidance := taskSpecificImplementGuidance(task, baseBranch)

return fmt.Sprintf(`You are an implementation agent. Execute the plan for this task.

Expand Down
79 changes: 78 additions & 1 deletion internal/orchestrator/orchestrator_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -809,6 +809,79 @@ func TestBuildImplementPrompt_WithoutBranch(t *testing.T) {
}
}

func TestBuildPlanPrompt_ChangelogSynthUsesConfiguredBaseBranch(t *testing.T) {
o := New()
o.SetRunMetadata(&RunMetadata{Branch: "codex/release-notes-drafter"})

task := &tasks.Task{
ID: "changelog-synth:/tmp/nightshift",
Title: "Changelog Synthesizer",
Description: "Draft a deterministic changelog update",
Type: tasks.TaskChangelogSynth,
}

prompt := o.buildPlanPrompt(task)

expected := []string{
"CHANGELOG.md",
".github/workflows/release.yml",
"Nightshift provided `codex/release-notes-drafter` as the comparison base",
"`git merge-base codex/release-notes-drafter HEAD`",
"exclude merge commits",
"preserve prior changelog history",
"Added, Changed, Fixed, Docs, Refactor, Tests, Chore, and Other",
"state the assumptions",
}

for _, want := range expected {
if !strings.Contains(prompt, want) {
t.Errorf("plan prompt missing %q\nGot:\n%s", want, prompt)
}
}

if strings.Contains(prompt, "git merge-base main HEAD") {
t.Errorf("plan prompt should not hardcode main\nGot:\n%s", prompt)
}
}

func TestBuildImplementPrompt_ChangelogSynthWithoutConfiguredBaseUsesInference(t *testing.T) {
o := New()

task := &tasks.Task{
ID: "changelog-synth:/tmp/nightshift",
Title: "Changelog Synthesizer",
Description: "Draft a deterministic changelog update",
Type: tasks.TaskChangelogSynth,
}
plan := &PlanOutput{
Steps: []string{"Inspect CHANGELOG.md", "Draft the newest changelog section"},
Description: "Use the correct comparison base for the repo and preserve changelog structure.",
}

prompt := o.buildImplementPrompt(task, plan, 1)

expected := []string{
"CHANGELOG.md",
".github/workflows/release.yml",
"Determine the intended comparison base for this repo or PR flow",
"`git merge-base <base> HEAD`",
"exclude merge commits",
"do not invent entries",
"Preserve prior changelog sections and structure",
"Emit Markdown-ready changelog content only",
}

for _, want := range expected {
if !strings.Contains(prompt, want) {
t.Errorf("implement prompt missing %q\nGot:\n%s", want, prompt)
}
}

if strings.Contains(prompt, "git merge-base main HEAD") {
t.Errorf("implement prompt should not hardcode main\nGot:\n%s", prompt)
}
}

func TestBuildPlanPrompt_ReleaseNotesIncludesRepoAwareGuidance(t *testing.T) {
o := New()

Expand Down Expand Up @@ -871,7 +944,7 @@ func TestBuildImplementPrompt_ReleaseNotesIncludesRepoAwareGuidance(t *testing.T
}
}

func TestBuildPrompts_GenericTasksDoNotReceiveReleaseNotesGuidance(t *testing.T) {
func TestBuildPrompts_GenericTasksDoNotReceiveTaskSpecificReleaseOrChangelogGuidance(t *testing.T) {
o := New()

task := &tasks.Task{
Expand All @@ -894,6 +967,10 @@ func TestBuildPrompts_GenericTasksDoNotReceiveReleaseNotesGuidance(t *testing.T)
"release boundary",
"current release-notes artifact and format",
"Features, Fixes, Breaking Changes, and Other",
"git merge-base <base> HEAD",
"comparison base",
"Added, Changed, Fixed, Docs, Refactor, Tests, Chore, and Other",
"Markdown-ready changelog content only",
}

for _, notWant := range unexpected {
Expand Down
13 changes: 9 additions & 4 deletions internal/tasks/tasks.go
Original file line number Diff line number Diff line change
Expand Up @@ -338,10 +338,15 @@ Apply safe updates directly, and leave concise follow-ups for anything uncertain
DefaultInterval: 24 * time.Hour,
},
TaskChangelogSynth: {
Type: TaskChangelogSynth,
Category: CategoryPR,
Name: "Changelog Synthesizer",
Description: "Generate changelog from commits",
Type: TaskChangelogSynth,
Category: CategoryPR,
Name: "Changelog Synthesizer",
Description: `Inspect the current branch against its comparison base and synthesize the next changelog update for this repository.
When Nightshift provides a base branch, use that branch as the comparison target; otherwise determine the repo's intended release/base branch and state the assumption.
Derive the commit range from git merge-base <base> HEAD through HEAD, excluding merge commits so the output stays deterministic.
Review the existing changelog artifact and preserve prior history and structure, updating only the newest relevant section instead of rewriting older entries.
Group entries into stable Markdown sections such as Added, Changed, Fixed, Docs, Refactor, Tests, Chore, and Other, omitting empty sections when appropriate.
Base every bullet on actual commits, do not invent changes, and emit Markdown-ready changelog content only.`,
CostTier: CostLow,
RiskLevel: RiskLow,
DefaultInterval: 168 * time.Hour,
Expand Down
25 changes: 25 additions & 0 deletions internal/tasks/tasks_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package tasks

import (
"strings"
"testing"
"time"
)
Expand Down Expand Up @@ -104,6 +105,30 @@ func TestGetDefinition(t *testing.T) {
}
}

func TestTaskChangelogSynthDefinitionIncludesDeterministicRepoAwareGuidance(t *testing.T) {
def, err := GetDefinition(TaskChangelogSynth)
if err != nil {
t.Fatalf("GetDefinition(TaskChangelogSynth) returned error: %v", err)
}

expected := []string{
"comparison base",
"Nightshift provides a base branch",
"git merge-base <base> HEAD",
"excluding merge commits",
"preserve prior history and structure",
"Added, Changed, Fixed, Docs, Refactor, Tests, Chore, and Other",
"do not invent changes",
"Markdown-ready changelog content only",
}

for _, want := range expected {
if !strings.Contains(def.Description, want) {
t.Errorf("TaskChangelogSynth description missing %q\nGot:\n%s", want, def.Description)
}
}
}

func TestGetCostEstimate(t *testing.T) {
// Low cost task
min, max, err := GetCostEstimate(TaskLintFix)
Expand Down
2 changes: 1 addition & 1 deletion website/docs/task-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ Fully formed, review-ready artifacts. These tasks create branches and open pull
| `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 |
| `changelog-synth` | Changelog Synthesizer | Generate changelog from commits | Low | Low | 7d |
| `changelog-synth` | Changelog Synthesizer | Draft a deterministic changelog update from current-branch commits since `git merge-base <base> HEAD`, using Nightshift's configured base branch when available and preserving existing `CHANGELOG.md` structure | Low | Low | 7d |
| `release-notes` | Release Note Drafter | Draft release-ready notes for the next version | Low | Low | 7d |
| `adr-draft` | ADR Drafter | Draft Architecture Decision Records | Medium | Low | 7d |
| `td-review` | TD Review Session | Review open td reviews, fix obvious bugs, create tasks for bigger issues | High | Medium | 72h |
Expand Down