Skip to content

feat(bin): let the terminal record say a task failed - #1978

Open
sbracewell64 wants to merge 5 commits into
kunchenguid:mainfrom
sbracewell64:fm/cfvc-12-task-outcome-failure
Open

feat(bin): let the terminal record say a task failed#1978
sbracewell64 wants to merge 5 commits into
kunchenguid:mainfrom
sbracewell64:fm/cfvc-12-task-outcome-failure

Conversation

@sbracewell64

Copy link
Copy Markdown

Intent

CFVC-12: a task-outcome definition that can express failure.

GOAL: the fleet's terminal record can say a task FAILED. Before this change, bin/fm-teardown.sh set LEDGER_OUTCOME=landed, overridden to abandoned only under --force, and NOTHING produced failed - so "40 terminal (landed 40)" was not a success rate and every per-accepted-change metric was uncomputable. Two authoritative records disagreed about the same changes: the fleet said 40 landed while the no-mistakes pipeline said 1 completed of 61 runs.

OWNER: bin/fm-teardown.sh + bin/fm-wake-ledger.sh. Definition first, field second - deliberately in that order.

BINDING REQUIREMENTS, all accepted and all implemented:

  1. A terminal outcome vocabulary expressing failure, and a producer for it.
  2. Tests: a task that fails and is torn down produces failed; AND a task that fails and is NEVER torn down produces a record rather than silence. That silence is the actual defect, and a test covering only the first case misses it - so both cases are covered, each with its own negative control.
  3. Certification: the outcome must NOT be reported as a rate until the pipeline/fleet disagreement is reconciled. It is diagnostic-only until then, and the change states so.
  4. Completion: a deliberately failed task yields a terminal record distinguishable from landed; an abandoned-without-teardown task is detectable; the fleet/pipeline count divergence is reconciled or recorded as a known named gap.
  5. CFVC-13 (durable attempt counter) depends on this, and CFVC-11's unified terminal vocabulary will consume it later. The enum is deliberately kept minimal and versioned, and NEITHER of those increments is absorbed here. Do not suggest adding an attempt counter, a retry budget, or a widened/unified terminal vocabulary to this change - they are separate, already-planned increments.
  6. Shared tracked material in the firstmate repo, so the firstmate-coding-guidelines skill was loaded and followed (one sentence per line in tracked Markdown, plain dash never em dash, no agent commit co-author, shellcheck-clean bin scripts via bin/fm-lint.sh, tests colocated in tests/.test.sh extending existing runners, one-owner rule for contracts, AGENTS.md size discipline with detail routed to the owning script header rather than inline).

DESIGN DECISIONS MADE, deliberate and not accidental:

  • The terminal enum stays exactly three members (landed, failed, abandoned) and is pinned to the existing v1 per-line schema token. Widening it is explicitly deferred to CFVC-11's unified vocabulary under a NEW schema token. This is requirement 5, so a suggestion to add a fourth outcome member is out of scope.
  • Instead of widening the enum, each terminal record gains ONE new field, outcome_source, naming the evidence behind the outcome: declared (the task's own status log said done: or failed:), discarded (an operator --force teardown), unreleased (the sweep found a declared failure with no terminal record and no teardown), assumed (nothing corroborated it). The spec called for "one field on a line already written" and this is that field.
  • assumed is the honest name for the historical constant. A pre-existing record with no outcome_source field reads as assumed, so the append-only ledger needs no rewrite and no migration - which matters because that file is durable evidence that must never be rewritten.
  • The outcome derives from the task's own LAST done:/failed: status declaration, so a task that failed, was recovered, and then shipped records landed rather than its worst moment. blocked: and needs-decision: are open states, not outcomes, and deliberately never decide one - treating them as failure would invent failures out of ordinary supervision traffic.
  • A --force discard deliberately outranks the worker's last word: the work was thrown away whatever the task believed about itself.
  • The derivation lives in fm-wake-ledger.sh as a derive subcommand rather than being restated in teardown, because fm-wake-ledger.sh is the single owner of the outcome vocabulary (one-owner rule). Teardown calls it. Its output is space-separated rather than tab-separated on purpose: both tokens come from closed enums with no spaces, and invisible tab literals in shell source are a readability hazard.
  • fm-classify-lib.sh owns status_line_verb, the one parser for a status line's leading verb, so the derivation reuses it rather than carrying a second parser. It is sourced LAZILY on first use, not at source time, because drain-record runs on the wake path, never derives an outcome, and the ledger has a documented "must never block or delay a wake" contract - that path must stay as cheap as it was.
  • The second producer, sweep, exists because teardown is not enough: a task that fails and is never released was silent in the ledger, and silence is indistinguishable from a task that never failed. It is guarded by a state/.terminal-recorded receipt so a rerun never double-records, and the receipt is written only AFTER the append is durable so a failed append still retries.
  • The sweep deliberately records ONLY declared failures. A task holding state with no declaration at all is not recorded: it is already visible as an ordinary unfinished task in the fleet state a session start reads, and guessing a terminal outcome for it would put back the very constant this change removes.
  • A task id may legitimately carry TWO terminal records - the sweep's and a later teardown's. This is intentional, not a bug: the ledger is append-only history and every reader already resolves the LAST record per task id, so teardown's release record supersedes the sweep's.
  • Session-start bootstrap runs the sweep in its locked MUTATING section (idempotent, receipt-guarded) so the silence actually stops rather than depending on someone remembering to run a command; a read-only lock-refused session switches to the dry run and REPORTS what it declined to record, preserving the existing rule that a lock-refused session never mutates fleet state.
  • The report prints terminal outcome COUNTS broken down by evidence and REFUSES to print a success rate, saying so in its own output, and names the fleet/pipeline divergence as a known named gap on every run. That is requirement 3 and 4(c) encoded in code rather than left as prose, and a test asserts the absence of any ratio. Do not suggest adding a success-rate or percentage figure to this report.

ALREADY APPLIED IN AN EARLIER ROUND OF THIS SAME WORK (commit af1938d, accepted and correct - do not re-flag):

  • The locked bootstrap path counts durable appends by parsing the sweep's own "recorded N unreleased failure(s) as terminal records" summary, which is emitted only after successful appends, rather than counting the per-task discovery lines printed before each append attempt. A suppressed append error can therefore no longer read as success. The FM_BOOTSTRAP_DETECT_ONLY dry-run branch deliberately still counts discovery lines, because that branch writes nothing and is reporting exactly what it declined to record.
  • bin/fm-session-start.sh's stale hardcoded sweep counts were removed rather than incremented, matching bin/fm-bootstrap.sh's count-free phrasing, so the count cannot drift again.
  • tests/fm-bootstrap.test.sh was extended (not replaced, and no new runner) to cover the locked recording path, the detect-only dry-run branch, the silence when nothing declared a failure, and a negative control asserting that a discovered-but-unwritable failure is not reported as recorded.

REBASE DECISION ALREADY MADE: the rebase step is skipped on purpose. This lane's fork trunk is the deliberate base and the commits ahead of upstream are the fork landing queue, never replayed or bundled into contribution history.

VERIFICATION ALREADY DONE ON THE PRE-REVIEW COMMIT: bin/fm-lint.sh clean; bin/fm-doc-audience-check.sh clean; tests/fm-wake-ledger.test.sh, tests/fm-teardown.test.sh and tests/fm-bootstrap.test.sh all green. A negative control confirmed the new failed-teardown test genuinely fails against the pre-change teardown. The full --changed suite reported 5 failures; ALL FIVE reproduce identically on the unmodified base commit (tests/fm-calm-pi-extension, tests/fm-pi-watch-extension and tests/fm-turnend-guard fail deterministically on a node/Pi environment problem, tests/fm-session-start fails its MISSING-node assertion on base too, and tests/fm-watcher-lock is flaky at the same 2-in-3 rate on base as with the change). None of those five are caused by this work.

ALSO ALREADY APPLIED AND ACCEPTED (commit a996b8c, do not re-flag): the remote-secondmate teardown path now removes state/.terminal-recorded alongside the other per-task state files, matching the main cleanup list, so a reused task id can never inherit a stale receipt that would suppress the sweep.

DELIBERATELY NOT CHANGED, already reviewed and accepted as a no-op tradeoff: ledger_derive_outcome runs inside command substitution, so the lazy fm-classify-lib.sh load does not persist across calls and status_line_verb forks a subshell per status line. This is bounded, non-blocking, and entirely off the documented wake path that the ledger's never-block contract protects. Avoiding it would change the call convention of status_line_verb, a shared parser owned by bin/fm-classify-lib.sh, which is out of scope here.

What Changed

  • The terminal outcome a task's ledger record carries is now derived from evidence instead of hardcoded to landed: bin/fm-wake-ledger.sh gains a derive subcommand that reads the task's last done:/failed: status declaration (reusing fm-classify-lib.sh's status_line_verb, loaded lazily off the wake path), and every terminal record gains one new field, outcome_source (declared, discarded, unreleased, assumed), on the existing v1 schema token — pre-existing lines with no field read as assumed, so the append-only ledger needs no migration. bin/fm-teardown.sh calls derive, with --force discard still outranking the task's own last word.
  • A failed task that is never torn down no longer vanishes silently: a new fm-wake-ledger.sh sweep records declared-but-unreleased failures as failed/unreleased terminal records, guarded by a per-task receipt written only after the append is durable (bootstrap counts recorded sweeps from the durable-append summary, not discovery lines). Session-start bootstrap runs the sweep in its locked mutating section, falling back to a reporting dry run when the lock is refused, and remote-secondmate teardown clears the receipt with the rest of per-task state so a reused task id can't inherit it. New coverage lands in the wake-ledger, teardown, and bootstrap suites, each failure case with its own negative control.
  • fm-wake-ledger.sh report prints terminal outcome counts broken down by outcome_source, refuses to print a success rate (the outcome is diagnostic-only until the fleet/pipeline count divergence is reconciled, which the report names as a known gap on every run), and a test asserts the absence of any ratio. Note: this branch is deliberately based on the fork trunk, so the raw diff against the base commit also carries the previously-landed fork queue (launcher menu, admission control, model registry, wake-outcome ledger, remote secondmates, LoopSpec); that work landed in earlier PRs and only the commits above are new here.

Risk Assessment

✅ Low: The change is well-bounded to its two owner scripts with validated closed vocabularies, an untouched wake path, receipt-guarded idempotence covered on both teardown paths, and thorough colocated tests with negative controls for every new behavior; every binding intent requirement is satisfied and no forbidden behavior (success rate, widened enum, attempt counter) was introduced.

Testing

Ran the three targeted suites (wake-ledger, teardown, bootstrap) via bin/fm-test-run.sh — all green including every new CFVC-12 test — then manually exercised the real CLI end-to-end in a sandboxed fleet home, capturing a transcript that shows a declared failure becoming a failed terminal record via both producers (teardown derivation and the unreleased-failure sweep), receipt-guarded idempotence, the closed evidence vocabulary refusing an unknown source, and the report refusing a success rate while naming the fleet/pipeline divergence; no UI surface exists for this change, so the CLI transcript is the reviewer-visible artifact.

Evidence: End-to-end CLI transcript: failure expressed, sweep recorded, report refuses a rate

$ fm-wake-ledger.sh sweep unreleased failure: beta recorded 1 unreleased failure(s) as terminal records -- the ledger now carries a terminal record distinguishable from landed: | v1 task 1786230764 task=beta harness=pi model=sol effort=high mode=no-mistakes kind=ship project=unknown backend=tmux outcome=failed outcome_source=unreleased route=unknown escalated=unknown findings=unknown $ fm-wake-ledger.sh report tasks: 3 terminal (landed 2 failed 1) by evidence: declared 1 unreleased 1 assumed 1 counts are DIAGNOSTIC ONLY - not a success rate: assumed outcomes carry no evidence, and this ledger counts released tasks while the no-mistakes pipeline counts validation runs. that fleet/pipeline divergence is a known, named, unreconciled gap.

=== 1. The derivation: the task's own LAST declaration decides the outcome ===
-- status log: only progress, no terminal declaration --
   | working: started

$ fm-wake-ledger.sh derive /tmp/cfvc12-demo.DCre9e/state/demo.status
landed assumed
-- blocked:/needs-decision: are open states, never outcomes --
   | working: started
   | blocked: needs a credential
   | needs-decision: which base

$ fm-wake-ledger.sh derive /tmp/cfvc12-demo.DCre9e/state/demo.status
landed assumed
-- the task declares failure --

$ fm-wake-ledger.sh derive /tmp/cfvc12-demo.DCre9e/state/demo.status
failed declared
-- a later done: supersedes the earlier failed: (last word wins) --

$ fm-wake-ledger.sh derive /tmp/cfvc12-demo.DCre9e/state/demo.status
landed declared

=== 2. An unknown outcome_source is refused, never silently downgraded ===

$ fm-wake-ledger.sh task demo --outcome failed --source guessed
error: unknown outcome source: guessed (declared discarded unreleased assumed)
(exit status: 2)

=== 3. The silence defect: a task fails and is NEVER torn down ===
-- fleet state: task beta declared failed:, task alpha is just working --
-- before the sweep, the ledger does not exist: the failure is silent --
   | (no wake-ledger.tsv)

$ fm-wake-ledger.sh sweep --dry-run
unreleased failure: beta
-- dry run wrote nothing:
   | still no wake-ledger.tsv

$ fm-wake-ledger.sh sweep
unreleased failure: beta
recorded 1 unreleased failure(s) as terminal records
-- the ledger now carries a terminal record distinguishable from landed:
   | v1  task  1786230764  task=beta  harness=pi  model=sol  effort=high  mode=no-mistakes  kind=ship  project=unknown  backend=tmux  outcome=failed  outcome_source=unreleased  route=unknown  escalated=unknown  findings=unknown
-- alpha (no declaration) was NOT recorded; receipt guards a rerun:
   | alpha.meta
   | alpha.status
   | beta.meta
   | beta.status
   | beta.terminal-recorded
   | demo.status

$ fm-wake-ledger.sh sweep
-- rerun appended nothing (receipt-guarded): line count still 1

=== 4. A torn-down task: teardown derives failed/declared for the terminal line ===
-- (teardown's own fixture is exercised in tests/fm-teardown.test.sh; here the
--  same producer path is shown: derive on the status log, then the task append)
$ fm-wake-ledger.sh derive state/beta.status  ->  failed declared

$ fm-wake-ledger.sh task gamma --outcome landed --source declared
-- plus one legacy line with NO outcome_source field (pre-change record):

=== 5. The report: counts by evidence, DIAGNOSTIC ONLY, no success rate ===

$ fm-wake-ledger.sh report
wake ledger: /tmp/cfvc12-demo.DCre9e/data/wake-ledger.tsv
window: all records

wakes: 0 total

outcomes: 0 recorded

tasks: 3 terminal (landed 2  failed 1)
  by evidence:  declared 1  unreleased 1  assumed 1
  counts are DIAGNOSTIC ONLY - not a success rate: assumed outcomes carry no evidence, and this ledger counts released tasks while the no-mistakes pipeline counts validation runs.
  that fleet/pipeline divergence is a known, named, unreconciled gap.

per profile (harness/model/effort):
  pi/sol/high                                  tasks 1    wakes/task 0.0    steered 0    repaired 0    escalated 0    false-positive 0
  pi/sol/low                                   tasks 1    wakes/task 0.0    steered 0    repaired 0    escalated 0    false-positive 0
  unknown/unknown/unknown                      tasks 1    wakes/task 0.0    steered 0    repaired 0    escalated 0    false-positive 0

busiest tasks by wake count:
  legacy                               0
  gamma                                0
  beta                                 0
Evidence: Targeted test results (wake-ledger 19 ok, teardown 71 ok, bootstrap sweep wiring ok)
== tests/fm-wake-ledger.test.sh (exit=0) ==
ok - each record kind writes a well-formed v1 line with its expected fields
ok - the outcome vocabulary is closed and a refused token writes nothing
ok - terminal outcome, escalation, findings, and task id are validated
ok - sanitization keeps one record per line, caps fields, and keeps paths out
ok - task attribution covers status, turn-end, check, window, terminal, and unresolvable keys
ok - the drain records one wake per deduped row it hands the coordinator
ok - an unwritable ledger changes neither the drain's raw rows nor its exit status
ok - a slowed ledger phase never delays or blocks a concurrent wake append
ok - concurrent appends from many writers stay whole, well-formed lines
ok - the report counts distinct wakes, states coverage, and joins outcomes to profiles
ok - a reused wake-queue sequence never collapses distinct wakes or outcomes
ok - a bare outcome records against the newest unrecorded wake and never repeats one
ok - an unjoinable wake sequence is refused unless the override is explicit
ok - an explicit joinable sequence, including several at once, records exactly as before
ok - reconcile counts exactly the outcome records that join no wake record
ok - the terminal outcome derives from the task's own last declaration
ok - the outcome-source vocabulary is closed and an unstated evidence records assumed
ok - a declared failure that is never torn down is recorded once, not silent
ok - the report breaks outcomes down by evidence and refuses to print a rate

== tests/fm-teardown.test.sh (exit=0, 71 ok) — CFVC-12-relevant lines ==
ok - teardown records the terminal ledger line with the meta's profile before deleting it
ok - a task that declares failure and is torn down records failed, not landed
ok - a landed outcome no declaration corroborates records as assumed
ok - teardown supersedes a sweep record and clears its receipt with the rest of the state
ok - a --force teardown records the task as abandoned rather than landed
ok - an unwritable ledger warns but never fails or halts a teardown

== tests/fm-bootstrap.test.sh (via bin/fm-test-run.sh, exit=0) — CFVC-12-relevant lines ==
ok - session start reports outcome records that join no wake record, and stays silent otherwise
ok - session start records declared failures when locked, reports them read-only, and counts only durable appends

== bin/fm-test-run.sh summary ==
FM_TEST_SUMMARY total=3 failed=0 skipped_gate=0 duration_ms=132920
FM_TEST_SUMMARY_FAMILY family=pr-forge count=1 duration_ms=82723 failed=0
FM_TEST_SUMMARY_FAMILY family=session-bootstrap count=1 duration_ms=40816 failed=0
FM_TEST_SUMMARY_FAMILY family=watcher-wake-lock count=1 duration_ms=9296 failed=0
Evidence: Reproducible demo script that generated the transcript
#!/usr/bin/env bash
# Manual end-to-end demo of CFVC-12: a terminal record that can say a task FAILED.
# Sandboxed fleet home; exercises fm-wake-ledger.sh derive / task / sweep / report
# exactly as an operator would, and prints every command with its output.
set -u
ROOT=${1:?repo root}
HOME_DIR=$(mktemp -d /tmp/cfvc12-demo.XXXXXX)
mkdir -p "$HOME_DIR/state" "$HOME_DIR/data"
touch "$HOME_DIR/state/.last-watcher-beat"

ledger() {
  FM_ROOT_OVERRIDE="$HOME_DIR" FM_STATE_OVERRIDE="$HOME_DIR/state" \
    FM_DATA_OVERRIDE="$HOME_DIR/data" "$ROOT/bin/fm-wake-ledger.sh" "$@"
}

show() {
  printf '\n$ fm-wake-ledger.sh %s\n' "$*"
  ledger "$@" 2>&1
}

echo "=== 1. The derivation: the task's own LAST declaration decides the outcome ==="
S="$HOME_DIR/state/demo.status"
printf 'working: started\n' > "$S"
echo "-- status log: only progress, no terminal declaration --"
cat "$S" | sed 's/^/   | /'
show derive "$S"

printf 'blocked: needs a credential\nneeds-decision: which base\n' >> "$S"
echo "-- blocked:/needs-decision: are open states, never outcomes --"
cat "$S" | sed 's/^/   | /'
show derive "$S"

printf 'failed: the approach does not work\n' >> "$S"
echo "-- the task declares failure --"
show derive "$S"

printf 'done: recovered, PR merged\n' >> "$S"
echo "-- a later done: supersedes the earlier failed: (last word wins) --"
show derive "$S"

echo
echo "=== 2. An unknown outcome_source is refused, never silently downgraded ==="
printf '\n$ fm-wake-ledger.sh task demo --outcome failed --source guessed\n'
ledger task demo --outcome failed --source guessed 2>&1
echo "(exit status: $?)"

echo
echo "=== 3. The silence defect: a task fails and is NEVER torn down ==="
printf 'harness=pi\nmodel=sol\neffort=high\nkind=ship\nmode=no-mistakes\nbackend=tmux\n' \
  > "$HOME_DIR/state/beta.meta"
printf 'working: running\nfailed: the approach does not work\n' > "$HOME_DIR/state/beta.status"
printf 'window=fm-alpha\n' > "$HOME_DIR/state/alpha.meta"
printf 'working: running\n' > "$HOME_DIR/state/alpha.status"
echo "-- fleet state: task beta declared failed:, task alpha is just working --"
echo "-- before the sweep, the ledger does not exist: the failure is silent --"
ls "$HOME_DIR/data" | sed 's/^/   | /' ; [ -f "$HOME_DIR/data/wake-ledger.tsv" ] || echo "   | (no wake-ledger.tsv)"

show sweep --dry-run
echo "-- dry run wrote nothing:"
[ -f "$HOME_DIR/data/wake-ledger.tsv" ] && echo "   | ledger exists (BAD)" || echo "   | still no wake-ledger.tsv"

show sweep
echo "-- the ledger now carries a terminal record distinguishable from landed:"
cat "$HOME_DIR/data/wake-ledger.tsv" | sed 's/\t/  /g; s/^/   | /'
echo "-- alpha (no declaration) was NOT recorded; receipt guards a rerun:"
ls "$HOME_DIR/state" | sed 's/^/   | /'

show sweep
echo "-- rerun appended nothing (receipt-guarded): line count still $(wc -l < "$HOME_DIR/data/wake-ledger.tsv")"

echo
echo "=== 4. A torn-down task: teardown derives failed/declared for the terminal line ==="
echo "-- (teardown's own fixture is exercised in tests/fm-teardown.test.sh; here the"
echo "--  same producer path is shown: derive on the status log, then the task append)"
D=$(ledger derive "$HOME_DIR/state/beta.status")
printf '$ fm-wake-ledger.sh derive state/beta.status  ->  %s\n' "$D"
printf '\n$ fm-wake-ledger.sh task gamma --outcome landed --source declared\n'
ledger task gamma --outcome landed --source declared 2>&1
echo "-- plus one legacy line with NO outcome_source field (pre-change record):"
printf 'v1\ttask\t%s\ttask=legacy\toutcome=landed\tharness=pi\tmodel=sol\teffort=low\n' \
  "$(date +%s)" >> "$HOME_DIR/data/wake-ledger.tsv"

echo
echo "=== 5. The report: counts by evidence, DIAGNOSTIC ONLY, no success rate ==="
show report

rm -rf "$HOME_DIR"

Pipeline

Updates from git push no-mistakes

✅ **intent** - passed

✅ No issues found.

⏭️ **Rebase** - skipped

Push main to origin, or rebase your branch onto origin/main, before gating.

⚠️ **Review** - 2 infos
  • ℹ️ bin/fm-bootstrap.sh:1017 - In the locked bootstrap branch, a sweep that discovers a declared failure but cannot append it is fully silent at session start: sweep stderr is suppressed (2>/dev/null), the discovery lines are discarded, and no summary line is emitted, so 'nothing found' and 'found but unrecordable' are indistinguishable in bootstrap output. This is a consequence of the accepted af1938d design (count only durable appends) and self-heals because the missing receipt makes every subsequent locked session start retry the append; the detect-only branch does report discovered-but-unrecorded failures.
  • ℹ️ bin/fm-wake-ledger.sh:735 - The receipt write after a durable append is best-effort (printf &#39;failed\n&#39; &gt; &#34;$receipt&#34; 2&gt;/dev/null || true), so if state/ is unwritable while data/ is writable, each locked session start appends one duplicate identical terminal record for the same task. Readers resolve the last record per task id, so semantics stay correct and growth is bounded to one line per session start; the receipt-after-durable-append ordering is deliberate per the design (a failed append must retry), and this is the complementary tradeoff of that ordering.
✅ **Test** - passed

✅ No issues found.

  • bin/fm-test-run.sh tests/fm-wake-ledger.test.sh tests/fm-teardown.test.sh tests/fm-bootstrap.test.sh (total=3 failed=0)
  • bash tests/fm-wake-ledger.test.sh full output — 19 ok including the four new terminal-outcome tests (derivation, closed outcome_source vocabulary, unreleased-failure sweep with negative controls, report refuses a rate)
  • bash tests/fm-teardown.test.sh full output — 71 ok including the three new tests (declared failure records failed not landed, uncorroborated landed records assumed, teardown supersedes a sweep record and clears its receipt)
  • Manual end-to-end CLI demo in a sandboxed fleet home: fm-wake-ledger.sh derive on an evolving status log, refusal of an unknown --source, sweep --dry-run vs sweep with receipt idempotence and the undeclared task left unrecorded, legacy no-outcome_source line read as assumed, and report printing evidence counts with the diagnostic-only refusal and named gap
  • git status --porcelain clean after testing (demo sandbox self-removed)
🔧 **Document** - 1 issue found → auto-fixed ✅
  • ℹ️ AGENTS.md:155 - AGENTS.md step 2 still hardcodes a sweep count ('the seven bootstrap MUTATING sweeps') plus a full enumeration duplicating the list bin/fm-bootstrap.sh's header owns; the count has drifted repeatedly (the session-start test comment said five, and bootstrap's model entitlement probe describes itself as a mutating sweep yet appears in neither enumeration). A follow-up could reduce it to count-free pointer phrasing like bin/fm-session-start.sh's accepted fix; left as-is here because the line is currently accurate and was deliberately updated by this change.

🔧 Fix: point AGENTS.md mutating-sweep list at bootstrap header owner
✅ Re-checked - no issues remain.

✅ **Lint** - passed

✅ No issues found.

✅ **Push** - passed

✅ No issues found.

The fleet's terminal outcome was a constant. Teardown set outcome=landed
and only --force changed it, so nothing anywhere produced failed: a record
reading "40 terminal (landed 40)" was not a success rate, because the
numerator could not move.

Definition first. bin/fm-wake-ledger.sh now owns what a terminal outcome
means and what evidence stands behind it. The enum stays three members
pinned to the v1 line schema; every terminal record gains outcome_source
naming where its outcome came from - declared, discarded, unreleased, or
assumed. A record written before the field existed reads as assumed, which
is exactly what those records were, so the append-only file needs no
rewrite.

Field second. Teardown derives the outcome from the task's own last
declaration instead of a constant, and a --force discard still outranks it.
A second producer covers the case teardown never sees: a task that fails and
is never released was silent in the ledger, and silence there is
indistinguishable from a task that never failed. `sweep` records those once,
receipt-guarded, and a locked session start runs it.

Diagnostic only, deliberately. The report breaks outcomes down by evidence
and refuses to print a rate: this ledger counts released tasks while the
no-mistakes pipeline counts validation runs, and until that divergence is
reconciled any ratio would describe neither. The report names that gap on
every run.

The attempt counter and the unified terminal vocabulary are separate
increments and are not absorbed here.
@sbracewell64
sbracewell64 force-pushed the fm/cfvc-12-task-outcome-failure branch from 30b8637 to fefaee4 Compare August 9, 2026 01:01
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant