feat(daemon): machine-readable reason on unknown verdicts (k8s phase+reason pattern)#474
Merged
Merged
Conversation
…reason pattern) The bare 'unknown' status conflates operationally distinct situations. Every unknown verdict emitted by step() now carries a reason attribute on the status event (model.AttrStatusReason): - never_alive: boot grace expired with no life observed (both planes, L3') — infrastructure problem, retry futile until the host is fixed - session_lost: opencode lost session observability after dead checks - agent_exited: process exited with shell prompt showing — check the transcript/worktree, retry plausible The status vocabulary itself stays closed (it is the coupling core); discrimination travels as payload, the way Kubernetes keeps phase tiny and puts reason beside it. The decision sites already logged the reason in prose — this wires the same information into the event log (SSOT), StatusChangeEvent.Reason for listeners, and orch ps, which now renders unknown(never_alive) for direct triage. Fence self-extension: run-status-write-surface and no-ignored-status-append now also ban the new NewStatusEventWithReason constructor outside the sanctioned writer (fixture case added; negative-tested with a synthetic writer in internal/cli, reverted). Law doc: run-state-machine.md §5 gains a 'Status reasons' table. Tests: TestStepUnknownVerdictsCarryReason (all four arms), TestUpdateStatusPersistsReason (attr persistence + empty-reason degradation). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Implements the owner-approved design from #467 review comment: a bare
unknownis uninformative — the verdict now carries a machine-readablereasonattribute, while the status vocabulary itself stays closed (it is the coupling core; discrimination is payload, exactly as k8s keepsphasetiny and putsreasonbeside it).What
never_alivesession_lostagent_exitedrunEffect.Reasonthreads from the step() arms to the sanctioned writer;updateStatusrecords it via the newmodel.NewStatusEventWithReason(empty reason degrades to the plain event — no attrs entry)StatusChangeEvent.Reasonexposes it to listeners (Slack notifications become self-explanatory)orch psrendersunknown(never_alive)inline;orch show's event list already printsreason=…via the generic attr formatFence self-extension
run-status-write-surfaceandno-ignored-status-appendnow also banNewStatusEventWithReasonoutside the sanctioned writer — adding the constructor without extending the fence would have opened a bypass. Fixture case added; negative-tested with a synthetic writer in internal/cli (1 finding, reverted).Law doc
run-state-machine.md §5 gains a "Status reasons" table tied to L3'.
Verification
go build ./...clean;go test ./internal/...: daemon/model/cli green incl. 2 new tests (TestStepUnknownVerdictsCarryReasoncovering all four arms,TestUpdateStatusPersistsReason)make lint: 106 rules, 0 findings; semgrep fixtures pass (10 guardrails matched)🤖 Generated with Claude Code