Skip to content

feat: strengthen fleet safety and decision handling - #34

Merged
Keigyoku merged 14 commits into
mainfrom
fm/fm-upstream-batchb-w9
Jul 23, 2026
Merged

Keigyoku merged 14 commits into
mainfrom
fm/fm-upstream-batchb-w9

Conversation

@Keigyoku

Copy link
Copy Markdown
Owner

Intent

Port firstmate upstream Batch B (safety take-soon) onto Keigyoku/firstmate as one big-wave PR with bisectable per-item commits.

B1: gate-refuse kunchenguid#518 + subagent guard kunchenguid#854, composing with fork PreToolUse stacks (arm-pretool, kill-guard, TDD, claim-guard) — proven by composition tests.
B2: cd-guard kunchenguid#483 + secondmate turnend scope kunchenguid#505 without regressing fork physical-identity matching / claim-guard Stop path.
B3: tip fm-lock-lib + teardown reconciliation, keeping /home<->/var/home path-spelling retry and landed-work refusals.
B4: decision-hold kunchenguid#593+kunchenguid#654 with bearings/fleet-snapshot decisions_open unstubbed for structured captain holds.
B5 partial: kunchenguid#752 wall-clock identity + kunchenguid#747 wake enrichment. SKIPPED (PR notes): kunchenguid#637 guard banners, kunchenguid#743 parked-stale, kunchenguid#744 ordinary-vs-recovery — fork private watch/guard evolution; avoid re-introducing double-fork/orphan watcher topology (fork #23 arm-as-child is law).

TDD red-first for behavior; RED evidence belongs in PR body. PR must target Keigyoku/firstmate base main (not kunchenguid). Do not merge.

What Changed

  • Add composed primary-session guards that block out-of-fleet delegation and persistent project-directory changes while preserving legitimate crewmate and secondmate behavior.
  • Introduce durable captain decision holds, require completed decision inventories before scout teardown, and surface open decisions in Bearings and fleet snapshots.
  • Harden teardown lock reconciliation, watcher identity checks, and wake draining with stale-lock protection, wall-clock-safe process matching, and bounded status context.

Risk Assessment

✅ Low: Captain, the latest scope fix correctly uses the shared marker-aware predicate, and all earlier decision-durability, deduplication, scoping, path-identity, teardown, and watcher-topology requirements remain intact.

Testing

The previously successful full-suite baseline was supplemented with 14 focused behavioral scripts and five end-to-end CLI/plugin checks; everything passed, the worktree remained clean, and no screenshots were needed because the changed end-user surfaces are CLI output.

Evidence: Guard composition and scope
Tracked hook stack (Claude):
Bash	"$CLAUDE_PROJECT_DIR"/bin/fm-arm-pretool-check.sh --claude	"$CLAUDE_PROJECT_DIR"/bin/fm-cd-pretool-check.sh --claude
.*	"$CLAUDE_PROJECT_DIR"/bin/fm-subagent-pretool-check.sh --claude

Primary scope delegation refusal:
exit=2
{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"deny"},"systemMessage":"[subagent-dispatch] the firstmate primary dispatches through the fleet, not the harness's own delegation tools: work started that way has no durable fleet record, leaves every firstmate guard inert, and dies with this session. Instead, investigation and ship work both go to bin/fm-brief.sh then bin/fm-spawn.sh (blocked tool: Agent, delegation-shaped on \"agent\"). Launch the session with FM_ALLOW_SUBAGENT=1 for a deliberate exception."}

Ordinary tool allowed by the same guard:
exit=0 output=<empty>

Primary persistent-cd refusal:
exit=2
{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"deny"},"systemMessage":"[persistent-cd] a persistent top-level directory change in the primary firstmate checkout is blocked; it would move the shell out of the home so a later firstmate-owned command runs inside a project clone. Reach the target without moving the shell - use git -C <dir> or an absolute path on the command itself - or scope the cd to a subshell like (cd <dir> && ...)."}

Subshell cd allowed:
exit=0 output=<empty>

Linked task-worktree scope remains inert:
git-dir=/var/home/mlight/.no-mistakes/repos/b4d18c208d2d.git/worktrees/01KY6GSH64D5A0EZCJHE2C4G24
common-dir=/var/home/mlight/.no-mistakes/repos/b4d18c208d2d.git
exit=0 output=<empty>
Evidence: Teardown reconciliation and refusal
Unlanded ship teardown (must refuse and preserve metadata):
exit=1 meta_preserved=yes
●━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
●  WATCHER DOWN - SUPERVISION IS OFF
●  2 task(s) in flight, but no watcher has a fresh beacon (last beat: never, grace 999999s).
●  Trust the emitted supervision protocol for this harness; do not use shell & for watcher repair.
●  This is a supervision warning only; the guarded operation WILL still run.
●  resume supervision according to the session-start block for this harness; do not use shell &.
●━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
REFUSED: worktree /tmp/no-mistakes-evidence/01KY6GSH64D5A0EZCJHE2C4G24/teardown-runtime/project has work not on any remote and not landed.
unpushed commits:
0fa22fd evidence unlanded change
Push the branch, land its PR, or get the captain's explicit OK to discard, then --force.

Provably stale lock during scout return (must retry, remove, and finish):
exit=0 lock_present=no meta_present=no treehouse_attempts=3
●━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
●  WATCHER DOWN - SUPERVISION IS OFF
●  2 task(s) in flight, but no watcher has a fresh beacon (last beat: never, grace 999999s).
●  Trust the emitted supervision protocol for this harness; do not use shell & for watcher repair.
●  This is a supervision warning only; the guarded operation WILL still run.
●  resume supervision according to the session-start block for this harness; do not use shell &.
●━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
fatal: Unable to create index.lock: File exists
teardown: worktree return failed with git lock /tmp/no-mistakes-evidence/01KY6GSH64D5A0EZCJHE2C4G24/teardown-runtime/scout-worktree/.git/index.lock present; waiting 0s and retrying (owning process may be exiting)
fatal: Unable to create index.lock: File exists
teardown: removed provably-stale git lock /tmp/no-mistakes-evidence/01KY6GSH64D5A0EZCJHE2C4G24/teardown-runtime/scout-worktree/.git/index.lock (age >= 30s, no live holder) and retrying worktree return
treehouse: returned /tmp/no-mistakes-evidence/01KY6GSH64D5A0EZCJHE2C4G24/teardown-runtime/scout-worktree
teardown: worktree return succeeded after stale-lock cleanup
teardown stale-scout complete (window evidence:fm-stale-scout, worktree /tmp/no-mistakes-evidence/01KY6GSH64D5A0EZCJHE2C4G24/teardown-runtime/scout-worktree)
Backlog: stale-scout just finished. Run tasks-axi done stale-scout --report data/stale-scout/report.md, then run tasks-axi ready for dependency-cleared candidates, check date gates, and dispatch only work whose blockers are gone and date is due.
Evidence: Structured decision lifecycle
Structured hold created: sample-review-decision-route

Bearings before resolution:
{
  "decisions_open": [
    {
      "id": "sample-review-decision-route",
      "key": "sample-review-decision-route",
      "verb": "captain-hold",
      "summary": "Choose sample route: captain route choice pending",
      "owner": "(main)"
    }
  ],
  "gates": [
    {
      "id": "sample-implementation",
      "title": "Apply selected route",
      "blocked_by": "sample-review-decision-route",
      "reason": "-",
      "owner": "(main)"
    }
  ],
  "reports": [
    {
      "id": "sample-review",
      "path": "/tmp/no-mistakes-evidence/01KY6GSH64D5A0EZCJHE2C4G24/decision-home/data/sample-review/report.md"
    }
  ]
}

Fleet snapshot before resolution (structured backlog record):
{
  "hold_record": {
    "order": 2,
    "state": "queued",
    "structured": true,
    "id": "sample-review-decision-route",
    "checked": false,
    "title": "Choose sample route",
    "repo": "sample",
    "kind": "captain",
    "priority": null,
    "hold_reason": "captain route choice pending",
    "hold_kind": "captain",
    "blocked_by": null,
    "blocked_by_ids": [],
    "blocked_reason": null,
    "since": "2026-07-22",
    "merged": null,
    "reported": null,
    "done": null,
    "completion": {
      "verb": null,
      "date": null
    },
    "links": [],
    "pr_url": null,
    "report_path": null,
    "local_note": null,
    "raw": "- [ ] sample-review-decision-route - Choose sample route (repo: sample) (kind: captain) (since 2026-07-22) (hold: captain route choice pending) (hold-kind: captain)",
    "body_lines": [
      "Origin: sample-review",
      "Decision key: route",
      "State: awaiting captain decision."
    ],
    "body_excerpt": "Origin: sample-review Decision key: route State: awaiting captain decision.",
    "unresolved_blocker_ids": [],
    "current_role": "queued",
    "requires_child_metadata": false,
    "captain_actionable": false
  },
  "origin_task": {
    "id": "sample-review",
    "kind": "scout",
    "harness": "codex",
    "mode": "scout",
    "yolo": "",
    "project": "/tmp/no-mistakes-evidence/01KY6GSH64D5A0EZCJHE2C4G24/decision-home/projects/sample",
    "backend": "tmux",
    "paths": {
      "meta": {
        "path": "/tmp/no-mistakes-evidence/01KY6GSH64D5A0EZCJHE2C4G24/decision-home/state/sample-review.meta",
        "present": true
      },
      "status_log": {
        "path": "/tmp/no-mistakes-evidence/01KY6GSH64D5A0EZCJHE2C4G24/decision-home/state/sample-review.status",
        "present": true,
        "kind": "event_history",
        "last_event": {
          "state": "captain-held [key=route]",
          "note": "tracked by sample-review-decision-route",
          "raw": "captain-held [key=route]: tracked by sample-review-decision-route"
        }
      },
      "worktree": {
        "path": "/tmp/no-mistakes-evidence/01KY6GSH64D5A0EZCJHE2C4G24/decision-home/projects/sample-review",
        "present": false
      },
      "home": {
        "path": null,
        "present": false
      },
      "report": {
        "path": "/tmp/no-mistakes-evidence/01KY6GSH64D5A0EZCJHE2C4G24/decision-home/data/sample-review/report.md",
        "present": true
      }
    },
    "secondmate_projects": [],
    "current_state": {
      "state": "unknown",
      "source": "none",
      "detail": "worktree gone (torn down?)",
      "raw": "state: unknown · source: none · worktree gone (torn down?)"
    },
    "endpoint": {
      "target": "evidence:fm-sample-review",
      "exists": false,
      "agent_alive": "not_checked"
    },
    "pr": {
      "url": null,
      "source": "absent"
    },
    "hints": {
      "pending_decision": false,
      "blocked_event": false,
      "scout_report_present": true,
      "last_event_text": "captain-held [key=route]: tracked by sample-review-decision-route"
    },
    "actions": {
      "watch": "bin/fm-peek.sh fm-sample-review",
      "steer": "bin/fm-send.sh fm-sample-review '<instruction>'",
      "return_channel_note": null
    },
    "backlog": {
      "order": 1,
      "state": "in_flight",
      "structured": true,
      "id": "sample-review",
      "checked": false,
      "title": "Investigate sample routing",
      "repo": "sample",
      "kind": "scout",
      "priority": null,
      "hold_reason": null,
      "hold_kind": null,
      "blocked_by": null,
      "blocked_by_ids": [],
      "blocked_reason": null,
      "since": "2026-07-22",
      "merged": null,
      "reported": null,
      "done": null,
      "completion": {
        "verb": null,
        "date": null
      },
      "links": [],
      "pr_url": null,
      "report_path": null,
      "local_note": null,
      "raw": "- [ ] sample-review - Investigate sample routing (repo: sample) (kind: scout) (since 2026-07-22)",
      "body_lines": [],
      "body_excerpt": null,
      "unresolved_blocker_ids": [],
      "current_role": "worker",
      "requires_child_metadata": true,
      "captain_actionable": false
    }
  }
}

Resolve only after dependent routing edge exists:
resolved: sample-review-decision-route -> sample-implementation
hold_state=done dependent_blocked=no

Bearings after resolution:
{
  "decisions_open": [],
  "gates": [
    {
      "id": "sample-implementation",
      "title": "Apply selected route",
      "blocked_by": "-",
      "reason": "-",
      "owner": "(main)"
    }
  ]
}
Evidence: Enriched wake and wall-clock identity
Enriched wake drain:
1784782660	1	signal	task.status	signal: task.status
wake annotation: latest wake-EVENT observed at drain, not current state: task.status: done: branch ready for review

Watcher identity across simulated wall-clock step:
before=linux-starttime=987654 cmdline-hex=62617368002f706174682077697468207370616365732f666d2d77617463682e7368002d2d666c616700
after_btime_change=linux-starttime=987654 cmdline-hex=62617368002f706174682077697468207370616365732f666d2d77617463682e7368002d2d666c616700
identity_stable=yes

Watcher identity after simulated PID reuse:
after_starttime_change=linux-starttime=987655 cmdline-hex=62617368002f706174682077697468207370616365732f666d2d77617463682e7368002d2d666c616700
pid_reuse_detected=yes
Evidence: OpenCode external-healthy-arm
{
  "externalHealthyOutput": "watcher: healthy pid=1 (beacon 0s)",
  "armRequest": "args=--restart",
  "guardExecuted": true,
  "blindTurnPromptDelivered": true,
  "promptExcerpt": "TURN WOULD END BLIND - supervision is off. Resume supervision according to the session-start operating block before ending the turn.\n\nTURN WOULD END BLIND: guard ran after external healthy watcher"
}
- Outcome: 🔧 1 issue found → auto-fixed (2) ✅ across 3 runs (56m11s)

Pipeline

Updates from git push no-mistakes

✅ **intent** - passed

✅ No issues found.

✅ **Rebase** - passed

✅ No issues found.

🔧 **Review** - 2 issues found → auto-fixed (3) ✅
  • 🚨 bin/fm-decision-hold.sh:150 - Captain, origin_open_decisions suppresses still-open keyed needs-decision/blocked events when a non-secondmate’s final status is merely done or failed. Consequently, complete --none, verify, and scout teardown can succeed without an explicit resolved or captain-held close. This contradicts required B4 behavior: “decision-hold feat: add durable captain decision holds kunchenguid/firstmate#593+fix(bin): strip quoted blocked_by values during decision hold resolve kunchenguid/firstmate#654 … unstubbed for structured captain holds.” Confirm whether terminal status should override an unresolved structured decision; otherwise remove this suppression.
  • ⚠️ bin/fm-bearings-snapshot.sh:361 - bearings_decisions includes secondmate captain holds, while decisions_all adds those holds separately before adding bearings_decisions. Each hold is emitted twice and consumes the bounded decision budget twice, potentially crowding out distinct decisions. Keep the reason-bearing explicit projection and make bearings_decisions status-only.

🔧 Fix: Captain: enforce durable decisions and deduplicate holds
1 warning still open:

  • ⚠️ AGENTS.md:803 - Captain, the operational instructions say fm-turnend-guard.sh never fires in a secondmate home, contradicting the newly widened marker-aware scope described two lines earlier. docs/turnend-guard.md:177 similarly equates claim-guard and turn-end scoping even though claim-guard intentionally retains the narrower Stop path. Update both statements to distinguish secondmate turn-end coverage from claim-guard’s unchanged scope.

🔧 Fix: Captain: clarify secondmate turn-end and claim-guard scope
1 warning still open:

  • ⚠️ docs/subagent-guard.md:128 - Captain, the new scope contract says nonexistent fm-sessionstart-nudge.sh and fm-turnend-guard.sh both use fm_primary_scope_matches, but turn-end instead duplicates the checks without calling the predicate. Remove the nonexistent consumer reference and have turn-end call the shared predicate—or document its separate implementation—so the promised no-drift invariant is accurate.

🔧 Fix: Captain: unify marker-aware primary scope enforcement
✅ Re-checked - no issues remain.

🔧 **Test** - 1 issue found → auto-fixed (2) ✅
  • 🚨 tests failed with exit code 1
  • command -v tmux >/dev/null || { echo "tmux is required for e2e tests" >&2; exit 1; }; tmux -V; rc=0; for t in tests/*.test.sh; do echo "== $t =="; bash "$t" || rc=1; done; exit "$rc"

🔧 Fix: Captain: complete scout decision inventories in teardown fixtures
1 error still open:

  • 🚨 tests failed with exit code 1
  • command -v tmux >/dev/null || { echo "tmux is required for e2e tests" >&2; exit 1; }; tmux -V; rc=0; for t in tests/*.test.sh; do echo "== $t =="; bash "$t" || rc=1; done; exit "$rc"

🔧 Fix: Captain, stabilize teardown fixtures and OpenCode guard
✅ Re-checked - no issues remain.

  • command -v tmux >/dev/null || { echo "tmux is required for e2e tests" >&2; exit 1; }; tmux -V; rc=0; for t in tests/*.test.sh; do echo "== $t =="; bash "$t" || rc=1; done; exit "$rc"
  • Configured baseline: command -v tmux &gt;/dev/null || { echo &#34;tmux is required for e2e tests&#34; &gt;&amp;2; exit 1; }; tmux -V; rc=0; for t in tests/*.test.sh; do echo &#34;== $t ==&#34;; bash &#34;$t&#34; || rc=1; done; exit &#34;$rc&#34; (previously passed)
  • for t in tests/fm-pretool-stack-composition.test.sh tests/fm-gate-refuse.test.sh tests/fm-subagent-pretool-check.test.sh tests/fm-cd-pretool-check.test.sh tests/fm-turnend-guard.test.sh tests/fm-lock-lib.test.sh tests/fm-backend.test.sh tests/fm-backend-orca.test.sh tests/fm-backend-zellij.test.sh tests/fm-decision-hold-lifecycle.test.sh tests/fm-bearings-snapshot.test.sh tests/fm-wake-queue.test.sh tests/fm-watcher-lock.test.sh tests/fm-pi-watch-extension.test.sh; do bash "$t" || rc=1; done; exit "$rc"
  • Manual composed PreToolUse refusal/allow and cd physical-scope CLI flows
  • Manual unlanded-work teardown refusal and stale-lock reconciliation flows
  • Manual decision hold, Bearings/fleet snapshot, routing, and resolution lifecycle
  • Manual enriched wake drain and simulated wall-clock/PID-reuse identity checks
  • OpenCode watch-arm and turn-end plugin integration with external healthy output
  • git status --short cleanup verification
✅ **Document** - passed

✅ No issues found.

✅ **Lint** - passed

✅ No issues found.

✅ **Push** - passed

✅ No issues found.

Keigyoku added 13 commits July 22, 2026 22:21
Port upstream kunchenguid#518 (6556882): gate agents must not spawn/send/teardown.
Sourced refuse lib on fleet entrypoints; FM_GATE_REFUSE_BYPASS for suite.

Cherry-picked-from: 6556882 (kunchenguid#518)
Port upstream kunchenguid#854 (50cc24a) plus primary-scope-lib dependency.
Compose subagent PreToolUse matcher with fork arm-pretool seatbelt and
claim-guard Stop path; do not track Claude permissions.deny.
Composition covered by tests/fm-pretool-stack-composition.test.sh.

Cherry-picked-from: 50cc24a (kunchenguid#854)
B2: upstream kunchenguid#483 (88e38ea) + kunchenguid#505 (2364817).

- cd-guard PreToolUse seatbelt composed with arm-pretool and subagent stacks
- export arm shell classifier for reuse; do not replace private PreToolUse rails
- turnend force-includes marked secondmate homes via primary-scope-lib
- keep fork physical-identity matching, queue-pending, and claim-guard Stop path

Cherry-picked-from: 88e38ea (kunchenguid#483), 2364817 (kunchenguid#505)
B3: replace deferred kunchenguid#435 with tip lock-lib ownership.
Teardown sources fm_lock_is_provably_stale for index.lock cleanup while
keeping fork path-spelling retry (7c0ad63 family) and landed-work refusals.

Cherry-picked-from: tip fm-lock-lib.sh (replaces deferred kunchenguid#435)
B4: upstream kunchenguid#593 (cd218f2) + kunchenguid#654 (024b96b).
Adds fm-decision-hold.sh, decision-hold-lifecycle skill/docs, classify
status_open_decisions fold, scout teardown verify gate, and unstubs
bearings/fleet-snapshot decisions_open for structured captain holds.

Cherry-picked-from: cd218f2 (kunchenguid#593), 024b96b (kunchenguid#654)
B5 partial: upstream kunchenguid#752 (a26b37c). Prefer Linux /proc starttime+cmdline
over ps lstart so WSL2 btime drift cannot false-evict a live watcher.

Cherry-picked-from: a26b37c (kunchenguid#752)
B5 partial: upstream kunchenguid#747 (4ab61fa). After raw queue commit, best-effort
annotations add home-local status context without blocking appends or
reordering authoritative drain rows. No watch-arm topology changes.

Cherry-picked-from: 4ab61fa (kunchenguid#747)
Copilot AI review requested due to automatic review settings July 23, 2026 05:08

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 was unable to review this pull request because the user who requested the review has reached their quota limit.

@Keigyoku
Keigyoku merged commit 7955e0c into main Jul 23, 2026
4 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.

2 participants