@zerwiz/ymir 0.1.4 → 0.1.5

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 (850) hide show
  1. package/.agents/README.md +291 -0
  2. package/.agents/agents/bragi-marketer.md +111 -0
  3. package/.agents/agents/brokk.md +352 -0
  4. package/.agents/agents/forseti-reviewer.md +93 -0
  5. package/.agents/agents/frigg-consent.md +77 -0
  6. package/.agents/agents/groa-update.md +77 -0
  7. package/.agents/agents/hnoss-designer.md +69 -0
  8. package/.agents/agents/huginn-researcher.md +99 -0
  9. package/.agents/agents/jord-projects.md +78 -0
  10. package/.agents/agents/kvasir-scout.md +91 -0
  11. package/.agents/agents/mimir-planner.md +92 -0
  12. package/.agents/agents/muninn-stow.md +77 -0
  13. package/.agents/agents/saga-bearings.md +78 -0
  14. package/.agents/agents/sindri-developer.md +114 -0
  15. package/.agents/agents/snotra-documenter.md +105 -0
  16. package/.agents/agents/syn-recovery.md +78 -0
  17. package/.agents/agents/tyr-check.md +78 -0
  18. package/.agents/agents/urdh-hold.md +77 -0
  19. package/.agents/agents/volundr-smith.md +80 -0
  20. package/.agents/agents/vor-diagnostics.md +77 -0
  21. package/.agents/assets/agents/naming.md +50 -0
  22. package/.agents/assets/agents/registry.md +93 -0
  23. package/.agents/assets/agents/runtime.md +36 -0
  24. package/.agents/assets/agents/toon-tasks-cli.md +42 -0
  25. package/.agents/assets/templates/company_entity.template.md +37 -0
  26. package/.agents/backend/backends/cmux.sh +649 -0
  27. package/.agents/backend/backends/herdr-eventwait.py +157 -0
  28. package/.agents/backend/backends/herdr-workspace-move.py +114 -0
  29. package/.agents/backend/backends/herdr.sh +3386 -0
  30. package/.agents/backend/backends/orca.sh +290 -0
  31. package/.agents/backend/backends/tmux.sh +357 -0
  32. package/.agents/backend/backends/zellij.sh +700 -0
  33. package/.agents/backend/fm-afk-launch.sh +654 -0
  34. package/.agents/backend/fm-afk-return.sh +241 -0
  35. package/.agents/backend/fm-afk-start.sh +172 -0
  36. package/.agents/backend/fm-arm-command-policy.mjs +960 -0
  37. package/.agents/backend/fm-arm-pretool-check.sh +200 -0
  38. package/.agents/backend/fm-backend-hometag-lib.sh +52 -0
  39. package/.agents/backend/fm-backend.sh +981 -0
  40. package/.agents/backend/fm-backlog-handoff.sh +909 -0
  41. package/.agents/backend/fm-backlog-receive.sh +187 -0
  42. package/.agents/backend/fm-backlog-transition-lib.sh +779 -0
  43. package/.agents/backend/fm-bearings-board.sh +204 -0
  44. package/.agents/backend/fm-bearings-snapshot.sh +546 -0
  45. package/.agents/backend/fm-bootstrap.sh +1568 -0
  46. package/.agents/backend/fm-branch-outcome.sh +203 -0
  47. package/.agents/backend/fm-branch-prompt.sh +101 -0
  48. package/.agents/backend/fm-brief.sh +452 -0
  49. package/.agents/backend/fm-busy-event.sh +234 -0
  50. package/.agents/backend/fm-busy-lib.sh +986 -0
  51. package/.agents/backend/fm-captain-hold.sh +1001 -0
  52. package/.agents/backend/fm-cd-command-policy.mjs +153 -0
  53. package/.agents/backend/fm-cd-pretool-check.sh +190 -0
  54. package/.agents/backend/fm-check-lib.sh +74 -0
  55. package/.agents/backend/fm-check-register.sh +42 -0
  56. package/.agents/backend/fm-check-unregister.sh +52 -0
  57. package/.agents/backend/fm-classify-lib.sh +1770 -0
  58. package/.agents/backend/fm-claude-stop-autoarm.sh +334 -0
  59. package/.agents/backend/fm-composer-lib.sh +1416 -0
  60. package/.agents/backend/fm-config-inherit-lib.sh +1202 -0
  61. package/.agents/backend/fm-config-push.sh +254 -0
  62. package/.agents/backend/fm-control-lib.sh +251 -0
  63. package/.agents/backend/fm-control.sh +878 -0
  64. package/.agents/backend/fm-crew-state.sh +625 -0
  65. package/.agents/backend/fm-cursor-lib.sh +243 -0
  66. package/.agents/backend/fm-decision-hold.sh +232 -0
  67. package/.agents/backend/fm-doc-audience-check.sh +269 -0
  68. package/.agents/backend/fm-dod-lib.sh +67 -0
  69. package/.agents/backend/fm-ensure-agents-md.sh +253 -0
  70. package/.agents/backend/fm-extension-launch-barrier.mjs +129 -0
  71. package/.agents/backend/fm-extension.mjs +2577 -0
  72. package/.agents/backend/fm-extension.sh +16 -0
  73. package/.agents/backend/fm-ff-lib.sh +420 -0
  74. package/.agents/backend/fm-fleet-snapshot.sh +1463 -0
  75. package/.agents/backend/fm-fleet-sync.sh +458 -0
  76. package/.agents/backend/fm-fleet-view.sh +96 -0
  77. package/.agents/backend/fm-gate-refuse-lib.sh +102 -0
  78. package/.agents/backend/fm-guard.sh +242 -0
  79. package/.agents/backend/fm-harness.sh +195 -0
  80. package/.agents/backend/fm-herdr-ci-cleanup.sh +115 -0
  81. package/.agents/backend/fm-herdr-lab.sh +346 -0
  82. package/.agents/backend/fm-herdr-session-cleanup.sh +337 -0
  83. package/.agents/backend/fm-home-seed.sh +984 -0
  84. package/.agents/backend/fm-home-summary-refresh.sh +255 -0
  85. package/.agents/backend/fm-hook-host-lib.sh +36 -0
  86. package/.agents/backend/fm-inactive-reconcile.sh +531 -0
  87. package/.agents/backend/fm-inbox.sh +399 -0
  88. package/.agents/backend/fm-install-actionlint.sh +84 -0
  89. package/.agents/backend/fm-install-herdr.sh +96 -0
  90. package/.agents/backend/fm-install-shellcheck.sh +84 -0
  91. package/.agents/backend/fm-install-treehouse.sh +95 -0
  92. package/.agents/backend/fm-kimi-turnend-hook.sh +276 -0
  93. package/.agents/backend/fm-lease-lib.sh +218 -0
  94. package/.agents/backend/fm-lease.sh +189 -0
  95. package/.agents/backend/fm-line-cap-lib.sh +51 -0
  96. package/.agents/backend/fm-lint-workflows.sh +137 -0
  97. package/.agents/backend/fm-lint.sh +591 -0
  98. package/.agents/backend/fm-lock-lib.sh +104 -0
  99. package/.agents/backend/fm-lock.sh +107 -0
  100. package/.agents/backend/fm-marker-lib.sh +12 -0
  101. package/.agents/backend/fm-merge-local.sh +74 -0
  102. package/.agents/backend/fm-merge-outcome-lib.sh +138 -0
  103. package/.agents/backend/fm-nm-run-lib.sh +124 -0
  104. package/.agents/backend/fm-on.sh +125 -0
  105. package/.agents/backend/fm-operational-input.sh +253 -0
  106. package/.agents/backend/fm-peek.sh +46 -0
  107. package/.agents/backend/fm-pending-reply-lib.sh +1409 -0
  108. package/.agents/backend/fm-pr-check.sh +135 -0
  109. package/.agents/backend/fm-pr-lib.sh +1016 -0
  110. package/.agents/backend/fm-pr-merge.sh +707 -0
  111. package/.agents/backend/fm-pr-poll.sh +110 -0
  112. package/.agents/backend/fm-primary-scope-lib.sh +33 -0
  113. package/.agents/backend/fm-procevent-extension-capture.pl +259 -0
  114. package/.agents/backend/fm-procevent-lavish.sh +628 -0
  115. package/.agents/backend/fm-procevent-lib.sh +892 -0
  116. package/.agents/backend/fm-procevent-quota.sh +290 -0
  117. package/.agents/backend/fm-procevent-remote-reply.sh +577 -0
  118. package/.agents/backend/fm-procevent-when.sh +504 -0
  119. package/.agents/backend/fm-procevent.sh +1466 -0
  120. package/.agents/backend/fm-project-mode.sh +92 -0
  121. package/.agents/backend/fm-project-origin-lib.sh +180 -0
  122. package/.agents/backend/fm-promote.sh +282 -0
  123. package/.agents/backend/fm-public-followup-emit.sh +260 -0
  124. package/.agents/backend/fm-public-followup-lib.sh +398 -0
  125. package/.agents/backend/fm-public-followup.sh +1330 -0
  126. package/.agents/backend/fm-push-transition-lib.sh +168 -0
  127. package/.agents/backend/fm-quota-axi-lib.sh +93 -0
  128. package/.agents/backend/fm-quota-choose.sh +384 -0
  129. package/.agents/backend/fm-remote-delta-read.sh +189 -0
  130. package/.agents/backend/fm-remote-doctor.sh +800 -0
  131. package/.agents/backend/fm-remote-entrypoint.sh +193 -0
  132. package/.agents/backend/fm-remote-file.sh +234 -0
  133. package/.agents/backend/fm-remote-home-provision.sh +259 -0
  134. package/.agents/backend/fm-remote-home-seed.sh +259 -0
  135. package/.agents/backend/fm-remote-inherit-push.sh +90 -0
  136. package/.agents/backend/fm-remote-inherit.sh +179 -0
  137. package/.agents/backend/fm-remote-job-lib.sh +1241 -0
  138. package/.agents/backend/fm-remote-job-reap-orphans.sh +137 -0
  139. package/.agents/backend/fm-remote-job-worker.sh +1124 -0
  140. package/.agents/backend/fm-remote-readiness-lib.sh +44 -0
  141. package/.agents/backend/fm-remote-secondmate-control.sh +340 -0
  142. package/.agents/backend/fm-review-diff.sh +158 -0
  143. package/.agents/backend/fm-secondmate-charter-lib.sh +44 -0
  144. package/.agents/backend/fm-secondmate-nudge-lib.sh +68 -0
  145. package/.agents/backend/fm-secondmate-parent-lib.sh +70 -0
  146. package/.agents/backend/fm-secondmate-reconcile.sh +371 -0
  147. package/.agents/backend/fm-secondmate-registry-lib.sh +311 -0
  148. package/.agents/backend/fm-secondmate-report.sh +88 -0
  149. package/.agents/backend/fm-send.sh +1059 -0
  150. package/.agents/backend/fm-session-lock-lib.sh +176 -0
  151. package/.agents/backend/fm-session-start.sh +970 -0
  152. package/.agents/backend/fm-sessionstart-cursor.sh +40 -0
  153. package/.agents/backend/fm-sessionstart-nudge.sh +45 -0
  154. package/.agents/backend/fm-sessionstart-run.sh +145 -0
  155. package/.agents/backend/fm-spawn.sh +3153 -0
  156. package/.agents/backend/fm-startup-memory-budget-lib.sh +224 -0
  157. package/.agents/backend/fm-startup-memory-budget.sh +94 -0
  158. package/.agents/backend/fm-startup-network.sh +642 -0
  159. package/.agents/backend/fm-stow-cascade.sh +251 -0
  160. package/.agents/backend/fm-subagent-pretool-check.sh +207 -0
  161. package/.agents/backend/fm-supervise-daemon.sh +1732 -0
  162. package/.agents/backend/fm-supervision-instructions.sh +215 -0
  163. package/.agents/backend/fm-supervision-lib.sh +90 -0
  164. package/.agents/backend/fm-supervisor-target-lib.sh +78 -0
  165. package/.agents/backend/fm-tangle-lib.sh +53 -0
  166. package/.agents/backend/fm-task-inbox-lib.sh +402 -0
  167. package/.agents/backend/fm-tasks-axi-lib.sh +123 -0
  168. package/.agents/backend/fm-teardown.sh +2922 -0
  169. package/.agents/backend/fm-test-isolation-proof.sh +588 -0
  170. package/.agents/backend/fm-test-run.sh +2191 -0
  171. package/.agents/backend/fm-timeout-lib.sh +141 -0
  172. package/.agents/backend/fm-timing-lib.sh +179 -0
  173. package/.agents/backend/fm-tmux-lib.sh +291 -0
  174. package/.agents/backend/fm-tool-update-check.sh +898 -0
  175. package/.agents/backend/fm-trace-context-lib.sh +227 -0
  176. package/.agents/backend/fm-transition-lib.sh +103 -0
  177. package/.agents/backend/fm-turnend-guard-cursor.sh +391 -0
  178. package/.agents/backend/fm-turnend-guard-grok.sh +90 -0
  179. package/.agents/backend/fm-turnend-guard.sh +432 -0
  180. package/.agents/backend/fm-update.sh +110 -0
  181. package/.agents/backend/fm-vendor-auth-probe.sh +190 -0
  182. package/.agents/backend/fm-voice-client.py +1373 -0
  183. package/.agents/backend/fm-voice-relay.py +1256 -0
  184. package/.agents/backend/fm-wake-drain.sh +592 -0
  185. package/.agents/backend/fm-wake-grant.sh +127 -0
  186. package/.agents/backend/fm-wake-lib.sh +1849 -0
  187. package/.agents/backend/fm-watch-arm.sh +602 -0
  188. package/.agents/backend/fm-watch-checkpoint.sh +109 -0
  189. package/.agents/backend/fm-watch.sh +1962 -0
  190. package/.agents/backend/fm-x-dismiss.sh +116 -0
  191. package/.agents/backend/fm-x-followup.sh +286 -0
  192. package/.agents/backend/fm-x-lib.sh +1000 -0
  193. package/.agents/backend/fm-x-link.sh +234 -0
  194. package/.agents/backend/fm-x-poll.sh +185 -0
  195. package/.agents/backend/fm-x-reply.sh +385 -0
  196. package/.agents/backend/fm_voice_frame.py +166 -0
  197. package/.agents/backend/fm_voice_records.py +574 -0
  198. package/.agents/backend/model-bridge.py +290 -0
  199. package/.agents/backend/opencode-go-bridge.py +30 -0
  200. package/.agents/bus/README.md +12 -0
  201. package/.agents/bus/messages.json +1 -0
  202. package/.agents/config/agents.machine.example.yaml +16 -0
  203. package/.agents/config/agents.yaml +50 -0
  204. package/.agents/config/agents.yaml.example +72 -0
  205. package/.agents/config/crew-dispatch.json +22 -0
  206. package/.agents/config/cron.yaml +9 -0
  207. package/.agents/config/eindri-dispatch.json +67 -0
  208. package/.agents/config/eindri-harness +1 -0
  209. package/.agents/config/startup-memory-budget +1 -0
  210. package/.agents/config/tailscale-sync.yaml +27 -0
  211. package/.agents/filebrowser/.filebrowser.json +6 -0
  212. package/.agents/filebrowser/README.md +11 -0
  213. package/.agents/gateway/README.md +12 -0
  214. package/.agents/github/README.md +15 -0
  215. package/.agents/github/webhooks/.gitkeep +0 -0
  216. package/.agents/github/workflows/.gitkeep +0 -0
  217. package/.agents/harness/opencode/plugins/lib/rodd-operational-input.js +37 -0
  218. package/.agents/harness/opencode/plugins/package.json +4 -0
  219. package/.agents/harness/opencode/plugins/saga-sessionstart.js +77 -0
  220. package/.agents/harness/opencode/plugins/syn-cd-check.js +61 -0
  221. package/.agents/harness/opencode/plugins/syn-pretool-check.js +68 -0
  222. package/.agents/harness/opencode/plugins/syn-turnend-guard.js +105 -0
  223. package/.agents/harness/opencode/plugins/syn-watch-arm.js +521 -0
  224. package/.agents/memory/README.md +46 -0
  225. package/.agents/memory/kaia.engram +0 -0
  226. package/.agents/memory/kaia.engram-shm +0 -0
  227. package/.agents/memory/kaia.engram-wal +0 -0
  228. package/.agents/memory/runes_audit.md +5 -0
  229. package/.agents/memory/well/episodes.jsonl +367 -0
  230. package/.agents/memory/well/workspace.jsonl +27 -0
  231. package/.agents/migrations/0001-hodd-layout.sh +38 -0
  232. package/.agents/migrations/0002-a2a-mcp.sh +27 -0
  233. package/.agents/migrations/0003-private-data-separation.sh +111 -0
  234. package/.agents/migrations/0004-hoard-and-realms.sh +104 -0
  235. package/.agents/sandbox/Dockerfile.utgard +19 -0
  236. package/.agents/sandbox/README.md +14 -0
  237. package/.agents/sandbox/utgard.config.json +14 -0
  238. package/.agents/skills/README.md +59 -0
  239. package/.agents/skills/bragi-marketing/README.md +3 -0
  240. package/.agents/skills/bragi-marketing/SKILL.md +82 -0
  241. package/.agents/skills/eindri-homes/README.md +3 -0
  242. package/.agents/skills/eindri-homes/SKILL.md +291 -0
  243. package/.agents/skills/eindri-homes/assets/control-plane.md +122 -0
  244. package/.agents/skills/eindri-homes/assets/dispatch.md +80 -0
  245. package/.agents/skills/frigg-consent/README.md +3 -0
  246. package/.agents/skills/frigg-consent/SKILL.md +56 -0
  247. package/.agents/skills/galdr-ymirsystem/README.md +3 -0
  248. package/.agents/skills/galdr-ymirsystem/SKILL.md +158 -0
  249. package/.agents/skills/galdr-ymirsystem/assets/README.md +74 -0
  250. package/.agents/skills/galdr-ymirsystem/assets/brokk-distro-runtime.md +390 -0
  251. package/.agents/skills/galdr-ymirsystem/assets/build-method.md +113 -0
  252. package/.agents/skills/galdr-ymirsystem/assets/build-tool-categories.md +178 -0
  253. package/.agents/skills/galdr-ymirsystem/assets/eindri-orchestration.md +518 -0
  254. package/.agents/skills/galdr-ymirsystem/assets/eindri-profiles.md +136 -0
  255. package/.agents/skills/galdr-ymirsystem/assets/harness-integration/README.md +673 -0
  256. package/.agents/skills/galdr-ymirsystem/assets/harness-integration/claude-code.md +197 -0
  257. package/.agents/skills/galdr-ymirsystem/assets/harness-integration/codex.md +191 -0
  258. package/.agents/skills/galdr-ymirsystem/assets/harness-integration/cursor.md +193 -0
  259. package/.agents/skills/galdr-ymirsystem/assets/harness-integration/opencode.md +262 -0
  260. package/.agents/skills/galdr-ymirsystem/assets/harness-integration/pi.md +295 -0
  261. package/.agents/skills/galdr-ymirsystem/assets/hlidskjalf-ui.md +496 -0
  262. package/.agents/skills/galdr-ymirsystem/assets/installation.md +571 -0
  263. package/.agents/skills/galdr-ymirsystem/assets/local-models/HOST-DATA.example.md +72 -0
  264. package/.agents/skills/galdr-ymirsystem/assets/local-models.md +274 -0
  265. package/.agents/skills/galdr-ymirsystem/assets/memory-well.md +161 -0
  266. package/.agents/skills/galdr-ymirsystem/assets/nornir-jobs.md +377 -0
  267. package/.agents/skills/galdr-ymirsystem/assets/norse-naming.md +325 -0
  268. package/.agents/skills/galdr-ymirsystem/assets/odrerir-hall.md +129 -0
  269. package/.agents/skills/galdr-ymirsystem/assets/pi-boot/einherjar-spawn.schema.json +81 -0
  270. package/.agents/skills/galdr-ymirsystem/assets/pi-boot/herdr-profile.toml +75 -0
  271. package/.agents/skills/galdr-ymirsystem/assets/pi-boot/pi-profile.yml +96 -0
  272. package/.agents/skills/galdr-ymirsystem/assets/pi-boot/supervision-tree.yml +163 -0
  273. package/.agents/skills/galdr-ymirsystem/assets/pi-boot-guide.md +58 -0
  274. package/.agents/skills/galdr-ymirsystem/assets/porting-upstream-to-norse.md +301 -0
  275. package/.agents/skills/galdr-ymirsystem/assets/principles.md +385 -0
  276. package/.agents/skills/galdr-ymirsystem/assets/reference-adoption.md +99 -0
  277. package/.agents/skills/galdr-ymirsystem/assets/registry.md +379 -0
  278. package/.agents/skills/galdr-ymirsystem/assets/runtime-compliance.md +463 -0
  279. package/.agents/skills/galdr-ymirsystem/assets/runtime-components.md +553 -0
  280. package/.agents/skills/galdr-ymirsystem/assets/smidja.md +175 -0
  281. package/.agents/skills/galdr-ymirsystem/schemas/toon-schemas.md +190 -0
  282. package/.agents/skills/galdr-ymirsystem/scripts/README.md +45 -0
  283. package/.agents/skills/galdr-ymirsystem/scripts/bench-one.sh +248 -0
  284. package/.agents/skills/galdr-ymirsystem/scripts/compliance-check.sh +360 -0
  285. package/.agents/skills/galdr-ymirsystem/scripts/gpu-sample.sh +43 -0
  286. package/.agents/skills/galdr-ymirsystem/scripts/stop-all.sh +34 -0
  287. package/.agents/skills/galdr-ymirsystem/scripts/toon-check.py +188 -0
  288. package/.agents/skills/gjallarhorn-relay/README.md +3 -0
  289. package/.agents/skills/gjallarhorn-relay/SKILL.md +283 -0
  290. package/.agents/skills/groa-update/SKILL.md +70 -0
  291. package/.agents/skills/hamr-adapters/README.md +3 -0
  292. package/.agents/skills/hamr-adapters/SKILL.md +123 -0
  293. package/.agents/skills/hamr-adapters/references/common/control-and-recovery.md +37 -0
  294. package/.agents/skills/hamr-adapters/references/common/dispatch.md +32 -0
  295. package/.agents/skills/hamr-adapters/references/common/model-and-effort.md +42 -0
  296. package/.agents/skills/hamr-adapters/references/common/primary-hooks.md +40 -0
  297. package/.agents/skills/hamr-adapters/references/harness/claude.md +55 -0
  298. package/.agents/skills/hamr-adapters/references/harness/codex.md +43 -0
  299. package/.agents/skills/hamr-adapters/references/harness/cursor.md +75 -0
  300. package/.agents/skills/hamr-adapters/references/harness/grok.md +69 -0
  301. package/.agents/skills/hamr-adapters/references/harness/kimi.md +51 -0
  302. package/.agents/skills/hamr-adapters/references/harness/muse.md +70 -0
  303. package/.agents/skills/hamr-adapters/references/harness/opencode.md +42 -0
  304. package/.agents/skills/hamr-adapters/references/harness/pi.md +56 -0
  305. package/.agents/skills/herdr-panes/README.md +3 -0
  306. package/.agents/skills/herdr-panes/SKILL.md +92 -0
  307. package/.agents/skills/herdr-panes/assets/herdr-backend.md +346 -0
  308. package/.agents/skills/herdr-panes/assets/tmux-backend.md +115 -0
  309. package/.agents/skills/hnoss-design/DESIGN.md +19 -0
  310. package/.agents/skills/hnoss-design/README.md +3 -0
  311. package/.agents/skills/hnoss-design/SKILL.md +83 -0
  312. package/.agents/skills/hvild-afk/README.md +3 -0
  313. package/.agents/skills/hvild-afk/SKILL.md +241 -0
  314. package/.agents/skills/jord-projects/README.md +3 -0
  315. package/.agents/skills/jord-projects/SKILL.md +92 -0
  316. package/.agents/skills/lifecycle/SKILL.md +56 -0
  317. package/.agents/skills/lifecycle/smoke_test.sh +52 -0
  318. package/.agents/skills/lifecycle/start.sh +21 -0
  319. package/.agents/skills/lifecycle/status.sh +35 -0
  320. package/.agents/skills/lifecycle/stop.sh +16 -0
  321. package/.agents/skills/muninn-stow/README.md +3 -0
  322. package/.agents/skills/muninn-stow/SKILL.md +309 -0
  323. package/.agents/skills/no-mistakes/SKILL.md +379 -0
  324. package/.agents/skills/nornir-schedule/README.md +3 -0
  325. package/.agents/skills/nornir-schedule/SKILL.md +25 -0
  326. package/.agents/skills/nornir-schedule/assets/events.md +137 -0
  327. package/.agents/skills/nornir-schedule/assets/quota.md +131 -0
  328. package/.agents/skills/nsr-compliance/README.md +3 -0
  329. package/.agents/skills/nsr-compliance/SKILL.md +29 -0
  330. package/.agents/skills/nsr-compliance/assets/nsr/README.md +40 -0
  331. package/.agents/skills/nsr-compliance/assets/nsr/assets/RULES/README.md +15 -0
  332. package/.agents/skills/nsr-compliance/assets/nsr/assets/RULES/git-workflow.md +18 -0
  333. package/.agents/skills/nsr-compliance/assets/nsr/assets/RULES/security.md +22 -0
  334. package/.agents/skills/nsr-compliance/assets/nsr/assets/RULES/tech-stack.md +8 -0
  335. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/SKILL.md +71 -0
  336. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/config/core_four.yaml +16 -0
  337. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/gates/check_danger.sh +102 -0
  338. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/gates/check_wiring.sh +69 -0
  339. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/gates/validate_code.sh +29 -0
  340. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/gates/verify_docs.py +66 -0
  341. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/generate.sh +109 -0
  342. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/harness/envelopes/result_envelope.json +14 -0
  343. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/harness/envelopes/task_envelope.json +20 -0
  344. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/harness/runner.py +303 -0
  345. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/telemetry/logger.py +112 -0
  346. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/templates/compliance/README.md +18 -0
  347. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/templates/compliance/config/README.md +10 -0
  348. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/templates/compliance/gates/README.md +12 -0
  349. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/templates/compliance/gates/check_env.sh +23 -0
  350. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/templates/compliance/gates/check_paths.sh +20 -0
  351. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/templates/compliance/gates/check_platform.sh +23 -0
  352. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/templates/compliance/harness/README.md +11 -0
  353. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/templates/compliance/harness/envelopes/README.md +10 -0
  354. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/templates/compliance/installer/README.md +10 -0
  355. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/templates/compliance/installer/install.sh +125 -0
  356. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/templates/compliance/telemetry/README.md +11 -0
  357. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/README.md +20 -0
  358. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/features/README.md +36 -0
  359. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/features/SKILL-template.md +14 -0
  360. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/git_ops/README.md +25 -0
  361. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/git_ops/SKILL.md +13 -0
  362. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/git_ops/create_branch.sh +6 -0
  363. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/git_ops/safe_commit.sh +9 -0
  364. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/git_ops/sync_upstream.sh +6 -0
  365. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/lifecycle/README.md +27 -0
  366. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/lifecycle/SKILL.md +15 -0
  367. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/lifecycle/smoke_test.sh +12 -0
  368. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/lifecycle/start.sh +19 -0
  369. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/lifecycle/status.sh +12 -0
  370. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/lifecycle/stop.sh +15 -0
  371. package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/README.md +18 -0
  372. package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/config/README.md +10 -0
  373. package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/config/core_four.yaml +13 -0
  374. package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/gates/README.md +12 -0
  375. package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/gates/check_danger.sh +102 -0
  376. package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/gates/check_env.sh +23 -0
  377. package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/gates/check_paths.sh +20 -0
  378. package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/gates/check_platform.sh +23 -0
  379. package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/gates/check_wiring.sh +69 -0
  380. package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/harness/README.md +11 -0
  381. package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/harness/envelopes/README.md +10 -0
  382. package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/harness/envelopes/result_envelope.json +14 -0
  383. package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/harness/envelopes/task_envelope.json +20 -0
  384. package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/harness/runner.py +303 -0
  385. package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/installer/README.md +10 -0
  386. package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/installer/install.sh +125 -0
  387. package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/telemetry/README.md +11 -0
  388. package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/telemetry/logger.py +112 -0
  389. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/BEST_PRACTICES/README.md +14 -0
  390. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/BEST_PRACTICES/api.md +17 -0
  391. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/BEST_PRACTICES/compliance-wiring.md +155 -0
  392. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/BEST_PRACTICES/frontend.md +15 -0
  393. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/CI_CD/README.md +11 -0
  394. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/CI_CD/deployment/README.md +23 -0
  395. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/CI_CD/deployment/clients/README.md +16 -0
  396. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/CI_CD/deployment/clients/template-README.md +11 -0
  397. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/CI_CD/deployment/deploy.sh +49 -0
  398. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/CI_CD/deployment/envs/README.md +12 -0
  399. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/CI_CD/deployment/envs/development.env.example +17 -0
  400. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/CI_CD/deployment/envs/production.env.example +17 -0
  401. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/CI_CD/deployment/envs/staging.env.example +17 -0
  402. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/CI_CD/deployment/tenants/README.md +11 -0
  403. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/CI_CD/deployment/tenants/template.env.example +8 -0
  404. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/DEVELOPER_SETUP/README.md +26 -0
  405. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/DEVELOPER_SETUP/agent.md +7 -0
  406. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/DEVELOPER_SETUP/developers/dev-1.md +20 -0
  407. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/DEVELOPER_SETUP/linux.md +9 -0
  408. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/DEVELOPER_SETUP/macos.md +10 -0
  409. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/DEVELOPER_SETUP/shared.md +19 -0
  410. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/DEVELOPER_SETUP/windows.md +14 -0
  411. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/HOSTING/README.md +25 -0
  412. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/HOSTING/_template.md +41 -0
  413. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/HOSTING/hosting-1.md +41 -0
  414. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/README.md +21 -0
  415. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/RUNBOOK/README.md +15 -0
  416. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/RUNBOOK/deployment.md +19 -0
  417. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/RUNBOOK/disaster-recovery.md +15 -0
  418. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/RUNBOOK/ops-tasks.md +14 -0
  419. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/features/README.md +16 -0
  420. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/project-skills.md +146 -0
  421. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/research/README.md +11 -0
  422. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/research/software-compliance-research.md +269 -0
  423. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/working-with-agents.md +260 -0
  424. package/.agents/skills/nsr-compliance/assets/nsr/assets/root/AGENTS.md +38 -0
  425. package/.agents/skills/nsr-compliance/assets/nsr/assets/root/ARCHITECTURE.md +36 -0
  426. package/.agents/skills/nsr-compliance/assets/nsr/assets/root/BEST_PRACTICES.md +27 -0
  427. package/.agents/skills/nsr-compliance/assets/nsr/assets/root/CI_CD.md +23 -0
  428. package/.agents/skills/nsr-compliance/assets/nsr/assets/root/FEATURES.md +15 -0
  429. package/.agents/skills/nsr-compliance/assets/nsr/assets/root/README.md +22 -0
  430. package/.agents/skills/nsr-compliance/assets/nsr/assets/root/RULES.md +37 -0
  431. package/.agents/skills/nsr-compliance/assets/nsr/assets/root/STRUCTURE.md +45 -0
  432. package/.agents/skills/nsr-compliance/assets/nsr/assets/root/TECH_STACK.md +28 -0
  433. package/.agents/skills/nsr-compliance/assets/nsr/scaffold-spec.md +63 -0
  434. package/.agents/skills/nsr-compliance/assets/nsr/scripts/scaffold.sh +103 -0
  435. package/.agents/skills/nsr-compliance/assets/nsr/scripts/validate.sh +52 -0
  436. package/.agents/skills/nsr-compliance/assets/nsrcompliance/SKILL.md +71 -0
  437. package/.agents/skills/nsr-compliance/assets/nsrcompliance/config/core_four.yaml +16 -0
  438. package/.agents/skills/nsr-compliance/assets/nsrcompliance/gates/check_danger.sh +102 -0
  439. package/.agents/skills/nsr-compliance/assets/nsrcompliance/gates/check_wiring.sh +69 -0
  440. package/.agents/skills/nsr-compliance/assets/nsrcompliance/gates/validate_code.sh +29 -0
  441. package/.agents/skills/nsr-compliance/assets/nsrcompliance/gates/verify_docs.py +66 -0
  442. package/.agents/skills/nsr-compliance/assets/nsrcompliance/generate.sh +109 -0
  443. package/.agents/skills/nsr-compliance/assets/nsrcompliance/harness/envelopes/result_envelope.json +14 -0
  444. package/.agents/skills/nsr-compliance/assets/nsrcompliance/harness/envelopes/task_envelope.json +20 -0
  445. package/.agents/skills/nsr-compliance/assets/nsrcompliance/harness/runner.py +303 -0
  446. package/.agents/skills/nsr-compliance/assets/nsrcompliance/telemetry/logger.py +112 -0
  447. package/.agents/skills/nsr-compliance/assets/nsrcompliance/templates/compliance/README.md +18 -0
  448. package/.agents/skills/nsr-compliance/assets/nsrcompliance/templates/compliance/config/README.md +10 -0
  449. package/.agents/skills/nsr-compliance/assets/nsrcompliance/templates/compliance/gates/README.md +12 -0
  450. package/.agents/skills/nsr-compliance/assets/nsrcompliance/templates/compliance/gates/check_env.sh +23 -0
  451. package/.agents/skills/nsr-compliance/assets/nsrcompliance/templates/compliance/gates/check_paths.sh +20 -0
  452. package/.agents/skills/nsr-compliance/assets/nsrcompliance/templates/compliance/gates/check_platform.sh +23 -0
  453. package/.agents/skills/nsr-compliance/assets/nsrcompliance/templates/compliance/harness/README.md +11 -0
  454. package/.agents/skills/nsr-compliance/assets/nsrcompliance/templates/compliance/harness/envelopes/README.md +10 -0
  455. package/.agents/skills/nsr-compliance/assets/nsrcompliance/templates/compliance/installer/README.md +10 -0
  456. package/.agents/skills/nsr-compliance/assets/nsrcompliance/templates/compliance/installer/install.sh +125 -0
  457. package/.agents/skills/nsr-compliance/assets/nsrcompliance/templates/compliance/telemetry/README.md +11 -0
  458. package/.agents/skills/pr-ops/README.md +3 -0
  459. package/.agents/skills/pr-ops/SKILL.md +109 -0
  460. package/.agents/skills/ratatoskr-a2a/README.md +3 -0
  461. package/.agents/skills/ratatoskr-a2a/SKILL.md +148 -0
  462. package/.agents/skills/rules-check-drift/SKILL.md +81 -0
  463. package/.agents/skills/saga-bearings/README.md +3 -0
  464. package/.agents/skills/saga-bearings/SKILL.md +26 -0
  465. package/.agents/skills/saga-bearings/assets/bearings.md +161 -0
  466. package/.agents/skills/saga-bearings/assets/board-template.html +731 -0
  467. package/.agents/skills/saga-bearings/assets/recap.md +56 -0
  468. package/.agents/skills/syn-recovery/README.md +3 -0
  469. package/.agents/skills/syn-recovery/SKILL.md +54 -0
  470. package/.agents/skills/tyr-check/README.md +3 -0
  471. package/.agents/skills/tyr-check/SKILL.md +218 -0
  472. package/.agents/skills/urdh-hold/README.md +3 -0
  473. package/.agents/skills/urdh-hold/SKILL.md +25 -0
  474. package/.agents/skills/urdh-hold/assets/decisions.md +15 -0
  475. package/.agents/skills/urdh-hold/assets/hold.md +54 -0
  476. package/.agents/skills/vor-diagnostics/README.md +3 -0
  477. package/.agents/skills/vor-diagnostics/SKILL.md +68 -0
  478. package/.agents/skills/ymir-host/README.md +3 -0
  479. package/.agents/skills/ymir-host/SKILL.md +34 -0
  480. package/.agents/skills/ymir-host/assets/desktop.md +36 -0
  481. package/.agents/skills/ymir-host/assets/omarchy.md +297 -0
  482. package/.agents/skills/ymir-host/assets/runbooks.md +15 -0
  483. package/.agents/skills/ymir-host/assets/thjazi.md +220 -0
  484. package/.agents/skills/ymir-host/assets/update.md +64 -0
  485. package/.agents/tests/assets/board-render-harness.mjs +117 -0
  486. package/.agents/tests/cmux-test-safety.sh +51 -0
  487. package/.agents/tests/fixtures.sh +291 -0
  488. package/.agents/tests/fm-afk-inject-e2e.test.sh +428 -0
  489. package/.agents/tests/fm-afk-inject-herdr-e2e.test.sh +536 -0
  490. package/.agents/tests/fm-afk-launch.test.sh +961 -0
  491. package/.agents/tests/fm-afk-pi-herdr-return-e2e.test.sh +297 -0
  492. package/.agents/tests/fm-afk-return.test.sh +283 -0
  493. package/.agents/tests/fm-arm-pretool-check.test.sh +482 -0
  494. package/.agents/tests/fm-ask-user-authority.test.sh +42 -0
  495. package/.agents/tests/fm-backend-autodetect-smoke.test.sh +176 -0
  496. package/.agents/tests/fm-backend-cmux-smoke.test.sh +188 -0
  497. package/.agents/tests/fm-backend-cmux.test.sh +1166 -0
  498. package/.agents/tests/fm-backend-herdr-eventwait-smoke.test.sh +136 -0
  499. package/.agents/tests/fm-backend-herdr-eventwait.test.py +106 -0
  500. package/.agents/tests/fm-backend-herdr-focus-flash-e2e.test.sh +409 -0
  501. package/.agents/tests/fm-backend-herdr-launcher-workspace-e2e.test.sh +437 -0
  502. package/.agents/tests/fm-backend-herdr-presentation-e2e.test.sh +1424 -0
  503. package/.agents/tests/fm-backend-herdr-prune-safety-e2e.test.sh +180 -0
  504. package/.agents/tests/fm-backend-herdr-respawn-idem-e2e.test.sh +182 -0
  505. package/.agents/tests/fm-backend-herdr-smoke.test.sh +355 -0
  506. package/.agents/tests/fm-backend-herdr-workspace-per-home-e2e.test.sh +251 -0
  507. package/.agents/tests/fm-backend-herdr.test.sh +4606 -0
  508. package/.agents/tests/fm-backend-orca.test.sh +1357 -0
  509. package/.agents/tests/fm-backend-tmux-smoke.test.sh +173 -0
  510. package/.agents/tests/fm-backend-zellij-smoke.test.sh +205 -0
  511. package/.agents/tests/fm-backend-zellij.test.sh +1360 -0
  512. package/.agents/tests/fm-backend.test.sh +1142 -0
  513. package/.agents/tests/fm-backlog-atomicity.test.sh +2302 -0
  514. package/.agents/tests/fm-backlog-handoff.test.sh +1360 -0
  515. package/.agents/tests/fm-bearings-board-render.test.sh +135 -0
  516. package/.agents/tests/fm-bearings-board.test.sh +414 -0
  517. package/.agents/tests/fm-bearings-snapshot.test.sh +1990 -0
  518. package/.agents/tests/fm-bootstrap-network-parallel.test.sh +316 -0
  519. package/.agents/tests/fm-bootstrap.test.sh +1178 -0
  520. package/.agents/tests/fm-branch-supervision.test.sh +552 -0
  521. package/.agents/tests/fm-brief.test.sh +785 -0
  522. package/.agents/tests/fm-busy-adapter-wiring.test.sh +324 -0
  523. package/.agents/tests/fm-busy-state.test.sh +465 -0
  524. package/.agents/tests/fm-calm-pi-extension.test.sh +3979 -0
  525. package/.agents/tests/fm-captain-hold-lifecycle.test.sh +1187 -0
  526. package/.agents/tests/fm-cd-pretool-check.test.sh +400 -0
  527. package/.agents/tests/fm-check-unregister.test.sh +198 -0
  528. package/.agents/tests/fm-classify-corr-token.test.sh +543 -0
  529. package/.agents/tests/fm-classify-decision-key.test.sh +340 -0
  530. package/.agents/tests/fm-claude-stop-autoarm-live-e2e.test.sh +164 -0
  531. package/.agents/tests/fm-claude-stop-autoarm.test.sh +1190 -0
  532. package/.agents/tests/fm-cmux-claude-composer-live-e2e.test.sh +110 -0
  533. package/.agents/tests/fm-codex-continuity-live-e2e.test.sh +55 -0
  534. package/.agents/tests/fm-composer-ghost.test.sh +716 -0
  535. package/.agents/tests/fm-composer-lib.test.sh +666 -0
  536. package/.agents/tests/fm-composer-matrix-live-e2e.test.sh +220 -0
  537. package/.agents/tests/fm-control-herdr-smoke.test.sh +149 -0
  538. package/.agents/tests/fm-control-relaunch.test.sh +1530 -0
  539. package/.agents/tests/fm-control.test.sh +910 -0
  540. package/.agents/tests/fm-crew-state.test.sh +1610 -0
  541. package/.agents/tests/fm-cursor-harness.test.sh +404 -0
  542. package/.agents/tests/fm-cursor-primary-live-e2e.test.sh +217 -0
  543. package/.agents/tests/fm-cursor-primary.test.sh +708 -0
  544. package/.agents/tests/fm-daemon.test.sh +2740 -0
  545. package/.agents/tests/fm-documentation-audiences.test.sh +141 -0
  546. package/.agents/tests/fm-ensure-agents-md.test.sh +371 -0
  547. package/.agents/tests/fm-extension-binding.test.sh +2187 -0
  548. package/.agents/tests/fm-fleet-snapshot-view.test.sh +816 -0
  549. package/.agents/tests/fm-fleet-sync.test.sh +721 -0
  550. package/.agents/tests/fm-gate-refuse.test.sh +356 -0
  551. package/.agents/tests/fm-gitignore-config.test.sh +89 -0
  552. package/.agents/tests/fm-gotmp.test.sh +230 -0
  553. package/.agents/tests/fm-grok-continuity-live-e2e.test.sh +112 -0
  554. package/.agents/tests/fm-grok-harness.test.sh +117 -0
  555. package/.agents/tests/fm-grok-stop-live-e2e.test.sh +219 -0
  556. package/.agents/tests/fm-guard-stale-banner.test.sh +709 -0
  557. package/.agents/tests/fm-harness-adapter-instructions-live-e2e.test.sh +134 -0
  558. package/.agents/tests/fm-harness-adapter-references.test.sh +30 -0
  559. package/.agents/tests/fm-harness-liveness-drift-live-e2e.test.sh +151 -0
  560. package/.agents/tests/fm-herdr-lab.test.sh +243 -0
  561. package/.agents/tests/fm-herdr-session-cleanup-e2e.test.sh +146 -0
  562. package/.agents/tests/fm-herdr-session-cleanup.test.sh +329 -0
  563. package/.agents/tests/fm-herdr-submit-confirm-live-e2e.test.sh +125 -0
  564. package/.agents/tests/fm-herdr-version-floor-live-e2e.test.sh +144 -0
  565. package/.agents/tests/fm-home-summary-refresh.test.sh +971 -0
  566. package/.agents/tests/fm-inactive-reconcile.test.sh +485 -0
  567. package/.agents/tests/fm-kimi-harness.test.sh +687 -0
  568. package/.agents/tests/fm-lint-workflows.test.sh +532 -0
  569. package/.agents/tests/fm-lint.test.sh +1023 -0
  570. package/.agents/tests/fm-muse-harness.test.sh +932 -0
  571. package/.agents/tests/fm-muse-signals-live-e2e.test.sh +205 -0
  572. package/.agents/tests/fm-no-mistakes-required.test.sh +72 -0
  573. package/.agents/tests/fm-on.test.sh +507 -0
  574. package/.agents/tests/fm-opencode-primary-live-e2e.test.sh +357 -0
  575. package/.agents/tests/fm-operational-input.test.sh +160 -0
  576. package/.agents/tests/fm-peek-remote.test.sh +110 -0
  577. package/.agents/tests/fm-pending-reply.test.sh +1288 -0
  578. package/.agents/tests/fm-pi-branch-extension.test.sh +3182 -0
  579. package/.agents/tests/fm-pi-branch-live-e2e.test.sh +527 -0
  580. package/.agents/tests/fm-pi-primary-live-e2e.test.sh +345 -0
  581. package/.agents/tests/fm-pi-primary-types.test.sh +68 -0
  582. package/.agents/tests/fm-pi-watch-extension.test.sh +2843 -0
  583. package/.agents/tests/fm-pr-check-security.test.sh +2140 -0
  584. package/.agents/tests/fm-pr-merge.test.sh +2135 -0
  585. package/.agents/tests/fm-procevent-quota.test.sh +225 -0
  586. package/.agents/tests/fm-procevent-when.test.sh +406 -0
  587. package/.agents/tests/fm-procevent.test.sh +1745 -0
  588. package/.agents/tests/fm-project-origin.test.sh +110 -0
  589. package/.agents/tests/fm-public-followup.test.sh +2272 -0
  590. package/.agents/tests/fm-quota-array-dispatch-live-e2e.test.sh +526 -0
  591. package/.agents/tests/fm-quota-choose.test.sh +626 -0
  592. package/.agents/tests/fm-remote-backlog-handoff.test.sh +453 -0
  593. package/.agents/tests/fm-remote-doctor.test.sh +625 -0
  594. package/.agents/tests/fm-remote-entrypoint.test.sh +59 -0
  595. package/.agents/tests/fm-remote-job-orphan-reap.test.sh +205 -0
  596. package/.agents/tests/fm-remote-job.test.sh +768 -0
  597. package/.agents/tests/fm-remote-reply.test.sh +520 -0
  598. package/.agents/tests/fm-remote-secondmate-lifecycle-e2e.test.sh +1256 -0
  599. package/.agents/tests/fm-remote-secondmate-parent-binding.test.sh +326 -0
  600. package/.agents/tests/fm-remote-secondmate-trace-context.test.sh +310 -0
  601. package/.agents/tests/fm-remote-transport-lanes.test.sh +431 -0
  602. package/.agents/tests/fm-review-diff.test.sh +176 -0
  603. package/.agents/tests/fm-secondmate-harness.test.sh +2600 -0
  604. package/.agents/tests/fm-secondmate-lifecycle-e2e.test.sh +259 -0
  605. package/.agents/tests/fm-secondmate-liveness.test.sh +559 -0
  606. package/.agents/tests/fm-secondmate-reconcile.test.sh +732 -0
  607. package/.agents/tests/fm-secondmate-safety.test.sh +2972 -0
  608. package/.agents/tests/fm-secondmate-sync.test.sh +913 -0
  609. package/.agents/tests/fm-send-inbox-doorbell-live-e2e.test.sh +206 -0
  610. package/.agents/tests/fm-send-inbox.test.sh +352 -0
  611. package/.agents/tests/fm-send-popup-settle.test.sh +167 -0
  612. package/.agents/tests/fm-send-remote-delivery.test.sh +796 -0
  613. package/.agents/tests/fm-send-resolve-key.test.sh +551 -0
  614. package/.agents/tests/fm-send-secondmate-marker-herdr-e2e.test.sh +187 -0
  615. package/.agents/tests/fm-send-secondmate-marker.test.sh +278 -0
  616. package/.agents/tests/fm-send-settle.test.sh +147 -0
  617. package/.agents/tests/fm-send-strict.test.sh +241 -0
  618. package/.agents/tests/fm-session-lock-ancestry.test.sh +365 -0
  619. package/.agents/tests/fm-session-start.test.sh +2515 -0
  620. package/.agents/tests/fm-sessionstart-hook-live-e2e.test.sh +631 -0
  621. package/.agents/tests/fm-sessionstart-instruction-refresh-live-e2e.test.sh +230 -0
  622. package/.agents/tests/fm-sessionstart-nudge.test.sh +1039 -0
  623. package/.agents/tests/fm-shared-captain-inheritance.test.sh +404 -0
  624. package/.agents/tests/fm-spawn-batch.test.sh +148 -0
  625. package/.agents/tests/fm-spawn-dispatch-profile.test.sh +827 -0
  626. package/.agents/tests/fm-spawn-pool-base-freshen.test.sh +440 -0
  627. package/.agents/tests/fm-spawn-worktree-settle.test.sh +147 -0
  628. package/.agents/tests/fm-startup-memory-budget.test.sh +333 -0
  629. package/.agents/tests/fm-startup-network.test.sh +712 -0
  630. package/.agents/tests/fm-stow-cascade.test.sh +370 -0
  631. package/.agents/tests/fm-subagent-pretool-check.test.sh +291 -0
  632. package/.agents/tests/fm-supervision-events.test.sh +156 -0
  633. package/.agents/tests/fm-supervision-instructions.test.sh +189 -0
  634. package/.agents/tests/fm-tangle-guard.test.sh +277 -0
  635. package/.agents/tests/fm-task-delivery.test.sh +420 -0
  636. package/.agents/tests/fm-task-inbox.test.sh +513 -0
  637. package/.agents/tests/fm-teardown-endpoint-safety.test.sh +374 -0
  638. package/.agents/tests/fm-teardown.test.sh +2665 -0
  639. package/.agents/tests/fm-test-fixture-cleanup.test.sh +172 -0
  640. package/.agents/tests/fm-test-fixtures.test.sh +132 -0
  641. package/.agents/tests/fm-test-isolation-proof.test.sh +278 -0
  642. package/.agents/tests/fm-test-run.test.sh +1196 -0
  643. package/.agents/tests/fm-tmux-agent-liveness.test.sh +354 -0
  644. package/.agents/tests/fm-tmux-submit-busy.test.sh +340 -0
  645. package/.agents/tests/fm-tool-update-check.test.sh +1044 -0
  646. package/.agents/tests/fm-trace-context-lib.test.sh +253 -0
  647. package/.agents/tests/fm-trace-context-spawn.test.sh +581 -0
  648. package/.agents/tests/fm-transition-lib.test.sh +54 -0
  649. package/.agents/tests/fm-turnend-guard.test.sh +1822 -0
  650. package/.agents/tests/fm-update.test.sh +304 -0
  651. package/.agents/tests/fm-vendor-auth-probe.test.sh +395 -0
  652. package/.agents/tests/fm-voice-relay.test.sh +4741 -0
  653. package/.agents/tests/fm-wake-daemon-lifecycle-e2e.test.sh +171 -0
  654. package/.agents/tests/fm-wake-drain-open-decisions-cursor.test.sh +356 -0
  655. package/.agents/tests/fm-wake-drain-open-decisions.test.sh +226 -0
  656. package/.agents/tests/fm-wake-drain-unread-status.test.sh +379 -0
  657. package/.agents/tests/fm-wake-queue.test.sh +1224 -0
  658. package/.agents/tests/fm-watch-arm.test.sh +821 -0
  659. package/.agents/tests/fm-watch-checkpoint.test.sh +87 -0
  660. package/.agents/tests/fm-watch-recovery-loop.test.sh +223 -0
  661. package/.agents/tests/fm-watch-triage.test.sh +3889 -0
  662. package/.agents/tests/fm-watcher-lock.test.sh +1133 -0
  663. package/.agents/tests/fm-x-mode.test.sh +3042 -0
  664. package/.agents/tests/gleipnir-guard-pretool-check.test.sh +51 -0
  665. package/.agents/tests/gleipnir-machine-lock.test.sh +130 -0
  666. package/.agents/tests/herdr-test-safety.sh +42 -0
  667. package/.agents/tests/lib.sh +315 -0
  668. package/.agents/tests/remote-herdr-fixture.sh +125 -0
  669. package/.agents/tests/secondmate-helpers.sh +193 -0
  670. package/.agents/tests/smoke.test.sh +93 -0
  671. package/.agents/tests/wake-helpers.sh +338 -0
  672. package/.agents/tests/zellij-test-safety.sh +60 -0
  673. package/.agents/tools/README.md +10 -0
  674. package/.agents/tools/bin/tasks-cli +267 -0
  675. package/.agents/tools/tasks-cli.ts +216 -0
  676. package/AGENTS.md +454 -0
  677. package/CHANGELOG.md +881 -0
  678. package/LICENSE +1 -1
  679. package/NOTICE +63 -0
  680. package/README.md +113 -59
  681. package/RULES/01-domains.md +51 -0
  682. package/RULES/02-agents.md +78 -0
  683. package/RULES/03-houses.md +61 -0
  684. package/RULES/04-hoard.md +65 -0
  685. package/RULES/05-platforms.md +67 -0
  686. package/RULES/06-append-only.md +58 -0
  687. package/RULES/07-config.md +45 -0
  688. package/RULES/08-delivery-gate.md +75 -0
  689. package/RULES/README.md +20 -0
  690. package/bin/README.md +107 -0
  691. package/bin/a2a-mcp.sh +98 -0
  692. package/bin/a2a-serve.py +103 -0
  693. package/bin/a2a-serve.sh +8 -0
  694. package/bin/a2a-talk.sh +50 -0
  695. package/bin/a2abridge-ensure.sh +108 -0
  696. package/bin/agent-run.sh +43 -0
  697. package/bin/agents-config.sh +233 -0
  698. package/bin/apodex-smoke-test.sh +92 -0
  699. package/bin/bifrost-bridge.sh +112 -0
  700. package/bin/bifrost-ingress.sh +77 -0
  701. package/bin/bootstrap-macos.sh +107 -0
  702. package/bin/bootstrap-windows.ps1 +112 -0
  703. package/bin/branch-guard.sh +64 -0
  704. package/bin/brokk +73 -0
  705. package/bin/brokk-classify-lib.sh +1770 -0
  706. package/bin/brokk-lease-lib.sh +222 -0
  707. package/bin/brokk-lease.sh +193 -0
  708. package/bin/brokk-lint.sh +65 -0
  709. package/bin/brokk-send.sh +38 -0
  710. package/bin/brokk-update.sh +9 -0
  711. package/bin/brokk-wake-grant.sh +127 -0
  712. package/bin/brokk-wake-lib.sh +1849 -0
  713. package/bin/changelog-guard.sh +165 -0
  714. package/bin/crash-sense.sh +160 -0
  715. package/bin/desktop-place.sh +224 -0
  716. package/bin/docs-guard.sh +39 -0
  717. package/bin/editor-place.sh +207 -0
  718. package/bin/eindri-acclaim.sh +34 -0
  719. package/bin/eindri-control.sh +28 -0
  720. package/bin/eindri-role.sh +83 -0
  721. package/bin/eindri-seen.sh +41 -0
  722. package/bin/eindri-send.sh +15 -0
  723. package/bin/eindri-start.sh +85 -0
  724. package/bin/eindri-watch.sh +57 -0
  725. package/bin/einherjar-spawn.sh +576 -0
  726. package/bin/eir-doctor.sh +112 -0
  727. package/bin/erindi-brief.sh +271 -0
  728. package/bin/fleet-apply.sh +68 -0
  729. package/bin/github-deploy.sh +81 -0
  730. package/bin/gjallarhorn-expose.sh +102 -0
  731. package/bin/gjallarhorn-notify.sh +58 -0
  732. package/bin/gjallarhorn-purge.sh +54 -0
  733. package/bin/gjallarhorn-tunnel.sh +72 -0
  734. package/bin/gleipnir-lock-lib.sh +284 -0
  735. package/bin/groa-update.sh +122 -0
  736. package/bin/hall-snapshot.sh +92 -0
  737. package/bin/hamr-harness.sh +246 -0
  738. package/bin/heimdall-ssh-keys.sh +374 -0
  739. package/bin/herdr-agents.py +44 -0
  740. package/bin/herdr-ensure.sh +104 -0
  741. package/bin/herdr-run.sh +431 -0
  742. package/bin/hermes-ensure.sh +88 -0
  743. package/bin/hoard-lib.sh +25 -0
  744. package/bin/hodd.sh +78 -0
  745. package/bin/host-sense.sh +199 -0
  746. package/bin/huginn-research-worker.sh +108 -0
  747. package/bin/local-model-lock.sh +51 -0
  748. package/bin/mimir-bridge.py +226 -0
  749. package/bin/mimir-bridge.sh +87 -0
  750. package/bin/mimir-ingest.sh +131 -0
  751. package/bin/mimir.sh +113 -0
  752. package/bin/mjollnir-webhook.sh +59 -0
  753. package/bin/mjollnir.sh +85 -0
  754. package/bin/model-hardware.sh +82 -0
  755. package/bin/model-resolve.sh +147 -0
  756. package/bin/models-detect.sh +178 -0
  757. package/bin/nornir-cron-start.sh +162 -0
  758. package/bin/nornir-job-daily-briefing.sh +191 -0
  759. package/bin/nornir-job-git-sync.sh +152 -0
  760. package/bin/nornir-job-memory-housekeeping.sh +114 -0
  761. package/bin/nornir-job-observer.sh +166 -0
  762. package/bin/nornir-job-skillopt-sleep.sh +76 -0
  763. package/bin/omarchy-hook-install.sh +60 -0
  764. package/bin/omarchy-install.sh +148 -0
  765. package/bin/omarchy-plugins.sh +174 -0
  766. package/bin/omarchy-sense.sh +149 -0
  767. package/bin/perm-guard.sh +30 -0
  768. package/bin/pi-agent.sh +68 -0
  769. package/bin/pi-ensure.sh +77 -0
  770. package/bin/pi-local.sh +37 -0
  771. package/bin/pi-seat.sh +63 -0
  772. package/bin/prereq-ensure.sh +166 -0
  773. package/bin/private-guard.sh +38 -0
  774. package/bin/project-git.sh +55 -0
  775. package/bin/public-guard.sh +70 -0
  776. package/bin/ratatoskr.sh +49 -0
  777. package/bin/realm-lib.sh +32 -0
  778. package/bin/repo-scrub.sh +62 -0
  779. package/bin/rodd-operational-input.sh +250 -0
  780. package/bin/runes-append.sh +198 -0
  781. package/bin/saga-session-start.sh +172 -0
  782. package/bin/saga-sessionstart-run.sh +79 -0
  783. package/bin/saga-wake-drain.sh +41 -0
  784. package/bin/secret-guard.sh +63 -0
  785. package/bin/sessrumnir-ensure.sh +100 -0
  786. package/bin/sessrumnir-sync.sh +237 -0
  787. package/bin/sessrumnir.sh +91 -0
  788. package/bin/skillopt-setup.sh +76 -0
  789. package/bin/skuld-branch-outcome.sh +203 -0
  790. package/bin/skuld-branch-prompt.sh +101 -0
  791. package/bin/smidja-bootstrap.sh +90 -0
  792. package/bin/smidja-observe.sh +86 -0
  793. package/bin/syn-arm-pretool-check.sh +57 -0
  794. package/bin/syn-asset-pretool-check.sh +76 -0
  795. package/bin/syn-cd-pretool-check.sh +28 -0
  796. package/bin/syn-guard-pretool-check.sh +83 -0
  797. package/bin/syn-turnend-guard.sh +36 -0
  798. package/bin/syn-watch-arm.sh +99 -0
  799. package/bin/tailscale-sync.sh +190 -0
  800. package/bin/telegram-bot.sh +69 -0
  801. package/bin/toolchain.sh +74 -0
  802. package/bin/utgard.sh +111 -0
  803. package/bin/valhalla.sh +115 -0
  804. package/bin/valknut-load.sh +260 -0
  805. package/bin/vor-crew-state.sh +124 -0
  806. package/bin/wedge-notify.sh +47 -0
  807. package/bin/workspace-provision.sh +80 -0
  808. package/bin/workspace-rag.sh +150 -0
  809. package/bin/wyrd-db.sh +47 -0
  810. package/bin/yggdrasil.sh +152 -0
  811. package/bin/ymir-edit.sh +145 -0
  812. package/bin/ymir-install.sh +641 -0
  813. package/bin/ymir-invite.sh +93 -0
  814. package/bin/ymir-isolation.sh +88 -0
  815. package/bin/ymir-migrate.sh +61 -0
  816. package/bin/ymir-platform.sh +348 -0
  817. package/bin/ymir-say.sh +97 -0
  818. package/bin/ymir-setup-auth.sh +108 -0
  819. package/bin/ymir-validate.sh +173 -0
  820. package/bin/ymir.js +74 -16
  821. package/docs/Architecture.md +339 -0
  822. package/docs/design.md +351 -0
  823. package/docs/installations/README.md +63 -0
  824. package/docs/installations/ymir-home.md +93 -0
  825. package/docs/integration.md +105 -0
  826. package/docs/lore.md +1056 -0
  827. package/docs/research/agent-harnesses.md +290 -0
  828. package/docs/runbooks/README.md +19 -0
  829. package/docs/runbooks/a2a.md +91 -0
  830. package/docs/runbooks/agent-permissions.md +99 -0
  831. package/docs/runbooks/agents.md +57 -0
  832. package/docs/runbooks/models.md +69 -0
  833. package/docs/runbooks/realm-onboarding.md +60 -0
  834. package/docs/runbooks/secrets-and-hoard.md +60 -0
  835. package/docs/runbooks/updates-and-migrations.md +110 -0
  836. package/docs/runbooks/worktrees-isolation.md +72 -0
  837. package/docs/session-start.md +105 -0
  838. package/docs/skillopt-integration.md +321 -0
  839. package/docs/sync-system.md +66 -0
  840. package/docs/workspaces.md +104 -0
  841. package/docs/ymir-rut.md +260 -0
  842. package/package.json +44 -14
  843. package/packaging/build.sh +74 -0
  844. package/packaging/macos/Ymir Installer.command +43 -0
  845. package/packaging/windows/ymir-setup.nsi +74 -0
  846. package/scripts/README.md +22 -0
  847. package/scripts/electron.sh +210 -0
  848. package/scripts/start.sh +167 -0
  849. package/scripts/stop.sh +113 -0
  850. package/install.sh +0 -35
@@ -0,0 +1,3153 @@
1
+ #!/usr/bin/env bash
2
+ # Spawn a direct report: a crewmate in a treehouse or Orca worktree, or a
3
+ # secondmate in its isolated firstmate home.
4
+ # Usage: fm-spawn.sh <task-id> <project-dir> --mode <no-mistakes|direct-PR|local-only> --yolo <on|off> [--harness <name>|harness|launch-command] [--model <name>] [--effort <level>] [--backend <name>]
5
+ # fm-spawn.sh <task-id> <project-dir> --scout [--harness <name>|harness|launch-command] [--model <name>] [--effort <level>] [--backend <name>]
6
+ # fm-spawn.sh <task-id> [<firstmate-home>] [--harness <name>|harness|launch-command] [--model <name>] [--effort <level>] [--backend <name>] --secondmate
7
+ # --mode and --yolo are this task's delivery contract, REQUIRED for every ship
8
+ # spawn and refused on --scout and --secondmate spawns. Firstmate resolves both
9
+ # per task at intake (AGENTS.md section 7); data/projects.md holds the captain's
10
+ # standing posture as context, not as this task's answer, so a spawn never looks
11
+ # the mode up. A ship spawn additionally reads the brief's recorded
12
+ # "Delivery contract: mode=<mode>" line and REFUSES a mismatch, so the worker's
13
+ # instructions and the recorded task delivery cannot drift apart; a brief
14
+ # scaffolded before that line existed warns once and launches on the flag. When
15
+ # the explicit mode carries less rigor than the project's standing posture, a
16
+ # loud one-line deviation notice is printed and the spawn continues.
17
+ # no-mistakes-prod-only is a registry policy rather than a task mode and is
18
+ # refused as a flag value.
19
+ # fm-spawn.sh <task-id> --relaunch [--harness <name>] [--model <name>] [--effort <level>]
20
+ # --relaunch launches a replacement agent for an EXISTING task into that
21
+ # task's own recorded endpoint and worktree instead of creating either. It is
22
+ # the launch half of the control plane (bin/fm-control.sh relaunch), which
23
+ # owns the checkpoint, the progress note, stopping the previous agent, and the
24
+ # transaction; call fm-control rather than this flag directly unless you are
25
+ # deliberately re-launching an already-stopped task. Every identity axis -
26
+ # backend, kind, project or home, worktree, endpoint - comes from the task's
27
+ # validated state/<id>.meta, so --backend, --scout, --secondmate, a project
28
+ # positional, and batch pairs are all refused alongside it; only harness,
29
+ # model, and effort may change, which is what makes a harness switch one
30
+ # ordinary relaunch. It refuses unless the recorded endpoint is positively
31
+ # agent-free on a backend with a recovery-grade agent-state classifier (tmux
32
+ # or herdr), refuses unless the endpoint's shell is sitting in the recorded
33
+ # worktree, and clears the previous harness's per-task wiring before arming
34
+ # the new incarnation.
35
+ # --harness <name> is the explicit per-spawn harness/profile adapter. The old
36
+ # positional harness arg still works for back-compat.
37
+ # --model <name> and --effort <low|medium|high|xhigh|max> are concrete profile
38
+ # axes chosen by firstmate at intake. They are only threaded into harnesses whose
39
+ # installed CLIs were verified to support that axis; unsupported axes are omitted
40
+ # from that harness's launch rather than guessed.
41
+ # --backend <name> is the explicit runtime session-provider backend for this
42
+ # exact task only (docs/configuration.md "Runtime backend" owns when that flag
43
+ # is authorized). Without it, the script resolves FM_BACKEND, then
44
+ # config/backend, then runtime auto-detection from the runtime firstmate's
45
+ # environment: $TMUX, HERDR_ENV=1, or cmux runtime signals (via
46
+ # bin/fm-backend.sh's fm_backend_detect, with cmux fallback details in
47
+ # docs/cmux-backend.md),
48
+ # then tmux.
49
+ # Spawn-capable backends are the reference tmux adapter and experimental
50
+ # herdr, zellij, orca, and cmux. Orca owns both the task worktree and
51
+ # terminal, so ship/scout Orca spawns do not run treehouse get; cmux is a
52
+ # session provider only, exactly like herdr/zellij, so it does. An
53
+ # auto-detected herdr or cmux spawn prints a loud stderr notice;
54
+ # auto-detected tmux stays silent; zellij and orca are never auto-detected.
55
+ # codex-app is not a known backend yet; docs/codex-app-backend.md owns that
56
+ # blocked backend contract. Default tmux spawns do not write backend= to meta;
57
+ # absent backend= means tmux. cmux does not support --secondmate spawns yet.
58
+ # A backend spawn refusal (missing dependency, version gate, unauthenticated
59
+ # socket, or unsupported secondmate mode) is terminal for that selected backend;
60
+ # callers must surface it instead of silently retrying another backend.
61
+ # A herdr crewmate or scout is placed in the exact workspace of the firstmate
62
+ # or secondmate process launching it, resolved from that process's own herdr
63
+ # pane rather than from a workspace label (herdr enforces no label uniqueness,
64
+ # so a label cannot tell two "firstmate" workspaces apart). A claimed parent
65
+ # identity that is unreadable, contradictory, stale, or from another herdr
66
+ # session stops the spawn before any worker endpoint exists. A launcher
67
+ # outside herdr has no workspace to inherit and uses this home's own labeled
68
+ # workspace, which must then match exactly one. --secondmate is the deliberate
69
+ # exception: it stands up that secondmate home's own workspace.
70
+ # Herdr additionally uses a presentation-only layout by default when the
71
+ # selected client and running server meet the Herdr 0.8.0 floor. The local
72
+ # config/herdr-presentation-spaces file can say off to disable it or on to
73
+ # opt in below that floor; an empty file remains the historical opt-in form.
74
+ # A clean fresh task first writes state/<id>.herdr-presentation atomically,
75
+ # then creates a disposable
76
+ # workspace containing only the ordinary task pane. A successful clean create
77
+ # upgrades its attempt journal with exact home, session, workspace, tab, pane,
78
+ # parent, and label bindings. On a same-identity restart, that complete binding
79
+ # plus authoritative metadata may replace one exact agent-free husk in place.
80
+ # The journal, visible token, and labels alone are never endpoint or ownership
81
+ # authority, and every ambiguous recovery stays on the flat fallback after
82
+ # duplicate-agent risk is independently absent. Treehouse allocation and task
83
+ # metadata are unchanged.
84
+ # A clean projected create or exact resume makes one bounded attempt to hold
85
+ # the one session-scoped presentation-order lock (keyed by named session plus
86
+ # canonical socket, outside any home's state/) through launch handoff. Lock
87
+ # contention warns and falls back to the ordinary flat layout before any
88
+ # projection mutation. The exact response-derived new workspace is inserted
89
+ # immediately after its owning parent (firstmate or 2ndmate-<id>) contiguous
90
+ # child block. Ordering never authorizes lifecycle cleanup, and any
91
+ # unavailable, ambiguous, or failed move warns while the spawn continues.
92
+ # Every projected create, prune, and move captures and verifies the named
93
+ # session's exact active workspace and tab. A detected focus change restores
94
+ # only that exact tab id; an ambiguous pre-operation snapshot refuses the
95
+ # focus-sensitive presentation mutation.
96
+ # Every single-task invocation holds one task-id-scoped lock across backend
97
+ # creation through metadata publication, so concurrent same-id spawns serialize
98
+ # even when they select different backends. A fresh spawn first takes the
99
+ # per-home task-set lock and refuses rather than waits when forced teardown owns
100
+ # it; relaunch is exempt because the existing task's control lock covers it.
101
+ # With no harness arg, a crewmate/scout spawn resolves the CREW harness only when
102
+ # config/crew-dispatch.json is absent. When that file exists, crewmate/scout
103
+ # spawns require an explicit harness so firstmate cannot silently skip dispatch
104
+ # profile consultation. A --secondmate spawn is exempt and resolves the SECONDMATE
105
+ # harness (config/secondmate-harness -> config/crew-harness -> own), so the
106
+ # secondmate-vs-crewmate split is DURABLE across every respawn (recovery,
107
+ # /updatefirstmate, restart). A bare adapter name (claude|codex|opencode|pi|pi-signed|grok|kimi|cursor|muse)
108
+ # overrides it for this spawn (either kind). A non-flag string containing
109
+ # whitespace is treated as a RAW launch command - the escape hatch for verifying
110
+ # new adapters. For pi and pi-signed, fm-spawn resolves the selected executable
111
+ # name from PATH once, probes that concrete path with --help, and launches the
112
+ # same path. It adds --tui-mode regular only when that help advertises the flag;
113
+ # a failed or inconclusive probe omits it so older Pi versions remain launchable.
114
+ # A missing selected executable refuses before endpoint creation, and pi-signed
115
+ # never falls back to pi.
116
+ # config/secondmate-harness may also carry an optional model and effort as extra
117
+ # whitespace-separated tokens ("<harness> [<model>] [<effort>]"). For a
118
+ # --secondmate spawn, those tokens apply only when this spawn also resolves its
119
+ # harness from config/secondmate-harness. An explicit per-spawn --harness,
120
+ # positional harness arg, or raw launch command starts with clean model/effort
121
+ # defaults unless the caller also passes explicit --model/--effort flags. When
122
+ # the file governs the spawn, its model/effort tokens are re-resolved on every
123
+ # respawn exactly like the harness axis, and explicit --model/--effort flags
124
+ # still win over the file's tokens.
125
+ # A --secondmate spawn also propagates the primary's declared inherited local
126
+ # material, so the secondmate's OWN crewmates inherit primary config and the
127
+ # secondmate receives the primary's read-only shared captain-preference file
128
+ # (fm-config-inherit-lib.sh). A successful launch clears pending inherited
129
+ # config reread generations because the new agent reads the converged files.
130
+ # --scout records kind=scout in the task's meta (report deliverable, scratch worktree;
131
+ # see AGENTS.md task lifecycle); --secondmate records kind=secondmate and launches in a
132
+ # provisioned firstmate home; the default is kind=ship.
133
+ # Before a secondmate launch, the home is locally fast-forwarded to the primary
134
+ # default-branch commit when safe; skipped syncs warn and launch unchanged.
135
+ # Ship/scout spawns refuse to launch unless the resolved task path is a real
136
+ # git worktree root distinct from the primary project checkout.
137
+ # Before a fresh ship or scout worker starts, its clean task worktree fetches
138
+ # origin, resolves the current remote default branch, and resets to its tip.
139
+ # An unreachable origin, unresolved default branch, or non-clean worktree
140
+ # refuses the spawn rather than risking a PR based on stale history.
141
+ # A slot whose only deviation is a stale submodule gitlink is refused by that
142
+ # same clean check, but is reported as a stale checkout naming each submodule
143
+ # and both pins; nothing is converged or removed, and no remedy is suggested.
144
+ # That report is only reached when each submodule's checked-out commit is
145
+ # already contained in one of its remotes, so a submodule carrying an unpushed
146
+ # commit keeps the conservative uncommitted-work refusal instead. That
147
+ # containment test reads local refs only and never fetches, so this gate stays
148
+ # usable offline; a stale remote-tracking ref can therefore make an unpushed
149
+ # commit look contained, which is exactly why no remedy command is printed.
150
+ # Batch dispatch: pass one or more `id=repo` pairs instead of a single <id> <project>, e.g.
151
+ # fm-spawn.sh fix-a-k3=projects/foo add-b-q7=projects/bar [--scout]
152
+ # Each pair re-execs this script in single-task mode, so the single path stays the only
153
+ # source of truth; shared --scout/--harness/--model/--effort/--backend/--mode/--yolo
154
+ # applies to every pair. A ship batch therefore carries one delivery contract, and each
155
+ # pair still checks it against its own brief; a batch spanning modes is two invocations.
156
+ # If config/crew-dispatch.json exists, shared --harness is required for crewmate
157
+ # and scout batches. The loop lives here, in bash, so callers never hand-write a
158
+ # multi-task shell loop (the tool shell is zsh, which does not word-split unquoted
159
+ # $vars and silently breaks ad-hoc `for ... in $pairs` loops).
160
+ # Launch templates live in launch_template() below; placeholders replaced before launch:
161
+ # __BRIEF__ absolute path to data/<task-id>/brief.md
162
+ # __PIBIN__ quoted concrete Pi-family executable path resolved from PATH
163
+ # __PITUIMODE__ optional --tui-mode regular when that executable advertises it
164
+ # __TURNEND__ absolute path to state/<task-id>.turn-ended (for harnesses whose
165
+ # turn-end signal rides the launch command, e.g. codex -c notify=[...])
166
+ # __PIEXT__ absolute path to state/<task-id>.pi-ext.ts (pi turn-end extension,
167
+ # written by this script; outside the worktree to avoid pi's trust gate)
168
+ # __PITURNEND__ absolute path to .pi/extensions/fm-primary-turnend-guard.ts in a pi secondmate home
169
+ # __PIWATCH__ absolute path to .pi/extensions/fm-primary-pi-watch.ts in a pi secondmate home
170
+ # __OPINPUT__ absolute path to the canonical operational-input encoder
171
+ # __WORKTREE__ absolute path to the task worktree
172
+ # __CURSORBIN__ resolved, cursor-verified executable for a cursor launch
173
+ # Verified per-harness turn-end hooks are installed automatically where enabled; some live outside the worktree.
174
+ # Kimi uses one surgically installed Firstmate region in $HOME/.kimi-code/config.toml,
175
+ # a firstmate-owned global hook and registry, and a gitignored per-task pointer.
176
+ # grok uses a firstmate-owned global hook under ${GROK_HOME:-$HOME/.grok}/hooks
177
+ # plus a gitignored .fm-grok-turnend worktree pointer and a state token.
178
+ # muse installs no hook at all - its plugin engine is off in the default build - so
179
+ # it writes state/<id>.muse-session to bind the pane to muse's own session event
180
+ # log; muse is crewmate/scout only and is refused for --secondmate.
181
+ # cursor installs no per-task hook either: it writes state/<id>.cursor-session to
182
+ # bind the pane to cursor's own conversation transcript (projects root, the exact
183
+ # workspace path cursor records in .workspace-trusted, and the conversations that
184
+ # already existed for that workspace). It is launched through the verified binary
185
+ # resolver because `cursor` is not the CLI name. A cursor SECONDMATE instead runs
186
+ # the tracked project-scope .cursor/hooks.json in its own home, whose stop-hook
187
+ # park owns that home's supervision (docs/supervision-protocols/cursor.md).
188
+ # Publishing the record and moving this home's backlog item to In flight are one
189
+ # step, not two: bin/fm-backlog-transition-lib.sh owns that invariant, and this
190
+ # script performs the transition under the task's own meta lock before it reports
191
+ # success. A ship or scout dispatch therefore REFUSES up front, before any
192
+ # endpoint, worktree, or record exists, unless the home's backlog has an
193
+ # unheld, unblocked Queued or In flight item for the id; a transition that fails
194
+ # after publication removes the record it just wrote rather than leaving a
195
+ # worker the backlog does not own. A relaunch re-reads the row instead of
196
+ # re-running the transition, so an eligible In-flight item is left untouched.
197
+ # The transition is
198
+ # skipped entirely for --secondmate spawns (persistent agents are not work
199
+ # items), on a config/backlog-backend=manual home, and in a home that keeps no
200
+ # data/backlog.md. An automatic-backend home with a backlog but no compatible
201
+ # tasks-axi refuses before creating any lifecycle state.
202
+ # On success prints: spawned <id> harness=<name> kind=<ship|scout|secondmate> [mode=<mode> yolo=<on|off>] window=<backend-target> worktree=<path>
203
+ # A ship task records the explicit mode/yolo it was passed; a secondmate spawn records
204
+ # mode=secondmate, yolo=off, home=, and projects=; a scout records neither, and both the
205
+ # success line and state/<id>.meta omit them.
206
+ # Every fresh spawn or relaunch records a new spawn_gen= incarnation token so durable
207
+ # consumers can distinguish a replacement worker that reuses the same task id.
208
+ # When the home session's frozen trace-context decision is enabled (see
209
+ # docs/configuration.md and bin/fm-trace-context-lib.sh), the meta also records
210
+ # one W3C traceparent= carrier, the same value injected into the pane as
211
+ # TRACEPARENT; the default-off path writes neither, leaving the generated meta
212
+ # and launch environment unchanged.
213
+ # --traceparent <carrier> delivers a carrier that a REMOTE parent already
214
+ # resolved and will record, instead of resolving one from this home's frozen
215
+ # decision. It is accepted only for --secondmate spawns, only as a strictly
216
+ # validated W3C traceparent, and exists because a remote secondmate's task
217
+ # identity is owned by the parent home that holds its task metadata, while the
218
+ # pane export happens on the remote host (bin/fm-remote-secondmate-control.sh).
219
+ # Local spawns never pass it and resolve their own carrier exactly as before.
220
+ set -eu
221
+
222
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
223
+
224
+ usage() {
225
+ # The whole leading comment block, ending at the first line that is not a
226
+ # comment. Derived rather than a fixed line range, which silently truncated
227
+ # this help mid-sentence every time the header above grew.
228
+ sed -n '2,${/^#/!q;p;}' "$0" | sed 's/^# \{0,1\}//'
229
+ }
230
+
231
+ case "${1:-}" in
232
+ -h|--help) usage; exit 0 ;;
233
+ esac
234
+
235
+ FM_ROOT="${FM_ROOT_OVERRIDE:-$(cd "$SCRIPT_DIR/.." && pwd)}"
236
+ FM_HOME="${FM_HOME:-${FM_ROOT_OVERRIDE:-$FM_ROOT}}"
237
+
238
+ # shellcheck source=bin/fm-tasks-axi-lib.sh
239
+ . "$SCRIPT_DIR/fm-tasks-axi-lib.sh"
240
+ # shellcheck source=bin/fm-backlog-transition-lib.sh
241
+ . "$SCRIPT_DIR/fm-backlog-transition-lib.sh"
242
+
243
+ resolve_directory_input() {
244
+ local name=$1 path=$2 resolved raw_bytes
245
+ raw_bytes=$(fm_backlog_bytes_of_string "$path") || return 1
246
+ if ! fm_backlog_control_bytes_valid 0 "$raw_bytes"; then
247
+ echo "error: $name directory contains an invalid control byte" >&2
248
+ return 1
249
+ fi
250
+ case "$path" in
251
+ /*) printf '%s\n' "$path"; return 0 ;;
252
+ esac
253
+ resolved=$(CDPATH='' cd -- "$path" 2>/dev/null && pwd -P) || {
254
+ echo "error: $name directory cannot be resolved: $path" >&2
255
+ return 1
256
+ }
257
+ printf '%s\n' "$resolved"
258
+ }
259
+
260
+ FM_HOME=$(resolve_directory_input FM_HOME "$FM_HOME") || exit 1
261
+ if [ -n "${FM_STATE_OVERRIDE:-}" ]; then
262
+ FM_STATE_OVERRIDE=$(resolve_directory_input FM_STATE_OVERRIDE "$FM_STATE_OVERRIDE") || exit 1
263
+ fi
264
+ if [ -n "${FM_DATA_OVERRIDE:-}" ]; then
265
+ FM_DATA_OVERRIDE=$(resolve_directory_input FM_DATA_OVERRIDE "$FM_DATA_OVERRIDE") || exit 1
266
+ fi
267
+ STATE="${FM_STATE_OVERRIDE:-$FM_HOME/state}"
268
+ DATA="${FM_DATA_OVERRIDE:-$FM_HOME/data}"
269
+ PROJECTS="${FM_PROJECTS_OVERRIDE:-$FM_HOME/projects}"
270
+ CONFIG="${FM_CONFIG_OVERRIDE:-$FM_HOME/config}"
271
+ SUB_HOME_MARKER=".fm-secondmate-home"
272
+ if [ -e "$STATE" ] || [ -L "$STATE" ]; then
273
+ fm_backlog_directory_present "$STATE" "state directory" || {
274
+ echo "error: spawn refused: $FM_BACKLOG_TRANSITION_ERROR" >&2
275
+ exit 1
276
+ }
277
+ fi
278
+ # shellcheck source=bin/fm-ff-lib.sh
279
+ . "$SCRIPT_DIR/fm-ff-lib.sh"
280
+ # shellcheck source=bin/fm-wake-lib.sh
281
+ . "$SCRIPT_DIR/fm-wake-lib.sh"
282
+ fm_backlog_directory_present "$STATE" "state directory" || {
283
+ echo "error: spawn refused: $FM_BACKLOG_TRANSITION_ERROR" >&2
284
+ exit 1
285
+ }
286
+ # shellcheck source=bin/fm-secondmate-nudge-lib.sh
287
+ . "$SCRIPT_DIR/fm-secondmate-nudge-lib.sh"
288
+ # shellcheck source=bin/fm-config-inherit-lib.sh
289
+ . "$SCRIPT_DIR/fm-config-inherit-lib.sh"
290
+ # shellcheck source=bin/fm-backend.sh
291
+ . "$SCRIPT_DIR/fm-backend.sh"
292
+ # shellcheck source=bin/fm-control-lib.sh
293
+ . "$SCRIPT_DIR/fm-control-lib.sh"
294
+ # shellcheck source=bin/fm-gate-refuse-lib.sh
295
+ . "$SCRIPT_DIR/fm-gate-refuse-lib.sh"
296
+ # shellcheck source=bin/fm-busy-lib.sh
297
+ . "$SCRIPT_DIR/fm-busy-lib.sh"
298
+ # shellcheck source=bin/fm-cursor-lib.sh
299
+ . "$SCRIPT_DIR/fm-cursor-lib.sh"
300
+ # shellcheck source=bin/fm-pr-lib.sh
301
+ . "$SCRIPT_DIR/fm-pr-lib.sh"
302
+ # shellcheck source=bin/fm-trace-context-lib.sh
303
+ . "$SCRIPT_DIR/fm-trace-context-lib.sh"
304
+ # shellcheck source=bin/fm-remote-readiness-lib.sh
305
+ . "$SCRIPT_DIR/fm-remote-readiness-lib.sh"
306
+ # Fail closed before any fleet mutation: a no-mistakes gate agent must never spawn
307
+ # a direct report (see bin/fm-gate-refuse-lib.sh).
308
+ fm_refuse_if_gate_agent
309
+ # Skip the watcher guard when re-exec'd for one pair of a batch (FM_SPAWN_NO_GUARD is
310
+ # set by the batch loop below), so the guard runs once for the batch, not once per pair.
311
+ [ -n "${FM_SPAWN_NO_GUARD:-}" ] || "$FM_ROOT/bin/fm-guard.sh" || true
312
+ KIND=ship
313
+ KIND_SET=0
314
+ HARNESS_ARG=
315
+ MODEL=
316
+ EFFORT=
317
+ BACKEND_ARG=
318
+ MODE=
319
+ YOLO=
320
+ TRACEPARENT_ARG=
321
+ HARNESS_SET=0
322
+ MODEL_SET=0
323
+ EFFORT_SET=0
324
+ BACKEND_SET=0
325
+ MODE_SET=0
326
+ YOLO_SET=0
327
+ TRACEPARENT_SET=0
328
+ RELAUNCH=0
329
+ POS=()
330
+ want_value=
331
+ for a in "$@"; do
332
+ if [ -n "$want_value" ]; then
333
+ case "$a" in
334
+ --*) echo "error: --$want_value requires a value" >&2; exit 1 ;;
335
+ esac
336
+ case "$want_value" in
337
+ harness) HARNESS_ARG=$a; HARNESS_SET=1 ;;
338
+ model) MODEL=$a; MODEL_SET=1 ;;
339
+ effort) EFFORT=$a; EFFORT_SET=1 ;;
340
+ backend) BACKEND_ARG=$a; BACKEND_SET=1 ;;
341
+ mode) MODE=$a; MODE_SET=1 ;;
342
+ yolo) YOLO=$a; YOLO_SET=1 ;;
343
+ traceparent) TRACEPARENT_ARG=$a; TRACEPARENT_SET=1 ;;
344
+ *) echo "error: internal parser state for --$want_value" >&2; exit 1 ;;
345
+ esac
346
+ want_value=
347
+ continue
348
+ fi
349
+ case "$a" in
350
+ --scout) KIND=scout; KIND_SET=1 ;;
351
+ --secondmate) KIND=secondmate; KIND_SET=1 ;;
352
+ --relaunch) RELAUNCH=1 ;;
353
+ --harness) want_value=harness ;;
354
+ --harness=*) HARNESS_ARG=${a#--harness=}; HARNESS_SET=1 ;;
355
+ --model) want_value=model ;;
356
+ --model=*) MODEL=${a#--model=}; MODEL_SET=1 ;;
357
+ --effort) want_value=effort ;;
358
+ --effort=*) EFFORT=${a#--effort=}; EFFORT_SET=1 ;;
359
+ --backend) want_value=backend ;;
360
+ --backend=*) BACKEND_ARG=${a#--backend=}; BACKEND_SET=1 ;;
361
+ --mode) want_value=mode ;;
362
+ --mode=*) MODE=${a#--mode=}; MODE_SET=1 ;;
363
+ --yolo) want_value=yolo ;;
364
+ --yolo=*) YOLO=${a#--yolo=}; YOLO_SET=1 ;;
365
+ --traceparent) want_value=traceparent ;;
366
+ --traceparent=*) TRACEPARENT_ARG=${a#--traceparent=}; TRACEPARENT_SET=1 ;;
367
+ *) POS+=("$a") ;;
368
+ esac
369
+ done
370
+ [ -z "$want_value" ] || { echo "error: --$want_value requires a value" >&2; exit 1; }
371
+ [ "$HARNESS_SET" -eq 0 ] || [ -n "$HARNESS_ARG" ] || { echo "error: --harness requires a non-empty value" >&2; exit 1; }
372
+ [ "$MODEL_SET" -eq 0 ] || [ -n "$MODEL" ] || { echo "error: --model requires a non-empty value" >&2; exit 1; }
373
+ [ "$EFFORT_SET" -eq 0 ] || [ -n "$EFFORT" ] || { echo "error: --effort requires a non-empty value" >&2; exit 1; }
374
+ [ "$BACKEND_SET" -eq 0 ] || [ -n "$BACKEND_ARG" ] || { echo "error: --backend requires a non-empty value" >&2; exit 1; }
375
+ [ "$MODE_SET" -eq 0 ] || [ -n "$MODE" ] || { echo "error: --mode requires a non-empty value" >&2; exit 1; }
376
+ [ "$YOLO_SET" -eq 0 ] || [ -n "$YOLO" ] || { echo "error: --yolo requires a non-empty value" >&2; exit 1; }
377
+ [ "$TRACEPARENT_SET" -eq 0 ] || [ -n "$TRACEPARENT_ARG" ] || { echo "error: --traceparent requires a non-empty value" >&2; exit 1; }
378
+ # A parent-delivered carrier replaces this home's own resolution, so it is
379
+ # refused unless it is a secondmate spawn carrying a strictly valid W3C value.
380
+ # Nothing else may reach the pane's TRACEPARENT export.
381
+ if [ "$TRACEPARENT_SET" -eq 1 ]; then
382
+ [ "$KIND" = secondmate ] || {
383
+ echo "error: --traceparent applies only to --secondmate spawns; every other spawn resolves its own carrier from this home's frozen trace-context decision" >&2
384
+ exit 1
385
+ }
386
+ fm_trace_context_valid "$TRACEPARENT_ARG" || {
387
+ echo "error: --traceparent is not a valid W3C traceparent" >&2
388
+ exit 1
389
+ }
390
+ fi
391
+ case "$EFFORT" in
392
+ ''|low|medium|high|xhigh|max) ;;
393
+ *) echo "error: --effort must be one of low, medium, high, xhigh, max" >&2; exit 1 ;;
394
+ esac
395
+
396
+ # --relaunch reuses an existing task's endpoint, worktree, project, and kind,
397
+ # so every axis this block resolves for a fresh spawn instead comes from that
398
+ # task's own durable record below. Contradicting it on the command line is a
399
+ # refusal rather than a silently-ignored flag.
400
+ if [ "$RELAUNCH" -eq 1 ]; then
401
+ [ "$BACKEND_SET" -eq 0 ] || { echo "error: --relaunch reuses the task's recorded backend; --backend cannot override it" >&2; exit 1; }
402
+ [ "$KIND_SET" -eq 0 ] || { echo "error: --relaunch reuses the task's recorded kind; --scout/--secondmate cannot override it" >&2; exit 1; }
403
+ [ "$MODE_SET" -eq 0 ] || { echo "error: --relaunch reuses the task's recorded delivery mode; --mode cannot override it" >&2; exit 1; }
404
+ [ "$YOLO_SET" -eq 0 ] || { echo "error: --relaunch reuses the task's recorded yolo posture; --yolo cannot override it" >&2; exit 1; }
405
+ else
406
+ # Delivery contract (AGENTS.md section 7). A ship task's mode and yolo are
407
+ # firstmate's per-task decision, so they are required and closed-set validated
408
+ # here rather than resolved from the project registry. Scouts deliver a report
409
+ # and record no delivery posture; secondmate spawns hardcode theirs.
410
+ if [ "$KIND" = ship ]; then
411
+ [ "$MODE_SET" -eq 1 ] || {
412
+ echo "error: ship spawns require --mode <no-mistakes|direct-PR|local-only>; resolve it at intake from the captain's instruction and the project's registered posture in data/projects.md" >&2
413
+ exit 1
414
+ }
415
+ [ "$YOLO_SET" -eq 1 ] || {
416
+ echo "error: ship spawns require --yolo <on|off>; it is this task's merge authority, not a project lookup" >&2
417
+ exit 1
418
+ }
419
+ case "$MODE" in
420
+ no-mistakes|direct-PR|local-only) ;;
421
+ no-mistakes-prod-only)
422
+ echo "error: no-mistakes-prod-only is a registry policy, not a task mode; classify this task's surface and resolve it to no-mistakes or direct-PR at intake" >&2
423
+ exit 1 ;;
424
+ *) echo "error: --mode must be one of no-mistakes, direct-PR, local-only (got '$MODE')" >&2; exit 1 ;;
425
+ esac
426
+ case "$YOLO" in
427
+ on|off) ;;
428
+ *) echo "error: --yolo must be on or off (got '$YOLO')" >&2; exit 1 ;;
429
+ esac
430
+ else
431
+ [ "$MODE_SET" -eq 0 ] || {
432
+ echo "error: --mode applies only to ship spawns; a scout delivers a report and a secondmate records its own fixed posture" >&2
433
+ exit 1
434
+ }
435
+ [ "$YOLO_SET" -eq 0 ] || {
436
+ echo "error: --yolo applies only to ship spawns; a scout delivers a report and a secondmate records its own fixed posture" >&2
437
+ exit 1
438
+ }
439
+ fi
440
+ fi
441
+
442
+ spawn_remote_secondmate() {
443
+ local id=$1 remote host root home harness positional model effort backend out rc meta tmp
444
+ local remote_backend remote_target remote_harness remote_herdr_session registry_lock remote_lock remote_generation
445
+ local remote_traceparent remote_recorded_traceparent
446
+ local -a launch_args
447
+ id=${POS[0]:-}
448
+ fm_task_id_creation_valid "$id" || { echo "error: invalid task id" >&2; return 2; }
449
+ mkdir -p "$STATE" || { echo "error: could not create parent state directory" >&2; return 1; }
450
+ SPAWN_TASK_LOCK="$STATE/.spawn-$id.lock"
451
+ if ! fm_lock_try_acquire "$SPAWN_TASK_LOCK"; then
452
+ echo "error: another spawn is already creating task $id" >&2
453
+ return 1
454
+ fi
455
+ registry_lock=$(secondmate_registry_lock_path "$STATE")
456
+ if ! fm_lock_acquire_wait "$registry_lock"; then
457
+ fm_lock_release "$SPAWN_TASK_LOCK" || true
458
+ echo "error: secondmate registry could not be locked for remote spawn" >&2
459
+ return 1
460
+ fi
461
+ remote=$(secondmate_registry_field "$DATA/secondmates.md" "$id" remote 2>/dev/null || true)
462
+ if [ "$remote" != 1 ]; then
463
+ fm_lock_release "$registry_lock" || true
464
+ fm_lock_release "$SPAWN_TASK_LOCK" || true
465
+ return 3
466
+ fi
467
+ host=$(secondmate_registry_field "$DATA/secondmates.md" "$id" host)
468
+ root=$(secondmate_registry_field "$DATA/secondmates.md" "$id" root)
469
+ home=$(secondmate_registry_field "$DATA/secondmates.md" "$id" home)
470
+ positional=${POS[1]:-}
471
+ if [ "${#POS[@]}" -gt 2 ]; then
472
+ fm_lock_release "$registry_lock" || true
473
+ fm_lock_release "$SPAWN_TASK_LOCK" || true
474
+ echo "error: remote secondmate spawn accepts no local home positional argument" >&2
475
+ return 2
476
+ fi
477
+ if [ -n "$HARNESS_ARG" ]; then
478
+ harness=$HARNESS_ARG
479
+ elif [ -n "$positional" ]; then
480
+ harness=$positional
481
+ else
482
+ harness=$("$FM_ROOT/bin/fm-harness.sh" secondmate)
483
+ fi
484
+ case "$harness" in
485
+ claude|codex|opencode|pi|pi-signed|grok|kimi|cursor) ;;
486
+ *)
487
+ fm_lock_release "$registry_lock" || true
488
+ fm_lock_release "$SPAWN_TASK_LOCK" || true
489
+ echo "error: remote secondmate spawn requires a verified harness adapter, not a raw launch command: $harness" >&2
490
+ return 1
491
+ ;;
492
+ esac
493
+ model=${MODEL:--}
494
+ effort=${EFFORT:--}
495
+ if [ -z "$HARNESS_ARG" ] && [ -z "$positional" ]; then
496
+ if [ "$MODEL_SET" -eq 0 ]; then
497
+ model=$("$SCRIPT_DIR/fm-harness.sh" secondmate-model)
498
+ [ -n "$model" ] || model=-
499
+ fi
500
+ if [ "$EFFORT_SET" -eq 0 ]; then
501
+ effort=$("$SCRIPT_DIR/fm-harness.sh" secondmate-effort)
502
+ [ -n "$effort" ] || effort=-
503
+ fi
504
+ fi
505
+ # A remote second mate always runs on Herdr: its server belongs to the host's
506
+ # own GUI login session, so the endpoint outlives every SSH connection that
507
+ # supervises it. bin/fm-remote-doctor.sh gates that host on the same
508
+ # requirement, and the remote home's config/backend never overrides it.
509
+ case "${BACKEND_ARG:--}" in
510
+ -|herdr) backend=herdr ;;
511
+ *)
512
+ fm_lock_release "$registry_lock" || true
513
+ fm_lock_release "$SPAWN_TASK_LOCK" || true
514
+ echo "error: a remote secondmate runs only on the herdr backend, not '$BACKEND_ARG'" >&2
515
+ return 1
516
+ ;;
517
+ esac
518
+ case "$effort" in
519
+ -|low|medium|high|xhigh|max) ;;
520
+ *)
521
+ fm_lock_release "$registry_lock" || true
522
+ fm_lock_release "$SPAWN_TASK_LOCK" || true
523
+ echo "error: invalid configured remote secondmate effort: $effort" >&2
524
+ return 1
525
+ ;;
526
+ esac
527
+ meta="$STATE/$id.meta"
528
+ if [ -e "$meta" ] || [ -L "$meta" ]; then
529
+ if ! fm_backlog_record_present "$meta" "task record" "$STATE" \
530
+ || [ "$(fm_meta_get "$meta" kind)" != secondmate ] \
531
+ || [ "$(fm_meta_get "$meta" remote_host)" != "$host" ] \
532
+ || [ "$(fm_meta_get "$meta" remote_root)" != "$root" ] \
533
+ || [ "$(fm_meta_get "$meta" home)" != "$home" ]; then
534
+ fm_lock_release "$registry_lock" || true
535
+ fm_lock_release "$SPAWN_TASK_LOCK" || true
536
+ echo "error: existing metadata for $id does not identify this remote secondmate route" >&2
537
+ return 1
538
+ fi
539
+ fi
540
+ # Gate the host before anything is published or transferred, so a host that
541
+ # cannot hold a durable Herdr endpoint refuses here rather than half-way
542
+ # through a launch. This is also the readiness gate every liveness relaunch
543
+ # passes through, because recovery respawns through this same route.
544
+ rc=0
545
+ fm_remote_readiness_ensure "$SCRIPT_DIR" "$id" || rc=$?
546
+ if [ "$rc" -ne 0 ]; then
547
+ fm_lock_release "$registry_lock" || true
548
+ fm_lock_release "$SPAWN_TASK_LOCK" || true
549
+ # Summary first, then the doctor's own text: a caller that reports only the
550
+ # first line, such as the startup liveness sweep, must still say something
551
+ # actionable.
552
+ if [ "$rc" -eq 255 ]; then
553
+ echo "error: remote secondmate $id readiness could not be confirmed; preserved route $host:$home" >&2
554
+ else
555
+ echo "error: remote secondmate $id host $host is not ready for a remote second mate; launch refused" >&2
556
+ fi
557
+ [ -z "$FM_REMOTE_READINESS_OUT" ] || printf '%s\n' "$FM_REMOTE_READINESS_OUT" >&2
558
+ [ "$rc" -ne 255 ] || return 255
559
+ return 1
560
+ fi
561
+ remote_lock=$(fm_remote_inherit_transaction_lock_path "$STATE" "$id")
562
+ if ! fm_lock_acquire_wait "$remote_lock"; then
563
+ fm_lock_release "$registry_lock" || true
564
+ fm_lock_release "$SPAWN_TASK_LOCK" || true
565
+ echo "error: remote secondmate $id inheritance transaction could not be locked" >&2
566
+ return 1
567
+ fi
568
+ remote_generation=$(fm_remote_inherit_generation_next "$STATE" "$id" 2>/dev/null || true)
569
+ if [ -z "$remote_generation" ]; then
570
+ fm_lock_release "$remote_lock" || true
571
+ fm_lock_release "$registry_lock" || true
572
+ fm_lock_release "$SPAWN_TASK_LOCK" || true
573
+ echo "error: remote secondmate $id inheritance generation could not be published" >&2
574
+ return 1
575
+ fi
576
+ if "$SCRIPT_DIR/fm-remote-inherit-push.sh" "$id" "$remote_generation" >/dev/null; then
577
+ :
578
+ else
579
+ rc=$?
580
+ fm_lock_release "$remote_lock" || true
581
+ fm_lock_release "$registry_lock" || true
582
+ fm_lock_release "$SPAWN_TASK_LOCK" || true
583
+ if [ "$rc" -eq 255 ]; then
584
+ echo "error: remote secondmate $id inheritance completion is unknown; launch refused and route preserved for reconciliation" >&2
585
+ else
586
+ echo "error: remote secondmate $id inheritance failed; launch refused" >&2
587
+ fi
588
+ return "$rc"
589
+ fi
590
+ # This parent home owns the remote secondmate's task identity because it holds
591
+ # the task metadata an observer reads, exactly as for a local spawn: the
592
+ # carrier is resolved against THIS task's own meta (reused verbatim on
593
+ # relaunch, freshly rooted otherwise, never adopting this process's ambient
594
+ # TRACEPARENT) under this home's frozen decision, then handed to the remote
595
+ # host to export into the agent's pane. Disabled resolves to empty and the
596
+ # remote launch call stays byte-identical to the untraced one.
597
+ remote_traceparent=
598
+ if [ "$(fm_trace_context_session_effective "$STATE/.trace-context-effective")" = on ]; then
599
+ remote_traceparent=$(FM_TRACE_CONTEXT=on fm_trace_context_resolve "$CONFIG" "$meta" || true)
600
+ fi
601
+ launch_args=("$id" "$harness" "$model" "$effort" "$backend")
602
+ [ -z "$remote_traceparent" ] || launch_args+=("$remote_traceparent")
603
+ if out=$("$SCRIPT_DIR/fm-on.sh" "$id" fm-remote-secondmate-control.sh launch \
604
+ "${launch_args[@]}" < /dev/null 2>&1); then
605
+ rc=0
606
+ else
607
+ rc=$?
608
+ fi
609
+ if [ "$rc" -ne 0 ]; then
610
+ fm_lock_release "$remote_lock" || true
611
+ fm_lock_release "$registry_lock" || true
612
+ fm_lock_release "$SPAWN_TASK_LOCK" || true
613
+ [ -z "$out" ] || printf '%s\n' "$out" >&2
614
+ if [ "$rc" -eq 255 ]; then
615
+ echo "error: remote secondmate $id is unavailable or launch completion is unknown; preserved route $host:$home" >&2
616
+ fi
617
+ return "$rc"
618
+ fi
619
+ remote_backend=$(printf '%s\n' "$out" | sed -n 's/^backend=//p' | tail -1)
620
+ remote_target=$(printf '%s\n' "$out" | sed -n 's/^target=//p' | tail -1)
621
+ remote_harness=$(printf '%s\n' "$out" | sed -n 's/^harness=//p' | tail -1)
622
+ remote_herdr_session=$(printf '%s\n' "$out" | sed -n 's/^herdr_session=//p' | tail -1)
623
+ if [ "$remote_backend" != herdr ]; then
624
+ fm_lock_release "$remote_lock" || true
625
+ fm_lock_release "$registry_lock" || true
626
+ fm_lock_release "$SPAWN_TASK_LOCK" || true
627
+ echo "error: remote launch returned backend '${remote_backend:-missing}', expected herdr; preserving the remote route for reconciliation" >&2
628
+ return 1
629
+ fi
630
+ [ -n "$remote_target" ] && [ "$remote_harness" = "$harness" ] || {
631
+ fm_lock_release "$remote_lock" || true
632
+ fm_lock_release "$registry_lock" || true
633
+ fm_lock_release "$SPAWN_TASK_LOCK" || true
634
+ echo "error: remote launch returned malformed route metadata; preserving the remote route for reconciliation" >&2
635
+ return 1
636
+ }
637
+ if [ "$remote_herdr_session" != fm-remote ] || [ "${remote_target%%:*}" != "$remote_herdr_session" ]; then
638
+ fm_lock_release "$remote_lock" || true
639
+ fm_lock_release "$registry_lock" || true
640
+ fm_lock_release "$SPAWN_TASK_LOCK" || true
641
+ echo "error: remote launch returned Herdr session '${remote_herdr_session:-missing}', expected 'fm-remote'; preserving the remote route for reconciliation" >&2
642
+ return 1
643
+ fi
644
+ # Record what the remote endpoint ACTUALLY carries, read back from its own
645
+ # launch, rather than what this side hoped to deliver. That keeps the #995
646
+ # guarantee that the recorded carrier is the identity the child received even
647
+ # when the remote host already had a live agent and reused its endpoint. An
648
+ # off decision delivers no carrier, but an endpoint already holding one still
649
+ # reports it here so the parent does not deny the agent's actual identity.
650
+ remote_recorded_traceparent=$(printf '%s\n' "$out" | sed -n 's/^traceparent=//p' | tail -1)
651
+ fm_trace_context_valid "$remote_recorded_traceparent" || remote_recorded_traceparent=
652
+ tmp="$meta.tmp.$$"
653
+ {
654
+ echo "window=remote:$id"
655
+ echo "endpoint_task_id=$id"
656
+ echo "worktree=$home"
657
+ echo "project=$root"
658
+ echo "harness=$harness"
659
+ echo "kind=secondmate"
660
+ echo "mode=secondmate"
661
+ echo "yolo=off"
662
+ echo "tasktmp="
663
+ echo "model=${model#-}"
664
+ echo "effort=${effort#-}"
665
+ echo "home=$home"
666
+ echo "projects=$(secondmate_registry_field "$DATA/secondmates.md" "$id" projects)"
667
+ echo "remote_host=$host"
668
+ echo "remote_root=$root"
669
+ echo "remote_backend=$remote_backend"
670
+ echo "remote_herdr_session=$remote_herdr_session"
671
+ echo "remote_target=$remote_target"
672
+ [ -z "$remote_recorded_traceparent" ] || echo "traceparent=$remote_recorded_traceparent"
673
+ } > "$tmp"
674
+ if ! fm_backlog_atomic_transition publish "$tmp" "$meta" "task record" "$STATE"; then
675
+ if [ "$SPAWN_TASK_SET_LOCK_HELD" = 1 ]; then
676
+ SPAWN_TASK_SET_LOCK_HELD=0
677
+ fm_lock_release "$SPAWN_TASK_SET_LOCK" || true
678
+ fi
679
+ fm_lock_release "$remote_lock" || true
680
+ fm_lock_release "$registry_lock" || true
681
+ fm_lock_release "$SPAWN_TASK_LOCK" || true
682
+ echo "error: remote secondmate $id launched, but its task record could not be published ($FM_BACKLOG_TRANSITION_ERROR)" >&2
683
+ return 1
684
+ fi
685
+ if [ "$SPAWN_TASK_SET_LOCK_HELD" = 1 ]; then
686
+ SPAWN_TASK_SET_LOCK_HELD=0
687
+ fm_lock_release "$SPAWN_TASK_SET_LOCK"
688
+ fi
689
+ fm_lock_release "$remote_lock" || true
690
+ fm_lock_release "$registry_lock" || true
691
+ fm_lock_release "$SPAWN_TASK_LOCK" || true
692
+ "$SCRIPT_DIR/fm-home-summary-refresh.sh" --best-effort || true
693
+ if ! "$SCRIPT_DIR/fm-procevent-remote-reply.sh" arm "$id" >/dev/null; then
694
+ echo "error: remote secondmate $id launched, but its reply source could not be armed; endpoint metadata is preserved" >&2
695
+ return 1
696
+ fi
697
+ echo "spawned $id harness=$harness kind=secondmate mode=secondmate yolo=off window=remote:$id worktree=$home remote=$host backend=$remote_backend"
698
+ return 0
699
+ }
700
+
701
+ BACKEND=
702
+ ORCA_ABORT_CLEANUP=0
703
+ ORCA_WORKTREE_ID=
704
+ ORCA_TERMINAL=
705
+ HERDR_PROJECTION_ABORT_CLEANUP=0
706
+ HERDR_PROJECTION_ABORT_SESSION=
707
+ HERDR_PROJECTION_ABORT_TASK_PANE=
708
+ HERDR_PROJECTION_ABORT_SEEDED_PANE=
709
+ HERDR_PRESENTATION_ORDER_LOCK=
710
+ HERDR_PRESENTATION_ORDER_LOCK_HELD=0
711
+ SPAWN_TASK_LOCK=
712
+ SPAWN_TASK_LOCK_HELD=0
713
+ SPAWN_CONTROL_LOCK=
714
+ SPAWN_CONTROL_LOCK_HELD=0
715
+ SPAWN_CONTROL_PARENT=0
716
+ SPAWN_META_TMP=
717
+ SPAWN_META_LOCK=
718
+ SPAWN_META_LOCK_HELD=0
719
+ SPAWN_META_PUBLISH_STARTED=0
720
+ SPAWN_FRESH_COMMIT_PENDING=0
721
+ SPAWN_TASK_SET_LOCK=
722
+ SPAWN_TASK_SET_LOCK_HELD=0
723
+ RELAUNCH_REPLACEMENT_PENDING=0
724
+ RELAUNCH_REPLACEMENT_BUSY_GEN=
725
+ RELAUNCH_REPLACEMENT_HARNESS=
726
+ RELAUNCH_REPLACEMENT_STATE=
727
+ RELAUNCH_REPLACEMENT_WT=
728
+ CONFIG_INHERIT_LOCK=
729
+ CONFIG_INHERIT_LOCK_HELD=0
730
+
731
+ spawn_fresh_commit_rollback() {
732
+ if fm_backlog_atomic_transition rollback "$STATE/$ID.meta" \
733
+ "$FM_ROOT/bin/fm-busy-event.sh" "$STATE" "$ID" "${BUSY_GEN:-}"; then
734
+ SPAWN_FRESH_COMMIT_PENDING=0
735
+ return 0
736
+ fi
737
+ echo "error: $FM_BACKLOG_TRANSITION_ERROR" >&2
738
+ return 1
739
+ }
740
+
741
+ parse_orca_worktree_result() {
742
+ local raw=$1 rest
743
+ ORCA_WORKTREE_ID=${raw%%$'\t'*}
744
+ if [ "$raw" = "$ORCA_WORKTREE_ID" ]; then
745
+ WT=
746
+ ORCA_TERMINAL=
747
+ return 1
748
+ fi
749
+ rest=${raw#*$'\t'}
750
+ WT=${rest%%$'\t'*}
751
+ if [ "$rest" != "$WT" ]; then
752
+ ORCA_TERMINAL=${rest#*$'\t'}
753
+ else
754
+ ORCA_TERMINAL=
755
+ fi
756
+ }
757
+
758
+ spawn_abort_cleanup() {
759
+ local status=$?
760
+ if [ "$RELAUNCH_REPLACEMENT_PENDING" = 1 ] \
761
+ && [ "$SPAWN_META_PUBLISH_STARTED" = 1 ] \
762
+ && [ -n "$SPAWN_META_TMP" ] \
763
+ && [ ! -e "$SPAWN_META_TMP" ] \
764
+ && [ ! -L "$SPAWN_META_TMP" ]; then
765
+ RELAUNCH_REPLACEMENT_PENDING=0
766
+ fi
767
+ if [ "$RELAUNCH_REPLACEMENT_PENDING" = 1 ]; then
768
+ RELAUNCH_REPLACEMENT_PENDING=0
769
+ if ! clear_relaunch_harness_wiring \
770
+ "$RELAUNCH_REPLACEMENT_HARNESS" \
771
+ "$RELAUNCH_REPLACEMENT_WT" \
772
+ "$RELAUNCH_REPLACEMENT_STATE" \
773
+ "$ID"; then
774
+ echo "warning: could not remove replacement wiring after aborted relaunch of $ID" >&2
775
+ fi
776
+ if [ -n "$RELAUNCH_REPLACEMENT_BUSY_GEN" ]; then
777
+ if ! "$FM_ROOT/bin/fm-busy-event.sh" retire \
778
+ "$RELAUNCH_REPLACEMENT_STATE" "$ID" \
779
+ --gen "$RELAUNCH_REPLACEMENT_BUSY_GEN"; then
780
+ echo "warning: could not retire replacement busy generation after aborted relaunch of $ID" >&2
781
+ fi
782
+ fi
783
+ fi
784
+ if [ "$HERDR_PROJECTION_ABORT_CLEANUP" = 1 ] \
785
+ && [ "$HERDR_PRESENTATION_ORDER_LOCK_HELD" != 1 ]; then
786
+ if ! spawn_herdr_presentation_order_lock_acquire "${HERDR_PROJECTION_ABORT_SESSION:-}"; then
787
+ echo "warning: herdr presentation focus lock unavailable; retaining the projection journal and refusing concurrent abort cleanup" >&2
788
+ HERDR_PROJECTION_ABORT_CLEANUP=0
789
+ fi
790
+ fi
791
+ if [ "$HERDR_PROJECTION_ABORT_CLEANUP" = 1 ]; then
792
+ HERDR_PROJECTION_ABORT_CLEANUP=0
793
+ fm_backend_herdr_projection_cleanup_exact \
794
+ "$HERDR_PROJECTION_ABORT_SESSION" \
795
+ "$HERDR_PROJECTION_ABORT_TASK_PANE" \
796
+ "$HERDR_PROJECTION_ABORT_SEEDED_PANE" || true
797
+ fi
798
+ if [ "$HERDR_PRESENTATION_ORDER_LOCK_HELD" = 1 ]; then
799
+ HERDR_PRESENTATION_ORDER_LOCK_HELD=0
800
+ fm_lock_release "$HERDR_PRESENTATION_ORDER_LOCK" || true
801
+ fi
802
+ if [ "$ORCA_ABORT_CLEANUP" = 1 ]; then
803
+ ORCA_ABORT_CLEANUP=0
804
+ if [ -n "${ORCA_TERMINAL:-}" ]; then
805
+ fm_backend_kill orca "$ORCA_TERMINAL" 2>/dev/null || true
806
+ fi
807
+ if [ -n "${ORCA_WORKTREE_ID:-}" ]; then
808
+ if ! fm_backend_remove_worktree orca "$ORCA_WORKTREE_ID" 2>/dev/null; then
809
+ if [ "$SPAWN_FRESH_COMMIT_PENDING" = 1 ]; then
810
+ if ! spawn_fresh_commit_rollback; then
811
+ status=1
812
+ fi
813
+ SPAWN_FRESH_COMMIT_PENDING=0
814
+ fi
815
+ mkdir -p "$STATE" 2>/dev/null || true
816
+ if [ -d "$STATE" ]; then
817
+ SPAWN_META_TMP="$STATE/.$ID.meta.orca-recovery.${BASHPID:-$$}"
818
+ {
819
+ echo "window=$W"
820
+ echo "endpoint_task_id=$ID"
821
+ echo "cleanup_recovery=orca"
822
+ echo "worktree=${WT:-}"
823
+ echo "project=$PROJ_ABS"
824
+ echo "harness=$HARNESS"
825
+ echo "kind=$KIND"
826
+ [ -z "${MODE:-}" ] || echo "mode=$MODE"
827
+ [ -z "${YOLO:-}" ] || echo "yolo=$YOLO"
828
+ echo "tasktmp=${TASK_TMP:-}"
829
+ echo "model=${MODEL:-default}"
830
+ echo "effort=${EFFORT:-default}"
831
+ echo "backend=orca"
832
+ echo "orca_worktree_id=$ORCA_WORKTREE_ID"
833
+ [ -z "${ORCA_TERMINAL:-}" ] || echo "terminal=$ORCA_TERMINAL"
834
+ } > "$SPAWN_META_TMP" 2>/dev/null \
835
+ && fm_backlog_atomic_transition publish "$SPAWN_META_TMP" "$STATE/$ID.meta" "task record" "$STATE" \
836
+ || true
837
+ fi
838
+ fi
839
+ fi
840
+ fi
841
+ if [ "$SPAWN_TASK_LOCK_HELD" = 1 ]; then
842
+ SPAWN_TASK_LOCK_HELD=0
843
+ fm_lock_release "$SPAWN_TASK_LOCK" || true
844
+ fi
845
+ if [ "$SPAWN_FRESH_COMMIT_PENDING" = 1 ]; then
846
+ if ! spawn_fresh_commit_rollback; then
847
+ status=1
848
+ fi
849
+ fi
850
+ if [ "$SPAWN_META_LOCK_HELD" = 1 ]; then
851
+ SPAWN_META_LOCK_HELD=0
852
+ fm_lock_release "$SPAWN_META_LOCK" || true
853
+ fi
854
+ if [ "$SPAWN_TASK_SET_LOCK_HELD" = 1 ]; then
855
+ SPAWN_TASK_SET_LOCK_HELD=0
856
+ fm_lock_release "$SPAWN_TASK_SET_LOCK" || true
857
+ fi
858
+ if [ "$SPAWN_CONTROL_LOCK_HELD" = 1 ]; then
859
+ SPAWN_CONTROL_LOCK_HELD=0
860
+ fm_lock_release "$SPAWN_CONTROL_LOCK" || true
861
+ fi
862
+ [ -z "$SPAWN_META_TMP" ] || rm -f "$SPAWN_META_TMP" 2>/dev/null || true
863
+ if [ "$CONFIG_INHERIT_LOCK_HELD" = 1 ]; then
864
+ CONFIG_INHERIT_LOCK_HELD=0
865
+ fm_lock_release "$CONFIG_INHERIT_LOCK" || true
866
+ fi
867
+ return "$status"
868
+ }
869
+ trap spawn_abort_cleanup EXIT
870
+
871
+ # One bounded lock per live Herdr session/socket, shared across all homes.
872
+ # <session> is required so secondmate and primary spawns serialize against the
873
+ # same session without writing any other home's state directory.
874
+ spawn_herdr_presentation_order_lock_acquire() {
875
+ local session=${1:-} attempt lock_path
876
+ [ -n "$session" ] || session=$(fm_backend_herdr_session)
877
+ lock_path=$(fm_backend_herdr_presentation_session_lock_path "$session") || return 1
878
+ HERDR_PRESENTATION_ORDER_LOCK="$lock_path"
879
+ attempt=0
880
+ while [ "$attempt" -lt 50 ]; do
881
+ if fm_lock_try_acquire "$HERDR_PRESENTATION_ORDER_LOCK"; then
882
+ HERDR_PRESENTATION_ORDER_LOCK_HELD=1
883
+ return 0
884
+ fi
885
+ sleep 0.1
886
+ attempt=$((attempt + 1))
887
+ done
888
+ return 1
889
+ }
890
+
891
+ clear_relaunch_harness_wiring() {
892
+ local harness=$1 wt=$2 state=$3 id=$4 token_path token auth_path path
893
+ # The wiring arms above match on harness PREFIXES, because a task launched
894
+ # from a raw command records that command's basename rather than the exact
895
+ # adapter name. The retirement tables are keyed by the exact adapter, so the
896
+ # recorded value is resolved to its adapter first; otherwise a task recorded
897
+ # as, say, `grok-2` would have wiring armed and never retired. An
898
+ # unrecognized value resolves to no adapter, which is also the case in which
899
+ # no wiring was armed to begin with.
900
+ harness=$(fm_control_harness_family "$harness") || harness=
901
+ token_path=$(fm_control_harness_turnend_token_path "$harness" "$state" "$id") || return 1
902
+ token=
903
+ if [ -n "$token_path" ] && [ -f "$token_path" ]; then
904
+ IFS= read -r token < "$token_path" || [ -n "$token" ] || return 1
905
+ fi
906
+ auth_path=$(fm_control_harness_turnend_auth_path "$harness" "$token") || return 1
907
+ if [ -n "$auth_path" ]; then
908
+ rm -f -- "$auth_path" || return 1
909
+ fi
910
+ while IFS= read -r path; do
911
+ [ -n "$path" ] || continue
912
+ rm -f -- "$path" || return 1
913
+ done <<EOF
914
+ $(fm_control_harness_wiring_paths "$harness" "$wt" "$state" "$id")
915
+ EOF
916
+ }
917
+
918
+ spawn_herdr_presentation_order_lock_release() {
919
+ [ "$HERDR_PRESENTATION_ORDER_LOCK_HELD" = 1 ] || return 0
920
+ HERDR_PRESENTATION_ORDER_LOCK_HELD=0
921
+ fm_lock_release "$HERDR_PRESENTATION_ORDER_LOCK" || true
922
+ }
923
+
924
+ # Batch dispatch (see header): when the first positional is an `id=repo` pair, treat every
925
+ # positional as one and spawn each by re-execing this script in single-task mode. We use
926
+ # the FM_ROOT path (not $0) so it works whatever cwd or relative path invoked us, and reuse
927
+ # the single path verbatim. A failed pair is reported and skipped; the rest still launch;
928
+ # exit is non-zero if any pair failed. Single-task invocations never carry an '=' in arg
929
+ # one (task ids are bare slugs), so they fall straight through to the logic below.
930
+ idpart=${POS[0]:-}
931
+ idpart=${idpart%%=*}
932
+ if [ "$RELAUNCH" -eq 1 ] && [ "${#POS[@]}" -gt 0 ] && [ "${POS[0]}" != "$idpart" ]; then
933
+ echo "error: --relaunch is single-task only; relaunch each task explicitly" >&2
934
+ exit 1
935
+ fi
936
+ if [ "${#POS[@]}" -gt 0 ] && [ "${POS[0]}" != "$idpart" ] && case "$idpart" in */*) false ;; *) true ;; esac; then
937
+ if [ "$KIND" != secondmate ] && [ -z "$HARNESS_ARG" ] && [ -f "$CONFIG/crew-dispatch.json" ]; then
938
+ echo "error: config/crew-dispatch.json is active - pass an explicit harness resolved from the dispatch rules (the consultation backstop, so the rules are never silently skipped)." >&2
939
+ exit 1
940
+ fi
941
+ rc=0
942
+ shared_args=()
943
+ [ -z "$HARNESS_ARG" ] || shared_args+=(--harness "$HARNESS_ARG")
944
+ [ -z "$MODEL" ] || shared_args+=(--model "$MODEL")
945
+ [ -z "$EFFORT" ] || shared_args+=(--effort "$EFFORT")
946
+ [ -z "$BACKEND_ARG" ] || shared_args+=(--backend "$BACKEND_ARG")
947
+ # One delivery contract applies to every pair in a batch, exactly like the shared
948
+ # harness. Each pair still re-validates it against its own brief, so a batch
949
+ # spanning several modes is two invocations rather than a silent mixed dispatch.
950
+ [ "$MODE_SET" -eq 0 ] || shared_args+=(--mode "$MODE")
951
+ [ "$YOLO_SET" -eq 0 ] || shared_args+=(--yolo "$YOLO")
952
+ for pair in "${POS[@]}"; do
953
+ case "$pair" in
954
+ *=*) : ;;
955
+ *) echo "error: batch dispatch expects every argument as id=repo; got '$pair'" >&2; rc=2; continue ;;
956
+ esac
957
+ if [ "$KIND" = secondmate ]; then
958
+ echo "error: batch dispatch does not support --secondmate; spawn each secondmate explicitly" >&2
959
+ rc=2
960
+ continue
961
+ elif [ "$KIND" = scout ]; then
962
+ if FM_SPAWN_NO_GUARD=1 "$FM_ROOT/bin/fm-spawn.sh" "${pair%%=*}" "${pair#*=}" "${shared_args[@]+"${shared_args[@]}"}" --scout; then :; else echo "batch: FAILED to spawn ${pair%%=*} (${pair#*=})" >&2; rc=1; fi
963
+ else
964
+ if FM_SPAWN_NO_GUARD=1 "$FM_ROOT/bin/fm-spawn.sh" "${pair%%=*}" "${pair#*=}" "${shared_args[@]+"${shared_args[@]}"}"; then :; else echo "batch: FAILED to spawn ${pair%%=*} (${pair#*=})" >&2; rc=1; fi
965
+ fi
966
+ done
967
+ exit "$rc"
968
+ fi
969
+ ID=${POS[0]}
970
+ fm_task_id_creation_valid "$ID" || { echo "error: invalid task id" >&2; exit 2; }
971
+ if [ -e "$STATE" ] || [ -L "$STATE" ]; then
972
+ fm_backlog_directory_present "$STATE" "state directory" || {
973
+ echo "error: spawn refused: $FM_BACKLOG_TRANSITION_ERROR" >&2
974
+ exit 1
975
+ }
976
+ elif [ "$RELAUNCH" -eq 1 ]; then
977
+ echo "error: spawn refused: state directory does not exist at $STATE" >&2
978
+ exit 1
979
+ fi
980
+ # Role partition: spawning NEW work is MAIN-owned. A relaunch of an existing
981
+ # task is legitimate branch recovery (fm-control drives it through this same
982
+ # entrypoint), so only a fresh spawn refuses the branch actor (contract:
983
+ # bin/fm-lease-lib.sh; no-op in homes without a branch actor).
984
+ # shellcheck source=bin/fm-lease-lib.sh
985
+ . "$SCRIPT_DIR/fm-lease-lib.sh"
986
+ if [ "$RELAUNCH" -ne 1 ]; then
987
+ fm_lease_forbid_branch "new-task spawn (fm-spawn)"
988
+ fi
989
+ if [ "$RELAUNCH" -eq 1 ]; then
990
+ SPAWN_CONTROL_LOCK="$STATE/.control-$ID.lock"
991
+ control_owner=$(cat "$SPAWN_CONTROL_LOCK/pid" 2>/dev/null || true)
992
+ if [ "$control_owner" = "$PPID" ] && fm_pid_alive "$control_owner"; then
993
+ SPAWN_CONTROL_PARENT=1
994
+ elif [ "$(fm_lease_actor)" = branch ]; then
995
+ # Role partition refinement: branch recovery relaunches only through the
996
+ # fm-control transaction that owns the control lock, never by invoking
997
+ # this entrypoint directly (contract: bin/fm-lease-lib.sh).
998
+ echo "error: relaunch (fm-spawn) refused - the supervision branch must relaunch through fm-control" >&2
999
+ exit "$FM_LEASE_REFUSE_EXIT"
1000
+ elif fm_lock_try_acquire "$SPAWN_CONTROL_LOCK"; then
1001
+ SPAWN_CONTROL_LOCK_HELD=1
1002
+ else
1003
+ echo "error: another lifecycle action is already running for task $ID" >&2
1004
+ exit 1
1005
+ fi
1006
+ fi
1007
+ if [ "$RELAUNCH" -eq 0 ]; then
1008
+ mkdir -p "$STATE" || {
1009
+ echo "error: could not create parent state directory" >&2
1010
+ exit 1
1011
+ }
1012
+ fm_backlog_directory_present "$STATE" "state directory" || {
1013
+ echo "error: spawn refused: $FM_BACKLOG_TRANSITION_ERROR" >&2
1014
+ exit 1
1015
+ }
1016
+ # A FRESH spawn changes which tasks this home has, so it must not interleave
1017
+ # with a forced teardown that has already enumerated that set: a record
1018
+ # published inside the enumerate-then-remove window is invisible to the
1019
+ # teardown's per-task preflight but visible to its cleanup, and gets mutated
1020
+ # while never lifecycle-locked (bin/fm-wake-lib.sh's fm_task_set_lock_path
1021
+ # owns the evidence; bin/fm-teardown.sh holds the same lock from enumeration
1022
+ # through cleanup). Taken before this task's own locks, matching the
1023
+ # acquisition order documented there, and held through publication.
1024
+ #
1025
+ # A relaunch is exempt: it republishes a task that already exists, so it is
1026
+ # already covered by that task's control lock, which the teardown preflight
1027
+ # tests.
1028
+ #
1029
+ # Refusing rather than waiting is the fail-closed direction: the home may be
1030
+ # moments from removal, so there is nothing worth waiting for.
1031
+ SPAWN_TASK_SET_LOCK=$(fm_task_set_lock_path "$STATE") || {
1032
+ echo "error: could not resolve the task-set lock for $STATE" >&2
1033
+ exit 1
1034
+ }
1035
+ if ! fm_lock_try_acquire "$SPAWN_TASK_SET_LOCK"; then
1036
+ echo "error: this home's task set is locked by another operation (a forced teardown is enumerating or removing its tasks); refusing to create task $ID rather than racing it" >&2
1037
+ exit 1
1038
+ fi
1039
+ SPAWN_TASK_SET_LOCK_HELD=1
1040
+ fi
1041
+ if [ "$KIND" = secondmate ]; then
1042
+ if spawn_remote_secondmate "$ID"; then
1043
+ exit 0
1044
+ else
1045
+ remote_spawn_rc=$?
1046
+ fi
1047
+ [ "$remote_spawn_rc" -eq 3 ] || exit "$remote_spawn_rc"
1048
+ fi
1049
+ # Backend selection (data/fm-backend-design-d7): explicit --backend, else
1050
+ # FM_BACKEND env, else config/backend, else runtime auto-detection, else
1051
+ # default tmux (fm_backend_name). fm_backend_validate_spawn refuses unknown or
1052
+ # non-spawn-capable backends. The resolved value is
1053
+ # recorded in meta only when it is NOT tmux (fm-teardown.sh and fm-watch.sh's
1054
+ # window_backend/fm_backend_of_meta already treat an absent backend= as tmux),
1055
+ # so the default path's meta stays byte-identical.
1056
+ if [ "$RELAUNCH" -eq 0 ]; then
1057
+ if [ "$BACKEND_SET" -eq 1 ]; then
1058
+ BACKEND=$BACKEND_ARG
1059
+ else
1060
+ BACKEND=$(fm_backend_name)
1061
+ fi
1062
+ fm_backend_validate_spawn "$BACKEND" || exit 1
1063
+ fm_backend_source "$BACKEND" || exit 1
1064
+ if [ "$BACKEND" = orca ] && [ "$KIND" = secondmate ]; then
1065
+ echo "error: backend=orca does not support --secondmate spawns yet" >&2
1066
+ exit 1
1067
+ fi
1068
+ if [ "$BACKEND" = cmux ] && [ "$KIND" = secondmate ]; then
1069
+ echo "error: backend=cmux does not support --secondmate spawns yet" >&2
1070
+ exit 1
1071
+ fi
1072
+ if [ "$BACKEND" = orca ]; then
1073
+ fm_backend_orca_runtime_check || exit 1
1074
+ fi
1075
+ fi
1076
+ SPAWN_TASK_LOCK="$STATE/.spawn-$ID.lock"
1077
+ if ! fm_lock_try_acquire "$SPAWN_TASK_LOCK"; then
1078
+ echo "error: another spawn is already creating task $ID" >&2
1079
+ exit 1
1080
+ fi
1081
+ SPAWN_TASK_LOCK_HELD=1
1082
+ PROJ=
1083
+ ARG3=
1084
+ FIRSTMATE_HOME=
1085
+
1086
+ # --relaunch adoption: every identity axis comes from the task's own validated
1087
+ # durable record, never from the command line, so a relaunch can only ever
1088
+ # re-launch the task it names. The endpoint identity check is the same shared
1089
+ # validation teardown uses, so a malformed, ambiguous, or foreign record
1090
+ # refuses here exactly as it refuses there.
1091
+ RELAUNCH_PRIOR_HARNESS=
1092
+ if [ "$RELAUNCH" -eq 1 ]; then
1093
+ [ "${#POS[@]}" -eq 1 ] || {
1094
+ echo "error: --relaunch takes the task id only; its project or home comes from the task's own record" >&2
1095
+ exit 1
1096
+ }
1097
+ RELAUNCH_META="$STATE/$ID.meta"
1098
+ if [ ! -e "$RELAUNCH_META" ] && [ ! -L "$RELAUNCH_META" ]; then
1099
+ echo "error: --relaunch needs an existing task record; no $RELAUNCH_META" >&2
1100
+ exit 1
1101
+ fi
1102
+ fm_backlog_record_present "$RELAUNCH_META" "task record" "$STATE" || {
1103
+ echo "error: --relaunch refused: $FM_BACKLOG_TRANSITION_ERROR" >&2
1104
+ exit 1
1105
+ }
1106
+ SPAWN_META_LOCK=$(fm_meta_lock_path "$RELAUNCH_META") || exit 1
1107
+ fm_lock_acquire_wait "$SPAWN_META_LOCK"
1108
+ SPAWN_META_LOCK_HELD=1
1109
+ fm_backlog_record_present "$RELAUNCH_META" "task record" "$STATE" || {
1110
+ echo "error: --relaunch refused after locking: $FM_BACKLOG_TRANSITION_ERROR" >&2
1111
+ exit 1
1112
+ }
1113
+ fm_backend_validate_task_endpoint "$RELAUNCH_META" "$ID" || exit 1
1114
+ BACKEND=$FM_BACKEND_VALIDATED_BACKEND
1115
+ RELAUNCH_TARGET=$FM_BACKEND_VALIDATED_TARGET
1116
+ fm_backend_validate_spawn "$BACKEND" || exit 1
1117
+ fm_backend_source "$BACKEND" || exit 1
1118
+ # A relaunch must PROVE the previous agent is gone before it launches another
1119
+ # one into the same endpoint, and only tmux and herdr have a recovery-grade
1120
+ # classifier that can (bin/fm-control-lib.sh owns that capability table).
1121
+ fm_control_backend_state_verified "$BACKEND" || {
1122
+ echo "error: backend '$BACKEND' has no recovery-grade agent-state classifier, so a relaunch cannot prove the previous agent exited; refusing rather than risking two agents in one endpoint" >&2
1123
+ exit 1
1124
+ }
1125
+ RELAUNCH_STATE=$(fm_backend_agent_state "$BACKEND" "$RELAUNCH_TARGET")
1126
+ [ "$RELAUNCH_STATE" = dead ] || {
1127
+ echo "error: task $ID's endpoint reads '$RELAUNCH_STATE'; a relaunch requires a positively agent-free endpoint (stop the agent first with bin/fm-control.sh $ID exit)" >&2
1128
+ exit 1
1129
+ }
1130
+ RELAUNCH_PRIOR_HARNESS=$(fm_meta_get "$RELAUNCH_META" harness)
1131
+ KIND=$(fm_meta_get "$RELAUNCH_META" kind)
1132
+ [ -n "$KIND" ] || KIND=ship
1133
+ MODE=$(fm_meta_get "$RELAUNCH_META" mode)
1134
+ YOLO=$(fm_meta_get "$RELAUNCH_META" yolo)
1135
+ RELAUNCH_WT=$(fm_meta_get "$RELAUNCH_META" worktree)
1136
+ [ -n "$RELAUNCH_WT" ] && [ -d "$RELAUNCH_WT" ] || {
1137
+ echo "error: task $ID's recorded worktree '${RELAUNCH_WT:-none}' is missing; refusing to relaunch without the local copy its work lives in" >&2
1138
+ exit 1
1139
+ }
1140
+ if [ "$KIND" = secondmate ]; then
1141
+ FIRSTMATE_HOME=$(fm_meta_get "$RELAUNCH_META" home)
1142
+ [ -n "$FIRSTMATE_HOME" ] || FIRSTMATE_HOME=$RELAUNCH_WT
1143
+ else
1144
+ PROJ=$(fm_meta_get "$RELAUNCH_META" project)
1145
+ [ -n "$PROJ" ] || {
1146
+ echo "error: task $ID has no recorded project; refusing to relaunch" >&2
1147
+ exit 1
1148
+ }
1149
+ fi
1150
+ if [ "$BACKEND" = herdr ]; then
1151
+ HERDR_SES=$(fm_meta_get "$RELAUNCH_META" herdr_session)
1152
+ HERDR_WORKSPACE_ID=$(fm_meta_get "$RELAUNCH_META" herdr_workspace_id)
1153
+ HERDR_TAB_ID=$(fm_meta_get "$RELAUNCH_META" herdr_tab_id)
1154
+ HERDR_PANE_ID=$(fm_meta_get "$RELAUNCH_META" herdr_pane_id)
1155
+ fi
1156
+ # With no explicit harness, a relaunch reuses the harness already recorded
1157
+ # for this task. It must NOT fall through to the fresh-spawn config
1158
+ # resolution, which would silently move an existing task onto whatever the
1159
+ # crew or secondmate default currently says. Choosing a different harness is
1160
+ # the caller's explicit decision, made with --harness (bin/fm-control.sh
1161
+ # resolves that decision, including a secondmate's durable pin).
1162
+ ARG3=${HARNESS_ARG:-$RELAUNCH_PRIOR_HARNESS}
1163
+ [ -n "$ARG3" ] || {
1164
+ echo "error: task $ID has no recorded harness; pass --harness to relaunch it" >&2
1165
+ exit 1
1166
+ }
1167
+ elif [ "$KIND" = secondmate ]; then
1168
+ case "${POS[1]:-}" in
1169
+ ''|claude|codex|opencode|pi|pi-signed|grok|kimi|cursor|muse)
1170
+ ARG3=${POS[1]:-}
1171
+ ;;
1172
+ *' '*)
1173
+ if [ "${#POS[@]}" -gt 2 ] || [ -d "${POS[1]}" ]; then
1174
+ FIRSTMATE_HOME=${POS[1]}
1175
+ ARG3=${POS[2]:-}
1176
+ else
1177
+ ARG3=${POS[1]}
1178
+ fi
1179
+ ;;
1180
+ *)
1181
+ FIRSTMATE_HOME=${POS[1]}
1182
+ ARG3=${POS[2]:-}
1183
+ ;;
1184
+ esac
1185
+ else
1186
+ PROJ=${POS[1]}
1187
+ ARG3=${POS[2]:-}
1188
+ fi
1189
+ [ -z "$HARNESS_ARG" ] || ARG3=$HARNESS_ARG
1190
+
1191
+ shell_quote() {
1192
+ printf "'"
1193
+ printf '%s' "$1" | sed "s/'/'\\\\''/g"
1194
+ printf "'"
1195
+ }
1196
+
1197
+ resolve_pi_executable() {
1198
+ local candidate dir
1199
+ candidate=$(type -P -- "$1" 2>/dev/null) || return 1
1200
+ [ -x "$candidate" ] || return 1
1201
+ case "$candidate" in
1202
+ /*) printf '%s\n' "$candidate" ;;
1203
+ *)
1204
+ dir=$(cd "$(dirname "$candidate")" 2>/dev/null && pwd -P) || return 1
1205
+ printf '%s/%s\n' "$dir" "$(basename "$candidate")"
1206
+ ;;
1207
+ esac
1208
+ }
1209
+
1210
+ # Pi's CLI surface is version-dependent, so probe the resolved executable's help
1211
+ # before composing the optional regular-TUI flag. An absent or inconclusive probe
1212
+ # omits the flag so older Pi versions can still spawn.
1213
+ pi_supports_tui_mode() {
1214
+ local executable=$1 help
1215
+ help=$("$executable" --help 2>&1) || return 1
1216
+ printf '%s\n' "$help" | grep -Eq -- '(^|[[:space:]])--tui-mode([[:space:]=]|$)'
1217
+ }
1218
+
1219
+ # The verified launch command per adapter. The knowledge half of each adapter
1220
+ # (busy-state source, exit command, dialogs, quirks) lives in the harness-adapters skill.
1221
+ launch_template() {
1222
+ local harness=$1 kind=${2:-ship}
1223
+ # shellcheck disable=SC2016 # single quotes are deliberate: $(cat ...) expands in the crewmate pane, not here
1224
+ case "$harness" in
1225
+ # CLAUDE_CODE_ENABLE_PROMPT_SUGGESTION=false disables claude's interactive
1226
+ # predicted-next-prompt ghost text, which renders as dim/faint text inside an
1227
+ # otherwise-empty composer and would otherwise read like real typed input when
1228
+ # firstmate captures the pane (see the harness-adapters skill). It is a per-launch env
1229
+ # prefix scoped to this firstmate-launched agent; it never touches the captain's
1230
+ # global config. The CLI's --prompt-suggestions flag is print/SDK-mode only and
1231
+ # does NOT suppress the interactive ghost text (verified empirically), so the env
1232
+ # var is the correct control. The dim-aware composer reader in fm-tmux-lib.sh is
1233
+ # the defense-in-depth backstop for any pane this flag cannot reach.
1234
+ claude) printf '%s' 'CLAUDE_CODE_ENABLE_PROMPT_SUGGESTION=false claude --dangerously-skip-permissions __MODELFLAG____EFFORTFLAG__"$(__OPINPUT__ encode launch-brief < __BRIEF__)"' ;;
1235
+ codex)
1236
+ if [ "$kind" = secondmate ]; then
1237
+ printf '%s' 'codex __MODELFLAG____EFFORTFLAG__--dangerously-bypass-approvals-and-sandbox "$(__OPINPUT__ encode launch-brief < __BRIEF__)"'
1238
+ else
1239
+ printf '%s' 'codex __MODELFLAG____EFFORTFLAG__--dangerously-bypass-approvals-and-sandbox -c "notify=[\"bash\",\"-c\",\"touch __TURNEND__\"]" "$(__OPINPUT__ encode launch-brief < __BRIEF__)"'
1240
+ fi
1241
+ ;;
1242
+ opencode) printf '%s' 'OPENCODE_CONFIG_CONTENT='\''{"permission":{"*":"allow"}}'\'' opencode __MODELFLAG__--prompt "$(__OPINPUT__ encode launch-brief < __BRIEF__)"' ;;
1243
+ pi|pi-signed)
1244
+ printf '%s' '__PIBIN____PITUIMODE__'
1245
+ if [ "$kind" = secondmate ]; then
1246
+ printf '%s' ' __MODELFLAG____EFFORTFLAG__-e __PITURNEND__ -e __PIWATCH__ "$(__OPINPUT__ encode launch-brief < __BRIEF__)"'
1247
+ else
1248
+ printf '%s' ' __MODELFLAG____EFFORTFLAG__-e __PIEXT__ "$(__OPINPUT__ encode launch-brief < __BRIEF__)"'
1249
+ fi
1250
+ ;;
1251
+ # grok (Grok Build TUI): a positional prompt starts the supervised interactive
1252
+ # session. --always-approve auto-approves every tool execution (verified: the
1253
+ # crewmate runs fully autonomously, no permission gate), which an unattended
1254
+ # crewmate needs; it is the targeted equivalent of claude's
1255
+ # --dangerously-skip-permissions. grok's turn-end signal does NOT ride the
1256
+ # launch command - it is a Stop-event hook installed below (global hook +
1257
+ # per-task pointer), so the template is identical for ship/scout/secondmate.
1258
+ grok) printf '%s' 'grok --always-approve __MODELFLAG____EFFORTFLAG__"$(__OPINPUT__ encode launch-brief < __BRIEF__)"' ;;
1259
+ # Cursor Agent CLI. --trust suppresses the workspace-trust prompt, which
1260
+ # --yolo does NOT cover and which would otherwise block every spawn, since
1261
+ # each task gets a fresh worktree path cursor has never seen. --yolo is the
1262
+ # --force alias whose TUI label is "Run Everything". --workspace pins the
1263
+ # exact worktree. -w/--worktree is deliberately never passed: it allocates a
1264
+ # SECOND worktree under ~/.cursor/worktrees and would break firstmate's
1265
+ # isolation contract. The binary is resolved rather than named because
1266
+ # `cursor` is not the CLI (the installed names are cursor-agent and the
1267
+ # legacy alias agent), and the foreign primary markers are cleared so an
1268
+ # inherited CLAUDECODE cannot outrank cursor's own marker in a process that
1269
+ # only reads the environment. Cursor exposes no effort flag, so the shared
1270
+ # effort axis is deliberately omitted and stays in task metadata only.
1271
+ cursor) printf '%s' 'env -u CLAUDECODE -u PI_CODING_AGENT -u GROK_AGENT -u FM_PI_HARNESS -u CURSOR_INVOKED_AS __CURSORBIN__ --trust --yolo __MODELFLAG__--workspace __WORKTREE__ "$(__OPINPUT__ encode launch-brief < __BRIEF__)"' ;;
1272
+ # Kimi Code rejects a positional prompt, so it launches bare and receives
1273
+ # only an absolute brief pointer after the TUI readiness gate below.
1274
+ # Its turn-end signal is a globally configured Stop hook plus a guarded
1275
+ # per-task worktree token, so no launch placeholder belongs here.
1276
+ kimi) printf '%s' '__KIMIBIN__ __MODELFLAG__--auto' ;;
1277
+ # muse (Muse Code): a positional prompt starts the supervised interactive
1278
+ # session. --yolo is the single flag that makes a crewmate pane viable: muse
1279
+ # ships approval prompts AND a filesystem/network sandbox ON by default
1280
+ # (--sandbox-network defaults to proxy-only, which refuses outright without a
1281
+ # managed proxy), and it gates a fresh workspace behind a trust dialog. One
1282
+ # --yolo disables approval, disables the sandbox so git and network work, and
1283
+ # trusts the workspace for the run, so no dialog appears on the fresh
1284
+ # per-task worktree (verified, muse 0.1.0-R708.1).
1285
+ # MUSE_EXPERIMENTAL_FOREIGN_PERSONAL_CONTEXT_KILL=on is the privacy control:
1286
+ # muse otherwise loads the OPERATOR's foreign personal rules from ~/.claude
1287
+ # into every run and ships them to Meta-hosted inference, even under an
1288
+ # isolated XDG_CONFIG_HOME. exec mode's --no-foreign-personal-context flag is
1289
+ # NOT accepted by the interactive TUI (it exits with "unexpected argument"),
1290
+ # so this env var is the only control that reaches a pane worker. Verified to
1291
+ # drop the foreign rules_file context block while KEEPING the project's own
1292
+ # AGENTS.md rules, which the crewmate contract depends on.
1293
+ # muse's turn-end signal rides neither the launch command nor a hook: its
1294
+ # plugin engine is off in the default build, so firstmate folds muse's own
1295
+ # session event log instead (bin/fm-busy-lib.sh), bound by the sidecar
1296
+ # written below. Nothing to place in the template for it.
1297
+ # codex, opencode, and kimi are also markerless and share this inherited-marker hazard; changing their verified launch boundaries belongs in follow-up work.
1298
+ muse) printf '%s' 'env -u CLAUDECODE -u PI_CODING_AGENT -u GROK_AGENT -u FM_PI_HARNESS XDG_CONFIG_HOME=__MUSECONFIG__ XDG_DATA_HOME=__MUSEDATA__ MUSE_EXPERIMENTAL_FOREIGN_PERSONAL_CONTEXT_KILL=on __MUSEBIN__ --yolo __MODELFLAG____EFFORTFLAG__"$(__OPINPUT__ encode launch-brief < __BRIEF__)"' ;;
1299
+ *) return 1 ;;
1300
+ esac
1301
+ }
1302
+
1303
+ case "$ARG3" in
1304
+ *' '*) # raw launch command (unverified-adapter escape hatch)
1305
+ LAUNCH=$ARG3
1306
+ HARNESS=""
1307
+ for word in $LAUNCH; do
1308
+ case "$word" in [A-Za-z_]*=*) continue ;; *) HARNESS=$(basename "$word"); break ;; esac
1309
+ done
1310
+ ;;
1311
+ '')
1312
+ # No explicit harness: resolve from config. A secondmate AGENT launches on the
1313
+ # secondmate harness (config/secondmate-harness -> config/crew-harness -> own);
1314
+ # every other kind uses the crew harness only when no dispatch profile file is
1315
+ # active. Resolving here on every spawn is what makes the split DURABLE - a
1316
+ # respawn (recovery, /updatefirstmate, restart) re-resolves, so
1317
+ # config/secondmate-harness keeps governing secondmate launches across restarts.
1318
+ # The launch_template lookup below is the unverified-adapter guard for both
1319
+ # kinds: a harness with no template aborts the spawn.
1320
+ if [ "$KIND" = secondmate ]; then
1321
+ HARNESS=$("$FM_ROOT/bin/fm-harness.sh" secondmate)
1322
+ harness_src='config/secondmate-harness (falling back to config/crew-harness)'
1323
+ else
1324
+ if [ -f "$CONFIG/crew-dispatch.json" ]; then
1325
+ echo "error: config/crew-dispatch.json is active - pass an explicit harness resolved from the dispatch rules (the consultation backstop, so the rules are never silently skipped)." >&2
1326
+ exit 1
1327
+ fi
1328
+ HARNESS=$("$FM_ROOT/bin/fm-harness.sh" crew)
1329
+ harness_src='config/crew-harness'
1330
+ fi
1331
+ LAUNCH=$(launch_template "$HARNESS" "$KIND") || { echo "error: no launch template for harness '$HARNESS' (from $harness_src or detection); pass a raw launch command to use an unverified adapter" >&2; exit 1; }
1332
+ ;;
1333
+ *)
1334
+ HARNESS=$ARG3
1335
+ LAUNCH=$(launch_template "$HARNESS" "$KIND") || { echo "error: unknown harness '$HARNESS'; pass a raw launch command to use an unverified adapter" >&2; exit 1; }
1336
+ ;;
1337
+ esac
1338
+
1339
+ # muse is verified as a CREWMATE/SCOUT adapter only. A secondmate is a firstmate
1340
+ # instance, so it needs a primary supervision protocol; muse has none, and its
1341
+ # Claude-compatible hook dialect explicitly rejects the model-reawakening and
1342
+ # asyncRewake handlers that firstmate's primary turn-end supervision is built on
1343
+ # (muse 0.1.0-R708.1). Refusing here keeps that gap loud instead of standing up a
1344
+ # secondmate whose supervision cycle could never be armed.
1345
+ if [ "$KIND" = secondmate ] && [ "$HARNESS" = muse ]; then
1346
+ echo "error: muse is a verified crewmate/scout adapter only and cannot run a secondmate; it has no primary supervision protocol. Select a harness verified for secondmates." >&2
1347
+ exit 1
1348
+ fi
1349
+
1350
+ case "$HARNESS" in
1351
+ pi|pi-signed)
1352
+ PI_BIN=$(resolve_pi_executable "$HARNESS") || {
1353
+ echo "error: $HARNESS executable not found on PATH; install it or select a different verified harness" >&2
1354
+ exit 1
1355
+ }
1356
+ PI_TUI_MODE=
1357
+ if pi_supports_tui_mode "$PI_BIN"; then
1358
+ PI_TUI_MODE=' --tui-mode regular'
1359
+ fi
1360
+ LAUNCH=${LAUNCH//__PITUIMODE__/$PI_TUI_MODE}
1361
+ LAUNCH="FM_PI_HARNESS=$HARNESS $LAUNCH"
1362
+ ;;
1363
+ cursor)
1364
+ # `cursor` is not the CLI name, and the legacy alias `agent` is far too
1365
+ # generic to launch on its name alone, so resolution runs through the
1366
+ # verified owner rather than a bare command lookup. Refusing here keeps a
1367
+ # missing install a loud spawn refusal instead of a pane that dies with a
1368
+ # command-not-found the supervisor would read as a wedged worker.
1369
+ CURSOR_BIN=$(fm_cursor_resolve_binary) || exit 1
1370
+ if [ -n "$MODEL" ] && [ "$MODEL" != default ]; then
1371
+ if CURSOR_MODELS=$(fm_cursor_list_models "$CURSOR_BIN"); then
1372
+ if ! printf '%s\n' "$CURSOR_MODELS" | fm_cursor_catalog_has_model "$MODEL"; then
1373
+ echo "error: Cursor model '$MODEL' is not available from '$CURSOR_BIN --list-models'; choose an id listed by that command or omit --model" >&2
1374
+ exit 1
1375
+ fi
1376
+ fi
1377
+ fi
1378
+ ;;
1379
+ esac
1380
+
1381
+ # config/secondmate-harness may carry optional model/effort tokens alongside the
1382
+ # harness ("<harness> [<model>] [<effort>]"). They apply only when this is a
1383
+ # --secondmate spawn and no explicit per-spawn harness/raw launch was supplied, so
1384
+ # the harness itself came from the secondmate config fallback chain. Resolving
1385
+ # here on every spawn makes the pin durable across respawns. Precedence: explicit
1386
+ # --model/--effort flags still win over the file's tokens.
1387
+ if [ "$KIND" = secondmate ] && [ -z "$ARG3" ]; then
1388
+ if [ "$MODEL_SET" -eq 0 ]; then
1389
+ SM_MODEL=$("$SCRIPT_DIR/fm-harness.sh" secondmate-model)
1390
+ [ -z "$SM_MODEL" ] || MODEL=$SM_MODEL
1391
+ fi
1392
+ if [ "$EFFORT_SET" -eq 0 ]; then
1393
+ SM_EFFORT=$("$SCRIPT_DIR/fm-harness.sh" secondmate-effort)
1394
+ if [ -n "$SM_EFFORT" ]; then
1395
+ case "$SM_EFFORT" in
1396
+ low|medium|high|xhigh|max) EFFORT=$SM_EFFORT ;;
1397
+ *) echo "warning: config/secondmate-harness effort token '$SM_EFFORT' is not one of low, medium, high, xhigh, max; ignoring" >&2 ;;
1398
+ esac
1399
+ fi
1400
+ fi
1401
+ fi
1402
+
1403
+ secondmate_registry_value() {
1404
+ secondmate_registry_field "$DATA/secondmates.md" "$1" "$2"
1405
+ }
1406
+
1407
+ resolve_kimi_binary() {
1408
+ local candidate dir fallback
1409
+ candidate=$(command -v kimi 2>/dev/null || true)
1410
+ if [ -n "$candidate" ] && [ -x "$candidate" ]; then
1411
+ case "$candidate" in
1412
+ /*) printf '%s\n' "$candidate"; return 0 ;;
1413
+ *)
1414
+ dir=$(cd "$(dirname "$candidate")" 2>/dev/null && pwd -P) || dir=
1415
+ if [ -n "$dir" ]; then
1416
+ printf '%s/%s\n' "$dir" "$(basename "$candidate")"
1417
+ return 0
1418
+ fi
1419
+ ;;
1420
+ esac
1421
+ fi
1422
+ fallback="${HOME:-}/.kimi-code/bin/kimi"
1423
+ if [ -n "${HOME:-}" ] && [ -x "$fallback" ]; then
1424
+ printf '%s\n' "$fallback"
1425
+ return 0
1426
+ fi
1427
+ echo "error: kimi executable not found; searched PATH for 'kimi' and fallback '$fallback'" >&2
1428
+ return 1
1429
+ }
1430
+
1431
+ resolve_muse_binary() {
1432
+ local candidate dir
1433
+ candidate=$(command -v muse 2>/dev/null || true)
1434
+ if [ -n "$candidate" ] && [ -x "$candidate" ]; then
1435
+ case "$candidate" in
1436
+ /*) printf '%s\n' "$candidate"; return 0 ;;
1437
+ *)
1438
+ dir=$(cd "$(dirname "$candidate")" 2>/dev/null && pwd -P) || dir=
1439
+ if [ -n "$dir" ]; then
1440
+ printf '%s/%s\n' "$dir" "$(basename "$candidate")"
1441
+ return 0
1442
+ fi
1443
+ ;;
1444
+ esac
1445
+ fi
1446
+ echo "error: muse executable not found on PATH; install Muse Code or select a different verified harness" >&2
1447
+ return 1
1448
+ }
1449
+
1450
+ # muse_credential_present: 0 when a launched muse pane can reach its provider
1451
+ # without an interactive login. muse offers exactly two credential paths
1452
+ # (verified, muse 0.1.0-R708.1): the META_API_KEY environment variable, which
1453
+ # always takes priority, and a stored credential written by `muse auth set` or
1454
+ # `muse login` into <config>/muse/auth.json. This is a PREFLIGHT rather than a
1455
+ # rendered-screen check because an unauthenticated pane does not exit - it sits
1456
+ # on an OAuth device-code prompt ("Sign in at this page ... Waiting for
1457
+ # approval...") waiting for a human who is not there, which would look to
1458
+ # supervision like a wedged worker rather than a missing credential.
1459
+ muse_worker_meta_api_key_present() {
1460
+ local session worker_env
1461
+ [ "$BACKEND" = tmux ] || return 1
1462
+ if [ -n "${TMUX:-}" ]; then
1463
+ session=$(tmux display-message -p '#S' 2>/dev/null) || return 1
1464
+ else
1465
+ tmux has-session -t firstmate 2>/dev/null || return 1
1466
+ session=firstmate
1467
+ fi
1468
+ worker_env=$(tmux show-environment -t "$session" META_API_KEY 2>/dev/null) || return 1
1469
+ case "$worker_env" in
1470
+ META_API_KEY=?*) return 0 ;;
1471
+ esac
1472
+ return 1
1473
+ }
1474
+
1475
+ muse_credential_present() {
1476
+ local auth=$1
1477
+ [ -s "$auth" ] || muse_worker_meta_api_key_present
1478
+ }
1479
+
1480
+ model_flag_for_harness() {
1481
+ local harness=$1 model=$2
1482
+ [ -n "$model" ] && [ "$model" != default ] || return 0
1483
+ case "$harness" in
1484
+ claude|codex|opencode|pi|pi-signed|grok|kimi|cursor|muse)
1485
+ printf -- '--model %s ' "$(shell_quote "$model")"
1486
+ ;;
1487
+ esac
1488
+ }
1489
+
1490
+ effort_flag_for_harness() {
1491
+ local harness=$1 effort=$2
1492
+ [ -n "$effort" ] && [ "$effort" != default ] || return 0
1493
+ case "$harness" in
1494
+ claude)
1495
+ case "$effort" in
1496
+ low|medium|high|xhigh|max) printf -- '--effort %s ' "$(shell_quote "$effort")" ;;
1497
+ esac
1498
+ ;;
1499
+ codex)
1500
+ # The installed codex config schema uses model_reasoning_effort, and the
1501
+ # bundled model catalog advertises low|medium|high|xhigh. Omit max rather
1502
+ # than passing an unsupported value.
1503
+ case "$effort" in
1504
+ low|medium|high|xhigh) printf -- '-c %s ' "$(shell_quote "model_reasoning_effort=\"$effort\"")" ;;
1505
+ esac
1506
+ ;;
1507
+ grok)
1508
+ # grok exposes both --effort and --reasoning-effort; firstmate's profile
1509
+ # axis is the reasoning knob. As of grok 0.2.99, --reasoning-effort accepts
1510
+ # only low|medium|high and rejects both xhigh and max, so omit those rather
1511
+ # than passing a known-bad value.
1512
+ case "$effort" in
1513
+ low|medium|high) printf -- '--reasoning-effort %s ' "$(shell_quote "$effort")" ;;
1514
+ esac
1515
+ ;;
1516
+ pi|pi-signed)
1517
+ # Pi 0.80.6 accepts the full shared effort vocabulary, including max, through
1518
+ # its --thinking flag.
1519
+ case "$effort" in
1520
+ low|medium|high|xhigh|max) printf -- '--thinking %s ' "$(shell_quote "$effort")" ;;
1521
+ esac
1522
+ ;;
1523
+ muse)
1524
+ # muse 0.1.0-R708.1 --reasoning-effort accepts none|minimal|low|medium|
1525
+ # high|xhigh|ultra and defaults to high, so low..xhigh map straight across.
1526
+ # ultra is muse's max-CLASS level, so firstmate's max maps onto it - but
1527
+ # only ever as an EXPLICIT captain choice, never as a fallback, because
1528
+ # AGENTS.md section 4 forbids selecting max without captain preference and
1529
+ # the omitted effort here leaves muse on its own high default. muse's extra
1530
+ # none/minimal levels sit below firstmate's shared vocabulary and are
1531
+ # deliberately unreachable rather than remapped onto low.
1532
+ case "$effort" in
1533
+ low|medium|high|xhigh) printf -- '--reasoning-effort %s ' "$(shell_quote "$effort")" ;;
1534
+ max) printf -- '--reasoning-effort %s ' "$(shell_quote ultra)" ;;
1535
+ esac
1536
+ ;;
1537
+ # opencode's interactive `opencode --prompt` launch has a verified --model
1538
+ # flag but no verified effort flag. Its `opencode run --variant` flag belongs
1539
+ # to a different, non-interactive launch mode, so fm-spawn does not pass it.
1540
+ # kimi likewise has no reasoning-effort flag; the requested axis stays in
1541
+ # task metadata but never reaches the launch command. Cursor encodes effort
1542
+ # in model ids such as cursor-grok-4.5-high, so it also receives no separate
1543
+ # effort flag.
1544
+ esac
1545
+ }
1546
+
1547
+ case "$LAUNCH" in
1548
+ *__MUSEBIN__*)
1549
+ MUSE_BIN=$(resolve_muse_binary) || exit 1
1550
+ MUSE_CONFIG_HOME=$(resolve_directory_input XDG_CONFIG_HOME "${XDG_CONFIG_HOME:-${HOME:-}/.config}") || exit 1
1551
+ MUSE_DATA_HOME=$(resolve_directory_input XDG_DATA_HOME "${XDG_DATA_HOME:-${HOME:-}/.local/share}") || exit 1
1552
+ MUSE_AUTH_FILE="$MUSE_CONFIG_HOME/muse/auth.json"
1553
+ if ! muse_credential_present "$MUSE_AUTH_FILE"; then
1554
+ if [ -n "${META_API_KEY:-}" ]; then
1555
+ echo "error: muse has no worker-reachable credential; META_API_KEY is set for fm-spawn but cannot be proven present in the $BACKEND worker environment. Store the fleet credential at '$MUSE_AUTH_FILE' with 'muse login' or 'muse auth set --api-key-stdin'. The secret will not be copied into the launch command." >&2
1556
+ else
1557
+ echo "error: muse has no worker-reachable credential; META_API_KEY cannot be proven present in the $BACKEND worker environment and '$MUSE_AUTH_FILE' is absent or empty. Store the fleet credential with 'muse login' or 'muse auth set --api-key-stdin'." >&2
1558
+ fi
1559
+ exit 1
1560
+ fi
1561
+ LAUNCH=${LAUNCH//__MUSEBIN__/$(shell_quote "$MUSE_BIN")}
1562
+ LAUNCH=${LAUNCH//__MUSECONFIG__/$(shell_quote "$MUSE_CONFIG_HOME")}
1563
+ LAUNCH=${LAUNCH//__MUSEDATA__/$(shell_quote "$MUSE_DATA_HOME")}
1564
+ ;;
1565
+ esac
1566
+
1567
+ case "$LAUNCH" in
1568
+ *__KIMIBIN__*)
1569
+ KIMI_BIN=$(resolve_kimi_binary) || exit 1
1570
+ LAUNCH=${LAUNCH//__KIMIBIN__/$(shell_quote "$KIMI_BIN")}
1571
+ if [ "$KIND" != secondmate ]; then
1572
+ "$FM_ROOT/bin/fm-kimi-turnend-hook.sh" install || {
1573
+ echo "error: refusing Kimi spawn because the global turn-end hook could not be installed safely" >&2
1574
+ exit 1
1575
+ }
1576
+ fi
1577
+ ;;
1578
+ esac
1579
+
1580
+ json_escape() {
1581
+ printf '%s' "$1" | sed 's/\\/\\\\/g; s/"/\\"/g'
1582
+ }
1583
+
1584
+ resolved_existing_dir() {
1585
+ local path=$1
1586
+ [ -d "$path" ] || { echo "error: firstmate home does not exist or is not a directory: $path" >&2; return 1; }
1587
+ cd "$path" && pwd -P
1588
+ }
1589
+
1590
+ resolve_project_dir_arg() {
1591
+ local path=$1
1592
+ case "$path" in
1593
+ projects/*) printf '%s/%s\n' "$PROJECTS" "${path#projects/}" ;;
1594
+ *) printf '%s\n' "$path" ;;
1595
+ esac
1596
+ }
1597
+
1598
+ path_is_ancestor_of() {
1599
+ local ancestor=$1 path=$2
1600
+ [ -n "$ancestor" ] || return 1
1601
+ [ -n "$path" ] || return 1
1602
+ [ "$ancestor" != "$path" ] || return 1
1603
+ case "$path" in
1604
+ "$ancestor"/*) return 0 ;;
1605
+ esac
1606
+ return 1
1607
+ }
1608
+
1609
+ validate_firstmate_home_for_spawn() {
1610
+ local id=$1 home=$2 abs_home abs_active_home abs_root marker_id
1611
+ abs_home=$(resolved_existing_dir "$home") || return 1
1612
+ abs_active_home=$(resolved_existing_dir "$FM_HOME")
1613
+ abs_root=$(resolved_existing_dir "$FM_ROOT")
1614
+ if [ "$abs_home" = "/" ]; then
1615
+ echo "error: secondmate home cannot be the filesystem root: $home" >&2
1616
+ return 1
1617
+ fi
1618
+ if [ "$abs_home" = "$abs_active_home" ]; then
1619
+ echo "error: secondmate home cannot be the active firstmate home: $home" >&2
1620
+ return 1
1621
+ fi
1622
+ if [ "$abs_home" = "$abs_root" ]; then
1623
+ echo "error: secondmate home cannot be the firstmate repo: $home" >&2
1624
+ return 1
1625
+ fi
1626
+ if path_is_ancestor_of "$abs_active_home" "$abs_home"; then
1627
+ echo "error: secondmate home cannot be inside the active firstmate home: $home" >&2
1628
+ return 1
1629
+ fi
1630
+ if path_is_ancestor_of "$abs_root" "$abs_home"; then
1631
+ echo "error: secondmate home cannot be inside the firstmate repo: $home" >&2
1632
+ return 1
1633
+ fi
1634
+ if path_is_ancestor_of "$abs_home" "$abs_active_home"; then
1635
+ echo "error: secondmate home cannot be an ancestor of the active firstmate home: $home" >&2
1636
+ return 1
1637
+ fi
1638
+ if path_is_ancestor_of "$abs_home" "$abs_root"; then
1639
+ echo "error: secondmate home cannot be an ancestor of the firstmate repo: $home" >&2
1640
+ return 1
1641
+ fi
1642
+ validate_firstmate_operational_dirs "$abs_home" "$abs_active_home" "$abs_root" || return 1
1643
+ if [ ! -f "$abs_home/$SUB_HOME_MARKER" ]; then
1644
+ echo "error: firstmate home $home is not a seeded secondmate home" >&2
1645
+ return 1
1646
+ fi
1647
+ marker_id=$(cat "$abs_home/$SUB_HOME_MARKER" 2>/dev/null || true)
1648
+ if [ "$marker_id" != "$id" ]; then
1649
+ echo "error: firstmate home $home is marked for secondmate ${marker_id:-unknown}, expected $id" >&2
1650
+ return 1
1651
+ fi
1652
+ if [ ! -f "$abs_home/AGENTS.md" ]; then
1653
+ echo "error: $home is not a firstmate home (missing AGENTS.md)" >&2
1654
+ return 1
1655
+ fi
1656
+ if [ ! -d "$abs_home/bin" ]; then
1657
+ echo "error: $home is not a firstmate home (missing bin/)" >&2
1658
+ return 1
1659
+ fi
1660
+ printf '%s\n' "$abs_home"
1661
+ }
1662
+
1663
+ validate_firstmate_operational_dirs() {
1664
+ local abs_home=$1 abs_active_home=$2 abs_root=$3 name dir abs_dir
1665
+ for name in data state config projects; do
1666
+ dir="$abs_home/$name"
1667
+ if [ -L "$dir" ] && [ ! -e "$dir" ]; then
1668
+ echo "error: secondmate $name directory must resolve inside the secondmate home: $dir" >&2
1669
+ return 1
1670
+ fi
1671
+ if [ -d "$dir" ]; then
1672
+ abs_dir=$(cd "$dir" && pwd -P)
1673
+ elif [ -e "$dir" ]; then
1674
+ echo "error: secondmate $name path is not a directory: $dir" >&2
1675
+ return 1
1676
+ else
1677
+ abs_dir="$abs_home/$name"
1678
+ fi
1679
+ if ! path_is_ancestor_of "$abs_home" "$abs_dir"; then
1680
+ echo "error: secondmate $name directory must resolve inside the secondmate home: $dir" >&2
1681
+ return 1
1682
+ fi
1683
+ if [ "$abs_dir" = "$abs_active_home" ] || path_is_ancestor_of "$abs_active_home" "$abs_dir"; then
1684
+ echo "error: secondmate $name directory cannot be inside the active firstmate home: $dir" >&2
1685
+ return 1
1686
+ fi
1687
+ if [ "$abs_dir" = "$abs_root" ] || path_is_ancestor_of "$abs_root" "$abs_dir"; then
1688
+ echo "error: secondmate $name directory cannot be inside the firstmate repo: $dir" >&2
1689
+ return 1
1690
+ fi
1691
+ done
1692
+ }
1693
+
1694
+ if [ "$KIND" = secondmate ]; then
1695
+ if [ -z "$FIRSTMATE_HOME" ] && { [ -e "$STATE/$ID.meta" ] || [ -L "$STATE/$ID.meta" ]; }; then
1696
+ fm_backlog_record_present "$STATE/$ID.meta" "task record" "$STATE" || {
1697
+ echo "error: secondmate task record is unsafe: $FM_BACKLOG_TRANSITION_ERROR" >&2
1698
+ exit 1
1699
+ }
1700
+ FIRSTMATE_HOME=$(grep '^home=' "$STATE/$ID.meta" | cut -d= -f2- || true)
1701
+ fi
1702
+ if [ -z "$FIRSTMATE_HOME" ]; then
1703
+ FIRSTMATE_HOME=$(secondmate_registry_value "$ID" home || true)
1704
+ fi
1705
+ fi
1706
+
1707
+ if [ "$KIND" = secondmate ]; then
1708
+ [ -n "$FIRSTMATE_HOME" ] || { echo "error: no firstmate home supplied or registered for $ID" >&2; exit 1; }
1709
+ PROJ_ABS=$(validate_firstmate_home_for_spawn "$ID" "$FIRSTMATE_HOME")
1710
+ if [ -e "$DATA/secondmates.md" ] || [ -L "$DATA/secondmates.md" ]; then
1711
+ if ! secondmate_registry_validate_bindings "$DATA/secondmates.md" resolve_path "$ID" "$FIRSTMATE_HOME"; then
1712
+ echo "error: $SECONDMATE_REGISTRY_ERROR" >&2
1713
+ exit 1
1714
+ fi
1715
+ SECONDMATE_PROJECTS=$SECONDMATE_REGISTRY_MATCH_PROJECTS
1716
+ fi
1717
+ WT="$PROJ_ABS"
1718
+ # Local-HEAD sync: before launch, fast-forward this secondmate's worktree to the
1719
+ # PRIMARY checkout's current default-branch commit, so a freshly spawned or
1720
+ # recovery-respawned secondmate always runs the primary's version (AGENTS.md
1721
+ # spawn section). Purely local - no fetch: the home is a worktree of this same
1722
+ # repo and already holds the commit. ff-only and guarded; a dirty, diverged, or
1723
+ # wrong-branch home is left untouched and launches as-is. The agent re-reads
1724
+ # AGENTS.md fresh on launch, so no nudge is needed here.
1725
+ if sm_primary_head=$(primary_head_commit "$FM_ROOT"); then
1726
+ sm_ff_out=$(ff_target "$PROJ_ABS" "secondmate $ID" "$sm_primary_head" yes yes 2>&1 || true)
1727
+ case "$sm_ff_out" in
1728
+ *': skipped:'*)
1729
+ sm_ff_line=$(first_line "$sm_ff_out")
1730
+ sm_ff_prefix="secondmate $ID: skipped: "
1731
+ sm_ff_reason=${sm_ff_line#"$sm_ff_prefix"}
1732
+ echo "warning: secondmate $ID sync skipped before launch: $sm_ff_reason" >&2
1733
+ ;;
1734
+ esac
1735
+ else
1736
+ echo "warning: secondmate $ID sync skipped before launch: primary default-branch commit cannot be resolved" >&2
1737
+ fi
1738
+ mkdir -p "$PROJ_ABS/state" || {
1739
+ echo "error: could not create secondmate state directory for $PROJ_ABS" >&2
1740
+ exit 1
1741
+ }
1742
+ if [ "${FM_SKIP_SECONDMATE_INHERIT:-0}" != 1 ]; then
1743
+ CONFIG_INHERIT_LOCK=$(fm_config_inherit_lock_path "$PROJ_ABS") || {
1744
+ echo "error: could not resolve secondmate inheritance lock for $PROJ_ABS" >&2
1745
+ exit 1
1746
+ }
1747
+ if ! fm_lock_acquire_wait "$CONFIG_INHERIT_LOCK"; then
1748
+ echo "error: could not acquire secondmate inheritance lock for $PROJ_ABS" >&2
1749
+ exit 1
1750
+ fi
1751
+ CONFIG_INHERIT_LOCK_HELD=1
1752
+ # Inheritance propagation: push the primary-authoritative live-safe local inheritance
1753
+ # surface into this secondmate home (fm-config-inherit-lib.sh).
1754
+ FM_CONFIG_INHERIT_LIVE=1 \
1755
+ propagate_secondmate_inheritance "$FM_HOME" "$PROJ_ABS" "$CONFIG" "$DATA" \
1756
+ || echo "warning: secondmate $ID inheritance failed for $PROJ_ABS" >&2
1757
+ fi
1758
+ if [ -f "$PROJ_ABS/data/charter.md" ]; then
1759
+ BRIEF="$PROJ_ABS/data/charter.md"
1760
+ else
1761
+ BRIEF="$DATA/$ID/brief.md"
1762
+ fi
1763
+ else
1764
+ PROJ_ABS="$(cd "$(resolve_project_dir_arg "$PROJ")" && pwd)"
1765
+ WT=""
1766
+ BRIEF="$DATA/$ID/brief.md"
1767
+ fi
1768
+ [ -f "$BRIEF" ] || { echo "error: task $ID has no brief at inaccessible data path $BRIEF" >&2; exit 1; }
1769
+
1770
+ delivery_rigor_rank() { # <mode> -> 3 (most rigor) .. 1 (least); 0 = not a task mode
1771
+ case "$1" in
1772
+ no-mistakes) echo 3 ;;
1773
+ direct-PR) echo 2 ;;
1774
+ local-only) echo 1 ;;
1775
+ *) echo 0 ;;
1776
+ esac
1777
+ }
1778
+
1779
+ # Brief/spawn delivery agreement, checked before any endpoint exists.
1780
+ # fm-brief.sh records a ship brief's mode as a fixed "Delivery contract: mode=<mode>"
1781
+ # line. A spawn that disagrees would launch a worker whose instructions and whose
1782
+ # recorded task delivery differ, which is the exact drift this contract prevents.
1783
+ if [ "$KIND" = ship ]; then
1784
+ PROJ_NAME=$(basename "$PROJ_ABS")
1785
+ BRIEF_MODE=$(sed -n 's/^Delivery contract: mode=\([^ ]*\).*$/\1/p' "$BRIEF" | head -n 1)
1786
+ if [ -z "$BRIEF_MODE" ]; then
1787
+ echo "warning: $BRIEF records no delivery contract line (scaffolded before ship briefs recorded one); launching on the explicit --mode $MODE - confirm its definition of done matches" >&2
1788
+ elif [ "$BRIEF_MODE" != "$MODE" ]; then
1789
+ echo "error: delivery mismatch for $ID: the brief says mode=$BRIEF_MODE but this spawn passed --mode $MODE; correct the flag or re-scaffold the brief so the worker's instructions and the task record agree" >&2
1790
+ exit 1
1791
+ fi
1792
+ # The registry holds the captain's standing posture, so dropping below it is
1793
+ # allowed (a current explicit captain instruction wins) but never silent. An
1794
+ # unregistered project resolves to the same no-mistakes standing default, which
1795
+ # is why the notice names the standing posture rather than the registry line. A
1796
+ # conditional policy is excluded: both of its legs are legitimate classifications.
1797
+ STANDING_MODE=$("$FM_ROOT/bin/fm-project-mode.sh" --raw "$PROJ_NAME" 2>/dev/null | cut -d' ' -f1) || STANDING_MODE=
1798
+ if [ -n "$STANDING_MODE" ] && [ "$STANDING_MODE" != no-mistakes-prod-only ] \
1799
+ && [ "$(delivery_rigor_rank "$MODE")" -lt "$(delivery_rigor_rank "$STANDING_MODE")" ]; then
1800
+ echo "notice: $ID ships mode=$MODE while the standing posture for $PROJ_NAME is $STANDING_MODE - less rigor than the captain's standing posture; proceed only on a current explicit captain instruction or an intake judgment you can state" >&2
1801
+ fi
1802
+ fi
1803
+
1804
+ BRIEF_DIR_REAL=$(cd "$(dirname "$BRIEF")" && pwd -P)
1805
+ BRIEF_REAL="$BRIEF_DIR_REAL/$(basename "$BRIEF")"
1806
+
1807
+ # PROJ_ABS can still carry a symlinked path component (e.g. macOS's /tmp ->
1808
+ # /private/tmp) when it came from the ship/scout branch's logical `pwd` above.
1809
+ # Every backend's own current-path read (tmux's pane_current_path, herdr's
1810
+ # foreground_cwd, zellij/cmux's active pwd probe against the live shell) can
1811
+ # report the OS-level, physically-resolved cwd, so comparing it against a
1812
+ # still-symlinked PROJ_ABS can misfire both ways: false-negative (the poll
1813
+ # below never notices the pane left the project) or false-positive (the
1814
+ # isolation guard refuses a spawn that never actually tangled). Canonicalize
1815
+ # once here so every downstream comparison uses the same physical form
1816
+ # (docs/herdr-backend.md "Known gaps").
1817
+ PROJ_ABS_REAL=$(cd "$PROJ_ABS" 2>/dev/null && pwd -P) || PROJ_ABS_REAL="$PROJ_ABS"
1818
+
1819
+ real_path_or_raw() { # <path>
1820
+ local path=$1 real
1821
+ if real=$(cd "$path" 2>/dev/null && pwd -P); then
1822
+ printf '%s\n' "$real"
1823
+ else
1824
+ printf '%s\n' "$path"
1825
+ fi
1826
+ }
1827
+
1828
+ # Session-provider container-ensure + task creation. tmux stays exactly as P1
1829
+ # left it (same session-name / new-window sequence, see bin/backends/tmux.sh);
1830
+ # a herdr spawn goes through the version-gated, workspace-per-HOME,
1831
+ # tab-per-task sequence in bin/backends/herdr.sh instead (D4/D5 as refined by
1832
+ # docs/herdr-backend.md's "workspace-per-home" pass, AGENTS.md task
1833
+ # herdr-sm-spaces-k4). Both branches converge on the same $T ("target") string
1834
+ # that every downstream operation (send/capture/kill) already treats as opaque
1835
+ # per-backend routing (fm_backend_resolve_selector).
1836
+ validate_spawn_worktree() { # <source> <inspect-target>
1837
+ local source=$1 inspect_target=$2 wt_real proj_real wt_top wt_top_real
1838
+ wt_real=
1839
+ if ! wt_real=$(cd "$WT" 2>/dev/null && pwd -P); then
1840
+ wt_real=
1841
+ fi
1842
+ proj_real=$PROJ_ABS_REAL
1843
+ wt_top=$(git -C "$WT" rev-parse --show-toplevel 2>/dev/null || true)
1844
+ wt_top_real=
1845
+ if ! wt_top_real=$(cd "$wt_top" 2>/dev/null && pwd -P); then
1846
+ wt_top_real=
1847
+ fi
1848
+ if [ -z "$wt_real" ] || [ -z "$wt_top_real" ] || [ "$wt_real" != "$wt_top_real" ] || [ "$wt_real" = "$proj_real" ]; then
1849
+ echo "error: $source did not yield an isolated worktree (resolved '$WT'; worktree root '${wt_top:-none}'; primary '$PROJ_ABS'); refusing to launch to avoid tangling the primary checkout. Inspect target $inspect_target" >&2
1850
+ exit 1
1851
+ fi
1852
+ }
1853
+
1854
+ # A pooled slot whose only deviation is a submodule gitlink is stale, not dirty:
1855
+ # an earlier refresh moved the superproject and left the submodule checkout on
1856
+ # the pin the previous base recorded. The refusal still stands and this gate
1857
+ # never touches the slot; it only names the cause, because "is not clean" while
1858
+ # the operator's own `git status` reads clean gives neither a cause nor a remedy.
1859
+ # A pin is only reported as stale when the commit the slot holds is already
1860
+ # contained in one of the submodule's remotes. Anything that cannot be proven
1861
+ # contained - an unpushed commit, a submodule with no remote, a git error - falls
1862
+ # through to the conservative uncommitted-work refusal, as does any entry that is
1863
+ # not exactly a clean submodule sitting on a different pin. The diagnosis is
1864
+ # buffered and only emitted once every entry qualifies, so it can never
1865
+ # contradict the verdict.
1866
+ #
1867
+ # No remedy command is printed, deliberately. That containment check reads local
1868
+ # refs only and never fetches, because this gate has to stay usable offline. A
1869
+ # remote-tracking ref that has gone stale - its upstream branch deleted or
1870
+ # force-pushed, and never pruned - therefore still reads as containment, so a
1871
+ # commit that is really unpushed can look contained. Naming the submodule and both
1872
+ # pins is what the operator actually needs; printing a checkout command on a
1873
+ # judgement that can be fooled could cost them that commit, so the remedy is left
1874
+ # to the operator, who can see the whole picture.
1875
+ describe_stale_submodule_pins() { # <worktree> <status>
1876
+ local worktree=$1 status=$2 line path want have unpushed lines=
1877
+ while IFS= read -r line; do
1878
+ [ -n "$line" ] || continue
1879
+ case $line in ' M '*) path=${line#' M '} ;; *) return 1 ;; esac
1880
+ [ "$(git -C "$worktree" ls-files --stage -- "$path" 2>/dev/null | cut -c1-6)" = 160000 ] || return 1
1881
+ [ -z "$(git -C "$worktree/$path" status --porcelain 2>/dev/null)" ] || return 1
1882
+ want=$(git -C "$worktree" rev-parse --verify --quiet "HEAD:$path" 2>/dev/null) || return 1
1883
+ have=$(git -C "$worktree/$path" rev-parse --verify --quiet HEAD 2>/dev/null) || return 1
1884
+ [ "$want" != "$have" ] || return 1
1885
+ unpushed=$(git -C "$worktree/$path" log --format=%H --max-count=1 "$have" --not --remotes -- 2>/dev/null) || return 1
1886
+ [ -z "$unpushed" ] || return 1
1887
+ lines+="error: submodule '$path' is checked out at $have, but this base records $want"$'\n'
1888
+ done <<EOF
1889
+ $status
1890
+ EOF
1891
+ [ -n "$lines" ] || return 1
1892
+ printf '%s' "$lines" >&2
1893
+ }
1894
+
1895
+ freshen_spawn_worktree_base() { # <worktree>
1896
+ local worktree=$1 default target expected actual status
1897
+ if ! git -C "$worktree" fetch --quiet origin; then
1898
+ echo "error: could not fetch origin for pooled worktree '$worktree'; refusing to launch from a potentially stale base" >&2
1899
+ return 1
1900
+ fi
1901
+ if ! git -C "$worktree" remote set-head origin --auto >/dev/null 2>&1; then
1902
+ echo "error: could not resolve origin's current default branch for pooled worktree '$worktree'; refusing to launch from a potentially stale base" >&2
1903
+ return 1
1904
+ fi
1905
+ default=$(default_branch "$worktree") || {
1906
+ echo "error: could not determine origin's default branch for pooled worktree '$worktree'; refusing to launch from a potentially stale base" >&2
1907
+ return 1
1908
+ }
1909
+ target="origin/$default"
1910
+ if ! git -C "$worktree" fetch --quiet origin "+refs/heads/$default:refs/remotes/origin/$default"; then
1911
+ echo "error: could not fetch '$target' for pooled worktree '$worktree'; refusing to launch from a potentially stale base" >&2
1912
+ return 1
1913
+ fi
1914
+ expected=$(git -C "$worktree" rev-parse --verify --quiet "$target^{commit}" 2>/dev/null) || {
1915
+ echo "error: '$target' is not a commit for pooled worktree '$worktree'; refusing to launch from a potentially stale base" >&2
1916
+ return 1
1917
+ }
1918
+ status=$(git -C "$worktree" -c core.quotePath=false status --porcelain) || {
1919
+ echo "error: could not inspect pooled worktree '$worktree' before refreshing its base" >&2
1920
+ return 1
1921
+ }
1922
+ if [ -n "$status" ]; then
1923
+ if describe_stale_submodule_pins "$worktree" "$status"; then
1924
+ echo "error: pooled worktree '$worktree' has a stale submodule checkout, not uncommitted work; refusing to launch and leaving it untouched" >&2
1925
+ else
1926
+ echo "error: pooled worktree '$worktree' is not clean; refusing to discard uncommitted work while refreshing its base" >&2
1927
+ fi
1928
+ return 1
1929
+ fi
1930
+ if ! git -C "$worktree" reset --hard "$target" >/dev/null; then
1931
+ echo "error: could not reset pooled worktree '$worktree' to '$target'; refusing to launch from a potentially stale base" >&2
1932
+ return 1
1933
+ fi
1934
+ actual=$(git -C "$worktree" rev-parse --verify --quiet HEAD 2>/dev/null || true)
1935
+ if [ "$actual" != "$expected" ]; then
1936
+ echo "error: pooled worktree '$worktree' is at '${actual:-unknown}', not current '$target' ('$expected'); refusing to launch" >&2
1937
+ return 1
1938
+ fi
1939
+ }
1940
+
1941
+ herdr_projection_meta_field_exact() { # <meta> <key>
1942
+ local meta=$1 key=$2 count
1943
+ [ -f "$meta" ] && [ ! -L "$meta" ] || return 1
1944
+ count=$(grep -c "^${key}=" "$meta" 2>/dev/null || true)
1945
+ [ "$count" = 1 ] || return 1
1946
+ grep "^${key}=" "$meta" 2>/dev/null | cut -d= -f2-
1947
+ }
1948
+
1949
+ # A stale presentation journal never grants launch authority.
1950
+ # Under the session lock, authoritative metadata must identify one positively
1951
+ # dead or agent-free endpoint before token inspection may allow flat fallback.
1952
+ # Exact Herdr fields are retained for the narrower version 2 reclaim path.
1953
+ herdr_projection_existing_meta_allows_flat() { # <meta>
1954
+ local meta=$1 old_backend old_target old_session old_pane old_state target_session target_pane
1955
+ HERDR_RECOVERY_BACKEND=""
1956
+ HERDR_RECOVERY_WORKSPACE_ID=""
1957
+ HERDR_RECOVERY_TAB_ID=""
1958
+ HERDR_RECOVERY_PANE_ID=""
1959
+ old_backend=$(fm_backend_of_meta "$meta")
1960
+ old_target=$(fm_backend_target_of_meta "$meta")
1961
+ [ -n "$old_target" ] || {
1962
+ echo "error: existing metadata for $ID has no endpoint; refusing duplicate launch while its herdr presentation journal is quarantined" >&2
1963
+ return 1
1964
+ }
1965
+ HERDR_RECOVERY_BACKEND=$old_backend
1966
+ if [ "$old_backend" = herdr ]; then
1967
+ fm_backend_herdr_parse_target "$old_target" || {
1968
+ echo "error: existing herdr endpoint for $ID is malformed; refusing duplicate launch" >&2
1969
+ return 1
1970
+ }
1971
+ target_session=$FM_BACKEND_HERDR_SESSION
1972
+ target_pane=$FM_BACKEND_HERDR_PANE
1973
+ old_session=$(herdr_projection_meta_field_exact "$meta" herdr_session) || {
1974
+ echo "error: existing herdr metadata for $ID has an ambiguous session; refusing duplicate launch" >&2
1975
+ return 1
1976
+ }
1977
+ HERDR_RECOVERY_WORKSPACE_ID=$(herdr_projection_meta_field_exact "$meta" herdr_workspace_id) || {
1978
+ echo "error: existing herdr metadata for $ID has an ambiguous workspace; refusing duplicate launch" >&2
1979
+ return 1
1980
+ }
1981
+ HERDR_RECOVERY_TAB_ID=$(herdr_projection_meta_field_exact "$meta" herdr_tab_id) || {
1982
+ echo "error: existing herdr metadata for $ID has an ambiguous tab; refusing duplicate launch" >&2
1983
+ return 1
1984
+ }
1985
+ old_pane=$(herdr_projection_meta_field_exact "$meta" herdr_pane_id) || {
1986
+ echo "error: existing herdr metadata for $ID has an ambiguous pane; refusing duplicate launch" >&2
1987
+ return 1
1988
+ }
1989
+ [ "$target_session" = "$old_session" ] && [ "$target_pane" = "$old_pane" ] || {
1990
+ echo "error: existing herdr metadata for $ID has inconsistent endpoint identities; refusing duplicate launch" >&2
1991
+ return 1
1992
+ }
1993
+ HERDR_RECOVERY_PANE_ID=$old_pane
1994
+ fm_backend_herdr_server_ensure "$old_session" || {
1995
+ echo "error: existing herdr endpoint for $ID could not be inspected; refusing duplicate launch" >&2
1996
+ return 1
1997
+ }
1998
+ old_state=$(fm_backend_herdr_pane_agent_state "$old_session" "$old_pane")
1999
+ case "$old_state" in
2000
+ dead|no-agent) return 0 ;;
2001
+ live|unknown)
2002
+ echo "error: existing herdr endpoint for $ID is $old_state; refusing duplicate launch" >&2
2003
+ return 1
2004
+ ;;
2005
+ esac
2006
+ fi
2007
+ old_state=$(fm_backend_agent_alive "$old_backend" "$old_target")
2008
+ case "$old_state" in
2009
+ dead) return 0 ;;
2010
+ alive|unknown)
2011
+ echo "error: existing $old_backend endpoint for $ID is $old_state; refusing duplicate launch" >&2
2012
+ return 1
2013
+ ;;
2014
+ esac
2015
+ }
2016
+
2017
+ # Backlog preflight (bin/fm-backlog-transition-lib.sh). This spawn is about to
2018
+ # become the sole owner of the row's In-flight transition, so prove the row is
2019
+ # transitionable BEFORE any endpoint, worktree, or record exists: a refusal here
2020
+ # costs nothing to unwind, while the same refusal after publication would strand
2021
+ # a live pane. The authoritative mutation still runs under the meta lock below.
2022
+ BACKLOG_TRANSITION=0
2023
+ BACKLOG_ROW_STATE=
2024
+ if fm_backlog_transition_applies "$CONFIG" "$DATA" "$KIND"; then
2025
+ BACKLOG_TRANSITION=1
2026
+ if fm_backlog_row_probe "$DATA" "$ID"; then
2027
+ BACKLOG_ROW_STATE=$FM_BACKLOG_ROW_STATE
2028
+ elif [ "$FM_BACKLOG_ROW_RESULT" = not_found ]; then
2029
+ echo "error: task $ID has no backlog item in this home, so dispatching it would leave a worker no record owns; add it first (tasks-axi add $ID '<title>' --kind $KIND) and re-run" >&2
2030
+ exit 1
2031
+ else
2032
+ echo "error: task $ID's backlog item could not be read before dispatch ($FM_BACKLOG_ROW_ERROR)" >&2
2033
+ exit 1
2034
+ fi
2035
+ if ! fm_backlog_row_dispatchable "$BACKLOG_ROW_STATE"; then
2036
+ echo "error: this home's backlog item $ID is not dispatchable in state $BACKLOG_ROW_STATE; refusing before creating its endpoint or local copy" >&2
2037
+ exit 1
2038
+ fi
2039
+ else
2040
+ BACKLOG_GATE_STATUS=$?
2041
+ if [ "$BACKLOG_GATE_STATUS" -eq 2 ]; then
2042
+ echo "error: task $ID cannot be dispatched because its backlog data directory is inaccessible: $DATA ($FM_BACKLOG_TRANSITION_ERROR)" >&2
2043
+ exit 1
2044
+ fi
2045
+ fi
2046
+
2047
+ if [ "$SPAWN_META_LOCK_HELD" != 1 ]; then
2048
+ SPAWN_META_LOCK=$(fm_meta_lock_path "$STATE/$ID.meta") || exit 1
2049
+ fm_lock_acquire_wait "$SPAWN_META_LOCK"
2050
+ SPAWN_META_LOCK_HELD=1
2051
+ fi
2052
+ if [ -e "$STATE/$ID.backlog-close" ] || [ -L "$STATE/$ID.backlog-close" ]; then
2053
+ echo "error: task $ID has a pending authoritative backlog close at $STATE/$ID.backlog-close; finish or repair that close before dispatching a new worker" >&2
2054
+ exit 1
2055
+ fi
2056
+
2057
+ W="fm-$ID"
2058
+ if [ "$RELAUNCH" -eq 1 ]; then
2059
+ # Adopt the recorded endpoint instead of creating one. This is what keeps a
2060
+ # relaunch a REPLACEMENT rather than a second copy of the task: no new
2061
+ # terminal, no second worktree, and every uncommitted change left exactly
2062
+ # where the previous agent left it.
2063
+ T=$RELAUNCH_TARGET
2064
+ # A secondmate's home already resolved WT above through the same validation a
2065
+ # fresh secondmate spawn uses; every other kind takes the recorded worktree.
2066
+ [ "$KIND" = secondmate ] || WT=$RELAUNCH_WT
2067
+ WT_TARGET=$T
2068
+ SES=${T%%:*}
2069
+ else
2070
+ case "$BACKEND" in
2071
+ tmux)
2072
+ SES=$(fm_backend_tmux_container_ensure)
2073
+ T="$SES:$W"
2074
+ # #134 robustness (tmux): fm_backend_tmux_create_task captures a stable window
2075
+ # id and pins the window name (automatic-rename/allow-rename off) so a captain's
2076
+ # non-default tmux config cannot rename the window away from fm-<id> once
2077
+ # treehouse cd's into the worktree. WT_TARGET carries that stable id for the
2078
+ # rename-critical worktree-detection steps below; the persisted window= handle
2079
+ # stays $T (the name form), which is safe now that rename is disabled.
2080
+ WID=$(fm_backend_tmux_create_task "$SES" "$W" "$PROJ_ABS") || exit 1
2081
+ WT_TARGET="$WID"
2082
+ ;;
2083
+ herdr)
2084
+ # fm_backend_herdr_workspace_label resolves the target workspace from
2085
+ # FM_HOME. For every KIND except secondmate, this process's own FM_HOME is
2086
+ # already the right home (the primary spawning its own crewmate/scout, or
2087
+ # a secondmate spawning ITS OWN crewmate/scout from its own process's
2088
+ # FM_HOME - the latter needs no glue at all). A --secondmate spawn is the
2089
+ # one case that does: it is the PRIMARY's own fm-spawn.sh process
2090
+ # launching a DIFFERENT home (PROJ_ABS, already validated above as the
2091
+ # secondmate's home), so FM_HOME here still names the primary. Shadow it
2092
+ # to PROJ_ABS for just these two calls (bash restores it automatically
2093
+ # after each prefixed simple-command call) so the secondmate's tab lands
2094
+ # in the secondmate's own workspace, not the primary's "firstmate" one.
2095
+ #
2096
+ # Placement, separately from labeling: a crewmate/scout belongs in the
2097
+ # EXACT herdr workspace this launching process is itself running in, which
2098
+ # only its own herdr pane identity can name (a same-labeled sibling
2099
+ # workspace must never be adopted). A --secondmate launch is the exception -
2100
+ # it stands up a DIFFERENT home's own workspace by design - so it asks for
2101
+ # the per-home container instead of inheriting this launcher's.
2102
+ HERDR_LABEL_HOME=$FM_HOME
2103
+ HERDR_LAUNCHER_RELATIONSHIP=launcher-home
2104
+ if [ "$KIND" = secondmate ]; then
2105
+ HERDR_LABEL_HOME=$PROJ_ABS
2106
+ HERDR_LAUNCHER_RELATIONSHIP=other-home
2107
+ fi
2108
+ HERDR_PRESENTATION_JOURNAL=$(fm_backend_herdr_projection_journal_path "$STATE" "$ID")
2109
+ HERDR_PROJECTED=0
2110
+ if [ "$KIND" != secondmate ] && fm_backend_herdr_presentation_enabled "$CONFIG" "$STATE"; then
2111
+ HERDR_SES=$(fm_backend_herdr_session)
2112
+ HERDR_PARENT_LABEL=$(FM_HOME="$HERDR_LABEL_HOME" fm_backend_herdr_workspace_label)
2113
+ if [ -e "$HERDR_PRESENTATION_JOURNAL" ] || [ -L "$HERDR_PRESENTATION_JOURNAL" ]; then
2114
+ fm_backend_herdr_server_ensure "$HERDR_SES" || {
2115
+ echo "error: herdr presentation recovery could not ensure its exact named session" >&2
2116
+ exit 1
2117
+ }
2118
+ spawn_herdr_presentation_order_lock_acquire "$HERDR_SES" || {
2119
+ echo "error: herdr presentation recovery could not acquire its session lock; refusing a concurrent resume" >&2
2120
+ exit 1
2121
+ }
2122
+ if [ -e "$STATE/$ID.meta" ] || [ -L "$STATE/$ID.meta" ]; then
2123
+ herdr_projection_existing_meta_allows_flat "$STATE/$ID.meta" || exit 1
2124
+ fi
2125
+ fm_backend_herdr_projection_recovery_allows_flat \
2126
+ "$HERDR_SES" "$HERDR_PRESENTATION_JOURNAL" "$ID" || exit 1
2127
+ if [ "${HERDR_RECOVERY_BACKEND:-}" = herdr ]; then
2128
+ set +e
2129
+ FM_HOME="$HERDR_LABEL_HOME" fm_backend_herdr_projection_reclaim_task \
2130
+ "$HERDR_SES" "$HERDR_PRESENTATION_JOURNAL" "$ID" "$HERDR_LABEL_HOME" \
2131
+ "$HERDR_RECOVERY_WORKSPACE_ID" "$HERDR_RECOVERY_TAB_ID" "$HERDR_RECOVERY_PANE_ID" \
2132
+ "$HERDR_PARENT_LABEL" "$W" "$PROJ_ABS"
2133
+ HERDR_RECLAIM_STATUS=$?
2134
+ set -e
2135
+ case "$HERDR_RECLAIM_STATUS" in
2136
+ 0)
2137
+ HERDR_PROJECTED=1
2138
+ HERDR_WORKSPACE_ID=$HERDR_RECOVERY_WORKSPACE_ID
2139
+ HERDR_SEEDED_DEFAULT_TAB_ID=""
2140
+ HERDR_TAB_ID=$FM_BACKEND_HERDR_PROJECTION_TAB_ID
2141
+ HERDR_PANE_ID=$FM_BACKEND_HERDR_PROJECTION_PANE_ID
2142
+ HERDR_PROJECTION_ABORT_CLEANUP=1
2143
+ HERDR_PROJECTION_ABORT_SESSION=$HERDR_SES
2144
+ HERDR_PROJECTION_ABORT_TASK_PANE=$HERDR_PANE_ID
2145
+ HERDR_PROJECTION_ABORT_SEEDED_PANE=""
2146
+ ;;
2147
+ 2)
2148
+ spawn_herdr_presentation_order_lock_release
2149
+ ;;
2150
+ *) exit 1 ;;
2151
+ esac
2152
+ else
2153
+ spawn_herdr_presentation_order_lock_release
2154
+ fi
2155
+ elif [ ! -e "$STATE/$ID.meta" ] && [ ! -L "$STATE/$ID.meta" ]; then
2156
+ # Session lock path resolution and exact parent binding both need a
2157
+ # live named-session socket before journal publication.
2158
+ if ! fm_backend_herdr_server_ensure "$HERDR_SES"; then
2159
+ echo "warning: herdr presentation could not ensure its session server; using the ordinary flat layout without projection" >&2
2160
+ elif [ "${FM_BACKEND_HERDR_PRESENTATION_PREFERENCE:-default}" = default ] \
2161
+ && ! fm_backend_herdr_presentation_default_supported "$STATE" "$HERDR_SES"; then
2162
+ :
2163
+ elif spawn_herdr_presentation_order_lock_acquire "$HERDR_SES"; then
2164
+ # The projected child is placed and bound UNDER this launcher's exact
2165
+ # parent workspace. Its own herdr pane identity names that workspace
2166
+ # directly; the label lookup is only the fallback for a launcher with
2167
+ # no herdr ancestry at all. A claimed-but-broken identity refuses here
2168
+ # rather than projecting under a guessed parent.
2169
+ set +e
2170
+ fm_backend_herdr_launcher_identity "$HERDR_SES"
2171
+ HERDR_LAUNCHER_STATUS=$?
2172
+ set -e
2173
+ case "$HERDR_LAUNCHER_STATUS" in
2174
+ 0) HERDR_PARENT_WORKSPACE_ID=$FM_BACKEND_HERDR_LAUNCHER_WORKSPACE_ID ;;
2175
+ 2) HERDR_PARENT_WORKSPACE_ID=$(fm_backend_herdr_projection_parent_workspace_exact \
2176
+ "$HERDR_SES" "$HERDR_PARENT_LABEL" 2>/dev/null || true) ;;
2177
+ *) spawn_herdr_presentation_order_lock_release; exit 1 ;;
2178
+ esac
2179
+ if [ -z "$HERDR_PARENT_WORKSPACE_ID" ]; then
2180
+ echo "warning: herdr presentation parent is absent or ambiguous; using the ordinary flat layout without projection" >&2
2181
+ spawn_herdr_presentation_order_lock_release
2182
+ else
2183
+ HERDR_PROJECTION_ID=$(fm_backend_herdr_projection_journal_create "$STATE" "$ID") || exit 1
2184
+ HERDR_PROJECTION_LABEL=$(fm_backend_herdr_projection_workspace_label "$ID" "$HERDR_PROJECTION_ID")
2185
+ if ! FM_HOME="$HERDR_LABEL_HOME" fm_backend_herdr_projection_create_task \
2186
+ "$PROJ_ABS" "$HERDR_PROJECTION_LABEL" "$W"; then
2187
+ if [ "${FM_BACKEND_HERDR_PROJECTION_CLEANUP_SAFE:-0}" = 1 ]; then
2188
+ HERDR_PROJECTION_ABORT_CLEANUP=1
2189
+ HERDR_PROJECTION_ABORT_SESSION=$FM_BACKEND_HERDR_PROJECTION_SESSION
2190
+ HERDR_PROJECTION_ABORT_TASK_PANE=$FM_BACKEND_HERDR_PROJECTION_PANE_ID
2191
+ HERDR_PROJECTION_ABORT_SEEDED_PANE=$FM_BACKEND_HERDR_PROJECTION_SEEDED_PANE_ID
2192
+ fi
2193
+ exit 1
2194
+ fi
2195
+ HERDR_PROJECTED=1
2196
+ HERDR_SES=$FM_BACKEND_HERDR_PROJECTION_SESSION
2197
+ HERDR_WORKSPACE_ID=$FM_BACKEND_HERDR_PROJECTION_WORKSPACE_ID
2198
+ HERDR_SEEDED_DEFAULT_TAB_ID=$FM_BACKEND_HERDR_PROJECTION_SEEDED_TAB_ID
2199
+ HERDR_TAB_ID=$FM_BACKEND_HERDR_PROJECTION_TAB_ID
2200
+ HERDR_PANE_ID=$FM_BACKEND_HERDR_PROJECTION_PANE_ID
2201
+ HERDR_PROJECTION_ABORT_CLEANUP=1
2202
+ HERDR_PROJECTION_ABORT_SESSION=$HERDR_SES
2203
+ HERDR_PROJECTION_ABORT_TASK_PANE=$HERDR_PANE_ID
2204
+ HERDR_PROJECTION_ABORT_SEEDED_PANE=$FM_BACKEND_HERDR_PROJECTION_SEEDED_PANE_ID
2205
+ fm_backend_herdr_projection_order_best_effort \
2206
+ "$HERDR_SES" "$HERDR_WORKSPACE_ID" "$HERDR_PARENT_LABEL" "$HERDR_PARENT_WORKSPACE_ID"
2207
+ HERDR_HOME_ID=$(fm_backend_herdr_projection_home_identity "$HERDR_LABEL_HOME" 2>/dev/null || true)
2208
+ if [ -n "$HERDR_HOME_ID" ] \
2209
+ && fm_backend_herdr_projection_live_binding_matches \
2210
+ "$HERDR_SES" "$HERDR_PROJECTION_ID" "$HERDR_WORKSPACE_ID" \
2211
+ "$HERDR_TAB_ID" "$HERDR_PANE_ID" "$HERDR_PARENT_WORKSPACE_ID" \
2212
+ "$HERDR_PARENT_LABEL" "$HERDR_PROJECTION_LABEL" "$W" \
2213
+ && fm_backend_herdr_projection_journal_bind \
2214
+ "$HERDR_PRESENTATION_JOURNAL" "$ID" "$HERDR_HOME_ID" "$HERDR_SES" \
2215
+ "$HERDR_WORKSPACE_ID" "$HERDR_TAB_ID" "$HERDR_PANE_ID" \
2216
+ "$HERDR_PARENT_WORKSPACE_ID" "$HERDR_PARENT_LABEL" "$HERDR_PROJECTION_LABEL" "$W"; then
2217
+ :
2218
+ else
2219
+ echo "warning: herdr presentation could not publish an exact restart binding; this task will use flat fallback after a restart" >&2
2220
+ fi
2221
+ fi
2222
+ else
2223
+ echo "warning: herdr presentation focus lock unavailable; using the ordinary flat layout without projection" >&2
2224
+ fi
2225
+ fi
2226
+ fi
2227
+ if [ "$HERDR_PROJECTED" -ne 1 ]; then
2228
+ HERDR_CONTAINER_RAW=$(FM_HOME="$HERDR_LABEL_HOME" fm_backend_herdr_container_ensure "$PROJ_ABS" "$HERDR_LAUNCHER_RELATIONSHIP") || exit 1
2229
+ # fm_backend_herdr_container_ensure echoes "<session>:<workspace_id>\t<seeded_default_tab_id>"
2230
+ # (the second field empty when this call ADOPTED a pre-existing workspace
2231
+ # rather than creating a fresh one). Split on the guaranteed single tab
2232
+ # character; the seeded tab id is threaded through to create_task
2233
+ # untouched, which is the only function permitted to prune it (never
2234
+ # re-derived from labels - see docs/herdr-backend.md "Default-tab prune").
2235
+ CONTAINER=${HERDR_CONTAINER_RAW%%$'\t'*}
2236
+ HERDR_SEEDED_DEFAULT_TAB_ID=${HERDR_CONTAINER_RAW#*$'\t'}
2237
+ HERDR_SES=${CONTAINER%%:*}
2238
+ HERDR_WORKSPACE_ID=${CONTAINER#*:}
2239
+ HERDR_TASK_IDS=$(FM_HOME="$HERDR_LABEL_HOME" fm_backend_herdr_create_task "$CONTAINER" "$W" "$PROJ_ABS" "$HERDR_SEEDED_DEFAULT_TAB_ID") || exit 1
2240
+ read -r HERDR_TAB_ID HERDR_PANE_ID <<EOF
2241
+ $HERDR_TASK_IDS
2242
+ EOF
2243
+ fi
2244
+ if [ -z "$HERDR_TAB_ID" ] || [ -z "$HERDR_PANE_ID" ]; then
2245
+ echo "error: herdr did not return a tab/pane id for $W" >&2
2246
+ exit 1
2247
+ fi
2248
+ T="$HERDR_SES:$HERDR_PANE_ID"
2249
+ ;;
2250
+ zellij)
2251
+ ZELLIJ_SES=$(fm_backend_zellij_container_ensure) || exit 1
2252
+ ZELLIJ_TASK_IDS=$(fm_backend_zellij_create_task "$ZELLIJ_SES" "$W" "$PROJ_ABS") || exit 1
2253
+ read -r ZELLIJ_TAB_ID ZELLIJ_PANE_ID <<EOF
2254
+ $ZELLIJ_TASK_IDS
2255
+ EOF
2256
+ if [ -z "$ZELLIJ_TAB_ID" ] || [ -z "$ZELLIJ_PANE_ID" ]; then
2257
+ echo "error: zellij did not return a tab/pane id for $W" >&2
2258
+ exit 1
2259
+ fi
2260
+ T="$ZELLIJ_SES:$ZELLIJ_PANE_ID"
2261
+ ;;
2262
+ cmux)
2263
+ fm_backend_cmux_container_ensure || exit 1
2264
+ CMUX_TASK_IDS=$(fm_backend_cmux_create_task "$W" "$PROJ_ABS") || exit 1
2265
+ read -r CMUX_WORKSPACE_ID CMUX_SURFACE_ID <<EOF
2266
+ $CMUX_TASK_IDS
2267
+ EOF
2268
+ if [ -z "$CMUX_WORKSPACE_ID" ] || [ -z "$CMUX_SURFACE_ID" ]; then
2269
+ echo "error: cmux did not return a workspace/surface id for $W" >&2
2270
+ exit 1
2271
+ fi
2272
+ T="$CMUX_WORKSPACE_ID:$CMUX_SURFACE_ID"
2273
+ ;;
2274
+ orca)
2275
+ set +e
2276
+ ORCA_WT_RAW=$(fm_backend_orca_worktree_create "$PROJ_ABS" "$W")
2277
+ ORCA_WT_STATUS=$?
2278
+ set -e
2279
+ if [ "$ORCA_WT_STATUS" -ne 0 ]; then
2280
+ if [ "$ORCA_WT_STATUS" -eq 2 ] && [ -n "$ORCA_WT_RAW" ]; then
2281
+ if parse_orca_worktree_result "$ORCA_WT_RAW" && [ -n "$ORCA_WORKTREE_ID" ]; then
2282
+ ORCA_ABORT_CLEANUP=1
2283
+ fi
2284
+ fi
2285
+ exit 1
2286
+ fi
2287
+ parse_orca_worktree_result "$ORCA_WT_RAW" || true
2288
+ ORCA_ABORT_CLEANUP=1
2289
+ if [ -z "$ORCA_WORKTREE_ID" ] || [ -z "$WT" ]; then
2290
+ echo "error: orca did not return a worktree id/path for $W" >&2
2291
+ exit 1
2292
+ fi
2293
+ validate_spawn_worktree "orca worktree create" "$W"
2294
+ if [ -z "$ORCA_TERMINAL" ]; then
2295
+ ORCA_TERMINAL=$(fm_backend_orca_terminal_create "$ORCA_WORKTREE_ID" "$W") || exit 1
2296
+ fi
2297
+ T="$ORCA_TERMINAL"
2298
+ ;;
2299
+ esac
2300
+ fi
2301
+ if [ "$KIND" = secondmate ]; then
2302
+ FM_INHERITABLE_CONFIG=trace-context \
2303
+ propagate_inheritable_config "$CONFIG" "$PROJ_ABS/config" \
2304
+ || echo "warning: secondmate $ID trace-context inheritance failed for $PROJ_ABS" >&2
2305
+ fi
2306
+ # #134 robustness: only tmux needs a worktree-detection target distinct from $T -
2307
+ # its rename-safe stable window id, set as WT_TARGET=$WID in the tmux branch above.
2308
+ # Every other backend addresses its pane/surface by the id already in $T, so default
2309
+ # WT_TARGET to $T for them (and for any future backend) - the shared treehouse-get +
2310
+ # worktree-detection steps below must never reference an unbound WT_TARGET under set -u.
2311
+ : "${WT_TARGET:=$T}"
2312
+ spawn_send_text_line() { # <target> <text>
2313
+ case "$BACKEND" in
2314
+ tmux) fm_backend_tmux_send_text_line "$1" "$2" ;;
2315
+ herdr) fm_backend_herdr_send_text_line "$1" "$2" ;;
2316
+ zellij) fm_backend_zellij_send_text_line "$1" "$2" "$W" ;;
2317
+ orca) fm_backend_orca_send_text_line "$1" "$2" ;;
2318
+ cmux) fm_backend_cmux_send_text_line "$1" "$2" "$W" ;;
2319
+ esac
2320
+ }
2321
+ spawn_current_path() { # <target>
2322
+ case "$BACKEND" in
2323
+ tmux) fm_backend_tmux_current_path "$1" ;;
2324
+ herdr) fm_backend_herdr_current_path "$1" ;;
2325
+ zellij) fm_backend_zellij_current_path "$1" "$W" ;;
2326
+ cmux) fm_backend_cmux_current_path "$1" "$W" ;;
2327
+ esac
2328
+ }
2329
+ spawn_send_literal() { # <target> <text>
2330
+ case "$BACKEND" in
2331
+ tmux) fm_backend_tmux_send_literal "$1" "$2" ;;
2332
+ herdr) fm_backend_herdr_send_literal "$1" "$2" ;;
2333
+ zellij) fm_backend_zellij_send_literal "$1" "$2" "$W" ;;
2334
+ orca) fm_backend_orca_send_literal "$1" "$2" ;;
2335
+ cmux) fm_backend_cmux_send_literal "$1" "$2" "$W" ;;
2336
+ esac
2337
+ }
2338
+ spawn_send_key() { # <target> <key>
2339
+ case "$BACKEND" in
2340
+ tmux) fm_backend_tmux_send_key "$1" "$2" ;;
2341
+ herdr) fm_backend_herdr_send_key "$1" "$2" ;;
2342
+ zellij) fm_backend_zellij_send_key "$1" "$2" "$W" ;;
2343
+ orca) fm_backend_orca_send_key "$1" "$2" ;;
2344
+ cmux) fm_backend_cmux_send_key "$1" "$2" "$W" ;;
2345
+ esac
2346
+ }
2347
+
2348
+ kimi_capture() {
2349
+ fm_backend_capture "$BACKEND" "$T" 120 "$W" 2>/dev/null || true
2350
+ }
2351
+
2352
+ # Kimi launch-readiness and delivery route their composer-emptiness half
2353
+ # through the shared classifier (bin/fm-composer-lib.sh via
2354
+ # fm_backend_composer_state), the same owner every steer and injection guard
2355
+ # reads. This retired a fourth, spawn-local copy of composer shape knowledge -
2356
+ # a hardcoded bordered `│ > │` regex that would have silently broken kimi
2357
+ # spawn readiness fleet-wide the day kimi's TUI goes borderless the way
2358
+ # claude's did. The banner and brief-echo greps below are launch-progress
2359
+ # signals, not composer shapes, so they stay here.
2360
+ kimi_composer_is_empty() {
2361
+ [ "$(fm_backend_composer_state "$BACKEND" "$T" "$W" 2>/dev/null)" = empty ]
2362
+ }
2363
+
2364
+ kimi_wait_for_ready() {
2365
+ local pane i=0 max=${FM_KIMI_READY_POLLS:-60} interval=${FM_KIMI_POLL_INTERVAL:-0.5}
2366
+ while [ "$i" -lt "$max" ]; do
2367
+ pane=$(kimi_capture)
2368
+ if printf '%s\n' "$pane" | grep -Fq 'Welcome to Kimi Code!' \
2369
+ || kimi_composer_is_empty; then
2370
+ return 0
2371
+ fi
2372
+ i=$((i + 1))
2373
+ [ "$i" -ge "$max" ] || sleep "$interval"
2374
+ done
2375
+ return 1
2376
+ }
2377
+
2378
+ kimi_delivery_is_confirmed() { # <plain-pane-capture>
2379
+ local pane=$1
2380
+ kimi_composer_is_empty || return 1
2381
+ if { printf '%s\n' "$pane" | grep -Fq '✨' \
2382
+ && printf '%s\n' "$pane" | grep -Fq 'Read the brief at'; } \
2383
+ || printf '%s\n' "$pane" \
2384
+ | grep -qiE 'context:[[:space:]]*(0\.[0-9]*[1-9][0-9]*|[1-9][0-9]*([.][0-9]+)?)[[:space:]]*%'; then
2385
+ return 0
2386
+ fi
2387
+ return 1
2388
+ }
2389
+
2390
+ kimi_wait_for_delivery() {
2391
+ local pane i=0 max=${FM_KIMI_DELIVERY_POLLS:-40} interval=${FM_KIMI_POLL_INTERVAL:-0.5}
2392
+ while [ "$i" -lt "$max" ]; do
2393
+ pane=$(kimi_capture)
2394
+ kimi_delivery_is_confirmed "$pane" && return 0
2395
+ i=$((i + 1))
2396
+ [ "$i" -ge "$max" ] || sleep "$interval"
2397
+ done
2398
+ return 1
2399
+ }
2400
+
2401
+ kimi_spawn_fail() { # <detail>
2402
+ printf 'failed: %s\n' "$1" >> "$STATE/$ID.status"
2403
+ echo "error: $1; inspect window $T" >&2
2404
+ }
2405
+
2406
+ if [ "$RELAUNCH" -eq 1 ]; then
2407
+ # No worktree is acquired: the recorded one is reused as-is. What must be
2408
+ # proven instead is that the adopted endpoint's shell is actually sitting in
2409
+ # that worktree, so the replacement agent starts where the work is rather
2410
+ # than wherever the pane happened to drift.
2411
+ relaunch_wt_real=$(real_path_or_raw "$WT")
2412
+ relaunch_seen=
2413
+ for _ in $(seq 1 10); do
2414
+ relaunch_seen=$(spawn_current_path "$WT_TARGET" || true)
2415
+ [ -z "$relaunch_seen" ] || [ "$(real_path_or_raw "$relaunch_seen")" != "$relaunch_wt_real" ] || break
2416
+ sleep 0.5
2417
+ done
2418
+ if [ -z "$relaunch_seen" ] || [ "$(real_path_or_raw "$relaunch_seen")" != "$relaunch_wt_real" ]; then
2419
+ echo "error: task $ID's endpoint is in '${relaunch_seen:-unknown}', not its recorded worktree '$WT'; refusing to relaunch an agent outside the copy holding its work" >&2
2420
+ exit 1
2421
+ fi
2422
+ [ "$KIND" = secondmate ] || validate_spawn_worktree "relaunch" "$T"
2423
+ elif [ "$KIND" != secondmate ] && [ "$BACKEND" != orca ]; then
2424
+ spawn_send_text_line "$WT_TARGET" 'treehouse get'
2425
+
2426
+ # Wait for the treehouse subshell: the pane's cwd moves from the project to the worktree.
2427
+ # Target the stable window id, not the name: if the name is ever lost (e.g. an
2428
+ # automatic-rename slips through), display-message -t <bad-name> falls back to the
2429
+ # active client's window, which would misread firstmate's OWN pane path as the
2430
+ # worktree and tangle a hook into the primary checkout. The window id never lies.
2431
+ # Compare against PROJ_ABS_REAL (physical), not PROJ_ABS: a symlinked project
2432
+ # prefix would otherwise make the pane's OS-level cwd read differ from
2433
+ # PROJ_ABS on the very first poll, before the pane has actually moved.
2434
+ #
2435
+ # A single read that already differs from PROJ_ABS_REAL is not proof the pane
2436
+ # settled there: on some tmux/WSL setups a brand-new window's pane_current_path
2437
+ # transiently reports an unrelated stale path (seen live as another real git
2438
+ # checkout entirely) before the shell catches up with treehouse get's cd. That
2439
+ # stale path still passes the PROJ_ABS_REAL comparison and validate_spawn_worktree
2440
+ # below (it resolves to a real, distinct worktree top-level too), so accepting it
2441
+ # on one read alone silently records the wrong worktree= in state/<id>.meta. Require
2442
+ # two consecutive reads to agree on the same non-project path before accepting it;
2443
+ # a mismatch just becomes the new candidate rather than resetting the wait, so a
2444
+ # pane that is already settled by the first real read only costs the one existing
2445
+ # inter-poll sleep as confirmation, not a whole extra cycle on top.
2446
+ candidate=""
2447
+ for _ in $(seq 1 60); do
2448
+ p=$(spawn_current_path "$WT_TARGET" || true)
2449
+ if [ -n "$p" ]; then
2450
+ p_real=$(real_path_or_raw "$p")
2451
+ if [ "$p_real" != "$PROJ_ABS_REAL" ]; then
2452
+ if [ -n "$candidate" ] && [ "$p_real" = "$candidate" ]; then
2453
+ WT="$p"
2454
+ break
2455
+ fi
2456
+ candidate="$p_real"
2457
+ else
2458
+ candidate=""
2459
+ fi
2460
+ else
2461
+ candidate=""
2462
+ fi
2463
+ sleep 1
2464
+ done
2465
+ if [ -z "$WT" ]; then
2466
+ echo "error: treehouse get did not enter a worktree within 60s; inspect window $T" >&2
2467
+ exit 1
2468
+ fi
2469
+
2470
+ validate_spawn_worktree "treehouse get" "$T"
2471
+ fi
2472
+ if [ "$RELAUNCH" -eq 0 ] && [ "$KIND" != secondmate ]; then
2473
+ freshen_spawn_worktree_base "$WT" || exit 1
2474
+ fi
2475
+
2476
+ # Per-task temp root: /tmp/fm-<id>/ with Go's build temp nested at gotmp/. Go won't
2477
+ # create GOTMPDIR, so mkdir before it is used; fm-teardown removes the whole root.
2478
+ # Nested (not a bare /tmp/fm-<id>/gotmp) so other per-task temp can live alongside
2479
+ # later, and teardown cleans one deterministic path. GOTMPDIR (not TMPDIR) is the
2480
+ # targeted knob: TMPDIR is too broad (affects every program's temp, not just Go's).
2481
+ TASK_TMP="/tmp/fm-$ID"
2482
+ mkdir -p "$TASK_TMP/gotmp"
2483
+
2484
+ # Per-harness turn-end hook where enabled: a file that touches
2485
+ # state/<id>.turn-ended when the agent finishes a turn. Worktree-resident hooks
2486
+ # and token pointers stay out of git's view so they never block teardown's dirty
2487
+ # check or leak into a commit.
2488
+ mkdir -p "$STATE"
2489
+ STATE_REAL=$(cd "$STATE" && pwd -P)
2490
+ TURNEND="$STATE_REAL/$ID.turn-ended"
2491
+ exclude_path() {
2492
+ local rel=$1 EXCL
2493
+ EXCL=$(git -C "$WT" rev-parse --git-path info/exclude 2>/dev/null || true)
2494
+ [ -n "$EXCL" ] || return 0
2495
+ mkdir -p "$(dirname "$EXCL")"
2496
+ grep -qxF "$rel" "$EXCL" 2>/dev/null || echo "$rel" >> "$EXCL"
2497
+ }
2498
+ if [ "$RELAUNCH" -eq 1 ]; then
2499
+ # Retire the previous incarnation's per-task harness wiring before arming the
2500
+ # new one. Without this, a harness switch would leave the old adapter's hook
2501
+ # files and turn-end token registry entries behind, and even a same-harness
2502
+ # relaunch would orphan the retired busy generation's token
2503
+ # (bin/fm-control-lib.sh owns where those artifacts live).
2504
+ clear_relaunch_harness_wiring "$RELAUNCH_PRIOR_HARNESS" "$WT" "$STATE_REAL" "$ID" || {
2505
+ echo "error: could not retire $RELAUNCH_PRIOR_HARNESS wiring for task $ID; refusing to arm the replacement" >&2
2506
+ exit 1
2507
+ }
2508
+ RELAUNCH_REPLACEMENT_PENDING=1
2509
+ RELAUNCH_REPLACEMENT_HARNESS=$HARNESS
2510
+ RELAUNCH_REPLACEMENT_STATE=$STATE_REAL
2511
+ RELAUNCH_REPLACEMENT_WT=$WT
2512
+ fi
2513
+ if [ "$KIND" != secondmate ]; then
2514
+ # Arm the semantic busy-state contract (bin/fm-busy-lib.sh) for every
2515
+ # adapter with a verified semantic source. The launch brief sent below IS a
2516
+ # submitted turn, so the seed record is busy/fm-spawn. The minted gen is
2517
+ # embedded into each adapter's wiring so an event from a superseded
2518
+ # incarnation is rejected as stale. Grok stays on its isolated rendered-tail
2519
+ # fallback and standalone Kimi stays unknown until fm_busy_kimi_verified
2520
+ # opens, so neither is armed here.
2521
+ BUSY_GEN=
2522
+ case "$HARNESS" in
2523
+ codex*)
2524
+ if fm_busy_codex_semantic_source; then
2525
+ echo "error: codex semantic busy-state wiring is not implemented; extend the probe only together with verified wiring" >&2
2526
+ exit 1
2527
+ fi
2528
+ ;;
2529
+ esac
2530
+ case "$HARNESS" in
2531
+ claude*|opencode*|pi|pi-signed)
2532
+ BUSY_GEN=$("$FM_ROOT/bin/fm-busy-event.sh" arm "$STATE_REAL" "$ID") || {
2533
+ echo "error: failed to arm the busy-state contract for $ID" >&2
2534
+ exit 1
2535
+ }
2536
+ [ "$RELAUNCH" -ne 1 ] || RELAUNCH_REPLACEMENT_BUSY_GEN=$BUSY_GEN
2537
+ ;;
2538
+ kimi*)
2539
+ # Standalone Kimi stays unknown until fm_busy_kimi_verified opens on a
2540
+ # live-verified installed version (bin/fm-busy-lib.sh owns the gate and
2541
+ # the required evidence). Arming without wiring would seed a busy record
2542
+ # nothing can ever clear, so the arm waits for the wiring.
2543
+ if fm_busy_kimi_verified; then
2544
+ echo "error: kimi semantic busy-state wiring is not implemented; open the gate only together with verified wiring" >&2
2545
+ exit 1
2546
+ fi
2547
+ ;;
2548
+ esac
2549
+ case "$HARNESS" in
2550
+ claude*)
2551
+ # Semantic busy-state hooks (bin/fm-busy-lib.sh): UserPromptSubmit opens
2552
+ # a turn; Stop (normal completion), StopFailure (API-error turn end),
2553
+ # and SessionEnd (process shutdown) all close it, so an abnormal end can
2554
+ # never leave a stale busy record. Claude fires no hook for a manual
2555
+ # interrupt: fm-control preserves the adapter-owned state, while the
2556
+ # legacy fm-send --key Escape path records idle/fm-interrupt. Stop keeps
2557
+ # the turn-ended NOTIFICATION touch for the watcher. Every
2558
+ # hook command tolerates a refused event (|| true) so a stale-gen writer
2559
+ # can never break Claude's own lifecycle.
2560
+ mkdir -p "$WT/.claude"
2561
+ busy_cmd_prefix="$(shell_quote "$FM_ROOT/bin/fm-busy-event.sh") apply $(shell_quote "$STATE_REAL") $(shell_quote "$ID")"
2562
+ busy_suffix="--gen $(shell_quote "$BUSY_GEN") --source claude-hook"
2563
+ j_submit=$(json_escape "$busy_cmd_prefix busy $busy_suffix --event user-prompt-submit 2>/dev/null || true")
2564
+ j_stop=$(json_escape "touch $(shell_quote "$TURNEND"); $busy_cmd_prefix idle $busy_suffix --event stop 2>/dev/null || true")
2565
+ j_stopfail=$(json_escape "$busy_cmd_prefix idle $busy_suffix --event stop-failure 2>/dev/null || true")
2566
+ j_sessionend=$(json_escape "$busy_cmd_prefix idle $busy_suffix --event session-end 2>/dev/null || true")
2567
+ cat > "$WT/.claude/settings.local.json" <<EOF
2568
+ {"hooks":{"UserPromptSubmit":[{"hooks":[{"type":"command","command":"$j_submit"}]}],"Stop":[{"hooks":[{"type":"command","command":"$j_stop"}]}],"StopFailure":[{"hooks":[{"type":"command","command":"$j_stopfail"}]}],"SessionEnd":[{"hooks":[{"type":"command","command":"$j_sessionend"}]}]}}
2569
+ EOF
2570
+ exclude_path '.claude/settings.local.json'
2571
+ ;;
2572
+ opencode*)
2573
+ mkdir -p "$WT/.opencode/plugins"
2574
+ cat > "$WT/.opencode/plugins/fm-busy-state.js" <<EOF
2575
+ // Firstmate semantic busy-state events + turn-end notification; written by
2576
+ // fm-spawn under the contract owned by bin/fm-busy-lib.sh.
2577
+ // Semantic state comes from OpenCode's session.status events: busy and retry
2578
+ // are active, idle is inactive. Scoping latches the first session that
2579
+ // reports activity (the worker's main session - a subagent child session can
2580
+ // only start while the main session is already busy) and ignores other
2581
+ // sessions' status until the latched session settles, so a child's idle can
2582
+ // never clear the worker's busy state. The session.idle touch stays the
2583
+ // watcher's wake NOTIFICATION, never current-state truth.
2584
+ import { execFile } from "node:child_process";
2585
+ const busyEvent = (state, event) =>
2586
+ new Promise((resolve) => {
2587
+ execFile("$FM_ROOT/bin/fm-busy-event.sh", [
2588
+ "apply", "$STATE_REAL", "$ID", state,
2589
+ "--gen", "$BUSY_GEN", "--source", "opencode-plugin", "--event", event,
2590
+ ], () => resolve());
2591
+ });
2592
+ export const FmBusyState = async () => {
2593
+ let activeSession = null;
2594
+ return {
2595
+ event: async ({ event }) => {
2596
+ if (event.type === "session.status") {
2597
+ const sessionID = event.properties.sessionID;
2598
+ const statusType = event.properties.status && event.properties.status.type;
2599
+ if (statusType === "busy" || statusType === "retry") {
2600
+ if (activeSession === null) activeSession = sessionID;
2601
+ if (sessionID === activeSession) await busyEvent("busy", "session-" + statusType);
2602
+ return;
2603
+ }
2604
+ if (statusType === "idle" && sessionID === activeSession) {
2605
+ activeSession = null;
2606
+ await busyEvent("idle", "session-status-idle");
2607
+ }
2608
+ return;
2609
+ }
2610
+ if (event.type === "session.idle") {
2611
+ if (event.properties.sessionID === activeSession) {
2612
+ activeSession = null;
2613
+ await busyEvent("idle", "session-idle");
2614
+ }
2615
+ await new Promise((resolve) => {
2616
+ execFile("touch", ["$TURNEND"], () => resolve());
2617
+ });
2618
+ }
2619
+ },
2620
+ };
2621
+ };
2622
+ EOF
2623
+ exclude_path '.opencode/plugins/fm-busy-state.js'
2624
+ ;;
2625
+ pi|pi-signed)
2626
+ # Written OUTSIDE the worktree: pi's project-trust gate fires on any extension
2627
+ # loaded from inside the project (verified live), but an explicit -e path
2628
+ # elsewhere loads without a dialog. Lives in state/, cleaned by teardown.
2629
+ cat > "$STATE/$ID.pi-ext.ts" <<EOF
2630
+ // Firstmate semantic busy-state events + turn-end notification; written by
2631
+ // fm-spawn under the contract owned by bin/fm-busy-lib.sh.
2632
+ // Semantic state: "agent_start" -> busy when a low-level agent run begins;
2633
+ // "agent_settled" -> idle only when ctx.isIdle() confirms Pi will not
2634
+ // continue automatically - auto-retries, auto-compaction retries, tool
2635
+ // loops, and queued continuations all keep the run un-settled, and a settle
2636
+ // that raced another extension's fresh run keeps state busy via isIdle().
2637
+ // "turn_end" fires at every inner turn boundary (one LLM response plus its
2638
+ // tool calls) and stays a wake NOTIFICATION touch for the watcher, never
2639
+ // current-state truth.
2640
+ import { execFile } from "node:child_process";
2641
+ const busyEvent = (state: string, event: string) =>
2642
+ new Promise<void>((resolve) => {
2643
+ execFile("$FM_ROOT/bin/fm-busy-event.sh", [
2644
+ "apply", "$STATE_REAL", "$ID", state,
2645
+ "--gen", "$BUSY_GEN", "--source", "pi-ext", "--event", event,
2646
+ ], () => resolve());
2647
+ });
2648
+ export default function (pi: any) {
2649
+ pi.on("agent_start", () => busyEvent("busy", "agent-start"));
2650
+ pi.on("agent_settled", (_event: any, ctx: any) => {
2651
+ if (ctx && typeof ctx.isIdle === "function" && !ctx.isIdle()) return;
2652
+ return busyEvent("idle", "agent-settled");
2653
+ });
2654
+ pi.on("turn_end", () => execFile("touch", ["$TURNEND"]));
2655
+ }
2656
+ EOF
2657
+ ;;
2658
+ codex*)
2659
+ # Semantic busy-state source negotiation (bin/fm-busy-lib.sh owns the
2660
+ # probes and the evidence). Neither Codex path is usable on the
2661
+ # installed binary: a pane worker's turns are not observable through
2662
+ # the app-server protocol, and its lifecycle hooks did not fire for a
2663
+ # firstmate-launched worker. Codex therefore classifies unknown with
2664
+ # an explicit reason rather than falling back to idle, and no busy
2665
+ # wiring is installed. The turn-end NOTIFICATION marker still rides
2666
+ # the launch command via -c notify=[...] and __TURNEND__.
2667
+ ;;
2668
+ grok*)
2669
+ # grok fires a Stop hook at every turn boundary (verified, grok 0.2.73), the
2670
+ # clean equivalent of codex's notify= and pi's turn_end. But grok only loads
2671
+ # PROJECT hooks (<worktree>/.grok/hooks/, <worktree>/.claude/settings.local.json)
2672
+ # after the folder is granted hook-trust, which is not automatic and which
2673
+ # firstmate cannot establish at launch without editing grok's own managed
2674
+ # trust store (a high-blast-radius write). GLOBAL hooks in ~/.grok/hooks/ are
2675
+ # always trusted and load on first launch with no gate. So the turn-end hook
2676
+ # lives OUTSIDE the worktree as a single firstmate-owned global hook that is a
2677
+ # guarded no-op for every non-firstmate grok session: it fires only when the
2678
+ # current workspace holds a .fm-grok-turnend token pointer that matches the
2679
+ # firstmate-owned hook registry. firstmate then drops that per-task pointer
2680
+ # (gitignored, like the other harnesses' worktree hook files).
2681
+ # Result: the hook is outside the worktree, needs no trust grant, and never
2682
+ # touches grok's managed config - only firstmate-owned files.
2683
+ GROK_HOOKS_DIR="${GROK_HOME:-$HOME/.grok}/hooks"
2684
+ GROK_AUTH_DIR="$GROK_HOOKS_DIR/fm-turn-end.d"
2685
+ mkdir -p "$GROK_AUTH_DIR"
2686
+ old_umask=$(umask)
2687
+ umask 077
2688
+ auth_file=$(mktemp "$GROK_AUTH_DIR/fm.XXXXXXXXXXXX")
2689
+ umask "$old_umask"
2690
+ printf '%s\n' "$TURNEND" > "$auth_file"
2691
+ printf '%s\n' "${auth_file##*/}" > "$STATE/$ID.grok-turnend-token"
2692
+ sq_grok_auth_dir=$(shell_quote "$GROK_AUTH_DIR")
2693
+ cat > "$GROK_HOOKS_DIR/fm-turn-end.sh" <<EOF
2694
+ #!/usr/bin/env bash
2695
+ set -u
2696
+ auth_dir=$sq_grok_auth_dir
2697
+ workspace=\${GROK_WORKSPACE_ROOT:-}
2698
+ [ -n "\$workspace" ] || exit 0
2699
+ p="\$workspace/.fm-grok-turnend"
2700
+ [ -f "\$p" ] || exit 0
2701
+ first=
2702
+ IFS= read -r -n 256 first < "\$p" 2>/dev/null || [ -n "\$first" ] || exit 0
2703
+ case "\$first" in token=*) token=\${first#token=} ;; *) exit 0 ;; esac
2704
+ case "\$token" in fm.????????????) : ;; *) exit 0 ;; esac
2705
+ case "\$token" in *[!A-Za-z0-9._-]*) exit 0 ;; esac
2706
+ t=\$(cat "\$auth_dir/\$token" 2>/dev/null) || exit 0
2707
+ case "\$t" in /*.turn-ended) : ;; *) exit 0 ;; esac
2708
+ touch "\$t" 2>/dev/null || true
2709
+ exit 0
2710
+ EOF
2711
+ chmod +x "$GROK_HOOKS_DIR/fm-turn-end.sh"
2712
+ hook_command=$(json_escape "bash $(shell_quote "$GROK_HOOKS_DIR/fm-turn-end.sh")")
2713
+ printf '{"hooks":{"Stop":[{"hooks":[{"type":"command","command":"%s"}]}]}}\n' "$hook_command" > "$GROK_HOOKS_DIR/fm-turn-end.json"
2714
+ printf 'token=%s\n' "${auth_file##*/}" > "$WT/.fm-grok-turnend"
2715
+ exclude_path '.fm-grok-turnend'
2716
+ ;;
2717
+ muse*)
2718
+ # muse's turn lifecycle is neither a hook nor a launch flag: its plugin
2719
+ # engine (the only hook surface) is disabled in the default build, so
2720
+ # firstmate reads muse's own durable session event log instead
2721
+ # (bin/fm-busy-lib.sh owns the fold). That is a PULL
2722
+ # source with no writer, so nothing is armed and no record is seeded -
2723
+ # exactly the reason standalone Kimi is not armed either.
2724
+ # This sidecar is the whole binding: it pins the sessions root, the
2725
+ # workspace root that muse records in each log's metadata, this pane's
2726
+ # binding identity, and every matching main log that predates this pane.
2727
+ # The classifier then accepts only one new matching log, so it never
2728
+ # guesses between pane incarnations. Recording the resolved root here
2729
+ # also means a later change to XDG_DATA_HOME cannot silently re-point an
2730
+ # already-running task at a different log tree.
2731
+ MUSE_SESSIONS_ROOT="${MUSE_DATA_HOME:-${XDG_DATA_HOME:-$HOME/.local/share}}/muse/sessions"
2732
+ MUSE_BINDING_ID="$$.$RANDOM.$(date +%s)"
2733
+ rm -f "$STATE/$ID.muse-session-current"
2734
+ {
2735
+ printf 'sessions_root=%s\n' "$MUSE_SESSIONS_ROOT"
2736
+ printf 'workspace_root=%s\n' "$WT"
2737
+ printf 'binding_id=%s\n' "$MUSE_BINDING_ID"
2738
+ while IFS= read -r MUSE_PRIOR_LOG; do
2739
+ [ -n "$MUSE_PRIOR_LOG" ] && printf 'prior_log=%s\n' "$MUSE_PRIOR_LOG"
2740
+ done <<EOF
2741
+ $(fm_busy_muse_matching_logs "$MUSE_SESSIONS_ROOT" "$WT" || true)
2742
+ EOF
2743
+ } > "$STATE/$ID.muse-session"
2744
+ ;;
2745
+ cursor*)
2746
+ # Cursor's turn lifecycle is neither a hook nor a launch flag: it writes
2747
+ # its own durable per-conversation transcript and brackets every turn
2748
+ # there (bin/fm-busy-lib.sh owns the fold). Like muse that is a PULL
2749
+ # source with no writer, so nothing is armed and no record is seeded.
2750
+ # This sidecar is the whole binding. It pins the projects root and the
2751
+ # exact workspace path cursor records in each project's
2752
+ # .workspace-trusted, plus every conversation that already exists for
2753
+ # that workspace, so a relaunch into a reused worktree folds its OWN
2754
+ # conversation instead of its predecessor's. The classifier then accepts
2755
+ # only one remaining conversation and never guesses between incarnations.
2756
+ CURSOR_PROJECTS_ROOT="${CURSOR_PROJECTS_ROOT_OVERRIDE:-$HOME/.cursor/projects}"
2757
+ {
2758
+ printf 'projects_root=%s\n' "$CURSOR_PROJECTS_ROOT"
2759
+ printf 'workspace_root=%s\n' "$WT"
2760
+ if CURSOR_PRIOR_PROJECT=$(fm_busy_cursor_project_dir "$CURSOR_PROJECTS_ROOT" "$WT" 2>/dev/null); then
2761
+ for CURSOR_PRIOR_DIR in "$CURSOR_PRIOR_PROJECT"/agent-transcripts/*/; do
2762
+ [ -d "$CURSOR_PRIOR_DIR" ] || continue
2763
+ printf 'prior_conversation=%s\n' "$(basename -- "${CURSOR_PRIOR_DIR%/}")"
2764
+ done
2765
+ fi
2766
+ } > "$STATE/$ID.cursor-session"
2767
+ ;;
2768
+ kimi*)
2769
+ # Kimi's Stop hook is global, but it is inert unless cwd contains this
2770
+ # task's token pointer and the token resolves through Firstmate's private
2771
+ # registry. The installer above owns the format-preserving config edit and
2772
+ # the always-zero, silent hook script.
2773
+ KIMI_AUTH_DIR="$HOME/.kimi-code/fm-turn-end.d"
2774
+ old_umask=$(umask)
2775
+ umask 077
2776
+ auth_file=$(mktemp "$KIMI_AUTH_DIR/fm.XXXXXXXXXXXX")
2777
+ umask "$old_umask"
2778
+ printf '%s\n' "$TURNEND" > "$auth_file"
2779
+ printf '%s\n' "${auth_file##*/}" > "$STATE/$ID.kimi-turnend-token"
2780
+ printf 'token=%s\n' "${auth_file##*/}" > "$WT/.fm-kimi-turnend"
2781
+ exclude_path '.fm-kimi-turnend'
2782
+ ;;
2783
+ esac
2784
+ fi
2785
+
2786
+ # Delivery posture recorded in meta so fm-teardown's safety check and the
2787
+ # validate/merge stages can branch on it. A ship task carries the explicit
2788
+ # per-task decision validated above; a secondmate's posture is fixed; a scout
2789
+ # records none at all, because its deliverable is a report rather than a merge
2790
+ # (fm-teardown.sh defaults an absent mode to no-mistakes, and fm-promote.sh
2791
+ # requires an explicit mode when a scout is promoted to a ship task).
2792
+ if [ "$KIND" = secondmate ]; then
2793
+ MODE=secondmate
2794
+ YOLO=off
2795
+ : "${SECONDMATE_PROJECTS:=}"
2796
+ elif [ "$KIND" = scout ]; then
2797
+ MODE=
2798
+ YOLO=
2799
+ fi
2800
+
2801
+ # Resolve the optional default-off W3C trace context (bin/fm-trace-context-lib.sh,
2802
+ # docs/configuration.md): the one carrier both recorded in meta and injected into
2803
+ # the pane, so an observer reads exactly what the child receives. Empty only when
2804
+ # disabled or on entropy/validation failure. Reuses this task's already-recorded
2805
+ # value on relaunch; any other spawn roots a fresh trace, never adopting this
2806
+ # process's own ambient TRACEPARENT, so each routed task is its own trace
2807
+ # boundary even under a persistent supervisor. Never aborts the spawn and adds
2808
+ # only the cost of reading a few bytes of entropy.
2809
+ #
2810
+ # The session-start path owns input resolution. Spawn consumes only the frozen
2811
+ # home-session state and reuses it for the carrier and Secondmate launch prefix.
2812
+ #
2813
+ # A remote secondmate launch is the one case where this process is not the home
2814
+ # that owns the task's identity: the parent home resolved and will record the
2815
+ # carrier, and this host only delivers it. The validated --traceparent value
2816
+ # then IS the decision, so the enablement snapshot handed to the new Secondmate
2817
+ # agrees with the carrier it receives exactly as on the local path.
2818
+ if [ "$TRACEPARENT_SET" -eq 1 ]; then
2819
+ SPAWN_TRACE_EFFECTIVE=on
2820
+ SPAWN_TRACEPARENT=$TRACEPARENT_ARG
2821
+ else
2822
+ SPAWN_TRACE_EFFECTIVE=$(fm_trace_context_session_effective "$STATE/.trace-context-effective")
2823
+ if [ "$SPAWN_TRACE_EFFECTIVE" = on ]; then
2824
+ SPAWN_TRACEPARENT=$(FM_TRACE_CONTEXT=on fm_trace_context_resolve "$CONFIG" "$STATE/$ID.meta" || true)
2825
+ else
2826
+ SPAWN_TRACEPARENT=
2827
+ fi
2828
+ fi
2829
+
2830
+ META_WINDOW=$T
2831
+ [ "$BACKEND" = orca ] && META_WINDOW=$W
2832
+ SPAWN_GEN="s$(date +%s).${BASHPID:-$$}.$RANDOM"
2833
+ SPAWN_META_PATH="$STATE/$ID.meta"
2834
+ if [ "$SPAWN_META_LOCK_HELD" != 1 ]; then
2835
+ SPAWN_META_LOCK=$(fm_meta_lock_path "$STATE/$ID.meta") || exit 1
2836
+ fm_lock_acquire_wait "$SPAWN_META_LOCK"
2837
+ SPAWN_META_LOCK_HELD=1
2838
+ fi
2839
+ if [ "$RELAUNCH" -eq 1 ]; then
2840
+ SPAWN_META_TMP="$STATE/.$ID.meta.relaunch.${BASHPID:-$$}"
2841
+ else
2842
+ SPAWN_META_TMP="$STATE/.$ID.meta.spawn.${BASHPID:-$$}"
2843
+ SPAWN_FRESH_COMMIT_PENDING=1
2844
+ fi
2845
+ SPAWN_META_PATH=$SPAWN_META_TMP
2846
+ preserve_relaunch_meta() {
2847
+ awk -F= '
2848
+ BEGIN {
2849
+ split("window endpoint_task_id worktree project harness kind mode yolo tasktmp model effort busy_gen spawn_gen traceparent backend herdr_session herdr_workspace_id herdr_tab_id herdr_pane_id zellij_session zellij_tab_id zellij_pane_id orca_worktree_id terminal cmux_workspace_id cmux_surface_id home projects control_relaunch_tx", keys, " ")
2850
+ for (i in keys) owned[keys[i]] = 1
2851
+ }
2852
+ !($1 in owned)
2853
+ ' "$RELAUNCH_META"
2854
+ }
2855
+ {
2856
+ echo "window=$META_WINDOW"
2857
+ echo "endpoint_task_id=$ID"
2858
+ echo "worktree=$WT"
2859
+ echo "project=$PROJ_ABS"
2860
+ echo "harness=$HARNESS"
2861
+ echo "kind=$KIND"
2862
+ [ -z "$MODE" ] || echo "mode=$MODE"
2863
+ [ -z "$YOLO" ] || echo "yolo=$YOLO"
2864
+ echo "tasktmp=$TASK_TMP"
2865
+ echo "model=${MODEL:-default}"
2866
+ echo "effort=${EFFORT:-default}"
2867
+ [ -z "${BUSY_GEN:-}" ] || echo "busy_gen=$BUSY_GEN"
2868
+ echo "spawn_gen=$SPAWN_GEN"
2869
+ # Default-off writes no traceparent= line.
2870
+ # backend= is written only for a non-default (non-tmux) backend, so the
2871
+ # default path's meta stays byte-identical (absent backend= means tmux;
2872
+ # data/fm-backend-design-d7's P1 compatibility contract).
2873
+ [ "$BACKEND" = tmux ] || echo "backend=$BACKEND"
2874
+ if [ "$BACKEND" = herdr ]; then
2875
+ echo "herdr_session=$HERDR_SES"
2876
+ echo "herdr_workspace_id=$HERDR_WORKSPACE_ID"
2877
+ echo "herdr_tab_id=$HERDR_TAB_ID"
2878
+ echo "herdr_pane_id=$HERDR_PANE_ID"
2879
+ fi
2880
+ if [ "$BACKEND" = zellij ]; then
2881
+ echo "zellij_session=$ZELLIJ_SES"
2882
+ echo "zellij_tab_id=$ZELLIJ_TAB_ID"
2883
+ echo "zellij_pane_id=$ZELLIJ_PANE_ID"
2884
+ fi
2885
+ if [ "$BACKEND" = orca ]; then
2886
+ echo "orca_worktree_id=$ORCA_WORKTREE_ID"
2887
+ echo "terminal=$ORCA_TERMINAL"
2888
+ fi
2889
+ if [ "$BACKEND" = cmux ]; then
2890
+ echo "cmux_workspace_id=$CMUX_WORKSPACE_ID"
2891
+ echo "cmux_surface_id=$CMUX_SURFACE_ID"
2892
+ fi
2893
+ if [ "$KIND" = secondmate ]; then
2894
+ echo "home=$PROJ_ABS"
2895
+ echo "projects=$SECONDMATE_PROJECTS"
2896
+ fi
2897
+ if [ "$RELAUNCH" -eq 1 ]; then
2898
+ preserve_relaunch_meta
2899
+ fi
2900
+ if [ "$SPAWN_CONTROL_PARENT" = 1 ] && [ -n "${FM_CONTROL_RELAUNCH_TX:-}" ]; then
2901
+ echo "control_relaunch_tx=$FM_CONTROL_RELAUNCH_TX"
2902
+ fi
2903
+ } > "$SPAWN_META_PATH" || {
2904
+ echo "error: task record for $ID could not be prepared at $SPAWN_META_PATH" >&2
2905
+ exit 1
2906
+ }
2907
+ if [ "$RELAUNCH" -eq 0 ]; then
2908
+ if ! fm_backlog_atomic_transition publish "$SPAWN_META_TMP" "$STATE/$ID.meta" "task record" "$STATE"; then
2909
+ echo "error: task record for $ID could not be published ($FM_BACKLOG_TRANSITION_ERROR)" >&2
2910
+ exit 1
2911
+ fi
2912
+ SPAWN_META_TMP=
2913
+ fi
2914
+
2915
+ # Fuse the backlog In-flight transition into the publication that just created
2916
+ # the record (bin/fm-backlog-transition-lib.sh owns the invariant). It runs under
2917
+ # this task's own meta lock, so a steer or teardown racing the same id stays
2918
+ # serialized exactly as before. The call itself is deferred to the final commit
2919
+ # point below so every earlier launch-delivery failure remains unwindable.
2920
+ spawn_commit_backlog_transition() {
2921
+ [ "$BACKLOG_TRANSITION" = 1 ] || return 0
2922
+ fm_backlog_atomic_transition dispatch "$STATE/$ID.meta" "$DATA" "$ID" "$STATE"
2923
+ }
2924
+
2925
+ if [ "$RELAUNCH" -eq 1 ]; then
2926
+ SPAWN_META_PUBLISH_STARTED=1
2927
+ if ! fm_backlog_atomic_transition publish "$SPAWN_META_TMP" "$STATE/$ID.meta" "task record" "$STATE"; then
2928
+ echo "error: replacement task record for $ID could not be published ($FM_BACKLOG_TRANSITION_ERROR)" >&2
2929
+ exit 1
2930
+ fi
2931
+ RELAUNCH_REPLACEMENT_PENDING=0
2932
+ SPAWN_META_PUBLISH_STARTED=0
2933
+ SPAWN_META_TMP=
2934
+ fi
2935
+ # A dispatch or relaunch keeps the per-task meta lock through launch delivery.
2936
+ # The backlog mutation is deliberately the final fallible commit below, so
2937
+ # teardown cannot remove a relaunched record while its replacement worker is
2938
+ # still being delivered, cannot observe or complete a fresh provisional record
2939
+ # between its state check and `tasks-axi start`, and a delivery failure cannot
2940
+ # follow a committed In-flight transition.
2941
+ if [ "$SPAWN_TASK_SET_LOCK_HELD" = 1 ]; then
2942
+ # The record is published, so this task is now part of the set a teardown
2943
+ # enumerates and locks per task. The set lock is only needed across that
2944
+ # publication.
2945
+ SPAWN_TASK_SET_LOCK_HELD=0
2946
+ fm_lock_release "$SPAWN_TASK_SET_LOCK"
2947
+ fi
2948
+ "$SCRIPT_DIR/fm-home-summary-refresh.sh" --best-effort || true
2949
+ [ "$BACKEND" = orca ] && ORCA_ABORT_CLEANUP=0
2950
+
2951
+ sq_brief=$(shell_quote "$BRIEF")
2952
+ sq_turnend=$(shell_quote "$TURNEND")
2953
+ sq_piext=$(shell_quote "$STATE/$ID.pi-ext.ts")
2954
+ sq_piturnend=$(shell_quote "$PROJ_ABS/.pi/extensions/fm-primary-turnend-guard.ts")
2955
+ sq_piwatch=$(shell_quote "$PROJ_ABS/.pi/extensions/fm-primary-pi-watch.ts")
2956
+ sq_opinput=$(shell_quote "$FM_ROOT/bin/fm-operational-input.sh")
2957
+ sq_worktree=$(shell_quote "$WT")
2958
+ MODELFLAG=$(model_flag_for_harness "$HARNESS" "$MODEL")
2959
+ EFFORTFLAG=$(effort_flag_for_harness "$HARNESS" "$EFFORT")
2960
+ LAUNCH=${LAUNCH//__MODELFLAG__/$MODELFLAG}
2961
+ LAUNCH=${LAUNCH//__EFFORTFLAG__/$EFFORTFLAG}
2962
+ LAUNCH=${LAUNCH//__BRIEF__/$sq_brief}
2963
+ LAUNCH=${LAUNCH//__TURNEND__/$sq_turnend}
2964
+ LAUNCH=${LAUNCH//__PIEXT__/$sq_piext}
2965
+ LAUNCH=${LAUNCH//__PITURNEND__/$sq_piturnend}
2966
+ LAUNCH=${LAUNCH//__PIWATCH__/$sq_piwatch}
2967
+ LAUNCH=${LAUNCH//__OPINPUT__/$sq_opinput}
2968
+ case "$HARNESS" in
2969
+ pi|pi-signed) LAUNCH=${LAUNCH//__PIBIN__/"$(shell_quote "$PI_BIN")"} ;;
2970
+ cursor) LAUNCH=${LAUNCH//__CURSORBIN__/"$(shell_quote "$CURSOR_BIN")"} ;;
2971
+ esac
2972
+ LAUNCH=${LAUNCH//__WORKTREE__/$sq_worktree}
2973
+ case "$HARNESS" in
2974
+ claude|codex|opencode|pi|pi-signed|grok|kimi|muse)
2975
+ LAUNCH="env -u CURSOR_AGENT -u CURSOR_INVOKED_AS $LAUNCH"
2976
+ ;;
2977
+ esac
2978
+ # Crewmate panes are created by a long-lived tmux/herdr daemon that does not
2979
+ # inherit firstmate's current environment, so a bare `claude` in the pane falls
2980
+ # back to the default ~/.claude store even when firstmate itself runs under a
2981
+ # different CLAUDE_CONFIG_DIR (for example a work-vs-personal subscription split).
2982
+ # Forward firstmate's own resolved store onto the claude launch so the crewmate
2983
+ # uses the same credential/config firstmate is authenticated with. Only when set;
2984
+ # an unset value is the single-store default and needs no prefix.
2985
+ if [ "$HARNESS" = claude ] && [ -n "${CLAUDE_CONFIG_DIR:-}" ]; then
2986
+ LAUNCH="CLAUDE_CONFIG_DIR=$(shell_quote "$CLAUDE_CONFIG_DIR") $LAUNCH"
2987
+ fi
2988
+ if [ "$KIND" = secondmate ]; then
2989
+ sq_home=$(shell_quote "$PROJ_ABS")
2990
+ sq_primary_home=$(shell_quote "$FM_HOME")
2991
+ # Keep this in step with fm_supervision_model (bin/fm-wake-lib.sh): Claude's
2992
+ # Stop auto-arm and Cursor's stop-hook park both run the watcher only BETWEEN
2993
+ # turns, so a fresh beacon with no live watcher is their healthy mid-turn state.
2994
+ case "$HARNESS" in
2995
+ claude|cursor) supervision_model=autoarm ;;
2996
+ *) supervision_model=persistent ;;
2997
+ esac
2998
+ # Deliver the primary's EFFECTIVE trace-context decision as a normalized on/off
2999
+ # literal (never the raw FM_TRACE_CONTEXT string) so a FM_TRACE_CONTEXT override
3000
+ # on the primary reaches the secondmate's OWN workers, not just the copied
3001
+ # config/trace-context file: otherwise off would not disable them and on would
3002
+ # not enable them across the launch boundary (bin/fm-trace-context-lib.sh header).
3003
+ # Reuse the single frozen decision from the carrier resolution above so the
3004
+ # injected carrier and this on/off snapshot are guaranteed to agree.
3005
+ LAUNCH="FM_ROOT_OVERRIDE= FM_STATE_OVERRIDE= FM_DATA_OVERRIDE= FM_PROJECTS_OVERRIDE= FM_CONFIG_OVERRIDE= FM_PUBLIC_FOLLOWUP_PRIMARY_HOME=$sq_primary_home FM_HOME=$sq_home FM_TRACE_CONTEXT=$SPAWN_TRACE_EFFECTIVE FM_SUPERVISION_MODEL=$supervision_model $LAUNCH"
3006
+ fi
3007
+ if [ -z "$SPAWN_TRACEPARENT" ] && [ "$RELAUNCH" -eq 1 ]; then
3008
+ LAUNCH="unset TRACEPARENT; $LAUNCH"
3009
+ fi
3010
+
3011
+ spawn_record_traceparent() {
3012
+ local meta="$STATE/$ID.meta" status=0 acquired=0
3013
+ # Fresh publication still owns the lock. Relaunch deliberately uses a short
3014
+ # independent critical section so other metadata interfaces can serialize.
3015
+ if [ "$SPAWN_META_LOCK_HELD" != 1 ]; then
3016
+ SPAWN_META_LOCK=$(fm_meta_lock_path "$meta") || return 1
3017
+ fm_lock_acquire_wait "$SPAWN_META_LOCK"
3018
+ SPAWN_META_LOCK_HELD=1
3019
+ acquired=1
3020
+ fi
3021
+ SPAWN_META_TMP="$STATE/.$ID.meta.trace.${BASHPID:-$$}"
3022
+ if [ ! -f "$meta" ] || [ ! -w "$meta" ] \
3023
+ || ! awk -F= '$1 != "traceparent"' "$meta" > "$SPAWN_META_TMP" \
3024
+ || ! printf 'traceparent=%s\n' "$SPAWN_TRACEPARENT" >> "$SPAWN_META_TMP" \
3025
+ || ! fm_backlog_atomic_transition publish "$SPAWN_META_TMP" "$meta" "task record" "$STATE"; then
3026
+ status=1
3027
+ rm -f "$SPAWN_META_TMP" 2>/dev/null || true
3028
+ fi
3029
+ SPAWN_META_TMP=
3030
+ if [ "$acquired" = 1 ]; then
3031
+ fm_lock_release "$SPAWN_META_LOCK" || status=1
3032
+ SPAWN_META_LOCK_HELD=0
3033
+ fi
3034
+ return "$status"
3035
+ }
3036
+
3037
+ # Export GOTMPDIR into the crewmate's pane shell so the agent and every child
3038
+ # process (go build, go test, ...) inherit it. Sent before the launch command so
3039
+ # the env is set when the agent starts; the brief sleep lets the export land.
3040
+ spawn_send_text_line "$T" "export GOTMPDIR=$TASK_TMP/gotmp"
3041
+ # Send through the exact channel that already ships GOTMPDIR, so every backend
3042
+ # and harness - ship, scout, and secondmate - gets it before launch. Skipped
3043
+ # entirely when trace context is off.
3044
+ if [ -n "$SPAWN_TRACEPARENT" ]; then
3045
+ if spawn_send_text_line "$T" "export TRACEPARENT=$SPAWN_TRACEPARENT"; then
3046
+ if ! spawn_record_traceparent; then
3047
+ LAUNCH="unset TRACEPARENT; $LAUNCH"
3048
+ fi
3049
+ else
3050
+ TRACE_SEND_STATUS=$?
3051
+ if [ "$TRACE_SEND_STATUS" -eq 2 ]; then
3052
+ echo "error: trace-context input could not be cleared for $W; refusing to append the launch command" >&2
3053
+ exit 1
3054
+ fi
3055
+ LAUNCH="unset TRACEPARENT; $LAUNCH"
3056
+ fi
3057
+ fi
3058
+ sleep 0.3
3059
+ spawn_send_literal "$T" "$LAUNCH"
3060
+ sleep 0.3
3061
+ if [ "${HERDR_PROJECTED:-0}" -eq 1 ]; then
3062
+ HERDR_PROJECTION_ABORT_CLEANUP=0
3063
+ spawn_herdr_presentation_order_lock_release
3064
+ fi
3065
+ spawn_send_key "$T" Enter
3066
+ if [ "$HARNESS" = kimi ]; then
3067
+ if ! kimi_wait_for_ready; then
3068
+ kimi_spawn_fail "kimi did not show a verified ready signal before brief delivery"
3069
+ exit 1
3070
+ fi
3071
+ KIMI_POINTER="Read the brief at $BRIEF_REAL and follow it exactly."
3072
+ KIMI_SUBMIT_RETRIES=${FM_KIMI_SUBMIT_RETRIES:-3}
3073
+ KIMI_SUBMIT_SLEEP=${FM_KIMI_SUBMIT_SLEEP:-${FM_KIMI_POLL_INTERVAL:-0.5}}
3074
+ KIMI_SUBMIT_SETTLE=${FM_KIMI_SUBMIT_SETTLE:-0}
3075
+ if ! KIMI_SUBMIT_VERDICT=$(fm_backend_send_text_submit \
3076
+ "$BACKEND" "$T" "$KIMI_POINTER" "$KIMI_SUBMIT_RETRIES" \
3077
+ "$KIMI_SUBMIT_SLEEP" "$KIMI_SUBMIT_SETTLE" "$W"); then
3078
+ kimi_spawn_fail "kimi brief pointer could not be submitted"
3079
+ exit 1
3080
+ fi
3081
+ if [ "$KIMI_SUBMIT_VERDICT" = send-failed ]; then
3082
+ kimi_spawn_fail "kimi brief pointer could not be submitted"
3083
+ exit 1
3084
+ fi
3085
+ if ! kimi_wait_for_delivery; then
3086
+ kimi_spawn_fail "kimi brief pointer delivery was not confirmed"
3087
+ exit 1
3088
+ fi
3089
+ fi
3090
+ if [ "$KIND" = secondmate ] && [ "${FM_SKIP_SECONDMATE_INHERIT:-0}" != 1 ]; then
3091
+ if ! fm_config_reread_discard_pending "$PROJ_ABS" "$ID" "$FM_HOME"; then
3092
+ if fm_config_reread_quarantine_pending "$PROJ_ABS" "$ID" "$FM_HOME"; then
3093
+ echo "CONFIG_REREAD: secondmate $ID: quarantined pre-relaunch generations after cleanup failure (destination=$PROJ_ABS/state/.fm-inherited-config-reread-quarantine source=$FM_HOME/state/.fm-inherited-config-reread-quarantine)" >&2
3094
+ else
3095
+ echo "CONFIG_REREAD: secondmate $ID: cleanup failed; pre-relaunch generations were force-cleared where possible (destination=$PROJ_ABS source=$FM_HOME)" >&2
3096
+ fi
3097
+ fi
3098
+ fi
3099
+
3100
+ # This is the commit point: all endpoint and harness delivery that can reject
3101
+ # the spawn has succeeded. Re-read and transition while holding the same
3102
+ # per-task lock as metadata publication, then and only then report success.
3103
+ if [ "$SPAWN_META_LOCK_HELD" != 1 ]; then
3104
+ SPAWN_META_LOCK=$(fm_meta_lock_path "$STATE/$ID.meta") || exit 1
3105
+ fm_lock_acquire_wait "$SPAWN_META_LOCK"
3106
+ SPAWN_META_LOCK_HELD=1
3107
+ fi
3108
+ SPAWN_DEFERRED_SIGNAL=
3109
+ if [ "$BACKLOG_TRANSITION" = 1 ]; then
3110
+ trap 'SPAWN_DEFERRED_SIGNAL=HUP' HUP
3111
+ trap 'SPAWN_DEFERRED_SIGNAL=INT' INT
3112
+ trap 'SPAWN_DEFERRED_SIGNAL=TERM' TERM
3113
+ fi
3114
+ SPAWN_BACKLOG_COMMIT_STATUS=0
3115
+ if spawn_commit_backlog_transition; then
3116
+ SPAWN_FRESH_COMMIT_PENDING=0
3117
+ else
3118
+ SPAWN_BACKLOG_COMMIT_STATUS=$?
3119
+ if spawn_commit_backlog_transition; then
3120
+ SPAWN_BACKLOG_COMMIT_STATUS=0
3121
+ SPAWN_FRESH_COMMIT_PENDING=0
3122
+ fi
3123
+ fi
3124
+ if [ "$SPAWN_BACKLOG_COMMIT_STATUS" -ne 0 ]; then
3125
+ if [ "$RELAUNCH" -eq 0 ]; then
3126
+ if spawn_fresh_commit_rollback; then
3127
+ echo "error: task $ID's backlog item could not be moved to In flight ($FM_BACKLOG_TRANSITION_ERROR); its record was removed so no worker is left that the backlog does not own - close out endpoint $T and local copy $WT by hand, then re-run the spawn" >&2
3128
+ else
3129
+ echo "error: task $ID's backlog item could not be moved to In flight ($FM_BACKLOG_TRANSITION_ERROR), and failed-dispatch cleanup is incomplete; the provisional record may remain at $STATE/$ID.meta - close out endpoint $T and local copy $WT by hand, then remove the record and busy state before retrying" >&2
3130
+ fi
3131
+ else
3132
+ echo "error: task $ID was republished but its backlog item could not be moved to In flight ($FM_BACKLOG_TRANSITION_ERROR); fix the backlog and re-run the relaunch" >&2
3133
+ fi
3134
+ fi
3135
+ trap - HUP INT TERM
3136
+ if [ "$SPAWN_BACKLOG_COMMIT_STATUS" -ne 0 ]; then
3137
+ exit "$SPAWN_BACKLOG_COMMIT_STATUS"
3138
+ fi
3139
+ fm_lock_release "$SPAWN_META_LOCK"
3140
+ SPAWN_META_LOCK_HELD=0
3141
+ if [ -n "$SPAWN_DEFERRED_SIGNAL" ]; then
3142
+ case "$SPAWN_DEFERRED_SIGNAL" in
3143
+ HUP) SPAWN_DEFERRED_SIGNAL_STATUS=129 ;;
3144
+ INT) SPAWN_DEFERRED_SIGNAL_STATUS=130 ;;
3145
+ TERM) SPAWN_DEFERRED_SIGNAL_STATUS=143 ;;
3146
+ esac
3147
+ echo "error: spawn of $ID was interrupted after launch delivery began; its paired task record and In-flight backlog state were preserved" >&2
3148
+ exit "$SPAWN_DEFERRED_SIGNAL_STATUS"
3149
+ fi
3150
+
3151
+ SPAWN_DELIVERY=
3152
+ [ -z "$MODE" ] || SPAWN_DELIVERY=" mode=$MODE yolo=$YOLO"
3153
+ echo "spawned $ID harness=$HARNESS kind=$KIND$SPAWN_DELIVERY window=$META_WINDOW worktree=$WT"