deliverer: empty NextPhase means stay-in-phase, not task completion - #40
Conversation
… completion Enforce the Result.NextPhase contract (empty means stay in current phase): the Kafka deliverer and applyStatusFrontmatter no longer map Done+empty NextPhase to phase: done / status: completed. Thread ContinueToNext into AgentResultInfo. Healthcheck steps now request NextPhase: done explicitly so healthcheck tasks still complete.
There was a problem hiding this comment.
Now I have a thorough understanding of the code. Let me compile my findings.
Correctness review of the core fix:
-
delivery/result-deliverer.go:141-151—AgentStatusDonewithNextPhase == ""now setsfrontmatter["status"] = "in_progress"andbreaks before any phase write. The phase is preserved fromfmMapcopied at line 126-131. This is the correct fix — early break is reached before any frontmatter writes. ✓ -
delivery/content-generator.go:54-62—applyStatusFrontmattermirrors the same logic. Signature changed from(content, status)to(content, result AgentResultInfo). ThefallbackContentGenerator,passthroughContentGenerator, andsectionContentGeneratorall call it correctly. ✓ -
agent_runner.go:70-76—ContinueToNextis forwarded fromResulttoAgentResultInfo. ✓ -
agent_status.go—ContinueToNextfield added with clear documentation that it's informational only. ✓ -
Healthcheck steps — All four (claude, gemini, nop, pi) now explicitly set
NextPhase: "done". ✓ -
resolveNextPhase— Empty-string handling removed (correctly, since all callers now check empty before calling). ✓ -
Tests — The old test for
sets phase=done when done result has empty NextPhasewas correctly removed and replaced with two tests verifying in-place save behavior with and withoutContinueToNext. ✓
Minor gap found: fallbackContentGenerator and sectionContentGenerator don't have explicit tests for Done+empty NextPhase → in-place save, relying on the passthroughContentGenerator test and shared applyStatusFrontmatter code path. This is acceptable but weaker than having per-generator coverage.
{
"verdict": "approve",
"summary": "The fix correctly implements the documented Result.NextPhase contract: empty NextPhase on AgentStatusDone is an in-place save (status: in_progress, phase preserved) rather than task completion. The early-break in result-deliverer:141 is reached before any frontmatter writes. All four healthcheck steps were updated to emit explicit NextPhase: \"done\". The ContinueToNext field was added for deliverer visibility but does not alter the branching logic.",
"comments": [
{
"file": "delivery/content-generator_test.go",
"line": 45,
"severity": "nit",
"message": "fallbackContentGenerator has no explicit test for Done+empty NextPhase (in-place save). Coverage relies on passthroughContentGenerator test + shared applyStatusFrontmatter. Consider adding a direct fallback test for defense in depth."
},
{
"file": "delivery/content-generator_test.go",
"line": 45,
"severity": "nit",
"message": "sectionContentGenerator has no explicit test for Done+empty NextPhase (in-place save). Same coverage gap as fallbackContentGenerator."
},
{
"file": "docs/task-flow-and-failure-semantics.md",
"line": 111,
"severity": "minor",
"message": "The PR description mentions 'execution' as a valid phase value (e.g., NextPhase=execution → phase: execution / status: in_progress), but the docs at line 111 only list 'planning, in_progress, ai_review, human_review, done'. Consider adding 'execution' to the happy-path doc for consistency."
}
],
"concerns_addressed": [
"correctness: AgentStatusDone with empty NextPhase now produces status: in_progress (phase preserved) — early-break in result-deliverer.go:141 reached before any frontmatter writes",
"correctness: applyStatusFrontmatter signature changed to take AgentResultInfo; all three generators (fallback, passthrough, section) updated to pass full result",
"correctness: Done+non-empty NextPhase resolves phase and only marks completed when resolved phase is terminal",
"correctness: resolveNextPhase no longer handles empty string — callers check empty before calling",
"correctness: All four healthcheck steps (claude, gemini, nop, pi) now emit explicit NextPhase: done",
"correctness: ContinueToNext field added to AgentResultInfo — informational only, does not change deliverer branching",
"correctness: StepRunner.Run forwards ContinueToNext from Result to AgentResultInfo",
"tests: Old test 'sets phase=done when done result has empty NextPhase' removed and replaced with two new tests verifying in-place save (with and without ContinueToNext)"
]
}…-nextphase # Conflicts: # CHANGELOG.md
There was a problem hiding this comment.
Based on my thorough review of the changed files, here is my assessment:
Review Summary
The PR is a correct, well-tested bug fix. The core behavioral change is sound: AgentStatusDone with empty NextPhase now means "in-place save" (preserve phase, status: in_progress) rather than "task completion" (phase: done, status: completed). Both kafkaResultDeliverer and applyStatusFrontmatter (content generators) implement the same semantics. resolveNextPhase is only called for non-empty NextPhase, which is safe. All four healthcheck steps now emit explicit NextPhase: "done".
Must Fix: None
Should Fix (Important): None
Nice to Have (Optional):
-
healthcheck/healthcheck-pi-step.go:53—NextPhase: "done"is present and correct, but there is nohealthcheck-pi-step_test.go. All other healthcheck steps (claude,gemini,nop) have dedicated test files that assertresult.NextPhase == "done". The missing test leaveshealthcheck-piunverified despite being part of the same semantic contract. -
docs/task-flow-and-failure-semantics.md:80–101— The "Result Routing" table still describes the pre-PR behavior wheredoneunconditionally maps tophase = done / status = completed. It does not distinguish theDone + empty NextPhasein-place-save path fromDone + explicit NextPhase: "done"terminal path. The happy-path description on line 111 does mention the new semantics, but the routing table above it is stale.
{
"verdict": "approve",
"summary": "The bug fix is correct and well-tested. AgentStatusDone with empty NextPhase now correctly produces status: in_progress with phase preserved (in-place save) instead of phase: done / status: completed. Both Kafka deliverer and content generators implement identical semantics. All four healthcheck steps now emit explicit NextPhase: done. One minor test gap: healthcheck-pi-step lacks a dedicated test file unlike the other three healthcheck steps. Documentation in task-flow-and-failure-semantics.md has a stale Result Routing table that pre-dates the behavioral change.",
"comments": [
{
"file": "healthcheck/healthcheck-pi-step.go",
"line": 53,
"severity": "nit",
"message": "nice-to-have: NextPhase: done is correct but no corresponding _test.go file exists — claude, gemini, and nop steps each have dedicated tests asserting NextPhase == done"
},
{
"file": "docs/task-flow-and-failure-semantics.md",
"line": 80,
"severity": "nit",
"message": "nice-to-have: Result Routing table describes pre-PR behavior (done → phase: done, status: completed unconditionally) and does not reflect the new Done+empty NextPhase in-place-save semantics"
}
],
"concerns_addressed": [
"correctness: AgentStatusDone+empty NextPhase writes status: in_progress, phase preserved — confirmed in result-deliverer.go:141-151 and content-generator.go:52-62, both paths identical",
"correctness: applyStatusFrontmatter signature changed to take AgentResultInfo — confirmed all three call sites (fallbackContentGenerator, passthroughContentGenerator, sectionContentGenerator) pass result correctly",
"correctness: resolveNextPhase only called for non-empty NextPhase — confirmed at result-deliverer.go:153, early return removed; content-generator.go:64 calls it only when NextPhase != ''",
"correctness: AgentResultInfo gains ContinueToNext — confirmed in agent_status.go:46-50, StepRunner forwards at agent_runner.go:75",
"correctness: healthcheck steps emit explicit NextPhase: done — confirmed in all four files (claude-step:56, gemini-step:58, nop-step:33, pi-step:53)",
"tests: result-deliverer_test.go covers Done+empty NextPhase (with/without ContinueToNext), Done+execution, Done+done, Done+invalid — comprehensive",
"tests: content-generator_test.go covers Done+done, Done+empty, Done+execution — confirmed for fallback and passthrough generators"
]
}
Diagnosis
StepRunner.Run(agent_runner.go) publishes EVERY step result viadeliverer.DeliverResult. Preflight steps in multi-step phases return{Status: Done, ContinueToNext: true}with emptyNextPhase. The deliverer'sAgentStatusDonecase calledresolveNextPhase, which mapped emptyNextPhaseto"done"— writingphase: done, status: completedon a live task. This contradicts the documentedResult.NextPhasecontract (agent_step.go): "Empty means stay in current phase — used for in-place saves between steps in a multi-step phase."Real-world impact: github-update-go-agent's planning preflight republish marked a live task
done/completedmid-run for ~13 min (self-healed only because a later result overwrote it). Every multi-step agent with Done+ContinueToNext preflights is affected.Fix
delivery/result-deliverer.go:AgentStatusDonewith emptyNextPhaseis now an in-place save exactly like theAgentStatusInProgressbranch —status: in_progress, phase preserved from incoming frontmatter, noresolveNextPhasecall. Terminating a task now requires an explicitNextPhase: "done".resolveNextPhasekeeps its normalize+warn path for non-empty values (invalid still falls back to"done").delivery/content-generator.go(applyStatusFrontmatter): required companion fix — the generators previously wrotephase: done, status: completedunconditionally on Done, so the Kafka deliverer's "preserve phase" would have preserved the generator-clobbereddone. Now: Done+empty NextPhase →status: in_progress, phase untouched; Done+non-empty → phase = normalized NextPhase,status: completedonly when resolved phase isdone(mirrors the Kafka deliverer; also fixes the file-deliverer path).agent_status.go/agent_runner.go:AgentResultInfogainsContinueToNext, forwarded byStepRunner(informational — Done+empty NextPhase is an in-place save regardless).docs/task-flow-and-failure-semantics.md: happy-path scenario updated to require explicitNextPhase: "done".Consumer sweep (this repo)
Steps that returned
Status: Donewith emptyNextPhaseand RELIED on the empty→done fallback to complete tasks — each given an explicitNextPhase: "done":healthcheck/healthcheck-claude-step.go(claudeStep.Run)healthcheck/healthcheck-gemini-step.go(geminiStep.Run)healthcheck/healthcheck-nop-step.go(nopStep.Run)healthcheck/healthcheck-pi-step.go(piStep.Run)Inspected, no change needed (config/LLM-driven
NextPhase, empty already documented as in-place save):claude/agent-step.go—AgentStepConfig.NextPhase("Empty means in-place save")pi/pi-step.go—StepConfig.NextPhaseagent_parser.goParseStep— constructornextPhaseparamclaude/task-runner.go+claude/result-deliverer.goadapter — forwards LLM-emittednext_phaseSemantic change for downstream consumers (minor bump): any out-of-repo step or single-shot LLM agent emitting
status: doneWITHOUTnext_phasepreviously completed its task; it now performs an in-place save (status: in_progress, phase unchanged). Terminal steps must emit/configurenext_phase: "done"explicitly.Tests
status: in_progress+ phase preserved; Done+"done"→ completed; Done+"execution"→phase: execution/status: in_progress; InProgress branch unchanged."done"→ completed/done; Done+"execution"→ execution/in_progress.ContinueToNextto the deliverer.NextPhase: "done".make precommitgreen (tests, lint, security, formatting).Follow-up
Controller-side staleness guard as defense-in-depth (reject/flag terminal writes that regress a newer task state) — separate issue.