Skip to content

sync: merge upstream main into fork - #9

Merged
sctru merged 342 commits into
mainfrom
fm/fm-upstream-sync-2026-09
Sep 29, 2026
Merged

sctru merged 342 commits into
mainfrom
fm/fm-upstream-sync-2026-09

Conversation

@sctru

@sctru sctru commented Sep 28, 2026

Copy link
Copy Markdown
Owner

Merge kunchenguid/firstmate main (d5c2507) into this fork's main with a true merge commit (not a rebase), keeping the fork's own commits in history.

Merge base correction

PR #3 was squash-merged, so git saw the fork's merge base as a much older upstream commit, which produced about 200 conflicting files.
PR #3's squashed description lists upstream commits through kunchenguid#5001, but its tree only carries upstream through b6930db7 (kunchenguid#4895).
The last five listed commits never landed in the fork: kunchenguid#4972 (stop labeling Herdr experimental), kunchenguid#4973 (treat a live no-mistakes run as current after rebase), kunchenguid#4994 (prevent long worker launch command truncation), kunchenguid#4998 (authorize isolated Herdr lab validation), and kunchenguid#5001 (vision update).
This merge used b6930db7 as the base (via a temporary local graft, since removed), so those five commits are pulled in here along with the rest.
In total this brings in 153 upstream commits.

Upstream changes pulled in (grouped)

Conflicts and how they were resolved

  • AGENTS.md structure (captain decision: option B). Fork docs: move conditional AGENTS.md contracts into triggered skills #8 and upstream docs: move situational AGENTS.md sections into on-demand skills kunchenguid/firstmate#5872 both split AGENTS.md into on-demand skills, with different splits.
    Adopted upstream's AGENTS.md and its skills (session-start-recovery, ship-landing, validation-supervision, operational-home-layout, scout-completion, away-quiet-supervision, agent-skill-trigger-index).
    The fork's docs: move conditional AGENTS.md contracts into triggered skills #8 skills (session-start, task-lifecycle, fleet-supervision, landing-lifecycle, validation-lifecycle, crewmate-briefs, captain-communication, operational-home-reference) and docs: move conditional AGENTS.md contracts into triggered skills #8's "extracted from AGENTS.md" additions to other skills were removed.
    The fork's docs: move conditional AGENTS.md contracts into triggered skills #8 layout is preserved on archive/agents-md-skill-split-pr8 (ad4322f, the pre-sync fork main); pull the split from there later if wanted.
    Re-applied fork-only content to upstream's owners:
    • AGENTS.md: the Captain, message-prefix rule (replacing "at least once per message"), the stricter hard rule 5 sentence, the quota-gate backstop line, nested-worker profile-array routing, the nested delegation pointer, the intake classifier / backlog-operations trigger, the gh-axi wrapper rule, the four scope/review-discipline rules, the pre-branch rebase requirement for briefs, and the CI-facts-versus-hypotheses rule.
    • operational-home-layout: the typed-classifier .env note and the dispatch-log.jsonl entry.
    • agent-skill-trigger-index: the backlog-operations entry.
    • validation-supervision: the "do not mark a scenario live without end-to-end evidence" rule.
    • diagnostic-reasoning: the CI/provider-failure evidence rule.
    • firstmate-coding-guidelines: the verification-cadence section.
  • Fork fix(bin): pause status across a no-mistakes pipeline step #1 pipeline pause wording (dropped; upstream covers it). Upstream fix: require declared waits for workers awaiting their own work kunchenguid/firstmate#5812 now requires a declared wait before waiting on one's own pipeline run, via the brief's status rule.
    Took upstream's fm-dod-lib.sh and fm-promote.sh unchanged, dropping the fork's PAUSED_VERB argument to fm_dod_block and its pipeline-step pause sentence.
    Removed the two fork-only tests asserting that sentence; upstream's test_ship_and_scout_teach_validation_round_pause covers the replacement.
  • Fork Use AGENTS.md without rewriting CLAUDE.md #6 AGENTS.md helper vs upstream fix: limit project memory edits to factual corrections kunchenguid/firstmate#5636 project-memory policy. Took upstream's corrections-only policy: ship briefs no longer run fm-ensure-agents-md.sh and never add project knowledge.
    Kept fork Use AGENTS.md without rewriting CLAUDE.md #6's behavior that the helper writes only AGENTS.md and never creates or changes CLAUDE.md; its header, docs/architecture.md, and docs/scripts.md describe it that way.
    The fork's brief wording that invited recording durable knowledge is gone (fork Use AGENTS.md without rewriting CLAUDE.md #6 brief text changed).
  • Trust-dialog auto-clear in fm-spawn.sh (kept). Upstream did not reimplement the fork's post-launch trust check (Down+Enter for Claude, Enter for Grok 1.0.34 and Codex).
    Kept it alongside upstream's new orca relaunch branch.
    Harness docs merge both views: the fork's auto-clear, plus upstream's rules that Escape on the Claude external-imports dialog records a permanent decline, that Grok trust lands on the primary checkout's path, and that the Grok training opt-in stays off.
  • fm-brief.sh. Kept the fork's nested-delegation section and resolve_directory_input config resolution, and took upstream's branch/forge arguments and brief-include feature.
  • docs/configuration.md. Restored upstream's full "Typed dispatch resolution" section (fork feat(bin): add opt-in intake classifier #5 had cut it to a two-line pointer), including upstream's later additions, and kept the fork's "Typed intake classification" and "Nested delegation" sections and quota env vars.
  • docs/scripts.md, docs/turnend-guard.md, docs/verification/supervision.md, tests/fm-watch-triage.test.sh. Kept both sides: fork quota-gate row and foreign-owner advisory docs, test coverage bullets, and verification record, together with upstream's new rows, restructured lists, and the new secondmate-liveness fixture variable.
  • .agents/skills/fmx-respond/SKILL.md. Took upstream (its Relay contract section replaces the fork docs: move conditional AGENTS.md contracts into triggered skills #8 index stub).

Fork features changed or dropped

Validation

  • CI=true bin/fm-lint.sh (full canonical set: all bin/*.sh, bin/backends/*.sh, tests/*.sh, ShellCheck 0.11.0 with extended analysis, plus actionlint 1.7.12 over 3 workflows): pass.
  • bin/fm-doc-audience-check.sh: pass (surfaces=119 local_links=693).
  • FM_LIVE=0 bin/fm-test-run.sh --all on the merge commit (local, inside a Claude worker pane): FM_TEST_SUMMARY total=243 failed=13 skipped_gate=48.
    • 3 were real interactions between fork code and new upstream behavior, fixed in the follow-up test commit and re-run green: tests/fm-brief.test.sh (fork pause assertions superseded by fix: require declared waits for workers awaiting their own work kunchenguid/firstmate#5812), tests/fm-spawn-trust-prompt.test.sh (upstream fix(bin): ensure resumed worker launches enter their recorded worktree kunchenguid/firstmate#5916 adds a pane capture after the pre-launch cd, which consumed the fake's dialog frame; the fake now shows the dialog only after launch, and production behavior is unchanged), and tests/fm-watch-triage.test.sh (new upstream fixtures lacked the .meta the fork's orphan-status guard requires, the same fix PR sync: merge upstream main into fork #3 applied).
    • 1 passed on re-run: tests/fm-test-run.test.sh.
    • 1 is local environment only: tests/fm-procevent-quota.test.sh fails under this host's umask 0002 and passes under umask 022 (CI's default); pristine upstream fails the same way here.
    • 8 fail identically on a pristine origin/main checkout on this host, so they are environment baseline rather than merge regressions: fm-backend (zsh herdr sourcing), fm-control-relaunch (Herdr Pi relaunch), fm-gemini-harness (the run happens inside a Claude pane), fm-on and fm-remote-doctor (remote runtime tools absent), fm-supervision-host (Stop hook write), and fm-gate-refuse plus the fork's fm-quota-gate (both blocked by a stale group-writable /tmp/fm-spawn-ok directory from 2026-09-19 on this host, which spawn correctly refuses to reuse).
  • CI on this PR is the clean-environment signal for the above.

feilipu and others added 30 commits September 9, 2026 07:17
)

Opt-in IMAP/SMTP mail plane (fm-mail.sh / fm-mail-check.sh). Absent FM_MAIL_* stays off.

Speaking as Kun's firstmate: this is merged. Thank you @feilipu — really appreciate you taking the time on this.
* fix(remote): start the fm-remote Herdr agent through a login shell

Launchd was exec-ing herdr directly, so the Aqua agent inherited a background session without login-keychain access. Start it via /bin/zsh -lc exec so panes keep login env and can refresh OAuth tokens after reboot.

* fix(remote): start fm-remote Herdr via the account login shell

Resolve UserShell from Directory Services and invoke it with separate -l and -c so bash, fish, and zsh all get login-keychain access. Fall back to SHELL, then /bin/zsh, then /bin/sh without failing the render.

* no-mistakes(review): Fix launch-agent shell fallback resolution

* no-mistakes(review): Preserve and escape Directory Services shell paths

* no-mistakes(document): Document login-shell LaunchAgent behavior

* no-mistakes(ci): Updated the trusted fm-remote-doctor SHA-256 identity in bin/fm-remote-entrypoint.sh. Verified with tests/fm-on.test.sh, tests/fm-remote-doctor.test.sh, bash syntax checks, and git diff --check

* no-mistakes(ci): Resolved the login shell exactly once per doctor invocation and threaded it through plist rendering, installed/loaded contract validation, repair reporting, and post-repair checks. Added a regression test proving repeated repair remains healthy and performs no reload when a hypothetical second Directory Services lookup would differ. Updated the trusted doctor hash. Verified doctor, fm-on, remote-entrypoint, lint tests, ShellCheck, syntax, and diff checks

* no-mistakes(ci): Made Darwin shell resolution hermetic with executable injection and a 2-second Directory Services timeout. Updated tests to inject shells by default, isolate dscl-specific cases, parse plists semantically, and verify stalled dscl fallback. Updated the trusted doctor hash. Doctor, fm-on, entrypoint, syntax, hash, and diff checks pass

* no-mistakes(ci): Raised portable serial CI timeout from 20 to 30 minutes, refreshed the specified timing hints, added missing hints, and recomputed shard documentation. Verified coverage, runner behavior tests, workflow lint tests, shell syntax, requested timing maxima, and diff checks
…enguid#3710)

* fix(bearings): keep captain-approved deliveries in Recently Landed

A closed task is never held: tasks-axi clears the held flag when a task
closes and keeps hold-kind and the hold reason as the record of the call
that was made. Recently Landed excluded every Done row whose hold-kind was
captain, so the marker it treated as "closed while still waiting on the
captain" was in fact the proof that the captain had approved the work. Every
merge routed through a captain decision disappeared from the list of what
shipped, including under --all-landed.

The selector now asks whether the closed row delivered something. Recently
Landed is merged PRs, completed scouts, and finished local-only merges, so a
row carrying one of those artifacts belongs there whoever approved it. A
captain question closes with an answer and no artifact of its own, and that
is what still stays out, so an answered question is never rendered as
shipped work.

The same rule was written twice - the bearings projection selects this
home's Done rows and the fleet snapshot selects each secondmate home's Done
rows into the roll-up the same section merges in - which is why one defect
hid deliveries in every home. Both now share bin/fm-landed-lib.sh.

* fix(review): Normalize landed evidence and exclude answered captain questions

* fix(review): Normalize captain delivery evidence across relocated data

* fix(review): Record authoritative delivery provenance with legacy fallback

* fix(review): Harden delivery provenance across forced and pruned completions

* fix(review): Replace premature merge closure with existing release contract

* fix(review): Document provenance-based Recently Landed selection

* fix(document): Align documentation with completion provenance

* fix(lint): Fix targeted ShellCheck warnings

* fix(ci): order the pinned tasks-axi install before its stock-Bash consumers

In `.github/workflows/ci.yml` the pinned tasks-axi install now precedes both
stock-Bash consumers, and the Bearings expectation is updated from 49 to 50
tests.

Verified with macOS Bash 3.2: snapshot 16/16, Bearings 50/50, public-followup
1/1. Full repository lint and all three workflow validations pass, and
`git diff --check` is clean.

* fix(review): Make completion provenance unambiguous

* fix(review): Make completion verdict authoritative over quoted provenance

* fix(review): Preserve retained artifacts through resumed captain closes

* fix(review): Unify completion provenance ordering across writer and reader

* fix(review): Preserve artifacts across failed captain closes

* fix(review): Refresh v1 assertions; provenance authority remains unresolved

* fix(review): Remove unreliable provenance while preserving landed deliveries

* fix(review): Reject stale home summaries visibly

* fix(review): Restore retained deliverable recording

* fix(review): Match landed artifacts and restore retention documentation

* fix(review): Disambiguate captain calls and restore landed artifact matching

* fix(review): Persist retained report and PR artifacts

* fix(review): Preserve staged artifacts before captain answers

* fix(review): Avoid wedging answers on unsupported report paths

* fix(review): Exclude unreleased captain-held pull requests

* fix(review): Exclude held local-only answers from landed

* fix(review): Preserve retained scout reports across snapshot rendering

* fix(document): Align landed lifecycle documentation with release semantics

* fix(review): Enforce landed artifact-kind ownership

* fix(review): Infer canonical task kinds in snapshots

* fix(review): Require captain-hold release before merges

* fix(review): Qualify merge lifecycle regression evidence

* fix(review): Serialize captain holds with merge operations

* fix(review): Document merge cleanup residuals honestly

* fix(test): Replace vacuous Bearings regression with behavioral cases

* fix(document): Align Bearings verification and merge lifecycle documentation

* fix(review): Serialize merges and exclude captain calls from landed

* fix(review): Harden merge identity and landed selection

* fix(document): Clarify landed selector compatibility filtering

* fix(bin): keep merge entrypoints usable on records without an incarnation

The merge identity guard refused any task record with no spawn_gen field.
That field identifies one exact incarnation, so comparing it across the wait
for the merge lock is what catches a task relaunched while the merge was
queued. Requiring it to be present is a different rule, and it refused every
record written before the field existed: a legacy task could no longer be
merged at all, and five behaviour suites refused before reaching the check
they were written to exercise.

The comparison only needs to notice a change. An absent field is now read as
an empty incarnation and compared like any other value, so a record that
gains, loses, or alters one is still refused, while a record that simply
predates the field merges. An ambiguous or unreadable field stays an error,
because a record that cannot name one incarnation cannot be compared. The
missing-record message each entrypoint had before the guard is restored, so
a genuinely absent record still says so in its own words.

The role partition now precedes reading the record. Refusing the supervision
branch is a statement about the actor, not about the task, so it cannot
depend on a record the wrong actor may not have.

A backlog file that does not exist meant "no longer an open captain call".
For a caller that asked to tell absence apart it now means absent, so a board
card whose home carries no backlog stays visible instead of being dropped as
resolved.

Fixture repositories pin their initial branch instead of inheriting
init.defaultBranch, which resolved to main on a developer machine and master
on a runner, so a fixture naming main failed only in CI.

* fix(review): read local-only note from body; surface pending-close failures

* fix(review): keep kindless local-only landings in Recently Landed

* fix(review): bind local-only note scan to the tasks-axi note line

* fix(review): Guard unavailable captain-hold authority records

* fix(document): Document unreadable authority predicate outcome

* fix(bin): read an absent backlog as absence, not an unreadable record

The merge gate refused every task whose home carries no backlog file. A
backlog that does not exist holds no captain call, so nothing can be held and
the merge is safe; only a backlog that exists and cannot be read may hide a
live hold. Those two states were collapsed into one refusal, which stopped
merges in any home that keeps no backlog.

The predicate now reports a missing backlog file as absence, alongside a row
the backlog does not carry. A record that exists but cannot be read still
leaves by the existing cannot-tell path, which both merge entrypoints already
refuse, so the restrictive direction is unchanged.

That leaves no way to reach the separate unavailable-record result, so the
result and the two branches that handled it are removed rather than left
describing an outcome that can no longer occur. The lifecycle documentation
loses the same claim.

Regressions cover both directions in each entrypoint: a home with a task
record and no backlog merges, and a backlog present but unreadable refuses
without reaching the forge.

* fix(review): Fail closed unreadable backend configuration

* fix(tests): pin the bare origin's initial branch in the remote seed fixture

The fixture created its bare origin with no initial branch, so that
repository's HEAD followed init.defaultBranch while the source repository
pushed the branch fm_git_init_commit pins. On a host that still defaults to
master the two disagreed: the bare origin's HEAD named a branch the push never
created, cloning it warned that the remote HEAD referred to a nonexistent ref
and checked out nothing, and the seed assertion for the cloned README failed.

A machine whose default is already main paired the two by accident and hid it,
which is why the fixture passed locally and failed on the runner.

Pinning the bare origin to the same branch removes the dependency on the
ambient default from both sides. Verified under both conditions: with
init.defaultBranch set to master, and set to main, the suite passes 26 of 26.

* fix(review): Fail closed unreadable user backend configuration

* fix(bin): republish the home summary as v1 and record two load-bearing rules

The published home-summary schema had moved to v3, which routed every
secondmate home still emitting the earlier version to the stale branch: their
landed rows, open decisions and holds all came back empty and their state read
as unknown until each home was updated. The payload never justified that. Its
field set, field order, truncations and the landed array construction are
byte-identical to v1, so only which rows the selector places in landed
differs, and a v1 consumer reads that the same way.

Republishing as v1 removes the rollout regression and, with it, the tolerance
machinery that existed only to soften the bump: the stale-schema predicate,
its two collection branches, the flag and its provenance branch, the omitted
surface that can no longer be reached, and the fixtures and assertions that
covered them.

Two rules that a scope review proposed removing are kept, each now carrying
the reason it exists, because both were measured to be load-bearing:

The artifact-kind ownership clause is what keeps an explicit scout that
recorded no report out of Recently Landed. Without it such a row has none of
the three artifacts, satisfies the compatibility fallback and renders as
shipped work with an empty artifact.

The kind fallback is needed because tasks-axi omits the kind metadata
entirely when a title begins with a canonical keyword. Without it a scout
titled "SCOUT ..." reports no kind, its recorded report stops counting as a
delivery, and it drops out of the section this selector exists to repair.

* fix(bin): move the scout guard note onto the rule and drop two dead pieces

The LOAD-BEARING note sat on an unreachable branch. Measured in both
directions: removing that branch together with the kind-is-not-scout guards
lets an explicit reportless scout into Recently Landed and fails
tests/fm-captain-hold-lifecycle.test.sh, while removing the branch alone
leaves that suite passing at 49 assertions. The guards carry the rule, so the
note now sits on them and the unreachable branch is gone. A note pointing a
later reader at the wrong line is the hazard this change corrects elsewhere.

summary_file_has_schema lost its only caller when the stale-schema machinery
was removed, so it goes with it.

* fix(review): Fix legacy report artifacts and canonical keyword boundaries

* fix(review): Update pinned Bearings test count to 56

* fix(document): Clarify landed summary compatibility documentation

* fix(review): Preserve unreadable backend configuration errors

* fix(review): Honor backend resolution errors at existing call sites

* fix(test): Stabilize remote collector tests under host load

* fix(document): Document backend resolution failure contracts

* fix(lint): Suppress intentional deferred probe expansion warnings

* fix(ci): Captain, quoted the two literal test IDs in tests/fm-backlog-atomicity.test.sh to fix SC2100 without changing behavior. Both warnings reproduced before the fix; the targeted fm-lint.sh run now passes with ShellCheck 0.11.0. Bash syntax and git diff --check also pass

* fix(ci): Fixed the resolver’s two configuration-parent checks to return 2 for inaccessible directories while preserving genuine absence. Added two behavioral tests; RED/GREEN and both requested mutation proofs confirmed. All 10 focused checks, targeted lint, syntax, and whitespace checks passed. Broader merge suite stopped after 10 passing cases under host load. Declined portable checks and merge-authority code remain unchanged
…nguid#4090)

* fix(remote): let the Aqua launch agent own the fm-remote Herdr session

A herdr server keeps the macOS audit session of whatever started it, and
only the Aqua login session (gui/<uid>) can read the login keychain
without a prompt. Herdr's SSH remote attach starts the fm-remote server
as its own child when it finds none, wins the socket at boot because sshd
accepts connections before the login session exists, and every claude
pane under that server then gets `security` exit 36, falls back to a stale
plaintext credentials file, and reports "Login expired". launchd's own job
lost the socket on every KeepAlive retry and the doctor still reported the
session ready because it only asked whether any server answered.

- Add bin/fm-remote-herdr-guard.sh, the launch agent's exec target: start
  the server in the foreground when nothing owns the socket, exit 0 when an
  Aqua-born server does, and otherwise stop the foreign server, wait for the
  socket, and exec the server at once.
- Add bin/fm-remote-herdr-owner-lib.sh, the single owner of socket-owner
  discovery (lsof; pgrep cannot see herdr's argv on macOS) and the birth
  markers (SSH_*, XPC_SERVICE_NAME, FM_REMOTE_JOB_ACTIVE, sshd or
  remote-client-bridge ancestry matched on argv[0] and whole arguments).
- Render the agent as the login shell exec'ing the guard with
  KeepAlive={SuccessfulExit=false} and ThrottleInterval=10, check the loaded
  job's successful-exit semaphore, and report a session served outside the
  Aqua login session as fixable so --fix retakes it through launchd; the
  reload waits for an Aqua-born owner rather than any running server.
- Correct the doctor and docs: the launch shell provides environment parity,
  the launchd domain provides keychain access.
- Pin the guard's decision table and the doctor's verdicts against real
  marker-carrying processes, and record the dated audit-session evidence.

* no-mistakes(review): Verify Aqua ownership through launchd domains

* no-mistakes(document): Document macOS lsof ownership requirement
…uid#2881)

* fix(bin): prefer a live no-mistakes run over a terminal one

A worktree can bind to more than one recorded no-mistakes run at once.
The branch-and-code-identity rule in bin/fm-nm-run-lib.sh accepts both an
exact-equal commit and a worktree-is-an-ancestor match, but never stated
which wins when both bind, so the tie fell to whichever candidate the
caller reached first.

Observed on a live fleet: a crashed validation daemon left a FAILED run
at the worktree's own commit while the live run that replaced it
validated a descendant commit on the same branch. Bare `axi status`
answers with the most-recently-touched run - the corpse - and it bound by
the equal-commit rule, so every recomputation reported `failed` for a
task whose real run was healthy. The same label had also read `failed`
earlier while the work was genuinely stalled, so the signal was wrong in
both directions.

State the live-over-terminal policy in the matching rule's own contract,
where the equal-commit and ancestor rules already live, and add
fm_nm_run_status_class as the one classifier that decides liveness from a
recorded status word. fm-crew-state.sh applies it on both selection
paths: the runs listing now scans past a terminal row for a live one, and
a terminal `axi status` answer is provisional until the listing has been
asked whether this worktree also has a live run.

Same-liveness-class candidates keep the listing's newest-first
precedence, and a status word the classifier cannot place keeps the
caller's own ordering rather than displacing a known result, so a
single-run task and a task whose runs are all terminal are unchanged.

Regression coverage reproduces the proven case (terminal run at the
worktree's exact commit plus a live run descending from it) and its
runs-list twin; both fail under the old tie-break. Two companion cases
pin the no-widening half - two terminal rows still resolve newest-first,
and a terminal run with no live sibling keeps its full run-step detail -
and both pass before and after the change.

* no-mistakes(review): accept unfetched live sibling anchored at exact worktree head

* docs(bin): name both ledger reads behind the runs-limit setting

The FM_CREW_STATE_RUNS_LIMIT comment in bin/fm-crew-state.sh still described
the runs ledger as scanned only by the cross-branch fallback, but the
live-over-terminal fix also consults it as the live-sibling probe behind a
terminal axi status answer. Point the comment at docs/configuration.md as the
setting's owner instead of restating a second copy.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
…chenguid#4009)

* fix(procevent): make the ordinary stop signal actually stop a runner

The owner guard that shipped in kunchenguid#3904 half-reaps. Against a poll child that
handles the ordinary stop signal and keeps waiting, the guard signals the group,
loses the runner leader to its own signal, then reads that success as a
leaderless group and exits without escalating. It destroys the only proof of
ownership that would have authorised the forced signal, so the survivor becomes
unreachable by retire, reconcile, sweep-home and the guard alike. A guard that
turns a leaking-but-identifiable generation into a permanently unreachable one
is worse than no guard at all.

Two defects, and they hid each other:

- The escalation re-derived ownership from the leader. `runner_group_signal`
  now takes a `proved` mode, passed only by the escalation inside the stop that
  already proved and signalled that exact generation moments earlier. A leader
  dying to our own signal is the ordinary outcome, not fresh ambiguity.
- Every stop held the per-source lock across its wait while the runner's own
  exit cleanup waited unboundedly for that same lock. That circular wait was
  broken only by the forced signal, so the forced signal silently became the
  normal path - and, by keeping the leader alive through the whole window, it
  masked the escalation defect above. The runner's exit cleanup now refuses that
  lock instead of waiting for it, which is what its existing `return 0` already
  said it did.

Fixing the lock alone would have turned every stop of a signal-proof child into
a refusal that leaves it running, so both land together and the tests pin that.

Measured on macOS with a stand-in poll child that traps TERM, INT and HUP:
the guard left it running past 70s and now clears the group within the lease
plus one check; retiring a healthy runner fell from ~2.8s with a forced group
signal every time to ~0.6s on the ordinary signal alone.

Unchanged and stated deliberately: a leader lost to anything other than the
stop's own signal still leaves a group that retire, reconcile, sweep-home and
the guard all refuse, permanently - and that source stops listening without
saying so. Whether such a group may ever be signalled is an open decision and
is not answered here.

* fix(review): Fix proved escalation race and stop regression assertions

* fix(review): Preserve proved escalation through transient identity failures

* fix(review): Simplify proved escalation and correct guard timing documentation

* fix(document): Clarify process-event stop ownership and cleanup limits

* fix(document): Clarify process-event stop ownership and fixture comments

* revert(skills): restore the leaderless-ambiguity limit to the loaded skill

An automatic documentation step in this branch's validation edited
.agents/skills/process-event-sources/SKILL.md, which no instruction in this
change asked it to touch. That file is not documentation about the code: it is
the agent-loaded instruction surface, what an agent reads to know what it is
permitted to do.

The step deleted this line:

  - leaderless PID/PGID-reuse ambiguity preserves the claim without signalling
    or replacement, as owned by the operating contract in
    [`docs/configuration.md`](../../../docs/configuration.md#process-to-event-sources-stateprocevent);

and folded it, with its neighbour, into a generic "registration and ownership
transitions, stop authority, and claim reclamation follow the operating
contract".

That deleted line states a PROHIBITION - that such a group is preserved WITHOUT
SIGNALLING - and it is the exact limit an open captain decision currently rests
on. Folded into a pointer, an agent reading the skill to learn what it may do
would have to chase a second document to discover it may not signal. A
prohibition that requires a second lookup is not a prohibition. The effect was
to weaken, in the instructions themselves, the boundary that keeps one home from
signalling another's process group - while the question of whether that boundary
should move at all is still open.

This is a deliberate revert, not an oversight, and it restores the file exactly
to its pre-branch state. The full statement also survives in
docs/configuration.md; that does not rescue it, because the agent handling a
process-event wake loads the skill and not the documentation.

* revert(procevent): restore the open-question marking beside the escalation

The same automatic documentation step that edited the loaded skill also removed
this from the comment above runner_group_signal:

  A leaderless group nobody in this call ever proved remains refused too, for
  every caller. That untouched refusal is what makes a crashed leader's group
  permanent, and relaxing it is a separate open question, not something this
  path assumes.

and replaced it with a pointer to docs/configuration.md.

This one fails differently from the skill deletion, which is why it is restored
separately. There, a prohibition was moved out of the reader's path, and a
missing prohibition gets violated. Here the prohibition survives in code - the
unproved path still refuses - and what was removed is the fact that the limit is
UNDECIDED. A prohibition that has quietly lost its "this is still open" reads as
settled design, and settled design gets relied on, extended, and eventually
relaxed by someone confident they understand why it is there. That question is
open right now.

The rule this branch's four instances produce, stated once here because this is
the point of decision: an unresolved question must be marked unresolved AT THE
POINT OF DECISION, not only where the contract is documented. A reader who does
not know something is open will treat it as closed, and that default is stronger
than any pointer overcomes.

The pointer added by that step is kept alongside; this restores what it replaced
rather than reverting it.

* docs(verification): restore the measured guard bound and its reason

The document step's rewrite of this record dropped the concrete figure while
keeping the surrounding measurements. What went missing was the bound itself -
lease plus two consecutive failed checks plus the stop's grace, roughly 630
seconds at the shipped 600-second lease and 15-second check - together with the
reason there are two checks rather than one: a single unreadable read must not
be enough to kill a live runner.

The mechanism survived elsewhere and the reason survived in
docs/configuration.md, so nothing was lost from the repository. The concreteness
was, and that is what this restores. A number recorded without why it is that
number is the one a later reader shortens; the reason is the whole safety
argument for the debounce, and the debounce is what stops the reaper killing a
live runner on one bad read.

* fix(document): Replace stale stop-authority summaries with owner pointers

* test(procevent): make the guard-bound case able to fail for its own reason

An automated reviewer observed that this case allowed sixty seconds for a bound
of roughly eight, so it could not go red for the reason it names: it would have
passed a guard that took fifty-five seconds. That is correct, and it is the same
family as the defect the case exists to defend against - a check that is green
because it cannot fail, rather than because the thing it guards is working.

The deadline is now derived from the bound itself - the lease, plus the two
consecutive failed checks the guard debounces on, plus the stop's own ordinary
and forced signal windows - rather than from a flat wall-clock number, and the
shortened lease and check the fixtures run under have a single definition so a
derived deadline cannot silently diverge from the settings the guard is given.
The doubling that remains is a load allowance and is documented as one; widening
it to make a slow guard pass would convert the assertion back into decoration.

Proven by mutation rather than by argument. Against the repaired case:

  correct code                                        ok
  guard debounces on 20 misses instead of 2           not ok - "still holding
                                                      the group after 16s,
                                                      against a documented
                                                      bound of 8s"
  proved escalation removed (the original defect)     not ok - same
  code restored                                       ok

The previous sixty-second version passes every one of those mutations.

The reviewer's other claim, that the guard can survive past the announced bound
when an owner disappears immediately after a check, was measured and does not
hold against what this branch announces. Sweeping the phase deliberately at
0.0, 0.2, 0.4, 0.6 and 0.8 of a check interval gave 7.21s, 7.31s, 6.75s, 6.49s
and 6.31s, worst 7.31s, against the announced lease plus two consecutive failed
checks plus stop grace, which is up to 8s at those settings. The mechanism the
reviewer describes is real and is the announced mechanism; the bound it was
measured against is a phrasing this branch no longer carries.

* revert(scope): return the instruction surfaces to their base state

This delivery is being split. It carries the two proven process fixes alone; the
instruction text travels separately, through a run that removes the
documentation step rather than refusing it at its gate.

Two surfaces are therefore returned to exactly what the base branch has, so this
delivery neither adds to them nor removes from them:

  .agents/skills/process-event-sources/SKILL.md - identical to base again. Three
  bullets an automatic documentation step had folded into a pointer, including
  that leaderless PID/PGID-reuse ambiguity preserves the claim WITHOUT
  SIGNALLING and that there is one identity-matched owner per canonical source
  across homes sharing one store.

  The header comment block of bin/fm-procevent.sh, which is what the script
  prints as its own help. Seven lines were removed from it: that a live owner is
  never displaced, that only a claim whose stale owner and independently absent
  process group prove its whole generation gone is reclaimed, that a crashed
  leader or reused pid whose process group still has members cannot relax
  ownership cleanup, and that reconcile signals only a live identity-matched
  runner group and otherwise keeps the claim without starting a replacement.
  The help output is now byte-identical to base.

Neither removal was requested by any instruction in this change, and both were
made to text that predates it. Returning them is scoping, not a third
restoration: nothing is being added to those files here.

* fix(ci): Captain, live CI revealed a fixture deadlock: it suspended the runner before startup released its lock. Added a public-list synchronization barrier in tests/fm-procevent.test.sh. Forced-delay reproduction detected the deadlock before the fix; all four cases passed afterward. Targeted lint, Bash syntax, and whitespace checks passed. Greptile’s watchdog requirement conflicts with the recorded R2 decision; runtime behavior and documentation remain unchanged. Full CI rerun belongs to the outer executor

* test(procevent): make the post-TERM cases report what they saw when they fail

On the failure path only, these cases now print what they actually saw: the
identity recorded at claim time, the identity readable at that moment, the size
of the signals file, the leader's state and wchan, every live member of the
runner's process group with its own state and wchan, the elapsed time since the
stop began, and what retire said. None of it runs when a case passes.

WHY THIS IS KEPT, stated accurately rather than by its original reason. It was
written to make an unexplained CI failure verifiable. That failure is now
explained - it was a fixture deadlock, diagnosed and repaired in the preceding
commit - so that justification has expired and is not the reason given here.
The reason it stays is smaller and independent of that failure: it is already
written, it is small, it sits in the file whose assertion this change reworked,
and an assertion that could not say why it failed cost most of a morning to
diagnose from the outside. The next failure will not be this one.

WHAT A PASSING RUN WOULD NOT MEAN: a pass is a sample of behaviour already
observed many times, not proof that anything is fixed. Only a failure carrying
the evidence above establishes a cause.

* fix(document): Clarify process-event fixture diagnostic rationale

* fix(ci): Captain, fixed two cleanup races in tests/fm-procevent.test.sh: removed premature child completion and waited for runner exit before retiring the restart fixture. Controlled Linux reproductions demonstrated failure before and success after. The full Linux process-event suite, six focused macOS checks, targeted ShellCheck, Bash syntax, and whitespace checks passed. Runtime behavior, guard debounce, and documentation remain unchanged. CI rerun belongs to the outer executor

* fix(procevent): bound owner-guard cleanup at one check interval, not two

A THIRD WAY, not a capitulation to the reviewer and not a refusal of it.

The automated reviewer's grievance was the LOOSENESS OF THE BOUND, not the
number of observations the guard makes before it acts. It asked for a single
read because that was the only route it could see to an acceptable bound. There
was another route, and this change takes it: the bound is reached and both reads
are kept.

TIGHTENED - the SPACING of the guard's two reads, not their number. The owner
watchdog now sleeps half the configured check interval and still requires two
consecutive failing reads, so the pair completes inside one check interval
instead of costing two. Worst-case detection falls from the lease term plus TWO
check intervals to the lease term plus ONE. At the shipped 600s lease and 15s
interval the stated bound falls from ~635s to ~620s.

PRESERVED - the second read. bin/fm-procevent.sh's two-consecutive-miss rule is
untouched. WHY IT PROTECTS: the guard's inputs are a lease read and a state-root
identity read, and either can fail transiently on a live, healthy home. Acting
on the first failure would let one isolated unreadable read kill a live service.
Requiring a second, independent read is what makes that impossible, and it is a
protection rather than padding. Nothing was traded away to reach the bound.

Both properties are now guarded by their own case, and each was proven by
MUTATION rather than asserted:

  - putting a full interval back between the two reads fails the bound case:
    "still running 17.0s after the last owner activity, against a documented
    bound of 15s";
  - acting on one failed read fails the new debounce case: "one unreadable lease
    read ended a runner whose home was still alive" - while the bound case then
    passes FASTER, 9.9s against 13.1s. The unsafe variant being the quicker one
    is exactly why these are two cases: one elapsed-time case would have
    registered the removal of the protection as an improvement.

MEASURED, sampling the phase between the guard's check clock and the lease clock
across eight runs per variant, on macOS (Darwin 25.5.0). Reaping an orphaned
listener whose home stopped refreshing its lease:

  lease 2s / interval 1s:  4.41-5.29s before,  3.48-4.65s after
  lease 2s / interval 4s:  7.69-8.12s before,  5.94-6.13s after

The 4s configuration is the informative one: the gap is about one check
interval, which is precisely the term that was removed.

A previously unstated term of the bound surfaced while measuring: the lease age
is compared in whole seconds, so a configured lease of N is honoured until that
age reads N+1. It is now part of the documented bound and of the regression's
derivation instead of being absorbed into a fudge factor.

The bound regression derives its deadline from the documented bound instead of a
flat number, and PINS the phase between the guard's check clock and the lease
clock rather than sampling it, because with a sampled phase a guard spending two
intervals passes about half the time on a lucky alignment. Its load slack is
additive and stays under half a check interval, so an extra whole interval
cannot hide inside it. The two flat deadlines that were there before (40s and
20s) and the doubling allowance on the derived one are gone; that looseness was
the reviewer's third complaint.

The stop's own grace is untouched: 2s for the ordinary signal, then 2s for the
forced one. It is a ceiling paid only by a group that outlives the signal it was
sent, not a delay every stop pays - a healthy runner's whole retire measures
0.40-0.66s on this host. The reviewer's literal "lease plus one tick" is
unreachable by any implementation, since signalling a process and giving it any
chance to exit takes non-zero time; detection now meets it and the stop runs
inside its own ceiling, and the contract says so rather than glossing it.

NECESSARY BUT NOT SUFFICIENT, and written BEFORE this head's integration runs
start rather than after they report. On the previous head, "Behavior portable
serial 1" and "Behavior portable serial 4" were both CANCELLED at the job
ceiling, independently of this finding. A new head triggers fresh runs, so those
two lanes MAY complete this time. IF THEY DO, THAT IS NOT EVIDENCE THE CEILING
DEFECT IS FIXED. It is one more sample of a lane that has been cut repeatedly
and sometimes is not; the shard-packing repair for it is open separately. Do not
reread a lucky pass here as a resolution.

Relatedly, and deliberately: the per-script duration hint in bin/fm-test-run.sh
was NOT updated even though the two new cases add ~19s of wall clock.
docs/fm-test-portable-shards.md says those hints are replaced wholesale from CI
timing artifacts of green runs, and that repair is the open request doing it; a
hand-edited estimate here would collide with it and silently repack the shards.
This suite runs in portable serial shard 3, which was green in the last run.

Verification: tests/fm-procevent.test.sh green, plus
tests/fm-captain-hold-lifecycle.test.sh, the test-coverage guard, and
bin/fm-lint.sh. The unrelated "reconcile stops a runner whose registration was
removed" case flaked in 4 of 7 local full runs; an isolated 20-trial
reproduction measured it at 13/20 unclean before this change and 11/20 after, so
it is issue 4080 and is not aggravated here.

* fix(procevent): repair our decimal-interval regression and enforce the timing phase

REPAIRED BEFORE PUBLICATION, AND IT WAS OURS. The half-interval arithmetic added
by the previous commit read a zero-prefixed interval as octal: 010 halved to 4
instead of 5, and 08 was not a number at all, so the owner guard died before
reporting ready and the runner failed closed and never listened. The validator
accepts those values and `[` compares them as decimal, so this broke a
configuration that worked before. Introduced by this delivery, found in review,
repaired here. Forcing base ten before the arithmetic is the whole runtime fix.

Proven by driving it rather than by reading the source: a new case starts a real
listener at 08 and at 010 and observes the guard's actual sleep argument - 4s and
5s. Removing the normalisation turns that case red with "a zero-prefixed decimal
interval (08) prevented the listener from starting".

THE TIMING PHASE IS NOW OBSERVED AND ENFORCED, NOT ASSUMED. The bound case
pinned its phase by CONSTRUCTION, from an assumed startup time, and enforced
nothing. Review was right that this is not enough: once startup reaches about two
seconds the expiry lands in a different part of the interval and the case
silently stops rejecting a two-interval guard while still reporting success. A
bound that cannot fail for the reason it names is the defect this whole delivery
exists to correct, so it must not ship inside the fix for it.

Now the lease is synchronised to the guard's own FIRST observed lease read,
every later real read is recorded, and the case REFUSES unless one recorded read
proves the required phase: it read the synchronised reference, it was still
fresh, and it began late enough that two further full intervals could not finish
before the deadline. An unestablished precondition refuses; it does not proceed
on trust. The derived deadline, the two-read debounce and the additive slack are
unchanged, and the slack invariant is now asserted rather than left to a comment.

Review also found the deadline was only ever checked while the group was still
alive, so a sampler descheduled past it would see the group gone and certify
success. The observed completion time is now checked too.

PROVEN BY MUTATION, each one run against this code:

  - remove the decimal normalisation -> the interval case fails on 08;
  - a full interval between the two reads -> "the guard exceeded its bound:
    group still running 17.1s ... against a documented bound of 15s";
  - a full interval WITH startup forced to ~2.5s, which is exactly the condition
    the old construction pin could not survive -> still red, same message;
  - the same ~2.5s startup with the correct guard -> still passes, 13.0s against
    the 15s bound, so the delay alone does not break the case;
  - phase evidence made unavailable -> "could not establish the required
    pre-expiry guard-read phase", a refusal rather than a pass, even though the
    group stopped quickly;
  - act on one failed read -> the debounce case fails and the bound case passes
    FASTER, 9.5s against 12.7s, which is why these remain separate cases.

Verification: full tests/fm-procevent.test.sh green, and bin/fm-lint.sh clean.

* fix(document): Correct process-event timing and debounce comments
…kunchenguid#3417)

* fix(backlog): honor configured task adapters

* no-mistakes(review): Harden backend purity lint against prefixed Beads calls

* no-mistakes(document): Document configured backend lifecycle transitions

* fix(backlog): preserve markdown exemptions

* no-mistakes(review): Enforce backend purity for explicit lint paths

* no-mistakes(document): Update lifecycle backend documentation

* no-mistakes(lint): Remove redundant backend lint pattern

* fix(backlog): close adapter routing gaps

* no-mistakes(review): Honor configured markdown paths and path-qualified Beads lint

* no-mistakes(document): Document environment-selected backlog adapters

* no-mistakes(lint): Fix empty local variable assignment

* fix(backlog): close quoted path gaps

* no-mistakes(review): Reject partially quoted direct Beads commands

* no-mistakes(document): Align lifecycle documentation with configured adapters

* test(backlog): keep structural cases markdown-only

* fix(backlog): honor configured task adapters

* no-mistakes(review): Harden backend purity lint against prefixed Beads calls

* no-mistakes(document): Document configured backend lifecycle transitions

* fix(backlog): preserve markdown exemptions

* no-mistakes(review): Enforce backend purity for explicit lint paths

* no-mistakes(document): Update lifecycle backend documentation

* no-mistakes(lint): Remove redundant backend lint pattern

* fix(backlog): close adapter routing gaps

* no-mistakes(review): Honor configured markdown paths and path-qualified Beads lint

* no-mistakes(document): Document environment-selected backlog adapters

* no-mistakes(lint): Fix empty local variable assignment

* fix(backlog): close quoted path gaps

* no-mistakes(review): Reject partially quoted direct Beads commands

* no-mistakes(document): Align lifecycle documentation with configured adapters

* test(backlog): keep structural cases markdown-only

* no-mistakes(review): Harden markdown lifecycle routing and close recovery

* fix(lint): catch dollar-quoted beads commands

* no-mistakes(review): Drop unused authorized-data-dir param; canonicalize fm-lint ROOT with pwd -P

* no-mistakes(document): Document fm-lint backend-purity check in header and CONTRIBUTING

* no-mistakes(ci): Two failing checks, one code-caused and one attestation-only. 1. Greptile Review (P1: ANSI-C quoting bypasses lint) — REAL DEFECT, FIXED. The backend-purity normalizer in bin/fm-lint.sh (invokes_bd awk function) stripped $'...' quote delimiters without decoding ANSI-C escapes, so a core script containing $'\x62\x64' close fm-example executes as `bd close` while the lint accepted it. Fix: the normalizer now tracks whether a single-quote context came from $' (ansi flag) and decodes ANSI-C escapes while building the command word: \xHH (1-2 hex), \uHHHH / \UHHHHHHHH (4/8 hex), \NNN (1-3 octal), \cX control characters, simple escapes (a/b/e/E/f/n/r/t/v decode to a placeholder that can never spell bd), NUL (value 0) truncates the word per bash C-string semantics, and unknown escapes drop the backslash per bash. Values outside printable ASCII decode to a placeholder so they can neither falsely match nor collide into `bd`. Regression coverage added to the existing lint-interface test test_rejects_direct_beads_cli_invocations in tests/fm-lint.test.sh: $'\x62\x64', $'\142\144' (octal), and b$'\x64' (split word), each written into a fixture and asserted rejected through the real fm-lint.sh executable. Verified regression property: with the fix stashed, the new hex case passes lint (reproduces the reported bypass); with the fix, it is rejected. Verification: all 29 tests in tests/fm-lint.test.sh pass, and the full CI-parity lint (CI=true bin/fm-lint.sh: ShellCheck 0.11.0 full analysis over the canonical set, backend-purity check, and actionlint workflow validation) exits 0. One iteration was needed because an awk comment containing a literal $'\x62\x64' sample broke shell-level quoting (bash -n / SC1001/SC2026); the comment was reworded without quotes. 2. PR must be raised via no-mistakes — NOT code-caused. The check failed with 'Pipeline attestation head_sha does not match the current PR head': the PR body attestation binds to 82b41c7 while the PR head is 1995cdb because a later pipeline push moved the head. This is exactly the stale-attestation condition the user intent describes; it clears when the outer pipeline re-runs 'git push no-mistakes' and re-binds the attestation to the new head (which now includes this Greptile fix). No code change can or should address it. Files changed: bin/fm-lint.sh (ANSI-C escape decoding in the backend-purity normalizer), tests/fm-lint.test.sh (three encoded-bd rejection cases)

* no-mistakes(review): fix tasks.toml hang, root authorization, lint quoting

* no-mistakes(review): validate tasks config before exemption; fix lint quote gap

* fix(backlog): address the markdown backlog as <data>/backlog.md

Resolving the markdown backlog through a configured `[markdown] path` was
scope this task never asked for. It is absent from main, which addresses
`<data>/backlog.md` everywhere, and it came from an earlier review round
rather than the task brief.

Making it effective on the transition path alone put that path at odds
with every other consumer of the same backlog - fm-captain-hold.sh,
fm-session-start.sh, fm-fleet-snapshot.sh, fm-inbox.sh,
fm-backlog-handoff.sh - which all still address `<data>/backlog.md`. In
fm-captain-hold.sh the split was live: its reads had already moved to the
shared gate while its writes had not, so the two could address different
files.

Address `<data>/backlog.md` from the shared gate, delete the unused
resolver, and drop the two tests that pinned the withdrawn behaviour.

What this task actually changes is unaffected: a configured non-markdown
adapter is still addressed by its own root, without `--file`.

* fix(backlog): honor configured task adapters

* no-mistakes(review): Harden backend purity lint against prefixed Beads calls

* no-mistakes(document): Document configured backend lifecycle transitions

* fix(backlog): preserve markdown exemptions

* no-mistakes(review): Enforce backend purity for explicit lint paths

* no-mistakes(document): Update lifecycle backend documentation

* no-mistakes(lint): Remove redundant backend lint pattern

* fix(backlog): close adapter routing gaps

* no-mistakes(review): Honor configured markdown paths and path-qualified Beads lint

* no-mistakes(document): Document environment-selected backlog adapters

* no-mistakes(lint): Fix empty local variable assignment

* fix(backlog): close quoted path gaps

* no-mistakes(review): Reject partially quoted direct Beads commands

* no-mistakes(document): Align lifecycle documentation with configured adapters

* test(backlog): keep structural cases markdown-only

* fix(lint): catch dollar-quoted beads commands

* no-mistakes(review): Drop unused authorized-data-dir param; canonicalize fm-lint ROOT with pwd -P

* no-mistakes(document): Document fm-lint backend-purity check in header and CONTRIBUTING

* no-mistakes(ci): Two failing checks, one code-caused and one attestation-only. 1. Greptile Review (P1: ANSI-C quoting bypasses lint) — REAL DEFECT, FIXED. The backend-purity normalizer in bin/fm-lint.sh (invokes_bd awk function) stripped $'...' quote delimiters without decoding ANSI-C escapes, so a core script containing $'\x62\x64' close fm-example executes as `bd close` while the lint accepted it. Fix: the normalizer now tracks whether a single-quote context came from $' (ansi flag) and decodes ANSI-C escapes while building the command word: \xHH (1-2 hex), \uHHHH / \UHHHHHHHH (4/8 hex), \NNN (1-3 octal), \cX control characters, simple escapes (a/b/e/E/f/n/r/t/v decode to a placeholder that can never spell bd), NUL (value 0) truncates the word per bash C-string semantics, and unknown escapes drop the backslash per bash. Values outside printable ASCII decode to a placeholder so they can neither falsely match nor collide into `bd`. Regression coverage added to the existing lint-interface test test_rejects_direct_beads_cli_invocations in tests/fm-lint.test.sh: $'\x62\x64', $'\142\144' (octal), and b$'\x64' (split word), each written into a fixture and asserted rejected through the real fm-lint.sh executable. Verified regression property: with the fix stashed, the new hex case passes lint (reproduces the reported bypass); with the fix, it is rejected. Verification: all 29 tests in tests/fm-lint.test.sh pass, and the full CI-parity lint (CI=true bin/fm-lint.sh: ShellCheck 0.11.0 full analysis over the canonical set, backend-purity check, and actionlint workflow validation) exits 0. One iteration was needed because an awk comment containing a literal $'\x62\x64' sample broke shell-level quoting (bash -n / SC1001/SC2026); the comment was reworded without quotes. 2. PR must be raised via no-mistakes — NOT code-caused. The check failed with 'Pipeline attestation head_sha does not match the current PR head': the PR body attestation binds to 82b41c7 while the PR head is 1995cdb because a later pipeline push moved the head. This is exactly the stale-attestation condition the user intent describes; it clears when the outer pipeline re-runs 'git push no-mistakes' and re-binds the attestation to the new head (which now includes this Greptile fix). No code change can or should address it. Files changed: bin/fm-lint.sh (ANSI-C escape decoding in the backend-purity normalizer), tests/fm-lint.test.sh (three encoded-bd rejection cases)

* fix(bin): preserve captain calls during teardown (kunchenguid#3595)

* fix(bin): never close a captain call during cleanup

A scout that held its own work item for the captain, which is what
captain-hold-lifecycle prefers ("hold the work item the question gates"),
was closed by bin/fm-teardown.sh's automatic backlog transition. The
completion gate passed, cleanup ran, and the captain's question moved to
Done with no recorded answer: the one thing the policy says must never
happen. `tasks-axi done` closes a held row silently, and nothing in
teardown asked whether the row was the captain's own call.

bin/fm-captain-hold.sh gains the read-only `open` predicate: exit 0 when
the task is still an open captain call, 1 when it is not, 2 when that
cannot be established. It reads the row through the transition library's
backend-aware probe, so it addresses the same backlog teardown does; the
script's other commands now address the configured data directory the
same way instead of FM_HOME, which also fixes captain holds in a home
with a relocated data directory.

Teardown asks `open` before any destructive step and refuses on 2. On 0
only the close changes: after cleanup and still under the task's own
lock, the row gets one "Deliverable of the finished work" line at the end
of its body and returns to Queued through `tasks-axi reopen`, keeping its
hold, so it lands in Captain's Call instead of reading as work under way.
--force does not lift this: it authorizes discarding unlanded work, never
the captain's question. The deliverable goes into the body because
`tasks-axi update --report` rewrites the title of a row that is not Done.

The crash window reuses the pending-close record teardown already stages:
a `mode=retain` line makes the existing replay record the deliverable and
reopen instead of closing, with the same validator, stale-generation
check, cleanup-incomplete marking, and non-blocking bootstrap lock as an
ordinary close. A retained row the captain answered first simply retires
the record. No parallel record type, recovery command, or second bootstrap
loop is introduced.

Regressions run the real executables: the captain-held scout survives
cleanup queued, held, with its deliverable and on the board, only
`answer` closes it, --force keeps it open, and an ordinary scout still
closes with its report; an interrupted cleanup leaves the row untouched
and the next session start retains it; a relocated backlog keeps the
retention in its one configured file; and a ship row whose hold cannot be
read refuses cleanup before anything destructive.

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

* no-mistakes(review): Serialize captain holds and fix backend-aware listing

* no-mistakes(document): Update captain-call retention documentation

* no-mistakes(document): Fix relocated captain-hold backlog diagnostics

* fix(backlog): honor configured task adapters

* no-mistakes(document): Update lifecycle backend documentation

* fix(backlog): close adapter routing gaps

* no-mistakes(review): Honor configured markdown paths and path-qualified Beads lint

* no-mistakes(lint): Fix empty local variable assignment

* fix(backlog): close quoted path gaps

* test(backlog): keep structural cases markdown-only

* no-mistakes(review): Harden markdown lifecycle routing and close recovery

* no-mistakes(review): fix tasks.toml hang, root authorization, lint quoting

* no-mistakes(review): validate tasks config before exemption; fix lint quote gap

* fix(backlog): address the markdown backlog as <data>/backlog.md

Resolving the markdown backlog through a configured `[markdown] path` was
scope this task never asked for. It is absent from main, which addresses
`<data>/backlog.md` everywhere, and it came from an earlier review round
rather than the task brief.

Making it effective on the transition path alone put that path at odds
with every other consumer of the same backlog - fm-captain-hold.sh,
fm-session-start.sh, fm-fleet-snapshot.sh, fm-inbox.sh,
fm-backlog-handoff.sh - which all still address `<data>/backlog.md`. In
fm-captain-hold.sh the split was live: its reads had already moved to the
shared gate while its writes had not, so the two could address different
files.

Address `<data>/backlog.md` from the shared gate, delete the unused
resolver, and drop the two tests that pinned the withdrawn behaviour.

What this task actually changes is unaffected: a configured non-markdown
adapter is still addressed by its own root, without `--file`.

* no-mistakes(review): restore home boundary guard and tighten purity lint

* no-mistakes(review): authorize home boundary for every backlog adapter

* no-mistakes(test): complete tasks-axi stubs in fm-gotmp teardown fixtures

* no-mistakes(document): align backlog transition docs with adapter-neutral addressing

* no-mistakes(review): label adapter data-dir authorization, drop dead row_probe local

* no-mistakes(review): pin markdown backend at relocated-data addressing roots

* no-mistakes(document): point lint-definition mention at fm-lint.sh header

* no-mistakes(document): point mutate comment at adapter addressing owner

* no-mistakes(review): Fix leftover-symlink refusal on non-markdown homes; hoist config check and lint/dedup cleanups

* no-mistakes(document): Align fm-lint purity scope header with bin/backends

---------

Co-authored-by: Kun Chen <3233006+kunchenguid@users.noreply.github.com>
…guid#4119)

* fix(bin): stop false watcher-down alarms on long Claude turns

A healthy Stop auto-arm rewake or open claim already explains a mid-turn
beacon that has aged past grace, because turn-end will re-arm. Keep the
supervision-off banner for a missing, failed, or exhausted generation.

* no-mistakes(review): Bind Claude rewakes to active recovery generation

* no-mistakes(document): Document Claude long-turn supervision exception

* no-mistakes(lint): Fix empty ShellCheck assignment

* no-mistakes(ci): Fixed all reported CI failures: quoted the hyphenated recovery-delivery value to satisfy ShellCheck SC2100, and updated the session-lock auto-arm fixture to emit the recovery marker and watcher beacon now required for a valid rewake. Verified fm-session-lock-ancestry, fm-test-run, stale-banner, Claude auto-arm, targeted lint, ShellCheck, and workflow lint checks pass
* fix(herdr): classify a gone session's endpoint as recoverable

A task whose Herdr endpoint could not be read was classified `unreadable`,
which blocks recovery by design. The commonest reason that read fails is
that the recorded session's server is not running at all - a host reboot, a
server exit, a session never restored - and that is authoritative absence
for every pane in that session, not an ambiguous answer about one of them.
Tasks in that state had no sanctioned way back.

The recovery-grade read now settles an uninterpretable pane read with the
session server's own `.server.running` state: positively stopped reads
`missing`, while a running server, or a server state that cannot itself be
read, still reads `unreadable`. Resting the verdict on that field rather
than on the `server_not_running` error code is what keeps it working across
Herdr 0.8.x and 0.9.0, since the field is present on both and the code is
not.

Only that one boundary is widened. The husk classifier under it stays
strict, so duplicate prevention, rollback, and teardown - the paths that can
destroy something - keep refusing on exactly the reads they refused on
before.

Separately, a relaunch refused outright when the endpoint's shell had
drifted out of the recorded worktree. An agent's own exit routinely leaves
its shell somewhere else, so that refusal stranded tasks whose work was
sitting untouched on disk. The shell is now told once to return, and only a
shell that will not go refuses; the replacement still never starts outside
the copy holding the work.

Herdr 0.8.x is not installed on this host, so protocol-20 coverage is
structural plus the adapter fixture exercising both response shapes, and is
recorded as such rather than as a live result.

Fixes kunchenguid#4091.

* no-mistakes(review): Restrict drift recovery to Herdr endpoints

* no-mistakes(review): Correct Herdr recovery verification coverage

* no-mistakes(document): Document Herdr endpoint recovery boundaries
kunchenguid#4033)

* fix(bin): keep an escalated undelivered handoff wake retryable

A remote backlog handoff holds its outbox until the backlog receipt and
the receiver wake are both confirmed, and retries the wake under the same
pending-reply correlation on every resume. When that wake's remote
transport was lost, the correlation stayed undelivered in delivery_unknown
and the watcher's next pending-reply tick escalated it. Both the reuse
predicate and the known-undelivered reset refused an escalated record, so
the resume refused to resend the wake forever and every later handoff to
that mate jammed behind the outbox.

Treat an escalated record with no confirmed delivery as the undelivered
correlation it is: fm_pending_reply_corr_reusable accepts it for its own
task and fm_pending_reply_reset_known_undelivered returns it to
awaiting_report for the idempotent remote resend, while a delivered
record is still never reset and a missed-report escalation keeps its
meaning. The published delivery-unknown decision stays open until the
record resolves, so a repeat loss neither re-notifies nor strands it.

Reproduce the deadlock end to end in the remote handoff test (lost wake
transport, watcher escalation, resume) and pin the predicate contract in
the pending-reply suite; the fm-send fixture that pinned the refusal now
uses a genuinely stale delivered escalation.

* no-mistakes(review): Decouple durable outboxes from best-effort wake retries

* no-mistakes(review): Align handoff documentation with durable receipt release policy

* no-mistakes(review): Handle unrecordable wake state as dropped

* no-mistakes(review): Prevent stale wake markers blocking handoffs

* no-mistakes(review): Prevent stale delivered markers suppressing new wakes

* no-mistakes(document): Clarify retry escalation decision lifecycle

* no-mistakes(document): Document pending receiver wake retries
* 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
tiago-peixoto and others added 29 commits September 26, 2026 15:24
…nchenguid#4806)

* fix: stop quarantining ordinary shared-captain source updates

* no-mistakes(document): Rewrap remote inherit header so usage prints fully
* docs: make calm easier to read

Restructure the Calm mode prose into sections, lists, and tables without changing documented behavior. Every original heading, anchor, fenced code block, inline-code span, link target, number, and quoted string is preserved.

* docs: restore reload case in calm override lead-in

The Built-in tool override collisions lead-in covers a session that reloads with Calm already on, as the original text did.
* docs: make turnend-guard easier to read

Restructure the turn-end guard doc's prose into shorter sections, lists, and tables without changing documented behavior. Every original heading, anchor, inline identifier, link target, and number is kept.

* no-mistakes(review): Restore legacy-only scope on TERM retirement sentence

* no-mistakes(review): Name Cursor park behavior in live e2e test line
…henguid#5872)

* docs: move situational AGENTS.md sections into on-demand skills

Backpass memory optimization: shrink the always-loaded AGENTS.md by moving
situational contracts (home layout, session-start recovery, validation and
landing supervision, scout completion, away/quiet supervision, Relay
ownership) into agent-only skills loaded at their triggers, with a trigger
index skill.

* docs: classify the new on-demand skills' documentation audience

Register the seven new agent-only skills as agent-runtime docs and fix a
link in validation-supervision that kept its AGENTS.md-relative path.

* docs: close load-timing gaps found by the live regression check

- load validation-supervision whenever an ask-user finding is decided or
  answered, so forbid --yes and process-every-return reach the worker
- keep the mid-task captain-ask rule, the unconfirmed network-checks rule,
  and the worker account pin rule inline in AGENTS.md
- fix cross-references that still pointed at moved AGENTS.md sections
…guid#5879)

* fix: route second-mate signal wakes by their new status span

A second mate's status log is a shared channel carrying many independently
keyed decisions, so judging its signal rows by every decision still open in
the whole log pinned each routine update to main behind any unrelated
parked hold. scopeForUnreadWake (the one owner for Pi and the attended
supervision host) now judges a second-mate signal row by the lines presented
since the last drain, bounded by the existing status-presentation cursor:
a decision, blocked, resolution, or captain-held line, or a line declaring
the key of a still-open decision, keeps the whole row on main, and any
cursor problem falls back to the whole log. Keys are read only at the
status parser's declared positions, with readable time stamps stripped as
bin/fm-classify-lib.sh does. Single-task crewmate and stale routing are
unchanged, and stale and signal rows for one mate keep independent verdicts.

The supervision branch now treats a second mate's done and merged lines as
relayed child outcomes, and fm-teardown refuses the branch actor second-mate
retirement through the existing role-partition helper in both postures.

* no-mistakes(review): Route second-mate resolutions to main only when closing open decision

* no-mistakes(review): Guard bare-verb fold lines and fold resolution spans incrementally

* no-mistakes(document): Clarify second-mate wake routing and retirement documentation

* no-mistakes(ci): The CI failure was a timing-sensitive watcher teardown test, not the PR’s signal-scope code. Extended the bounded wait for both state-directory and home removal. The watcher suite passes locally, and git diff --check is clean
…extension (kunchenguid#5882)

register-extension took the extension lifecycle lock and then the source
lock, while reconcile republishing an unhandled extension result holds the
source lock and reaches the lifecycle lock through the extension host's
process-event path. Both waits are unbounded and both owners stay alive, so
the two could wait on each other forever and freeze the home's monitoring
cycle.

register-extension now takes the source lock first, matching every other
path that holds both. The lifecycle lock still spans binding resolution
through registration publication, so binding retirement stays serialized.

A new lifecycle-order section in the extension-binding suite, run in the
default aggregate, holds a re-registration inside binding resolution while
reconcile republishes that source's unhandled result and requires both to
finish within a bound.

Fixes kunchenguid#5866
* fix(bin): run the repository's own hooks when git -c carries the per-task hooksPath

The per-task hooks wrappers cleared only the GIT_CONFIG_COUNT override before
looking up the repository's own hooks directory. When core.hooksPath reached git
through git -c (GIT_CONFIG_PARAMETERS), directly or inherited by a child
process, the lookup found the wrapper directory again and exited 0, so the
repository's real hook - such as a pre-push publish guard - never ran and the
push succeeded.

The lookup now ignores GIT_CONFIG_PARAMETERS too, so only the repository's
config files decide its hooks directory, and a failed lookup exits nonzero
instead of skipping the hook. AI-trailer stripping is unchanged.

Fixes kunchenguid#5871

* no-mistakes(document): Document git hook chaining and lookup failure behavior
…unchenguid#5744)

* feat(bin): add an optional never-send list to typed dispatch resolution

* Added config/dispatch-never-send, an optional local list of literal
  values and re: regular expressions checked against every string of
  the resolver request before it is sent to typesafe.ai
* A match, an unreadable list, or an empty or invalid pattern now stops
  the request and falls back to the off path, so firstmate dispatches
  through its existing intake; the one stderr diagnostic names at most
  the list line number and never the value
* No list, or a list with no match, leaves resolution unchanged

* no-mistakes(review): Match never-send literals across whitespace, drop regex mode

* no-mistakes(review): Inherit the never-send list into secondmate homes

* no-mistakes(ci): ci-2 (Lint 2), caused by this PR and now fixed. The rule that broke: the test script must not share shell variables with a library it sources. The new secondmate-inheritance test in tests/fm-dispatch-resolve.test.sh sourced bin/fm-config-inherit-lib.sh inside a `( ... )` subshell. That lib assigns `out`, so ShellCheck flagged every later `$out` in the test with SC2031. The subshell was the only place this PR sources that lib. The fix runs the propagation in a child shell instead (`bash -c '. "$1" && propagate_inheritable_config "$2" "$3"' _ lib from to`), so the test shell never sources the lib. `bin/fm-lint.sh tests/fm-dispatch-resolve.test.sh` now exits 0, and `bash tests/fm-dispatch-resolve.test.sh` passes. That includes the check that an inherited list is enforced in a secondmate home. ci-1 (Behavior portable serial 8), not caused by this PR, so no code change for it. The only failing test is tests/fm-remote-secondmate-relaunch.test.sh, which fails with "not ok - could not arm the PR poll fixture for the relaunch-ordering test". This PR doesn't touch that test or the code it runs. I reproduced the same failure locally on the base commit ea7c7f7. Main's own CI run on ea7c7f7 (run 36212602588) fails only this job, with the same message. The recent main runs before it also concluded failure
…hrashing guard (kunchenguid#5903)

* feat(jev): add the guard framework and the memory RSS/swap thrashing guard

A Jev guard is a bounded read-only host diagnostic that turns one class of
resource pressure into a machine-readable audit record and a one-line verdict.
This lands the framework contract (docs/jev-guards.md) with one representative
family (Pattern 46, memory RSS and swap thrashing) as the pilot: thin bash
wrapper, stdlib-only python engine, and a behavioral test through the CLI.

* no-mistakes(review): fix jev mem guard fail-open unknown and contract

* no-mistakes(review): fix mem guard rss pairing, memfree, tests, docs

* no-mistakes(review): fix inverted UNKNOWN branch in fail-forcing test

* no-mistakes(review): register docs/jev-guards.md in audience inventory

* no-mistakes(review): simplify wrapper, drop dead top_n, single-source thresholds

* no-mistakes(review): assert exact exit code in fail-forcing test leg

* no-mistakes(review): tolerate any stdout encoding in text output

* no-mistakes(document): Fix guard contract dash style and output wording
…enguid#5900)

* fix(bin): stop slow GitHub reads from starving and waking the contributions poll

The poll's fixed budget cut the tail URL's reads short, and any read killed at the bound was recorded as a forge failure, so routine GitHub slowness produced an observation-unavailable wake. A configured FM_CONTRIBUTIONS_BUDGET now rides the generated check shim and is cut down to the watcher's per-check bound with a margin, a read killed at the bound or the deadline is budget refusal that leaves records untouched, and the poll moves to the next URL that still has a full observation reserve instead of ending.

* fix(bin): report the bound when a signal death leaks through fm_run_timed

fm_run_external_timeout trusted the wrapper's recorded status over the runner's own bound verdict, so a read killed by the bound's TERM could surface as 143 and callers such as the contributions poll classified a budget-bound read as a forge failure. When the runner reports the bound, a signal-death status is the bound's own TERM racing the wrapper's bookkeeping and is reported as 124; a natural exit still passes through. The replace-shell probe also moves to the repo's portable BASHPID fallback so the suite runs on the macOS system bash.

* no-mistakes(review): Rotate contribution polling and verify generated budget behavior

* no-mistakes(review): Stabilize contribution rotation across successful observation refreshes

* no-mistakes(review): Exclude settled contributions from live observation rotation

* no-mistakes(document): Document contribution poll rotation and observation reserves

* no-mistakes(ci): Captain, fixed timeout handling with a one-line change preserving natural exit 137. Full session-start and contributions suites, targeted regressions, and lint passed. The timeout suite still fails on the known Bash 3.2 BASHPID issue, left unchanged as instructed. Logs retained in .no-mistakes/ci-evidence/. Remote CI was not rerun

* no-mistakes(ci): Fixed the fixture’s lock-acquisition race with a one-line bounded wait. Forced contention reproduced the CI error before the fix and passed afterward; the ordinary held-lock case, lint, syntax, and diff checks passed. Local Bash 3.2 failures remain: “cleanup lock bound 08 gave up before the marker lock freed” (also reproduced without the fix) and “TERM did not stop a watcher blocked inside a poll”. Remote CI was not rerun
…railers (kunchenguid#5859)

* feat: add keep AI trailers setting

* no-mistakes(document): Drop duplicate keep-ai-trailers line, update Cursor attribution note

* no-mistakes(document): Honor keep-ai-trailers for Devin worker attribution

* no-mistakes(document): Qualify Devin attribution note with keep-ai-trailers flag

* no-mistakes(review): Inherit keep-ai-trailers into secondmate homes
…sh-command popup cannot hide the composer (kunchenguid#5876)

* Fix Herdr composer reads blinded by the slash-command popup

Capture the full visible viewport for every Herdr composer state and content read instead of a bounded tail.
Claude Code renders its slash-command popup between the composer and the pane bottom, which pushes the composer outside a tail window.
The pre-Enter payload proof then read an empty composer, judged a typed command unsent, and cleared it without pressing Enter.
The proof-lines value now bounds only the clear cost, not the capture size.
Growing the window adds rows above the composer only, so bottom-most shape selection and prior verdicts are unchanged.
The suffix refusal is kept, and the shared inbox pending-line read stays a bounded tail.
Portable regressions cover the popup-below-composer layout, and the live submit-confirmation guard gains a third exit scenario.
The runtime-backends verification record documents the Herdr 0.9.0 and Claude Code 2.1.283 run.

Closes kunchenguid#5533

* no-mistakes(document): Clarify composer capture bound ownership

* no-mistakes(review): Drop unrequired bracketed-paste Enter fallback from live guard

* no-mistakes(review): Latch trust prompt, check idle composer arm first
…configurable bound (kunchenguid#5917)

Each per-task endpoint read in the session-start fleet digest runs in its own crash-isolated child under the existing timeout helper with a configurable FM_SESSION_START_ENDPOINT_TIMEOUT bound (default 10s).

A hung or killed read becomes that task's endpoint error while the digest continues, and the wrapper banners any abnormal child exit naming its status.
…nchenguid#5886)

* Keep lab tmux sockets on short private paths

* no-mistakes(review): Fix Linux stat checks and fail-closed lab tmux teardown

* no-mistakes(document): Update lab helper documentation for isolated tmux sockets
* Allow silent task-level no-change outcomes

* no-mistakes(review): Exclude silent outcomes from captain-return handoffs

* fix: look up supervision receipts by exact sequence

* no-mistakes(review): Suppress silent notes in away-return brief

* no-mistakes(review): Clarify visible notes; remove unused mode

* no-mistakes(review): Clarify silent outcomes and avoid false drain promises

* no-mistakes(document): Clarify silent supervision outcome documentation
…chenguid#5928)

* feat: make /quiet a statement where the attended supervision host runs

On a home that opted into the supervision host, quiet mode is what the
attended host already does, so /quiet now enters nothing there instead of
launching the quiet daemon and writing a record that would park a present
captain's main.

- bin/fm-afk-launch.sh quiet-check says quiet mode needs nothing where the
  attended host runs, or that the session is paused while its
  broken-session latch holds; a quiet enter refuses there before writing
  anything.
- Where the home opted in but the attended host lacks a part (engine,
  tools, verified mirror writer, identifiable main session, valid mirror),
  quiet-check names it and quiet mode falls back to the daemon.
- Under a live away record on that home, quiet-check and a quiet enter
  refuse and name the record, so the return runs first, whatever
  state/.afk says.
- A quiet enter records mode: quiet in the posture record, so start and
  start-native launch the quiet daemon without FM_AFK_MODE, and the away
  refusal wording fires only for away.
- bin/fm-host-mirror.sh check validates the dialog mirror read-only and
  exits 1 on a missing, unreadable, or invalid mirror.
- The quiet and afk skills and the supervision-host docs describe the new
  behavior; homes without the opt-in and Pi homes keep the daemon path.

* no-mistakes(review): Archive the quiet record when a quiet daemon start fails

* no-mistakes(document): Clarify quiet-mode documentation and remove stale duplicates
…uid#5884)

* fix(bin): grant Claude workers their task-channel dirs via --add-dir

Since Claude Code 2.1.257, a file-tool read (Read/Glob/Grep, and an
Edit's mandatory prior read) of a path outside the working directories
parks --permission-mode auto panes on a one-time interactive question,
and a "Block" answer lands permissions.blockReadsOutsideWorkingDirectories
in user settings, refusing the same reads even under bypass. Firstmate
launches Claude with no --add-dir, so a secondmate's parent-home steering
inbox and a ship or scout worker's launch record, steering inbox, brief
dir, and code-root .agents/skills were all outside: workers wedged on
the question the first time they read a steer.

Every Claude launch, spawn and relaunch, in both permission modes, now
grants exactly the task's channel directories: state/<id>.inbox for a
secondmate (in the parent home), or state/operational-inbox,
state/<id>.inbox, data/<id>, and the code root's .agents/skills for a
ship or scout. Paths resolve to real paths and lazily created channel
dirs are made before launch so the grant never names a not-yet-existing
directory; the whole state/ is deliberately never granted.

The grant keeps the bypass-mode launch argv changed on purpose: it also
protects bypass workers against a machine-recorded Block answer.

* no-mistakes(document): Consolidate Claude launch guidance in configuration reference

* no-mistakes(document): Clarify Claude permission documentation reference
…nguid#4819)

* fix(supervision): prevent idle recovery loops without stranding wakes

* no-mistakes(review): Remove unused wake-append rollback helper
…id#5889)

* fix(bin): stop the remote-job worker busy-polling an idle queue

The serving loop slept 50ms between passes and re-ran state preparation
(chmod on every queue directory), the heartbeat publish, and the stale sweep
on every pass. It now blocks on a worker.wake FIFO that staging,
cancellation, and lane exit nudge, keeps a short fast-poll window after
activity, refreshes the heartbeat at most once a second, and runs the sweep
(which re-applies the queue directories' 0700 modes) at startup and then on
a bounded interval. Lane-owned records are no longer re-read every pass.

Measured with a fork/execve-interposing counter on a --serve worker in a
disposable HOME and queue, bash 3.2, 20-second windows (the counter slows
the old loop to about 5 passes a second, so real-host rates were higher):
  idle worker             146 forks/s, 61 execs/s -> 4.8 forks/s, 3.1 execs/s
  one running long job    232 forks/s, 100 execs/s -> 15 forks/s, 11 execs/s
Stage-to-result latency for a no-op job, idle and back to back, stayed at
about 0.8-1.2s in both versions (dominated by job execution, not pickup).

* perf(bin): drop per-cycle forks from watcher, drain, and lock helpers

The watcher, drain, inactive-reconcile scan, and branch-outcome reads forked
small external commands on every cycle where bash can do the same work.

- fm-wake-lib.sh gains fm_dirname_to, fm_basename_to, and
  fm_epoch_seconds_to, exact stand-ins for $(dirname --), $(basename --),
  and $(date +%s); the clock uses printf %(%s)T on bash 4.2+ and still forks
  date exactly once on stock macOS bash 3.2.
- fm_lock_abs_path, fm_wake_signal_seen_path, fm_path_age, the watcher's
  age_of and wedge timer, and the recovery-marker line count use them or
  plain reads instead of dirname/basename/tr/date/wc.
- window_to_task reads a meta file once instead of two
  grep | tail -1 | cut -d= -f2- pipelines per file per call.
- fm-classify-lib.sh reads uname -s once at source time instead of in every
  status stat helper.
- Libraries sourced every cycle derive their own directory without forking
  dirname, including the backend adapter siblings a subshell re-sources on
  each probe.

tests/fm-fork-free-helpers.test.sh pins each replacement against the command
it replaces on edge-case inputs, under every available bash and both the C
and a UTF-8 locale; CI's stock macOS bash lane runs it under /bin/bash 3.2.

Measured with a fork/execve-interposing counter in a disposable home, one
tmux crew task, FM_POLL=1 (forks and execs per watcher cycle, per run
otherwise):
  watcher cycle      bash 5.3  299/138 -> 199/66   bash 3.2  341/146 -> 224/80
  drain              bash 5.3  492/238 -> 430/200  bash 3.2  567/250 -> 491/212
  inactive scan      bash 5.3   27/14  ->  17/4    bash 3.2   37/14  ->  17/4
  branch-outcome     bash 5.3   40/21  ->  35/16   bash 3.2   48/24  ->  38/19

* test: note the interpreter-expanded version probe for shellcheck

* no-mistakes(review): Fix worker idle bounds and fork-free contributions snapshot

* no-mistakes(review): Coalesce buffered worker wake nudges into one wake

* no-mistakes(review): Coalesce wake nudges via pending marker so publishers never block

* no-mistakes(review): Claim wake nudges atomically via noclobber pending marker

* no-mistakes(review): Release abandoned wake claims only after a 30-second bound

* no-mistakes(review): Drop worker wake FIFO; load path helpers side-effect free

* no-mistakes(document): Document remote worker polling and preemption cadence

* no-mistakes(ci): Fixed both failing CI shards: isolated remote and teardown test fixtures now include fm-path-lib.sh, which fm-wake-lib.sh requires. The three affected tests, fm-lint.sh, and git diff --check pass locally
…uid#5941)

* fix: keep a successor watcher and remote-reply listeners across the gaps that dropped them

A main-only supervision pass-through exited without leaving a watcher, and each remote-reply poll released its claim until the next cycle, so short-lived listeners stayed down.

* no-mistakes(document): Clarify listener and supervision continuity documentation

* no-mistakes(ci): Fixed the three Greptile findings: failed ingestion leaves one durable capture, failed reads exit instead of relistening, and the disposable-checkout guard rejects state paths outside the marked lab. Added behavioral tests; the remote-reply and watcher-lock suites, shell syntax checks, and git diff checks passed

* no-mistakes(ci): Fixed a race in the wake-queue interruption test: it now waits for the drain to own the lock and enter handling before signaling it. The wake-queue suite, shell syntax check, and diff check pass
…enguid#5925)

* fix: date replayed branch outcomes and ask main to check current state first

A captain outcome main never acknowledged is presented again, which after a
harness or posture switch, or the first drain after the upgrade whose earlier
presenter never advanced the read cursor, can be days after its situation
settled. The replay read as fresh news, so a PR since merged looked ready.

bin/fm-branch-outcome.sh now adds a "recordedAgo" age (minutes, hours, then
days) to present and unprocessed rows, one owner of that wording for both
presenters. The drain's BRANCH OUTCOMES captain lines and the Pi branch's
processing request name that age and ask main to check the task's current
state first; an outcome already settled needs only the acknowledgement, with
nothing relayed to the captain. Nothing is adopted as processed, so a fresh
home's first outcome is still presented until acknowledged.

* no-mistakes(review): Absent processed marker reads 0; never adopt read cursor

* no-mistakes(review): Require recordedAgo in Pi requests; report undated rows to main

* no-mistakes(review): Keep recordedAgo on captain rows only in present output

* no-mistakes(document): Correct cutover documentation and retire stale migration guidance

* fix: keep settled branch outcomes out of main's reply to the captain

A live Pi primary that took over a host-drain home received the carried-over
outcomes dated and check-first, but its processing reply still told the
captain about an outcome whose decision had since been answered. The request
also claimed every outcome was already shown as an anchor entry in this
transcript, which is false for an outcome carried over from before a restart
or a switch of primary.

The Pi processing request now says each outcome was recorded earlier and may
already have been seen or handled, and that a settled outcome gets no
captain-facing mention at all in the reply or any recap, not even that it is
settled. The drain's BRANCH OUTCOMES header and the supervision docs state the
same rule, and the tests check both delivered texts.

* fix: scope main's outcome reply to what is still open

Telling main what not to say about a settled outcome was not enough: in two
live Pi trials the processing reply still told the captain that an answered
decision was settled. Main now sorts the outcomes by current state first, and
its reply to the captain covers only the still-open ones, written as if the
settled ones had never been listed. With that framing three live Pi trials
kept the settled outcome out of the reply and relayed the open one each time.

The drain's BRANCH OUTCOMES header and the supervision docs use the same
framing, and the tests check both delivered texts.

* no-mistakes(review): Clarify that main acknowledges every presented captain outcome

* no-mistakes(document): Clarify outcome cursor ownership across Pi and host

* no-mistakes(ci): Fixed Pi replay by batching unprocessed captain outcomes oldest-first and acknowledging only through each batch. Verified a marker-less backlog over 1 MiB replays through all batches. A real-drain regression confirms an older keyed decision remains under OPEN DECISIONS after a newer branch row is acknowledged; the check-first instruction now names those decisions. Relevant targeted tests and branch-supervision tests passed; the full host suite timed out

* no-mistakes(ci): Fixed the host drain’s check-first wording in bin/fm-wake-drain.sh; the CI fixture now passes. The full host suite passed the affected fixtures but timed out later. Syntax and diff checks passed

* no-mistakes(ci): Fixed ci-1: abbreviated Pi outcome summaries now stay within 1,024 characters and include a row-specific full-outcome lookup command. The delivered instruction requires reading the full outcome before acting, relaying, or acknowledging it. The new extension-driver regression failed before the fix and passes now; the Pi and supervision-host suites pass

* no-mistakes(ci): Corrected the batching sentence in docs/pi-supervision-branch.md. The cancelled CI check needs no code fix; its clean rerun passed. The Pi branch extension suite and git diff check passed
…guid#5961)

* fix: wake an idle Claude primary for attended main-only hand-backs

An attended main-only pass-through confirmed a handling handoff for the
successor it leaves running, which flipped the recovery marker to handling.
The Claude Stop hook only rewakes main while that marker reads downtime, so
the close reached no one and an idle primary slept with wakes queued.

The pass-through now leaves the marker at downtime, and a close that turns
main-only at its turn hands the consumed handoff back to downtime before it
reaches main. Regression tests drive the real Stop hook around the real host
on both paths and for the successor's own later close, and a new opt-in live
guard proves it against an idle interactive Claude primary with a pre-fix
negative control.

* no-mistakes(review): Assert live lab Stop-hook registration via parsed settings JSON

* no-mistakes(document): Correct supervision hand-back documentation

* no-mistakes(ci): Fixed the failed downtime-write path so the Stop hook notifies main instead of silently dropping the close. Corrected the live guard’s tracked-hook check and added the requested at-turn main-only scenario. The new regression failed before the fix and passed after it; the host suite, syntax checks, and diff check pass. The credentialed live guard was not run in this CI phase because it writes outside the worktree

* no-mistakes(ci): Fixed the Stop hook’s retry ordering: a crashed host gets its bounded retry before a non-crash hand-back failure is reported. The Stop-hook suite passes, including the crash regression. The host suite passed the failed-marker-write regression but timed out before completing; syntax and diff checks pass
kunchenguid#5916)

* Fix worker launches to enter recorded worktrees

* no-mistakes(review): placeholder

* no-mistakes(document): Update agent-control.md worktree-refusal note to match new universal cd+assert

* no-mistakes(review): Add regression tests for Orca spawn/relaunch worktree carve-outs

* Fix PR relaunch and prelaunch cwd verification

* no-mistakes(document): Fix docs/agent-control.md: worktree cwd check is pre-launch, not post-launch

* fix: slim worktree launch change onto upstream main
…ardown (kunchenguid#5997)

* WIP: retire task-keyed watcher markers and orphan journals at teardown

Re-applies old PR kunchenguid#5584 on current main: teardown retires the
turn-ended .seen-* signature and an orphaned Herdr presentation
journal whose workspace is already gone, and the wake-drain rotates
its own dead scratch files. Not yet validated through no-mistakes.

* no-mistakes(ci): Fixed the Greptile P1 finding in bin/backends/herdr.sh. fm_backend_herdr_projection_token_workspace_gone used `! ... jq -e ... 2>&1`, which swallowed a jq runtime error (thrown when a non-object workspace entry, e.g. a number before a live token-bearing workspace, hits `.label`) and flipped it to a "gone" verdict, causing teardown to delete a still-live v1 presentation journal. Invariant: a workspace-query error/ambiguity must never be read as token absence; only a cleanly-parsed list with no token-bearing label is "gone". Replaced the body with a single jq verdict (unknown/present/gone): a non-array list or any non-object/non-string-label entry yields "unknown", jq errors/empty output fall through `|| return 1` to unknown, and only "gone" returns 0. Sibling fm_backend_herdr_projection_endpoint_matches_journal already fails safe on jq error (empty match -> journal kept), so it needed no change, matching the author's scoping. Added test_teardown_retains_v1_journal_when_workspace_query_ambiguous driving real teardown with a malformed workspace-list entry, proving the journal is kept and no workspace close occurs. Verified the old logic returns GONE on that input (test fails before, passes after); full tests/fm-teardown.test.sh suite passes (exit 0) and shellcheck is clean. Marker-naming finding left untouched per explicit out-of-scope instruction
…unchenguid#6002)

* fix(tests): disable Claude Code's auto-updater during live harness runs

fm_live_gate let a live run proceed without ever setting
DISABLE_AUTOUPDATER, so a live Claude test could let the real updater
repoint ~/.local/bin/claude into a temporary directory and stop every
Claude process on the machine from starting. Export
DISABLE_AUTOUPDATER=1 on every path where the gate lets a live run
proceed, and assert the export in tests/fm-live-gate.test.sh, including
that it reaches a child process the same way a real harness pane would
inherit it.

* no-mistakes(ci): Greptile flagged that the PR's DISABLE_AUTOUPDATER inheritance test only checked a `bash -c` direct child, not the fm-spawn.sh launch path. The user chose to fix it with a regression on that path. In tests/fm-live-gate.test.sh I replaced the generic child test with test_disable_autoupdater_reaches_the_claude_pane_on_the_fm_spawn_launch_path: it drives the real fm-spawn claude launch through the spawn fixtures, captures the exact staged launch command, and runs it as a synthetic pane whose only `claude` is a stub recording the inherited DISABLE_AUTOUPDATER, asserting it saw 1. Switched the file to source fixtures.sh (pulls in lib.sh, guarded) for the spawn helpers. Verified it is a real guard: the stub records `1` when the ambient var is set and `unset` when absent, so it fails if fm-spawn ever scrubbed the variable (e.g. env -i or -u). This confirms fm-spawn's launch construction never references the name and passes it through via ordinary ambient inheritance with no allowlist. Full suite passes (12 tests ok), shellcheck clean. Note for the outer executor: I embedded the daemon caveat as a code comment in the test, but the finding also asks the PR body to state that a backend daemon already running before the gate exported the variable does not inherit it and fully covering that would need launcher support - that forge-side PR-body sentence is outside this CI phase's scope

* no-mistakes(ci): Fixed Greptile finding ci-1. Root cause: fm-spawn.sh handed its launch command to an already-running backend daemon that never inherited the test process's exported DISABLE_AUTOUPDATER, so ambient inheritance dropped it and Claude's auto-updater could still run. Fix (bin/fm-spawn.sh): when DISABLE_AUTOUPDATER is set in the spawn's own environment, embed `export DISABLE_AUTOUPDATER=<value>;` into the LAUNCH command text (same idiom as the adjacent COMPACT_ADVISER_DISABLE export), so it survives a daemon-built pane, the env -i allowlist path, and relaunch alike; gated on presence so ordinary spawns are unchanged. Added regression test test_disable_autoupdater_survives_a_daemon_pane_that_never_inherited_it in tests/fm-live-gate.test.sh: stages a real claude launch with DISABLE_AUTOUPDATER set in the spawn env, then runs that exact command in a synthetic pane with `env -u DISABLE_AUTOUPDATER` and asserts the claude stub still recorded autoupdater=1. Verified the test fails (autoupdater=unset) without the fix and passes with it; the round-1 ambient test stays green either way. Full suite passes (13 ok); test file and isolated snippet shellcheck-clean (full fm-spawn.sh shellcheck kept getting terminated by the memory-constrained host, not by findings). Forge-side note for the outer executor: the PR-body caveat that fully covering the daemon case would need launcher support no longer applies to the Claude launch path and should be corrected
…cevent record (kunchenguid#6010)

* fix(bin): ring the inbox doorbell only for a newly published procevent result

publish_result rewrote a worker's captured Lavish round idempotently on
every reconcile, unconditionally moved an already-acknowledged inbox
record back out of handled/, and rang the doorbell every time - so an
already-processed round rang the owning worker on every cycle. Snapshot
the existing active and handled records before the idempotent write and
ring, or move anything, only when the write actually created a fresh
record; re-delivery of a still-open round is left to the inbox's own
re-ring ladder.

* no-mistakes(document): docs: reflect worker-board doorbell rings only on fresh inbox record

* no-mistakes(ci): Fixed Greptile finding ci-1 in tests/fm-procevent.test.sh (test-only change). The redelivery regression previously moved the delivered note into handled/ before any repeated reconciles, so it only proved an acknowledged note stays quiet and would still pass if an unchanged active note rang every cycle. Per the user's instruction, I inserted (before the mv into handled/) five repeated `pe reconcile` runs with the note still in the active inbox and asserted the ring log holds exactly one line and 001.msg remains active; the existing acknowledged-note assertion after the move is kept unchanged. No product code changed. bash -n confirms syntax is valid; the block mirrors the already-passing post-move reconcile/ring-count assertion directly below it
Merge kunchenguid/firstmate main (d5c2507) into the fork, keeping the
fork's commits. The merge base is b6930db, the upstream commit PR #3
actually integrated, so the five upstream commits PR #3 listed but did
not carry (kunchenguid#4972, kunchenguid#4973, kunchenguid#4994, kunchenguid#4998, kunchenguid#5001) land here too.

Adopt upstream's AGENTS.md and on-demand skills in place of the fork's
PR #8 skill split, re-applying fork-only rules; the #8 layout is kept on
archive/agents-md-skill-split-pr8.
Drop the fork pipeline-step pause assertions superseded by upstream's
declared-wait rule, serve the trust-prompt fake's dialog only after the
launch command (upstream now captures the pane after the pre-launch cd),
and give the new upstream watch-triage fixtures the matching .meta the
fork's orphan guard requires.
@sctru
sctru merged commit d6fa8c1 into main Sep 29, 2026
18 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.