diff --git a/internal/orchestrator/orchestrator.go b/internal/orchestrator/orchestrator.go
index 519c380..129a8b3 100644
--- a/internal/orchestrator/orchestrator.go
+++ b/internal/orchestrator/orchestrator.go
@@ -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 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" +
@@ -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" +
@@ -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.
@@ -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.
diff --git a/internal/orchestrator/orchestrator_test.go b/internal/orchestrator/orchestrator_test.go
index 6c9ba4d..623b9fd 100644
--- a/internal/orchestrator/orchestrator_test.go
+++ b/internal/orchestrator/orchestrator_test.go
@@ -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 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()
@@ -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{
@@ -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 HEAD",
+ "comparison base",
+ "Added, Changed, Fixed, Docs, Refactor, Tests, Chore, and Other",
+ "Markdown-ready changelog content only",
}
for _, notWant := range unexpected {
diff --git a/internal/tasks/tasks.go b/internal/tasks/tasks.go
index 0959c08..0e7ef4a 100644
--- a/internal/tasks/tasks.go
+++ b/internal/tasks/tasks.go
@@ -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 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,
diff --git a/internal/tasks/tasks_test.go b/internal/tasks/tasks_test.go
index 03cdf18..da91218 100644
--- a/internal/tasks/tasks_test.go
+++ b/internal/tasks/tasks_test.go
@@ -1,6 +1,7 @@
package tasks
import (
+ "strings"
"testing"
"time"
)
@@ -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 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)
diff --git a/website/docs/task-reference.md b/website/docs/task-reference.md
index 754a067..3fc97e7 100644
--- a/website/docs/task-reference.md
+++ b/website/docs/task-reference.md
@@ -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 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 |