Skip to content

feat(bin): merge upstream/main into the fork - #23

Merged
onyx-space merged 38 commits into
mainfrom
fm/fork-sync-conflict-firstmate
Sep 14, 2026
Merged

onyx-space merged 38 commits into
mainfrom
fm/fork-sync-conflict-firstmate

Conversation

@onyx-space

Copy link
Copy Markdown
Owner

Intent

队长原话:「0201全派」。本任务是今天 18:00 fork 同步里被跳过的三条冲突之一。

现状(实测 2026-09-14):~/code/firstmate 与 upstream(kunchenguid/firstmate)合并时冲突,脚本按纪律已自动 abort、主检出未被改动(仍在 main ddb70a6c、干净)。冲突文件:.github/workflows/ci.yml / AGENTS.md / bin/fm-backlog-transition-lib.sh / bin/fm-pr-merge.sh;落后 upstream 约 141 提交。

要点:本仓就是正在运行的 firstmate 主 home;改共享面必须走 no-mistakes 管线 + PR(AGENTS.md §1),不许直接改主检出。

读这份 ask 需要的事实(本次复跑冲突面后实测;与上面那句冲突面/提交数不一致的地方以实测为准):

  • 本次 git fetch upstream 到的 upstream/main 是 b182d0f9,main 是 ddb70a6c,两者合并面上有 18 个冲突文件、32 个冲突 hunk:bin/*.sh 7 个、tests/*.test.sh 6 个、docs/*.md 3 个、.github/workflows/ci.yml 1 个、AGENTS.md 1 个。上面列的 4 个文件对应 18:00 那次更早的上游状态,不是本次的实际冲突面。
  • 提交方向:upstream 领先 main 32 个提交,main 领先 upstream 141 个提交(现状里那句「落后 upstream 约 141 提交」把方向写反了)。这个 fork 与 upstream 是双向独立演进:两边在并行测试分片、teardown 池槽位归属、更新后 poll 迁移这几处各自独立改过同一批文件,所以冲突里既有「吸收上游的新实现」,也有「本地已经做了同类改造」的情形。
  • 交付物:把 upstream/main 合进 fork 并解决这 18 个文件的冲突,走本仓 no-mistakes 管线,产出一个待队长评审的 PR(不自合)。

What Changed

  • Merges upstream kunchenguid/firstmate main (32 commits) into the fork, resolving 32 conflict hunks across 18 files in bin/, tests/, docs/, .github/workflows/ci.yml, and AGENTS.md.
  • Absorbs upstream's new worker and harness capabilities — the Antigravity (agy) adapter with trust pre-registration, quiet AFK supervision, bin/fm-tasks-axi.sh, the guarded herdr foreground viewer, and merge-authority persistence — while keeping the fork's parallel test-shard packing, teardown pool-slot ownership, and post-update poll migration.
  • Adds and updates the corresponding regression suites (fm-agy-harness, fm-harness-precedence, fm-herdr-lab, fm-tasks-axi, fm-backlog-read-bound, and others) plus verification docs, and follows the merge with review fixes: dropping a dead teardown helper, correcting portable-shard doc totals, isolating the fm-update rebind fixture's world name, and aligning the CI-workflow test to the fork's 15-minute parallel caps.

Risk Assessment

⚠️ Medium: The previous round's blocking test/ci.yml contradiction is correctly resolved and no new concrete defect was found, but the change is a broad 164-file merge of 32 upstream commits whose safety-critical teardown slot-ownership logic was hand-composed rather than taken from either side, so it is safe to merge while keeping that composition under human attention.

Testing

针对运行中的产品演练了 merge 后的 fork:所需的 CI test-coverage guard 将全部 209 个 merge 后的 test scripts 在 parallel/serial/herdr lanes 中恰好各调度一次,unhinted=0,ci.yml 调用的每个 lane name 都解析为可汇总到 guard 总计的非空不相交 membership,且 runner 以干净的 BEGIN/END/SUMMARY 输出端到端执行脚本。review 要求的两个 15-minute parallel cap 对齐被双向驱动:针对 branch 的 ci.yml 为绿,将 ci.yml 降回 10 时为红,证明该断言仍具约束力。所有冲突已解决的表面都通过其随附 suite 驱动:LPT partition 加上 upstream guard report fields(40 ok)、composed teardown slot-ownership proofs(38 + 84 ok)、post-update re-exec 和 second-pass rebind(18 ok)、upstream gh-based pr-merge 加上 fork Gitea refusal(88 ok)、watcher control lock 加上 FM_HOME(117 ok)、configurable alarm knobs(36 ok)、双方的 brief cases(29 ok)、backlog bounded-read latch(10 ok)、spawn(5 ok)。invariants 和 macos-stock-bash CI job 已在本地重放并一致,包括对 119-file merge 后 shell inventory 的 stock-bash-3.2 parse sweep,以及 18/59/1 的 snapshot 消费方。4 个不可执行的冲突文件(AGENTS.md 和 3 个 docs/*.md)是纯散文且没有可执行表面;它们承载的唯一切实运行时契约 - CLAUDE.md/@AGENTS.md pointer 以及 personal-path tracking invariants - 已被实时驱动并通过。记录了一条 warning finding:文档记载的 --changed verify 命令在此 branch 上以退出码 2 退出,因为 tests/assets/board-render-harness.mjs(被吸收的 upstream commit 46ff99c 修改)在 runner 的 changed-path map 中没有规则;对 base revision 的 runner 进行对照运行也以相同方式终止,所以该缺口是原本就存在的,且 CI 不受影响。完整的 lane execution 和 remote regression suite 仍由 CI 负责,此处未运行。

  • Live validation: ✅ go - 17 of 18 scenarios driven live against the product
Scenario Result Live Evidence
运行 bin/fm-test-run.sh --check-coverage,看到每个 merge 后的 tests/*.test.sh 在 parallel、serial 和 herdr lanes 中恰好各调度一次 ✅ pass live 01-coverage-guard.txt: FM_TEST_COVERAGE ok total=209 parallel=24 parallel_unhinted=0 serial=169 serial_shards=5 serial_unhinted=21 herdr=16, rc=0
通过 runner 运行 .github/workflows/ci.yml 调用的每个 lane 和 family 名称,看到非空、不相交且可汇总到 guarded partition 的 memberships ✅ pass live 02-ci-lane-wiring.txt: parallel 13+11=24, serial 34+34+33+35+33=169, herdr 16;tests/fm-ci-workflow.test.sh 解析为 portable-serial-1of5
在 branch 上运行 tests/fm-ci-workflow.test.sh,看到 fork 的两个 15-minute parallel cap 被接受,断言旁带有 deviation 注释 ✅ pass live 03-ci-workflow-test-and-teeth.txt: 6 ok, rc=0,包括 'the already-measured lane bounds are unchanged'
对抗性地将 ci.yml 两个 parallel cap 都降回 upstream 的 10,确认对齐后的测试失败(无静默漂移) ✅ pass live 03-ci-workflow-test-and-teeth.txt: 'not ok - tests-portable-parallel-1 timeout must stay 15 minutes, got 10', rc=1;ci.yml 已恢复且 worktree 干净
在一个 merge 后的 contract test 上端到端驱动 runner,看到干净的 BEGIN/END/SUMMARY 且退出码为 0 ✅ pass live 08-runner-drives-ci-workflow.txt: FM_TEST_SUMMARY total=1 failed=0, runner rc=0
运行 tests/fm-test-run.test.sh,看到 fork 的严格 weight-table refusal 与 upstream 的 guard report fields / 5% lane balance 同时成立 ✅ pass live 04-fm-test-run.txt: 40 ok / 0 not ok, rc=0
运行 teardown safety suites,看到 composed slot-ownership proofs 拒绝触碰另一个 task 的 live worker 或 slot ✅ pass live 07-fm-teardown-endpoint-safety.txt(38 ok)and 10-fm-teardown.txt(84 ok),both rc=0
运行 tests/fm-update.test.sh,看到 post-update re-exec 和 second-pass watch rebind 都正常工作 ✅ pass live 05-fm-update.txt: 18 ok / 0 not ok, rc=0(T12-T15)
运行 tests/fm-pr-merge.test.sh,看到被吸收的 gh-based merge 路径工作,同时保留 fork 的 Gitea refusal ✅ pass live 06-fm-pr-merge.txt: 88 ok / 0 not ok, rc=0;GitHub CLI layer 由 suite 的 fakebin 伪造,product script 本身真实运行
运行 tests/fm-watch-triage.test.sh,看到 merge 后的 watcher 保留 control-lock/snapshot verification 及其 FM_HOME pr-poll 路径 ✅ pass live 11-fm-watch-triage.txt: 117 ok / 0 not ok, rc=0
运行 tests/fm-pr-check-security.test.sh,看到 fork 的 configurable alarm 与 upstream 的 configurable check timeout 都被遵守 ✅ pass live 14-fm-pr-check-security.txt: 36 ok / 0 not ok, rc=0 at the default 10s alarm
运行 tests/fm-brief.test.sh,看到冲突双方的 brief cases 都存在且通过 ✅ pass live 15-fm-brief.txt: 29 ok / 0 not ok, rc=0
运行 tests/fm-backlog-read-bound.test.sh,看到被吸收的 upstream bounded-read latch 被组合进 fork 提取出的 helper ✅ pass live 19-fm-backlog-read-bound.txt: 10 ok / 0 not ok, rc=0
运行 tests/fm-spawn-batch.test.sh,看到 spawn 冲突表面行为正常 ✅ pass live 20-fm-spawn-batch.txt: 5 ok / 0 not ok, rc=0
在 AGENTS.md merge resolution 之后,重放所需的 CI invariants job(CLAUDE.md pointer、skills symlink、no tracked personal paths) ✅ pass live 16-ci-invariants.txt: all invariants hold
用真实的 /bin/bash 3.2.57 对 merge 后的 shell inventory 重放所需的 CI macos-stock-bash parse sweep ✅ pass live 17-stock-bash-parse-sweep.txt: 119/119 files parse, parse_fail=0
在 stock bash 下重放所需的 CI macos-stock-bash snapshot 消费方,看到 CI 预期的断言计数 ✅ pass live 18-stock-bash-snapshots.txt: fleet-snapshot-view 18 ok, bearings-snapshot 59 ok, bash32 register 1 ok, all rc=0
4 个纯散文冲突文件(AGENTS.md 和 docs/agent-control.md、docs/architecture.md、docs/fm-test-portable-shards.md)解析为 fork 的措辞加上 upstream 的新增内容 ⏸️ untested no Natural-language instruction 和 documentation 文件没有可执行表面,且 test-quality contract 禁止通过检查其内容来证明 prompt/doc 文本。它们承载的唯一可机检契约(CLAUDE.md/@AGENTS.md compatibility pointer 以及 tracked-path invariants)已在 invarian…
Evidence: Coverage guard + lane inventory

Source: Coverage guard + lane inventory

=== --check-coverage ===
FM_TEST_COVERAGE ok total=209 parallel=24 parallel_max_ms=406109 parallel_imbalance_ms=42 parallel_unhinted=0 serial=169 serial_shards=5 serial_unhinted=21 herdr=16
rc=0

=== --list-lanes ===
portable-parallel-1
portable-parallel-2
portable-serial
portable-serial-1of5
portable-serial-2of5
portable-serial-3of5
portable-serial-4of5
portable-serial-5of5
real-herdr-gated
rc=0
Evidence: ci.yml lane wiring: memberships sum to the guarded partition

Source: ci.yml lane wiring: memberships sum to the guarded partition

=== lanes referenced by ci.yml (job -> lane expression) ===
tests-portable-parallel-1: --lane portable-parallel-1
tests-portable-parallel-2: --lane portable-parallel-2
tests-portable-serial: --lane "$FM_SERIAL_LANE
tests-herdr: --family real-herdr-gated

=== each ci.yml lane accepted by the runner (membership count) ===
lane=portable-parallel-1      members=13
lane=portable-parallel-2      members=11
lane=portable-serial-1of5     members=34
lane=portable-serial-2of5     members=34
lane=portable-serial-3of5     members=33
lane=portable-serial-4of5     members=35
lane=portable-serial-5of5     members=33
family=real-herdr-gated       members=16

=== which lane owns tests/fm-ci-workflow.test.sh ===
fm-ci-workflow.test.sh -> portable-serial-1of5
Evidence: Review-fix contract green, and red when ci.yml is lowered to 10 (teeth)

Source: Review-fix contract green, and red when ci.yml is lowered to 10 (teeth)

=== A. branch ci.yml (fork 15) vs aligned test ===
ok - a newer push to one PR supersedes that PR's in-flight CI
ok - distinct PRs get distinct concurrency groups
ok - every main push keeps its own group and is never cancelled
ok - every ci.yml job carries a finite timeout
ok - the incident's unbounded jobs keep their recommended caps
ok - the already-measured lane bounds are unchanged
rc=0

=== B. adversarial: lower BOTH parallel caps to upstream 10, expect the test to go red ===
replacements: 2
tests-portable-parallel-1=10
tests-portable-parallel-2=10
ok - a newer push to one PR supersedes that PR's in-flight CI
ok - distinct PRs get distinct concurrency groups
ok - every main push keeps its own group and is never cancelled
ok - every ci.yml job carries a finite timeout
ok - the incident's unbounded jobs keep their recommended caps
not ok - tests-portable-parallel-1 timeout must stay 15 minutes, got 10
rc=1 (expected non-zero)

=== restore ci.yml ===
git-status-rc=0
tests-portable-parallel-1=15
tests-portable-parallel-2=15
Evidence: Runner drives the merged ci-workflow test end to end

Source: Runner drives the merged ci-workflow test end to end

\### runner drives the merged ci-workflow contract test end to end
FM_TEST_BEGIN 2026-09-14T18:04:40Z tests/fm-ci-workflow.test.sh family=unclassified expected_gate_skip=none
ok - a newer push to one PR supersedes that PR's in-flight CI
ok - distinct PRs get distinct concurrency groups
ok - every main push keeps its own group and is never cancelled
ok - every ci.yml job carries a finite timeout
ok - the incident's unbounded jobs keep their recommended caps
ok - the already-measured lane bounds are unchanged
FM_TEST_END 2026-09-14T18:04:41Z tests/fm-ci-workflow.test.sh exit=0 duration_ms=1017 gate_skip=false
FM_TEST_SUMMARY total=1 failed=0 skipped_gate=0 duration_ms=1084
FM_TEST_SUMMARY_FAMILY family=unclassified count=1 duration_ms=1017 failed=0
FM_TEST_SLOWEST rank=1 script=tests/fm-ci-workflow.test.sh duration_ms=1017
runner rc=0
Evidence: Fork LPT refusal + upstream guard fields / 5% balance both pass

Source: Fork LPT refusal + upstream guard fields / 5% balance both pass

\### tests/fm-test-run.test.sh (fork LPT partition + upstream guard report fields)
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
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 - proven-isolated scheduling ignores parallel hints
ok - family, all, changed, and script selections ignore parallel hints
ok - portable shard union, disjointness, and coverage guard hold
ok - a missing, blank, or non-numeric parallel weight refuses the lane instead of shrinking it
ok - portable parallel lanes are fully hinted and packed within 5% of each other
ok - portable serial shards are a deterministic disjoint cover of the serial lane
ok - a malformed serial weight refuses the shard lane instead of truncating it
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
rc=0
Evidence: Composed teardown slot-ownership safety suite

Source: Composed teardown slot-ownership safety suite

\### tests/fm-teardown-endpoint-safety.test.sh (composed slot-ownership proofs)
ok - fm-teardown: missing, empty, malformed, ambiguous, and task-mismatched endpoints refuse before every mutation or runtime call
ok - fm-teardown: a concurrent lifecycle action refuses before mutation
ok - fm-teardown: non-pool cleanup ignores unrelated task publication locks
ok - fm-teardown: destructive cleanup serializes with metadata writers
ok - cleanup identity: valid tmux, Herdr, Zellij, Orca, and cmux records validate while every empty backend target refuses
ok - tmux backend: direct empty target returns nonzero without invoking tmux
ok - process cleanup: creation-time PID identity removes only the exact child and preserves the control child
ok - fm-teardown: exact tmux cleanup preserves invalid and prefix-matched neighbors while removing only the recorded target
ok - Treehouse locking resolves a bare local origin against its source project, matching the provisioned clone
ok - fm-teardown: a pool slot named by a second task record is never returned, killed, or reset
ok - fm-teardown: a pool slot is released to the live record once the other record's endpoint is provably gone
ok - fm-teardown: an aliased fm home resolves the shared-slot guard by physical path, never against itself
ok - fm-teardown: a pool slot is never released while the record sharing it is still running
ok - fm-teardown: an unreadable co-claimant endpoint still refuses rather than reading as gone
ok - fm-teardown: two gone records leave the slot contested instead of guessing which claim is current
ok - fm-teardown: shared-slot live-work proofs each refuse on their own - uncommitted copy, unpushed commit, unowned process, and an unprovable pane leader
ok - fm-teardown: the slot's current occupant collects a claim the pool's own owner record supersedes
ok - fm-teardown: the superseded side still refuses and names the slot's current occupant
ok - fm-teardown: a superseded claim releases nothing without the pool's owner record and the destruction proofs, --force included
ok - fm-teardown: an explicit retirement clears the two states no destruction proof can reach, and never without being asked
ok - fm-teardown: a retirement is recorded once, survives its record, and stops that record returning the slot
ok - fm-teardown: a stale-claim retirement is bound to the incarnation, retiring record, and slot it names
ok - fm-teardown: the retirement writer restamps a stale marker for the incarnation it retires
ok - fm-teardown: a later occupant rewrites a predecessor's stale retirement, and a repeat does not
ok - fm-teardown: a retirement is rewritten after the task moves to another slot
ok - fm-teardown: the superseded side takes --retire-superseded-claim without returning the slot
ok - fm-teardown: a forced secondmate retires a descendant claim and only the occupant returns the slot
ok - fm-teardown: forced cleanup skips the return for a descendant whose claim was retired
ok - fm-teardown: a refusal names the live record to tear down first
ok - fm-teardown: a pool slot held by another firstmate home is never returned
ok - fm-teardown: a task that solely holds its slot still returns it
ok - fm-teardown: a pool slot claimed by another task is left alone while the task's own cleanup finishes
ok - fm-teardown: a task's own slot claim, and an unclaimed slot, both still tear down
ok - fm-teardown: an exact recorded endpoint still tears down after changing cwd outside its worktree
ok - Treehouse project locking anchors at the local root for main-home, local-secondmate, and remote-seeded layouts
ok - fm-teardown: a remote-seeded secondmate home returns its own uncontested pool slot
ok - fm-teardown: slot ownership across a remote-seeded home and its local child still refuses
ok - Treehouse project locking still serializes two homes across the remote-seeded boundary
rc=0
Evidence: Upstream gh-based merge path + fork Gitea refusal

Source: Upstream gh-based merge path + fork Gitea refusal

\### tests/fm-pr-merge.test.sh (upstream gh-based merge + fork Gitea refusal)
ok - fm-pr-merge reports exact queue retry flags after a zero-exit false success
ok - fm-pr-merge omits merge-queue retry guidance for a closed GitHub PR
ok - fm-pr-merge aggregates agreeing merge-queue rules
ok - fm-pr-merge reports ambiguity for conflicting merge-queue rules
ok - fm-pr-merge records pr= and pr_head= for a verified GitHub merge
ok - fm-pr-merge records pr= before the forge call can land the merge
ok - fm-pr-merge propagates a real merge failure without silently succeeding
ok - fm-pr-merge refuses a GitHub merge call that leaves the PR open and unqueued
ok - fm-pr-merge keeps PR bookkeeping when it cannot read a successful merge call's outcome
ok - fm-pr-merge refuses with the forge's own output quoted apart from its verdict
ok - fm-pr-merge quotes the forge output when it cannot read the outcome either
ok - fm-pr-merge does not echo back queue flags the caller already used
ok - fm-pr-merge still names retry flags when the caller used a different method
ok - fm-pr-merge names the queue requirement even when its method is unrecognised
ok - fm-pr-merge distinguishes unreadable branch rules from a base with no merge queue
ok - fm-pr-merge says nothing about a merge queue when the base branch has no queue rule
ok - fm-pr-merge accepts only a proved merge from the gh-axi fallback
ok - fm-pr-merge explains an armed auto-merge that landed nothing on a queue-less base
ok - fm-pr-merge never reports auto-merge as armed when the merge command failed
ok - fm-pr-merge claims no acceptance for a failed merge command carrying queue flags
ok - fm-pr-merge falls back to the gh-axi view when gh's read fails
ok - fm-pr-merge names a landed state hiding behind a failed GitHub merge command
ok - fm-pr-merge refuses a GitHub merge when gh is missing, before recording
ok - fm-pr-merge refuses a GitHub merge when gh is missing rather than merging blind
ok - fm-pr-merge verifies a genuinely merged GitHub pull request
ok - fm-pr-merge refuses to claim a merge when poll recording fails
ok - fm-pr-merge accepts and accurately reports a GitHub merge-queue entry
ok - fm-pr-merge explains how to retry with the required GitHub merge queue method
ok - fm-pr-merge refuses branch deletion unless --attended-override is passed
ok - fm-pr-merge refuses before merging when task meta is missing
ok - fm-pr-merge refuses malformed PR URLs before calling gh-axi
ok - fm-pr-merge refuses unsafe PR URL segments before recording state
ok - fm-pr-merge refuses repo override args before recording state
ok - fm-pr-merge refuses a bundled short-option repo override and refuses -d unless attended
ok - fm-pr-merge does not add default --squash when the caller passes an explicit merge method
ok - fm-pr-merge respects --method=<value> as an explicit merge method
ok - fm-pr-merge parses a GitHub PR URL into gh-axi number and --repo arguments
ok - fm-pr-merge refuses a caller --sha on GitHub because the head comes from the live read
ok - fm-pr-merge merges a GitLab merge request through glab instead of refusing it
ok - fm-pr-merge takes the GitLab instance from the URL rather than assuming one
ok - fm-pr-merge imposes no merge method on GitLab, leaving the project's own one
ok - fm-pr-merge refuses GitLab source-branch deletion unless --attended-override is passed
ok - fm-pr-merge propagates a real glab merge failure without silently succeeding
ok - fm-pr-merge refuses on each GitLab pre-merge condition independently
ok - fm-pr-merge reports every failing GitLab condition, not only the first
ok - fm-pr-merge reports a stale recorded head and verifies the live one
ok - fm-pr-merge refuses an unreadable GitLab merge request state rather than merging blind
ok - fm-pr-merge refuses a GitLab head commit it cannot validate
ok - fm-pr-merge refuses before recording anything when glab or jq is absent
ok - fm-pr-merge refuses a GitLab head override before recording state
ok - a merge a secondmate home performs itself is reported upward exactly once
ok - a locally routed secondmate home reports the landed PR into its parent's own channel
ok - a landed GitLab merge request is reported upward on the same channel
ok - a queued GitLab merge stays silent and leaves confirmation to the armed poll
ok - a refused or failed merge reports no outcome
ok - a GitLab merge refused before the forge call reports no outcome
ok - a merge a main home performs itself leaves one durable wake naming the PR
ok - a queued GitHub merge stays silent and leaves confirmation to the armed poll
ok - distinct merged PRs for one task retain distinct captain-facing wakes
ok - an uncommitted marker retry preserves at least one durable outcome
ok - a secondmate home that cannot report upward says so instead of merging in silence
ok - fm-pr-merge proceeds when the home carries no backlog at all
ok - fm-pr-merge refuses when the backlog exists but cannot be read
ok - fm-pr-merge refuses when its configured backend cannot be read
ok - fm-pr-merge refuses when its user backend configuration cannot be read
ok - fm-pr-merge refuses when its user backend configuration directory cannot be traversed
ok - fm-pr-merge proceeds once when its user configuration directory and backlog are genuinely absent
ok - fm-pr-merge honors a backend override over an unreadable user configuration
ok - fm-pr-merge refuses red GitHub checks and waives only a named --allow-red check
ok - fm-pr-merge merges when a failed check run was replaced by a passing re-run
ok - fm-pr-merge never lets a check run supersede a legacy status context
ok - fm-pr-merge still refuses when a check's current run failed after an earlier pass
ok - fm-pr-merge uses start order when the old success finishes last
ok - fm-pr-merge supersedes an old cancellation that finishes last
ok - fm-pr-merge keeps a check red while its re-run is still in flight
ok - fm-pr-merge never lets one check's pass clear another check's failure
ok - fm-pr-merge clears a failure only on a proven later pass of the same check
ok - fm-pr-merge keeps --allow-red scoped to its named check beside a superseded failure
ok - fm-pr-merge rechecks away presence before an attended red merge
ok - fm-pr-merge accepts exactly one separately named red-check waiver
ok - away merges require yolo or a grant, and --attended-override does not skip that
ok - away posture permits immediate merges but refuses every asynchronous path
ok - a grant does not bypass red checks, and a recorded pr= must match the URL
ok - an unreadable away-posture record refuses the merge instead of skipping the grant
ok - no away-record archive or grant revocation lands between the authority read and the merge
ok - a grant revoked before the merge's own authority read refuses the merge
ok - a merge that cannot lock the away record refuses instead of merging unlocked
ok - fm-pr-merge refuses --allow-red on GitLab
rc=0
Evidence: Post-update re-exec + rebind (T12-T15)

Source: Post-update re-exec + rebind (T12-T15)

\### tests/fm-update.test.sh (post-update re-exec + procevent rebind)
ok - T1 main + secondmate fast-forward (single-parent), reread + restart signalled
ok - T3 a non-instruction advance still restarts the live secondmate
ok - T3b a bin/-only advance restarts the secondmate
ok - T3c an unverifiable secondmate receives the fallback nudge
ok - T3d an already-stopped secondmate is left to startup recovery
ok - T3e a legacy remote advance still restarts the live remote mate
ok - T4 dirty secondmate skipped, local edit preserved
ok - T5 diverged secondmate skipped, local commit preserved
ok - T6 an already-current live secondmate is still restarted
ok - T6b an already-current mate with an unprovable runtime is steered, not claimed as reloaded
ok - T7 registry backstop resolves, dedups meta+registry, excludes the firstmate repo
ok - T9 firstmate off its default branch is skipped, not forced
ok - T10 firstmate detached HEAD is skipped
ok - T11 unsafe secondmate home is not fast-forwarded
ok - T12 a mate's armed poll is re-anchored against the mate's own template
ok - T13 a registry-backstop home's armed poll is refreshed without a live window
ok - T14 the update hands the post-advance half to the on-disk copy once
ok - T15 a self-update rebinds a locally armed watch on the primary
# all fm-update tests passed
rc=0
Evidence: macos-stock-bash job replayed: bash 3.2 parse sweep + snapshot consumers

Source: macos-stock-bash job replayed: bash 3.2 parse sweep + snapshot consumers

=== CI job 'macos-stock-bash': stock /bin/bash parse sweep over the merged shell inventory ===
GNU bash, version 3.2.57(1)-release (arm64-apple-darwin25)
host bash ok
GNU bash, version 3.2.57(1)-release (arm64-apple-darwin25)
ok - jq present
inventory files: 119
parsed=119 parse_fail=0
ok - every shell file in the merged inventory parses under stock macOS bash 3.2.57
Evidence: Review-fix scope: only the test changed, ci.yml still 15 with its rationale

Source: Review-fix scope: only the test changed, ci.yml still 15 with its rationale

=== review-fix commit scope (must touch only tests/fm-ci-workflow.test.sh) ===
a27d1b5 no-mistakes(review): Align fm-ci-workflow test to fork's 15-minute parallel caps

 tests/fm-ci-workflow.test.sh | 13 +++++++++++--
 1 file changed, 11 insertions(+), 2 deletions(-)

=== .github/workflows/ci.yml is NOT modified by the review fix ===
(empty above = untouched)

=== ci.yml parallel job caps on the branch tip ===
tests-portable-parallel-1  timeout-minutes=15
tests-portable-parallel-2  timeout-minutes=15
tests-portable-serial      timeout-minutes=30
tests-herdr                timeout-minutes=75
macos-stock-bash           timeout-minutes=10
lint                       timeout-minutes=25
test-coverage              timeout-minutes=5
tests-timing-aggregate     timeout-minutes=5
invariants                 timeout-minutes=5

=== the fork rationale comment still lives on the two ci.yml cap lines ===
79:    timeout-minutes: 15
124:    timeout-minutes: 15
73:    # so 15 minutes leaves a little over 2x margin over the worst healthy wall.
75:    # held both heavyweights here and reached the previous 10-minute cap while
76:    # every script exited 0 (run 34586799603 ran 592.7 s of lane wall clock

=== worktree clean after all drives (no source mutations left) ===
status lines: 0
Evidence: Changed-path map gap reproduced on branch and on the base revision

Source: Changed-path map gap reproduced on branch and on the base revision

=== control: does the BASE mapping also die on this diff? ===
fm-test-run: no changed-test mapping for source path: tests/assets/board-render-harness.mjs
base-mapping rc=2
=== target mapping again ===
fm-test-run: no changed-test mapping for source path: tests/assets/board-render-harness.mjs
target rc=2
=== worktree clean for probe? ===
- Outcome: ⚠️ 1 warning across 1 run (24m26s)

Pipeline

Updates from git push no-mistakes

✅ **intent** - passed

✅ No issues found.

✅ **Rebase** - passed

✅ No issues found.

🔧 **Review** - 1 issue found → auto-fixed ✅
  • 🚨 tests/fm-ci-workflow.test.sh:145 - This merge adds upstream's tests/fm-ci-workflow.test.sh, whose test_measured_lanes_keep_their_existing_bounds asserts tests-portable-parallel-1 10 and tests-portable-parallel-2 10 (lines 145-146), while this branch's conflict resolution of .github/workflows/ci.yml kept the fork's timeout-minutes: 15 for both parallel jobs (ci.yml:79 and ci.yml:124, together with the fork's own rationale comment). The test loads ci.yml as YAML via ruby and fails on any mismatch, and the file is not proven-isolated or real-herdr-gated, so bin/fm-test-run.sh places it in portable-serial (169 scripts, shards 1-5) - the required tests-portable-serial CI job is therefore guaranteed red on this branch. Every other cap that test pins does match (serial 30, herdr 75, macos-stock-bash 10; lint 25, coverage/timing/invariants 5), so only these two rows are wrong. Two deliberate policies collide: the fork raised the cap 10 -> 15 in ac30b31 after the old partition reached the 10-minute cap (its comment: 592.7 s against the old 600 s cap), and upstream's test pins the 10-minute value it measured. The captain has to say which side yields: either correct the two expected values at lines 145-146 to 15 (keeps this branch's ci.yml resolution and the fork's documented margin; mechanical, no behavior change), or lower ci.yml:79/124 back to 10 and drop the fork's rationale comment. I did not pick one, and there is no third option short of deleting the assertion.

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

⚠️ **Test** - 1 warning
  • ⚠️ bin/fm-test-run.sh:1625 - 在此 branch 上实时复现:文档中记载的 changed-file 验证命令 bin/fm-test-run.sh --changed(CONTRIBUTING.md:85)以退出码 2 退出,并报错 fm-test-run: no changed-test mapping for source path: tests/assets/board-render-harness.mjs。此次 merge 携带了 upstream commit 46ff99c(fix: identify underway tasks and sort charted work kunchenguid/firstmate#4245),它修改了 tests/assets/board-render-harness.mjs,而 families_for_changed_path 中有针对 tests/.test.sh 和 tests/fixtures//* 的规则,却没有 tests/assets/* 的规则,因此 tests/*) 这个 fallback 会产出 __unmapped__ 并终止。对 base revision 自身 runner(ddb70a6)在同一次 diff 上的对照运行也以相同方式终止,所以缺失规则是原本就存在的,且已在 main 上可触发(60bedde 触碰过同一个 asset);是这个 branch 让它现在真正触发了。影响范围有限:CI 不使用 --changed(它运行的是 --lane / --family),--check-coverage 和所有 lane membership 都干净,受影响的 asset 有一个真实的消费方(tests/fm-bearings-board-render.test.sh),所以本地 verify 路径是唯一的受损方。如果 captain 需要,最小修复方案是:增加一个 tests/assets/*) case,像 tests/fixtures/*/* case 那样运行 families_for_test_reference &#34;$path&#34;,或者将 tests/assets/* 与现有的 assets/* no-op 分支并列排除。此处未改动,以保持本次 merge PR 的范围仅限于 conflict resolution。
  • Live validation: ✅ go - 17 of 18 scenarios driven live against the product
Scenario Result Live Evidence
运行 bin/fm-test-run.sh --check-coverage,看到每个 merge 后的 tests/*.test.sh 在 parallel、serial 和 herdr lanes 中恰好各调度一次 ✅ pass live 01-coverage-guard.txt: FM_TEST_COVERAGE ok total=209 parallel=24 parallel_unhinted=0 serial=169 serial_shards=5 serial_unhinted=21 herdr=16, rc=0
通过 runner 运行 .github/workflows/ci.yml 调用的每个 lane 和 family 名称,看到非空、不相交且可汇总到 guarded partition 的 memberships ✅ pass live 02-ci-lane-wiring.txt: parallel 13+11=24, serial 34+34+33+35+33=169, herdr 16;tests/fm-ci-workflow.test.sh 解析为 portable-serial-1of5
在 branch 上运行 tests/fm-ci-workflow.test.sh,看到 fork 的两个 15-minute parallel cap 被接受,断言旁带有 deviation 注释 ✅ pass live 03-ci-workflow-test-and-teeth.txt: 6 ok, rc=0,包括 'the already-measured lane bounds are unchanged'
对抗性地将 ci.yml 两个 parallel cap 都降回 upstream 的 10,确认对齐后的测试失败(无静默漂移) ✅ pass live 03-ci-workflow-test-and-teeth.txt: 'not ok - tests-portable-parallel-1 timeout must stay 15 minutes, got 10', rc=1;ci.yml 已恢复且 worktree 干净
在一个 merge 后的 contract test 上端到端驱动 runner,看到干净的 BEGIN/END/SUMMARY 且退出码为 0 ✅ pass live 08-runner-drives-ci-workflow.txt: FM_TEST_SUMMARY total=1 failed=0, runner rc=0
运行 tests/fm-test-run.test.sh,看到 fork 的严格 weight-table refusal 与 upstream 的 guard report fields / 5% lane balance 同时成立 ✅ pass live 04-fm-test-run.txt: 40 ok / 0 not ok, rc=0
运行 teardown safety suites,看到 composed slot-ownership proofs 拒绝触碰另一个 task 的 live worker 或 slot ✅ pass live 07-fm-teardown-endpoint-safety.txt(38 ok)and 10-fm-teardown.txt(84 ok),both rc=0
运行 tests/fm-update.test.sh,看到 post-update re-exec 和 second-pass watch rebind 都正常工作 ✅ pass live 05-fm-update.txt: 18 ok / 0 not ok, rc=0(T12-T15)
运行 tests/fm-pr-merge.test.sh,看到被吸收的 gh-based merge 路径工作,同时保留 fork 的 Gitea refusal ✅ pass live 06-fm-pr-merge.txt: 88 ok / 0 not ok, rc=0;GitHub CLI layer 由 suite 的 fakebin 伪造,product script 本身真实运行
运行 tests/fm-watch-triage.test.sh,看到 merge 后的 watcher 保留 control-lock/snapshot verification 及其 FM_HOME pr-poll 路径 ✅ pass live 11-fm-watch-triage.txt: 117 ok / 0 not ok, rc=0
运行 tests/fm-pr-check-security.test.sh,看到 fork 的 configurable alarm 与 upstream 的 configurable check timeout 都被遵守 ✅ pass live 14-fm-pr-check-security.txt: 36 ok / 0 not ok, rc=0 at the default 10s alarm
运行 tests/fm-brief.test.sh,看到冲突双方的 brief cases 都存在且通过 ✅ pass live 15-fm-brief.txt: 29 ok / 0 not ok, rc=0
运行 tests/fm-backlog-read-bound.test.sh,看到被吸收的 upstream bounded-read latch 被组合进 fork 提取出的 helper ✅ pass live 19-fm-backlog-read-bound.txt: 10 ok / 0 not ok, rc=0
运行 tests/fm-spawn-batch.test.sh,看到 spawn 冲突表面行为正常 ✅ pass live 20-fm-spawn-batch.txt: 5 ok / 0 not ok, rc=0
在 AGENTS.md merge resolution 之后,重放所需的 CI invariants job(CLAUDE.md pointer、skills symlink、no tracked personal paths) ✅ pass live 16-ci-invariants.txt: all invariants hold
用真实的 /bin/bash 3.2.57 对 merge 后的 shell inventory 重放所需的 CI macos-stock-bash parse sweep ✅ pass live 17-stock-bash-parse-sweep.txt: 119/119 files parse, parse_fail=0
在 stock bash 下重放所需的 CI macos-stock-bash snapshot 消费方,看到 CI 预期的断言计数 ✅ pass live 18-stock-bash-snapshots.txt: fleet-snapshot-view 18 ok, bearings-snapshot 59 ok, bash32 register 1 ok, all rc=0
4 个纯散文冲突文件(AGENTS.md 和 docs/agent-control.md、docs/architecture.md、docs/fm-test-portable-shards.md)解析为 fork 的措辞加上 upstream 的新增内容 ⏸️ untested no Natural-language instruction 和 documentation 文件没有可执行表面,且 test-quality contract 禁止通过检查其内容来证明 prompt/doc 文本。它们承载的唯一可机检契约(CLAUDE.md/@AGENTS.md compatibility pointer 以及 tracked-path invariants)已在 invarian…
  • bin/fm-test-run.sh --check-coverage -> rc=0, FM_TEST_COVERAGE ok total=209 parallel=24 parallel_max_ms=406109 parallel_imbalance_ms=42 parallel_unhinted=0 serial=169 serial_shards=5 serial_unhinted=21 herdr=16
  • bin/fm-test-run.sh --list-lanes 加上 .github/workflows/ci.yml 调用的每个 lane/family 的 membership 计数:portable-parallel-1=13, portable-parallel-2=11, portable-serial-{1..5}of5=34/34/33/35/33, real-herdr-gated=16(13+11=24, serial 合计 169, herdr 16)
  • 用 ruby -ryaml 提取 ci.yml 引用的 lanes,然后对每一个运行 bin/fm-test-run.sh --lane &lt;name&gt; --list;确认 tests/fm-ci-workflow.test.sh 落在 portable-serial-1of5
  • bash tests/fm-ci-workflow.test.sh -> rc=0(6 ok),包括两个 fork 15-minute parallel cap 以及其他未变更的行(serial 30, herdr 75, macos-stock-bash 10;lint 25, coverage/timing/invariants 5)
  • 对抗性测试:通过 python rewrite 将 ci.yml 中的两个 parallel cap 都降到 upstream 的 10,重新运行 bash tests/fm-ci-workflow.test.sh -> not ok - tests-portable-parallel-1 timeout must stay 15 minutes, got 10,rc=1;恢复 ci.yml 并确认 git status --short 干净
  • bin/fm-test-run.sh tests/fm-ci-workflow.test.sh -> FM_TEST_BEGIN/FM_TEST_END exit=0/FM_TEST_SUMMARY total=1 failed=0, rc=0
  • bash tests/fm-test-run.test.sh -> 40 ok / 0 not ok, rc=0(包括 'a missing, blank, or non-numeric parallel weight refuses the lane instead of shrinking it' 和 'portable parallel lanes are fully hinted and packed within 5% of each other')
  • bash tests/fm-teardown-endpoint-safety.test.sh -> 38 ok / 0 not ok, rc=0;bash tests/fm-teardown.test.sh -> 84 ok / 0 not ok, rc=0
  • bash tests/fm-update.test.sh -> 18 ok / 0 not ok, rc=0(T12 mate poll re-anchor, T13 registry backstop, T14 re-exec hands off once, T15 rebind locally armed watch)
  • bash tests/fm-pr-merge.test.sh -> 88 ok / 0 not ok, rc=0(upstream gh-based live-head/merge-queue 路径加上 fork Gitea refusal)
  • bash tests/fm-watch-triage.test.sh -> 117 ok / 0 not ok, rc=0
  • bash tests/fm-pr-check-security.test.sh -> 36 ok / 0 not ok, rc=0
  • bash tests/fm-brief.test.sh -> 29 ok / 0 not ok, rc=0
  • bash tests/fm-backlog-read-bound.test.sh -> 10 ok / 0 not ok, rc=0;bash tests/fm-spawn-batch.test.sh -> 5 ok / 0 not ok, rc=0
  • 重放了所需的 CI invariants job 步骤(CLAUDE.md canonical pointer、.claude/skills symlink target、no tracked data/state/config/projects/.no-mistakes)
  • 重放了所需的 CI macos-stock-bash job:bin/fm-lint.sh --list-files(119 files)通过 /bin/bash -n 在 GNU bash 3.2.57 下管道处理 -> parse_fail=0
  • 在 /bin/bash 下重放了所需的 CI macos-stock-bash snapshot 消费方:fm-fleet-snapshot-view 18 ok, fm-bearings-snapshot 59 ok, fm-public-followup bash32 register 1 ok - 全部与 CI 的预期计数一致
  • 验证了 review-fix 范围:git show --stat a27d1b5 只触碰 tests/fm-ci-workflow.test.sh;git diff --name-only a27d1b5^ a27d1b5 -- .github/workflows/ci.yml 为空;ci.yml 的第 79/124 行 cap 仍为 15,并带有 fork rationale 注释
  • changed-map 缺口的对照探测:从 worktree 运行 git show ddb70a6:bin/fm-test-run.sh 为 --changed --base ddb70a6 --list,其终止方式与 branch runner 相同,报同样的 unmapped-path 错误(探测脚本已移除;worktree 干净)
  • 最终 worktree 上的 git status --short -> 0 行;没有遗留 /tmp scratch 文件
✅ **Document** - passed

✅ No issues found.

✅ **Lint** - passed

✅ No issues found.

✅ **Push** - passed

✅ No issues found.

mremond and others added 30 commits September 10, 2026 07:20
* docs: bound the mandatory captain address to the chat channel

AGENTS.md's opening address rule said "address the user as captain at
least once in every response" and never said what a response is. The
artefact exclusion two lines below governed only the optional nautical
seasoning, not the mandatory address. An agent that reads this file
without being the first mate - a pipeline corrector agent running inside
a copy of this repo - therefore read the obligation as applying
everywhere and the exclusion as applying only to flavour, and opened its
delivery message with "Captain,". That reading was correct.

Patch the existing owner rather than adding a rule elsewhere:

- bound the obligation to chat messages sent to the captain;
- state the artefact exclusion once, explicitly binding every agent that
  reads this file whether or not it is the first mate, and naming commit
  messages, PR and issue descriptions, briefs, code and comments;
- fold the seasoning under the same bound instead of carrying a second,
  narrower copy of the exclusion.

The obligation itself is unchanged: the captain is still addressed in
every chat message.

AGENTS.md goes from 603 to 602 lines: the redundant "never send a
response with zero direct address" clause and the duplicated seasoning
exclusion pay for the new bound.

The two cross-references that paraphrased the unbounded wording
(bin/fm-parent-channel-lib.sh's header and
docs/secondmate-parent-channel.md's problem statement) now match the
owner; neither restates the rule.

* fix(review): Limit address exclusions to artifacts while preserving public replies

* fix(document): Consolidate captain address guidance
…nguid#4131)

* fix(herdr): close persisted-focused tabs when no live client is attached

The teardown active-tab guard treated Herdr's last-focused pointer as a live viewer, so detached sessions could not close panes on that tab.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(herdr): allow detached seeded-tab prune after live-client gate

Projection create still restored the persisted focused tab after a successful prune, so a detached last-focused seeded tab still quarantined the spawn.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(herdr): probe live client after seeded prune only when that tab was focused

The extra title-clear read after every prune shifted canned CLI fixtures and failed projection create.

Co-authored-by: Cursor <cursoragent@cursor.com>

* no-mistakes(review): Tighten Herdr active-tab close guard

* no-mistakes(review): Guard Herdr mutations with fresh target focus

* no-mistakes(document): Document Herdr live-viewer teardown guard

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
…guid#4169)

* fix(bin): escalate decision-owned wakes once as the decision

The away-mode daemon treated a needs-decision: queued payload as an
unknown wake, so suppression markers never committed and the same open
decision re-escalated on every poll.

Classify that payload through the existing signal path so it escalates
once, labelled as the decision, and an unchanged repeat is suppressed
on the same terms as any other signal.

Fixes kunchenguid#4096

* no-mistakes(review): Escalate captain-held decision-owned rows once as the decision

* no-mistakes(review): Self-handle captain-held decision-owned rows instead of escalating them

* no-mistakes(document): Name away daemon as needs-decision payload reader
* test(herdr): pin leftover-shell vs live-idle via agent get

Herdr 0.9.0 already distinguishes a Pi that exits to a surviving pane shell
from a sibling live idle occupant. Pin that pair through agent get and the
recovery classifier so a lagged pane-get status cannot silently reclaim the
leftover shell as alive.

Co-authored-by: Cursor <cursoragent@cursor.com>

* no-mistakes(document): Document Herdr leftover-shell liveness regression

* no-mistakes(ci): Fixed Lint failure SC2034 by replacing the unused wait-loop variable with `_`. Verified with the pinned project lint command, Bash syntax check, and git diff check

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
…nchenguid#4151)

* ci: rebalance the portable parallel lanes on measured runner durations

Both portable parallel lanes are capped at 10 minutes. Lane 1 was cancelled at
that cap on every request raised on 2026-09-10 while lane 2 finished in about
3.5 minutes, so no request could go green.

CONTRACT CLASS: RESTORE.
The workflow already promises two duration-balanced lanes and the shard
documentation already claims a measured wall; this re-establishes both against
what the lanes now cost, and changes no lane count, no cap, and no scope of what
runs. The counter-argument, so nobody has to take that on trust: two pieces here
are genuinely new rather than restored, and either could be argued to make this
a NEW-behavior change. `--list-scheduled` now ranks a parallel lane on measured
durations where it previously handed every parallel script the serial default
weight and returned an alphabetical order; and `--check-coverage` gains three
reported fields. I classify the change RESTORE because both exist only to make
the already-promised property checkable, but they are named here rather than
folded into the restoration.

=== PART 1: THE TOTAL, AND HOW IT WAS OBTAINED ===

This section stands on its own. It establishes what the parallel set costs. It
derives no packing; Part 2 does that, from this number.

THE TOTAL: 828568 ms, about 13 min 49 s of serial work across the 24 scripts.
Lane 1 held 624299 ms of it and lane 2 held 204269 ms, a 3.06:1 split.

HOW IT WAS OBTAINED. The difficulty was that lane 1 had never finished, so its
duration did not exist as a recorded figure anywhere and no timing artifact was
expected for it. It turned out to be recoverable from the real lane without
estimating, by two routes, across six CI runs on 2026-09-10 (34459949083,
34460760299, 34462530836, 34462758357, 34466966385, 34470382458):

  - Run 34462758357's lane-1 job finished its suite 18 s BEFORE the wall and
    uploaded a complete fm-test-timing-portable-parallel-1 artifact carrying all
    11 scripts, FM_TEST_SUMMARY total=11 failed=0 duration_ms=598225. The
    upload step is if: always(), so the cancellation did not suppress it. This
    is one full, untruncated lane-1 measurement.
  - The five other lane-1 jobs were cancelled mid-suite, but each logs every
    script that had already finished as an FM_TEST_END duration_ms= marker.
    Those per-script records are complete measurements of completed scripts;
    only the script in flight at cancellation is lost, and it differs by run.

Lane 2 completed in all six runs, so its scripts come from the six uploaded
fm-test-timing-portable-parallel-2 artifacts.

Every one of the 24 scripts therefore carries at least one untruncated
measurement: 20 of them measured in all six runs, two in three or four runs, and
two (fm-brief, fm-transition-lib, the tail of lane 1) in the single complete run.
Each hint is the SLOWEST value that script reached, so the total is an upper
envelope rather than an average. NO FIGURE IN IT IS DERIVED FROM A TRUNCATED
LANE, and no lower bound was ever extrapolated into a total.

THE ENVIRONMENT, AND WHETHER IT TRANSFERS. Every hint is a serial run of the
real portable parallel lane on a GitHub ubuntu-latest runner, produced by the
lane's own CI job. It transfers because it is not a proxy for the lane; it is
the lane. Nothing in the total came from this machine or from any harness of
mine.

That mattered, and here is what it would have cost. A same-day macOS
cross-check of the same scripts ran 1.7x to 5.0x slower with the ratio varying
per script (fm-test-run 157420 ms against 92944 ms, fm-x-mode 67217 ms against
31870 ms, fm-composer-ghost 10521 ms against 2120 ms). Local timings therefore
do not scale the lane, they REORDER it, so a packing derived from them would
have balanced the wrong thing while looking clean.

WHAT IT REPLACES, which is the root cause. The lanes were packed from the
2026-08-20 concurrent isolation proof: 24 candidates across four LOCAL workers.
That record answers whether the candidates are isolation-safe, not how long a
SERIAL CI lane runs, so it was structurally incapable of representing lane wall
clock even when it was fresh. It was also never refreshed while the set grew
about 3.2x. Both the wrong instrument and the staleness are fixed here: the
hints now come from the lane itself and carry their run ids and date.

=== PART 2: THE SPLIT DERIVED FROM THAT TOTAL ===

Longest-processing-time assignment over those hints gives 414269 ms and
414299 ms, 30 ms apart, against 624299/204269 before.

tests/fm-pi-primary-types.test.sh stays in lane 1 because that is the job which
installs the Pi package, so ci.yml needs no step changes.

=== PART 3: DOES THE MARGIN SURVIVE MACHINE VARIANCE ===

Stated explicitly, because 6.90 min against a 10 min cap is 69% of cap before
any variance is applied, and the cap covers the whole job rather than the suite.

  worst lane, script time                         414299 ms   6.90 min
  job overhead, measured on the real lane             ~18 s   (see below)
  expected healthy job                            ~432300 ms  7.21 min
  x1.29 on the script time, plus overhead         ~552400 ms  9.21 min
  cap                                             600000 ms  10.00 min
  room left after the multiplication                ~47.6 s   7.9% of cap

The 1.29x is the runner variance measured today on the SIBLING SERIAL lane, as
supplied; it is not this lane's own figure. This lane family does have its own,
and it is tighter: the six full lane-2 sums today span 192939 ms to 203451 ms,
a spread of 1.054x. At that figure the worst lane lands near 7.58 min with about
2.4 min of room. I have used the LARGER, borrowed 1.29x for the verdict rather
than the tighter one this lane actually shows, and note that the hints are
already per-script maxima, so 1.29x on top is conservative twice over.

THE MARGIN SURVIVES THE MULTIPLICATION, so this proceeds rather than stopping.
The 18 s overhead is measured, not assumed: in run 34462758357 the lane-1 job
ran 10 min 16 s against a 598.2 s suite, and lane 2 ran 3 min 21 s against a
192.9 s suite, a ~10 s difference that matches lane 1's extra Pi package install.

The cap is unchanged, the lane count is unchanged, and nothing in the serial
lane, its shard count, its guard or its hint table is touched.

=== PART 4: THE RECORDED FACT ===

The workflow comment no longer restates the shard wall as a literal, which is
how "~1 min of serial sum" survived a 10x change without announcing it. It now
points at bin/fm-test-run.sh --check-coverage, which prints parallel_max_ms,
parallel_imbalance_ms and parallel_unhinted derived from the hint table, so the
current number is computed on demand. The shard documentation carries the dated
run ids, which route it was taken by, and the local cross-check that shows why
local numbers are not admissible as hints.

Two regressions pin what rotted: lane membership must be stored
longest-measured-first, and the lanes must be fully hinted and packed within 5%
of each other. Both were run against the old composition and both fail on it
(420030 ms imbalance against a 624299 ms worst lane). The ordering assertion they
replace named a specific script by hand and had itself gone stale.

=== PART 5: NAMED AND LEFT, OUTSIDE THIS REBALANCE ===

tests/fm-captain-hold-lifecycle.test.sh alone is 296481 ms, 36% of the whole
set, so it is the floor of any two-lane split: no repacking can put a lane below
it. After this rebalance the cap is about 1.45x the healthy lane where the
sibling serial lane keeps roughly 2x.

Nothing refuses a stale parallel hint the way PORTABLE_SERIAL_MAX_UNHINTED_PERCENT
bounds the serial lane. parallel_unhinted is reported, not enforced, which is
what let this drift for three weeks unnoticed.

* fix(review): Restrict parallel scheduling hints to portable parallel lanes

* fix(document): Clarify parallel lane scheduling and timing evidence
… PR (kunchenguid#4148)

pr_for_task fell back to scraping the whole status log with tail -1, so
any PR URL a worker ever mentioned in prose - including a scout citing
someone else's PR - became the task's delivered PR in the parent-channel
terminal report. Recorded meta pr= is now the only authoritative source,
the fallback scrape accepts only a preferred terminal line in a mode's
ready-signal shape (done: PR <url> or done: PR <url> checks green), and
a scout never carries pr= at all.
…claims instead of counting a dead drop as started (kunchenguid#4212)

* fix(procevent): stop a dead runner owning a source and reconcile reporting it

The captain answered ten calls on a bearings board, the board accepted
them, and nothing collected them. He had to answer all ten again in chat.
A surface that presents as armed while being a dead drop is worse than one
that visibly fails, because the answers looked recorded.

Two independent defects, reproduced together in an isolated home where
reconcile reports started=1 on every run while ownership never moves and
no runner ever attaches.

1. reconcile counted a launch it never verified. detach_runner is
   fire-and-forget and discards the child's stderr, so a runner that died
   before it could claim was counted exactly like one that is listening.
   Launches are now confirmed - the source observed owned, or its runner
   record moved - before being reported as started; the rest are reported
   as failed= with a non-zero exit. The runner-record clause is what keeps
   a fast-completing source from being reported as a failure when it
   finished between two polls. One bounded window covers a whole cycle's
   launches, so a home full of broken sources costs the same wait as one.

2. A claim whose whole generation is provably gone could be refused
   forever. Reclaiming it ran cleanups over that dead generation's own
   leftovers, and any failure vetoed the claim - permanently, because none
   of those conditions clears on its own. Every one of those leftovers is
   keyed by the dead generation's claim token and a replacement always
   claims a fresh one, so none can collide with what replaces it.
   fm_procevent_claim_capture_reservation_reclaim_locked already said this
   for the reservation record; the staging file and the shape check on the
   registry directory recorded to hold it now take the same rule. Removing
   the claim record itself stays a hard precondition: two owners is the one
   outcome worse than none.

Two smaller repairs to the same "registered is not listening" confusion:

- `list` reported OWNER=none for a source nothing can claim. A reused PID
  whose process group survives reaches that state through the stale branch
  rather than the leaderless one, so it read as an idle source waiting to
  be started - the reassuring answer this surface gave while a board
  collected nothing. It now reports the orphaned state it shares.
- reconcile relaunched into that same unclaimable state on every cycle,
  spawning a runner that could only die on the claim. docs/configuration.md
  already promised it preserves such a claim without starting a
  replacement; the code now does that and reports it as uncertain.

This is NOT a third instance of today's two lock-identity defects
(4e1bf9aa and its replayed predecessor). Those were wrong liveness
predicates: a reused PID read as a live holder, then an exec'd holder read
as dead. Here the predicate is right - the code correctly proves the owner
dead and refuses the claim anyway, on a condition unrelated to liveness.

Regression coverage, each failing on the parent commit for its own reason:
- tests/fm-procevent.test.sh: a source that cannot start is reported as
  failed rather than started; a dead generation whose leftovers cannot be
  tidied no longer keeps owning its source (the parent reports a start
  while nothing ever runs); the existing reused-PID fixture now also
  asserts the orphaned listing and that no doomed relaunch is reported.
- tests/fm-captain-hold-lifecycle.test.sh: a board answer reaches the
  keyed-answer intake through the runner end to end - durable capture, the
  wake, and the closed task carrying the captain's selection. This one
  passes on the parent, because that chain was never what broke.

fm-procevent 100, fm-bearings-board 18, fm-captain-hold-lifecycle 50,
fm-procevent-when 13 and fm-procevent-quota 18 pass; bin/fm-lint.sh and
bin/fm-doc-audience-check.sh clean. tests/fm-extension-binding.test.sh has
two failures identical on the parent commit (EACCES on package install in
this sandbox) and unrelated to this change.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016gxgshn5jkWJ3GEYWy7vTG

* no-mistakes(review): confirm reconcile launches on durable launch stamps

* no-mistakes(review): announce stranded sources and refuse bad confirm windows

* no-mistakes(review): announce leaderless strands, bound confirm window, fix recovery docs

* no-mistakes(review): announce unconfirmed launches once per episode, qualify start reclaim

* no-mistakes(review): nonce launch-failed keys, refuse bad window at arm

* no-mistakes(review): state only observed launch outcome, shorten episode nonce

* no-mistakes(test): assert launch-failed headline not re-delivered, allow recovery wake

* no-mistakes(document): docs: cover strand and launch-failure wakes in skill trigger and verification record

* no-mistakes(lint): restructure SC2015 chain into explicit if-block

* test(watch-triage): fix two timing-exposed defects the pipeline found

Both surfaced in the no-mistakes test step on this branch, each failing one
full run of tests/fm-watch-triage.test.sh; neither was accepted as a flake to
retry past.

1. The new launch-failed delivery test assumed an already-surfaced key never
   wakes the watcher again. That is false: a fresh watcher legitimately
   re-surfaces any unacknowledged queue row through its downtime-recovery
   path ("check: rearm-resurface"), so the assertion failed whenever a
   re-arm landed between its two checks. The pipeline's own fix tolerated any
   wake lacking the repeated key's headline; this tightens it to exactly one
   tolerated reason, by its exact line, with a failure message that names the
   expectation so a reworded path reads as "the tolerated recovery path
   changed" rather than as a mystery - and so nobody restores the strict
   silence check. The positive assertion (a fresh-suffix key is delivered
   under its own headline) is unchanged.

2. seed_captured_procevent_result retired its source in the gap between the
   runner publishing its wake and releasing its claim, so retire read the
   exiting runner's ownership as uncertain and refused ("cannot confirm
   runner identity"). The fixture and retire path pre-date this branch; the
   confirm window returns reconcile closer to the moment of capture, which
   made the gap easier to hit. The fixture now waits, bounded, for the claim
   release the publish promises, with the reason at the wait.

Verified on this head with tasks-axi on PATH: fm-watch-triage 113/113 with
no skips, fm-procevent 106/106, fm-captain-hold-lifecycle 50/50,
fm-watch-arm 15/15, fm-bearings-board 18/18, fm-procevent-when 13/13,
fm-procevent-quota 18/18; bin/fm-lint.sh and bin/fm-doc-audience-check.sh
exit 0. First attempt, no retries.

* no-mistakes(document): docs: route stranded and launch-failed wakes in skill handling

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
…gistration (kunchenguid#4191)

* fix(herdr): verify agent registrations at process level before trusting them

Herdr keeps a Pi registration (`agent get` -> agent=pi, agent_status=idle)
after the Pi process has exited to a plain shell whenever a nested interactive
shell sits under the pane's top shell, which is the crew shape `treehouse get`
leaves behind. The pane classifier trusted that registration alone, so
`fm-control.sh <id> relaunch`, `fm-spawn.sh --relaunch`, and the crew-state
recovery read all treated a shell-only pane as a live agent and refused
recovery for as long as the record lived.

The Herdr adapter now reads `pane process-info` plus the real process table
through a shared harness-process classifier (bin/fm-agent-process-lib.sh,
moved verbatim out of the tmux adapter so both backends mean the same thing by
agent, shell, and other) before a registered agent counts as live. A
registration over a shell-only pane is the new explicit `stale-agent` pane
state, which the recovery-grade read maps to `dead`; husk detection, reclaim,
presentation recovery, and session cleanup keep refusing it, so recovery reuses
the pane and nothing gains close authority. A working record is verified the
same way before the native busy verdict reports busy, so the recovery
classifier never reports a shell-only pane as working. An unreadable process
view reads unknown, trusting neither the registration nor its absence.

Reproduced and measured on Herdr 0.9.0 with Pi 0.85.1 in an isolated lab; the
new default-on live guard tests/fm-herdr-pi-stale-registration-live-e2e.test.sh
exercises the real stale record, tests/fm-control-herdr-smoke.test.sh proves
exit and relaunch through the control plane, and the portable suites pin the
classifier over real processes.

Fixes kunchenguid#4115. Duplicates: kunchenguid#3639, kunchenguid#3487, kunchenguid#2908, kunchenguid#3545.

* no-mistakes(review): settle transient prompt helpers before trusting herdr process state

* no-mistakes(review): drop stray codegraph file; read spaced comm whole in descendant walk

* no-mistakes(review): untrack stray .codegraph/.gitignore

* no-mistakes(review): untrack codegraph file; make spaced-path walk test discriminating

* no-mistakes(review): untrack stray .codegraph/.gitignore

* no-mistakes(review): untrack stray .codegraph/.gitignore re-added by fix round

* no-mistakes(review): untrack stray .codegraph/.gitignore

* no-mistakes(review): untrack codegraph file, drop dead control case, record process-info floor

* no-mistakes(review): refuse stale-agent on fresh herdr spawn preflight

Documented non-goal: fresh-spawn, reclaim, and presentation-recovery auto-recovery for a stale-agent pane is a separate design change, out of scope here, to be proposed upstream as its own issue if wanted.

* no-mistakes(test): Fix herdr flake: don't misread transient empty foreground as unreadable

* no-mistakes(document): Add fm-agent-process-lib.sh to scripts inventory

* no-mistakes(fix): update remote herdr fixture to the real pane process-info shape

The shared remote-secondmate herdr fixture still returned the old flat
process-info body ({"result":{"process":{"name":...}}}). The process-level
liveness classifier added for kunchenguid#4115 requires the real
{"result":{"type":"pane_process_info","process_info":{...foreground_processes}}}
shape and treated the old body as unreadable, so an already-launched remote
endpoint's agent-state read failed and any relaunch attempt against it died
with "remote endpoint state is unreadable; refusing duplicate launch"
instead of reaching the state it was actually exercising
(tests/fm-remote-secondmate-parent-binding.test.sh,
tests/fm-remote-secondmate-lifecycle-e2e.test.sh).

* no-mistakes(review): test: add empty-foreground regression test for herdr flake fix

* no-mistakes(document): docs: register new stale-registration live-e2e test in herdr entry points
* Bind GitHub merges to a live green head and require an away-task grant.

A GitHub merge now re-reads the pull request and passes
--match-head-commit, so a red or moved head cannot land the way GitLab
already refused. While an away record exists, only yolo or a named
grant may merge, so hold-for-return cannot ship an ungated PR.

Co-authored-by: Cursor <cursoragent@cursor.com>

* no-mistakes(review): Harden away merge authorization and grant parsing

* no-mistakes(review): Restrict fallback outcomes to proved GitHub merges

* no-mistakes(document): Refresh merge safety documentation

* no-mistakes(ci): Fixed all three CI failures by updating legacy GitHub merge fixtures for live-head verification/direct gh merges and removing a process-event runner cleanup race. Verified fm-pr-check-security, fm-captain-hold-lifecycle, and fm-watch-triage pass locally; shell syntax and git diff checks also pass

* no-mistakes(document): Document attended red-check exception

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
…m epoch (kunchenguid#4221)

The --claude guard's re-block budget charged the auto-arm ledger epoch, not
the re-block: `budget_account_current_epoch` advanced the session count only
when `state/.claude-autoarm-epoch` named a different generation than the
previous accounting. The epoch advances only inside the auto-arm hook's
generation claim, so a hook kept inert before that claim - a session lock
held by a live harness outside its ancestry, a hook that never fires, or an
identity or write failure ahead of `fm_autoarm_claim_next` - left the ledger
frozen at its last outcome and the count frozen with it. Reproduced in a
fixture: twelve consecutive Stops re-blocked with the count at 0 and the
attended fail-open never fired, leaving only Claude's silent 8-block
override, the blind end the bounded alarm exists to prevent.

The budget now charges a re-block against an epoch the previous re-block
already charged, while still charging each epoch at most once per Stop so
the wait loop's repeated observations of one fresh terminal outcome and the
same invocation's block decision cannot double count. The advancing-epoch
progression is unchanged: three re-blocks, then one attended fail-open for a
verified failure episode, and a frozen epoch now follows the same shape.
Budget exhaustion without a verified failure still blocks, by the existing
contract, and positive watcher recovery still clears the whole episode.

Regression coverage drives the real auto-arm hook against a foreign session
lock holder, asserts the ledger itself stays frozen, and fails before the fix
in both the verified and unverified shapes; the existing unverified budget
test now proves its budget actually ran out.
…enguid#4242)

* feat(herdr): attach a real foreground viewer so the live-client teardown cases can be driven

PR kunchenguid#4131 gated the Herdr active-tab close refusal on a live foreground client
instead of the persisted `.focused` pointer, but only its two detached
scenarios could be validated live. Every pseudo-terminal the runner built
started at a zero-sized window grid, so Herdr registered no foreground client
and `terminal title clear` kept answering `no_foreground_client`, leaving the
four attached-client scenarios untested. That was a harness limit, not a
product one.

Add `fm-herdr-lab.sh viewer start|stop <session>`, backed by
`bin/fm-herdr-lab-viewer.py`. The launcher sets the pty window size on the
master fd BEFORE the fork, so the TUI cannot read the grid until it is already
non-zero, and scrubs the inherited `HERDR_*` variables so Herdr's nested-viewer
refusal does not fire when the helper runs inside one of its own panes. Attach
and detach are both confirmed against the session's own foreground-client
reason rather than assumed from a signal.

The viewer inherits the lab's isolation contract: it attaches only to a session
carrying this lab's ownership tripwire, never to `default`, and it signals only
the processes it recorded, so a client someone else attached is never touched.
Teardown now refuses while an owned viewer is still attached.

Turn the reproduction into the regression with
`tests/fm-herdr-attached-viewer-live-e2e.test.sh`, which drives kunchenguid#4131's
scenarios 3, 4, 5, and 7 live against real Herdr and asserts the close refusal
fires. Scenarios 4 and 5 need a focus change at one exact product boundary, so
a PATH shim performs the real `tab focus` when the close helper issues its
planning `pane get`. Removing either half of the recipe from the launcher makes
the guard fail with the same `no_foreground_client` symptom kunchenguid#4131 reported.

* test(herdr): fail loudly when an attached-viewer fixture cannot be created

The fixture helpers run inside command substitutions, where fail() exits only
the subshell and leaves the script running with empty ids. Return non-zero
instead and carry the message at each call site.

* fix(herdr): stop the viewer launcher's kill timer from raising on an exited child

The SIGALRM escalation called os.kill unguarded, so a viewer that exited
during the grace window turned an ordinary shutdown into a traceback inside
the signal handler.

* docs: list the lab viewer's pty engine in the bin toolbelt

* no-mistakes(review): Harden Herdr viewer ownership and live CI coverage

* no-mistakes(review): Validate viewer startup timeout and process ownership

* no-mistakes(document): Document Herdr viewer safety contracts

* no-mistakes(review): Fix viewer timeout to two seconds

* no-mistakes(review): Cancel timed-out viewers and fix PTY grid

* no-mistakes(review): Serialize viewer transitions and verify process parentage

* no-mistakes(review): Harden viewer ownership locks and deduplicate CI

* no-mistakes(review): Release interrupted locks and preserve viewer escalation

* no-mistakes(review): Remove viewer locks and cancel interrupted launches

* no-mistakes(review): Close viewer launch signal races

* no-mistakes(document): Document attached Herdr viewer regression
…unchenguid#3766)

* fix(bootstrap): allow nonvisual work without Lavish

* no-mistakes(review): Gate scout brief Lavish line on bootstrap version floor
…kunchenguid#3825)

* fix(tests): isolate fixture Git configuration from host preferences

Ignore global and system Git configuration in the shared test library,
which all four fixture helper entry points source. Keep local config,
command-line overrides and explicitly supplied test config usable without
changing the caller's environment or real project signing preferences.

Exercise global and system signing inputs through all four helpers, real
fixture and child commits, explicit signing overrides, unchanged input
files, and signing refusal outside fixture subprocesses.

Verification evidence for issue kunchenguid#3770:
On pristine upstream f09de8a, all 12 reported suites failed and each logged
"No secret key" using a private GIT_CONFIG_GLOBAL containing
commit.gpgsign=true and gpg.format=openpgp, GIT_CONFIG_NOSYSTEM=1, and an
empty private GNUPGHOME (GIT_CONFIG_COUNT and GIT_CONFIG_PARAMETERS unset).
With this change, all 12 pass in the identical environment through
bin/fm-test-run.sh --per-script-timeout-secs 900:
fm-backlog-atomicity, fm-bootstrap-network-parallel, fm-bootstrap,
fm-crew-state, fm-fleet-sync, fm-gate-refuse, fm-grok-harness,
fm-session-start, fm-sessionstart-nudge, fm-tangle-guard, fm-test-run,
and fm-update (all tests/<name>.test.sh).
The new fm-test-fixtures regression failed before the library change and
passes after it. Canonical bin/fm-lint.sh passes.

Additional verification exposed fm-teardown's
herdr-preflight-missing-adapter assertion on both this branch and an
unchanged f09de8a archive with signing neutralized. That pre-existing
failure needs separate disposition; it is not repaired or skipped here.
The separately owned Muse and composer fixture defects remain untouched.

Fixes kunchenguid#3770

* no-mistakes(review): Complete fixture Git isolation and scope config assertions

* no-mistakes(review): Share Git isolation across standalone fixture entry points

* no-mistakes(review): Map git-config helper changes to lib.sh dependents

* no-mistakes(review): Select fixture-isolation regression on runner change; halve config matrix

* no-mistakes(review): Scope fixture-isolation regression selection to the runner alone

* no-mistakes(document): Give fixture Git isolation helper its owning header

* no-mistakes(document): Record fixture Git-isolation coverage in fixtures suite header

* no-mistakes(review): Fix linked-worktree fixtures and remove redundant Git isolation

* no-mistakes(document): Correct stale runner-selection documentation

* no-mistakes(document): Clarify family antecedent in isolation-proof runner evidence
… in auto mode (kunchenguid#4239)

* feat(spawn): add config/claude-permission-mode to launch Claude workers in auto mode

Every Claude worker launched with --dangerously-skip-permissions, and a
captain who refuses bypass mode had no way to select Claude Code's
classifier-reviewed auto mode instead. A new one-token local config,
config/claude-permission-mode, selects the permission flag for every
Claude launch: absent or `bypass` keeps today's launch byte-for-byte,
`auto` swaps in --permission-mode auto, and any other value refuses the
spawn before any endpoint, worktree, or record exists and names the
accepted values.

fm-spawn resolves the file on every spawn and relaunch, threads the flag
through the Claude launch template for crewmates, scouts, and secondmates
alike, and records claude_permission_mode=auto in the task meta only
under auto so the default meta stays unchanged; a relaunch re-resolves
rather than preserving the line. The file is a captain-wide safety
preference, so it joins the inherited local material pushed into
secondmate homes.

The Claude adapter reference records the verified auto launch shape on
Claude Code 2.1.269 and that it never meets the once-per-machine bypass
confirmation dialog; docs/configuration.md owns the schema.

* no-mistakes(review): drop unread claude_permission_mode meta line and its assertions
… untouched (kunchenguid#4243)

* fix(teardown): refuse to return a Treehouse pool slot reassigned to another task

A pool slot is reused across tasks, so a finished task's worktree= line can name
a slot a different, live task now holds. Teardown already refused when a second
task record named the same live path, but that scan cannot prove the record it
is tearing down is the current owner: the task that took the slot next may leave
no record the scan can reach - its own worker may have exited and its record been
cleaned up, or it may live in a home this machine does not register. Teardown
then killed every process under the path, hard-reset it and returned it, and its
unlanded-work refusal never fired because it was inspecting a directory that no
longer belonged to the task being torn down (observed 2026-09-07).

Treehouse's own state file cannot answer the ownership question. It records a
slot's owner as a live process lease (owner_pid plus owner_started_at, with
`treehouse status` reporting in-use from the processes actually running under
the path), which names no task and is released by the very event that makes a
record stale - the worker exiting. An unleased slot therefore reads identical
whether it is still this task's or has since been handed on, and a slot whose
new holder has also exited but left uncommitted work reads as free. So the
identity source is Firstmate's own claim, not Treehouse's lease.

fm-spawn writes that claim - the task id - into the slot at the moment it takes
it, under the same project lock that allocates the slot, and fm-teardown drops it
only after the slot is genuinely returned. It lives at <pool>/<slot>/.fm-slot-owner,
a sibling of the repo checkout rather than a file inside it, so claiming a slot
can never dirty the copy the landed-work checks inspect. A claim naming another
task, or one that cannot be read, refuses; --force does not lift either refusal,
because --force authorizes discarding this task's unlanded work, never another
task's live work. A slot that cannot be claimed refuses the spawn instead.

An absent claim proceeds on exactly the record-scan protection it had before:
slots taken before claims existed, and slots already returned, carry none, and
refusing those would strand every task in flight across this change on no
evidence at all.

The refusal is deliberately all-or-nothing rather than partially completing the
task's own cleanup. state/<id>.meta is the only durable record naming the
worktree and endpoint, so removing it would destroy the evidence needed to
reconcile which record is wrong, and its removal is one step with the backlog
transition. Nothing is stranded: clearing the stale worktree= line leaves a
record with no slot to release, which then tears down normally, and the refusal
names that remedy.

Repairing the previous claimant's stale worktree= line at spawn time is left for
separate work. It would have the new owner write another task's record - the same
class of cross-task mutation this bug is - and would need that record's own meta
lock; with the claim in place teardown refuses on evidence rather than depending
on the stale pointer having been scrubbed. For the same reason the relaunch path
writes no claim: it holds no allocation lock, and a record whose worktree= is
already stale would stamp the wrong task's claim onto a live sibling's slot.

The regression reproduces the reuse sequence with only one discoverable record,
including a clean, fully landed ship copy torn down without --force - the shape
of the real incident, which the previous code returned to the pool - and fails
against the previous code; the existing two-record, cross-home, own-slot
and no-claim cases still pass unchanged.

This builds ON upstream b028e8b (kunchenguid#3837), which is already in this branch's base
(origin/main 40c50ea) and owns the record-exclusivity scan. Nothing here
replaces that scan; the claim is the positive proof it cannot supply.

Claude-Session: https://claude.ai/code/session_01JTBmuqKugaPUj7k9TXQwFS

* no-mistakes(review): teardown leaves reassigned slot; spawn abort drops claim

* no-mistakes(review): narrow Treehouse lease evidence; gate abort claim release on lock

* no-mistakes(review): pin spawn-side slot claim; narrow abort-release header

* no-mistakes(document): docs: point slot-claim rationale at fm-wake-lib owner
…nguid#4247)

The reviewer treats Captain's intent as acceptance criteria, so a widened ask there drives over-built work; the spec should carry only what the ask requires.
* feat(bearings): name the Underway rows and order Charted Next newest filed first

The fleet board's Underway rows led with the run status alone, so a scan told
the captain where a pipeline stood but never which task the row was, and
Charted Next rendered in backlog order rather than by when work was filed.

The snapshot now projects the durable task name onto every in_flight row - from
this home's backlog title, and from a secondmate home's own ledger for an active
child - and the durable filed date onto every gate. The board's Underway row
leads with that name and keeps the run status on its second line, and Charted
Next renders newest filed first, with rows carrying no comparable date keeping
their payload order after every dated row.

The payload validator requires an explicit name marker on every Underway row and
refuses a filed value that is not an ISO date, so the board can never sort on
garbage or invent a label.

* no-mistakes(review): Fix Bearings labels, bounds, and filed validation

* no-mistakes(review): Fix Bearings identifiers and eligible queue bounds

* no-mistakes(document): Document Bearings labels and newest-first bounds

* no-mistakes(ci): Updated the stock macOS Bash CI expectation from 56 to 59 Bearings tests. Verified the suite under /bin/bash 3.2: all 59 tests pass. git diff --check also passes
…nchenguid#4248)

* fix(bearings): report the away-return catch-up instead of refusing

A captain returning from away and asking for bearings got zero bytes and an
error: fm-bearings-snapshot.sh ran the away-return guard with `|| exit $?`
before reading any fleet state, so the mere existence of the catch-up gate
killed every bearings mode (and /ahoy with them).

Bearings now consults that guard rather than obeying it. fm-afk-return.sh
separates its two refusal branches by exit status, so an ACTIVE away window
still refuses exactly as before - the right answer there is to run the return
first - while return catch-up (exit 4) lets collection and projection proceed
and is disclosed as one action-free `(return-catchup)` gate row, following the
existing `(main-inventory)` precedent. It stays out of decisions_open: these
blockers are firstmate-actionable, not the captain's own call, and the per-task
blockers already project as their own Underway rows.

The guard's refusal text also stops promising a blocker list it cannot produce:
a gate retained for a lifecycle reason alone now names that retention reason,
and bearings carries the same reason in the gate row's title.

Reporting is not ordinary work. AGENTS.md already scopes the return hold to
work rather than reporting, so only the /afk and bearings skills needed the
correction.

* no-mistakes(document): Refresh away-return Bearings verification

* no-mistakes(review): Reserve catch-up gate outside Bearings truncation

* no-mistakes(review): Preserve filed dates in catch-up gate output

* no-mistakes(document): Document reserved catch-up gate projection
…forked code-root copy (kunchenguid#4223)

* fix(backlog): address the home's backlog from any directory and detect a forked code-root copy

A home outside the code root forks its queue: the tracked .tasks.toml names
data/backlog.md relative to tasks-axi's working directory, so a bare
tasks-axi call from the code root writes the code root's data/ while session
start, spawn, and teardown use $FM_HOME/data. Linking the code-root copy into
the home does not hold, because tasks-axi 0.2.4 writes by renaming a temp file
over its target and rename(2) replaces a symlink: add, start, hold, and done
from the code root each turn the link back into a regular file. The archive
path is resolved against the working directory too, even with --file.

bin/fm-tasks-axi.sh runs tasks-axi against this home's backlog from any
directory, using the lifecycle transitions' existing addressing (run from the
data directory's parent, pin <data>/backlog.md through TASKS_AXI_FILE). It
keeps relative --to/--*-file arguments meaning the caller's paths, and refuses
a caller --file, an unresolvable home, and a symlinked home backlog. The
fm-send hold lookup, fm-public-followup, and the fm-decision-hold shim, which
relied on cwd discovery, now go through it with an explicit FM_HOME and a
cleared data override, so they keep addressing exactly $FM_HOME/data and an
ambient TASKS_AXI_FILE cannot divert them; every agent-facing backlog command
names it instead of bare tasks-axi.

Bootstrap gains a detect-only BACKLOG_RECONCILE check, also run read-only:
when the home's data directory is not the code root's, a code-root
data/backlog.md or data/done-archive.md that is not the home's own file is
reported as a fork, with the merge procedure in bootstrap-diagnostics.

* test(teardown): assert the completion hint names bin/fm-tasks-axi.sh ready

The completion hint now points at the home-addressed command instead of a
bare tasks-axi call, so the dependency-cleared follow-up assertion checks for
that command.

* no-mistakes(test): clear ambient tasks-axi env in tests/lib.sh

* no-mistakes(document): drop bare tasks-axi example from cd-guard doc

* no-mistakes(lint): replace ls -A decoy listing with find for SC2012

* no-mistakes: apply CI fixes

* revert: keep the compliance gate unchanged; the synchronize race is filed separately
* fix(spawn): pre-register Claude workspace trust for secondmate homes

A claude --secondmate launch skipped workspace-trust registration
entirely, so a standalone-clone secondmate home (an explicit
~/fm-homes/<id> path) had no store entry and its pane wedged on the
"Is this a project you trust?" dialog before it read its charter.
The step was gated on the task kind rather than on the harness, so the
spawn's fail-closed guard had nothing to run against and reported a
launch that could never start work.

fm-claude-trust.sh gains a secondmate-home mode. A secondmate home is a
whole firstmate instance, produced either as a leased worktree or as a
standalone clone, so the linked-worktree test cannot decide it and the
seed is the evidence instead: the .fm-secondmate-home marker must be a
regular file this user owns naming exactly the id being spawned, the
home must hold AGENTS.md and bin/, and each operational directory must
resolve inside the home. That is the set fm-home-seed.sh writes and
fm-spawn.sh's own home validation re-checks, so nothing wider than a
home a secondmate spawn would launch into can earn home-level trust.
The worktree path is unchanged, and still refuses a home.

fm-spawn.sh now runs the registration for every claude launch and keeps
refusing the spawn when it fails, rather than launching an agent that
would wedge.

* no-mistakes(document): Correct Claude secondmate trust guidance
* fix(pr-merge): judge each required check by its current run

When the base branch advances, GitHub cancels a pull request's in-flight
run and re-triggers it. The cancelled run stays in statusCheckRollup
beside the passing re-run, so the rollup can hold several runs of one
check name at the same head while GitHub itself reports the pull request
CLEAN. github_checks_not_green judged every run independently, so that
superseded failure refused a genuinely mergeable pull request and pushed
the operator toward a needless --allow-red.

Group the rollup by the reported name and judge each check by its
current run. Supersession is proven, never assumed: a name leaves the red
set only when every one of its non-green runs is strictly older than one
of its green runs, dated by the forge's own settled timestamp - a check
run's completedAt once its status is COMPLETED, or a status context's
createdAt - and only in the whole-second UTC form GitHub emits, which is
the one spelling that orders correctly as plain text. A run with no such
timestamp is never superseded, so a still-running, queued or undated run
keeps its check red, and a name with no green run at all stays red. An
unnamed entry is grouped alone so two unrelated unnamed checks are never
treated as one.

Every comparison is one-directional: it can only clear a failure a later
success provably replaced, and never clears a check whose current run
failed, is pending, or is missing. No other guard moves - the pull
request must still be open, undrafted, mergeable, conflict-free and
head-bound, and --allow-red still waives exactly its named check with
every other check green.

Live reproduction: PR kunchenguid#4224 read CLEAN with an old FAILURE and a newer
SUCCESS for one check name and was refused; it now verifies, while
kunchenguid#4208 and kunchenguid#4210, whose latest runs failed, still refuse.

* no-mistakes(review): Use check-run start times for safe supersession

* no-mistakes(document): Clarify GitHub check-rollup documentation
…guid#4266)

* fix(merge): persist the merge authority on poll-detected merge outcomes

The merge ledger tags a merge with the authority that permitted it while the
away-posture record existed, but only the direct attended merge in
bin/fm-pr-merge.sh recorded it. A merge the forge queued, or one the merge
poll detected after the fact, published an untagged row, so exactly the
merges no agent watched were the least auditable.

bin/fm-merge-authority-lib.sh now owns that answer, read from the same
structured sources the merge gate already used: the task's recorded yolo
posture and the away-posture record's mechanical grant list, never prose.
bin/fm-pr-merge.sh keeps its own refusal wording and gates on that answer;
bin/fm-watch.sh only records it on the row its poll publishes, so reading the
authority never becomes a second path to a merge. An unresolved answer records
an untagged row rather than dropping the outcome or inventing an authority.

* no-mistakes(review): Persist canonical merge authority for queued poll outcomes

* no-mistakes(review): Harden merge authority persistence against lifecycle races

* no-mistakes(review): Serialize poll authority publication with teardown

* no-mistakes(document): Clarify persisted merge authority lifecycle

* no-mistakes(ci): Added targeted SC2034 suppressions for the two public result assignments in bin/fm-merge-authority-lib.sh. Verified successfully with `CI=true bin/fm-lint.sh`
…4281)

The 2026-09-12 Actions starvation incident found firstmate CI with no
concurrency deduplication, so every superseded PR head kept its full
13-job fan-out, and four jobs with no timeout at all.

Add per-PR supersession keyed on the PR number for pull_request events
and on the unique run id for push events, cancelling only pull_request
runs, so a new PR head replaces its own in-flight CI while every main
push keeps its own group and is never cancelled. Add hang tripwires to
the four previously unbounded jobs: 25 minutes for lint (measured at
14-16 minutes) and 5 minutes each for the coverage guard, the timing
aggregate, and the repo invariants. Measured lane bounds are unchanged.

tests/fm-ci-workflow.test.sh resolves the workflow's concurrency
expressions against simulated pull_request and push contexts and holds
every job's finite timeout.
…henguid#4288)

Every other make_hold_home caller in this file skips when tasks-axi is
absent; this test was the one unguarded call, so hosts without tasks-axi
hard-fail the fixture build instead of skipping.
…nnot blind a session start (kunchenguid#4027)

* fix(bin): bound each backlog row read so one wedged backend cannot blind a session start

bin/fm-bootstrap.sh's reconcile and close-replay sweeps read the backlog
backend once per item through fm_backlog_row_show, and that read was
unbounded. A single wedged `tasks-axi show` therefore consumed the whole
FM_SESSION_START_TIMEOUT and truncated the digest before the wake queue,
supervision instructions, fleet state, and context sections ever printed,
leaving the fleet unsupervised with no live watcher. The harm was a blind
startup, not a slow one.

Bound the read with the existing shared timeout primitive
(bin/fm-timeout-lib.sh), so a wedged backend degrades to a loud partial
reconcile: the sweep's existing BACKLOG_RECONCILE diagnostic names the item
it could not read and the loop continues to the next one. The first bound hit
also latches FM_BACKLOG_ROW_SHOW_WEDGED, so a sweep over many items pays one
bound rather than one per item and still names every item it skipped, which is
what keeps the digest whole on a home carrying a large fleet.

The bound holds regardless of any particular tasks-axi install, so it does not
depend on the 0.2.5 `show` hang being resolved separately.

* fix(bin): set the wedged-backend latch where it survives, and prove it

The latch added with the read bound was inert. fm_backlog_row_show runs inside
a command substitution in both of its status-capturing callers, so the subshell
read the inherited value correctly but its write died with the subshell. Every
item still paid a full bound and reported `exceeded`, never `skipped`, which
left the large-fleet case the latch existed to cover completely uncovered.

Move the write to the two callers that capture the read's status and own the
surviving shell, and leave fm_backlog_row_show reading the latch only. Correct
the comments that claimed an ownership the function never had.

The test that was supposed to cover this asserted only that the second read
finished under a generous ceiling, which is true whether or not the latch
works. Assert instead that a latched read is strictly faster than one bound and
that it reports its own item as skipped, so an inert latch fails the test.

* test: cover every item the wedged-backend latch skips

The latch assertion exercised a single skipped item, so "every skipped item is
still named" was inferred rather than tested. Probe three items instead and
assert each skipped one names itself and costs less than a bound.

Verified as a real guard by removing both latch writes: the suite then fails on
the first skipped item instead of passing.

* no-mistakes(review): distinguish backlog read-bound hits from absent rows

* no-mistakes(review): preserve read-bound status through the captain verify gates

* no-mistakes(review): Preserve backlog read-bound hits through resolve_entry and reconcile instead of spending them as absent rows

* no-mistakes(review): Preserve backlog read-bound 124 through migrated-prefix scan and remaining task_show call sites

* no-mistakes(document): Document bounded backlog row reads and FM_BACKLOG_ROW_TIMEOUT_SECS

* no-mistakes(ci): Fixed all four failing CI checks with one root-cause fix plus one test-heredity fix. (1) bin/fm-captain-hold.sh: task_show carries the row in TASK_SHOW_OUTPUT and emits no stdout, but four call sites still used the stale command-substitution convention show=$(task_show ...), leaving show empty: task_show_or_fail (every captain hold failed with 'did not retain its hold-set stamp' - broke fm-captain-hold-lifecycle in parallel 1 and fm-bearings-board in serial 3), resolve_migrated_entry (migrated-prefix resolution could never match), reconcile-requests (existing rows were refused as absent), and command_open --identity (printed a constant '#0' identity, so fm-watch-triage's re-held captain call inherited the previous call's silence in serial 1). This is also the Greptile P1. Fixed by invoking task_show in the current shell and reading show=$TASK_SHOW_OUTPUT, the convention the other eight call sites already use; read-bound hits still stop loudly by name. (2) tests/fm-backlog-read-bound.test.sh (serial 4, unclassified family): the new e2e half implicitly relied on the author's process tree containing a harness process so fm-lock.sh would grant the fleet lock; on CI runners the lock is refused, the reconcile sweep is skipped, and the final BACKLOG_RECONCILE assertion fails. Reproduced by simulating a CI ancestry via a ps shim, fixed by pinning the lock evidence with the established fake-ps harness fixture pattern from tests/fm-session-start.test.sh. Verified: shellcheck clean; parallel-1, serial-3, and serial-4 lanes fully green locally (failed=0); serial-1 lane green except fm-gemini-harness, which fails only under local Node v26 (comm=node-MainThread); CI's default Node 22 reports comm=node, the branch that test passes on, so it is not a CI failure

* no-mistakes(document): Verified bounded backlog read docs accurate across branch
…guid#4285)

* fix(merge): serialize the away-authority check with a synchronous merge

bin/fm-pr-merge.sh read the away-posture record for merge authority (the
per-task merge grant and the yolo/away-grant decision) and handed the merge to
the forge afterwards. An archive at the captain's return or a grant revoked by
a replacement record could land in between, so a merge could proceed on away
authority that no longer held.

The away record now carries a cross-subsystem lock, built on the existing
bounded lock primitive rather than a new lock format: the record-mutating
subcommands hold it across their mutation, and the merge holds it across both
its authority read and the forge command. Because a queued or auto merge
returns before the pull request lands, and would therefore outlive the lock,
an away merge is now refused whenever it could land asynchronously: a
requested --auto, a base branch whose merge-queue state does not prove an
immediate merge, and GitLab's asynchronous flags and configuration. What
remains permitted while away is the synchronous merge that lands inside the
lock.

This closes the common away-record/merge race against a live lock owner. It
does not make the merge atomic in every case, and two narrow races are
accepted and documented at their sites rather than hidden, both
confused-agent-grade in the sense bin/fm-lease-lib.sh already uses:

- A merge-queue rule change or a PR base change in the window between the
  queue-free preflight and the forge call can still enqueue the merge, which
  can then land after its grant lapses.
- Killing the lock-owning shell while its gh or glab child is still running
  lets stale-owner recovery reclaim the lock and the record be archived or
  replaced, after which the orphaned child can complete the merge on lapsed
  authority.

Closing either one needs landing verification or an ownership handoff, which
is deliberately out of scope here.

No existing gate is relaxed. The lock is taken after the live green-at-head
verify and the captain-hold check, the in-lock authority read is unchanged,
and a lock that cannot be taken refuses the merge rather than proceeding
unlocked. The away grant stays a structured field; no prose is parsed.

* no-mistakes(review): Fix GitHub rollup fixture base branch

* no-mistakes(document): Document atomic away-authority merge locking

* no-mistakes(ci): Updated two executable GitHub API fixtures to include the required baseRefName. Both previously failing test suites now pass: fm-captain-hold-lifecycle.test.sh and fm-pr-check-security.test.sh. git diff --check also passes
…unchenguid#4200)

* feat(agy): verify Antigravity CLI as third worker/scout adapter

Detection by anchored ancestry in fm-harness.sh (no marker of its own);
bootstrap harness and effort validation; launch template with model and
effort mapping plus reachable-catalog model validation; rendered-tail
busy fallback in fm-busy-lib.sh with delivery footer in fm-composer-lib.sh;
control mechanics with crewmate/scout-only refusal; tmux liveness naming;
router entry with concise adapter reference; dated verification record;
portable regression plus opt-in live drift guard.

Verified live on agy 1.2.0: supervised spawn, durable steering,
same-copy relaunch, and exit, with Herdr-native busy agreement.

* no-mistakes(review): bound agy model probe, gate trust dialog, narrow busy signature

* no-mistakes(review): pre-register agy workspace trust, make readiness gate strict

* no-mistakes(review): Close Orca terminal on gate failure; isolate live-guard HOME; tighten agy matching

* no-mistakes(document): Document agy adapter in stale harness enumerations

* no-mistakes(review): Clamp non-positive FM_AGY_MODELS_TIMEOUT to the default bound

* no-mistakes(document): Fix stale test-shard snapshots after agy lane additions

* no-mistakes(ci): Fixed ci-3 (tests/fm-agy-harness.test.sh:519). Root cause: the agy spawn fixture's default base PATH (/usr/bin:/bin:/usr/sbin:/sbin) omits node's directory, but the spawn drives the real bin/fm-agy-trust.sh (which hard-requires node to record trust) and the fixture's fake tmux trust lookup (node -e) under that PATH. On the ubuntu-latest CI runner node lives in the toolcache (/usr/local/bin), so trust pre-registration failed on portable serial 2; on typical Arch hosts node is in /usr/bin, masking the defect. Fix (smallest, following the existing tests/fm-kimi-harness.test.sh precedent of carrying the interpreter's resolved directory): resolve node from the invoking environment (failing the test with 'test needs node' if absent, as kimi does for python3) and prepend its directory to the fixture's default base PATH; the FM_TEST_BASE_PATH override contract is untouched. Verified locally: (1) pre-fix reproduction with a CI-shaped base PATH (system bins minus node) produced exactly the reported failure — 'node is required to record workspace trust and was not found on PATH' plus the fake tmux 'node: command not found'; (2) post-fix, all 29 tests in the file pass both with node available only via a leading non-standard dir in the base PATH (CI's shape) and with the default base PATH on this host. bash -n clean; ShellCheck is not installed in this worktree (previously recorded as environmental)

* no-mistakes(test): Give agy typed sends a longer submit-confirm budget

* no-mistakes(document): Document agy send budget, trust gate, and control coverage

* no-mistakes(document): Document agy busy fallback inventory and send-timing evidence
…uid#4337)

* feat(afk): add quiet supervision mode for a present captain

Adds a first-class quiet supervision mode alongside /afk for
kunchenguid#2356: the same away-mode daemon, injection,
busy/composer guards, classification policy, and reliability
properties, but the captain staying present and chatting no longer
exits it - only an explicit /quiet off does.

state/.afk's first line now declares its mode (away, the default, or
quiet); fm_afk_mode() in bin/fm-wake-lib.sh is the single reader,
falling back to away for missing/empty/unreadable/unrecognized
content (including the legacy bare-epoch-timestamp format written
before mode existed) so nothing regresses. fm_afk_flag_write()
preserves the on-disk mode on a bare refresh (no explicit mode given)
rather than defaulting to away, which is what keeps the daemon's own
redundant terminal-side re-write from silently resetting a captain's
quiet mode back to away underneath them.

New .agents/skills/quiet/SKILL.md is a thin wrapper cross-referencing
/afk for every shared mechanism, per the one-owner rule. AGENTS.md
gains the state/.afk table entry and section 8's exit-trigger line.
bin/fm-supervision-instructions.sh, bin/fm-session-start.sh, and
bin/fm-guard.sh's stale-watcher banner all become mode-aware so a
quiet-mode captain is never misdirected to /afk in captain-facing
text.

Closes kunchenguid#2356

* no-mistakes(review): Fix AFK epoch parsing and quiet-mode digest wording for two-line flag

* no-mistakes(document): Fix turnend-guard.md daemon-ownership contract for quiet mode

---------

Co-authored-by: NewAiCoder <claude@theinbtw.com>
Co-authored-by: NewAiCoder <170579485+NewAiCoder@users.noreply.github.com>
…kunchenguid#3578)

* fix(bin): let verified harness ancestry outrank retained markers (#3)

* fix(bin): let a structural harness ancestor outrank a retained marker

bin/fm-harness.sh treated a verified environment marker as unconditionally
authoritative, so a Codex session started from an environment that had retained
CLAUDECODE=1 detected as claude. Session start then emitted Claude's Stop-owned
supervision protocol to a Codex primary, and every turn end was blocked for
missing Claude recovery.

The defect is the precedence boundary, not any one harness. codex, opencode,
kimi, and muse publish no identity marker at all, so with markers winning
outright any retained CLAUDECODE renamed them; the Cursor-before-Claude ordering
was a point patch on the same class of problem, and the launch-time marker
clearing only ever covered sessions fm-spawn started.

Markers and ancestry are now separate evidence layers that detect_own arbitrates:

- no ancestry match, or no marker: the single available layer answers, unchanged;
- same harness family: the marker's finer verdict stands, so a launch-selected
  pi-signed is not flattened to pi by an ancestry walk that can only see the
  shared launcher name;
- different harness with a structural (command-name) ancestor: ancestry wins,
  because only ancestry proves who owns the process tree;
- different harness with only a bare-interpreter script-path match: the marker
  wins, since a harness-shaped path in some node process's arguments is weaker
  evidence than a harness publishing its own identity.

The correction is symmetric: a retained CURSOR_AGENT no longer renames a claude
worker nested under cursor either.

Adds fm-harness.sh ancestry [<pid>], ancestry evidence with no marker layer, so
a real harness process can be asked what the walk makes of it.

tests/fm-harness-precedence.test.sh is the portable regression, built from real
renamed processes with no harness installed. Every case drives the two layers
apart and asserts each alone as well as the combination, so no case can pass
vacuously; it also pins Codex's real two-process install topology, since the fix
depends on the native binary being what a tool subprocess meets first. The
opt-in drift guard gains the matching live half: each installed harness's real
running process must still be identified by the ancestry walk, and it fails
naming the harness and version when a release changes that name.

Documentation follows the corrected contract in the script header, the
harness-adapters detection section, the codex, opencode, kimi, and cursor
references, and a dated verification record.

* fix(tests): drop the unused argument pass-through in the shim-topology helper

bin/fm-lint.sh refused the branch: run_shim declared a `[ancestry]` argument and
forwarded "$@", but every call site that varies the environment or passes the
ancestry subcommand invokes the shim entry point directly, so the helper is only
ever called with no arguments (ShellCheck SC2120/SC2119).

Behavior is unchanged: with no arguments "$@" expanded to nothing.

* fix(bin): examine the top of the process chain instead of assuming init

harness_ancestry stopped as soon as the next pid was 1, on the assumption that
pid 1 is always init and can never be a harness.
Inside a PID namespace that assumption inverts: the harness itself is pid 1, so
the walk never examined the one process that proves who owns the tree, reported
no ancestry at all, and handed the verdict straight back to a retained marker.

A real Codex session under `codex sandbox`, holding CLAUDECODE=1 and
CLAUDE_CODE_ENTRYPOINT=cli, is exactly that shape: it resolved claude and
rendered Claude's Stop-owned supervision protocol even with the marker-vs-ancestry
precedence boundary in place.
The same probe now resolves codex and renders the Codex foreground checkpoint.

A host's real pid 1 (init, systemd, launchd) matches no harness name, so
examining it costs one ps call and can introduce no false positive; the walk
still stops once that top process has been read, and a non-numeric or zero ppid
still ends it.

tests/fm-harness-precedence.test.sh pins the namespace shape with a fake ps that
reports every process as bash with ppid 1 and pid 1 as the harness.
The case asserts the marker still answers alone when pid 1 is host-shaped, so it
cannot pass vacuously, and it fails against the previous stop condition.

* docs(verification): record the real-Codex retained-marker evidence

The existing record proved the precedence boundary with the portable regression
and recorded each installed harness's process name behind the ancestry walk, but
it had no evidence from a real Codex process actually holding a retained Claude
marker, which is the failure the boundary exists for.

Adds the dated before/after result from codex-cli 0.152.0 under `codex sandbox`,
with the exact command and the decisive verdict and rendered protocol on each
side, and records the second boundary that shape exposed: the walk must examine
the top of the process chain, because inside a PID namespace the harness is pid 1.
Refreshes the portable regression's observed output for the case it gained.

* no-mistakes(review): blind ancestry in marker-pinned harness tests

* no-mistakes(review): blind ancestry in the Pi guard-routing test

* no-mistakes(review): classify precedence suite, dedupe ps stub, soften claims

* no-mistakes(review): model the spawn-and-wait Codex shim topology

* no-mistakes(document): correct stale muse marker-clearing detection claims

* no-mistakes: apply CI fixes

* fix(bin): examine the top of the chain in the lock and nudge walks too

The pid-1 defect corrected in bin/fm-harness.sh survived unchanged in the two
other harness-ancestry walks, on the exact topology the branch verified against
a real Codex process.

bin/fm-session-lock-lib.sh's fm_harness_ancestry_pids stopped as soon as the next
pid was 1, so a firstmate whose harness is pid 1 of its own PID namespace could
not find that harness at all and did not recognize its own session lock.
bin/fm-sessionstart-nudge.sh carried the same stop plus a blanket rejection of a
lock pid of 1, so the same session was told to run session start again on every
turn.

Both walks now compare the top process before stopping, matching the shape used
in bin/fm-harness.sh.
For the lock walk this is safe because fm_harness_process_matches rejects a
host's real pid 1.
For the nudge, `kill -0` still gates the lock pid, and on a host an unprivileged
`kill -0 1` fails, so a lock file that wrongly names pid 1 leaves the hook silent
rather than acting on init.

Each walk gains one regression case. The lock case drives a deterministic process
table whose pid 1 is the harness and asserts a host-shaped pid 1 still finds
nothing, so it cannot pass vacuously. The nudge case needs a real PID namespace,
because the builtin `kill -0` gate cannot be reached through a fake ps, and it
first proves the same fixture nudges with no lock present; it skips explicitly
where unprivileged namespaces are unavailable.

* no-mistakes(review): assert comm-strength detection from subprocess vantage in drift guard

* fix(bin): verify the live harness guard at the strength the guarantee needs

The marker-versus-ancestry boundary this branch ships is a strength claim:
detect_own hands an args-strength verdict straight back to a retained foreign
marker, so a harness is only protected where the ancestry walk reaches it at
comm strength.

The installed-harness drift guard probed the pane process alone. Under an
interpreter shim the pane process IS the shim, whose own script path is args
strength, while the native binary that carries comm strength is its child. The
guard therefore observed args for Codex, passed, and would have kept passing if
a release stopped spawning that native child at all, while real sessions
silently regressed to the original bug.

fm-harness.sh gains `ancestry-subtree`, which asks the walk from the pane
process and every descendant of it, the vantage a tool subprocess actually
occupies. The guard now requires comm strength somewhere in that set and
requires every vantage to name the same harness.

This supersedes the preceding commit's in-guard leaf walk, which reached the
same vantage but left the logic inside the test file, where CI could not pin it
and nothing else could reuse it. A harness-dependent check needs both halves:
`tests/fm-harness-precedence.test.sh` now carries a portable case proving the
subtree probe reaches a strength the top-of-session probe cannot, mutation
checked twice, once against the pre-change script and once by disabling
descendant enumeration. The subtree walk also avoids depending on tty and
process-group semantics that differ between Linux and macOS.

Verified live: codex-cli 0.152.0 reports [args codex;comm codex] and Claude Code
2.1.257 reports [comm claude].

* no-mistakes(review): narrow drift guard to the upward vantage path

* no-mistakes(review): judge only comm-strength vantages in drift guard

* no-mistakes(document): drop duplicated rationale in detection precedence evidence

* no-mistakes(review): fix pid-1 nudge case vacuity and descent no-arg expansion

* no-mistakes(document): drop branch-relative phrasing in detection precedence evidence

* no-mistakes(review): guard remaining empty positional expansions in fm-harness

* no-mistakes(document): scope cursor marker-ordering claim to the marker layer

* no-mistakes(review): Prefer comm-strength leaves in equal-depth descent ties

* no-mistakes(document): Document comm-strength descent tie-break

---------

* no-mistakes(review): Blind ancestry in stale gemini/rovo marker-precedence tests

* no-mistakes(document): Add missing equal-depth-tie test line to precedence evidence transcript

* no-mistakes(review): Fix stale/vacuous agy precedence test, add agy to precedence suite and docs

* no-mistakes(document): Fix stale kimi.md marker doc missed by ancestry-precedence fix

---------

Co-authored-by: NewAiCoder <170579485+NewAiCoder@users.noreply.github.com>
…rkers (kunchenguid#3944)

Claude Code's external-imports check (hasClaudeMdExternalIncludesApproved)
reads only the canonical git-root project entry in ~/.claude.json, which its
own worktree-to-primary-checkout canonicalization means is never the task
worktree fm-claude-trust.sh registered. The trust dialog kept working
previously only because its check has an ancestor-walk fallback that happens
to reach the worktree entry; the external-imports check has no such
fallback.

Verified by disassembling the installed claude binary and reproducing in an
isolated three-way tmux launch: identical flags registered only at the
worktree key still showed the external-imports dialog, and registering them
at the primary checkout key suppressed both dialogs.

fm-claude-trust.sh now registers all three flags on both the worktree entry
and the primary-checkout entry in one atomic write, and refuses when the
<project> argument is not itself a primary checkout (its own write target
would then be wrong). Extends the harness-adapters Claude reference and the
trust test suite.

Co-authored-by: NewAiCoder <170579485+NewAiCoder@users.noreply.github.com>
NewAiCoder-bot and others added 8 commits September 13, 2026 03:37
…nguid#4355)

The marker lifecycle (fm-wake-lib.sh _fm_recovery_marker_ack) leaves
state/.watcher-down behind in an acked:* state after a downtime episode
is handled. health_snapshot's presence check reported that as an open
gap on every later return, so a handled episode kept surfacing as a
false GAP forever.
…kunchenguid#4361)

* fix(update): rebind fm-procevent-when watches after a self-update

A self-update fast-forwards bin/ in place, changing an armed watch's
action executable bytes with no tampering involved. The watch's trust
binding was hashed at arm time, so the very next fire was refused as
not matching the registered binding and the watch died silently.

Add fm-procevent-when.sh rebind-all: it re-hashes and republishes the
trust binding for every watch whose action executable lives under
FM_ROOT, using the same spec/trust validation as an ordinary fire, and
leaves any watch whose action lives outside FM_ROOT untouched. Wire it
into fm-update.sh right after a successful fast-forward, for both the
primary home and any local secondmate home that advances.

* no-mistakes(review): Canonicalize FM_ROOT for rebind-all's containment check

* no-mistakes(document): Document fm-update.sh's automatic watch rebind and its verification evidence

* no-mistakes(lint): fix(tests): double-quote printf scripts to satisfy shellcheck SC2016

* no-mistakes(review): Reload trust binding from disk before firing to reach live pollers

* no-mistakes(review): Lock the fire-time trust reload against rebind_one's publish race

* no-mistakes(document): Document rebind-all's self-update guarantee and its two review-round test rows

---------

Co-authored-by: NewAiCoder <170579485+NewAiCoder@users.noreply.github.com>
Merge kunchenguid/firstmate (32 upstream commits) into the fork's main.
18 files conflicted across 32 hunks. Each was resolved to keep this fork's
existing behaviour and to absorb the upstream change, never by taking either
side wholesale; where the two sides independently solved the same problem
(portable test-shard packing, pool-slot ownership at teardown, post-update
poll migration) the two mechanisms were composed rather than one dropped.

Per-file decisions, the two places where the brief's conflict surface and
commit direction were wrong, and the judgement calls that need review are
recorded in data/fork-sync-conflict-firstmate/report.md.
Both the fork mate-poll test and upstream's rebind test scaffolded a world
named t12, so in the merged file the second new_world collided with the
directory the first one had already created. The rebind test keeps its own
label and now uses t15 end to end.
@onyx-space
onyx-space merged commit a8552b4 into main Sep 14, 2026
13 of 14 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.