Phase Outcomes: no supersede path when a posted outcome's premise turns out false #45
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#45
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?
PREQ: Phase Outcome supersede — a typed correction path for a false terminal record
Created: 2026-08-26
Problem
A phase lead posts a terminal Phase Outcome, and then learns that something it states is false.
There is no way to say so.
phase-outcome:v1is immutable by design (forge-contract §8) — editing one"silently corrupts the audit chain" — so the only act available is an untyped prose comment that no
reader and no helper connects to the record it corrects.
Every other durable record in the suite already has a correction path. This is the only one that does not:
decision-resolution:v1withdrawn/superseded-bystatusessreq:v1,test-plan:v1,qa-report:v1phase-outcome:v1findings[]carry no annulmentA re-run already posts a new outcome at the next ordinal, and the phase lookup already returns the
highest one — so "what phase am I in" self-corrects on a re-run. What has no path is the record's
content: a false finding stays live, and a Pending Decision raised on a false premise stays open.
Users:
that its own terminal record states something untrue — and today can only leave it standing.
has no deliberate, recorded act available.
decisions-settled-check.mdand the phase lookup — whichtoday cannot tell a live premise from an annulled one.
account of what happened. This project declares
observability: mode: nonewith the reason "runtraceability is the forge record (Phase Outcomes, deliverables), not logs" — so a knowingly-false
Phase Outcome is a hole in the only audit surface this repo has.
Current state — the recorded incident (feature-230,
/dev:technical-plan):The SREQ recovered on its own — deliverables are latest-wins. The Phase Outcome did not, and the
"correction comment" that was filed is not a record: nothing resolves it, no helper links it to the
outcome it corrects, and a reader who finds the outcome does not find the correction.
A second instance, observed while gathering this PREQ (2026-08-26). The Tier 3 panel that reviewed
this very requirement spawned five lenses; two of them went idle without delivering their findings.
Read as "the panel returned nothing", that is the feature-230 shape exactly — and would have written
the same false claim into this PREQ's own Phase Outcome. A single ping to each recovered both, and
both returned substantive concerns that changed this document. The pattern is not a one-off.
Why prevention alone is not enough, and why the stated prevention would not have worked. The lead
in the feature-230 incident did account for its seven agents — two control probes, sentinel-file wait
loops, both models — and concluded they were unrecoverable. A rule requiring each spawned agent to be
"retired, reported, or declared unrecoverable" is satisfied by exactly that reasoning, and the panel
results still arrive 34 seconds later. Prevention only bites when unrecoverable is an act, not a
judgement; and even then, the cases a 34-second window cannot catch still need a way back.
Proposed Solution
Two halves of one failure — recovery and prevention.
1. A supersede path (recovery). A typed, attributed record that annuls part or all of a posted
Phase Outcome. It names its target — one or more findings, or the whole outcome — and a reason. It is
always forward: the annulled record is never edited or deleted, and stays readable as history. Both
actors may post one: a skill (so an unattended run can correct itself rather than hand off on a
record it knows is false), and the operator, at any later time from any session, through a command
in the shape of
/dev:resolve.Annulment is honoured in one place: the suite's Phase Outcome read helper. The contract already
forbids a skill from re-implementing scan-and-resolve (§9 — "Every skill consumes it via
decision_resolution.read_state. No skill re-implements scan-and-resolve."). Putting annulment behindread_all/read_latestbinds every compliant consumer by construction, including ones writtenbefore this feature existed, rather than by an enumerated list that goes stale.
What the readers then do:
that already exists for
next_state: skipped, not a new concept. With no survivor it reportsbacklog, which is already the suite's answer for an issue with no Phase Outcome at all.question raised on a premise that no longer holds has nothing to answer.
A supersede is itself correctable, by the rule the suite already uses. The newest supersede naming
a given target is the one that counts — latest-wins per target, exactly as
decision-resolution:v1behaves under §9. A supersede posted in error is corrected by posting a corrected one, and a duplicate
or concurrent supersede resolves without a special case. This deliberately avoids recreating, one level
up, the very gap this feature closes.
2. A pre-post roll-call (prevention). Every terminal Phase Outcome carries a roll-call — one
entry per agent the phase spawned, each disposed
reportedorterminated. A phase that spawnednothing declares an explicitly empty roll-call; absence is refused and never read as "spawned
none", so omission stops being an escape route. The posting helper refuses a missing or malformed
roll-call.
The teeth are in what is not an accepted disposition: there is no
unrecoverable. You terminate theagent — so a late report cannot arrive — or you wait. "I judged the transport broken" stops being
something the record can say.
Enforcement is deliberately on the record's shape, not the harness. A helper that enumerated live
agents would need harness-specific knowledge in the shipped tier, which the stack-agnostic rule
forbids. The shape check cannot prove a roll-call complete — that limit is accepted knowingly — but
it converts a silent judgement into a written, attributable claim, which is the difference that matters.
Scope: Standard — both halves, because they are prevention and recovery for one failure, and
because half of each was already shown to be insufficient alone.
User Stories
that an unattended run does not hand off on a statement I already know is wrong.
from a different session, so that correcting the record is a recorded act and not an untyped comment.
read it, so that I can trust the record without cross-checking every prose comment on the issue.
so that a run neither blocks on, nor proceeds from, a premise that no longer holds.
so that I cannot record "X could not be run" while X is still running.
Acceptance Criteria
Reading an annulled record
posted, then reading the issue through the suite's Phase Outcome read helper reports that finding
as annulled, carrying its reason and the identity of the record that annulled it.
then the annulled finding is not in the set the consumer acts on — and the original Phase Outcome
comment is still present, unedited, and still readable as history.
when it runs against an issue carrying supersede records, then it reads without error and, with no
change to its own code, no longer acts on annulled data.
Effect on pipeline state
to the newest surviving outcome's
next_state; the annulled outcome is walked past, exactlyas
next_state: skippedalready is.looked up, then it reports
backlog— the same result the suite already gives an issue that hasno Phase Outcome at all.
including when every finding in the outcome has been annulled individually. Moving the phase
requires annulling the outcome.
gate runs, then those decisions no longer block.
Posting and correcting a supersede
the operator), exactly what it annuls, and a non-empty reason.
different session naming the target and a reason, then the record is posted and the target reads
as annulled.
the later one is what counts — the same latest-wins rule the suite already applies to decision
resolutions.
the named outcome — when it is posted, then it is refused, naming the bad target.
naming what is missing.
Roll-call
agent the phase spawned, each disposed
reportedorterminated, or an explicitly emptyroll-call when the phase spawned none.
refused — absence is never read as "spawned none".
reportedorterminated— includingunrecoverable— when the post is attempted, then it is refused.Out of Scope
annulled outcome from blocking. A decision mooted by anything else — a superseding QA round, a
declaration change — stays #36's problem.
sreq:v1,qa-report:v1,test-plan:v1). Already latest-wins, and therecorded incident's SREQ recovered on its own.
forward correction path and never a mutation.
tier, breaching the stack-agnostic rule. The gate checks the record's shape instead, and the resulting
limit — a roll-call cannot be proved complete — is accepted knowingly.
rewritten. An old outcome can still be annulled going forward, because a supersede is a new record.
Dependencies
forge-contract.md§8 (immutability) and §9 (the lookup rule) — this feature extends both. §7'sreader test governs whether it can stay additive within
phase-outcome:v1or forces a:v2bump;a bump is expensive (cross-version reads are not supported) and should be avoided if semantics allow.
_shared/procedures/phase-outcome.md—post,read_all,read_latestare where annulment isproduced and honoured, including the existing
skippedwalk-back this reuses. Centralising it here iswhat makes the compatibility criterion above true.
_shared/procedures/decisions-settled-check.md— the gate that must stop counting an annulledoutcome's decisions.
/dev:resolve— the precedent for the operator-facing command shape (structured mode, explicittarget ID, rationale) and the existing consumer of a finding via
finding_ref._shared/procedures/wait-discipline.md§1a/§1b — the roll-call is the post-time counterpart toits spawn-time rules; the two must read as one discipline, not two competing ones.
local-fs,tea-cli,glab-cliandgh-clithrough the existingcontract operations. No new adapter primitive should be needed.
Timeline
Notes
Constraints: the portability baseline (POSIX glue; helper tier bash ≥ 3.2,
jq,git); no newadapter primitive; avoid a
phase-outcome:v1→:v2bump if the semantics allow, per §7's reader test.Settled during the panel round (2026-08-26):
terminatedis exactly what the supersedepath is for — the recovery case working as designed, not a separate mechanism.
outcomes must carry a roll-call.
observable, and attribution already records who posted the record. It remains intent, not a rule.
Deferred to
/dev:technical-plan(panel detail, deliberately not settled in a PREQ): the orderingkey for "newest surviving outcome"; whether "spawned agents" counts transitive children; the precise
definitions of
reportedandterminatedand whether they are exclusive; the required fields of aroll-call entry and of a supersede record (which fix what "malformed" means); the attribution value
used for the operator; whether one supersede may target findings across multiple outcomes; and the
enumeration of terminal
next_statevalues.Key decisions (2026-08-26):
(leaves the cause in place), roll-call as text only (§1a is already such a rule, and the incident
happened with it in place).
for a wrong
next_state), outcome-only (annuls true findings alongside the false one).months-later discovery is still correctable.
skippedcarve-out; withno survivor it reports
backlog. Rejected: forcing a replacement outcome (heavy for a one-linecorrection), leaving the label untouched (record and label then disagree).
that is the 22-hour failure #36 was filed for), and refusing to annul while decisions are open
(deadlocks when the blocking decision is the false thing).
enforcement.
unrecoverableis deliberately not an accepted disposition; that omission is the teeth.found that an enumerated list makes the compatibility promise and the exclusion promise contradict.
(two lenses noted this recreates the original gap one level up, on the same "no recorded instance"
reasoning that left it open before), and one-supersede-per-target (makes an error permanent).
absence-means-none (the evasion is one omission wide).
Test Plan: phase-outcome-supersede (issue #45)
Validation cases derived from the PREQ alone, before any design exists. They name only what the PREQ
names and assert outcomes a person can observe. Lanes are deliberately absent — an absent lane means
e2e-browserby default, and/dev:technical-plan2.6b assigns the real lanes once the design is known.Prerequisites
The state the scenarios need. How each state is produced is a design decision; the PREQ requires only
that each be reachable.
distinguishable from "the only one"
the others stand
used unmodified, to check the compatibility promise
Required Test Data
Test Scenarios
Scenario 1: A false finding reads as annulled
Acceptance criterion: "Given a posted Phase Outcome containing a false finding, when a supersede naming that finding is posted, then reading the issue through the suite's Phase Outcome read helper reports that finding as annulled, carrying its reason and the identity of the record that annulled it."
Expected outcome: the false statement is marked wrong at the place a reader encounters it, and the reader can follow it to the correction.
Scenario 2: An annulled finding drops out of what consumers act on, but not out of history
Acceptance criterion: "Given an annulled finding, when any consumer obtains the issue's findings through the read helper, then the annulled finding is not in the set the consumer acts on — and the original Phase Outcome comment is still present, unedited, and still readable as history."
Expected outcome: consumers stop acting on the false finding while the record of it having been said survives intact.
Scenario 3: An unmodified pre-existing consumer honours annulment for free
Acceptance criterion: "Given a consumer written before this feature that reads Phase Outcomes through the read helper, when it runs against an issue carrying supersede records, then it reads without error and, with no change to its own code, no longer acts on annulled data."
Expected outcome: honouring annulment is inherited by everything that reads through the shared path, rather than being a promise each consumer must separately keep.
Scenario 4: The phase lookup walks past an annulled outcome
Acceptance criterion: "Given an outcome-level supersede, when the current pipeline phase is looked up, then it resolves to the newest surviving outcome's
next_state; the annulled outcome is walked past, exactly asnext_state: skippedalready is."Expected outcome: the pipeline's idea of where the feature is stops being driven by a record that has been withdrawn.
Scenario 5: Every outcome annulled falls back to the pre-pipeline state
Acceptance criterion: "Given an issue whose every Phase Outcome has been annulled, when the current pipeline phase is looked up, then it reports
backlog— the same result the suite already gives an issue that has no Phase Outcome at all."backlogExpected outcome: annulling everything leaves the issue at the same starting point as an issue that never began, with no undefined or error state in between.
Scenario 6: Annulling every finding does not move the phase
Acceptance criterion: "Given a finding-level supersede, when the pipeline phase is looked up, then it is unchanged — including when every finding in the outcome has been annulled individually. Moving the phase requires annulling the outcome."
Expected outcome: annulling content and withdrawing a record are distinct acts with distinct effects, and doing the first repeatedly never silently becomes the second.
Scenario 7: Decisions on an annulled outcome stop blocking
Acceptance criterion: "Given an annulled outcome that carries open blocking Pending Decisions, when the decisions-settled gate runs, then those decisions no longer block."
Expected outcome: a question raised on a premise that no longer holds stops holding the run up, without the question being erased from the record.
Scenario 8: A supersede says who, what and why
Acceptance criterion: "Given any supersede record, when it is read, then it names who posted it (the producing skill, or the operator), exactly what it annuls, and a non-empty reason."
Expected outcome: a reader who finds a correction can tell who made it and why, without asking anyone.
Scenario 9: The operator corrects an old outcome from a different session
Acceptance criterion: "Given a false outcome from an earlier run, when the operator invokes the supersede command from a different session naming the target and a reason, then the record is posted and the target reads as annulled."
Expected outcome: correcting the record does not require being the session that wrote it, or being anywhere near the original run.
Scenario 10: A supersede posted in error is corrected by a later one
Acceptance criterion: "Given a supersede posted in error, when a later supersede naming the same target is posted, then the later one is what counts — the same latest-wins rule the suite already applies to decision resolutions."
Expected outcome: the correction mechanism can itself be corrected, so a slip does not become permanent.
Scenario 11: A supersede with no such target is refused
Acceptance criterion: "Given a supersede whose target does not exist — an unknown outcome, or a finding not present in the named outcome — when it is posted, then it is refused, naming the bad target."
Expected outcome: a supersede cannot point at nothing, and a mistyped target fails loudly instead of annulling something unintended or silently doing nothing.
Scenario 12: A supersede missing its reason or attribution is refused
Acceptance criterion: "Given a supersede missing its reason or its attribution, when it is posted, then it is refused, naming what is missing."
Expected outcome: an unattributed or unexplained annulment cannot enter the record at all.
Scenario 13: A terminal outcome carries a roll-call
Acceptance criterion: "Given any terminal Phase Outcome, when it is posted, then it carries a roll-call: one entry per agent the phase spawned, each disposed
reportedorterminated, or an explicitly empty roll-call when the phase spawned none."Expected outcome: every terminal record states what happened to everything the phase started, including stating that it started nothing.
Scenario 14: A terminal outcome with no roll-call is refused
Acceptance criterion: "Given a terminal Phase Outcome with no roll-call at all, when the post is attempted, then it is refused — absence is never read as 'spawned none'."
Expected outcome: omitting the roll-call stops being a way to say nothing was spawned, so silence is never mistaken for an answer.
Scenario 15: A roll-call cannot say "unrecoverable"
Acceptance criterion: "Given a roll-call entry disposed as anything other than
reportedorterminated— includingunrecoverable— when the post is attempted, then it is refused."unrecoverableExpected outcome: judging an agent dead is no longer something the record can express — the lead must end it or wait for it, which is what stops a late report arriving after the record claims it never will.
Scenario 16: Edge cases
annulled. Verify: the result is either a clear refusal or an inert, recorded no-op — not a silent
success that changes nothing, and not an error a reader cannot interpret.
terminateddeliver a report anyway,after the outcome is posted. Verify: the supersede path is available and sufficient to correct the
outcome — no separate mechanism is needed and none is offered.
sessions. Verify: one of them governs, deterministically, and reading the issue afterwards gives the
same answer every time.
annul a finding. Verify: the read path handles the first-ever supersede the same as a later one.
refused, naming the missing target rather than failing obscurely.
Verify: nothing about the output changed for issues that were never corrected.
Expected outcome: the correction path behaves predictably at its boundaries — first use, concurrent use, repeated use, and no use.
Notes
Those are design decisions;
/dev:technical-plansettles them and annotates the lanes here.not the compatibility promise.
well-formed and attributable, not that it is true — the PREQ accepts that limit explicitly, because
the alternative needs harness knowledge the suite is not allowed to have.
Software Requirements: 45-phase-outcome-supersede
Context
phase-outcome:v1is the suite's terminal handoff record and is immutable by contract (§8) — soit is the only durable record with no correction path. A phase lead that discovers, after posting,
that its own record states something false can do nothing about it; a false finding stays live and a
Pending Decision raised on a false premise stays open. The recorded incident: a five-lens review
panel returned nothing, the lead concluded the transport was broken and wrote "panel could not be
run" into its Phase Outcome, and the panel's results arrived 34 seconds later carrying two
blocking concerns. This slice adds a supersede path (recovery) and a roll-call (prevention),
which the PREQ ships together because each was shown insufficient alone.
Approaches Considered
Approach A: A new sibling record kind, honoured inside the read helper
Summary: annulment is a new append-only comment kind,
phase-outcome-supersede:v1, and the oneplace that honours it is
phase_outcome.read_all— so every compliant consumer inherits thebehaviour unmodified.
Pros:
phase-outcome:v1's wire format is untouched, so no:v2bump and no history goesinvisible; the annulled comment is never edited, so §8 stands unweakened; the compatibility promise
is true by construction rather than by an enumerated consumer list; latest-wins-per-target reuses
§9's rule verbatim, and the append-only
active/withdrawnshape reusesfolded-finding:v1'sproven pattern; no new adapter primitive.
Cons:
read_allgains a second comment scan per call; the record set a consumer sees is nolonger literally the comment set on the issue, so history needs an explicit opt-in flag.
Effort: Medium
Approach B: Annulment carried inside a new Phase Outcome
Summary: add a
supersedes[]body field tophase-outcome:v1; correcting a record means mintinga new outcome that names what it annuls.
Pros: no new record kind; the corrector is itself a first-class outcome, so it carries its own
attribution, roll-call and ordinal, and the ledger remains the whole story in one chain.
Cons: ruinously heavy for the actual use case. An operator annulling one false finding months
later must mint a whole Phase Outcome — which forces them to choose a
next_state(moving pipelinestate as a side effect of a correction), supply a roll-call for a run that never happened, and pick
a producer name from a closed enum that has no
operatorin it. The PREQ rejected forcing areplacement outcome for exactly this weight (decision 4).
Effort: Medium
Approach C: Reuse
decision-resolution:v1Summary: express annulment as an "annulment decision" resolved through
/dev:resolve.Pros: zero new schema; reuses the §9 lookup rule and the whole existing operator surface.
Cons: a decision is a question; an annulment is an assertion. It needs a Pending Decision
to exist first, which the false outcome never raised — so a self-correcting skill would be answering
its own invented question. And decision IDs are derived from Phase Outcome ordinals, so an operator
arriving weeks later has no ID to name.
Effort: Low
Decision
Selected: Approach A — a new sibling record kind, honoured inside the read helper.
Rationale: it is the only shape that keeps both promises the PREQ treats as non-negotiable — the
annulled record is never touched (§8 stands) and the exclusion is inherited by consumers written
before the feature existed (honoured in one place, not an enumerated list). B and C both fail the
second promise or pay for the first with a correction act heavier than the thing it corrects.
Architecture
Component Overview
Data Flow
Annulling. A skill (mid-run, on discovering its own record is false) or the operator (later, from
any session) calls the supersede helper with a target, a reason and an attribution. The helper reads
the unfiltered ledger, refuses an unresolvable target, and posts one append-only comment.
Reading.
read_allscans the Phase Outcome records exactly as today, runs the §8 immutabilitycheck, parses, applies the foreign-issue gate, and enforces ordinal density on the raw ledger.
Only then does it scan the supersede kind, resolve latest-wins per target through
_po_annulment,and project the filtered view.
read_latestwrapsread_all, so the current-phase lookup inheritsannulment with no code of its own.
Preventing.
phase_outcome.postrequires aroll_callarray. It validates the array's shapeand refuses absence — it never inspects live agents.
The supersede record
scope(outcome vs finding) is derived fromref's form (PO-…vsF-PO-…) and deliberatelynot stored — a third copy of the same fact is a third thing that can disagree. The body echoes
target/byfor a human reading the comment, exactly asfolded-finding:v1echoes its headertokens; the post helper refuses any header/body mismatch, and readers treat the header as
authoritative for identity.
Matching
refis exact-token, never substring.F-PO-45-3-2is a substring-prefix ofF-PO-45-3-21, and this suite has already shipped that bug once (ref=D-PO-2-4-1matchedref=D-PO-2-4-12, observed live). The reader parses the header'skey=valueattributes with theanchored-header regex and compares with jq string equality — it never greps the raw body.
Read-side gating of supersede records — fail closed
The supersede kind removes data from what consumers act on, so a record of it that cannot be
trusted must never be honoured silently, and must never be dropped silently either. Four disjoint
outcomes in
_po_annulment, mirroring the family's existing partition:refnames a target on this issuestatusEXIT_ENV), naming the comment id and the offending element — the_po_parsetreatment. Silently ignoring it could resurrect a finding that was genuinely annulled.refclaims a target on a different issue_po_foreign_gate. A session must not annul another issue's records.edited_ms > created_ms)EXIT_IMMUTABLE). Detected by the same signal every adapter already renders (local-fs pins mtime toepoch_ms; remote adapters rendercreated_at/updated_at) — no new mechanism. One edited record refuses the whole issue read, §8 parity. That an edit can therefore take an issue's reads offline is tamper-evidence working as intended, not a defect.External Data Contracts
git.wihslon.com/jbr870/devwork-skills) via thetea-cliadapter — the comment ledger this feature reads and writesscan_comments(issue, header)read;post_comment(issue, body)write[{comment_id: string, created_ms: number, edited_ms: number, raw_body: string}]; line 1 of a record round-trips byte-identical, read back as<!-- phase-outcome:v1 id=PO-45-1 skill=requirements -->recorded→external-contracts/forge-comment-ledger.provenance.jsonThe capture confirms the two facts the design rests on: an HTML-comment header with space-delimited
key=valueattributes survives the round trip (so a new sibling kind needs no new adapterprimitive), and the scan envelope already carries
created_ms+edited_ms— the ordering key forlatest-wins-per-target and the §8 edit signal are both present today. No boundary is
unverified, sothis phase raises no precondition.
Observability & Audit
Not applicable as a policy obligation: the project declares
observability: mode: none, so theapplicability test does not intersect and no logging/audit AC rows are owed. Recorded here only
because the reason for that
noneis load-bearing for this feature — "run traceability is the forgerecord (Phase Outcomes, deliverables), not logs". The record trail is this project's only audit
surface, which is why §8's tamper-evidence is preserved rather than relaxed, and why the supersede
record itself carries mandatory attribution and reason.
The honest limit on attribution.
byis an unverified self-report, exactly as the roll-callis. Nothing in the forge binds it to the comment's actual author, and the suite deliberately adds no
adapter primitive to fetch one. It is a claim, recorded and permanent — which is the same standing
the suite already gives a
/dev:resolveresolution posted by an autonomous run. State this in theschema beside the roll-call's limit; do not let a reader mistake
by=operatorfor proof of a human.Key Decisions
Each of these settles something the PREQ deliberately deferred to this phase, or something the
review panel raised.
phase-outcome-supersede:v1(see The supersede record)phase-outcome:v1's wire format untouched, so §7's reader test is trivially passed and no bump is owed.refis a PO id (PO-45-3) or a finding id (F-PO-45-3-2)refidentifies both cases and latest-wins keys on one value. N targets = N records. (Settles "may one supersede target findings across multiple outcomes" — no.)ref/byare authoritative; the body echoes them; post refuses a mismatch;scopeis derived, never storedfolded-finding:v1's header/body echo.refmatchingF-PO-45-3-2prefixesF-PO-45-3-21; this exact bug class already shipped once.read_allalready sorts by it and enforces density; ordinals are minted monotonically and are the ledger's identity. Timestamps are the supersede key, not the outcome key.(created_ms, comment_id)status: withdrawn, append-only — never an edit or a delete.withdrawncarries the same mandatoryreason+byand the same refusal set asactivefolded-finding:v1pattern. Un-annulling restores a record to force, so it deserves equal attribution rigour.PO-45-3does not moot awithdrawnonF-PO-45-3-2; each target resolves on its own, and an annulled outcome leaves the actionable set whatever its findings sayby=operator; every other value must be a canonical producer nameoperatoris disjoint from the closed producer enum (requirements,technical-plan,develop,rebase,qa,integrate,promote,vibecoding,resolve,setup), so skill-vs-operator is decidable from the value alone. Its truthfulness is a claim, not a proof — see Observability & Audit.phase-outcome-read-all.sh, via one_po_annulmentinbin/_lib.shfindings[]into an additiveannulled_findings[]; plus an additiveannulled_counton every surviving outcome's sibling projectionread_latest'slastbecomes the newest survivor; an empty array →null→backlog).annulled_countlets a consumer know filtered history exists without opting into it — the visibility hook that keeps an annulment from being invisible at a human touchpoint.--include-annulledreturns the whole ledger withsuperseded: {…}|nullon each outcome and finding[.[].ordinal] == [range(1;length+1)]dense check. Consumers must not re-assert density on read output, nor computelen+1from it.pending_decisions[]— the gate keeps counting them./dev:resolvepresents such a decision but restricts it toacceptnext_statestill stand — pipeline state advancing on annulled evidence, which AC-6 forbids. The real hazard (resolvingdefer-to-issuespawning a sibling issue from a finding annulled as false) is closed at the acting site instead of by hiding the question.postseesreported|terminated, exactly one per entry, exactly one entry per spawned agent,agentnames unique within a roll-call. Nounrecoverable; any other value refusedreported= the lead read the result, or reconstructed the outcome from the tree perwait-discipline.md§1a step 3.terminated= the lead ended the agent before posting, so no late report can arrive. A terminated agent whose report was already read isreported. The omission is the teeth.--roll-callomitted, or given an empty string, is refused;[]is a valid "spawned none". The flag still accepts@fileor an inline literal via_json_arg_json_argentirely would silently drop@filesupport and break symmetry with--produced/--findings. Sentinel for absence,_json_argfor the value.roll_call: null, exactly assuitedoes. Enforcement lives only at the post seam, so a hand-posted record bypasses it — which is why readers must tolerate absence.phase-outcome:v1next_statevalues": the qualifier is the record kind, not a state list — and it leaves no escape hatch.suite), sorted byagentbefore comparing. A landed pre-feature record withroll_call: nullnever matches an incoming[]suiteis excluded because it describes the writer; a roll-call describes the run's content. Sorting matters: array-order jitter between two attempts would otherwise defeat the guard and mint a duplicate outcome — the failure the guard exists to prevent.EXIT_USAGE(2) for missing/empty--roll-call,--reason,--by, a bad disposition or a malformed entry;EXIT_FINDING(6) for an unresolvable target;EXIT_ENV(7) for a foreign or unparseable supersede at read time;EXIT_IMMUTABLE(4) for an edited onealready_annulled: truewithdrawn, and recording a better reason, are both this path. Posting the record is what makes it an inert, recorded no-op rather than a silent one./dev:resolve(--supersede <target> --reason "…", plus an interactive menu entry)/dev:resolve". One orchestrator surface, and it avoids adding a shipped skill (which also carries theSHIPPED_SKILLSlint step). Rejected: a new/dev:supersedeskill. The helper is the real API — a skill correcting itself mid-run calls it directly.Technical Risks
--roll-callmandatory breaks every existing producer call site at oncetest/keeps the flag-day to one edit per suite.$PO_ORDINAL = len(prior_outcomes) + 1silently drifts once outcomes can be filtered outpostmintsmax+1); annulment makes it wrong more often. Fixed in this slice and covered by a mechanical scan for otherlen(...)+1-on-read-output call sites —developis one of N, not necessarily the only one.read_all's array is still the literal comment set, or re-asserts density on it--include-annulledis the documented history door;annulled_countsignals filtered history in default mode; the schema, procedure doc and contract all state the default is the actionable set, not the ledger, and that it is ordinal-sparse.read_all, amplified repo-wide by the candidate sweeps —promotion-candidates.sh/fold-candidates.shgo from one to two forge calls per open issue, including issues that can never carry a supersedephase-outcome-supersede:v1contains the substringphase-outcome, so one scan could feed both filters locally. Not taken now — it entangles the anchored-header / near-miss discrimination (#54) for a cost nobody is paying. If the sweeps ever feel slow, fix it there (one comment fetch per issue, reused for both kinds), not inread_all.read_allare not atomicExpert Review
Panel: Tier 3, five lenses, on
fable. Reviewer reports carried 12 blocking concerns; 11 areactioned below and 1 is surfaced as a finding for the orchestrator.
Reviewers
ref-substring hazard would recur; three copies of the target have no conflict rule; the filtered array is ordinal-sparse and consumers must be told._json_argfor--roll-callsilently drops@file; roll-call array-order jitter defeats the retry guard.byis a self-declared claim and the plan said so for the roll-call but not for attribution; a supersede can dissolve a blocking approval gate; the foreign-issue gate was unstated for the new kind.Changes Made
pending_decisions[]and the gate keeps counting them;/dev:resolverestricts such a decision toacceptso no sibling issue is spawned from a finding annulled as false.refmatching in jq against parsed header attributes, never a raw-body substring, naming the shipped prefix-collision bug it prevents.scopefrom the record as derived, and made a header/body mismatch a post-time refusal._json_arg(and therefore@file) for--roll-call's value while adding a was-the-flag-seen sentinel for the absence refusal; added per-entry validation including uniqueagentnames.roll_callbyagentin the retry-guard normalization, and definednullvs[]as an explicit non-match.byis an unverified self-report, with the same candour the roll-call's limit already had, and addedannulled_countto default read output so an annulment cannot be invisible at a human touchpoint.reason+byrigour onwithdrawnas onactive.$PO_ORDINALfix to a mechanical scan for otherlen+1-on-read-output sites.created_mstiebreak as an explicit case intest/supersede-flow.sh(AC-10), and a shared fixture helper for the roll-call flag-day (Technical Risks).--include-annulledoutput-shape note to the schema deliverable so no consumer mixes modes.Noted (not actioned)
scan_commentsfor the commonphase-outcomeprefix, partitioned twice locally). Raised by two lenses, so it is promoted into the plan — but as a documented, deliberately-declined lever in Technical Risks rather than as work in this slice: the performance lens found no blocking cost at this load, and the change entangles the anchored-header / near-miss discrimination that #54 hardened. Accepted as a cost chosen with the numbers in hand.approvaldecision. Real and worth deciding, but AC-7 does not qualify by attribution, and narrowing it here would be the design overruling the requirement. Note also that it grants no new capability: an autonomous run can already self-answer a gate through/dev:resolve's structured mode. Surfaced as a finding with adefer-to-issuerecommendation so a human decides it on its own merits.Acceptance Criteria
test/supersede-flow.sh— post PO, supersede one finding, assertannulled_findings[0]carriesreason,byand the annullingcomment_idfindings[]excludes it and still contains the sibling; assert the PO comment'sraw_bodyis byte-identical andedited_ms == created_mspromotion-candidates.shandfold-candidates.shunmodified against an issue carrying supersedes; assert exit 0 and that the annulled outcome's state is not what they report. Mechanical: a grep-scan asserting no consumer outsideread-all.sh/_lib.shreferences the supersede kind — the "honoured in one place" constraint, enforced the way the helper-funnel rule requiresskippedalready isread_latestreturns the middle one'snext_statebacklog— the same answer as an issue with no outcomeread_latest→null; assert issue C (no POs) →null; assert the two agreeread_all'spending_decisions[]after — the exact setdecisions-settled-check.mditerates — and still present under--include-annulledby,target,reasonon a skill-posted and an operator-posted record; assertby=operatoris distinguishable from every producer name; assert a header/body mismatch is refused/dev:resolve --supersedeon this repo's own tracker from a fresh session (dogfooding). Helper-level half also covered bysupersede-flow.sh(the helper carries no session state)withdrawnfor the same target, assert restored; assert the first record is still readable; named case: two records on one target sharingcreated_msresolve deterministically bycomment_id; named case:F-PO-x-y-2is unaffected by a supersede targetingF-PO-x-y-21by, and an invalidby→ four refusals at exit 2, each naming the missing or invalid element; the same four forstatus: withdrawnreported/terminated, or an explicitly empty onereported; with onereported+ oneterminated; with[]— assert each round-trips throughread_all; assert@fileand inline forms both work--roll-call→ exit 2 naming the missing element, no comment posted;--roll-call ''→ same; then post the same payload with[]→ accepted. Read side: a pre-feature record with noroll_callreads asnullwithout error — enforcement is post-onlyreported/terminated—unrecoverableincluded — is refusedunrecoverable→ refused; an invented value → refused; a missing/emptyagent→ refused; two entries with the sameagent→ refused; thenterminated→ acceptedread_all's filtered output;develop's$PO_ORDINALis highest-ordinal + 1len(/length-plus-one arithmetic on Phase-Outcome read output, asserting zero hits; integration: a ledger with an annulled outcome yields the same ordinal from the skill's stated formula aspostactually mintsroll_callentries reordered → assertsretried: trueand no new ordinal; a landedroll_call: nullrecord vs an incoming[]→ not a match, mints a new ordinalCoverage note.
e2e,a11yandsecurity-browserare declarednot_applicablefor thisproject, so no browser lane exists to route to. Every automated row above is carried by the shell
test harness under
plugin/skills/_shared/procedures/test/, which was confirmed green in thissession (
smoke.sh,typed-scan.sh,lint-conventions.shall rc=0) — the lane boots here today.On AC-13's honesty. The roll-call is a self-report, and these rows check that the record is
well-formed and attributable, never that it is true. The same applies to
byin AC-8. The PREQaccepts the roll-call limit explicitly; this SREQ extends the same candour to attribution. Do not let
a green AC-13 or AC-8 be read as proof that a phase enumerated its agents faithfully, or that
by=operatormeans a human was present.Implementation Scope
Areas
plugin/skills/_shared/schemas/phase-outcome-supersede.v1.md; the table + anchored-header notes inschemas/README.mdplugin/skills/_shared/procedures/bin/phase-outcome-supersede.shplugin/skills/_shared/procedures/bin/_lib.sh— add_po_annulmentbeside_po_foreign_gate, carrying the four-way fail-closed partitionbin/phase-outcome-read-all.sh(annulment pass,--include-annulled,annulled_count, density moved onto the raw ledger)bin/phase-outcome-post.sh(--roll-call: sentinel-for-absence +_json_argfor the value, per-entry validation, agent-sorted in the retry-guard normalization)schemas/phase-outcome.v1.md— optionalroll_call, the post-only enforcement seam, the normalization-excluded-set note, both projected read shapesforge-contract.md§2.1 (phase_outcome.supersede), §7 (why no bump), §8 (the new immutable kind), §9 (latest-wins per target)procedures/phase-outcome.md(the new op + both read shapes),decisions-settled-check.md(the gate inherits exclusion; finding-level annulment does not unblock),wait-discipline.md(the roll-call as §1a/§1b's post-time counterpart)plugin/skills/resolve/SKILL.md—--supersedestructured mode, interactive entry, frontmatter description, and theaccept-only restriction on a decision whosefinding_refis annulledrequirements/SKILL.md(2),technical-plan/SKILL.md(3),develop/SKILL.md(3, plus the$PO_ORDINALfix),integrate/SKILL.md(1),promote/SKILL.md(2),procedures/rebase-onto-base.md(1),procedures/qa-playbook.md§8 (the per-round PO — the domain-spawning phase where the roll-call matters most)procedures/test/supersede-flow.sh(new) + a shared roll-call fixture helper;smoke.sh,typed-scan.sh,typed-scan-family.sh,provenance.sh,disposition-flow.sh,promotion-flow.sh,release-flow.sh,supervise/test/supervisor-flow.sh(everyphase-outcome-post.shcall gains--roll-call)File Boundaries
Three independent tracks once the schema is fixed:
_lib.sh_po_annulment+read-all.sh.phase-outcome-supersede.sh+ the new schema doc.post.sh+ the ~12 producer call sites + the existing suites.They touch
_lib.shandpost.shdisjointly (_po_annulmentis a new function;--roll-callis anew arm in
post.sh's parser), so they can proceed in parallel. Doc/contract edits follow whichevertrack settles the behaviour they describe.
resolve/SKILL.mdis touched by the write track (the--supersededoor) and by the read track (theaccept-only restriction) — assign it to one owner.Dependencies & Sequencing
header-authoritative identity rule and its latest-wins rule; settling it late means two
implementations drift.
supersede-flow.shneeds both to be exercisable end-to-end.load-bearing:
post.shmust not require--roll-calluntil the call sites and suites pass it,or every suite goes red at once. Land the fixture helper, then the call sites and suites, then
flip the requirement on.
Constraints & Non-Goals
Constraints:
jq+git, nopython/node, no bashisms in emitted shell.post_comment+scan_commentsonly (verified against the realforge; see External Data Contracts).
phase-outcome:v1→:v2bump. Cross-version reads are unsupported, so a bump would makeevery existing record invisible to the new reader.
post seam.
Non-goals (do NOT build):
annulled outcome from blocking.
sreq:v1,qa-report:v1,test-plan:v1) — already latest-wins.resulting "cannot be proved complete" limit is accepted knowingly.
nothing is rewritten.
byagainst the comment's real author — would need a new adapter primitive; the fieldis a recorded claim, and the schema says so.
Test Plan: phase-outcome-supersede (issue #45)
Validation cases derived from the PREQ alone, before any design existed. They name only what the PREQ
names and assert outcomes a person can observe. Lanes were assigned by
/dev:technical-plan2.6b on2026-08-26, once the design existed; the scenarios themselves are unchanged from the PREQ-derived
original. No scenario is routed to
e2e-browser: this project declares thee2e,a11yandsecurity-browserdomainsnot_applicable, so there is no browser lane here to route to. Everyautomated case is carried by the shell test harness under
plugin/skills/_shared/procedures/test/,which was confirmed green in this session (
smoke.sh,typed-scan.sh,lint-conventions.shall rc=0)before any lane was assigned.
Prerequisites
The state the scenarios need. How each state is produced is a design decision; the PREQ requires only
that each be reachable.
distinguishable from "the only one"
the others stand
used unmodified, to check the compatibility promise
Required Test Data
Test Scenarios
Scenario 1: A false finding reads as annulled
Acceptance criterion: "Given a posted Phase Outcome containing a false finding, when a supersede naming that finding is posted, then reading the issue through the suite's Phase Outcome read helper reports that finding as annulled, carrying its reason and the identity of the record that annulled it."
Lane: integration-covered —
plugin/skills/_shared/procedures/test/supersede-flow.shExpected outcome: the false statement is marked wrong at the place a reader encounters it, and the reader can follow it to the correction.
Scenario 2: An annulled finding drops out of what consumers act on, but not out of history
Acceptance criterion: "Given an annulled finding, when any consumer obtains the issue's findings through the read helper, then the annulled finding is not in the set the consumer acts on — and the original Phase Outcome comment is still present, unedited, and still readable as history."
Lane: integration-covered —
plugin/skills/_shared/procedures/test/supersede-flow.shExpected outcome: consumers stop acting on the false finding while the record of it having been said survives intact.
Scenario 3: An unmodified pre-existing consumer honours annulment for free
Acceptance criterion: "Given a consumer written before this feature that reads Phase Outcomes through the read helper, when it runs against an issue carrying supersede records, then it reads without error and, with no change to its own code, no longer acts on annulled data."
Lane: integration-covered —
plugin/skills/_shared/procedures/test/supersede-flow.sh, which drivespromotion-candidates.shandfold-candidates.shunmodified as the pre-existing consumers, plus a mechanical grep-scan asserting no consumer outside the read helper references the supersede kind.Expected outcome: honouring annulment is inherited by everything that reads through the shared path, rather than being a promise each consumer must separately keep.
Scenario 4: The phase lookup walks past an annulled outcome
Acceptance criterion: "Given an outcome-level supersede, when the current pipeline phase is looked up, then it resolves to the newest surviving outcome's
next_state; the annulled outcome is walked past, exactly asnext_state: skippedalready is."Lane: integration-covered —
plugin/skills/_shared/procedures/test/supersede-flow.shExpected outcome: the pipeline's idea of where the feature is stops being driven by a record that has been withdrawn.
Scenario 5: Every outcome annulled falls back to the pre-pipeline state
Acceptance criterion: "Given an issue whose every Phase Outcome has been annulled, when the current pipeline phase is looked up, then it reports
backlog— the same result the suite already gives an issue that has no Phase Outcome at all."Lane: integration-covered —
plugin/skills/_shared/procedures/test/supersede-flow.shbacklogExpected outcome: annulling everything leaves the issue at the same starting point as an issue that never began, with no undefined or error state in between.
Scenario 6: Annulling every finding does not move the phase
Acceptance criterion: "Given a finding-level supersede, when the pipeline phase is looked up, then it is unchanged — including when every finding in the outcome has been annulled individually. Moving the phase requires annulling the outcome."
Lane: integration-covered —
plugin/skills/_shared/procedures/test/supersede-flow.shExpected outcome: annulling content and withdrawing a record are distinct acts with distinct effects, and doing the first repeatedly never silently becomes the second.
Scenario 7: Decisions on an annulled outcome stop blocking
Acceptance criterion: "Given an annulled outcome that carries open blocking Pending Decisions, when the decisions-settled gate runs, then those decisions no longer block."
Lane: integration-covered —
plugin/skills/_shared/procedures/test/supersede-flow.shExpected outcome: a question raised on a premise that no longer holds stops holding the run up, without the question being erased from the record.
Scenario 8: A supersede says who, what and why
Acceptance criterion: "Given any supersede record, when it is read, then it names who posted it (the producing skill, or the operator), exactly what it annuls, and a non-empty reason."
Lane: integration-covered —
plugin/skills/_shared/procedures/test/supersede-flow.shExpected outcome: a reader who finds a correction can tell who made it and why, without asking anyone.
Scenario 9: The operator corrects an old outcome from a different session
Acceptance criterion: "Given a false outcome from an earlier run, when the operator invokes the supersede command from a different session naming the target and a reason, then the record is posted and the target reads as annulled."
Lane: human-uat — owner: the operator, at the UAT gate, dogfooding
/dev:resolve --supersedeagainst this repo's own tracker from a session other than the one that posted the outcome. The helper-level half (a supersede posted from a fresh process, carrying no session state) is additionally covered byplugin/skills/_shared/procedures/test/supersede-flow.sh.Expected outcome: correcting the record does not require being the session that wrote it, or being anywhere near the original run.
Scenario 10: A supersede posted in error is corrected by a later one
Acceptance criterion: "Given a supersede posted in error, when a later supersede naming the same target is posted, then the later one is what counts — the same latest-wins rule the suite already applies to decision resolutions."
Lane: integration-covered —
plugin/skills/_shared/procedures/test/supersede-flow.shExpected outcome: the correction mechanism can itself be corrected, so a slip does not become permanent.
Scenario 11: A supersede with no such target is refused
Acceptance criterion: "Given a supersede whose target does not exist — an unknown outcome, or a finding not present in the named outcome — when it is posted, then it is refused, naming the bad target."
Lane: integration-covered —
plugin/skills/_shared/procedures/test/supersede-flow.shExpected outcome: a supersede cannot point at nothing, and a mistyped target fails loudly instead of annulling something unintended or silently doing nothing.
Scenario 12: A supersede missing its reason or attribution is refused
Acceptance criterion: "Given a supersede missing its reason or its attribution, when it is posted, then it is refused, naming what is missing."
Lane: integration-covered —
plugin/skills/_shared/procedures/test/supersede-flow.shExpected outcome: an unattributed or unexplained annulment cannot enter the record at all.
Scenario 13: A terminal outcome carries a roll-call
Acceptance criterion: "Given any terminal Phase Outcome, when it is posted, then it carries a roll-call: one entry per agent the phase spawned, each disposed
reportedorterminated, or an explicitly empty roll-call when the phase spawned none."Lane: integration-covered —
plugin/skills/_shared/procedures/test/supersede-flow.sh. NOTE: this checks that the record is well-formed and attributable, never that it is true; the roll-call is a self-report by the PREQ's own accepted limit. The "a real phase populated it" reading is dogfooded at the UAT gate, not asserted here.Expected outcome: every terminal record states what happened to everything the phase started, including stating that it started nothing.
Scenario 14: A terminal outcome with no roll-call is refused
Acceptance criterion: "Given a terminal Phase Outcome with no roll-call at all, when the post is attempted, then it is refused — absence is never read as 'spawned none'."
Lane: integration-covered —
plugin/skills/_shared/procedures/test/supersede-flow.shExpected outcome: omitting the roll-call stops being a way to say nothing was spawned, so silence is never mistaken for an answer.
Scenario 15: A roll-call cannot say "unrecoverable"
Acceptance criterion: "Given a roll-call entry disposed as anything other than
reportedorterminated— includingunrecoverable— when the post is attempted, then it is refused."Lane: integration-covered —
plugin/skills/_shared/procedures/test/supersede-flow.shunrecoverableExpected outcome: judging an agent dead is no longer something the record can express — the lead must end it or wait for it, which is what stops a late report arriving after the record claims it never will.
Scenario 16: Edge cases
Lane: integration-covered —
plugin/skills/_shared/procedures/test/supersede-flow.sh. All six edge cases are mechanical, including "a late report after termination": what it asserts is that the supersede path is sufficient and that no separate mechanism is offered — a grep-scan for any competing correction path plus the Scenario 1-4 assertions, not a live agent.annulled. Verify: the result is either a clear refusal or an inert, recorded no-op — not a silent
success that changes nothing, and not an error a reader cannot interpret.
terminateddeliver a report anyway,after the outcome is posted. Verify: the supersede path is available and sufficient to correct the
outcome — no separate mechanism is needed and none is offered.
sessions. Verify: one of them governs, deterministically, and reading the issue afterwards gives the
same answer every time.
annul a finding. Verify: the read path handles the first-ever supersede the same as a later one.
refused, naming the missing target rather than failing obscurely.
Verify: nothing about the output changed for issues that were never corrected.
Expected outcome: the correction path behaves predictably at its boundaries — first use, concurrent use, repeated use, and no use.
Notes
Those are design decisions;
/dev:technical-plansettles them and annotates the lanes here.not the compatibility promise.
well-formed and attributable, not that it is true — the PREQ accepts that limit explicitly, because
the alternative needs harness knowledge the suite is not allowed to have.
Linked: this issue is sibling #36 (recorded by the devwork pipeline).
Linked: this issue is sibling #418 (recorded by the devwork pipeline).
Linked: this issue is sibling #419 (recorded by the devwork pipeline).
Linked: this issue is sibling #420 (recorded by the devwork pipeline).
Linked: this issue is sibling #421 (recorded by the devwork pipeline).
approvalPending Decision, so an autonomous run cannot annul the outcome carrying its own human gate and proceed? #422Linked: this issue is sibling #422 (recorded by the devwork pipeline).
Linked: this issue is sibling #452 (recorded by the devwork pipeline).
Linked: this issue is sibling #453 (recorded by the devwork pipeline).
Software Requirements: 45-phase-outcome-supersede
Context
phase-outcome:v1is the suite's terminal handoff record and is immutable by contract (§8) — soit is the only durable record with no correction path. A phase lead that discovers, after posting,
that its own record states something false can do nothing about it; a false finding stays live and a
Pending Decision raised on a false premise stays open. The recorded incident: a five-lens review
panel returned nothing, the lead concluded the transport was broken and wrote "panel could not be
run" into its Phase Outcome, and the panel's results arrived 34 seconds later carrying two
blocking concerns. This slice adds a supersede path (recovery) and a roll-call (prevention),
which the PREQ ships together because each was shown insufficient alone.
Approaches Considered
Approach A: A new sibling record kind, honoured inside the read helper
Summary: annulment is a new append-only comment kind,
phase-outcome-supersede:v1, and the oneplace that honours it is
phase_outcome.read_all— so every compliant consumer inherits thebehaviour unmodified.
Pros:
phase-outcome:v1's wire format is untouched, so no:v2bump and no history goesinvisible; the annulled comment is never edited, so §8 stands unweakened; the compatibility promise
is true by construction rather than by an enumerated consumer list; latest-wins-per-target reuses
§9's rule verbatim, and the append-only
active/withdrawnshape reusesfolded-finding:v1'sproven pattern; no new adapter primitive.
Cons:
read_allgains a second comment scan per call; the record set a consumer sees is nolonger literally the comment set on the issue, so history needs an explicit opt-in flag.
Effort: Medium
Approach B: Annulment carried inside a new Phase Outcome
Summary: add a
supersedes[]body field tophase-outcome:v1; correcting a record means mintinga new outcome that names what it annuls.
Pros: no new record kind; the corrector is itself a first-class outcome, so it carries its own
attribution, roll-call and ordinal, and the ledger remains the whole story in one chain.
Cons: ruinously heavy for the actual use case. An operator annulling one false finding months
later must mint a whole Phase Outcome — which forces them to choose a
next_state(moving pipelinestate as a side effect of a correction), supply a roll-call for a run that never happened, and pick
a producer name from a closed enum that has no
operatorin it. The PREQ rejected forcing areplacement outcome for exactly this weight (decision 4).
Effort: Medium
Approach C: Reuse
decision-resolution:v1Summary: express annulment as an "annulment decision" resolved through
/dev:resolve.Pros: zero new schema; reuses the §9 lookup rule and the whole existing operator surface.
Cons: a decision is a question; an annulment is an assertion. It needs a Pending Decision
to exist first, which the false outcome never raised — so a self-correcting skill would be answering
its own invented question. And decision IDs are derived from Phase Outcome ordinals, so an operator
arriving weeks later has no ID to name.
Effort: Low
Decision
Selected: Approach A — a new sibling record kind, honoured inside the read helper.
Rationale: it is the only shape that keeps both promises the PREQ treats as non-negotiable — the
annulled record is never touched (§8 stands) and the exclusion is inherited by consumers written
before the feature existed (honoured in one place, not an enumerated list). B and C both fail the
second promise or pay for the first with a correction act heavier than the thing it corrects.
Architecture
Component Overview
Data Flow
Annulling. A skill (mid-run, on discovering its own record is false) or the operator (later, from
any session) calls the supersede helper with a target, a reason and an attribution. The helper reads
the unfiltered ledger, refuses an unresolvable target, and posts one append-only comment.
Reading.
read_allscans the Phase Outcome records exactly as today, runs the §8 immutabilitycheck, parses, applies the foreign-issue gate, and enforces ordinal density on the raw ledger.
Only then does it scan the supersede kind, resolve latest-wins per target through
_po_annulment,and project the filtered view.
read_latestwrapsread_all, so the current-phase lookup inheritsannulment with no code of its own.
Preventing.
phase_outcome.postrequires aroll_callarray. It validates the array's shapeand refuses absence — it never inspects live agents.
The supersede record
scope(outcome vs finding) is derived fromref's form (PO-…vsF-PO-…) and deliberatelynot stored — a third copy of the same fact is a third thing that can disagree. The body echoes
target/byfor a human reading the comment, exactly asfolded-finding:v1echoes its headertokens; the post helper refuses any header/body mismatch, and readers treat the header as
authoritative for identity.
Matching
refis exact-token, never substring.F-PO-45-3-2is a substring-prefix ofF-PO-45-3-21, and this suite has already shipped that bug once (ref=D-PO-2-4-1matchedref=D-PO-2-4-12, observed live). The reader parses the header'skey=valueattributes with theanchored-header regex and compares with jq string equality — it never greps the raw body.
Read-side gating of supersede records — fail closed
The supersede kind removes data from what consumers act on, so a record of it that cannot be
trusted must never be honoured silently, and must never be dropped silently either. Four disjoint
outcomes in
_po_annulment, mirroring the family's existing partition:refnames a target on this issuestatusEXIT_ENV), naming the comment id and the offending element — the_po_parsetreatment. Silently ignoring it could resurrect a finding that was genuinely annulled.refclaims a target on a different issue_po_foreign_gate. A session must not annul another issue's records.edited_ms > created_ms)EXIT_IMMUTABLE). Detected by the same signal every adapter already renders (local-fs pins mtime toepoch_ms; remote adapters rendercreated_at/updated_at) — no new mechanism. One edited record refuses the whole issue read, §8 parity. That an edit can therefore take an issue's reads offline is tamper-evidence working as intended, not a defect.External Data Contracts
git.wihslon.com/jbr870/devwork-skills) via thetea-cliadapter — the comment ledger this feature reads and writesscan_comments(issue, header)read;post_comment(issue, body)write[{comment_id: string, created_ms: number, edited_ms: number, raw_body: string}]; line 1 of a record round-trips byte-identical, read back as<!-- phase-outcome:v1 id=PO-45-1 skill=requirements -->recorded→external-contracts/forge-comment-ledger.provenance.jsonThe capture confirms the two facts the design rests on: an HTML-comment header with space-delimited
key=valueattributes survives the round trip (so a new sibling kind needs no new adapterprimitive), and the scan envelope already carries
created_ms+edited_ms— the ordering key forlatest-wins-per-target and the §8 edit signal are both present today. No boundary is
unverified, sothis phase raises no precondition.
Observability & Audit
Not applicable as a policy obligation: the project declares
observability: mode: none, so theapplicability test does not intersect and no logging/audit AC rows are owed. Recorded here only
because the reason for that
noneis load-bearing for this feature — "run traceability is the forgerecord (Phase Outcomes, deliverables), not logs". The record trail is this project's only audit
surface, which is why §8's tamper-evidence is preserved rather than relaxed, and why the supersede
record itself carries mandatory attribution and reason.
The honest limit on attribution.
byis an unverified self-report, exactly as the roll-callis. Nothing in the forge binds it to the comment's actual author, and the suite deliberately adds no
adapter primitive to fetch one. It is a claim, recorded and permanent — which is the same standing
the suite already gives a
/dev:resolveresolution posted by an autonomous run. State this in theschema beside the roll-call's limit; do not let a reader mistake
by=operatorfor proof of a human.Key Decisions
Each of these settles something the PREQ deliberately deferred to this phase, or something the
review panel raised.
phase-outcome-supersede:v1(see The supersede record)phase-outcome:v1's wire format untouched, so §7's reader test is trivially passed and no bump is owed.refis a PO id (PO-45-3) or a finding id (F-PO-45-3-2)refidentifies both cases and latest-wins keys on one value. N targets = N records. (Settles "may one supersede target findings across multiple outcomes" — no.)ref/byare authoritative; the body echoes them; post refuses a mismatch;scopeis derived, never storedfolded-finding:v1's header/body echo.refmatchingF-PO-45-3-2prefixesF-PO-45-3-21; this exact bug class already shipped once.read_allalready sorts by it and enforces density; ordinals are minted monotonically and are the ledger's identity. Timestamps are the supersede key, not the outcome key.(created_ms, comment_id)status: withdrawn, append-only — never an edit or a delete.withdrawncarries the same mandatoryreason+byand the same refusal set asactivefolded-finding:v1pattern. Un-annulling restores a record to force, so it deserves equal attribution rigour.PO-45-3does not moot awithdrawnonF-PO-45-3-2; each target resolves on its own, and an annulled outcome leaves the actionable set whatever its findings sayby=operator; every other value must be a canonical producer nameoperatoris disjoint from the closed producer enum (requirements,technical-plan,develop,rebase,qa,integrate,promote,vibecoding,resolve,setup), so skill-vs-operator is decidable from the value alone. Its truthfulness is a claim, not a proof — see Observability & Audit.phase-outcome-read-all.sh, via one_po_annulmentinbin/_lib.shfindings[]into an additiveannulled_findings[]; plus an additiveannulled_counton every surviving outcome's sibling projectionread_latest'slastbecomes the newest survivor; an empty array →null→backlog).annulled_countlets a consumer know filtered history exists without opting into it — the visibility hook that keeps an annulment from being invisible at a human touchpoint.--include-annulledreturns the whole ledger withsuperseded: {…}|nullon each outcome and finding[.[].ordinal] == [range(1;length+1)]dense check. Consumers must not re-assert density on read output, nor computelen+1from it.pending_decisions[]— the gate keeps counting them./dev:resolvepresents such a decision but restricts it toacceptnext_statestill stand — pipeline state advancing on annulled evidence, which AC-6 forbids. The real hazard (resolvingdefer-to-issuespawning a sibling issue from a finding annulled as false) is closed at the acting site instead of by hiding the question.postseesreported|terminated, exactly one per entry, exactly one entry per spawned agent,agentnames unique within a roll-call. Nounrecoverable; any other value refusedreported= the lead read the result, or reconstructed the outcome from the tree perwait-discipline.md§1a step 3.terminated= the lead ended the agent before posting, so no late report can arrive. A terminated agent whose report was already read isreported. The omission is the teeth.--roll-callomitted, or given an empty string, is refused;[]is a valid "spawned none". The flag still accepts@fileor an inline literal via_json_arg_json_argentirely would silently drop@filesupport and break symmetry with--produced/--findings. Sentinel for absence,_json_argfor the value.roll_call: null, exactly assuitedoes. Enforcement lives only at the post seam, so a hand-posted record bypasses it — which is why readers must tolerate absence.phase-outcome:v1next_statevalues": the qualifier is the record kind, not a state list — and it leaves no escape hatch.suite), sorted byagentbefore comparing. A landed pre-feature record withroll_call: nullnever matches an incoming[]suiteis excluded because it describes the writer; a roll-call describes the run's content. Sorting matters: array-order jitter between two attempts would otherwise defeat the guard and mint a duplicate outcome — the failure the guard exists to prevent.EXIT_USAGE(2) for missing/empty--roll-call,--reason,--by, a bad disposition or a malformed entry;EXIT_FINDING(6) for an unresolvable target;EXIT_ENV(7) for a foreign or unparseable supersede at read time;EXIT_IMMUTABLE(4) for an edited onealready_annulled: truewithdrawn, and recording a better reason, are both this path. Posting the record is what makes it an inert, recorded no-op rather than a silent one./dev:resolve(--supersede <target> --reason "…", plus an interactive menu entry)/dev:resolve". One orchestrator surface, and it avoids adding a shipped skill (which also carries theSHIPPED_SKILLSlint step). Rejected: a new/dev:supersedeskill. The helper is the real API — a skill correcting itself mid-run calls it directly.Technical Risks
--roll-callmandatory breaks every existing producer call site at oncetest/keeps the flag-day to one edit per suite.$PO_ORDINAL = len(prior_outcomes) + 1silently drifts once outcomes can be filtered outpostmintsmax+1); annulment makes it wrong more often. Fixed in this slice and covered by a mechanical scan for otherlen(...)+1-on-read-output call sites —developis one of N, not necessarily the only one.read_all's array is still the literal comment set, or re-asserts density on it--include-annulledis the documented history door;annulled_countsignals filtered history in default mode; the schema, procedure doc and contract all state the default is the actionable set, not the ledger, and that it is ordinal-sparse.read_all, amplified repo-wide by the candidate sweeps —promotion-candidates.sh/fold-candidates.shgo from one to two forge calls per open issue, including issues that can never carry a supersedephase-outcome-supersede:v1contains the substringphase-outcome, so one scan could feed both filters locally. Not taken now — it entangles the anchored-header / near-miss discrimination (#54) for a cost nobody is paying. If the sweeps ever feel slow, fix it there (one comment fetch per issue, reused for both kinds), not inread_all.read_allare not atomicExpert Review
Panel: Tier 3, five lenses, on
fable. Reviewer reports carried 12 blocking concerns; 11 areactioned below and 1 is surfaced as a finding for the orchestrator.
Reviewers
ref-substring hazard would recur; three copies of the target have no conflict rule; the filtered array is ordinal-sparse and consumers must be told._json_argfor--roll-callsilently drops@file; roll-call array-order jitter defeats the retry guard.byis a self-declared claim and the plan said so for the roll-call but not for attribution; a supersede can dissolve a blocking approval gate; the foreign-issue gate was unstated for the new kind.Changes Made
pending_decisions[]and the gate keeps counting them;/dev:resolverestricts such a decision toacceptso no sibling issue is spawned from a finding annulled as false.refmatching in jq against parsed header attributes, never a raw-body substring, naming the shipped prefix-collision bug it prevents.scopefrom the record as derived, and made a header/body mismatch a post-time refusal._json_arg(and therefore@file) for--roll-call's value while adding a was-the-flag-seen sentinel for the absence refusal; added per-entry validation including uniqueagentnames.roll_callbyagentin the retry-guard normalization, and definednullvs[]as an explicit non-match.byis an unverified self-report, with the same candour the roll-call's limit already had, and addedannulled_countto default read output so an annulment cannot be invisible at a human touchpoint.reason+byrigour onwithdrawnas onactive.$PO_ORDINALfix to a mechanical scan for otherlen+1-on-read-output sites.created_mstiebreak as an explicit case intest/supersede-flow.sh(AC-10), and a shared fixture helper for the roll-call flag-day (Technical Risks).--include-annulledoutput-shape note to the schema deliverable so no consumer mixes modes.Noted (not actioned)
scan_commentsfor the commonphase-outcomeprefix, partitioned twice locally). Raised by two lenses, so it is promoted into the plan — but as a documented, deliberately-declined lever in Technical Risks rather than as work in this slice: the performance lens found no blocking cost at this load, and the change entangles the anchored-header / near-miss discrimination that #54 hardened. Accepted as a cost chosen with the numbers in hand.approvaldecision. Real and worth deciding, but AC-7 does not qualify by attribution, and narrowing it here would be the design overruling the requirement. Note also that it grants no new capability: an autonomous run can already self-answer a gate through/dev:resolve's structured mode. Surfaced as a finding with adefer-to-issuerecommendation so a human decides it on its own merits.Acceptance Criteria
test/supersede-flow.sh— post PO, supersede one finding, assertannulled_findings[0]carriesreason,byand the annullingcomment_idfindings[]excludes it and still contains the sibling; assert the PO comment'sraw_bodyis byte-identical andedited_ms == created_mspromotion-candidates.shandfold-candidates.shunmodified against an issue carrying supersedes; assert exit 0 and that the annulled outcome's state is not what they report. Mechanical: a grep-scan asserting no consumer outsideread-all.sh/_lib.shreferences the supersede kind — the "honoured in one place" constraint, enforced the way the helper-funnel rule requiresskippedalready isread_latestreturns the middle one'snext_statebacklog— the same answer as an issue with no outcomeread_latest→null; assert issue C (no POs) →null; assert the two agreeread_all'spending_decisions[]after — the exact setdecisions-settled-check.mditerates — and still present under--include-annulledby,target,reasonon a skill-posted and an operator-posted record; assertby=operatoris distinguishable from every producer name; assert a header/body mismatch is refused/dev:resolve --supersedeon this repo's own tracker from a fresh session (dogfooding). Helper-level half also covered bysupersede-flow.sh(the helper carries no session state)withdrawnfor the same target, assert restored; assert the first record is still readable; named case: two records on one target sharingcreated_msresolve deterministically bycomment_id; named case:F-PO-x-y-2is unaffected by a supersede targetingF-PO-x-y-21by, and an invalidby→ four refusals at exit 2, each naming the missing or invalid element; the same four forstatus: withdrawnreported/terminated, or an explicitly empty onereported; with onereported+ oneterminated; with[]— assert each round-trips throughread_all; assert@fileand inline forms both work--roll-call→ exit 2 naming the missing element, no comment posted;--roll-call ''→ same; then post the same payload with[]→ accepted. Read side: a pre-feature record with noroll_callreads asnullwithout error — enforcement is post-onlyreported/terminated—unrecoverableincluded — is refusedunrecoverable→ refused; an invented value → refused; a missing/emptyagent→ refused; two entries with the sameagent→ refused; thenterminated→ acceptedread_all's filtered output;develop's$PO_ORDINALis highest-ordinal + 1len(/length-plus-one arithmetic on Phase-Outcome read output, asserting zero hits; integration: a ledger with an annulled outcome yields the same ordinal from the skill's stated formula aspostactually mintsroll_callentries reordered → assertsretried: trueand no new ordinal; a landedroll_call: nullrecord vs an incoming[]→ not a match, mints a new ordinalCoverage note.
e2e,a11yandsecurity-browserare declarednot_applicablefor thisproject, so no browser lane exists to route to. Every automated row above is carried by the shell
test harness under
plugin/skills/_shared/procedures/test/, which was confirmed green in thissession (
smoke.sh,typed-scan.sh,lint-conventions.shall rc=0) — the lane boots here today.On AC-13's honesty. The roll-call is a self-report, and these rows check that the record is
well-formed and attributable, never that it is true. The same applies to
byin AC-8. The PREQaccepts the roll-call limit explicitly; this SREQ extends the same candour to attribution. Do not let
a green AC-13 or AC-8 be read as proof that a phase enumerated its agents faithfully, or that
by=operatormeans a human was present.Implementation Scope
Areas
plugin/skills/_shared/schemas/phase-outcome-supersede.v1.md; the table + anchored-header notes inschemas/README.mdplugin/skills/_shared/procedures/bin/phase-outcome-supersede.shplugin/skills/_shared/procedures/bin/_lib.sh— add_po_annulmentbeside_po_foreign_gate, carrying the four-way fail-closed partitionbin/phase-outcome-read-all.sh(annulment pass,--include-annulled,annulled_count, density moved onto the raw ledger)bin/phase-outcome-post.sh(--roll-call: sentinel-for-absence +_json_argfor the value, per-entry validation, agent-sorted in the retry-guard normalization)schemas/phase-outcome.v1.md— optionalroll_call, the post-only enforcement seam, the normalization-excluded-set note, both projected read shapesforge-contract.md§2.1 (phase_outcome.supersede), §7 (why no bump), §8 (the new immutable kind), §9 (latest-wins per target)procedures/phase-outcome.md(the new op + both read shapes),decisions-settled-check.md(the gate inherits exclusion; finding-level annulment does not unblock),wait-discipline.md(the roll-call as §1a/§1b's post-time counterpart)plugin/skills/resolve/SKILL.md—--supersedestructured mode, interactive entry, frontmatter description, and theaccept-only restriction on a decision whosefinding_refis annulledrequirements/SKILL.md(2),technical-plan/SKILL.md(3),develop/SKILL.md(3, plus the$PO_ORDINALfix),integrate/SKILL.md(1),promote/SKILL.md(2),procedures/rebase-onto-base.md(1),procedures/qa-playbook.md§8 (the per-round PO — the domain-spawning phase where the roll-call matters most)scripts/lint-conventions.sh— home of the repo's mechanical checks, and therefore where AC-3's "no consumer outside the read helper references the supersede kind" scan and AC-16's "nolen+1arithmetic on Phase-Outcome read output" scan land. Omitted from the first draft of this table; added 2026-08-31 after the collision review showed this file is contended by every concurrent feature.procedures/test/supersede-flow.sh(new) + a shared roll-call fixture helper; the suites that actually invokephase-outcome-post.sh, verified by grep against the merged base:smoke.sh,typed-scan.sh,provenance.sh,disposition-flow.sh,promotion-flow.sh,supervise/test/supervisor-flow.sh, and the harness fixturesupervise/test/fixtures/bin/claude(each call gains--roll-call). NOTtyped-scan-family.sh,release-flow.shor the newpipeline-identity.sh— none of them post a Phase Outcome.File Boundaries
Three independent tracks once the schema is fixed:
_lib.sh_po_annulment+read-all.sh.phase-outcome-supersede.sh+ the new schema doc.post.sh+ the ~12 producer call sites + the existing suites.They touch
_lib.shandpost.shdisjointly (_po_annulmentis a new function;--roll-callis anew arm in
post.sh's parser), so they can proceed in parallel. Doc/contract edits follow whichevertrack settles the behaviour they describe.
resolve/SKILL.mdis touched by the write track (the--supersededoor) and by the read track (theaccept-only restriction) — assign it to one owner.Dependencies & Sequencing
header-authoritative identity rule and its latest-wins rule; settling it late means two
implementations drift.
supersede-flow.shneeds both to be exercisable end-to-end.load-bearing:
post.shmust not require--roll-calluntil the call sites and suites pass it,or every suite goes red at once. Land the fixture helper, then the call sites and suites, then
flip the requirement on.
Constraints & Non-Goals
Constraints:
jq+git, nopython/node, no bashisms in emitted shell.post_comment+scan_commentsonly (verified against the realforge; see External Data Contracts).
phase-outcome:v1→:v2bump. Cross-version reads are unsupported, so a bump would makeevery existing record invisible to the new reader.
post seam.
Non-goals (do NOT build):
annulled outcome from blocking.
sreq:v1,qa-report:v1,test-plan:v1) — already latest-wins.resulting "cannot be proved complete" limit is accepted knowingly.
nothing is rewritten.
byagainst the comment's real author — would need a new adapter primitive; the fieldis a recorded claim, and the schema says so.
Base and concurrency (recorded 2026-08-31)
This plan was authored against
mainat8de068eand has since been rebased ontobd2532c(release 0.4.0), 48 commits later. All seven pre-existing shell suites and
lint-conventions.shrun green on that base, so nothing the plan depends on was disturbed. Two concurrent features landed
in between, and both touch this slice's surface:
deployed, shipped in 0.4.0) rewrote_priminbin/_lib.shinto a capability-gateddispatch and claimed exit codes 8 (
EXIT_UNSUPPORTED) and 9 (EXIT_QUERY). This slice'smapping — 2 / 4 / 6 / 7 — is unaffected, and it must stay unaffected: do not mint a new exit
code for the supersede helper. It also rewrote
forge-contract.md§2.1/§2.2, which is where thisslice adds
phase_outcome.supersede— extend the merged table, do not restore the older one._po_annulmentreaches the forge through_typed_scan→_prim scan_comments, andscan_commentsis a required primitive, so the new gating changes nothing for this slice.develop/SKILL.mdandqa-playbook.mdare on this base),but the issue is back at
phase/requirements— a QA round postedPO-43-11 skill=qa → requirements. Its feature is therefore not finished, and it may re-open exactly the two filesthis slice's roll-call track needs. Re-check before the roll-call track reaches them.
The
phase-outcome-post.shcall-site inventory was re-counted against the merged base and isunchanged: 12 invocations across
requirements(2),technical-plan(3),develop(3),integrate(1),promote(2), andrebase-onto-base.md(1), plusqa-playbook.md§8's per-roundPhase Outcome, which is prose rather than an emitted call.
Five work units, five waves — strictly sequential. This repo is a single shared worktree, so
3.1's isolation rule forfeits intra-wave parallelism; one WU per wave makes that explicit and
removes all file contention. Every file above is owned by exactly one unit.
Why this order, and not the SREQ's three-track order. The SREQ's File Boundaries section
offers read/write/roll-call as parallel tracks, and notes the roll-call track "can land first".
Here it must: WU-45-3-2 flips
--roll-callfrom unknown-arg to mandatory at the post seam, andboth later suites (
supersede-read.sh,supersede-flow.sh) callphase-outcome-post.shto buildtheir fixtures. Landing roll-call after them would turn two green suites red the moment the
requirement flipped. Sequencing it second — after the schema, before anything that posts — is what
keeps the flag-day to one wave.
means two implementations drift"). Artifact-shape: the ACs it carries are schema rows, not
behaviour, so per 3.2 it runs Implementer-only and the skip is recorded in its outcome summary.
flag optionally, land the shared fixture helper, update every call site and suite, then flip
the requirement on. Also carries AC-16's skill-text half, because
develop/SKILL.mdis both aproducer call site and the home of the
$PO_ORDINALformula — one owner per file._po_annulmentin_lib.shplus the two projected read shapes inread-all.sh. Its suite hand-posts supersede comments as fixtures, so it does not depend on thewrite helper — which is also exactly what AC-11's dangling-record case needs.
already_annulledanswer comesfrom
_po_annulment, and it reads history through--include-annulled.resolve/SKILL.mdis touched by both tracks per the SREQ, soit gets a single owner here.
scripts/lint-conventions.shcarries AC-3's and AC-16's mechanicalscans; it is contended by every concurrent feature, so it lands last and alone.
Concurrency note carried forward from the plan. #43's code is merged into this base but its
issue is back at
phase/requirements, sodevelop/SKILL.mdandqa-playbook.mdmay re-open.WU-45-3-2 owns
develop/SKILL.md;qa-playbook.mdcarries no emittedphase-outcome-post.shcall on this base (verified by grep), so it is deliberately not in any unit's file list.
Dev journal — WU-45-3-1 (supersede record schema,
roll_call, contract sections)2026-08-31T10:20 DISCOVERY: the
phase-outcome:v1prefilter cannot see a supersede record at allContext: the WU brief asked whether
_typed_scan's outcome 4 ("an anchored header of a DIFFERENTkind → excluded silently") covers
phase-outcome-supersede:v1being returned by a scan forphase-outcome:v1, since the new token nests with an existing Phase Outcome kind.Pattern: it never gets that far.
_typed_scanappends the version itself(
_prim scan_comments --issue "$issue" --header "${kind}:v1",_lib.sh:225), so the adapter'ssubstring test is against
phase-outcome:v1— not the bare tokenphase-outcome. A supersedeheader reads
<!-- phase-outcome-supersede:v1 ref=… by=… -->, which interposes-supersedebetween
phase-outcomeand:v1, socontains($p)is false on every adapter(
tea-cli/bin/scan_comments.sh:63,glab-cli/bin/scan_comments.sh:33,local-fs/bin/scan_comments.sh:31— all the same case-exact substring test). Verified by runningthe three predicates rather than reading them. The reverse direction is impossible for the same
reason: the longer token is not a substring of the shorter header.
Outcome 4 is nevertheless the correct backstop: the supersede header does match
_typed_scan's$anykindpattern (_lib.sh:232) and not thephase-outcome:v1pattern, so if the prefilter wereever widened to the bare token — the single-scan optimization the SREQ records as deliberately
declined — the record would be excluded silently, never mis-warned as a near-miss.
Why it matters: the brief's premise ("the token contains
phase-outcome, so the prefilter cansee it") is true of the bare token only, and the prefilter does not use the bare token. Both
layers are now written into
schemas/README.mdas a worked example, in that order, so the nextreader gets the real mechanism rather than the plausible one.
2026-08-31T10:45 DISCOVERY:
_po_foreign_gate's segment index does not transfer to a finding refContext: the SREQ specifies the supersede foreign-issue refusal as "same gate and wording as
_po_foreign_gate", which reads like an invitation to reuse the function.Pattern: it cannot be reused verbatim.
_po_foreign_gateextracts the issue with(.id | split("-")[1]), which is right for aPO-{issue}-{ordinal}id and wrong for a findingref:
F-PO-45-3-2splits to["F","PO","45","3","2"], putting the issue at index 2. A read-trackimplementer that calls the existing helper on a supersede
refwould comparePOagainst45andrefuse every finding-level supersede as foreign.
Why it matters: it is a silent-looking bug with a loud symptom (
EXIT_ENVon a valid record),and it lands in the fail-closed path where the refusal looks legitimate. Recorded as an explicit
paragraph in
phase-outcome-supersede.v1.mdunder Read-side gating, together with the numericcomparison rule the existing gate carries for the
--issue 070case, so the later WU has it at thepoint of use.
2026-08-31T11:05 DISCOVERY: the
phase_outcome.postsignature in the schema was already staleContext: adding
roll_callmeant touching the Producer helper section, which statesphase_outcome.post(issue, skill, next_state, produced, findings, pending_decisions).Pattern: that signature already omitted
suite— a field the helper stamps itself — and gave nohint that the argument list is not the body. It is a small drift, but it is the line an implementer
reads to know what to pass. Extended it with
roll_calland an explicit note that the schema marksthe field optional while the post helper requires it, since that asymmetry is the whole enforcement
design and a bare signature hides it.
Why it matters: a finding, not a learning — the signature line in a schema doc is not covered
by any gate, so it drifts silently every time the helper grows an argument. Worth a mechanical check
one day; out of scope here.
2026-08-31T11:30 COMPLETE
Hardest part: deciding how much of the SREQ's rationale to carry into the schema versus leave on
the issue — the read-side gating table and the
by-is-a-claim paragraph are only useful if a readerwho never sees the SREQ still gets the reason, so both were written out in full rather than
referenced.
If I did this again: I would check the prefilter's actual argument (
${kind}:v1, not$kind)before accepting the framing of the nesting question — three minutes of running the predicate
replaced an assumption that would have shipped a wrong worked example into the README.
Dev journal — WU-45-3-2 (roll-call track, issue #45)
2026-08-31T08:20 DISCOVERY: read_all had to be touched for roll_call to round-trip
Context: WU-45-3-2's file boundary excludes
phase-outcome-read-all.sh(the read track owns it),but
test/roll-call.shasserts three AC-13 round-trips and one AC-14 read-side null throughread_all.read_allbuilds its output with an explicitmap({...})projection, so a body key itdoes not name simply does not exist in its output —
roll_callwas invisible.Pattern: added exactly one additive projection line,
roll_call: (.body.roll_call // null),immediately beside the existing
suite: (.body.suite // null). Nothing else in the file changed.Why it matters: the boundary as written is unsatisfiable — no change to
post.shalone can makeAC-13's round-trip assertions pass. The read track's later work rewrites this same
map({...})forannulment and must preserve the line. Reported to the lead.
2026-08-31T08:35 DISCOVERY: an explicit projection makes every additive body field a two-file change
Context:
phase-outcome.v1.md§Versioning says optional top-level body fields are additive and"every reader projects the keys it knows". In practice
read_all's allow-list projection means a newoptional field is silently dropped by every consumer until someone remembers to add a line here.
Why it matters: the schema's promise reads as "add the field and readers cope"; the code's shape
is "add the field and readers ignore it until edited".
suite(#34) androll_call(#45) have noweach paid this tax. Worth a note in the schema, or a projection that merges unknown body keys through.
2026-08-31T09:05 DISCOVERY:
procedures/phase-outcome.mdnow documents a call that the helper refusesContext: the operations table there gives the post helper's usage string:
phase-outcome-post.sh --issue N --skill NAME --next-state STATE [--produced @F] [--findings @F] [--pending @F]. As of this unit that command exits 2 —--roll-callis required.Why it matters: it is the canonical operations reference every producer skill is pointed at, so
a lead that copies it gets a refusal. The file is outside WU-45-3-2's boundary (a later doc work unit
owns it), so it was deliberately left alone rather than edited. It must be updated before the
feature lands. Reported to the lead.
2026-08-31T09:10 DISCOVERY: three flag-day suite call sites are guarded by
2>&1 || trueContext: the supervise harness's
claudestub posts a real Phase Outcome with--skill supervise-stub— a producer name that is not in_VALID_PRODUCERS, so the call alreadyfailed before this unit and will keep failing. It is invisible because the invocation ends
>/dev/null 2>&1 || true.Why it matters: it is a fixture that has quietly not been doing its job, and the
|| trueguarantees no flag day will ever reveal it.
--roll-call '[]'was added anyway (so the call iscorrect the day the producer name is fixed), but the real defect is the swallowed status. Same
principle as the baseline's rc-from-the-command rule, applied to a fixture.
2026-08-31T09:20 COMPLETE
Hardest part: deciding what each producer's roll-call should actually say. The mechanical half
(a flag, a validator, 20 call sites) took one pass; writing guidance that is true per phase —
/dev:developenumerating Test Writer/Implementer pairs by theirwu-status.mdnames, a QA roundenumerating per-domain-per-stage agents,
/dev:integrateand/dev:promoteclaiming[]becausethey genuinely spawn nothing — is where the unit's value is, and none of it is find-and-replace.
If I did this again: flip the requirement on first, in a throwaway commit, purely to enumerate
the call sites the suites actually exercise, then revert and follow the ordered sequence. The SREQ's
call-site inventory was accurate for skill text but the suite counts drifted (7 in
typed-scan.sh,not 10), and a one-minute red run would have settled that before the grep did.
2026-08-31T09:40 DISCOVERY:
test-run-resource-claims.shCR-70 is red on this repo, on main tooContext:
scenario_cr70_cr57_comment_misstates_journal_spellingreadsREAL_JOURNAL="$REPO_ROOT/.devwork/feature-qa-intra-run-lane-ownership/dispatch-journal.md"andreturns 1 when the file is missing. That path holds only
SREQ.mdandtest-plan.md— in thisworktree and in the main checkout.
.devwork/is gitignored staging, so the journal thescenario measures was never committed and is now gone.
Why it matters:
test-run-resource-claims.shis one of CLAUDE.md's four required verificationcommands, and it is currently red at 1 FAIL for everyone, independent of any feature branch. Verified
unrelated to this unit:
git diff 4e2ca40..HEAD --name-onlytouches neitherscripts/nor thatfolder. A repo-level harness whose input lives only in gitignored staging cannot stay green; either
commit the journal (or a fixture copy of it) or make the scenario report undetermined the way
scenario_cr54_root_guardalready does, rather than failing.Development Journal — WU-45-3-3 (read track:
_po_annulment+ the two read shapes)2026-08-31T15:05:00Z DISCOVERY:
set -eis OFF inside the command substitution of an assignment — so every helper reached asvar="$(func …)"must own each internal rcContext:
_po_annulment's dangling-target check had a real jq bug ([$targets[]] | index(.ref)re-binds
.to the array, so.refindexes an array and jq aborts at runtime with exit 5). Iexpected
set -euo pipefailto turn that into a loud death. It did not: the read completed, exited0, and reported no annulments.
Pattern: measured on bash 5.3.9 —
errexitis not in effect inside the command substitution on the right-hand side of an assignment.Every consumer-facing entry point in this tier is reached exactly that way —
records="$(_po_parse …)",scan="$(_typed_scan …)", and nowannul="$(_po_annulment …)"— so a mid-function failure inany of them is silent unless the function checks it explicitly. Only the final status of the
substitution propagates, which is why
_die(anexit) still works while a bare failing assignmentdoes not.
Why it matters: this is the fail-closed rule's blind spot, and it is the exact shape the SREQ
warns about. A jq that dies mid-program hands back an empty string, which
_po_annulmentwouldhave returned as
{}— read downstream as "nothing is annulled", silently resurrecting records anoperator deliberately annulled. The existing tier already survives this only because
_po_parseand_typed_scanhappen to write|| _die/|| returnon every internal step; nothing states whythat is load-bearing rather than stylistic. Every jq in
_po_annulmentnow carries its own|| _die "$EXIT_ENV" …, with a comment saying this is the reason. This doubles as theown-your-rc rule: jq's 1–5 exit range overlaps
EXIT_COLLISION(3)/EXIT_IMMUTABLE(4)/EXIT_ADAPTER(5), so a leaked jq 5 would have been read as a broken adapter install.Related, outside this WU's boundary (not changed):
_po_foreign_gate'sforeign="$(jq -r … <<<"$parsed")"has no|| _die. It is called without a substitution, soerrexit does apply there — but that means a jq failure kills the caller with jq's exit 5, which
collides with
EXIT_ADAPTER. Same class as the #47_gl_api | jq '.[0]'misroute. Low risk today(the program is total over
_po_parse-validated input) but worth a one-line|| _die "$EXIT_ENV"when someone owns that function.
2026-08-31T15:20:00Z DISCOVERY:
supersede-read.sh's Gate row 5 fixture could not produce the state it assertedContext: 51 of 54 assertions went green; the three Gate-5 reds (edited supersede →
EXIT_IMMUTABLE) would not move.Pattern: the fixture planted the supersede at
po16_ms + 1000— one second in the future ofthe wall clock — then appended to it ~120 ms later.
plantpins mtime to the filename's ms andlocal-fs derives
created_msfrom the filename andedited_msfrom mtime, so the append produced anegative delta: measured
{"created_ms":1788189770473,"edited_ms":1788189769592,"delta":-881}.The normative signal is strictly greater-than in three places
(
phase-outcome-supersede.v1.md:93and:112,local-fs/bin/scan_comments.sh:5), so no correctimplementation could fire.
smoke.sh:52-54uses the samesleep 0.05; printf 'tamper' >>idiomsuccessfully because it tampers with a helper-posted comment, whose
created_msis the realclock.
Why it matters: the assertion was not merely red, it was latently flaky — a box slow enough
to spend >1 s between the post and the append would have passed it for the wrong reason. I reported
the diagnosis and the one-line fix (
$((po16_ms-1000))) to the Test Writer rather than editingtheir file or weakening the check; they applied it and the suite is 54/54. The general form: when a
fixture pins a synthetic timestamp AND then relies on the real clock for a second event, the two
clocks must be ordered deliberately, not left to whichever the copied idiom assumed.
2026-08-31T15:30:00Z DISCOVERY:
annulled_countis only useful as a ledger-wide totalContext:
phase-outcome.v1.md§Read shapes says "every surviving outcome carries an additiveannulled_count" and, separately, that it exists "so a consumer can know that filtered historyexists without opting into it". It does not say what is counted, and no assertion in the suite pins
it.
Pattern: implemented as the number of items the default projection removed across the whole
ledger — annulled outcomes plus annulled findings — identical on every surviving outcome.
Why it matters: the per-outcome reading (count only this outcome's annulled findings) defeats
the stated rationale. The dominant human touchpoint is
read_latest, which returns a singlesurviving outcome; if an entire outcome had been annulled out from under it, a per-outcome count
would read
0and the annulment would be exactly as invisible as the schema says it must never be.The ledger-wide total is the only reading under which
read_latestalone can reveal that filteringhappened. Flagged to the lead as an interpretation, since the schema is another unit's file.
2026-08-31T15:34:00Z COMPLETE
Hardest part: diagnosing why a jq runtime error inside
_po_annulmentproduced a clean exit 0and an empty annulment map instead of a loud failure — the answer (errexit is suspended inside an
assignment's command substitution) is invisible at the call site and made a genuine bug look like a
correctly-empty result.
If I did this again: write
|| _dieon every helper-internalvar="$(jq …)"from the firstdraft rather than trusting
set -e, and check a red assertion's fixture against the clock itdepends on before assuming the implementation is at fault.
Development Journal — WU-45-3-4 (phase-outcome-supersede.sh, the write door)
[2026-08-31T16:17:37Z] DISCOVERY: resolve the target against --include-annulled, never the default read
Context: deciding which read door the write helper should use to answer "does this target exist?".
Pattern:
phase-outcome-read-all.sh --issue N --include-annulled, then exact jq string equality of--refagainst.[].id(outcome targets) and.[].findings[].id(finding targets).Why it matters: the two read modes have different output shapes, and the DEFAULT one deletes
annulled outcomes from the array entirely. Resolving against it would make an already-annulled
outcome absent, so the helper would answer
EXIT_FINDING(6, "target cannot be resolved") forexactly the two cases the schema requires it to ACCEPT — re-annulling after a
withdrawn, andrecording a better reason for an annulment that already stands. The failure would have looked like a
correct refusal, which is the worst shape for a bug: the caller is told its target does not exist
when the truth is that it exists and is annulled. The history door is also what makes
already_annulledcomputable at all — it is.superseded != nullon the resolved hit.[2026-08-31T16:17:37Z] DISCOVERY: three harness bugs in supersede-flow.sh, all in the READER half of the test
Context: the helper passed 76 of 89 assertions; the remaining 13 were traced before any
implementation change was considered.
Pattern: all 13 came from three defects in the suite's own readers, none from the helper:
body_field()calledjq -Rswithout-r, so every body value came back JSON-quoted(
["PO-701-1"]vs[PO-701-1]). It also silently broke the four "header/body identity" checks,which compare
header_attr(raw) againstbody_field(quoted) — those could never be equalfor any implementation.
jq -r '.already_annulled // "MISSING"'maps a booleanfalseto"MISSING": jq's//treats
falseexactly likenull. The twoalready_annulled=falseassertions were thereforeunsatisfiable against a JSON boolean.
read_latestisnull—but the older outcome is still in force, so
read_latestcorrectly returns it. Thenullexpectation was copy-pasted from AC-10, where the fixture genuinely has a single outcome.
Why it matters: #2 is the one with teeth. The green-making "fix" was to emit the string
"false"in the receipt, which would have passed all fouralready_annulledassertions and shippeda wrong wire format — a machine-read receipt field typed as a string instead of a bool — to satisfy a
reader bug. The assertions were left red and reported to the Test Writer instead; they fixed all
three (commit
11de472) and the suite went 89/89. Verification of the diagnosis was done on athrowaway copy of the suite carrying only those three edits, so the claim "the helper is complete"
was proven before the harness was touched by anyone.
Not in CLAUDE.md: the existing learning "when the claim is about the ABSENCE of output, assert on
the raw capture, not a
jqparse of it" has a sibling this run found://is not anabsence test, it is a falsiness test, so it silently swallows a legitimate
false. Any assertionof the form
jq -r '.flag // "MISSING"'over a boolean field is wrong for one of its two values.[2026-08-31T16:17:37Z] DISCOVERY: a foreign --ref is a USAGE error, not an unresolvable target
Context:
--issue 45 --ref PO-46-2is exercised by no test, so the exit code was mine to choosebetween
EXIT_USAGE(2) andEXIT_FINDING(6).Pattern: refused at
EXIT_USAGE, from the arguments alone, before the ledger is read at all.Why it matters: the schema's own distinction is that
EXIT_FINDINGis for the state-dependentcase ("state-dependent, so it is not a usage error"), and a ref naming another issue is decidable
with no state whatsoever. The stakes make it worth getting right rather than leaving to the target
lookup: a foreign supersede record that actually lands is a hard
EXIT_ENVrefusal on EVERYsubsequent read of that issue (
_po_annulment's foreign gate), so one mistyped ref would take theissue's reads offline until an operator deleted the comment. Comparing numerically on both sides
(
10#$seg) mirrors_po_foreign_gate, so--issue 070and a ref segment of70agree.[2026-08-31T16:17:37Z] DISCOVERY: _validate_producer's diagnostic is hardcoded to --skill
Context: the briefing asked me to reuse
_validate_producerfor--by's producer case.Pattern:
_validate_producerboth decides and diagnoses, and its diagnostic string names'--skill $s'literally. Reusing it verbatim from this helper would tell an operator who mistyped--byto fix their--skillflag. I kept the reuse for the decision — it runs in a subshell, so its_dieexits only that subshell — and emit a--by-accurate refusal myself. The producer enum staysin exactly one place (
$_VALID_PRODUCERS), which was the point of the reuse.Why it matters: this will recur.
_validate_produceris now called from two flags with differentnames, and the next caller will be
/dev:resolve --supersede(a later wave) with a third. If a thirdsite appears, the clean fix is a flag-name parameter on the validator rather than three subshell
wrappers — worth raising in the meta lane, not worth doing inside this WU's boundary.
[2026-08-31T16:17:37Z] COMPLETE
Hardest part: telling apart the two reasons an assertion can be red — my helper being wrong, and
the harness being unable to observe a correct helper — for thirteen failures at once, without
reaching for the change that would have turned four of them green by shipping a string where the
contract pins a boolean.
If I did this again: I would run the pinned receipt shape through the suite's own reader helpers
(
body_field, the// "MISSING"idiom) on a hand-written fixture before writing the helper. Bothof the reader bugs are visible in thirty seconds that way, and reporting them at minute one rather
than minute forty would have let the Test Writer fix them fully in parallel with the implementation.
Development Journal — WU-45-3-5
Operator surface (
/dev:resolve --supersede), procedure docs, and the two mechanical gates(AC-3, AC-16) for feature 45-phase-outcome-supersede.
[2026-08-31T18:40] DISCOVERY: the AC-16 scan cannot be a plain grep — the suite writes the wrong formula down five times on purpose
What: a naive "flag
len(...)+1in the shipped tree" scan fails the AC-16 positive control ontoday's clean tree. Five shipped sites carry the arithmetic as a counterexample:
schemas/README.md:48,procedures/phase-outcome.md:117,schemas/phase-outcome.v1.md:169,develop/SKILL.md:91,local-fs/SKILL.md:41. Four of the five are files this work unit may nottouch, so an
allow:anchor (the linter's usual escape hatch) was not available.Why it matters: the rule's whole point is that the suite teaches the wrong formula in order to
forbid it. A gate that cannot tell a prohibition from a recipe would force the suite to stop writing
the prohibition down.
Resolution: three conjunct conditions instead of one pattern — the unit must (a) name the read
helper (
phase-outcome-read-all/read_all), (b) carry the arithmetic, and (c) carry nonegation marker (
not/never/wrong/unaffected/ …). Every one of the five counterexamplesfails (a) or (c), most fail both.
develop/SKILL.md:91is the instructive one: it fails (a) becausethe prohibition talks about "read output" in English and never names the helper, and it fails (c) on
"never" — but it sits two lines below a paragraph that does name the helper, which is why the
prose unit had to be the paragraph and not a fixed line window.
[2026-08-31T18:52] STRUGGLE: awk -v eats backslashes out of a regex passed as a variable
Problem: the AC-16 arithmetic pattern was written
(\([^)]*\))?…\+….Actual: six
awk: warning: escape sequence '\(' treated as plain '('on every run, and thepattern silently changed meaning (
*+where*\+was intended). The scan still went green, which isthe dangerous part — a mis-parsed regex that happens to match is a false green waiting to happen.
Solution: bracket expressions —
[(],[)],[+].lint-conventions.shalready documents thisconstraint on
PRIM_RE("No backslashes in the pattern: it is passed to awk via -v"); the note nowsits on these patterns too.
Key insight: awk warnings on stderr are part of a gate's output. A gate that prints warnings and
exits 0 has not been read.
[2026-08-31T19:05] DISCOVERY: the canonical producer reference taught a
postcall that exits 2What: the operations table's
post(...)usage string carried no--roll-call. Since theroll-call track flipped the flag to required, that exact command exits 2 and posts nothing. This file
is the canonical reference every producer skill is pointed at ("NEVER inline adapter mechanics — run
the script"), so the one document a stuck producer reads was handing it a failing call.
Fixed in this unit: the usage string now carries
--roll-call '[]'|@F, with a paragraph underthe table stating that absence is refused and
[]is the explicit "spawned none".Why it survived the roll-call wave: that wave's scope was the ~12 producer call sites — real
invocations, enumerable by grep for the script name. A usage string in a reference table is not a
call site and does not run, so nothing failed when it went stale.
[2026-08-31T19:12] DISCOVERY: without the accept-only restriction, a decision on an annulled finding reports the ledger as CORRUPT
What: Step 7b's first sub-step resolves
decision.finding_refagainstpo.findings[]and, whenabsent, FAILs with "which is not present in PO-… (the Phase Outcome is immutable, so a dangling ref
is a corruption signal, not a normal state)". An annulled finding is absent from
findings[]—it has moved to
annulled_findings[]— so resolving such a decisiondefer-to-issuewould havereported a perfectly healthy ledger as corrupt, and told the operator to go looking for tampering.
Why it does not bite: Step 5's
accept-only restriction (this unit) refusesdefer-to-issuebefore Step 7b can run, and
acceptnever triggers Step 7b at all. The path is closed. I stillrecorded the distinction at Step 7b — a reader arriving there should not have to reconstruct why it
is unreachable, and the "corruption signal" wording is exactly the kind of confident diagnosis that
gets acted on.
[2026-08-31T19:18] DISCOVERY: a stale
len(existing)+1in the ordinal-collision row contradicted the raw-ledger ruleWhat: the Failure-modes row for an ordinal collision explained a deleted PO as "deletion derives
n = len(existing)+1to a value already in use".postdoes not derive that way — it mints highestparsed ordinal + 1 — so the row named the wrong mechanism, in the same file where this unit was
adding "
postalways reads the RAW ledger … highest ordinal + 1". Two contradictory statements aboutminting, one file. Rewrote the parenthetical to the real mechanism (deleting the highest-ordinal
record lowers the maximum, so the next post re-mints an ordinal whose derived Decision IDs already
exist); the row's conclusion and recovery were already right.
Out-of-boundary sibling, NOT fixed:
plugin/skills/local-fs/SKILL.md:41states "PO-ordinalderivation (
n = len(existing)+1) is a count, not an order, and is unaffected." Same wrong formula,asserted as fact rather than as a counterexample, in an adapter doc this unit does not own. The
sentence's actual point (filename order does not affect ordinal derivation) survives the correction,
so it is a one-clause fix for whoever owns that file next.
[2026-08-31T19:22] DISCOVERY:
_po_foreign_gate's unguarded jq would have been reported as a broken adapter installWhat (assigned by the lead, confirmed here):
foreign="$(jq -r … <<<"$parsed")"had no|| _die.The function is called without a command-substitution wrapper at its call sites, so errexit does
apply — and a jq failure would kill the caller with jq's own exit 5, which is
EXIT_ADAPTERinthis tier's shared vocabulary. A failed foreign-issue gate would have been diagnosed as a broken
adapter install. Same class as the
_gl_api | jq '.[0]'misroute recorded on #47. One line added.{
"phase": "develop",
"learnings": [
{"id": "L-1", "scope": "project", "status": "applied",
"summary": "
set -eis OFF inside the command substitution of an assignment, so a helper reached as var="$(func …)" must own every internal rc or a mid-function failure reads as a benign empty answer.","evidence": "DISCOVERY, WU-45-3-3 journal: a real jq bug in _po_annulment's dangling check made the read report NO annulments and exit 0, silently resurrecting deliberately-annulled records.",
"source_ref": "comment:2560", "applied_to": "CLAUDE.md"},
{"id": "L-2", "scope": "project", "status": "applied",
"summary": "An absence-shaped default hides a real signal: jq's
//is a falsiness test (a genuine boolean false reads as MISSING) and.key | lengthis 0 for a key that does not exist, so the diagnostic points at the producer while the bug is in the reader.","evidence": "DISCOVERY, WU-45-3-4 journal: two already_annulled assertions unsatisfiable against a JSON boolean; plus the lead's own journal read, where a wrong key name rendered five populated journals as length 0.",
"source_ref": "comment:2563", "applied_to": "CLAUDE.md"},
{"id": "L-3", "scope": "project", "status": "applied",
"summary": "
findhere is bfs, not GNU findutils: it rejects GNU's relative -newermt '-N minutes' with a hard error, which a suppressed stderr turns into a silent empty result. Use -mmin -N.","evidence": "Lead watchdog on this run reported 'no worktree file written in 12 minutes' while files were actively being written; bfs 4.1.1 printed 'Invalid timestamp' into /dev/null.",
"source_ref": "comment:2560", "applied_to": "CLAUDE.md"},
{"id": "L-4", "scope": "project", "status": "applied",
"summary": "A fixture that pins a synthetic mtime and then relies on the real clock for a second event must order the two deliberately — a future-pinned created_ms inverts edited_ms > created_ms and the tamper-evidence gate silently never fires.",
"evidence": "DISCOVERY, WU-45-3-3 journal: supersede-read.sh Gate row 5 measured created_ms=1788189770473, edited_ms=1788189769592, delta=-881; no correct implementation could have tripped it.",
"source_ref": "comment:2560", "applied_to": "CLAUDE.md"},
{"id": "L-5", "scope": "project", "status": "applied",
"summary": "
awk -vstrips backslashes from a regex passed as a variable, warns on stderr and still exits 0 — use bracket expressions; and more generally, a gate that prints warnings and exits 0 has not been read.","evidence": "STRUGGLE, WU-45-3-5 journal: six 'escape sequence treated as plain' warnings silently turned *\+ into *+ while the AC-16 scan still exited 0.",
"source_ref": "comment:2566", "applied_to": "CLAUDE.md"}
]
}
Linked: this issue is sibling #514 (recorded by the devwork pipeline).