Skip to content

ci: give the no-mistakes test analyzer a live-evidence runbook - #6

Merged
Mauryanx merged 13 commits into
mainfrom
fm/fv-13-ci-analyzer-decision
Sep 12, 2026
Merged

Mauryanx merged 13 commits into
mainfrom
fm/fv-13-ci-analyzer-decision

Conversation

@Mauryanx

@Mauryanx Mauryanx commented Sep 12, 2026 •

Copy link
Copy Markdown
Owner

Intent

The captain approved the voice plan and said "Spin up workers on Opus 5 to get this plan done" and "You have standing permission to merge everything... Make all the decisions."

Context. This task was filed 2026-09-11 as not urgent. It is now BLOCKING: two pieces of the approved voice plan cannot finish validation because of it. The firstmate repo's .no-mistakes.yaml has no test.instructions, so the pipeline's test analyzer derives its own scenarios and refuses ones it marked pass as "requires live validation". Evidence today: afk-inject-wedge-claude-pane failed its test step twice with step test failed: validate test analyzer findings: scenario 5 result "pass" requires live validation (runs 01M29ZSNGHRD9A91244NV75AR2, 01M2A0XYT1YPAPMVP7Q3XS1202), and fv-13 hit the same refusal three times before it. Both changes are covered by the repo's real suites; the analyzer simply has no instruction telling it so.

What Changed

  • Added a test.instructions runbook to .no-mistakes.yaml that tells the gate's test analyzer what this repo's product entry points are (executables under bin/ plus the .pi extensions), that the tests/*.test.sh suites drive those entry points as real subprocesses and so may be cited as live evidence, and which scenarios stay untested (vendor agent harnesses, network/forge/SSH/credential surfaces, fixture-replay cases, and cases that only parse a declarative artifact without running its consumer, including fm-nm-test-contract.test.sh itself); it also forbids forcing skipped live guards on via FM_LIVE or FM_GATE_REFUSE_BYPASS.
  • Extended tests/fm-nm-test-contract.test.sh with a case asserting test.instructions stays present and non-empty, mapped .no-mistakes.yaml changes to that script in bin/fm-test-run.sh selection, and covered that mapping with a new case in tests/fm-test-run.test.sh.
  • Factored YAML parsing into a shared fm_yaml_to_json helper in tests/lib.sh that prefers python3+PyYAML and falls back to ruby (returning non-zero when neither exists, so suites fail loudly instead of skipping green), and moved the contract and ci.yml timeout tests onto it.
  • Documented the runbook in docs/configuration.md and the coding-guidelines skill, noting that the analyzer reads it only from the default-branch copy so a feature branch cannot relax the policy judging its own run, and updated the CONTRIBUTING helper inventory.

Risk Assessment

✅ Low: The change is well-bounded to a trusted default-branch config runbook plus its guard test, doc pointers, and a portability fix, and every source-verifiable claim it makes (config key recognized and default-branch-trusted, gate-skip/FM_TEST_END marker semantics, FM_LIVE and FM_GATE_REFUSE_BYPASS, the fm-inbox subprocess exemplar, the 75/20 ci.yml timeouts, and the new selector arm being the only thing that selects an otherwise unclassified script) verified against source.

Testing

Drove the change end-to-end through the real runner: an isolated copy of the repo with only .no-mistakes.yaml edited now selects tests/fm-nm-test-contract.test.sh via bin/fm-test-run.sh --list --changed, the same selection holds on the actual branch against its base, and the contract guard passes on the branch config. Four adversarial mutations of the config - runbook deleted, whitespace-only, retyped as a list, and a deterministic commands.test reintroduced - each produced a loud not ok with exit 1 and gate_skip=false, and restoring the file made it pass again. With PyYAML made unimportable and no ruby on this host, the shared parser helper hard-fails naming both parsers rather than skipping green, on both the contract script and the ci.yml consumer. The runner's own suite ran green including the new selector case and the reworked ci.yml timeout case, and the documentation-audiences suite still passes after the prose edits. Two things stayed untested and are reported as such: the ruby/Psych fallback branch (no ruby interpreter on this host, and installing one is outside the worktree boundary) and the gate analyzer actually consuming test.instructions, which is readable only from the trusted default-branch copy and would require invoking no-mistakes, which this phase must not do. No UI surface exists in this change, so evidence is CLI transcripts.

  • Live validation: ✅ go - 7 of 9 scenarios driven live against the product
Scenario Result Live Evidence
Editing .no-mistakes.yaml makes the changed-file runner select the contract that guards it ✅ pass live 02-nm-only-change-selection.txt: in an isolated copy of HEAD with only .no-mistakes.yaml modified, bin/fm-test-run.sh --list --changed --base HEAD lists tests/fm-nm-test-contract.test.sh; 01-chang…
The contract guard passes on this branch's config (no commands.test, runbook present) ✅ pass live 03-contract-test-pass.txt: bin/fm-test-run.sh tests/fm-nm-test-contract.test.sh prints ok - no-mistakes does not configure commands.test and `ok - no-mistakes carries a non-empty test.instructions…
Adversarial: deleting, blanking, or mistyping test.instructions fails the guard instead of passing silently ✅ pass live 04-contract-guard-adversarial.txt cases A/B/C: key removed -> test.instructions is NoneType rather than a string; whitespace-only -> test.instructions is empty; YAML list -> `test.instructions is…
Adversarial: reintroducing a deterministic commands.test suite walk is rejected ✅ pass live 04-contract-guard-adversarial.txt case D: not ok - commands.test must be absent or empty so Test stays intent-targeted; got: 'bin/fm-test-run.sh --all', exit=1
Adversarial: a host with no YAML parser hard-fails the suite rather than skipping green ✅ pass live 05-parser-absent-hard-fail.txt: ruby genuinely absent on this host and PyYAML shadowed by an ImportError package; the case prints `not ok - could not parse .no-mistakes.yaml as YAML (needs python3 wit…
The runner's own suite stays green, including the new selector case and the ci.yml case moved onto the shared helper ✅ pass live 06-runner-suite.txt: ok - no-mistakes config change selects its contract test and ok - Herdr CI family-run step times out at 20 min under a 75 min job backstop, FM_TEST_END exit=0
Changed documentation still satisfies the repository's documentation inventory contract ✅ pass live 07-docs-audiences.txt: bin/fm-test-run.sh tests/fm-documentation-audiences.test.sh exit=0 with all four ok lines after the docs/configuration.md and SKILL.md edits
PyYAML absent but ruby present: the Psych fallback parses the config ⏸️ untested no No ruby interpreter exists on this host and installing system packages is outside this worktree's boundary. To drive it, install ruby (its bundled psych) on the gate host, or run this step on a host t…
The gate's test analyzer receives the runbook and stops refusing suite-proven passes ⏸️ untested no This is the gate pipeline itself, which this test phase is explicitly forbidden to invoke (no init, axi run, or rerun), and the runbook is trusted only from the default branch, so it cannot take effec…
Evidence: Changed-file selection on the real branch (contract test selected)

Source: Changed-file selection on the real branch (contract test selected)

$ git diff --name-only d859c15 HEAD
.agents/skills/firstmate-coding-guidelines/SKILL.md
.no-mistakes.yaml
bin/fm-test-run.sh
docs/configuration.md
tests/fm-nm-test-contract.test.sh
tests/fm-test-run.test.sh
tests/lib.sh

$ bin/fm-test-run.sh --list --changed --base d859c15
tests/fm-arm-pretool-check.test.sh
tests/fm-ask-user-authority.test.sh
tests/fm-bearings-board.test.sh
tests/fm-brief.test.sh
tests/fm-calm-pi-extension.test.sh
tests/fm-captain-hold-lifecycle.test.sh
tests/fm-cd-pretool-check.test.sh
tests/fm-classify-decision-key.test.sh
tests/fm-composer-ghost.test.sh
tests/fm-composer-lib.test.sh
tests/fm-crew-state.test.sh
tests/fm-documentation-audiences.test.sh
tests/fm-ensure-agents-md.test.sh
tests/fm-grok-harness.test.sh
tests/fm-harness-adapter-references.test.sh
tests/fm-herdr-lab.test.sh
tests/fm-kimi-harness.test.sh
tests/fm-lint-workflows.test.sh
tests/fm-lint.test.sh
tests/fm-muse-harness.test.sh
tests/fm-omp-harness.test.sh
tests/fm-operational-input.test.sh
tests/fm-pi-primary-types.test.sh
tests/fm-rovo-harness.test.sh
tests/fm-send-popup-settle.test.sh
tests/fm-send-settle.test.sh
tests/fm-subagent-pretool-check.test.sh
tests/fm-supervision-instructions.test.sh
tests/fm-task-delivery.test.sh
tests/fm-test-isolation-proof.test.sh
tests/fm-test-run.test.sh
tests/fm-tmux-submit-busy.test.sh
tests/fm-trace-context-lib.test.sh
tests/fm-transition-lib.test.sh
tests/fm-vendor-auth-probe.test.sh
tests/fm-afk-inject-herdr-e2e.test.sh
tests/fm-afk-launch.test.sh
tests/fm-backend-autodetect-smoke.test.sh
tests/fm-backend-herdr-agent-exit-shell-e2e.test.sh
tests/fm-backend-herdr-eventwait-smoke.test.sh
tests/fm-backend-herdr-focus-flash-e2e.test.sh
tests/fm-backend-herdr-launcher-workspace-e2e.test.sh
tests/fm-backend-herdr-presentation-e2e.test.sh
tests/fm-backend-herdr-prune-safety-e2e.test.sh
tests/fm-backend-herdr-respawn-idem-e2e.test.sh
tests/fm-backend-herdr-smoke.test.sh
tests/fm-backend-herdr-stale-active-tab-e2e.test.sh
tests/fm-backend-herdr-workspace-per-home-e2e.test.sh
tests/fm-control-herdr-smoke.test.sh
tests/fm-herdr-session-cleanup-e2e.test.sh
tests/fm-afk-contract.test.sh
tests/fm-afk-inject-e2e.test.sh
tests/fm-afk-return.test.sh
tests/fm-afk-pi-herdr-return-e2e.test.sh
tests/fm-bearings-board-lavish-live-e2e.test.sh
tests/fm-claude-stop-autoarm-live-e2e.test.sh
tests/fm-cmux-claude-composer-live-e2e.test.sh
tests/fm-codex-continuity-live-e2e.test.sh
tests/fm-composer-matrix-live-e2e.test.sh
tests/fm-cursor-primary-live-e2e.test.sh
tests/fm-grok-continuity-live-e2e.test.sh
tests/fm-grok-stop-live-e2e.test.sh
tests/fm-harness-adapter-instructions-live-e2e.test.sh
tests/fm-harness-liveness-drift-live-e2e.test.sh
tests/fm-herdr-submit-confirm-live-e2e.test.sh
tests/fm-herdr-version-floor-live-e2e.test.sh
tests/fm-muse-signals-live-e2e.test.sh
tests/fm-omp-primary-live-e2e.test.sh
tests/fm-opencode-primary-live-e2e.test.sh
tests/fm-pi-branch-live-e2e.test.sh
tests/fm-pi-branch-responsiveness-live-e2e.test.sh
tests/fm-pi-codex-native.test.sh
tests/fm-pi-primary-live-e2e.test.sh
tests/fm-quota-array-dispatch-live-e2e.test.sh
tests/fm-rovo-signals-live-e2e.test.sh
tests/fm-send-inbox-doorbell-live-e2e.test.sh
tests/fm-send-secondmate-marker-herdr-e2e.test.sh
tests/fm-sessionstart-hook-live-e2e.test.sh
tests/fm-sessionstart-instruction-refresh-live-e2e.test.sh
tests/fm-backend-cmux-smoke.test.sh
tests/fm-backend-cmux.test.sh
tests/fm-backend-herdr.test.sh
tests/fm-backend-tmux-smoke.test.sh
tests/fm-backend.test.sh
tests/fm-claude-trust.test.sh
tests/fm-control-relaunch.test.sh
tests/fm-control.test.sh
tests/fm-herdr-session-cleanup.test.sh
tests/fm-send-inbox.test.sh
tests/fm-send-resolve-key.test.sh
tests/fm-send-strict.test.sh
tests/fm-spawn-batch.test.sh
tests/fm-spawn-dispatch-profile.test.sh
tests/fm-spawn-worktree-settle.test.sh
tests/fm-teardown-endpoint-safety.test.sh
tests/fm-tmux-agent-liveness.test.sh
tests/fm-trace-context-spawn.test.sh
tests/fm-backend-orca.test.sh
tests/fm-backend-zellij-smoke.test.sh
tests/fm-backend-zellij.test.sh
tests/fm-backlog-atomicity.test.sh
tests/fm-bootstrap-network-parallel.test.sh
tests/fm-bootstrap.test.sh
tests/fm-fleet-sync.test.sh
tests/fm-gate-refuse.test.sh
tests/fm-gotmp.test.sh
tests/fm-session-start.test.sh
tests/fm-sessionstart-nudge.test.sh
tests/fm-startup-network.test.sh
tests/fm-tangle-guard.test.sh
tests/fm-update.test.sh
tests/fm-backlog-handoff.test.sh
tests/fm-on.test.sh
tests/fm-remote-backlog-handoff.test.sh
tests/fm-remote-doctor.test.sh
tests/fm-remote-herdr-guard.test.sh
tests/fm-remote-job-orphan-reap.test.sh
tests/fm-remote-job.test.sh
tests/fm-remote-reply.test.sh
tests/fm-remote-secondmate-lifecycle-e2e.test.sh
tests/fm-remote-secondmate-trace-context.test.sh
tests/fm-remote-transport-lanes.test.sh
tests/fm-secondmate-harness.test.sh
tests/fm-secondmate-lifecycle-e2e.test.sh
tests/fm-secondmate-liveness.test.sh
tests/fm-secondmate-reconcile.test.sh
tests/fm-secondmate-restart.test.sh
tests/fm-secondmate-safety.test.sh
tests/fm-secondmate-sync.test.sh
tests/fm-send-secondmate-marker.test.sh
tests/fm-shared-captain-inheritance.test.sh
tests/fm-startup-memory-budget.test.sh
tests/fm-stow-cascade.test.sh
tests/fm-bearings-board-render.test.sh
tests/fm-bearings-snapshot.test.sh
tests/fm-fleet-snapshot-view.test.sh
tests/fm-home-summary-refresh.test.sh
tests/fm-branch-supervision.test.sh
tests/fm-busy-adapter-wiring.test.sh
tests/fm-busy-state.test.sh
tests/fm-classify-corr-token.test.sh
tests/fm-claude-stop-autoarm.test.sh
tests/fm-cursor-harness.test.sh
tests/fm-extension-binding.test.sh
tests/fm-gitignore-config.test.sh
tests/fm-live-gate.test.sh
tests/fm-no-mistakes-required.test.sh
tests/fm-peek-remote.test.sh
tests/fm-pending-reply.test.sh
tests/fm-pi-branch-extension.test.sh
tests/fm-procevent-quota.test.sh
tests/fm-procevent-when.test.sh
tests/fm-procevent.test.sh
tests/fm-project-origin.test.sh
tests/fm-public-followup.test.sh
tests/fm-quota-choose.test.sh
tests/fm-remote-entrypoint.test.sh
tests/fm-remote-secondmate-parent-binding.test.sh
tests/fm-send-remote-delivery.test.sh
tests/fm-spawn-pool-base-freshen.test.sh
tests/fm-test-fixture-cleanup.test.sh
tests/fm-test-fixtures.test.sh
tests/fm-voice-relay.test.sh
tests/fm-wake-drain-open-decisions-cursor.test.sh
tests/fm-wake-drain-open-decisions.test.sh
tests/fm-wake-drain-outcome-backstop.test.sh
tests/fm-check-unregister.test.sh
tests/fm-pr-check-security.test.sh
tests/fm-pr-merge.test.sh
tests/fm-review-diff.test.sh
tests/fm-teardown.test.sh
tests/fm-x-mode.test.sh
tests/fm-cursor-primary.test.sh
tests/fm-daemon.test.sh
tests/fm-guard-stale-banner.test.sh
tests/fm-inactive-reconcile.test.sh
tests/fm-mail-check.test.sh
tests/fm-mail.test.sh
tests/fm-pi-watch-extension.test.sh
tests/fm-session-lock-ancestry.test.sh
tests/fm-supervision-events.test.sh
tests/fm-task-inbox.test.sh
tests/fm-tool-update-check.test.sh
tests/fm-turnend-guard.test.sh
tests/fm-wake-daemon-lifecycle-e2e.test.sh
tests/fm-wake-drain-unread-status.test.sh
tests/fm-wake-drain-voice-first.test.sh
tests/fm-wake-drain-voice-hold.test.sh
tests/fm-wake-queue.test.sh
tests/fm-watch-arm.test.sh
tests/fm-watch-checkpoint.test.sh
tests/fm-watch-recovery-loop.test.sh
tests/fm-watch-triage.test.sh
tests/fm-watcher-lock.test.sh
tests/fm-gemini-harness.test.sh
tests/fm-inbox-conversation.test.sh
tests/fm-nm-test-contract.test.sh
tests/fm-pi-windows-shell-invocation.test.sh
tests/fm-stat-shadowing.test.sh
exit=0
Evidence: Isolated repo: only .no-mistakes.yaml edited selects the contract test

Source: Isolated repo: only .no-mistakes.yaml edited selects the contract test

# isolated copy of the repo at HEAD, only .no-mistakes.yaml edited

$ printf "\n# operator tweak\n" >> .no-mistakes.yaml && git status --porcelain
 M .no-mistakes.yaml

$ bin/fm-test-run.sh --list --changed --base HEAD
tests/fm-arm-pretool-check.test.sh
tests/fm-ask-user-authority.test.sh
tests/fm-bearings-board.test.sh
tests/fm-brief.test.sh
tests/fm-calm-pi-extension.test.sh
tests/fm-captain-hold-lifecycle.test.sh
tests/fm-cd-pretool-check.test.sh
tests/fm-classify-decision-key.test.sh
tests/fm-composer-ghost.test.sh
tests/fm-composer-lib.test.sh
tests/fm-crew-state.test.sh
tests/fm-documentation-audiences.test.sh
tests/fm-ensure-agents-md.test.sh
tests/fm-grok-harness.test.sh
tests/fm-harness-adapter-references.test.sh
tests/fm-herdr-lab.test.sh
tests/fm-kimi-harness.test.sh
tests/fm-lint-workflows.test.sh
tests/fm-lint.test.sh
tests/fm-muse-harness.test.sh
tests/fm-omp-harness.test.sh
tests/fm-operational-input.test.sh
tests/fm-pi-primary-types.test.sh
tests/fm-rovo-harness.test.sh
tests/fm-send-popup-settle.test.sh
tests/fm-send-settle.test.sh
tests/fm-subagent-pretool-check.test.sh
tests/fm-supervision-instructions.test.sh
tests/fm-task-delivery.test.sh
tests/fm-test-isolation-proof.test.sh
tests/fm-test-run.test.sh
tests/fm-tmux-submit-busy.test.sh
tests/fm-trace-context-lib.test.sh
tests/fm-transition-lib.test.sh
tests/fm-vendor-auth-probe.test.sh
tests/fm-afk-inject-herdr-e2e.test.sh
tests/fm-afk-launch.test.sh
tests/fm-backend-autodetect-smoke.test.sh
tests/fm-backend-herdr-agent-exit-shell-e2e.test.sh
tests/fm-backend-herdr-eventwait-smoke.test.sh
tests/fm-backend-herdr-focus-flash-e2e.test.sh
tests/fm-backend-herdr-launcher-workspace-e2e.test.sh
tests/fm-backend-herdr-presentation-e2e.test.sh
tests/fm-backend-herdr-prune-safety-e2e.test.sh
tests/fm-backend-herdr-respawn-idem-e2e.test.sh
tests/fm-backend-herdr-smoke.test.sh
tests/fm-backend-herdr-stale-active-tab-e2e.test.sh
tests/fm-backend-herdr-workspace-per-home-e2e.test.sh
tests/fm-control-herdr-smoke.test.sh
tests/fm-herdr-session-cleanup-e2e.test.sh
tests/fm-nm-test-contract.test.sh
exit=0
Evidence: Contract guard passes on the branch config

Source: Contract guard passes on the branch config

$ bin/fm-test-run.sh tests/fm-nm-test-contract.test.sh
FM_TEST_BEGIN 2026-09-12T17:34:04Z tests/fm-nm-test-contract.test.sh family=unclassified expected_gate_skip=none
ok - no-mistakes does not configure commands.test
ok - no-mistakes carries a non-empty test.instructions runbook
FM_TEST_END 2026-09-12T17:34:05Z tests/fm-nm-test-contract.test.sh exit=0 duration_ms=706 gate_skip=false
FM_TEST_SUMMARY total=1 failed=0 skipped_gate=0 duration_ms=896
FM_TEST_SUMMARY_FAMILY family=unclassified count=1 duration_ms=706 failed=0
FM_TEST_SLOWEST rank=1 script=tests/fm-nm-test-contract.test.sh duration_ms=706
exit=0
Evidence: Adversarial config mutations all fail the guard loudly

Source: Adversarial config mutations all fail the guard loudly

\### A. test.instructions key removed (regression that reopens the analyzer refusal)
--- parsed .no-mistakes.yaml ---
commands: {'lint': 'bin/fm-lint.sh'}
test keys: ['evidence']
test.instructions: NoneType None
$ bin/fm-test-run.sh tests/fm-nm-test-contract.test.sh
FM_TEST_BEGIN 2026-09-12T17:34:42Z tests/fm-nm-test-contract.test.sh family=unclassified expected_gate_skip=none
ok - no-mistakes does not configure commands.test
not ok - test.instructions must stay present and non-empty so the test analyzer keeps its live-evidence runbook; test.instructions is NoneType rather than a string
FM_TEST_END 2026-09-12T17:34:42Z tests/fm-nm-test-contract.test.sh exit=1 duration_ms=624 gate_skip=false
FM_TEST_SUMMARY total=1 failed=1 skipped_gate=0 duration_ms=796
FM_TEST_SUMMARY_FAMILY family=unclassified count=1 duration_ms=624 failed=1
FM_TEST_SLOWEST rank=1 script=tests/fm-nm-test-contract.test.sh duration_ms=624
exit=1

\### B. test.instructions present but whitespace-only
--- parsed .no-mistakes.yaml ---
commands: {'lint': 'bin/fm-lint.sh'}
test keys: ['evidence', 'instructions']
test.instructions: str '   \n  \n'
$ bin/fm-test-run.sh tests/fm-nm-test-contract.test.sh
FM_TEST_BEGIN 2026-09-12T17:34:43Z tests/fm-nm-test-contract.test.sh family=unclassified expected_gate_skip=none
ok - no-mistakes does not configure commands.test
not ok - test.instructions must stay present and non-empty so the test analyzer keeps its live-evidence runbook; test.instructions is empty
FM_TEST_END 2026-09-12T17:34:43Z tests/fm-nm-test-contract.test.sh exit=1 duration_ms=709 gate_skip=false
FM_TEST_SUMMARY total=1 failed=1 skipped_gate=0 duration_ms=887
FM_TEST_SUMMARY_FAMILY family=unclassified count=1 duration_ms=709 failed=1
FM_TEST_SLOWEST rank=1 script=tests/fm-nm-test-contract.test.sh duration_ms=709
exit=1

\### C. test.instructions replaced by a YAML list
--- parsed .no-mistakes.yaml ---
commands: {'lint': 'bin/fm-lint.sh'}
test keys: ['evidence', 'instructions']
test.instructions: list ['read the runbook']
$ bin/fm-test-run.sh tests/fm-nm-test-contract.test.sh
FM_TEST_BEGIN 2026-09-12T17:34:44Z tests/fm-nm-test-contract.test.sh family=unclassified expected_gate_skip=none
ok - no-mistakes does not configure commands.test
not ok - test.instructions must stay present and non-empty so the test analyzer keeps its live-evidence runbook; test.instructions is list rather than a string
FM_TEST_END 2026-09-12T17:34:44Z tests/fm-nm-test-contract.test.sh exit=1 duration_ms=591 gate_skip=false
FM_TEST_SUMMARY total=1 failed=1 skipped_gate=0 duration_ms=770
FM_TEST_SUMMARY_FAMILY family=unclassified count=1 duration_ms=591 failed=1
FM_TEST_SLOWEST rank=1 script=tests/fm-nm-test-contract.test.sh duration_ms=591
exit=1

\### E. restored file passes again
FM_TEST_BEGIN 2026-09-12T17:34:45Z tests/fm-nm-test-contract.test.sh family=unclassified expected_gate_skip=none
ok - no-mistakes does not configure commands.test
ok - no-mistakes carries a non-empty test.instructions runbook
FM_TEST_END 2026-09-12T17:34:45Z tests/fm-nm-test-contract.test.sh exit=0 duration_ms=687 gate_skip=false
FM_TEST_SUMMARY total=1 failed=0 skipped_gate=0 duration_ms=877
FM_TEST_SUMMARY_FAMILY family=unclassified count=1 duration_ms=687 failed=0
FM_TEST_SLOWEST rank=1 script=tests/fm-nm-test-contract.test.sh duration_ms=687
exit=0
\### D. a deterministic commands.test is reintroduced
$ bin/fm-test-run.sh tests/fm-nm-test-contract.test.sh
FM_TEST_BEGIN 2026-09-12T17:34:57Z tests/fm-nm-test-contract.test.sh family=unclassified expected_gate_skip=none
not ok - commands.test must be absent or empty so Test stays intent-targeted; got: 'bin/fm-test-run.sh --all'
FM_TEST_END 2026-09-12T17:34:57Z tests/fm-nm-test-contract.test.sh exit=1 duration_ms=379 gate_skip=false
FM_TEST_SUMMARY total=1 failed=1 skipped_gate=0 duration_ms=574
FM_TEST_SUMMARY_FAMILY family=unclassified count=1 duration_ms=379 failed=1
FM_TEST_SLOWEST rank=1 script=tests/fm-nm-test-contract.test.sh duration_ms=379
exit=1
Evidence: No YAML parser available: hard fail, gate_skip=false

Source: No YAML parser available: hard fail, gate_skip=false

# host capability probe
$ command -v ruby; python3 -c "import yaml; print(yaml.__version__)"
(ruby: not installed)
PyYAML 6.0.3

# PyYAML made unimportable via a shadowing package; ruby genuinely absent -> neither parser
$ PYTHONPATH=<shadow> python3 -c "import yaml"
ImportError: simulated: PyYAML not installed on this host

$ PYTHONPATH=<shadow> bin/fm-test-run.sh tests/fm-nm-test-contract.test.sh
FM_TEST_BEGIN 2026-09-12T17:35:06Z tests/fm-nm-test-contract.test.sh family=unclassified expected_gate_skip=none
not ok - could not parse .no-mistakes.yaml as YAML (needs python3 with PyYAML, or ruby with psych)
FM_TEST_END 2026-09-12T17:35:06Z tests/fm-nm-test-contract.test.sh exit=1 duration_ms=439 gate_skip=false
FM_TEST_SUMMARY total=1 failed=1 skipped_gate=0 duration_ms=619
FM_TEST_SUMMARY_FAMILY family=unclassified count=1 duration_ms=439 failed=1
FM_TEST_SLOWEST rank=1 script=tests/fm-nm-test-contract.test.sh duration_ms=439
exit=1

$ PYTHONPATH=<shadow> bash tests/fm-nm-test-contract.test.sh   # script run directly
not ok - could not parse .no-mistakes.yaml as YAML (needs python3 with PyYAML, or ruby with psych)
exit=1
Evidence: tests/fm-test-run.test.sh full run (new selector case green)

Source: tests/fm-test-run.test.sh full run (new selector case green)

$ bin/fm-test-run.sh tests/fm-test-run.test.sh
FM_TEST_BEGIN 2026-09-12T17:37:19Z tests/fm-test-run.test.sh family=pure-contract-unit expected_gate_skip=none
ok - exact suite coverage: --all lists every tests/*.test.sh once
ok - family selection returns a proper subset of the suite
ok - single-script selection lists exactly that path
ok - changed-file selection stays conservative (never silent full suite)
ok - a task marker refuses execution in the primary checkout and leaves worktrees and inspection alone
ok - runner and its documentation surfaces select their curated family, not just their contract owners
ok - shell line-ending policy selects runner coverage
ok - no-mistakes config change selects its contract test
fm-test-run: no tests selected for changes vs HEAD (map is conservative; use --all for the complete suite)
ok - changed selection covers dependents, fails closed for live unmapped source, and accepts retired unconsumed source
ok - a bin reference selects the referencing scripts, and consumers still select their curated families
ok - changed defaults to bounded automatic scheduling with serial override
ok - Windows emulation exempts only synthetic POSIX modes
ok - a plain script list defaults to bounded automatic concurrency without an automatic timeout
ok - family proofs run concurrently only within separate family phases
ok - empty changed selection emits deterministic text and JSON summaries
ok - timing markers and JSON artifact are valid
ok - aggregate exit reflects any script failure
ok - gate-skip accounting is honest and non-failing
ok - a gate skip records why it skipped
ok - a script that actually ran records no skip reason
ok - live guards are recorded as a capability class, not a bare env opt-in
ok - fail-on-gate-skip converts herdr-not-found into a hard failure
ok - exclude-family drops the named primary family after selection
ok - portable shard union, disjointness, and coverage guard hold
ok - portable serial shards are a deterministic disjoint cover of the serial lane
ok - coverage guard reports and bounds the unmeasured portable serial share
ok - portable serial shard lanes refuse mismatched, out-of-range, and countless names
ok - --jobs refuses non-proven / stateful selections
ok - --jobs admits and schedules a family with a recorded concurrent proof
ok - an unclassified new test stays serial while the proven residual family runs concurrently
ok - a concurrent run starts the longest-hint script first
ok - --per-script-timeout-secs turns a hung script into a bounded failure
ok - --max-wall-ms fails an over-budget run and refuses a malformed budget
ok - jobs scheduler runs proven scripts; failure propagates; non-proven refused
ok - Herdr CI family-run step times out at 20 min under a 75 min job backstop
ok - aggregate-json merges lane timing artifacts
FM_TEST_END 2026-09-12T17:42:42Z tests/fm-test-run.test.sh exit=0 duration_ms=322477 gate_skip=false
FM_TEST_SUMMARY total=1 failed=0 skipped_gate=0 duration_ms=322649
FM_TEST_SUMMARY_FAMILY family=pure-contract-unit count=1 duration_ms=322477 failed=0
FM_TEST_SLOWEST rank=1 script=tests/fm-test-run.test.sh duration_ms=322477
exit=0
Evidence: Documentation audience contract after the prose edits

Source: Documentation audience contract after the prose edits

$ bin/fm-test-run.sh tests/fm-documentation-audiences.test.sh
FM_TEST_BEGIN 2026-09-12T17:40:12Z tests/fm-documentation-audiences.test.sh family=pure-contract-unit expected_gate_skip=none
ok - documentation inventory classifies every maintained prose surface exactly once
ok - classification, setup routing, and maintained-prose scope fail safely
ok - required documentation owner pointers cannot silently disappear
ok - local links resolve while dates, versions, commands, and incident prose remain semantically reviewed
FM_TEST_END 2026-09-12T17:40:15Z tests/fm-documentation-audiences.test.sh exit=0 duration_ms=2721 gate_skip=false
FM_TEST_SUMMARY total=1 failed=0 skipped_gate=0 duration_ms=2940
FM_TEST_SUMMARY_FAMILY family=pure-contract-unit count=1 duration_ms=2721 failed=0
FM_TEST_SLOWEST rank=1 script=tests/fm-documentation-audiences.test.sh duration_ms=2721
exit=0
Evidence: Shared fm_yaml_to_json helper across both consumers

Source: Shared fm_yaml_to_json helper across both consumers

# the same shared helper backs the ci.yml timeout case in tests/fm-test-run.test.sh
$ bash -c ". tests/lib.sh; fm_yaml_to_json .github/workflows/ci.yml | head -c 120"
{"name": "CI", "true": {"push": {"branches": ["main"]}, "pull_request": {"branches": ["main"]}}, "permissions": {"conten

$ PYTHONPATH=<shadow> bash -c ". tests/lib.sh; fm_yaml_to_json .github/workflows/ci.yml; echo rc=$?"
rc=127
Evidence: Guard fires on a removed runbook
ok - no-mistakes does not configure commands.test
not ok - test.instructions must stay present and non-empty so the test analyzer keeps its live-evidence runbook; test.instructions is NoneType rather than a string
FM_TEST_END ... exit=1 gate_skip=false

Pipeline

Updates from git push no-mistakes

... (13 earlier update rounds omitted to keep the PR body within GitHub's 65536-char limit; full history is in the run log.)

🔧 **Review** - 4 issues found → auto-fixed (11) ✅

🔧 Fix applied.
3 issues (2 warnings, 1 info) still open:

  • ⚠️ .no-mistakes.yaml:76 - The live-evidence rule's own example set contains a case that is not a subprocess drive, and the voice suite matches that example while never running the entry point as a process. Lines 72-77 state the rule as "Only the test case that runs such an entry point as a subprocess drove the product - directly, or through a child contract executable the case launches", then give two illustrations: tests/fm-inbox-conversation.test.sh -> tests/fm-inbox-conversation-cases.py, "and as the Pi extension suites drive a .pi/extensions/.ts extension through the real installed pi package". The first illustration really is a subprocess drive (tests/fm-inbox-conversation-cases.py:19 does subprocess.run([str(cli), 'conversation', command]) against root/'bin/fm-inbox.sh'). The second is not: a .ts extension is never a subprocess, it is loaded in-process by the pi binary. So the runbook establishes that an entry point loaded in-process by a real process the case launched still counts as live. Concrete reachable sequence on exactly the surface the intent unblocks: the voice plan changes bin/fm-voice-relay.py. families_for_changed_path routes bin/ through families_for_unmapped_bin (bin/fm-test-run.sh:1461-1465), selecting tests/fm-voice-relay.test.sh. Most of its relay cases launch an inline python3 - &#34;$ROOT/bin&#34; &lt;&lt;&#39;PY&#39; child and then IMPORT the relay - tests/fm-voice-relay.test.sh:193-215 does importlib.util.spec_from_file_location("relay", .../fm-voice-relay.py), spec.loader.exec_module(relay), and calls relay.read_uplink_frame(reader) directly. Shape-wise this is indistinguishable from the fm-inbox-conversation illustration (a case launching a child python contract executable) and matches the Pi illustration exactly (entry point loaded in-process by a real launched process), so an analyzer reports the uplink scenario "pass" with "live": true. The relay process was never started; only an imported function ran - which line 66 ("not importing a function") and line 110-112 (a case that "sources a library and feeds it recorded strings ... proves the classifier's logic, not the live surface") both forbid. Same runbook, same transcript, opposite labels, and neither path errors. This is a defect inside the component the change introduces, so the smallest honest remedy is a wording correction, not new machinery: say that the entry point itself must be started as a process by the case or by a child the case launches, and name the Pi case as the single explicit exception (the extension is loaded by the real shipped pi harness, not by a test harness). Flagged ask-user because the live-evidence policy wording is the author's deliberate call and the user has already iterated on this exact sentence twice.
  • ⚠️ .no-mistakes.yaml:55 - The product enumeration is wrong in both directions, and the wrong half lands on the voice surface the intent names as blocked. Lines 53-60 define the product as "the bin/ commands whatever language each is written in (bin/fm-.sh, bin/backends/.sh and .py, the bin/fm_.py modules such as bin/fm_inbox_conversation.py, bin/fm_voice_frame.py and bin/fm_voice_records.py, bin/fm-extension.mjs and bin/fm-procevent-extension-capture.pl) and the .pi/extensions/.ts editor extensions". Over-inclusive: bin/fm_voice_frame.py is mode -rw-rw-r--, has no shebang and no main(), and its own docstring calls it "the wire format between the voice client and the relay" - it can only ever be imported, never run as a process, so calling it a "bin/ command" contradicts line 64-66 ("driving a scenario end-to-end here means running those real entry points as real processes ... not importing a function") and reinforces the false-live path in the finding above. Under-inclusive: the repository's actual executable non-.sh entry points bin/fm-voice-client.py, bin/fm-voice-relay.py, bin/fm-mail.py, bin/fm-arm-command-policy.mjs, bin/fm-cd-command-policy.mjs and bin/fm-extension-launch-barrier.mjs are all mode -rwxrwxr-x and match NONE of the listed globs (bin/fm-.sh excludes .py; bin/fm_*.py uses an underscore and does not match fm-voice-relay.py; only fm-extension.mjs is named, not the other three .mjs files). Concrete sequence: the voice plan changes bin/fm-voice-relay.py; an analyzer applying the enumeration literally - the exact failure mode the prior round corrected at the live-test line - finds the relay in no named product category and cannot classify tests/fm-voice-relay.test.sh:347 (python3 &#34;$ROOT/bin/fm-voice-relay.py&#34; --help, a genuine process drive) as a product drive. The one-sentence principle at lines 60-61 is what rescues it, which means the enumeration is actively working against the principle it is supposed to illustrate. Remedy is the author's product-definition call: either name the runnable entry points by their real names and drop bin/fm_voice_frame.py from the entry-point list (it is a library the entry points import), or drop the parenthetical enumeration entirely and keep only "being a shipped first-party entry point is the test, not the language". Note the prior round's instruction explicitly listed bin/fm_voice_frame.py, but that instruction's governing principle was "every first-party entry point the repository ships" - that this file is not runnable, and that the two voice processes are missing, is new information.
  • ℹ️ .github/workflows/ci.yml:242 - The change adds three copies of the same PyYAML assertion; the copy in tests-herdr is inert. That job's only suite step runs bin/fm-test-run.sh --family real-herdr-gated (.github/workflows/ci.yml:313-322), and neither YAML-parsing script is in that family: tests/fm-test-run.test.sh resolves to pure-contract-unit (bin/fm-test-run.sh:275) and tests/fm-nm-test-contract.test.sh is unmatched by family_for_basename, so it falls to the unclassified remainder that list_portable_serial collects. The two copies that actually enforce the guarantee are the new steps at lines 67-73 (tests-portable-parallel-1, which runs tests/fm-test-run.test.sh via list_portable_parallel_1) and lines 176-182 (tests-portable-serial, which runs tests/fm-nm-test-contract.test.sh). Harmless, but it is a third copy of a rule in a lane that can never exercise it. Worth surfacing because the prior round's instruction said to add the assertion to "the existing CI required-tools preflight" on the assumption that step was shared across lanes - it is herdr-only, which is why the fixer also had to add the two lane-level copies. Removing the herdr copy is the simplification; keeping it is the author's call since it was explicitly requested.

🔧 Fix applied.
1 warning still open:

  • ⚠️ .no-mistakes.yaml:80 - The Pi carve-out names a drive that does not exist in this suite, and its nearest real counterpart is the exact pattern the next sentence forbids. Line 80-81 says a live drive includes "a .pi/extensions extension loaded by the real installed pi binary, because being loaded by pi is how that extension ships and runs", and lines 81-84 then say importing a module into the test's own interpreter does NOT qualify "even when the interpreter is a child process". No test in this repository loads an extension through the pi binary. tests/fm-pi-primary-types.test.sh is a tsc --noEmit typecheck: it symlinks the installed package's declarations into a temp tsconfig project (lines 43-47) and never executes anything. tests/fm-pi-watch-extension.test.sh writes a stub node_modules/@earendil-works/pi-coding-agent (lines 43-52) and runs node --input-type=module with const mod = await import(pathToFileURL(process.env.PLUGIN).href) (lines 89-107, 167-181, 234-248) against a hand-rolled pi object. tests/fm-calm-pi-extension.test.sh is the dangerous one: it symlinks the REAL installed @earendil-works/pi-coding-agent into the fixture's node_modules (line 175), prints its version via node -p &#34;require(&#39;$PI_PACKAGE_DIR/package.json&#39;).version&#34; (line 160), then still does const extension = await import(...) in a plain node process with a fabricated pi (lines 184-196). Concrete reachable sequence: a change to .pi/extensions/fm-branch-supervision.ts or lib/fm-calm-visibility.ts routes through bin/fm-test-run.sh:1299-1314, which selects fm-pi-branch-extension, fm-pi-watch-extension, fm-calm-pi-extension, fm-pi-primary-types plus live-harness-optin (which gate-skips without opt-in). The only non-skipped evidence available for a Pi extension scenario is therefore the fake-host import. The analyst reads the transcript's ok - ... lines, reads the case, sees the real installed pi package resolved in node_modules, and applies line 80 verbatim: scenario reported "pass" with "live": true, when the pi runtime never started and the host object was a fake. No error is raised; the label is simply wrong, and it is wrong in the same direction the whole runbook exists to prevent. The defect lives inside a component the change introduces, so the smallest honest remedy is to correct the sentence rather than add machinery: either drop the Pi carve-out (no case here satisfies it, so it can only ever be misapplied), or state the condition in terms the analyst can check against the transcript - the pi executable itself appears in the command the case ran - and say explicitly that loading a .pi/extensions file with node and supplying a hand-written pi object is NOT live, because that is what every Pi suite in this repository actually does. Flagged ask-user rather than auto-fix because the user explicitly directed this sentence in the previous round ("A TypeScript editor extension loaded in-process by the real installed pi binary qualifies because that is how it ships and runs. State both explicitly."); that the suites contain no such case, and that fm-calm-pi-extension.test.sh links the real package while still faking the host, is new information that the instruction was not written against.

🔧 Fix applied.
3 issues (2 warnings, 1 info) still open:

  • ⚠️ .no-mistakes.yaml:97 - The runbook states flatly that every "ok -" line proves a drive, and this change ships a counterexample in the same commit. Lines 97-99 say: "Each &fix(watcher): make check wakes lossless via watcher-side suppression kunchenguid/firstmate#34;ok - <behavior>&fix(watcher): make check wakes lossless via watcher-side suppression kunchenguid/firstmate#34; line, and each &fix(watcher): make check wakes lossless via watcher-side suppression kunchenguid/firstmate#34;PASS: <behavior>&fix(watcher): make check wakes lossless via watcher-side suppression kunchenguid/firstmate#34; line a child contract executable prints through its owning .test.sh wrapper, names a behavior that was driven". That contradicts line 85 in the same runbook ("the transcript proves the case ran and its named assertion held, never that it launched anything") and lines 119-125 (a case that "parses a declarative artifact - a YAML, JSON or config file - without running its consumer" stays untested). The new tests/fm-nm-test-contract.test.sh is exactly that case: it calls yaml.safe_load on .no-mistakes.yaml, launches no product surface at all, and prints "ok - no-mistakes does not configure commands.test" and "ok - no-mistakes carries a non-empty test.instructions runbook". Concrete reachable sequence: a later change edits .no-mistakes.yaml (this repo edits it routinely); the new mapping at bin/fm-test-run.sh:1432 selects tests/fm-nm-test-contract.test.sh; the transcript shows two "ok -" lines plus "FM_TEST_END ... exit=0 ... gate_skip=false"; the analyzer applies line 97-99 literally and reports that scenario "pass" with "live": true, when the gate - the actual consumer of that config - was never launched. No error is raised; only the label is wrong, and it is wrong in the one direction this entire runbook exists to prevent. The surrounding sentences do rescue a careful reader, but the runbook is prose consumed by an LLM, so a single unqualified sentence licensing the wrong verdict is the defect. Smallest honest remedy is a wording narrowing, not new machinery: say the "ok -" and "PASS:" lines name an assertion the case made, and that whether it was driven is settled only by reading the case, as line 83-87 already requires. Flagged ask-user because it edits prose the author and prior rounds deliberately shaped.
  • ⚠️ tests/fm-test-run.test.sh:1511 - A fix round rewrote an existing, unrelated test's parser and introduced a new third-party Python dependency for the portable suite. test_herdr_ci_family_run_has_a_step_timeout has nothing to do with test.instructions - it asserts the tests-herdr job/step timeouts - yet commit 78a61e6 replaced its working ruby/psych parse with python3 + PyYAML, and the two ci.yml preflight steps (lines 67-73, 176-182) exist only to support that port plus the new contract test. Nothing in the intent ("the repo's .no-mistakes.yaml has no test.instructions ... the analyzer simply has no instruction telling it so") requires touching that test or adding a CI dependency assertion. The port is also a portability regression on a platform this repo treats as first class: ruby with psych ships with macOS, PyYAML does not ship with macOS's system python3, and both scripts hard-fail rather than skip. Concrete sequence: a contributor on a stock Mac edits .github/workflows/ci.yml or .no-mistakes.yaml, runs the documented bin/fm-test-run.sh --changed (CONTRIBUTING.md:85), and gets "not ok - python3 with PyYAML is required to parse .github/workflows/ci.yml as YAML" on a test that passed before this branch - with no CI lane able to catch it, since the macos-stock-bash job runs neither script. CONTRIBUTING.md:120 states the portable suite stays safe on machines without optional tools. Because the defect lives in machinery a prior fix round added beyond what the original finding required, the smallest honest remedy is to revert that round to the minimal fix: restore the base ruby parse in test_herdr_ci_family_run_has_a_step_timeout (and drop the parallel-1 preflight it needs), keeping only the parser the new test.instructions guard actually requires. The alternative - keeping PyYAML and declaring it a documented prerequisite - is a dependency-policy call, which is why this is ask-user rather than auto-fix. Note the round-7 instruction endorsed "the two lane-level assertions"; that the swap trades a macOS-bundled interpreter for one that is not bundled is new information that instruction was not written against.
  • ℹ️ bin/fm-test-run.sh:1432 - The new script mapping sits in the shared .github/workflows/ci.yml|.no-mistakes.yaml case arm, so editing ci.yml also selects tests/fm-nm-test-contract.test.sh, which only ever opens .no-mistakes.yaml. The narrower form that satisfies the intent is a separate .no-mistakes.yaml) arm carrying the script entry, leaving ci.yml with pure-contract-unit (which already picks up tests/fm-test-run.test.sh, the script that does assert on ci.yml). Cost today is a fraction of a second of extra test time, so this is informational rather than something to act on.

🔧 Fix applied.
2 issues (1 error, 1 warning) still open:

  • 🚨 tests/fm-nm-test-contract.test.sh:11 - The round-9 revert to Ruby/Psych makes this test unrunnable on the host that runs this pipeline. Verified on this machine (hermes, Linux): command -v ruby finds nothing - no /usr/bin/ruby, no rbenv/rvm/snap/homebrew ruby, nothing in a login shell - while python3 -c &#39;import yaml&#39; reports PyYAML 6.0.3. So line 11 fires fail &#34;ruby is required to parse .no-mistakes.yaml as YAML&#34;, the script prints "not ok" and exits 1.

Concrete reachable sequence introduced by this change: the new mapping at bin/fm-test-run.sh:1432 adds __script__:fm-nm-test-contract.test.sh to the .github/workflows/ci.yml|.no-mistakes.yaml arm; this change itself edits .no-mistakes.yaml; so bin/fm-test-run.sh --changed (the path CONTRIBUTING.md:85 documents and this run's test step would take) now selects tests/fm-nm-test-contract.test.sh and it hard-fails. At base that script was family unclassified and no mapping selected it, so this selection is new. tests/fm-test-run.test.sh:1511 fails identically (it is pure-contract-unit, already selected for these paths), so the changed-file run goes red with two failures on the gate host.

This is not a portability nicety: CONTRIBUTING.md:120 states the portable suite "remains safe on machines without those tools", no document lists ruby as a prerequisite, and the only two ruby call sites in the whole repo are these. GitHub's ubuntu-latest and macOS runners ship ruby, so CI stays green and nothing catches this.

The defect lives in machinery a prior fix round rewrote: commit 1750abf replaced a working python3+PyYAML implementation with ruby to satisfy the round-9 instruction "fully revert the PyYAML port ... no new third-party dependency or preflight should remain". That instruction's premise - that ruby is the interpreter this repo can count on - does not hold on the gate host, which is new information it was not written against. Choosing the remedy is a dependency-policy decision, not a mechanical correction, which is why this is ask-user rather than auto-fix: the options are (a) install ruby on the fleet hosts and document it as a suite prerequisite, (b) go back to python3+PyYAML and accept it as a documented prerequisite, or (c) accept either parser with a hard fail when neither is present. Any of the three keeps the round-9 requirement that parser absence hard-fails and never skips green.

  • ⚠️ .no-mistakes.yaml:59 - Lines 59-60 keep "the agent-facing contracts in AGENTS.md, .agents/skills/ and docs/" in the list of "product entry points", but the very next rule defines driving as "running those real entry points as real processes against a throwaway FM_HOME" (lines 66-68) and "exercised the shipped artefact through the real runtime that actually runs it, in a process the case launched" (lines 74-76). A markdown contract has no process runtime, so the definition is incoherent for exactly the members this sentence adds - and the incoherence resolves in the unsafe direction.

Concrete reachable sequence, once this runbook is on the default branch: a change edits .agents/skills/harness-adapters/SKILL.md; bin/fm-test-run.sh:1422 maps that path to pure-contract-unit, which includes tests/fm-harness-adapter-references.test.sh. That case launches no product process at all - it awk-extracts a ```json harness-adapter-routing-v1 fence out of the SKILL.md, shape-checks it with jq, and prints "ok - harness adapter routing artifact is normalized and every target is readable". The analyzer has just been told this SKILL.md IS a product entry point and that "THE SUITES IN tests/ ARE THAT DRIVE". The refused-boundary clause that would catch it names only "a declarative artifact - a YAML, JSON or config file" (lines 119-121), which a markdown skill file is not. Result: the scenario is reported "pass" with "live": true on evidence that never started the consumer, no error raised, wrong label in the one direction this runbook exists to prevent - the same defect round 9 fixed for the "ok -" line, reopened for prose.

This clause is the surviving half of the "AGENT-FACING PROSE IS A REAL SURFACE HERE" section that commit 5fa1db7 already deleted from this runbook. Nothing in the intent needs it: the blocked changes it names (afk-inject-wedge-claude-pane, fv-13) are code changes whose real suites drive bin/ entry points. The smallest honest remedy is removing the component, not hardening it - drop "and the agent-facing contracts in AGENTS.md, .agents/skills/ and docs/" from the entry-point sentence, so a prose-only change falls back to the gate's built-in policy, which already has a "no-surface" verdict for docs-only changes. Flagged ask-user because this sentence is author-written prose (present since 5cef065) that states deliberate product policy.

🔧 Fix applied.
3 issues (2 warnings, 1 info) still open:

  • ⚠️ bin/fm-test-run.sh:1432 - The new __script__:fm-nm-test-contract.test.sh entry is a real behavior change to the changed-file selector, and nothing asserts it. grep -n &#34;no-mistakes.yaml&#34; tests/fm-test-run.test.sh returns nothing, so no case covers this arm.

Why that matters concretely rather than as generic coverage advice: __script__ names are resolved at bin/fm-test-run.sh:1536 by if [ -f &#34;tests/$script_name&#34; ]; then add_script ...; fi - with no else. A name that does not resolve is silently dropped. So if tests/fm-nm-test-contract.test.sh is ever renamed (a plausible move: it is the only fm-nm-* script and is family unclassified, so it is not pinned by any family table, lane, or coverage guard), bin/fm-test-run.sh --changed on a .no-mistakes.yaml edit silently stops selecting the contract test, exits 0, prints no warning, and the guard this change just added quietly stops running. That is a wrong selection set produced without erroring, which is exactly the failure mode the sibling __unmapped__: branch at line 1504 raises a die for.

The repo already has the pattern and it is cheap to follow: tests/fm-test-run.test.sh:242-268 and :274-284 assert mapping arms by writing to a path in the fixture repo and checking bin/fm-test-run.sh --list --changed --base HEAD output. Remedy: add fm-nm-test-contract.test.sh to the fixture script list in init_changed_fixture_repo (tests/fm-test-run.test.sh:96-121), touch .no-mistakes.yaml in that fixture, and assert the listing contains tests/fm-nm-test-contract.test.sh. That is an assertion over the runner's own executed output, not over source text, so it fits the repo's test-quality rule.

  • ⚠️ tests/fm-nm-test-contract.test.sh:11 - The round-10 fix introduced yaml_to_json() byte-identically in two places: tests/fm-nm-test-contract.test.sh:11-20 and tests/fm-test-run.test.sh:1507-1516. Both files already source tests/lib.sh, and CONTRIBUTING.md states plainly that "Shared test helpers live in tests/lib.sh (reporters, temp roots, git fixtures)" and "Source those instead of copying a fake toolchain into a new suite." So there is an immediately competing semantic owner named by the repo's own instructions, not an abstraction preference.

These two copies are also the repository's only YAML parse sites (grep -rln &#39;YAML.load_file\|yaml.safe_load&#39; tests/ bin/ returns exactly these two files), so the parser-selection policy this change just established now has two hand-synchronized definitions. The next change to that policy - the third one in as many rounds - has to find and edit both, and a divergence produces two different answers to "is this host able to parse YAML" with no test catching the drift.

Remedy is mechanical and non-user-visible: move the single definition into tests/lib.sh next to the other shared primitives and delete both copies. No behavior changes.

  • ℹ️ tests/fm-nm-test-contract.test.sh:25 - The parser selection is only half portable, and the failure message does not say so. yaml_to_json falls back to ruby -ryaml -rjson when PyYAML is missing, but the value-extraction stage that consumes its output is unconditionally python3 (tests/fm-nm-test-contract.test.sh:26-34 and :48-64, tests/fm-test-run.test.sh:1525-1544). So on a host with ruby but no python3 at all, yaml_to_json succeeds, the pipe to python3 -c fails with 127, and the case dies with "failed to read commands.test from the parsed .no-mistakes.yaml" - not the message at line 25 that names both parsers. The fallback advertised as "or ruby with psych" cannot actually carry the case alone.

Recording this as informational rather than actionable because python3 is already a de-facto hard dependency of this suite: bin/fm-test-run.sh:1607 dies with "--json requires python3" (the mode the gate's evidence artifact uses), line 1025 does the same for --aggregate-json, and 25 of the tests/*.test.sh scripts invoke python3 directly. A host that cannot run python3 cannot run this suite at all, so the ruby branch is correctly scoped to the case it was asked to cover - python3 present, PyYAML absent - and that case works. The only inaccuracy is the wording of the error, and the hard-fail-never-skip requirement holds on every path. Noting it so the "portable parser selection" claim is not read as broader than it is.

🔧 Fix applied.
✅ Re-checked - no issues remain.

✅ **Test** - passed

✅ No issues found.

  • Live validation: ✅ go - 7 of 9 scenarios driven live against the product
Scenario Result Live Evidence
Editing .no-mistakes.yaml makes the changed-file runner select the contract that guards it ✅ pass live 02-nm-only-change-selection.txt: in an isolated copy of HEAD with only .no-mistakes.yaml modified, bin/fm-test-run.sh --list --changed --base HEAD lists tests/fm-nm-test-contract.test.sh; 01-chang…
The contract guard passes on this branch's config (no commands.test, runbook present) ✅ pass live 03-contract-test-pass.txt: bin/fm-test-run.sh tests/fm-nm-test-contract.test.sh prints ok - no-mistakes does not configure commands.test and `ok - no-mistakes carries a non-empty test.instructions…
Adversarial: deleting, blanking, or mistyping test.instructions fails the guard instead of passing silently ✅ pass live 04-contract-guard-adversarial.txt cases A/B/C: key removed -> test.instructions is NoneType rather than a string; whitespace-only -> test.instructions is empty; YAML list -> `test.instructions is…
Adversarial: reintroducing a deterministic commands.test suite walk is rejected ✅ pass live 04-contract-guard-adversarial.txt case D: not ok - commands.test must be absent or empty so Test stays intent-targeted; got: &#39;bin/fm-test-run.sh --all&#39;, exit=1
Adversarial: a host with no YAML parser hard-fails the suite rather than skipping green ✅ pass live 05-parser-absent-hard-fail.txt: ruby genuinely absent on this host and PyYAML shadowed by an ImportError package; the case prints `not ok - could not parse .no-mistakes.yaml as YAML (needs python3 wit…
The runner's own suite stays green, including the new selector case and the ci.yml case moved onto the shared helper ✅ pass live 06-runner-suite.txt: ok - no-mistakes config change selects its contract test and ok - Herdr CI family-run step times out at 20 min under a 75 min job backstop, FM_TEST_END exit=0
Changed documentation still satisfies the repository's documentation inventory contract ✅ pass live 07-docs-audiences.txt: bin/fm-test-run.sh tests/fm-documentation-audiences.test.sh exit=0 with all four ok lines after the docs/configuration.md and SKILL.md edits
PyYAML absent but ruby present: the Psych fallback parses the config ⏸️ untested no No ruby interpreter exists on this host and installing system packages is outside this worktree's boundary. To drive it, install ruby (its bundled psych) on the gate host, or run this step on a host t…
The gate's test analyzer receives the runbook and stops refusing suite-proven passes ⏸️ untested no This is the gate pipeline itself, which this test phase is explicitly forbidden to invoke (no init, axi run, or rerun), and the runbook is trusted only from the default branch, so it cannot take effec…
  • bin/fm-test-run.sh --list --changed --base d859c15 in the branch worktree (real changed set includes tests/fm-nm-test-contract.test.sh)
  • bin/fm-test-run.sh --list --changed --base HEAD in an isolated copy of HEAD where only .no-mistakes.yaml was edited
  • bin/fm-test-run.sh tests/fm-nm-test-contract.test.sh on the branch config (both ok lines)
  • Adversarial mutations in the isolated copy, each re-run through bin/fm-test-run.sh tests/fm-nm-test-contract.test.sh: test.instructions key removed; whitespace-only; replaced by a YAML list; commands.test: &#39;bin/fm-test-run.sh --all&#39; reintroduced
  • PYTHONPATH=&lt;yaml-shadow&gt; bin/fm-test-run.sh tests/fm-nm-test-contract.test.sh and the same script run directly, with ruby genuinely absent from the host (no-parser hard-fail path)
  • bin/fm-test-run.sh tests/fm-test-run.test.sh (includes the new test_nm_config_change_selects_its_contract_test fixture case and the ci.yml timeout case that now uses the shared helper)
  • bin/fm-test-run.sh tests/fm-documentation-audiences.test.sh after the docs/configuration.md and SKILL.md prose edits
  • bash -c &#39;. tests/lib.sh; fm_yaml_to_json .github/workflows/ci.yml&#39; with and without PyYAML available (rc=127 when no parser)
  • Host capability probe: command -v ruby (absent), python3 -c &#39;import yaml&#39; (PyYAML 6.0.3)
⚠️ **Document** - 1 info
  • ℹ️ .no-mistakes.yaml:51 - The new test.instructions runbook hand-enumerates product entry points (bin/fm-voice-client.py, bin/fm-voice-relay.py, bin/fm-mail.py, bin/fm-arm-command-policy.mjs, bin/fm-cd-command-policy.mjs, bin/fm-extension-launch-barrier.mjs) while docs/scripts.md is the authoritative inventory of bin/ scripts and their roles. The two already disagree: docs/scripts.md:76 lists bin/fm-extension.mjs as a runnable entry point ("Bind, inspect, verify, and strictly invoke trusted external process-event adapter packages") and bin/fm_inbox_conversation.py as the transport behind fm-inbox.sh conversation, and neither appears in the runbook's list. The list is principle-first ("Being something the fleet runs is the test, not the language"), so the omission does not misdirect the analyzer today, and .no-mistakes.yaml is executable configuration rather than a classified prose surface (docs/documentation-audiences.json scope), so its wording is config-owned and out of this phase's scope - especially since the runbook's exact prose was tuned by explicit user decisions across review rounds 9-11. Left unchanged deliberately. Follow-up worth considering: replace the enumeration with the principle plus a pointer to docs/scripts.md as the inventory owner, so a new non-shell entry point cannot silently fall outside the runbook's examples.
✅ **Lint** - passed

✅ No issues found.

✅ **Push** - passed

✅ No issues found.

CI shard timing

This change adds tests/fm-nm-test-contract.test.sh to the portable-serial remainder. Its measured runner duration is 706 ms; the final Behavior portable serial 1 job passed in 22m58s, so this PR adds about 0.05% of that shard runtime. The added case is a measurable contributor, but it does not explain the shard timeout pattern tracked separately.

@Mauryanx
Mauryanx merged commit 95b1ff9 into main Sep 12, 2026
39 of 43 checks passed
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