Skip to content

Merge upstream main into fork (commits #4894, #4889, #4910) - #11

Merged
matthewstrud merged 4 commits into
mainfrom
fm/fm-upstream-sync
Sep 19, 2026
Merged

matthewstrud merged 4 commits into
mainfrom
fm/fm-upstream-sync

Conversation

@matthewstrud

Copy link
Copy Markdown
Owner

Merge three commits from kunchenguid/firstmate main:

  1. fix(bin): preserve Claude lock ownership after helper recycling kunchenguid/firstmate#4894 - fix(bin): preserve Claude lock ownership after helper recycling
  2. feat: park main under the away posture on Pi kunchenguid/firstmate#4889 - feat: park main under the away posture on Pi
  3. ci: standardize workflow timeouts into three tiers kunchenguid/firstmate#4910 - ci: standardize workflow timeouts into three tiers

Conflict resolution

The only conflict was in tests/fm-send-resolve-key.test.sh, where both sides added a test function at the same point:

  • Fork: test_secondmate_helper_keyed_report_then_resolve_key (secondmate report decision-key routing)
  • Upstream: test_decision_answer_partition_relocates_under_the_record (away-posture decision partition)

Both are independent functions - kept both with their test registrations. The fork's REPORT variable was also retained.

Verification

  • All 22 tests in tests/fm-send-resolve-key.test.sh pass
  • All 23 tests in tests/fm-branch-supervision.test.sh pass
  • All 16 tests in tests/fm-session-lock-ancestry.test.sh pass
  • All 52 tests in tests/fm-pi-branch-extension.test.sh pass
  • All 49 tests in tests/fm-pi-watch-extension.test.sh pass
  • (tests/fm-ci-workflow.test.sh skipped - needs Ruby for YAML parsing, not available in worktree)
  • Changed bin scripts verified clean with shellcheck

Stats

38 files changed, 2706 insertions(+), 222 deletions(-)

kunchenguid and others added 4 commits September 18, 2026 23:03
…henguid#4894)

* fix(bin): let a background Claude session keep owning its session lock

Session-lock ownership was decided by process ancestry alone. Under an
unattended Claude session the model loop runs in a transient bg-spare
bridged to the front-end by a shared daemon; when that bridge is
recycled the contiguous claude-named ancestry from a hook to the
recorded owner breaks while the owner pid stays alive, so the Stop
auto-arm stood down as a foreign live owner, the turn-end guard ended
every turn with its read-only diagnostic, and fm-lock.sh refused - a
self-sustaining outage until restart.

Ownership is now ancestry membership OR a trusted same-session id,
never id-first:

- fm-session-lock-lib.sh accepts CLAUDE_CODE_SESSION_ID only when
  CLAUDE_PID is a Claude-shaped member of the current contiguous run,
  compares it against the id recorded in state/.lock-session, and
  requires the recorded pid to still be a live harness. No id, no
  sidecar, an untrusted id, a different id, or a dead recorded pid
  leaves the ancestry verdict unchanged. Ids are never read from ps
  argv.
- fm-lock.sh accepts a same-session holder at both refusal sites,
  writes, refreshes, and clears the sidecar only under its claim lock
  (including the early already-mine exit, skipped only while the
  deferred startup sweep leases that lock), keeps it byte-identical
  across a same-session confirmation, records CLAUDE_PID on lock line 1
  for a session with a trusted id so a shared daemon or front-end that
  outlives the session never keeps a dead session's lock alive, never
  rewrites a live line 1 on a same-session confirmation, and names the
  recorded id in the live-owner refusal.
- The .lock line-1 format is unchanged, so every reader that takes the
  whole first line as the pid keeps working; the guard's foreign-owner
  exit is unchanged and inherits the fix through the shared predicate.

Tests: the ancestry suite drives the ancestry and id signals apart in a
deterministic process table (asserting the divergence) and runs a real
orphaned front-end/daemon/pty-host/spare tree through six phases with
the real lock, auto-arm, and guard scripts; the foreign-owner repro
keeps its negative control and adds a same-id positive control.

Disclosure: no live unattended Claude background session ran on the
verifying machine. The topology is documented by the real process
listings in kunchenguid#3902, kunchenguid#2314, kunchenguid#3398, and kunchenguid#4066; coverage is the structural
predicate plus the executable fixtures, not a live pass.

Residual: bin/fm-sessionstart-nudge.sh keeps its own private ancestry
walk (it only decides whether to print a nudge) and may nudge on a
resume in the recycled case.

Out of scope, deliberately: no structured lock format, no guard budget
changes, no daemon-identity rejection, no fork lineage.

* no-mistakes(review): Wait for claim lock; revert failed sidecars

* no-mistakes(review): Revalidate ownership after wait; restore sidecars

* no-mistakes(review): Roll back sidecar by publication phase

* no-mistakes(review): Restore sidecar only if lock line is unchanged

* no-mistakes(review): Trust session ids without a spelling allowlist

* no-mistakes(review): Disarm sidecar rollback before backup cleanup

* no-mistakes(document): Updated session-lock ownership documentation
* feat: park main under the away posture on Pi

While the away-posture record exists on a Pi primary, the supervision branch
takes every actionable wake, no processing turn opens on main, captain rows
accumulate for the return brief, and main's standing authority relocates to
the branch through the existing guarded scripts.

- lib/fm-branch-dispatch.ts: read the record at every routing decision; while
  it exists claim check, decision-owned, and heartbeat rows too, keeping the
  two broken-queue vetoes; expose checkSeqs so a claimed check row lifts task
  scoping.
- fm-primary-pi-watch.ts: offer every actionable row under the record; a
  declined wake and every watcher-failure alarm still reach main.
- fm-branch-supervision.ts: drop the legacy .afk decline; append a fixed
  POSTURE: AWAY tail carrying the record's read-back verbatim per wake; open no
  processing request while the record exists, re-checked immediately before a
  request would open and at every run boundary; present the accumulated rows
  at the first run boundary after archive.
- fm-lease-lib.sh: fm_lease_forbid_branch passes the branch for opted-in
  actions only while fm-afk-contract.sh validate succeeds on a confirmed live
  record; PR merge, fresh spawn, and decision answer opt in, local landing
  never does.
- fm-send.sh: a --resolve-key naming an open needs-decision or captain-held
  task is a decision answer and meets the partition; blocked: keys stay
  steering.
- fm-spawn.sh: enforce the record's spend cap for a fresh ordinary spawn by
  either actor; relaunches and secondmates exempt.
- fm-branch-prompt.sh: fixed Postures section and the verbatim
  ask-user-authority policy; the prefix stays byte-stable.
- fm-afk-return.sh: count what the away session handled from the store.
- docs, afk skill, AGENTS.md stub: main parked on Pi, green merge gate
  absolute while away.
- tests: watcher and branch extension suites, fleet-record, merge, and
  decision-answer suites cover the relocation, the vetoes, the tail, the
  parked processing turn, the cancellation, the re-presentation, and the
  spend cap; dated live-guard evidence recorded.

* no-mistakes(review): Refuse branch merge after preflight archive race

* no-mistakes(review): Fix away wake, spawn, and processing races

* no-mistakes(review): Suppress parked processing; narrow away-only rejection

* no-mistakes(review): Abort dedicated processing; gate branch spawn once

* no-mistakes(review): Stamp away-only on the dispatch offer

* no-mistakes(review): Treat invalid away records as spend-cap absence

* no-mistakes(review): Drop spawn test hook; abort processing-opened runs

* no-mistakes(review): Bind abort to opening prompt; cap-read absence

* no-mistakes(review): Limit away branch spawn to queued work only

* no-mistakes(document): Correct AFK posture documentation
* ci: simplify CI job timeouts to a three-tier policy

Replace the scattered per-job timeout values (10m parallel, 25m lint, 30m
serial, 10m macOS) with three readable tiers, each a hang tripwire with
headroom rather than a packing estimate:

- fast (5m): coverage guard, repo invariants, timing aggregate
- normal (30m, one shared budget): lint partitions, portable parallel
  shards, portable serial shards, macOS stock Bash
- heavy (Herdr only): 20m step tripwire on the family run so always()
  cleanup still runs, under a 75m job-level last-resort backstop

The workflow's header comment states the policy and points at
docs/fm-test-portable-shards.md "Timeouts", which now owns it, and each
job names its tier beside timeout-minutes. tests/fm-ci-workflow.test.sh
asserts the policy against the parsed workflow instead of the old
per-job minute values: every job joins exactly one tier, exactly three
distinct job-level values exist, the fast tier stays within 5-10
minutes, the normal budget stays at least double the modeled parallel
lane sum reported by fm-test-run.sh --check-coverage, and the Herdr step
tripwire stays below its job backstop with an always() cleanup after it.

Concurrency supersession, shard counts, lane membership, and fail-fast
settings are unchanged.

* no-mistakes(review): Decouple the normal timeout from packing estimates

* no-mistakes(review): Assert Herdr teardown follows the family run

* no-mistakes(review): Pin Herdr family-run timeout to 20 minutes

* no-mistakes(review): Ignore comments when identifying Herdr steps

* no-mistakes(review): Identify Herdr steps by declarative ids

* no-mistakes(document): Clarify authoritative three-tier timeout policy
Bring in three commits from kunchenguid/firstmate main:
- 4812db8 fix(bin): preserve Claude lock ownership after helper recycling (kunchenguid#4894)
- 65a3bac feat: park main under the away posture on Pi (kunchenguid#4889)
- 2bcb88c ci: standardize workflow timeouts into three tiers (kunchenguid#4910)

Conflict resolution in tests/fm-send-resolve-key.test.sh:
- Fork change added test_secondmate_helper_keyed_report_then_resolve_key
  (fm-secondmate-report.sh --key decision routing)
- Upstream change added test_decision_answer_partition_relocates_under_the_record
  (away-posture decision answer partition)
- Both are independent functions, kept both with their test registrations
Copilot AI lite review requested due to automatic review settings September 19, 2026 09:04

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🟡 Changes recommended

Five unresolved findings affect away-posture routing, cancellation, acknowledgement, and contract validation.

Get a fresh assessment by requesting another Copilot review.

Review effort: Lite
Findings: 1 High severity

Open (1)
What changed in this PR

Merges upstream fixes for Claude lock ownership, Pi away-posture supervision, and CI timeout tiers.

Changes:

  • Preserves trusted Claude session ownership across helper recycling.
  • Parks Pi main during away posture and routes gated work to the supervision branch.
  • Standardizes CI jobs into fast, normal, and heavy timeout tiers.

Review findings:

  • .pi/extensions/fm-branch-supervision.ts:1499 — Moderate, 1 vote: Reject away-only wakes before claiming ordinary task rows.
  • .pi/extensions/fm-branch-supervision.ts:1095 — Moderate, 1 vote: Track and cancel queued nextTurn processing when away posture appears.
  • .pi/extensions/lib/fm-branch-dispatch.ts:37 — Critical, 1 vote: Validate .afk-contract records before routing away.
  • .pi/extensions/lib/fm-branch-dispatch.ts:307 — Moderate, 1 vote: Preserve durable acknowledgements for branch-handled check rows.
  • bin/fm-branch-prompt.sh:99 — Moderate, 1 vote: Keep Relay/public check rows on main or provide complete branch handling.
File Reviewed change
tests/​fm-turnend-foreign-owner-repro.py Adds same-session lock regression coverage.
tests/​fm-send-resolve-key.test.sh Tests away decision-answer routing.
tests/​fm-pr-merge.test.sh Tests away merge authorization and races.
tests/​fm-pi-watch-extension.test.sh Tests away wake eligibility.
tests/​fm-pi-branch-extension.test.sh Tests parked-main lifecycle behavior.
tests/​fm-ci-workflow.test.sh Tests timeout-tier policy.
tests/​fm-branch-supervision.test.sh Tests away authority and spend caps.
docs/​watcher-continuity.md Documents session ownership continuity.
docs/​verification/​supervision.md Records lock verification.
docs/​verification/​runtime-backends.md Records away-posture verification.
docs/​turnend-guard.md Documents trusted lock ownership.
docs/​supervision-protocols/​pi.md Updates Pi supervision protocol.
docs/​sessionstart-nudge.md Documents same-session lock handling.
docs/​scripts.md Updates lock-library ownership guidance.
docs/​pi-supervision-branch.md Defines away-posture contracts.
docs/​fm-test-portable-shards.md Defines timeout tiers.
docs/​configuration.md Documents Pi away behavior.
docs/​architecture.md Documents parked-main architecture.
bin/​fm-turnend-guard.sh Uses shared ownership verification.
bin/​fm-startup-network.sh Clarifies lock sweep ownership.
bin/​fm-spawn.sh Adds away dispatch and spend-cap gates.
bin/​fm-session-start.sh Updates lock ownership documentation.
bin/​fm-session-lock-lib.sh Implements trusted session ownership.
bin/​fm-send.sh Partitions decision answers by posture.
bin/​fm-pr-merge.sh Supports guarded away merges.
bin/​fm-merge-local.sh Keeps local landing main-owned.
bin/​fm-lock.sh Persists trusted session identity.
bin/​fm-lease-lib.sh Adds posture-aware authority relocation.
bin/​fm-claude-stop-autoarm.sh Preserves lock ownership across recycling.
bin/​fm-branch-prompt.sh Adds away-posture role rules.
bin/​fm-afk-return.sh Reports away-session outcomes.
AGENTS.md Documents lock sidecars and Pi away behavior.
.pi/​extensions/​lib/​fm-branch-dispatch.ts Adds posture-aware eligibility and routing.
.pi/​extensions/​fm-primary-pi-watch.ts Routes away-posture wake rows.
.pi/​extensions/​fm-branch-supervision.ts Implements parked-main routing and outcome handling.
.github/​workflows/​ci.yml Applies the three timeout tiers.
.agents/​skills/​afk/​SKILL.md Documents Pi away-posture operation.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment on lines +37 to +42
export function afkPostureRecordPresent(state: string): boolean {
try {
return statSync(join(state, AFK_CONTRACT_FILE)).isFile();
} catch {
return false;
}
@matthewstrud
matthewstrud merged commit cc3aede into main Sep 19, 2026
18 of 19 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.

3 participants