@zerwiz/ymir 0.1.16 → 0.1.18

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (171) hide show
  1. package/.agents/config/app-repos.yaml +38 -0
  2. package/.claude/settings.json +31 -0
  3. package/.codex/hooks.json +43 -0
  4. package/.cursor/hooks.json +33 -0
  5. package/.pi/extensions/README.md +39 -0
  6. package/.pi/extensions/gna-pi-watch.ts +15 -0
  7. package/.pi/extensions/lib/ro-assistant-layout.ts +96 -0
  8. package/.pi/extensions/lib/ro-operational-user-layout.ts +145 -0
  9. package/.pi/extensions/lib/ro-visibility.ts +99 -0
  10. package/.pi/extensions/lib/ro-working-ship.ts +243 -0
  11. package/.pi/extensions/lib/rodd-operational-input.ts +62 -0
  12. package/.pi/extensions/lib/skuld-branch-dispatch.ts +252 -0
  13. package/.pi/extensions/lib/skuld-branch-model-picker.ts +77 -0
  14. package/.pi/extensions/lib/vordr-sessionstart-supervisor.mjs +48 -0
  15. package/.pi/extensions/ro.ts +11 -0
  16. package/.pi/extensions/skuld-branch-supervision.ts +11 -0
  17. package/.pi/extensions/syn-turnend-guard.ts +11 -0
  18. package/.pi/mcp.json +16 -0
  19. package/.pi/mcp.json.example +11 -0
  20. package/.pi/npm/.gitignore +2 -0
  21. package/.pi/settings.json +8 -0
  22. package/.pi/shared/extensions/gna-pi-watch.ts +750 -0
  23. package/.pi/shared/extensions/herdr-agent-state.ts +257 -0
  24. package/.pi/shared/extensions/open-editor.ts +509 -0
  25. package/.pi/shared/extensions/ro.ts +504 -0
  26. package/.pi/shared/extensions/skuld-branch-supervision.ts +1535 -0
  27. package/.pi/shared/extensions/syn-turnend-guard.ts +683 -0
  28. package/.pi/shared/extensions/todo.ts +133 -0
  29. package/.pi/shared/extensions/ymir-subagents.ts +287 -0
  30. package/.pi/shared/extensions/ymir-well.ts +130 -0
  31. package/CHANGELOG.md +23 -0
  32. package/assets/Ymir.md +4654 -0
  33. package/assets/healer.html +717 -0
  34. package/assets/icon-family/family-sheet.png +0 -0
  35. package/assets/icon-family/hlidskjalf.svg +1 -0
  36. package/assets/icon-family/sessrumnir.svg +1 -0
  37. package/assets/icon-family/smidja.svg +1 -0
  38. package/assets/icon-family/ymir.svg +1 -0
  39. package/assets/mimirswell.html +880 -0
  40. package/assets/mock/README.md +16 -0
  41. package/assets/mock/feeds.ts +175 -0
  42. package/assets/mock/mock.ts +408 -0
  43. package/assets/reference/docs/agent-control.md +122 -0
  44. package/assets/reference/docs/apodex/Appdex_as_Orchestrator_Planner_Reviewer_Assessment.md +176 -0
  45. package/assets/reference/docs/apodex/Final Report(1).md +198 -0
  46. package/assets/reference/docs/apodex/Final Report(2).md +54 -0
  47. package/assets/reference/docs/apodex/Final Report(3).md +40 -0
  48. package/assets/reference/docs/apodex/Final Report(4).md +234 -0
  49. package/assets/reference/docs/apodex/Final Report(5).md +13 -0
  50. package/assets/reference/docs/apodex/Final Report.md +82 -0
  51. package/assets/reference/docs/architecture.md +392 -0
  52. package/assets/reference/docs/arm-pretool-check.md +247 -0
  53. package/assets/reference/docs/calm-mode-feasibility.md +541 -0
  54. package/assets/reference/docs/calm.md +59 -0
  55. package/assets/reference/docs/captain-hold-lifecycle.md +96 -0
  56. package/assets/reference/docs/cd-guard.md +165 -0
  57. package/assets/reference/docs/cmux-backend.md +133 -0
  58. package/assets/reference/docs/codex-app-backend.md +57 -0
  59. package/assets/reference/docs/configuration.md +924 -0
  60. package/assets/reference/docs/documentation-audiences.json +468 -0
  61. package/assets/reference/docs/documentation-audiences.md +28 -0
  62. package/assets/reference/docs/examples/crew-dispatch.json +26 -0
  63. package/assets/reference/docs/examples/process-event-extension/file-signal.mjs +96 -0
  64. package/assets/reference/docs/examples/process-event-extension/firstmate-extension.json +15 -0
  65. package/assets/reference/docs/examples/watched-tools.json +24 -0
  66. package/assets/reference/docs/examples/wedge-alarm +22 -0
  67. package/assets/reference/docs/extension-bindings.md +237 -0
  68. package/assets/reference/docs/fm-test-isolation-proof.json +37 -0
  69. package/assets/reference/docs/fm-test-isolation-proof.md +149 -0
  70. package/assets/reference/docs/fm-test-portable-shards.md +118 -0
  71. package/assets/reference/docs/gitlab-merge-watch.md +275 -0
  72. package/assets/reference/docs/herdr-backend.md +346 -0
  73. package/assets/reference/docs/orca-backend.md +84 -0
  74. package/assets/reference/docs/pi-supervision-branch-poster.svg +125 -0
  75. package/assets/reference/docs/pi-supervision-branch.md +96 -0
  76. package/assets/reference/docs/plans/agentic-engineering-workflow.md +395 -0
  77. package/assets/reference/docs/plans/agentic-factory-implementation-plan.md +615 -0
  78. package/assets/reference/docs/plans/command-factory-enhancement-plan.md +420 -0
  79. package/assets/reference/docs/plans/phase-1-layer3-program-design.md +134 -0
  80. package/assets/reference/docs/plans/phase-2-vertical-slices.md +100 -0
  81. package/assets/reference/docs/plans/phase-3-measurable-goals.md +106 -0
  82. package/assets/reference/docs/plans/phase-4-context-budget.md +106 -0
  83. package/assets/reference/docs/plans/phase-5-pre-mortem-confidence.md +100 -0
  84. package/assets/reference/docs/plans/phase-6-incident-to-agent.md +109 -0
  85. package/assets/reference/docs/remote-secondmates.md +268 -0
  86. package/assets/reference/docs/scripts.md +146 -0
  87. package/assets/reference/docs/sessionstart-nudge.md +115 -0
  88. package/assets/reference/docs/subagent-guard.md +383 -0
  89. package/assets/reference/docs/supervision-protocols/claude.md +27 -0
  90. package/assets/reference/docs/supervision-protocols/codex.md +16 -0
  91. package/assets/reference/docs/supervision-protocols/cursor.md +31 -0
  92. package/assets/reference/docs/supervision-protocols/grok.md +39 -0
  93. package/assets/reference/docs/supervision-protocols/opencode.md +17 -0
  94. package/assets/reference/docs/supervision-protocols/pi.md +33 -0
  95. package/assets/reference/docs/supervision-protocols/unknown.md +13 -0
  96. package/assets/reference/docs/tmux-backend.md +115 -0
  97. package/assets/reference/docs/trace-context.md +117 -0
  98. package/assets/reference/docs/turnend-guard.md +170 -0
  99. package/assets/reference/docs/verification/dispatch-auth.md +204 -0
  100. package/assets/reference/docs/verification/muse.md +221 -0
  101. package/assets/reference/docs/verification/process-event-sources.md +208 -0
  102. package/assets/reference/docs/verification/public-followup.md +119 -0
  103. package/assets/reference/docs/verification/runtime-backends.md +1022 -0
  104. package/assets/reference/docs/verification/stow-memory.md +53 -0
  105. package/assets/reference/docs/verification/supervision.md +534 -0
  106. package/assets/reference/docs/verification/trace-context.md +31 -0
  107. package/assets/reference/docs/voice-relay.md +295 -0
  108. package/assets/reference/docs/watcher-continuity.md +119 -0
  109. package/assets/reference/docs/wedge-alarm.md +39 -0
  110. package/assets/reference/docs/zellij-backend.md +114 -0
  111. package/assets/reference/index.html +1172 -0
  112. package/assets/reference/skills/afk/SKILL.md +241 -0
  113. package/assets/reference/skills/ahoy/SKILL.md +56 -0
  114. package/assets/reference/skills/ask-user-authority/SKILL.md +56 -0
  115. package/assets/reference/skills/bearings/SKILL.md +161 -0
  116. package/assets/reference/skills/bearings/assets/board-template.html +731 -0
  117. package/assets/reference/skills/bootstrap-diagnostics/SKILL.md +68 -0
  118. package/assets/reference/skills/captain-hold-lifecycle/SKILL.md +54 -0
  119. package/assets/reference/skills/decision-hold-lifecycle/SKILL.md +15 -0
  120. package/assets/reference/skills/diagnostic-reasoning/SKILL.md +53 -0
  121. package/assets/reference/skills/firstmate-codexapp/SKILL.md +110 -0
  122. package/assets/reference/skills/firstmate-codexapp/agents/openai.yaml +4 -0
  123. package/assets/reference/skills/firstmate-coding-guidelines/SKILL.md +127 -0
  124. package/assets/reference/skills/firstmate-orca/SKILL.md +92 -0
  125. package/assets/reference/skills/fmx-respond/SKILL.md +283 -0
  126. package/assets/reference/skills/harness-adapters/SKILL.md +95 -0
  127. package/assets/reference/skills/harness-adapters/references/common/control-and-recovery.md +37 -0
  128. package/assets/reference/skills/harness-adapters/references/common/dispatch.md +32 -0
  129. package/assets/reference/skills/harness-adapters/references/common/model-and-effort.md +42 -0
  130. package/assets/reference/skills/harness-adapters/references/common/primary-hooks.md +40 -0
  131. package/assets/reference/skills/harness-adapters/references/harness/claude.md +55 -0
  132. package/assets/reference/skills/harness-adapters/references/harness/codex.md +43 -0
  133. package/assets/reference/skills/harness-adapters/references/harness/cursor.md +75 -0
  134. package/assets/reference/skills/harness-adapters/references/harness/grok.md +69 -0
  135. package/assets/reference/skills/harness-adapters/references/harness/kimi.md +51 -0
  136. package/assets/reference/skills/harness-adapters/references/harness/muse.md +70 -0
  137. package/assets/reference/skills/harness-adapters/references/harness/opencode.md +42 -0
  138. package/assets/reference/skills/harness-adapters/references/harness/pi.md +56 -0
  139. package/assets/reference/skills/process-event-sources/SKILL.md +137 -0
  140. package/assets/reference/skills/project-management/SKILL.md +92 -0
  141. package/assets/reference/skills/quota-array-dispatch/SKILL.md +131 -0
  142. package/assets/reference/skills/secondmate-provisioning/SKILL.md +254 -0
  143. package/assets/reference/skills/stow/SKILL.md +309 -0
  144. package/assets/reference/skills/stuck-crewmate-recovery/SKILL.md +54 -0
  145. package/assets/reference/skills/updatefirstmate/SKILL.md +64 -0
  146. package/assets/skills/README.md +5 -0
  147. package/assets/yggdrasil.html +649 -0
  148. package/assets/ymir-banner-01.png +0 -0
  149. package/assets/ymir-banner-02.png +0 -0
  150. package/assets/ymir-banner-03.png +0 -0
  151. package/assets/ymir-banner-04.png +0 -0
  152. package/assets/ymir-banner-05.png +0 -0
  153. package/assets/ymir-banner-06.png +0 -0
  154. package/assets/ymir-emblem-darksteel.svg +63 -0
  155. package/assets/ymir-emblem-forged.svg +55 -0
  156. package/assets/ymir-emblem-slate.svg +55 -0
  157. package/assets/ymir-mark-algiz-anvil.svg +9 -0
  158. package/assets/ymir-stave.svg +53 -0
  159. package/assets/ymir.html +563 -0
  160. package/bin/ymir-install.sh +6 -1
  161. package/data/learnings.md +33 -0
  162. package/data/local-models.md +34 -0
  163. package/data/operator.md +15 -0
  164. package/data/projects.md +28 -0
  165. package/data/realm.md +1 -0
  166. package/deploy/Containerfile +27 -0
  167. package/deploy/README.md +104 -0
  168. package/deploy/compose/compose.yaml +45 -0
  169. package/deploy/env.example +30 -0
  170. package/deploy/quadlet/ymir.container +57 -0
  171. package/package.json +10 -8
@@ -0,0 +1,115 @@
1
+ # tmux runtime backend
2
+
3
+ tmux is Firstmate's verified reference runtime backend and the fully supported baseline for secondmate homes.
4
+ [`configuration.md`](configuration.md#runtime-backend-configbackend--fm_backend) owns shared backend selection and metadata semantics.
5
+
6
+ ## Setup
7
+
8
+ Install tmux with `brew install tmux` or your platform package manager.
9
+ The universal harness and toolchain requirements are in [`configuration.md`](configuration.md#toolchain).
10
+
11
+ tmux is the hard default when no explicit setting or runtime auto-detection selects another backend.
12
+ Select it explicitly with local `config/backend` containing `tmux`, with `FM_BACKEND=tmux` for one launch, or by asking Firstmate to use tmux.
13
+ An explicit selection is also the opt-out from Herdr or cmux runtime auto-detection.
14
+
15
+ No provisioning is required before the first task.
16
+
17
+ ## Watching the crew
18
+
19
+ For the best visible experience, launch the primary harness inside a tmux session:
20
+
21
+ ```sh
22
+ tmux new -s firstmate
23
+ ```
24
+
25
+ Crew tasks become windows in that session.
26
+ `tmux display-message -p '#S'` prints its name.
27
+ If the primary harness runs outside tmux, Firstmate creates or reuses a detached session named `firstmate`:
28
+
29
+ ```sh
30
+ tmux attach -t firstmate
31
+ ```
32
+
33
+ Each task window is named `fm-<id>`.
34
+
35
+ ```sh
36
+ tmux list-windows -t <session-name>
37
+ tmux select-window -t <session-name>:fm-<id>
38
+ ```
39
+
40
+ Typing into an attached task window is authoritative direct intervention.
41
+ Routine supervision does not require attachment: `bin/fm-peek.sh <id>` captures a bounded tail and `FM_HOME=<home> bin/fm-send.sh <id> '<text>'` steers the recorded endpoint.
42
+
43
+ Verify setup by spawning a small task and confirming its `fm-<id>` window appears in the selected session.
44
+
45
+ ## Current behavior and safety
46
+
47
+ ### Agent liveness probe
48
+
49
+ A target-existence check proves only that the pane exists.
50
+ The deeper tmux agent-liveness probe first verifies exact window membership, then reads process names to distinguish a running harness from a bare idle shell.
51
+ It classifies recognized Claude, Codex, OpenCode, Pi, pi-signed, Grok, Kimi, Cursor, and Muse process identities as `alive`, common shells as `dead`, an authoritatively absent window as `missing`, unreadable state as `unreadable`, and every other process as `ambiguous`.
52
+ Only `dead` and `missing` authorize recovery because a false dead result could launch a duplicate agent.
53
+
54
+ For positive attribution, the probe combines two independent name sources rather than making either one load-bearing.
55
+ `#{pane_current_command}` and the pane tty foreground process group's kernel `comm` values expose different name fields, and which one retains executable identity is platform-dependent.
56
+ The foreground probe also reads argv[0] so an exact harness install-path component can carry the verdict when the other fields expose a rewritten process name.
57
+ Either source naming a verified harness is enough for `alive`, because a false `dead` is the one verdict that can start a duplicate agent on a live worktree, while a readable foreground process group settles the negative verdicts.
58
+
59
+ Scoping the second source to the foreground process group rather than to the pane's descendants is deliberate: a harness-named process left running in the background of an otherwise idle pane must not read as an agent.
60
+ The same scoping covers multi-process launchers without a special case, so the Pi Launcher path is attributed through its `pi-signed` wrapper and `pi` engine even though its title is the exact foreground command `pi-launcher`.
61
+ Direct executable identities `pi`, `pi-signed`, and `Pi` remain accepted exactly, and similar or prefixed process names are not accepted through those exact Pi-family entries.
62
+ Muse is likewise anchored to the exact `muse` launcher identity or the installed `muse-bin-<version>` prefix, so unrelated names such as `musescore` and `amuse` remain ambiguous.
63
+ Cursor is identified from its exact `cursor-agent` identity or versioned install tree in the foreground process path or structured argv[0]; a bare `node` or unrelated `agent` remains ambiguous.
64
+
65
+ The CI-enforced portable regression and opt-in real-harness drift guard follow the split owned by `.agents/skills/firstmate-coding-guidelines/SKILL.md`.
66
+ Run the real-harness guard after any harness upgrade and before trusting refreshed evidence.
67
+
68
+ ### Composer, busy state, and delivery
69
+
70
+ Agent liveness and composer safety are separate checks.
71
+ The tmux reader is a thin adapter over the fleet-wide classifier in `bin/fm-composer-lib.sh`: it contributes one styled full-pane capture, the `#{cursor_y}` cursor row, and foreground-process identity probes, and the shape containing the cursor - a complete bordered box (titled bottom borders tolerated), a bare agent-glyph row with its wrapped input, opencode's left bar, or Pi's identity-corroborated separator pair - normally decides the verdict.
72
+ Real text in an identified shape is pending, while only positively proven emptiness reads empty.
73
+ A blank or otherwise unidentified cursor row is `unknown` and every consumer defers, except that a foreground process proven to be Cursor is re-read cursorlessly because Cursor parks its terminal cursor below its footer.
74
+ That identity-gated exception preserves the strict container-proof rule for every other pane, so a modal dialog, a dead shell between stale rules, or a mid-redraw pane is never an injection target.
75
+ The shared classifier accepts a shell glyph as an empty agent composer only inside a bordered container.
76
+ A bare shell prompt is `unknown`, so away-mode escalation is never injected into a dead shell.
77
+
78
+ Busy state is not read from rendered text on this backend.
79
+ A task's busy, idle, unknown, or dead verdict comes from the semantic busy-state contract owned by `bin/fm-busy-lib.sh`; [architecture](architecture.md#busy-state-is-semantic-per-adapter) owns its boundaries.
80
+ The one remaining rendered-tail reader is Grok's isolated fallback inside that contract, which can only classify a Grok task.
81
+ The submit acknowledgement and away-mode supervisor-pane busy guard below still consult rendered output, but only to decide whether input can be delivered, never to decide recorded task state.
82
+ The supervisor guard selects only the detected primary harness's signature rather than a global union of vendor patterns.
83
+
84
+ `bin/fm-tmux-lib.sh` owns exact type-and-submit mechanics.
85
+ It types a message once and retries Enter only until the composer clears.
86
+ Only a proven empty composer is a positive delivery acknowledgement.
87
+ Text left in established structure remains `pending`, text in ambiguous structure remains unproven, and unreadable or unsafe state remains unknown.
88
+ An ordinary local `fm-send.sh` text steer and every remote text steer no longer ride this verified submit at all: they become durable steering-inbox records plus best-effort constant doorbell lines (`bin/fm-task-inbox-lib.sh`).
89
+ The verdicts above are delivery-critical only for the local typed plane - harness-native invocations and explicit backend targets - where `fm-send.sh` still never retypes or assumes a confirmed submit for an unconfirmed verdict; its header owns the distinct delivered-unconfirmed exit status and operator response.
90
+
91
+ OpenCode 1.18.4 has one busy-queue exception.
92
+ While OpenCode is mid-turn, Enter queues the message but leaves its text visible until the turn completes.
93
+ After the normal retry budget, only structurally proven pending text in a provably busy pane is accepted as queued, while an idle pane remains `pending` as a genuine swallowed Enter.
94
+ Ambiguous pending text never receives the busy-queue conversion.
95
+ A second, baseline-gated conversion covers harnesses whose mid-turn screen the classifier cannot identify (Pi replaces its separated composer while working): when and only when the pane was idle before the text was typed, an idle-to-busy transition across the submit's own Enter confirms delivery, the same turn-started signal Herdr reads natively.
96
+ Without that baseline, an `unknown` verdict is preserved untouched, so a busy-looking pane can never convert an unread composer into a confirmation.
97
+ `tests/fm-tmux-submit-busy.test.sh` covers busy and idle panes with proven, ambiguous, and cleared composers.
98
+
99
+ ## Limits and regression entry points
100
+
101
+ - tmux is the reference path and supports secondmate homes.
102
+
103
+ ```sh
104
+ tests/fm-backend-tmux-smoke.test.sh
105
+ tests/fm-tmux-agent-liveness.test.sh
106
+ tests/fm-harness-liveness-drift-live-e2e.test.sh
107
+ tests/fm-composer-ghost.test.sh
108
+ tests/fm-kimi-harness.test.sh
109
+ tests/fm-cursor-harness.test.sh
110
+ tests/fm-muse-harness.test.sh
111
+ tests/fm-tmux-submit-busy.test.sh
112
+ tests/fm-bootstrap.test.sh
113
+ ```
114
+
115
+ [`verification/runtime-backends.md`](verification/runtime-backends.md#tmux) records the active foreground-process and submit evidence.
@@ -0,0 +1,117 @@
1
+ # Native W3C trace-context propagation
2
+
3
+ Firstmate can propagate a W3C [`traceparent`](https://www.w3.org/TR/trace-context/) to every agent it spawns so an external observer can identify each task as exactly one trace and correlate everything that task runs under that one identity.
4
+ The trace boundary is the task: a persistent Secondmate is routing infrastructure with its own agent identity, never a shared trace root for the unrelated tasks routed through it.
5
+ The capability is default-off, source-owned, vendor-neutral, and deliberately narrow.
6
+ This document is the rationale and current-behavior guide; `docs/configuration.md` owns the configuration schema, `bin/fm-trace-context-lib.sh`'s header owns the exact mechanics, and [`verification/trace-context.md`](verification/trace-context.md) records the repeatable test evidence.
7
+
8
+ ## Why this is a source change at all
9
+
10
+ Firstmate's durable operational artifacts already let a downstream observer derive logical task identity and lifecycle.
11
+ The source capability an observer cannot reconstruct after launch is a task-scoped trace id delivered in the agent's environment before launch and recorded under the same identity in task metadata.
12
+ This feature adds only that carrier seam.
13
+
14
+ ## What it does
15
+
16
+ When enabled, for each spawn Firstmate resolves one W3C `traceparent` carrier for the task - minted as a fresh root on the task's first spawn and reused verbatim from the meta on relaunch - and:
17
+
18
+ - forms it as `00-<32 hex trace id>-<16 hex span id>-<2 hex flags>`, with random ids for a new root;
19
+ - injects it into the agent's pane shell as the `TRACEPARENT` environment variable immediately before launch, through the same `spawn_send_text_line` channel that already ships `GOTMPDIR`; and
20
+ - records the identical value as `traceparent=` in `state/<id>.meta`.
21
+
22
+ `TRACEPARENT` as an environment variable is a Firstmate convention carrying a W3C-formatted value: W3C Trace Context standardizes the `traceparent` HTTP header, not an env var, and OpenTelemetry SDKs do not read it from the environment automatically, so a downstream observer must explicitly read this env value or the `traceparent=` meta field.
23
+ This feature parents no SDK span by itself.
24
+
25
+ Because the injected carrier and the recorded carrier are the same string, an observer that reads the metadata reconstructs exactly the identity the child received.
26
+ The injection sits at the unconditional pre-launch export site, so it covers ship and scout spawns across `claude`, `codex`, `opencode`, `pi`, `pi-signed`, `grok`, `kimi`, `cursor`, and `muse`, plus Secondmate spawns across that same set except the deliberately crewmate-only `muse` adapter.
27
+ This is the same coverage `GOTMPDIR` already has and requires no trace-specific `launch_template()` behavior.
28
+ Ship and scout spawns reach that site on every spawn backend (`tmux`, `herdr`, `zellij`, `orca`, `cmux`); a Secondmate reaches it on every backend that accepts a Secondmate spawn (`tmux`, `herdr`, `zellij`), because `bin/fm-spawn.sh` rejects a Secondmate on `orca` and `cmux`.
29
+
30
+ ### Remote Secondmate routes
31
+
32
+ A Secondmate on a [remote route](remote-secondmates.md) never reaches that export site in the parent's own process: the parent hands the launch to the configured host, which runs its own `bin/fm-spawn.sh` there.
33
+ The identity is still the parent's, because the parent home holds the task metadata an observer reads.
34
+ The parent therefore resolves the carrier against that task's own metadata under its own frozen decision - reused verbatim on relaunch, freshly rooted otherwise, never adopting the parent process's ambient `TRACEPARENT` - and passes it to the remote host, which exports it at the same unconditional pre-launch site and returns the carrier its endpoint actually holds.
35
+ The parent records that returned value, so an already-alive remote endpoint that was not relaunched reports the identity its agent really received rather than one the parent merely intended.
36
+ The remote host validates the delivered carrier as a strict W3C value before it can reach any pane, and a disabled parent passes nothing, leaving the remote launch identical to the untraced one.
37
+ If the endpoint is already alive, no new launch or injection occurs; the parent still records any carrier that endpoint reports, even when the parent's current decision is `off`, so its metadata does not deny the running agent's actual identity.
38
+ The enablement decision travels with it exactly as on the local path: the remote home inherits `config/trace-context` as declared inherited material and the new Secondmate process receives the parent's frozen `FM_TRACE_CONTEXT=on|off` snapshot.
39
+
40
+ ## Root and recovery semantics
41
+
42
+ The point of these rules is one trace per task: never merge unrelated tasks, and never mint a second identity for the same task.
43
+
44
+ - **Root** - a spawn whose task meta holds no valid recorded carrier mints a fresh trace id, a fresh span id, and sampled flags (`01`).
45
+ This begins a new trace, one per task.
46
+ The spawning process's own ambient `TRACEPARENT` is never adopted: that value is the agent identity the process itself received at its launch, and a persistent Secondmate keeps it for its whole life while unrelated requests are routed through it.
47
+ Adopting it would chain every routed task into one ever-growing trace per Secondmate; instead each routed task roots its own trace.
48
+ - **Recovery** - a valid `traceparent=` already recorded in the task's meta is reused verbatim, so a relaunched or recovered task keeps one stable identity across restarts rather than starting a second trace.
49
+ A corrupt recorded value is re-minted as a fresh root rather than propagated.
50
+
51
+ Because ambient `TRACEPARENT` is never read, the environment a supervisor happens to run under - a Secondmate's launch-time carrier, or an operator shell with a leftover `TRACEPARENT` - cannot leak into new task identities.
52
+ Disabling propagation is an intentional trace boundary: a disabled home injects no carrier into a newly launched or relaunched agent even when the task meta already contains a valid `traceparent=`.
53
+ An actual disabled relaunch regenerates the task meta without `traceparent=`, so a later enabled relaunch roots a new trace instead of resuming the identity from before the boundary; reusing an already-alive remote endpoint is not a relaunch and preserves the carrier that agent already holds.
54
+
55
+ ### Enablement is home-session-scoped
56
+
57
+ Each locked `bin/fm-session-start.sh` run resolves that home's `config/trace-context` plus `FM_TRACE_CONTEXT` exactly once into session-scoped effective state.
58
+ The decision is atomically published through a same-directory temporary file and bound to the current session lock, so a failed publication cannot reactivate a stale `on` record from an earlier session.
59
+ Every spawn from that home reads only the frozen `on` or `off` decision.
60
+ Later config or environment edits are ignored until that home starts a new session.
61
+ Missing, stale, unreadable, invalid, or unsuccessfully published effective state defaults safely to `off`.
62
+
63
+ When the primary launches a Secondmate, local or remote, it propagates `config/trace-context` into the Secondmate home and passes the primary session's frozen decision as a non-empty `FM_TRACE_CONTEXT=on|off` launch override.
64
+ The Secondmate resolves that inherited override when its own home session starts.
65
+ That flag is session-scoped enablement rather than durable configuration, so it is transferred at the launch convergence point - where the frozen decision is handed over with it - and left untouched by live convergence into an already-running home, on local and remote routes alike.
66
+ What propagates is the enablement decision, never trace identity: a Secondmate launched while enabled receives its own task carrier from the primary - the Secondmate agent's identity, reused verbatim when the Secondmate itself is relaunched - and each worker it spawns roots its own per-task trace.
67
+ A Secondmate launched while disabled keeps its workers untraced even if `config/trace-context` is present in its home.
68
+ When enabled, a relaunch reuses the task's valid recorded carrier; a task without one roots a fresh trace.
69
+ A duplicate Secondmate launch is refused before trace-context inheritance, so duplicate-launch preflight does not mutate the Secondmate home.
70
+
71
+ Changing the setting across the whole fleet requires a manual full fleet restart so every home starts a new session and freezes the new decision.
72
+ Firstmate does not monitor setting drift, detect mismatches, refuse launches, or automatically stop or restart any home.
73
+
74
+ ## Sampling
75
+
76
+ A new root sets the W3C trace flags to `01` (sampled).
77
+ This is a deliberate, source-owned choice:
78
+
79
+ - The capability is **opt-in** and default-off, so a home that enables it is asking for its spawns to be traced; an unsampled (`00`) root would produce a trace id that most downstream parent-based samplers drop, yielding nothing for the operator who opted in.
80
+ - **A recorded carrier keeps its flags verbatim.**
81
+ Recovery reuses the task's recorded carrier byte-for-byte, flags included, so a task's sampling decision is stable across restarts.
82
+ Firstmate chooses the flag only when it mints a *root*, which is the only way a new carrier is created.
83
+ - **Cost and privacy consequence.**
84
+ `01` records a sampling *decision*, and a conforming downstream parent-based sampler will honor it - but it does not by itself guarantee that any collector stores a span, and Firstmate emits no spans of its own; it only sets the flag on the carrier.
85
+ An operator who enables the capability and points sampling-respecting instrumentation at it should expect on the order of one trace per task to be recorded, at whatever cardinality and retention that instrumentation is configured for.
86
+ An operator who wants unsampled roots or head-sampling owns that downstream or via a later, explicitly-scoped option; Firstmate does not embed a sampler.
87
+
88
+ ## Safety
89
+
90
+ - **Default-off.**
91
+ With no `config/trace-context` and no `FM_TRACE_CONTEXT`, a fresh spawn or actual relaunch injects nothing and writes no `traceparent=` line, so the generated meta and the launch environment are unchanged.
92
+ Reusing an already-alive remote endpoint records any carrier that endpoint reports without injecting a new one.
93
+ A locked session start makes the one config-file check, and each spawn sources one extra library and reads the frozen effective-state file, so the process is not literally byte-for-byte identical, but nothing an agent, an observer, or the task meta can see differs.
94
+ - **What is and is not exposed.**
95
+ A Firstmate-*minted* root uses a random id and reads no prompt, path, task prose, credential, or arbitrary environment key, so Firstmate never *originates* sensitive data in the carrier.
96
+ Every carrier Firstmate injects is either such a mint or the same task's previously recorded carrier reused verbatim; ambient `TRACEPARENT` is never read, so no caller-controlled bytes enter a new carrier.
97
+ Exposure is bounded to that fixed-width carrier - it cannot carry a `tracestate`, an `OTEL_*` credential variable, or any arbitrary environment key, and there is no configurable or arbitrary command (only the fixed local `od`/`tr` for entropy).
98
+ - **Fail-independent.**
99
+ Minting is a small local entropy pipeline: it reads a few bytes from `/dev/urandom` through the fixed local `od` and `tr` (resolved from PATH).
100
+ There is no configured provider command, no network, and no watchdog.
101
+ The normal cost is small, but `od`/`tr` are external processes, so there is no hard latency guarantee - this is not a guaranteed-negligible bound.
102
+ Any entropy or self-validation failure that returns omits the carrier for that spawn without aborting source work; a corrupt recorded carrier is re-minted as a fresh root rather than propagated (it is not an omission).
103
+ If the pre-launch carrier export fails, Firstmate omits the `traceparent=` metadata claim and still launches the task.
104
+ If the backend reports that failed trace input could not be cleared, Firstmate refuses to append the launch command rather than risk launching with an unknown partial carrier.
105
+ If recording the carrier fails after export, Firstmate unsets `TRACEPARENT` in the launch command and still launches the task, so the child never receives an identity absent from its metadata.
106
+ - **Metadata-only.**
107
+ The value lives in the ephemeral pane shell and in `state/<id>.meta`; teardown removes state as before, so there is no new durable surface and no schema migration.
108
+
109
+ ## Relationship to OpenTelemetry and later increments
110
+
111
+ Firstmate learns nothing about OpenTelemetry, any exporter, collector, storage, or UI.
112
+ It emits a standard W3C carrier and records the same identity; a downstream observer owns everything else and discovers active propagation from the home session's frozen decision or the `traceparent=` field.
113
+ Native lifecycle-event emission, extra stable IDs, intake metadata, and any embedded OTLP are deliberately deferred until a running observer demonstrates a concrete fidelity gap that the derived artifacts cannot cover.
114
+
115
+ ## Verification
116
+
117
+ Repeatable test evidence - the unit and spawn-path suites with exact commands and output - lives in [`verification/trace-context.md`](verification/trace-context.md).
@@ -0,0 +1,170 @@
1
+ # Primary turn-end supervision guard
2
+
3
+ This is the authoritative current contract for the "no turn ends blind" primary backstop referenced from AGENTS.md section 8.
4
+ The predicate lives in `bin/fm-turnend-guard.sh`.
5
+ Primary scope lives in `bin/fm-primary-scope-lib.sh`, shared with the native session-start adapters in [`sessionstart-nudge.md`](sessionstart-nudge.md).
6
+ Harness hook files adapt each enabled primary harness integration's turn-end mechanism to that shared predicate.
7
+
8
+ Related PreToolUse guards deny unsafe commands before execution rather than detecting a blind turn end afterward.
9
+ Their separate owners are [`arm-pretool-check.md`](arm-pretool-check.md), [`cd-guard.md`](cd-guard.md), and [`subagent-guard.md`](subagent-guard.md).
10
+ Do not infer this guard's scope, loop safety, or compatibility tradeoffs for those guards.
11
+
12
+ ## Current invariant
13
+
14
+ `bin/fm-guard.sh` is a pull-based warning that runs only when another supervision command invokes it.
15
+ The turn-end guard closes the remaining gap at the primary's own turn boundary.
16
+ When work, a process-event source, or Relay polling needs supervision at that boundary and no identity-matched watcher has a fresh beacon, the harness integration must either block the turn end or force one bounded follow-up that uses the recovery instruction from the emitted session-start protocol.
17
+ The mid-turn pull warning uses the model-aware supervision verdict described below, while the turn-end guard keeps the PID-strict watcher predicate.
18
+ The guard remains a backstop; [`watcher-continuity.md`](watcher-continuity.md) owns normal continuity.
19
+
20
+ ## Guard predicates
21
+
22
+ The guard first calls the shared primary scope.
23
+ A secondmate home runs its own primary Firstmate session, so a genuine `.fm-secondmate-home` marker includes it whether the home is a linked worktree or plain clone.
24
+ The marker must be a regular non-symlink file whose whitespace-stripped first line is a non-empty identifier containing only letters, digits, dots, underscores, and dashes.
25
+ An unmarked checkout or invalid marker falls through to the git-dir check.
26
+ That check keeps crewmate and scout linked worktrees inert because their git dir differs from their git common dir.
27
+ It also requires `AGENTS.md`, `bin/`, and the effective state directory.
28
+
29
+ For an in-scope primary, the guard counts in-flight work from `state/*.meta`.
30
+ Registered `state/procevent/*.source` records also require supervision even though they have no task metadata.
31
+ The default cross-harness mode exits silently with no supervision need.
32
+ Every mode treats `state/x-watch.check.sh` as supervision need, so Relay polling remains guarded without an in-flight task.
33
+ Otherwise it calls `fm_watcher_healthy <state-dir> <watch-path> [grace-seconds] [home]` from `bin/fm-wake-lib.sh`, the same PID-strict identity-matched lock and fresh-beacon check used by `bin/fm-watch-arm.sh`: a stale beacon blocks even when a watcher pid is live, and a fresh leftover beacon blocks when the lock is missing, dead, or identity-mismatched.
34
+ The turn-end guard needs that strict check because it fires at the turn boundary, where the auto-arm is bringing a fresh watcher up for the upcoming idle period, and it cooperates with that arm rather than trusting a beacon left by the cycle that just ended.
35
+ `bin/fm-guard.sh`, the pull warning, instead uses the model-aware `fm_watcher_supervision_verdict` from the same library, because it fires mid-turn when the auto-arm model runs no watcher at all.
36
+ Under the Claude Stop auto-arm model a beacon fresh within grace is healthy even with no live watcher process, and only a beacon stale beyond grace (or absent) alarms.
37
+ Under the Pi extension model a live identity-matched watcher is the ordinary healthy state, but a genuinely unheld lock with a beacon fresh within grace is also healthy while a live Pi session provably owns continuity, because `.pi/extensions/fm-primary-pi-watch.ts` tears the watcher down on every actionable wake and spawns the replacement itself.
38
+ A lock is genuinely unheld only when the lock directory or its symlinked owner directory is absent, or when the existing lock records no pid at all.
39
+ Any lock with a recorded pid remains down when its pid, home, watcher path, or process identity fails the strict watcher health check.
40
+ That ownership proof is `fm_pi_extension_owns_supervision` in `bin/fm-wake-lib.sh`: both Pi primary extensions must be recorded in their state markers at their current on-disk builds by the process named in `state/.lock`, and that process must still be alive.
41
+ Requiring the turn-end guard extension as well as the watch extension is deliberate, because a home without that structural backstop has no benign hand-off to tolerate.
42
+ Without that proof an unheld lock alarms exactly as it did before, so an unloaded, version-drifted, or exited Pi session is loud immediately, and a cycle the extension never restores is loud once the beacon passes grace.
43
+ Under every persistent-watcher harness a live identity-matched watcher with a fresh beacon is still required, so the pull guard keeps the same strict semantics there.
44
+ Its banner names the true failing condition, either a missing live watcher process or a genuinely stale beacon with its real age, and keys the once-per-episode dedup on that condition rather than the beacon mtime.
45
+
46
+ `FM_STATE_OVERRIDE` wins over `FM_HOME/state`, and `FM_HOME` wins over repository-root `state/`.
47
+ `FM_GUARD_GRACE` controls beacon freshness and defaults to 300 seconds.
48
+ If `jq` is missing or hook stdin is empty, the guard exits 0 because it cannot safely read loop-guard fields.
49
+
50
+ ## Harness integrations
51
+
52
+ - Claude registers two `Stop` hooks in `.claude/settings.json`, both anchored through `CLAUDE_PROJECT_DIR`: `bin/fm-turnend-guard.sh --claude`, and `bin/fm-claude-stop-autoarm.sh` with `asyncRewake: true` and `timeout: 28800`.
53
+ - Codex registers a `Stop` hook in `.codex/hooks.json`, anchors the executable to the hook process working directory, verifies a Firstmate-shaped hook-bearing root, and passes the original payload to the shared guard.
54
+ - OpenCode listens for `session.idle` in `.opencode/plugins/fm-primary-turnend-guard.js`, lets the watcher coordinator act first, and calls `client.session.promptAsync` once when the guard returns 2.
55
+ - Pi listens for `agent_settled` in `.pi/extensions/fm-primary-turnend-guard.ts`, runs once per logical agent run, and calls `pi.sendUserMessage(..., { deliverAs: "followUp" })` once when the guard returns 2.
56
+ - Cursor registers a `stop` hook in `.cursor/hooks.json` and delegates the whole turn boundary to `bin/fm-turnend-guard-cursor.sh`, the park described below.
57
+ Cursor also loads `<project>/.claude/settings.json`, so every tracked Claude-shaped entrypoint whose event Cursor covers stands down on a Cursor-delivered payload through `bin/fm-hook-host-lib.sh`.
58
+ That predicate reads the delivered payload's own `cursor_version`, never the environment: Cursor exports `CURSOR_INVOKED_AS`, `CURSOR_PROJECT_DIR`, and `CURSOR_VERSION` into every child process, so an environment guard would also disable the hooks of a Claude session started by hand from a Cursor pane, which is the hazard the `GROK_SESSION_ID` exclusion below records.
59
+ The guarded set is the `SessionStart` entry, the two `PreToolUse` Bash entries, and both `Stop` entries.
60
+ Cursor 2026.08.11-e8db854 does not fire the Claude-shaped `Stop` entry at all, but it is guarded anyway because Cursor has no `asyncRewake`: if a later build did fire it, `bin/fm-claude-stop-autoarm.sh` would run synchronously inside Cursor's stop step and hold that turn open for its declared multi-hour timeout, exactly the wedge grok 1.0.0 produced.
61
+ - Grok registers a `Stop` hook in `.grok/hooks/fm-primary-turnend-guard.json` and delegates capability selection to `bin/fm-turnend-guard-grok.sh`.
62
+ The tracked Claude Stop entries are inert when `GROK_AGENT` or `GROK_HOOK_EVENT` is present, so Grok's Claude-compatible settings loading cannot create a second continuation path.
63
+ Both markers are required because Grok does not inject the same variables into every process kind: grok 0.2.73 set `GROK_AGENT` for child and tool processes, while grok 1.0.0 hook processes carry `GROK_HOOK_EVENT`, `GROK_HOOK_NAME`, `GROK_SESSION_ID`, and `GROK_WORKSPACE_ROOT` but no `GROK_AGENT`.
64
+ A guard keyed on `GROK_AGENT` alone therefore stopped firing on grok 1.0.0, and the resulting Claude-only auto-arm ran synchronously under Grok - Grok has no `asyncRewake`, so it waited on the foregrounded watcher for the declared 28800-second timeout and the Grok turn never ended.
65
+ Do NOT widen this guard to `GROK_SESSION_ID`: Grok injects that into every child process, so it can survive into a Claude session that Grok launched and would silently disable Claude's own continuity.
66
+ The same marker guard carries every tracked `.claude/settings.json` entry whose event Grok already covers through its own `.grok/hooks/` registration, which is both `Stop` entries, the `SessionStart` entry, and the two `PreToolUse` Bash entries; `bin/fm-subagent-pretool-check.sh` is the one deliberate unguarded exception because no Grok registration covers the subagent-spawn event, recorded in [`subagent-guard.md`](subagent-guard.md) "Known residual gap".
67
+ `tests/fm-turnend-guard.test.sh` pins that inventory so neither the guarded set nor the exception can change silently.
68
+
69
+ Claude and Codex can block a Stop directly with exit status 2 and stderr.
70
+ Both payloads carry `stop_hook_active`.
71
+ In the default Codex mode, a true value lets the second stop finish after one forced continuation.
72
+
73
+ Claude runs the guard with `--claude`, which ignores `stop_hook_active` and cooperates with the Stop-owned auto-arm.
74
+ Claude Code sets `stop_hook_active=true` on every stop after any stop-hook continuation, including `asyncRewake` rewakes, which re-opened the 2026-07-21 blind window under the default one-shot behavior.
75
+ The Claude mode waits up to `FM_CLAUDE_AUTOARM_SYNC_WAIT_MS` (default 800 milliseconds) and allows the stop when the watcher is healthy, the auto-arm's generation claim is open, or `state/.claude-autoarm-epoch` contains a fresh actionable rewake owned by this event epoch.
76
+ The claim is the ledger entry itself: the epoch sequence in `state/.claude-autoarm-epoch` is a monotonic claim generation, line 1 is the classic epoch record, and line 2 records the claiming process's mandatory pid-identity (`fm_autoarm_claim_open` and `fm_autoarm_claim_next` in `bin/fm-wake-lib.sh` own the contract).
77
+ A claim is open while its outcome is `arming`, its owner pid is alive, its recorded identity successfully recomputes and matches that pid, and it is not stuck - stuck meaning the entry and the watcher beacon are both older than the guard grace, which proves the owner hung mid-arm (a healthy hours-long foregrounded cycle keeps the beacon beating, and every arming phase with no watcher is bounded in seconds).
78
+ Anything else - a finished outcome, a dead or identity-mismatched owner, a stuck owner, an identityless entry, or no entry - lets the next Stop-owned firing take the next generation and arm; taking a newer generation is the reclaim, and a steady-state predecessor is never signalled or revoked.
79
+ No mutex is held across arming or output: `state/.claude-autoarm.lock` survives only as a micro-mutex serializing individual ledger writes, and a superseded owner goes completely silent - ownership is re-verified before every arm invocation, episode-state mutation, ledger write, and continuation.
80
+ The irrevocable commit point of a translation is the exit status, because the harness delivers the collected stderr banner only on exit 2, so an owned terminal commit decides the exit: markerless outcomes commit with the ledger write, while the once-per-episode failure notice commits only when its marker is created after the winning failed write in the same critical section.
81
+ A generation whose required marker cannot be created is refused and exits 0 silently even after printing; its terminal ledger entry is superseded by a later firing, which retries the notice.
82
+ Without those boundaries a cycle that armed, delivered one rewake, and exited left both Stop participants deferring to its leftover lock indefinitely (2026-08-14: two tasks in flight, a beacon 40 minutes cold, every turn blind until an operator intervened), and a hook that hung mid-arm kept a live pid on the lock so the watcher was never auto-re-armed again (2026-08-26).
83
+ Two bounded residuals are accepted intent, each costing at most one extra continuation turn absorbed by the durable idempotent wake queue: an owner that dies between its owned terminal write and its own process exit, and a hung old-build owner that resumes during the one legacy upgrade window.
84
+ A legacy build's lock-holding claim (recognizable by its `autoarm` role file) still defers or reclaims under the legacy abandonment proof, with a live identity-verified stuck owner retired via TERM before its lock is removed and an unverified pid never signalled, so an upgrade mid-session can neither double-arm nor deadlock, and a failed reclaim re-blocks rather than allowing a blind stop.
85
+ Fresh `failed` and `failed-suppressed` outcomes enter or advance the failure progression instead of acting as unconditional recovery proof.
86
+ The auto-arm itself rechecks the healthy watcher predicate and retries a bounded number of times before reporting a genuine failure.
87
+ The first fresh exhausted-failure epoch preserves its handoff without consuming a blocked-stop count, while later fresh failed epochs advance the same monotonic progression instead of resetting it.
88
+ When none of those proofs appears, it re-blocks up to `FM_CLAUDE_TURNEND_BLOCK_BUDGET` times (default 3, below Claude's 8-block override).
89
+ In Claude mode, positive watcher recovery clears the block budget, failure notice, and attended alarm together under the existing budget lock before either hook reports ordinary recovery.
90
+ The one loud attended fail-open is available only when the auto-arm has recorded an exhausted failure, its one notice is already consumed, the block budget is exhausted, and a final check finds neither a healthy watcher nor an automatic continuation.
91
+ Each epoch identity is accounted at most once under the budget lock.
92
+ Whenever both coordination locks are needed, positive auto-arm recovery and the terminal check acquire the auto-arm owner lock before the budget lock.
93
+ After that alarm, the Stop auto-arm suppresses further exit-2 continuations until positive watcher recovery, so the final fail-open remains reachable.
94
+ The alarm cannot repeat during that failure episode, and a later unhealthy stop blocks again.
95
+ A positively verified healthy watcher clears the failure notice, alarm, and block budget for a future independent episode.
96
+ A Claude failure notice describes the automatic mechanism as broken and does not direct a routine manual background arm.
97
+
98
+ OpenCode, Pi, and pi-signed expose passive callbacks for this purpose.
99
+ Their adapters fail open at the hook boundary to protect the user session but schedule one bounded follow-up when the predicate blocks.
100
+ The generated prompts use the canonical `turn-end-guard` kind after the U+2063 `FIRSTMATE_OP: ` prefix, so Ahoy does not treat them as captain messages.
101
+ Each passive adapter owns a loop latch.
102
+ Pi keeps the latch across internal tool turns and clears it only when the generated follow-up settles or delivery fails.
103
+ OpenCode's forced follow-up is supported for persistent TUI sessions and remains fail-open in headless `opencode run`.
104
+
105
+ Grok makes exactly one typed capability decision from each running Stop payload.
106
+ A boolean `stopHookActive` selects native blocking, including both false on the initial stop and true on the bounded continuation.
107
+ The camel-case field has precedence when both spellings appear; when it is absent, a boolean `stop_hook_active` selects the same native path for compatibility.
108
+ The native path returns the shared guard's status and stderr to the same Grok process and never starts `grok --resume`.
109
+ When both capability spellings are absent, the adapter preserves one pre-native `grok --resume` fallback guarded by `GROK_TURNEND_GUARD_ACTIVE` and intentionally omits `--permission-mode`.
110
+ Malformed JSON, a selected field with a non-boolean type, missing `jq`, missing hook prerequisites, or an already-active legacy guard allows the stop without starting either continuation path.
111
+ Grok's project hook requires the checkout to be trusted with `/hooks-trust` or launch-time `--trust`; genuine pre-native builds can run the same tracked hook from an isolated global hook directory.
112
+
113
+ Cursor cannot block a turn end at all: its blocked-response mapper returns an empty object for the `stop` step, so exit 2 is a silent no-op, verified both statically and live.
114
+ `bin/fm-turnend-guard-cursor.sh` therefore never exits 2 and never writes a banner expecting it to be read; every path exits 0 and its only channel is at most one `followup_message` on stdout.
115
+ Cursor runs that hook synchronously and awaits it, so one script owns both halves of the boundary.
116
+ While supervision is needed it PARKS: it runs `bin/fm-watch-arm.sh` as its own tracked child, holds the boundary open until the watcher closes, and returns an actionable close as one `watcher`-kind follow-up, spending no model tokens while parked.
117
+ This is the same between-turns shape as Claude's Stop auto-arm, so `fm_supervision_model` classifies Cursor as `autoarm` and the mid-turn pull guard accepts a fresh beacon without a live watcher.
118
+ The park stands down without arming when `PI_CODING_AGENT=true` and neither `CURSOR_AGENT` nor `CURSOR_INVOKED_AS` is set.
119
+ Pi-with-Cursor-provider sessions (pi-cursor-sdk) load project `.cursor/hooks.json` into the Pi process, and a Cursor park there would race Pi's extension-owned `fm_watch_arm_pi` continuity, resurface rearm wakes, and abort in-flight asks.
120
+ `fm-spawn`'s cursor launch clears `PI_CODING_AGENT`; a hand-started cursor-agent may still inherit it.
121
+ When either Cursor identity marker is present, the park still runs despite a leaked `PI_CODING_AGENT`.
122
+ When the park cannot establish a cycle it asks this shared guard with `--cursor` and renders a returned exit 2 as one bounded `turn-end-guard` follow-up, capped by `FM_CURSOR_TURNEND_BLOCK_BUDGET` (default 3) consecutive unproductive nags per session; a delivered wake resets that budget because it is productive work.
123
+ The follow-up loop is bounded TWICE, because either bound alone is insufficient.
124
+ `loop_limit` in `.cursor/hooks.json` is Cursor's own ceiling and the only one that still holds if the adapter is broken or replaced: once `loop_count` reaches it Cursor stops invoking the hook, verified live.
125
+ `FM_CURSOR_TURNEND_LOOP_CEILING` (default 180) bounds the payload's `loop_count` from inside and sits deliberately BELOW the registered `loop_limit`, so firstmate's bound bites first and emits one final loud notice instead of supervision going silently dark at Cursor's ceiling.
126
+ `loop_count` is Cursor's richer analogue of `stop_hook_active`: verified live as 0 on the first stop after a real user message, +1 per follow-up-driven stop, and reset to 0 by the next real user message.
127
+
128
+ A captain message typed while the hook is parked is accepted and runs its turn immediately, and Cursor does NOT terminate the parked hook.
129
+ The older park remains the recorded owner until that captain turn ends and the next `stop` hook claims the baton, so an actionable watcher close in that window can still be delivered by the older park as one follow-up.
130
+ That delivery is bounded and safe: only one park exists before the next `stop` claim, so it is a real wake and never a stale duplicate of another park's wake, while the durable wake queue makes handling idempotent.
131
+ Each invocation publishes its sequence in `state/.cursor-park-owner` under the short publication and commit lock `state/.cursor-park-owner.lock`.
132
+ The same bounded critical section covers the final owner and away-mode checks, follow-up output, and repair-budget commit, so the next `stop` claim makes an older park that is still running stand down without emitting or changing shared state.
133
+ The lock is never held while the arm is sleeping, while the hook is polling, or while output is prepared.
134
+ The park revalidates session ownership while polling and again inside the final commit section, but it deliberately does not hold the fleet session lock across output because an awaited hook must not block home-wide session acquisition; the remaining microsecond takeover window can produce at most one harmless wake that drains the durable queue.
135
+ Without those records an older park still running after the next `stop` could leak one process and one stale duplicate wake.
136
+ Cursor's `beforeSubmitPrompt` step fires once on a real captain message and does not fire for hook-driven follow-ups, so invalidating the park baton there would close the pre-claim window exactly.
137
+ That hook is deliberately left to a follow-up alongside the deferred `preCompact` surface and is not registered in this change.
138
+
139
+ If a passive adapter cannot invoke its SDK, or the Grok legacy fallback cannot find `grok` or a session id, the next pull-based `fm-guard.sh` call reports the problem.
140
+ That warning uses `bin/fm-supervision-instructions.sh --repair-line`, so it always points to the active harness protocol rather than embedding another repair command.
141
+
142
+ ## Compatibility limits
143
+
144
+ - Child crewmate and scout worktrees are outside scope.
145
+ - A valid secondmate home is in scope; an idle secondmate endpoint with no Relay poll remains healthy because it has no supervision need.
146
+ - The blocking and bounded-follow-up mechanisms are limited to the primary integrations listed above.
147
+ - OpenCode headless mode and untrusted Grok project hooks remain fail-open at the host boundary.
148
+ - Cursor's `stop` step does not fire in headless `cursor-agent -p`, the same class of limit as OpenCode headless; firstmate primaries run interactive.
149
+ - A Cursor primary must be launched with `--trust`, or its project hooks never load and the whole integration is inert.
150
+ - Cursor's `preCompact` step is deliberately unregistered: its response can return only `user_message` and it is absent from Cursor's `additional_context` step set, so a post-compaction re-emit needs its own design and is deferred to a follow-up ([`sessionstart-nudge.md`](sessionstart-nudge.md) owns that uncovered surface).
151
+ - Kimi Code CLI 0.29.1 exposes only global `[[hooks]]` configuration in `~/.kimi-code/config.toml`, including a `Stop` event with snake_case payload fields `hook_event_name`, `session_id`, `cwd`, and `stop_hook_active`.
152
+ - Kimi has no project-level hook configuration and remains outside the primary guard integrations above.
153
+ - Captain-approved Kimi crew wake support uses `bin/fm-kimi-turnend-hook.sh` to edit only one marker-delimited Firstmate region in that global config and install a silent always-zero hook.
154
+ - The hook remains inert unless the payload `cwd` contains a per-task token pointer that resolves through Firstmate's private registry to one `state/<id>.turn-ended` marker.
155
+ - Installation refuses before writing unless `python3` with `tomllib` and `jq` are available.
156
+ - If `jq` is removed after installation, the hook remains silent and exits 0, turn-end wakes stop, and Kimi crews fall back to idle detection.
157
+ - Unreadable hook input remains fail-open.
158
+ - No harness adapter uses a shell ampersand to manufacture supervision.
159
+
160
+ ## Regression coverage
161
+
162
+ `tests/fm-turnend-guard.test.sh` covers the predicate, main and secondmate primary scope, child-worktree exclusion, `FM_HOME` and `FM_STATE_OVERRIDE` precedence, the live-lock and fresh-beacon guard predicate, the cooperative `--claude` open-generation claim wait, monotonic failed-epoch progression, bounded attended fail-open, post-alarm continuation suppression, positive recovery reset, generation and legacy claim cases that must block or clear instead of allowing a blind stop, Pi logical-run latching, missing-`jq` behavior, all five primary registrations, Grok native and legacy selection, typed field precedence, malformed input, and exactly-one-path safety.
163
+ `tests/fm-guard-stale-banner.test.sh` covers the pull-guard predicate, including the persistent-model fresh-leftover-beacon negative control, the auto-arm model's healthy fresh-beacon-without-a-watcher case and stale-beacon alarm, and the extension model's live-watcher path, ownership-qualified fresh hand-off, held-lock failures, independently broken ownership signals, stale-beacon alarm, queued-wake warning, and Pi and pi-signed harness routing.
164
+ It also covers true-reason banner wording and reason-keyed episode dedup surviving a beacon mtime change.
165
+ `tests/fm-cursor-primary.test.sh` covers the Cursor park end to end over real processes with no harness installed: each tracked Claude-shaped entrypoint standing down on a Cursor payload, both follow-up sources, the bounded repair nag and its reset, the nested loop bounds, supersession, away-mode and lock-ownership inertness, Pi-host stand-down without Cursor identity and continued parking when `PI_CODING_AGENT` leaks alongside `CURSOR_AGENT` or `CURSOR_INVOKED_AS`, child-worktree exclusion, and that the adapter never exits 2.
166
+ `FM_CURSOR_PRIMARY_LIVE_E2E=1 tests/fm-cursor-primary-live-e2e.test.sh` is the opt-in guard that proves the same behavior against the installed cursor-agent and fails naming the harness and version.
167
+ `tests/fm-kimi-harness.test.sh` covers the separate Kimi crew hook's format preservation, idempotence, refusal cases, token guard, spawn registration, and teardown cleanup.
168
+ `tests/fm-supervision-instructions.test.sh` covers recovery-line ownership and pi-signed's identity-preserving reuse of Pi's protocol.
169
+ `FM_PI_LIVE_E2E=1 tests/fm-pi-primary-live-e2e.test.sh` is the opt-in isolated Pi path.
170
+ [`verification/supervision.md`](verification/supervision.md#turn-end-guard) records the active cross-harness empirical evidence, including the 2026-07-24 Claude `asyncRewake` revalidation.