Select the presenter by outcome; escalate replies must not ask for repair #826
Labels
No labels
P0
P1
P2
P3
bug
category/code-change
category/cross-repo
category/eval-run
category/investigation
category/no-test-target
category/owner-ruling
category/prose-artifact
category/recurring-chore
category/structure
category/umbrella
crc-refactor
create
delete
enhancement
filed-by/agent
filed-by/user
frozen
lint-rule
needs-info
needs-triage
next
plugin/cc-architect
plugin/os
plugin/os-adr
plugin/os-aidd-lint
plugin/os-backlog
plugin/os-context
plugin/os-doc-hygiene
plugin/os-sdlc
plugin/os-vault
project/cc-os
ready-for-agent
ready-for-human
recurring
review
rule-proposal
update
waiting
wayfinder:grilling
wayfinder:map
wayfinder:map
wayfinder:research
wayfinder:task
wayfinder:task
wontfix
worklist/deviations
worklist/lint-rule
worklist/new-implement-build
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
jared/cc-os#826
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Problem
On an escalate verdict, the agent gets the wrong instruction.
Gate::Reply#message(plugins/os-sdlc/lib/os_sdlc/presenter/gate/reply.rb:52-56) tells the agent to "correct the failures above, then rerun" for every verdict that is not PASS. An escalation has no failures to correct. Two classifications escalate (Gate::ESCALATING_CLASSIFICATIONS, model/gate.rb:6):unexpected_pass: only from the test-writer node'sdiff-testgate (expect: red). The agent reads "correct the failures", but no test failed; the new test passed.agents/test-writer.md:111-120tells it to "fix the tests" until red, which invites weakening the assertion.green_report_nonzero_exit: from any expect-green test gate (programmer, refactorer, suite-check). Suite-check never reruns (agents/suite-check.md:59).The escalate verdict ends the run only if the agent stops while the verdict is still escalate. A rerun with a correct red result overwrites the verdict with pass. ADR-0158 routing does not change.
The cause is structural. Each caller hard-codes one presenter:
Gate::Run#record(command/gate/run.rb:77) always usesGate::Reply, andPipeline::NextStep(line 62) always usesImplementation::TerminalPayload. Each presenter then branches inside itself.FailureDirectivehas one fixed text for every escalation reason.User direction (2026-09-29): a presenter owns each communication that the AI or the user sees. The code triggers the reply and selects the presenter that fits the outcome.
Reproduce
n/a: design ticket. The wrong text is visible in the
Reply#messagebranch for any non-PASS verdict.Approach
Select the presenter by verdict and classification at the call site (
Gate::Run#record), so each presenter has one message and no branch on verdict. Replies per classification:unexpected_pass(test-writer):green_report_nonzero_exit: stop and report; the reply names the classification and points to the known causes inreferences/escalation-procedure.md. It does not tell the agent to correct failures or rerun.agents/test-writer.md:111-120: separateunexpected_pass(diagnose, then repair or report, as above) frombroken/test-lint(repair loop, unchanged).Terminal side:
FailureDirectivetakes the escalation reason and names the matching section ofreferences/escalation-procedure.md(the cause text stays only in that file).TerminalPayload(presenter/implementation/terminal_payload.rb:34) passes the reason;OpenPayload:19calls it with no reason and keeps today's text.Required work: none beyond the acceptance criteria.
Owner sketch
Gate::Run#record(command/gate/run.rb:77) owns the choice: it selects the reply presenter from the verdict's classification instead of always buildingReply.Reply: pass and fail (text unchanged).unexpected_pass(repair-or-report text in Approach).green_report_nonzero_exit(stop-and-report text, points to references/escalation-procedure.md).The shared payload shape (
verdict,step,classification,passed_steps,output) stays the same for all of them.FailureDirectiveowns the directive text:FailureDirective.for(reason:)returns the section-specific text for a known reason prefix and today's text fornilor an unknown prefix.TerminalPayloadpasses@implementation.failed_reason;OpenPayloadpasses no reason.agents/test-writer.mdowns the agent-side instruction forunexpected_pass.Acceptance criteria
unexpected_pass, when the agent receives the gate reply, then the reply does not say "failures" and tells the agent to repair the test or, if the behavior already exists, to stop and report.green_report_nonzero_exit, when the agent receives the gate reply, then the reply tells the agent to stop and report, and does not tell it to correct failures.green_report_nonzero_exit:, when the Orchestrator receives the terminal payload, then the directive names the matching section ofreferences/escalation-procedure.md.Origin
Gate::Replybranches only on PASS ← callers hard-code one presenter per call site ← DESIGN (presenter/gate/reply.rb, pipeline/next_step.rb:62)Approach update (2026-09-30, user ruling)
Corrections to the ticket text:
presenter/gate/reply.rb:52-56,command/gate/run.rb:77(Reply.new),pipeline/next_step.rb:62,presenter/failure_directive.rb.unexpected_passcomes only from the test-writer node'sdiff-testgate (expect: red), not from suite-check.green_report_nonzero_exitcomes from any expect-green test gate (programmer, refactorer, suite-check).Chosen reply per classification (option A):
unexpected_pass(test-writer):green_report_nonzero_exit(suite-check never reruns,agents/suite-check.md:59): stop and report; the reply names the classification and the known causes inreferences/escalation-procedure.md.Added scope:
agents/test-writer.md:111-120separatesunexpected_pass(diagnose, then repair or report) frombroken/test-lint(repair loop).Terminal side:
FailureDirectivetakes the escalation reason and names the matching section ofreferences/escalation-procedure.md; the cause text stays only in that file.OpenPayload:19calls it with no reason.Revised acceptance criteria:
unexpected_pass, when the agent receives the gate reply, then the reply does not say "failures" and tells the agent to repair the test or, if the behavior already exists, to stop and report.green_report_nonzero_exit, when the agent receives the gate reply, then the reply tells the agent to stop and report, and does not tell it to correct failures.green_report_nonzero_exit:, when the Orchestrator receives the terminal payload, then the directive names the matching section ofreferences/escalation-procedure.md.Work started: os-sdlc pipeline on branch ticket-826 (worktree .claude/worktrees/ticket-826).
Resolution
Done: Gate::Run#record selects the reply presenter by classification: UnexpectedPassReply (repair-or-report), GreenReportNonzeroExitReply (stop and report), Reply for pass/fail (unchanged). FailureDirective.for(reason:) names the matching escalation-procedure.md section; TerminalPayload passes the reason, OpenPayload passes none. agents/test-writer.md separates unexpected_pass from broken/test-lint.
Evidence: Commit
2e9b4d99on branch ticket-826 (os-sdlc implementation 3): tests run_reply_test.rb and terminal_escalation_directive_test.rb red then green; behavior-verifier passed all 4 acceptance criteria; suite-check gate passed; contract-auditor passed.Follow-ups: none
Follow-up dropped by ticket-skeptic: os-sdlc-implementation-report refusing after implementation_complete is already open as #836.