Phase Outcome ledger: a non-conforming comment is counted as a PO — silent ordinal corruption + opaque read failure #54
Labels
No labels
component:adapters
component:lifecycle
component:qa
component:setup
component:shared
component:worktrees
phase/accepted
phase/backlog
phase/deployed
phase/developing
phase/integrating
phase/planning
phase/qa
phase/requirements
phase/uat
priority:critical
priority:critical
priority:high
priority:high
priority:low
priority:low
priority:medium
priority:medium
type:bug
type:chore
type:docs
type:feature
type:infra
type:tech-debt
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
jbr870/devwork-skills#54
Loading…
Add table
Add a link
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?
Observation
A comment that merely contains
phase-outcome:v1in its first line is absorbed into an issue'sPhase Outcome ledger with no shape validation, and everything downstream then misbehaves — one of
them silently and permanently.
Reproduced for real on #50 (2026-08-22). A UAT closure note was posted with the header
<!-- phase-outcome:v1 scenario=13 -->— noid=, noskill=, no JSON fence. Consequences, inorder of severity:
phase-outcome-post.shstep 1computes
n = length(scan) + 1. With the note counted, #50 had 5 real POs and a scan length of6, so the next Phase Outcome would have been minted as
PO-50-7— skippingPO-50-6. Thestep-3 collision check cannot catch this (it only checks that the computed id is absent), and
the resulting gap then trips
phase-outcome-read-all.sh's own density check forever after, onevery subsequent read. Nothing would have pointed at the cause.
phase-outcome-read-all.sh --issue 50exited 5 with a bare jqerror —
jq: error (at <stdin>:38): null (null) only strings can be parsed— naming no commentid, no header, and no diagnosis. The parser's
hdr("id")returnsnullfor a header with noid=, andnull | split("-")is what raises it. Everything that funnels throughread_alldies with it, including
/dev:resolve's decision discovery, which is how it surfaced.the offending comment. That is fine here (it is not a Phase Outcome), but the helpers offer no
way to say so — the immutability check keys on the scan, so the very thing that made the
comment visible also made it look protected.
Recurrence
First occurrence, but mechanical: it needs no unusual input, only a comment whose first line
contains the schema token. Every typed header in the contract has the same substring-match
exposure (
scan_commentsfilters onfirst line starts with <!--ANDcontains PATTERN), so thisis a family, not a one-off —
decision-resolution:v1,work-unit-outcome:v1and the rest areequally reachable. The agent-authored-header path makes it a live risk, not a theoretical one: the
malformed header here was written by an agent (me) reaching for the nearest-looking schema name for
a note that had no schema.
Hypothesis
Three cheap, independent fixes — the first is the one that matters:
phase-outcome-post.shmust derive the ordinal from parsed ids, not fromlength. Takemax(id ordinal) + 1over comments that actually parse as Phase Outcomes, and fail loudly onany scanned comment that does not parse. A record-writer must never count something it cannot
read.
phase-outcome-read-all.shshould reject with a diagnosis, not a jq stack error — name thecomment id and the missing field ("comment 1067: header has no
id=; not a Phase Outcome").Ambiguous by construction, so it must be loud: this is exactly the shape the CLAUDE.md baseline
warns about (a gate whose failure output identifies nothing).
(
<!-- {schema} id=… skill=… -->) rather than a substring, so a non-conforming comment is nevercollected in the first place. Whether that belongs in
scan_comments(adapter tier, affects allschemas) or in each skill-facing reader is the design call.
Suggested lane
Full pipeline — helper behaviour across the adapter tier and the skill-facing tier, and it
touches how every typed comment is matched. Closely related to #34 (run-record integrity); found
while confirming #34's UAT-F33.
Filed 2026-08-22 from #50 UAT.
nonebranch unreachable #55