@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,1530 @@
1
+ #!/usr/bin/env bash
2
+ # fm-control.sh relaunch: the transactional replace-the-agent verb.
3
+ #
4
+ # Relaunch is the only control verb that changes durable records, so these
5
+ # tests pin the transaction itself, hermetically (stubbed session provider, no
6
+ # real agent):
7
+ # 1. A same-harness relaunch keeps every identity axis and reuses the SAME
8
+ # endpoint and worktree - it replaces an agent, it never forks a task.
9
+ # 2. A harness switch is one ordinary relaunch: the record follows, the
10
+ # previous harness's per-task wiring is cleared, and profile axes chosen
11
+ # for the old harness do not silently carry to the new one.
12
+ # 3. The progress note is required where the replacement needs it, lands in
13
+ # the instructions the replacement reads, and never rewrites a charter.
14
+ # 4. A refusal before the agent is stopped changes nothing.
15
+ # 5. A launch failure after the agent is stopped keeps the prior record,
16
+ # reports the concrete state, and preserves the work.
17
+ # 6. fm-spawn --relaunch refuses on its own: a live agent, a contradicting
18
+ # flag, an extra positional, or a backend that cannot prove the previous
19
+ # agent exited.
20
+ set -u
21
+
22
+ # shellcheck source=tests/lib.sh
23
+ . "$(dirname "${BASH_SOURCE[0]}")/lib.sh"
24
+ # shellcheck source=/dev/null
25
+ . "$ROOT/bin/fm-control-lib.sh"
26
+ # shellcheck source=/dev/null
27
+ . "$ROOT/bin/fm-trace-context-lib.sh"
28
+
29
+ CONTROL="$ROOT/bin/fm-control.sh"
30
+ SPAWN="$ROOT/bin/fm-spawn.sh"
31
+ PROMOTE="$ROOT/bin/fm-promote.sh"
32
+ X_LINK="$ROOT/bin/fm-x-link.sh"
33
+ # fm_test_tmproot's own cleanup trap fires when its command substitution exits,
34
+ # so recreate the root before resolving it and clean it up from this file's trap.
35
+ TMP_ROOT=$(fm_test_tmproot fm-control-relaunch)
36
+ mkdir -p "$TMP_ROOT"
37
+ TMP_ROOT=$(cd "$TMP_ROOT" && pwd)
38
+ TASK_TMPS=()
39
+
40
+ relaunch_cleanup() {
41
+ local d
42
+ for d in "${TASK_TMPS[@]:-}"; do
43
+ [ -n "$d" ] && rm -rf "$d"
44
+ done
45
+ rm -rf "$TMP_ROOT"
46
+ }
47
+ trap relaunch_cleanup EXIT
48
+
49
+ # The same lifecycle-modelling tmux stub as tests/fm-control.test.sh: the
50
+ # harness's exit command stops the agent, and a launch-brief literal starts the
51
+ # harness named in `becomes`.
52
+ make_tmux_stub() { # <dir>
53
+ local fb="$1/fakebin"
54
+ mkdir -p "$fb"
55
+ cat > "$fb/tmux" <<'SH'
56
+ #!/usr/bin/env bash
57
+ set -u
58
+ D=$FM_FAKE_DIR
59
+ case "${1:-}" in
60
+ send-keys)
61
+ shift
62
+ literal=0
63
+ while [ $# -gt 0 ]; do
64
+ case "$1" in
65
+ -t) shift 2 ;;
66
+ -l) literal=1; shift ;;
67
+ *) break ;;
68
+ esac
69
+ done
70
+ payload=${1:-}
71
+ if [ "$literal" = 1 ]; then
72
+ printf '%s\n' "$payload" >> "$D/literal"
73
+ case "$payload" in
74
+ /exit|/quit)
75
+ printf 'zsh' > "$D/command"
76
+ [ -z "${FM_FAKE_EXIT_TRANSPORT_FAIL_AFTER_STOP:-}" ] || exit 1
77
+ ;;
78
+ *'encode launch-brief'*)
79
+ cat "$D/becomes" > "$D/command"
80
+ [ -z "${FM_FAKE_LAUNCH_TRANSPORT_FAIL_AFTER_START:-}" ] || exit 1
81
+ ;;
82
+ esac
83
+ else
84
+ printf '%s\n' "$payload" >> "$D/keys"
85
+ case "$payload" in
86
+ 'export GOTMPDIR='*)
87
+ if [ -n "${FM_FAKE_TRACE_PREPARE:-}" ]; then
88
+ : > "$FM_FAKE_TRACE_PREPARE"
89
+ while [ ! -e "$FM_FAKE_TRACE_RELEASE" ]; do /bin/sleep 0.01; done
90
+ fi
91
+ ;;
92
+ 'export TRACEPARENT='*)
93
+ [ -z "${FM_FAKE_TRACE_EXPORTED:-}" ] || : > "$FM_FAKE_TRACE_EXPORTED"
94
+ ;;
95
+ esac
96
+ fi
97
+ exit 0 ;;
98
+ display-message)
99
+ for a in "$@"; do
100
+ case "$a" in
101
+ *cursor_y*) printf '1\n'; exit 0 ;;
102
+ *pane_current_command*) cat "$D/command"; printf '\n'; exit 0 ;;
103
+ *pane_current_path*)
104
+ if [ -n "${FM_FAKE_CWD_RACE_READY:-}" ]; then
105
+ : > "$FM_FAKE_CWD_RACE_READY"
106
+ /bin/sleep 1
107
+ fi
108
+ cat "$D/cwd"; printf '\n'; exit 0 ;;
109
+ esac
110
+ done
111
+ printf 'fakepane\n'; exit 0 ;;
112
+ capture-pane) printf '╭────╮\n│ │\n╰────╯\n'; exit 0 ;;
113
+ list-windows) [ -f "$D/windows" ] && cat "$D/windows"; exit 0 ;;
114
+ esac
115
+ exit 0
116
+ SH
117
+ chmod +x "$fb/tmux"
118
+ cat > "$fb/sleep" <<'SH'
119
+ #!/usr/bin/env bash
120
+ [ -z "${FM_FAKE_LOCK_WAITING:-}" ] || : > "$FM_FAKE_LOCK_WAITING"
121
+ exit 0
122
+ SH
123
+ chmod +x "$fb/sleep"
124
+ }
125
+
126
+ # new_case <name> [id] -> echoes a case dir with a live claude ship task.
127
+ new_case() {
128
+ local id=${2:-t1} dir="$TMP_ROOT/$1-$RANDOM"
129
+ mkdir -p "$dir/home/state" "$dir/home/data" "$dir/fake"
130
+ : > "$dir/fake/literal"
131
+ : > "$dir/fake/keys"
132
+ printf 'claude' > "$dir/fake/command"
133
+ printf 'claude' > "$dir/fake/becomes"
134
+ printf '%s\n' "fm-$id" > "$dir/fake/windows"
135
+ make_tmux_stub "$dir"
136
+ printf '%s\n' "$dir"
137
+ }
138
+
139
+ # add_ship_task <case-dir> <id> [harness]
140
+ add_ship_task() {
141
+ local dir=$1 id=$2 harness=${3:-claude}
142
+ local home="$dir/home" proj="$dir/proj" wt="$dir/wt"
143
+ fm_git_worktree "$proj" "$wt" "task-$id"
144
+ mkdir -p "$home/data/$id"
145
+ printf '# brief for %s\n\nDo the thing.\n' "$id" > "$home/data/$id/brief.md"
146
+ {
147
+ echo "window=fmses:fm-$id"
148
+ echo "endpoint_task_id=$id"
149
+ echo "worktree=$wt"
150
+ echo "project=$proj"
151
+ echo "harness=$harness"
152
+ echo "kind=ship"
153
+ echo "mode=no-mistakes"
154
+ echo "yolo=off"
155
+ echo "tasktmp=/tmp/fm-$id"
156
+ echo "model=default"
157
+ echo "effort=default"
158
+ } > "$home/state/$id.meta"
159
+ printf '%s\n' "fm-$id" > "$dir/fake/windows"
160
+ printf '%s' "$wt" > "$dir/fake/cwd"
161
+ TASK_TMPS+=("/tmp/fm-$id")
162
+ }
163
+
164
+ run_control() { # <case-dir> <args...>
165
+ local dir=$1; shift
166
+ env PATH="$dir/fakebin:$PATH" FM_HOME="$dir/home" FM_FAKE_DIR="$dir/fake" \
167
+ FM_SPAWN_NO_GUARD=1 GROK_HOME="$dir/grokhome" \
168
+ FM_CONTROL_POLL=0.01 FM_CONTROL_EXIT_WAIT=0.05 FM_CONTROL_LAUNCH_WAIT=0.05 \
169
+ FM_REAL_GIT="${FM_REAL_GIT:-}" FM_FAKE_GIT_FAILURE="${FM_FAKE_GIT_FAILURE:-}" \
170
+ FM_REAL_MV="${FM_REAL_MV:-}" FM_FAKE_COMPLETE_JOURNAL_MV_FAIL="${FM_FAKE_COMPLETE_JOURNAL_MV_FAIL:-}" \
171
+ FM_FAKE_META_PUBLISH_MV_FAIL="${FM_FAKE_META_PUBLISH_MV_FAIL:-}" \
172
+ FM_FAKE_TRACE_PREPARE="${FM_FAKE_TRACE_PREPARE:-}" \
173
+ FM_FAKE_TRACE_RELEASE="${FM_FAKE_TRACE_RELEASE:-}" \
174
+ FM_FAKE_META_WRITER_READY="${FM_FAKE_META_WRITER_READY:-}" \
175
+ FM_FAKE_TRACE_EXPORTED="${FM_FAKE_TRACE_EXPORTED:-}" \
176
+ "$CONTROL" "$@" 2>&1
177
+ }
178
+
179
+ run_spawn() { # <case-dir> <args...>
180
+ local dir=$1; shift
181
+ env PATH="$dir/fakebin:$PATH" FM_HOME="$dir/home" FM_FAKE_DIR="$dir/fake" \
182
+ FM_SPAWN_NO_GUARD=1 GROK_HOME="$dir/grokhome" \
183
+ "$SPAWN" "$@" 2>&1
184
+ }
185
+
186
+ meta_field() { # <case-dir> <id> <key>
187
+ grep "^$3=" "$1/home/state/$2.meta" | tail -1 | cut -d= -f2-
188
+ }
189
+
190
+ journal_field() { # <case-dir> <id> <key>
191
+ grep "^$3=" "$1/home/state/$2.control-relaunch" | tail -1 | cut -d= -f2-
192
+ }
193
+
194
+ make_git_failure_stub() { # <case-dir>
195
+ cat > "$1/fakebin/git" <<'SH'
196
+ #!/usr/bin/env bash
197
+ case "${FM_FAKE_GIT_FAILURE:-}:$*" in
198
+ head:*' rev-parse --verify HEAD'|head:*' symbolic-ref -q HEAD') exit 128 ;;
199
+ status:*' status --porcelain') exit 128 ;;
200
+ esac
201
+ exec "$FM_REAL_GIT" "$@"
202
+ SH
203
+ chmod +x "$1/fakebin/git"
204
+ }
205
+
206
+ make_mv_failure_stub() { # <case-dir>
207
+ cat > "$1/fakebin/mv" <<'SH'
208
+ #!/usr/bin/env bash
209
+ if [ -n "${FM_FAKE_COMPLETE_JOURNAL_MV_FAIL:-}" ]; then
210
+ for path in "$@"; do
211
+ if [ -f "$path" ] && grep -Fqx 'phase=complete' "$path"; then
212
+ exit 1
213
+ fi
214
+ done
215
+ fi
216
+ if [ -n "${FM_FAKE_META_PUBLISH_MV_FAIL:-}" ]; then
217
+ for path in "$@"; do
218
+ [ "$path" != "$FM_FAKE_META_PUBLISH_MV_FAIL" ] || exit 1
219
+ done
220
+ fi
221
+ source_path=
222
+ target_path=
223
+ for path in "$@"; do
224
+ source_path=$target_path
225
+ target_path=$path
226
+ done
227
+ if [ -n "${FM_FAKE_META_WRITER_TARGET:-}" ] \
228
+ && [ "$target_path" = "$FM_FAKE_META_WRITER_TARGET" ] \
229
+ && grep -q '^x_request=' "$source_path" 2>/dev/null; then
230
+ : > "$FM_FAKE_META_WRITER_READY"
231
+ while [ ! -e "$FM_FAKE_META_WRITER_RELEASE" ]; do /bin/sleep 0.01; done
232
+ fi
233
+ exec "$FM_REAL_MV" "$@"
234
+ SH
235
+ chmod +x "$1/fakebin/mv"
236
+ }
237
+
238
+ make_rm_failure_stub() { # <case-dir>
239
+ cat > "$1/fakebin/rm" <<'SH'
240
+ #!/usr/bin/env bash
241
+ for arg in "$@"; do
242
+ if [ -n "${FM_FAKE_RM_FAIL_PATH:-}" ] && [ "$arg" = "$FM_FAKE_RM_FAIL_PATH" ]; then
243
+ exit 1
244
+ fi
245
+ done
246
+ exec "$FM_REAL_RM" "$@"
247
+ SH
248
+ chmod +x "$1/fakebin/rm"
249
+ }
250
+
251
+ # Give a case home a real backlog carrying <id>, so the relaunch path's paired
252
+ # backlog transition (bin/fm-backlog-transition-lib.sh) is live rather than
253
+ # skipped for want of a backlog file.
254
+ seed_backlog() { # <case-dir> <id> <queued|in_flight>
255
+ local dir=$1 id=$2 want=$3 file="$1/home/data/backlog.md"
256
+ printf '%s\n' '# Backlog' '' '## In flight' '' '## Queued' '' '## Done' > "$file"
257
+ tasks-axi add "$id" "relaunch fixture task" --kind ship --file "$file" >/dev/null
258
+ [ "$want" != in_flight ] || tasks-axi start "$id" --file "$file" >/dev/null
259
+ }
260
+
261
+ backlog_state() { # <case-dir> <id>
262
+ tasks-axi show "$2" --file "$1/home/data/backlog.md" 2>/dev/null |
263
+ sed -n 's/^ state: *//p' | head -1
264
+ }
265
+
266
+ # Shadow tasks-axi so every `start` fails and every other verb is real. A
267
+ # relaunch that re-reads the row before acting never calls it; one that assumes
268
+ # it must re-run the transition trips over it.
269
+ break_tasks_axi_start() { # <case-dir>
270
+ local dir=$1 real
271
+ real=$(command -v tasks-axi)
272
+ cat > "$dir/fakebin/tasks-axi" <<SH
273
+ #!/usr/bin/env bash
274
+ if [ "\${1:-}" = start ]; then
275
+ echo 'error: "start refused"' >&2
276
+ exit 1
277
+ fi
278
+ exec "$real" "\$@"
279
+ SH
280
+ chmod +x "$dir/fakebin/tasks-axi"
281
+ }
282
+
283
+ # --- 1. same-harness relaunch -----------------------------------------------
284
+
285
+ test_same_harness_relaunch_keeps_identity_and_reuses_the_endpoint() {
286
+ local dir out rc gen_before gen_after
287
+ dir=$(new_case same rl1)
288
+ add_ship_task "$dir" rl1 claude
289
+ gen_before=$("$ROOT/bin/fm-busy-event.sh" arm "$dir/home/state" rl1)
290
+ printf 'busy_gen=%s\n' "$gen_before" >> "$dir/home/state/rl1.meta"
291
+ out=$(run_control "$dir" rl1 relaunch --note "stopped mid-refactor"); rc=$?
292
+ expect_code 0 "$rc" "a same-harness relaunch should succeed"$'\n'"$out"
293
+ assert_contains "$out" "relaunched rl1 harness=claude from=claude" "the outcome should name the transition"
294
+ [ "$(meta_field "$dir" rl1 window)" = "fmses:fm-rl1" ] \
295
+ || fail "the endpoint must be reused, not recreated"
296
+ [ "$(meta_field "$dir" rl1 worktree)" = "$dir/wt" ] \
297
+ || fail "the worktree must be reused, not reallocated"
298
+ [ "$(meta_field "$dir" rl1 kind)" = ship ] || fail "kind must survive the relaunch"
299
+ [ "$(meta_field "$dir" rl1 project)" = "$dir/proj" ] || fail "project must survive the relaunch"
300
+ gen_after=$(meta_field "$dir" rl1 busy_gen)
301
+ [ -n "$gen_after" ] && [ "$gen_after" != "$gen_before" ] \
302
+ || fail "a relaunch must arm a fresh busy generation, got '$gen_after'"
303
+ [ "$(journal_field "$dir" rl1 phase)" = complete ] \
304
+ || fail "the transaction journal should end complete"
305
+ assert_grep "/exit" "$dir/fake/literal" "the previous agent should have been exited"
306
+ assert_grep "encode launch-brief" "$dir/fake/literal" "the replacement should have been launched"
307
+ pass "fm-control relaunch: a same-harness relaunch replaces the agent in the same endpoint and worktree"
308
+ }
309
+
310
+ test_relaunch_preserves_durable_task_metadata() {
311
+ local dir out rc
312
+ dir=$(new_case durable-meta rl19)
313
+ add_ship_task "$dir" rl19 claude
314
+ {
315
+ printf '%s\n' 'pr=https://github.com/example/repo/pull/19'
316
+ printf '%s\n' 'pr_head=feature/relaunch'
317
+ printf '%s\n' 'x_request=request-19'
318
+ printf '%s\n' 'decisions_reviewed=1'
319
+ } >> "$dir/home/state/rl19.meta"
320
+
321
+ out=$(run_control "$dir" rl19 relaunch --note "continuing review work"); rc=$?
322
+ expect_code 0 "$rc" "relaunch should preserve durable metadata"$'\n'"$out"
323
+ [ "$(meta_field "$dir" rl19 pr)" = "https://github.com/example/repo/pull/19" ] \
324
+ || fail "the task PR must survive relaunch"
325
+ [ "$(meta_field "$dir" rl19 pr_head)" = "feature/relaunch" ] \
326
+ || fail "the task PR head must survive relaunch"
327
+ [ "$(meta_field "$dir" rl19 x_request)" = "request-19" ] \
328
+ || fail "the task X request must survive relaunch"
329
+ [ "$(meta_field "$dir" rl19 decisions_reviewed)" = 1 ] \
330
+ || fail "the task decision state must survive relaunch"
331
+ pass "fm-control relaunch: durable task metadata survives replacement launch publication"
332
+ }
333
+
334
+ test_relaunch_serializes_concurrent_durable_metadata_publication() {
335
+ local dir control_pid link_pid rc i=0 traceparent prepare launch_release waiting ready release
336
+ dir=$(new_case metadata-race rl28)
337
+ add_ship_task "$dir" rl28 claude
338
+ printf '%s\n' "$$" > "$dir/home/state/.lock"
339
+ printf '%s on\n' "$$" > "$dir/home/state/.trace-context-effective"
340
+ make_mv_failure_stub "$dir"
341
+ prepare="$dir/trace-prepare"
342
+ launch_release="$dir/trace-release"
343
+ waiting="$dir/meta-writer-waiting"
344
+ ready="$dir/meta-writer-ready"
345
+ release="$dir/meta-writer-release"
346
+ FM_REAL_MV=$(command -v mv) \
347
+ FM_FAKE_TRACE_PREPARE="$prepare" \
348
+ FM_FAKE_TRACE_RELEASE="$launch_release" \
349
+ run_control "$dir" rl28 relaunch --note "continue after publication" > "$dir/control.out" &
350
+ control_pid=$!
351
+ while [ ! -e "$prepare" ] && [ "$i" -lt 200 ]; do
352
+ /bin/sleep 0.01
353
+ i=$((i + 1))
354
+ done
355
+ [ -e "$prepare" ] || {
356
+ kill "$control_pid" 2>/dev/null || true
357
+ wait "$control_pid" 2>/dev/null || true
358
+ fail "relaunch did not reach trace delivery"
359
+ }
360
+ env PATH="$dir/fakebin:$PATH" FM_HOME="$dir/home" FM_ROOT_OVERRIDE="$ROOT" \
361
+ FM_REAL_MV="$(command -v mv)" \
362
+ FM_FAKE_LOCK_WAITING="$waiting" \
363
+ FM_FAKE_META_WRITER_TARGET="$dir/home/state/rl28.meta" \
364
+ FM_FAKE_META_WRITER_READY="$ready" \
365
+ FM_FAKE_META_WRITER_RELEASE="$release" \
366
+ "$X_LINK" rl28 request-28 --carry-count 1 --carry-ts 1700000000 \
367
+ --carry-platform x --carry-max 280 > "$dir/link.out" 2>&1 &
368
+ link_pid=$!
369
+ i=0
370
+ while [ ! -e "$waiting" ] && [ "$i" -lt 200 ]; do
371
+ /bin/sleep 0.01
372
+ i=$((i + 1))
373
+ done
374
+ [ -e "$waiting" ] && [ ! -e "$ready" ] || {
375
+ : > "$launch_release"
376
+ : > "$release"
377
+ wait "$link_pid" 2>/dev/null || true
378
+ wait "$control_pid" 2>/dev/null || true
379
+ fail "a durable metadata writer was not blocked during relaunch delivery"
380
+ }
381
+ : > "$launch_release"
382
+ i=0
383
+ while [ ! -e "$ready" ] && [ "$i" -lt 200 ]; do
384
+ /bin/sleep 0.01
385
+ i=$((i + 1))
386
+ done
387
+ [ -e "$ready" ] || {
388
+ kill "$link_pid" "$control_pid" 2>/dev/null || true
389
+ wait "$link_pid" 2>/dev/null || true
390
+ wait "$control_pid" 2>/dev/null || true
391
+ fail "durable metadata writer did not resume after relaunch delivery committed"
392
+ }
393
+ : > "$release"
394
+ wait "$link_pid"; rc=$?
395
+ expect_code 0 "$rc" "concurrent X metadata publication should serialize"$'\n'"$(cat "$dir/link.out")"
396
+ wait "$control_pid"; rc=$?
397
+ expect_code 0 "$rc" "relaunch should complete before serialized metadata publication"$'\n'"$(cat "$dir/control.out")"
398
+ [ "$(meta_field "$dir" rl28 x_request)" = request-28 ] \
399
+ || fail "relaunch erased metadata published concurrently through the X interface"
400
+ [ "$(meta_field "$dir" rl28 x_followups)" = 1 ] \
401
+ || fail "relaunch erased the concurrent follow-up count"
402
+ traceparent=$(meta_field "$dir" rl28 traceparent)
403
+ fm_trace_context_valid "$traceparent" \
404
+ || fail "concurrent metadata publication erased the replacement's trace carrier"
405
+ pass "fm-control relaunch: delivery and concurrent task metadata publication serialize"
406
+ }
407
+
408
+ test_disabled_relaunch_clears_prior_trace_context() {
409
+ local dir out rc
410
+ dir=$(new_case trace-off rl33)
411
+ add_ship_task "$dir" rl33 claude
412
+ printf '%s\n' 'traceparent=00-aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa-bbbbbbbbbbbbbbbb-01' \
413
+ >> "$dir/home/state/rl33.meta"
414
+ printf '%s\n' "$$" > "$dir/home/state/.lock"
415
+ printf '%s off\n' "$$" > "$dir/home/state/.trace-context-effective"
416
+
417
+ out=$(run_control "$dir" rl33 relaunch --note "crossing trace boundary"); rc=$?
418
+ expect_code 0 "$rc" "disabled relaunch should succeed"$'\n'"$out"
419
+ [ -z "$(meta_field "$dir" rl33 traceparent)" ] \
420
+ || fail "disabled relaunch must remove the prior trace carrier from metadata"
421
+ grep -q '^unset TRACEPARENT; .*claude' "$dir/fake/literal" \
422
+ || fail "disabled relaunch must clear the pane carrier before replacement launch"
423
+ ! grep -q '^export TRACEPARENT=' "$dir/fake/literal" \
424
+ || fail "disabled relaunch must not export a replacement trace carrier"
425
+ pass "fm-control relaunch: disabling tracing clears metadata and pane context"
426
+ }
427
+
428
+ test_relaunch_appends_the_progress_note_to_the_instructions() {
429
+ local dir out rc brief
430
+ dir=$(new_case note rl2)
431
+ add_ship_task "$dir" rl2 claude
432
+ out=$(run_control "$dir" rl2 relaunch --note "reproduced the crash in parser.go"); rc=$?
433
+ expect_code 0 "$rc" "relaunch should succeed"$'\n'"$out"
434
+ brief="$dir/home/data/rl2/brief.md"
435
+ assert_grep "Do the thing." "$brief" "the original instructions must survive"
436
+ assert_grep "## Progress note" "$brief" "the note should be a dated section in the instructions"
437
+ assert_grep "reproduced the crash in parser.go" "$brief" "the note text should reach the replacement"
438
+ assert_grep "reproduced the crash in parser.go" "$dir/home/state/rl2.control-relaunch.note" \
439
+ "the note should also be preserved beside the transaction record"
440
+ pass "fm-control relaunch: the progress note lands in the instructions the replacement reads"
441
+ }
442
+
443
+ test_relaunch_requires_a_note_for_a_ship_task() {
444
+ local dir out rc before
445
+ dir=$(new_case nonote rl3)
446
+ add_ship_task "$dir" rl3 claude
447
+ before=$(cat "$dir/home/data/rl3/brief.md")
448
+ out=$(run_control "$dir" rl3 relaunch); rc=$?
449
+ expect_code 1 "$rc" "a ship relaunch without a note should refuse"
450
+ assert_contains "$out" "requires --note" "the refusal should name the missing note"
451
+ [ "$(cat "$dir/home/data/rl3/brief.md")" = "$before" ] \
452
+ || fail "a refused relaunch must not touch the instructions"
453
+ [ -z "$(cat "$dir/fake/literal")" ] || fail "a refused relaunch must send nothing"
454
+ [ "$(cat "$dir/fake/command")" = claude ] || fail "a refused relaunch must not stop the agent"
455
+ pass "fm-control relaunch: a ship task refuses without the progress note its replacement needs"
456
+ }
457
+
458
+ # --- 2. harness switch -------------------------------------------------------
459
+
460
+ test_harness_switch_moves_the_record_and_clears_prior_wiring() {
461
+ local dir out rc
462
+ dir=$(new_case switch rl4)
463
+ add_ship_task "$dir" rl4 claude
464
+ # Wiring the previous claude incarnation left in the worktree.
465
+ mkdir -p "$dir/wt/.claude"
466
+ printf '{"hooks":{}}\n' > "$dir/wt/.claude/settings.local.json"
467
+ printf 'codex' > "$dir/fake/becomes"
468
+ out=$(run_control "$dir" rl4 relaunch --harness codex --note "switching runtime"); rc=$?
469
+ expect_code 0 "$rc" "a harness switch should succeed"$'\n'"$out"
470
+ assert_contains "$out" "harness=codex from=claude" "the outcome should name both harnesses"
471
+ [ "$(meta_field "$dir" rl4 harness)" = codex ] || fail "the record should follow the switch"
472
+ [ ! -e "$dir/wt/.claude/settings.local.json" ] \
473
+ || fail "the previous harness's per-task wiring must be cleared on a switch"
474
+ assert_grep "codex" "$dir/fake/literal" "the replacement launch should be the new harness"
475
+ [ "$(journal_field "$dir" rl4 from_harness)" = claude ] || fail "the journal should record the origin harness"
476
+ [ "$(journal_field "$dir" rl4 to_harness)" = codex ] || fail "the journal should record the target harness"
477
+ pass "fm-control relaunch: switching harness is one ordinary relaunch, and the old wiring goes with the old agent"
478
+ }
479
+
480
+ test_harness_switch_does_not_carry_the_old_profile_axes() {
481
+ local dir out rc
482
+ dir=$(new_case profile rl5)
483
+ add_ship_task "$dir" rl5 claude
484
+ sed 's/^model=default$/model=opus/; s/^effort=default$/effort=xhigh/' \
485
+ "$dir/home/state/rl5.meta" > "$dir/home/state/rl5.meta.tmp"
486
+ mv "$dir/home/state/rl5.meta.tmp" "$dir/home/state/rl5.meta"
487
+ printf 'codex' > "$dir/fake/becomes"
488
+ out=$(run_control "$dir" rl5 relaunch --harness codex --note "switching runtime"); rc=$?
489
+ expect_code 0 "$rc" "a harness switch should succeed"$'\n'"$out"
490
+ [ "$(meta_field "$dir" rl5 model)" = default ] \
491
+ || fail "a model chosen for the old harness must not carry to a different one"
492
+ [ "$(meta_field "$dir" rl5 effort)" = default ] \
493
+ || fail "an effort chosen for the old harness must not carry to a different one"
494
+ pass "fm-control relaunch: a harness switch resets model and effort unless they are named too"
495
+ }
496
+
497
+ test_harness_switch_resolves_a_prefixed_recorded_harness() {
498
+ local dir out rc auth
499
+ dir=$(new_case prefixcontrol rl32)
500
+ add_ship_task "$dir" rl32 grok-2
501
+ printf 'grok-2' > "$dir/fake/command"
502
+ mkdir -p "$dir/grokhome/hooks/fm-turn-end.d"
503
+ printf 'fm.abcdefabcdef\n' > "$dir/home/state/rl32.grok-turnend-token"
504
+ auth="$dir/grokhome/hooks/fm-turn-end.d/fm.abcdefabcdef"
505
+ printf '%s\n' "$dir/home/state/rl32.turn-ended" > "$auth"
506
+ printf 'token=fm.abcdefabcdef\n' > "$dir/wt/.fm-grok-turnend"
507
+
508
+ out=$(run_control "$dir" rl32 relaunch --harness claude --note "switching runtime"); rc=$?
509
+ expect_code 0 "$rc" "relaunch should resolve a prefixed recorded harness"$'\n'"$out"
510
+ [ "$(sed -n '1p' "$dir/fake/literal")" = /exit ] \
511
+ || fail "relaunch should stop a grok-prefixed task with grok's exit command"
512
+ [ "$(meta_field "$dir" rl32 harness)" = claude ] \
513
+ || fail "relaunch should publish the explicitly selected replacement harness"
514
+ [ "$(journal_field "$dir" rl32 from_harness)" = grok-2 ] \
515
+ || fail "relaunch should retain the recorded harness basename in its provenance"
516
+ assert_contains "$out" "harness=claude from=grok-2" \
517
+ "relaunch should report the recorded-to-selected harness transition"
518
+ [ ! -e "$auth" ] && [ ! -e "$dir/home/state/rl32.grok-turnend-token" ] \
519
+ && [ ! -e "$dir/wt/.fm-grok-turnend" ] \
520
+ || fail "relaunch should retire wiring owned by the prefixed prior harness"
521
+ pass "fm-control relaunch: a prefixed recorded harness can switch adapters transactionally"
522
+ }
523
+
524
+ test_prefixed_recorded_harness_requires_explicit_replacement() {
525
+ local dir out rc meta brief
526
+ dir=$(new_case prefixrefuse rl34)
527
+ add_ship_task "$dir" rl34 grok-2
528
+ printf 'grok-2' > "$dir/fake/command"
529
+ meta="$dir/home/state/rl34.meta"
530
+ brief="$dir/home/data/rl34/brief.md"
531
+ cp "$meta" "$dir/meta.before"
532
+ cp "$brief" "$dir/brief.before"
533
+
534
+ out=$(run_control "$dir" rl34 relaunch --note "continue safely"); rc=$?
535
+ expect_code 1 "$rc" "implicit relaunch from a prefixed command should refuse"
536
+ assert_contains "$out" "original launch command cannot be reconstructed from its recorded basename" \
537
+ "the refusal should name the missing launch identity"
538
+ assert_contains "$out" "would substitute the canonical adapter 'grok'" \
539
+ "the refusal should name the unsafe substitution"
540
+ assert_contains "$out" "Pass an explicit --harness" \
541
+ "the refusal should name the deliberate replacement path"
542
+ cmp -s "$meta" "$dir/meta.before" \
543
+ || fail "a refused prefixed relaunch must leave metadata byte-identical"
544
+ cmp -s "$brief" "$dir/brief.before" \
545
+ || fail "a refused prefixed relaunch must leave instructions byte-identical"
546
+ [ "$(cat "$dir/fake/command")" = grok-2 ] \
547
+ || fail "a refused prefixed relaunch must leave the original agent alive"
548
+ [ -z "$(cat "$dir/fake/literal")" ] && [ -z "$(cat "$dir/fake/keys")" ] \
549
+ || fail "a refused prefixed relaunch must deliver no lifecycle input"
550
+ [ ! -e "$dir/home/state/rl34.control-relaunch" ] \
551
+ || fail "a refused prefixed relaunch must not create a durable journal"
552
+ pass "fm-control relaunch: a prefixed command requires an explicit replacement harness"
553
+ }
554
+
555
+ test_same_harness_relaunch_keeps_the_profile_axes() {
556
+ local dir out rc
557
+ dir=$(new_case keepprofile rl6)
558
+ add_ship_task "$dir" rl6 claude
559
+ sed 's/^model=default$/model=opus/; s/^effort=default$/effort=high/' \
560
+ "$dir/home/state/rl6.meta" > "$dir/home/state/rl6.meta.tmp"
561
+ mv "$dir/home/state/rl6.meta.tmp" "$dir/home/state/rl6.meta"
562
+ out=$(run_control "$dir" rl6 relaunch --note "same runtime"); rc=$?
563
+ expect_code 0 "$rc" "a same-harness relaunch should succeed"$'\n'"$out"
564
+ [ "$(meta_field "$dir" rl6 model)" = opus ] || fail "the model should carry across a same-harness relaunch"
565
+ [ "$(meta_field "$dir" rl6 effort)" = high ] || fail "the effort should carry across a same-harness relaunch"
566
+ pass "fm-control relaunch: a same-harness relaunch keeps the profile axes it was running with"
567
+ }
568
+
569
+ test_explicit_model_wins_over_the_recorded_one() {
570
+ local dir out rc
571
+ dir=$(new_case explicit rl7)
572
+ add_ship_task "$dir" rl7 claude
573
+ out=$(run_control "$dir" rl7 relaunch --model sonnet --effort low --note "dialling down"); rc=$?
574
+ expect_code 0 "$rc" "relaunch with explicit axes should succeed"$'\n'"$out"
575
+ [ "$(meta_field "$dir" rl7 model)" = sonnet ] || fail "an explicit model should be recorded"
576
+ [ "$(meta_field "$dir" rl7 effort)" = low ] || fail "an explicit effort should be recorded"
577
+ pass "fm-control relaunch: explicit model and effort win over the recorded ones"
578
+ }
579
+
580
+ test_relaunch_onto_an_unverified_harness_is_refused() {
581
+ local dir out rc
582
+ dir=$(new_case badharness rl8)
583
+ add_ship_task "$dir" rl8 claude
584
+ out=$(run_control "$dir" rl8 relaunch --harness someagent --note "x"); rc=$?
585
+ expect_code 1 "$rc" "an unverified target harness should refuse"
586
+ assert_contains "$out" "not a verified harness" "the refusal should name the unverified adapter"
587
+ [ "$(cat "$dir/fake/command")" = claude ] || fail "a refused relaunch must not stop the agent"
588
+ pass "fm-control relaunch: refuses to relaunch onto an adapter with no verified mechanics"
589
+ }
590
+
591
+ test_prior_harness_turnend_registry_entry_is_cleared() {
592
+ local dir auth
593
+ dir=$(new_case grokauth rl9)
594
+ add_ship_task "$dir" rl9 grok
595
+ mkdir -p "$dir/grokhome/hooks/fm-turn-end.d"
596
+ printf 'fm.abcdefabcdef\n' > "$dir/home/state/rl9.grok-turnend-token"
597
+ auth="$dir/grokhome/hooks/fm-turn-end.d/fm.abcdefabcdef"
598
+ printf '%s\n' "$dir/home/state/rl9.turn-ended" > "$auth"
599
+ printf 'grok' > "$dir/fake/command"
600
+ printf 'grok' > "$dir/fake/becomes"
601
+ run_control "$dir" rl9 relaunch --note "restart on the same runtime" >/dev/null
602
+ [ ! -e "$auth" ] \
603
+ || fail "the previous incarnation's turn-end registry entry must not outlive it"
604
+ pass "fm-control relaunch: the retired incarnation's global turn-end token is revoked"
605
+ }
606
+
607
+ test_wiring_removal_failure_refuses_before_replacement_arm() {
608
+ local dir hook out rc real_rm
609
+ dir=$(new_case wiring-failure rl29)
610
+ add_ship_task "$dir" rl29 claude
611
+ hook="$dir/wt/.claude/settings.local.json"
612
+ mkdir -p "${hook%/*}"
613
+ printf '{}\n' > "$hook"
614
+ real_rm=$(command -v rm)
615
+ make_rm_failure_stub "$dir"
616
+ out=$(FM_REAL_RM="$real_rm" FM_FAKE_RM_FAIL_PATH="$hook" \
617
+ run_control "$dir" rl29 relaunch --note "retry after wiring cleanup"); rc=$?
618
+ expect_code 1 "$rc" "an undeletable prior hook must fail closed"$'\n'"$out"
619
+ assert_contains "$out" "could not retire claude wiring" \
620
+ "the failure should identify prior wiring cleanup"
621
+ [ -e "$hook" ] || fail "the fixture should retain the undeletable prior hook"
622
+ assert_no_grep "encode launch-brief" "$dir/fake/literal" \
623
+ "replacement launch must not be armed after wiring cleanup fails"
624
+ [ "$(journal_field "$dir" rl29 phase)" = failed:launching ] \
625
+ || fail "the transaction should record the partial launch failure"
626
+ [ "$(journal_field "$dir" rl29 rollback)" = prior-record-kept ] \
627
+ || fail "unpublished rollback should retain the live durable record"
628
+ pass "fm-control relaunch: wiring cleanup failure refuses replacement arming"
629
+ }
630
+
631
+ test_turnend_auth_paths_are_owned_by_the_control_adapter() {
632
+ local dir state grok_path kimi_path token_path
633
+ dir=$(fm_test_tmproot fm-control-auth)
634
+ state="$dir/state"
635
+ mkdir -p "$state"
636
+ printf 'fm.111111111111\n' > "$state/x.grok-turnend-token"
637
+ printf 'fm.222222222222\n' > "$state/x.kimi-turnend-token"
638
+ token_path=$(fm_control_harness_turnend_token_path grok "$state" x)
639
+ [ "$token_path" = "$state/x.grok-turnend-token" ] \
640
+ || fail "the grok token path should be computed without reading it"
641
+ grok_path=$(GROK_HOME="$dir/gh" fm_control_harness_turnend_auth_path grok fm.111111111111)
642
+ [ "$grok_path" = "$dir/gh/hooks/fm-turn-end.d/fm.111111111111" ] \
643
+ || fail "grok's registry path should resolve under GROK_HOME, got '$grok_path'"
644
+ kimi_path=$(HOME="$dir/kh" fm_control_harness_turnend_auth_path kimi fm.222222222222)
645
+ [ "$kimi_path" = "$dir/kh/.kimi-code/fm-turn-end.d/fm.222222222222" ] \
646
+ || fail "kimi's registry path should resolve under the home store, got '$kimi_path'"
647
+ grok_path=$(GROK_HOME="$dir/gh" fm_control_harness_turnend_auth_path grok 'not a token/../..')
648
+ [ -z "$grok_path" ] || fail "a malformed token must resolve to no path, got '$grok_path'"
649
+ pass "fm-control-lib: one owner resolves each harness's turn-end registry entry, and refuses a malformed token"
650
+ }
651
+
652
+ test_secondmate_relaunch_picks_up_the_configured_harness_pin() {
653
+ local dir home out rc
654
+ dir=$(new_case smpin sm3)
655
+ home="$dir/home"
656
+ mkdir -p "$home/config"
657
+ printf 'codex some-model high\n' > "$home/config/secondmate-harness"
658
+ mkdir -p "$home/data/sm3"
659
+ printf '# secondmate brief\n' > "$home/data/sm3/brief.md"
660
+ fm_git_worktree "$dir/proj" "$dir/smhome" sm-branch
661
+ mkdir -p "$dir/smhome/state" "$dir/smhome/data" "$dir/smhome/bin"
662
+ printf 'sm3\n' > "$dir/smhome/.fm-secondmate-home"
663
+ printf '# agents\n' > "$dir/smhome/AGENTS.md"
664
+ {
665
+ echo "window=fmses:fm-sm3"
666
+ echo "endpoint_task_id=sm3"
667
+ echo "worktree=$dir/smhome"
668
+ echo "project=$dir/smhome"
669
+ echo "harness=claude"
670
+ echo "kind=secondmate"
671
+ echo "mode=secondmate"
672
+ echo "yolo=off"
673
+ echo "model=default"
674
+ echo "effort=default"
675
+ echo "home=$dir/smhome"
676
+ } > "$home/state/sm3.meta"
677
+ printf '%s\n' "fm-sm3" > "$dir/fake/windows"
678
+ printf '%s' "$dir/smhome" > "$dir/fake/cwd"
679
+ printf 'codex' > "$dir/fake/becomes"
680
+ out=$(run_control "$dir" sm3 relaunch); rc=$?
681
+ expect_code 0 "$rc" "a configured secondmate harness should relaunch"$'\n'"$out"
682
+ [ "$(journal_field "$dir" sm3 to_harness)" = codex ] \
683
+ || fail "a secondmate relaunch should pick up the configured harness pin, got '$(journal_field "$dir" sm3 to_harness)'"
684
+ [ "$(journal_field "$dir" sm3 to_model)" = some-model ] \
685
+ || fail "the configured model token should come with the pin"
686
+ [ "$(journal_field "$dir" sm3 to_effort)" = high ] \
687
+ || fail "the configured effort token should come with the pin"
688
+ assert_not_contains "$out" "not a verified harness" "codex is a verified harness"
689
+ pass "fm-control relaunch: a secondmate relaunch re-resolves its durable configured harness pin"
690
+ }
691
+
692
+ test_secondmate_relaunch_ignores_invalid_configured_effort_before_stop() {
693
+ local dir home out rc
694
+ dir=$(new_case invalid-effort sm6)
695
+ home="$dir/home"
696
+ mkdir -p "$home/config" "$home/data/sm6"
697
+ printf 'codex some-model impossible\n' > "$home/config/secondmate-harness"
698
+ printf '# secondmate brief\n' > "$home/data/sm6/brief.md"
699
+ fm_git_worktree "$dir/proj" "$dir/smhome" sm-branch
700
+ mkdir -p "$dir/smhome/state" "$dir/smhome/data" "$dir/smhome/bin"
701
+ printf 'sm6\n' > "$dir/smhome/.fm-secondmate-home"
702
+ printf '# agents\n' > "$dir/smhome/AGENTS.md"
703
+ {
704
+ echo "window=fmses:fm-sm6"
705
+ echo "endpoint_task_id=sm6"
706
+ echo "worktree=$dir/smhome"
707
+ echo "project=$dir/smhome"
708
+ echo "harness=claude"
709
+ echo "kind=secondmate"
710
+ echo "mode=secondmate"
711
+ echo "yolo=off"
712
+ echo "model=default"
713
+ echo "effort=default"
714
+ echo "home=$dir/smhome"
715
+ } > "$home/state/sm6.meta"
716
+ printf '%s\n' "fm-sm6" > "$dir/fake/windows"
717
+ printf '%s' "$dir/smhome" > "$dir/fake/cwd"
718
+ printf 'codex' > "$dir/fake/becomes"
719
+ out=$(run_control "$dir" sm6 relaunch); rc=$?
720
+ expect_code 0 "$rc" "an invalid configured effort should be ignored before stop"$'\n'"$out"
721
+ assert_contains "$out" "effort token 'impossible'" \
722
+ "relaunch should surface the same warning as a normal secondmate spawn"
723
+ [ "$(journal_field "$dir" sm6 to_effort)" = default ] \
724
+ || fail "invalid configured effort should normalize to default"
725
+ pass "fm-control relaunch: invalid configured effort is ignored before stop"
726
+ }
727
+
728
+ # muse is a verified adapter, but only for crewmates and scouts: it has no
729
+ # primary supervision protocol, so bin/fm-spawn.sh refuses it for a secondmate.
730
+ # That refusal alone is not enough here, because the launch owner is reached
731
+ # only AFTER the running agent has been stopped - a secondmate would be left
732
+ # with no agent at all. The control plane asks the same capability question
733
+ # before it touches anything, so the refusal lands while the agent is still up.
734
+ test_secondmate_relaunch_onto_a_crewmate_only_adapter_refuses_before_stop() {
735
+ local dir home out rc
736
+ dir=$(new_case smkind sm7)
737
+ home="$dir/home"
738
+ mkdir -p "$home/config" "$home/data/sm7"
739
+ printf '# secondmate brief\n' > "$home/data/sm7/brief.md"
740
+ fm_git_worktree "$dir/proj" "$dir/smhome" sm-branch
741
+ mkdir -p "$dir/smhome/state" "$dir/smhome/data" "$dir/smhome/bin"
742
+ printf 'sm7\n' > "$dir/smhome/.fm-secondmate-home"
743
+ printf '# agents\n' > "$dir/smhome/AGENTS.md"
744
+ {
745
+ echo "window=fmses:fm-sm7"
746
+ echo "endpoint_task_id=sm7"
747
+ echo "worktree=$dir/smhome"
748
+ echo "project=$dir/smhome"
749
+ echo "harness=claude"
750
+ echo "kind=secondmate"
751
+ echo "mode=secondmate"
752
+ echo "yolo=off"
753
+ echo "model=default"
754
+ echo "effort=default"
755
+ echo "home=$dir/smhome"
756
+ } > "$home/state/sm7.meta"
757
+ printf '%s\n' "fm-sm7" > "$dir/fake/windows"
758
+ printf '%s' "$dir/smhome" > "$dir/fake/cwd"
759
+ out=$(run_control "$dir" sm7 relaunch --harness muse); rc=$?
760
+ expect_code 1 "$rc" "a crewmate-only adapter should refuse a secondmate relaunch"
761
+ assert_contains "$out" "not verified to run a secondmate task" \
762
+ "the refusal should name the kind the adapter cannot run"
763
+ [ "$(cat "$dir/fake/command")" = claude ] \
764
+ || fail "the refusal must land before the running agent is stopped"
765
+ [ "$(meta_field "$dir" sm7 harness)" = claude ] \
766
+ || fail "a refused relaunch must leave the durable record on the recorded harness"
767
+ pass "fm-control relaunch: an adapter unverified for this task kind refuses before the agent is stopped"
768
+ }
769
+
770
+ test_explicit_secondmate_harness_ignores_configured_profile_axes() {
771
+ local dir home out rc
772
+ dir=$(new_case smexplicit sm4)
773
+ home="$dir/home"
774
+ mkdir -p "$home/config"
775
+ printf 'claude opus high\n' > "$home/config/secondmate-harness"
776
+ mkdir -p "$home/data/sm4"
777
+ printf '# secondmate brief\n' > "$home/data/sm4/brief.md"
778
+ fm_git_worktree "$dir/proj" "$dir/smhome" sm-branch
779
+ mkdir -p "$dir/smhome/state" "$dir/smhome/data" "$dir/smhome/bin"
780
+ printf 'sm4\n' > "$dir/smhome/.fm-secondmate-home"
781
+ printf '# agents\n' > "$dir/smhome/AGENTS.md"
782
+ {
783
+ echo "window=fmses:fm-sm4"
784
+ echo "endpoint_task_id=sm4"
785
+ echo "worktree=$dir/smhome"
786
+ echo "project=$dir/smhome"
787
+ echo "harness=claude"
788
+ echo "kind=secondmate"
789
+ echo "mode=secondmate"
790
+ echo "yolo=off"
791
+ echo "model=opus"
792
+ echo "effort=high"
793
+ echo "home=$dir/smhome"
794
+ } > "$home/state/sm4.meta"
795
+ printf '%s\n' "fm-sm4" > "$dir/fake/windows"
796
+ printf '%s' "$dir/smhome" > "$dir/fake/cwd"
797
+ printf 'codex' > "$dir/fake/becomes"
798
+ out=$(run_control "$dir" sm4 relaunch --harness codex); rc=$?
799
+ expect_code 0 "$rc" "an explicit secondmate harness should relaunch"$'\n'"$out"
800
+ [ "$(meta_field "$dir" sm4 model)" = default ] \
801
+ || fail "an explicit secondmate harness must not inherit the configured model"
802
+ [ "$(meta_field "$dir" sm4 effort)" = default ] \
803
+ || fail "an explicit secondmate harness must not inherit the configured effort"
804
+ pass "fm-control relaunch: explicit secondmate harness resets unnamed profile axes"
805
+ }
806
+
807
+ test_ship_relaunch_ignores_the_crew_harness_config() {
808
+ local dir out
809
+ dir=$(new_case crewcfg rl20)
810
+ add_ship_task "$dir" rl20 claude
811
+ mkdir -p "$dir/home/config"
812
+ printf 'codex\n' > "$dir/home/config/crew-harness"
813
+ out=$(run_control "$dir" rl20 relaunch --note "same worker, same runtime")
814
+ assert_contains "$out" "harness=claude from=claude" \
815
+ "a ship relaunch must keep its recorded harness rather than re-reading crew config"
816
+ [ "$(meta_field "$dir" rl20 harness)" = claude ] \
817
+ || fail "a ship relaunch must not silently move onto the configured crew harness"
818
+ pass "fm-control relaunch: a ship task keeps its recorded harness instead of re-reading crew config"
819
+ }
820
+
821
+ test_spawn_relaunch_without_a_harness_reuses_the_recorded_one() {
822
+ local dir out
823
+ dir=$(new_case spawnharness rl21)
824
+ add_ship_task "$dir" rl21 claude
825
+ mkdir -p "$dir/home/config"
826
+ printf 'codex\n' > "$dir/home/config/crew-harness"
827
+ printf 'zsh' > "$dir/fake/command"
828
+ out=$(run_spawn "$dir" rl21 --relaunch)
829
+ [ "$(meta_field "$dir" rl21 harness)" = claude ] \
830
+ || fail "fm-spawn --relaunch without --harness must reuse the recorded harness, got '$(meta_field "$dir" rl21 harness)'"
831
+ assert_contains "$out" "spawned rl21 harness=claude" "the launch should report the recorded harness"
832
+ pass "fm-spawn --relaunch: with no explicit harness it reuses the task's recorded one, never the crew default"
833
+ }
834
+
835
+ # fm-spawn arms per-task wiring on harness PREFIXES, because a task launched
836
+ # from a raw command records that command's basename rather than the exact
837
+ # adapter name. Retirement must resolve the same way, or a task recorded as
838
+ # `grok-2` would have its turn-end token and hook pointer armed and never
839
+ # retired - leaving a registry entry that outlives the agent that owned it.
840
+ test_prefixed_prior_harness_wiring_is_still_retired() {
841
+ local dir auth
842
+ dir=$(new_case prefixwiring rl30)
843
+ add_ship_task "$dir" rl30 grok-2
844
+ mkdir -p "$dir/grokhome/hooks/fm-turn-end.d"
845
+ printf 'fm.abcdefabcdef\n' > "$dir/home/state/rl30.grok-turnend-token"
846
+ auth="$dir/grokhome/hooks/fm-turn-end.d/fm.abcdefabcdef"
847
+ printf '%s\n' "$dir/home/state/rl30.turn-ended" > "$auth"
848
+ printf 'token=fm.abcdefabcdef\n' > "$dir/wt/.fm-grok-turnend"
849
+ printf 'zsh' > "$dir/fake/command"
850
+ run_spawn "$dir" rl30 --relaunch --harness claude >/dev/null
851
+ [ ! -e "$auth" ] \
852
+ || fail "a prefixed prior harness must still have its turn-end registry entry revoked"
853
+ [ ! -e "$dir/home/state/rl30.grok-turnend-token" ] \
854
+ || fail "a prefixed prior harness must still have its private token retired"
855
+ [ ! -e "$dir/wt/.fm-grok-turnend" ] \
856
+ || fail "a prefixed prior harness must still have its worktree hook pointer removed"
857
+ pass "fm-spawn --relaunch: wiring armed under a prefixed harness name is still retired"
858
+ }
859
+
860
+ # muse installs no hook; its busy source is its own session event log, bound to
861
+ # the pane by two firstmate-owned sidecars. Relaunching AWAY from muse must
862
+ # retire that binding, or a retired incarnation's session pin outlives the agent
863
+ # that produced it.
864
+ test_muse_session_binding_is_retired_on_a_harness_switch() {
865
+ local dir
866
+ dir=$(new_case musewiring rl31)
867
+ add_ship_task "$dir" rl31 muse
868
+ printf 'sessions_root=/nonexistent\nworkspace_root=%s\nbinding_id=1.2.3\n' "$dir/wt" \
869
+ > "$dir/home/state/rl31.muse-session"
870
+ printf '/nonexistent/session.jsonl\n' > "$dir/home/state/rl31.muse-session-current"
871
+ printf 'zsh' > "$dir/fake/command"
872
+ run_spawn "$dir" rl31 --relaunch --harness claude >/dev/null
873
+ [ ! -e "$dir/home/state/rl31.muse-session" ] \
874
+ || fail "the retired muse incarnation's session binding must not outlive it"
875
+ [ ! -e "$dir/home/state/rl31.muse-session-current" ] \
876
+ || fail "the retired muse incarnation's resolved session pin must not outlive it"
877
+ pass "fm-spawn --relaunch: switching away from muse retires its session binding"
878
+ }
879
+
880
+ test_cursor_session_binding_is_retired_on_a_harness_switch() {
881
+ local dir
882
+ dir=$(new_case cursorwiring rl35)
883
+ add_ship_task "$dir" rl35 cursor
884
+ printf 'workspace=%s\nprior_conversation=old-conversation\n' "$dir/wt" \
885
+ > "$dir/home/state/rl35.cursor-session"
886
+ printf 'zsh' > "$dir/fake/command"
887
+ run_spawn "$dir" rl35 --relaunch --harness claude >/dev/null
888
+ [ ! -e "$dir/home/state/rl35.cursor-session" ] \
889
+ || fail "the retired cursor incarnation's session binding must not outlive it"
890
+ pass "fm-spawn --relaunch: switching away from cursor retires its session binding"
891
+ }
892
+
893
+ # --- 3 and 4. refusals before the agent is touched ---------------------------
894
+
895
+ test_missing_worktree_refuses_before_stopping_anything() {
896
+ local dir out rc
897
+ dir=$(new_case nowt rl10)
898
+ add_ship_task "$dir" rl10 claude
899
+ rm -rf "$dir/wt"
900
+ out=$(run_control "$dir" rl10 relaunch --note "x"); rc=$?
901
+ expect_code 1 "$rc" "a missing worktree should refuse"
902
+ assert_contains "$out" "recorded worktree" "the refusal should name the missing local copy"
903
+ [ "$(cat "$dir/fake/command")" = claude ] || fail "a refused relaunch must not stop the agent"
904
+ [ -z "$(cat "$dir/fake/literal")" ] || fail "a refused relaunch must send nothing"
905
+ pass "fm-control relaunch: an unaccountable local copy refuses before the agent is touched"
906
+ }
907
+
908
+ test_missing_instructions_refuse_before_stopping_anything() {
909
+ local dir out rc
910
+ dir=$(new_case nobrief rl11)
911
+ add_ship_task "$dir" rl11 claude
912
+ rm -f "$dir/home/data/rl11/brief.md"
913
+ out=$(run_control "$dir" rl11 relaunch --note "x"); rc=$?
914
+ expect_code 1 "$rc" "missing instructions should refuse"
915
+ assert_contains "$out" "no instructions" "the refusal should name the missing instructions"
916
+ [ "$(cat "$dir/fake/command")" = claude ] || fail "a refused relaunch must not stop the agent"
917
+ pass "fm-control relaunch: a worker with nothing to work from is never launched"
918
+ }
919
+
920
+ test_checkpoint_refusal_leaves_the_record_byte_identical() {
921
+ local dir before after
922
+ dir=$(new_case bytes rl12)
923
+ add_ship_task "$dir" rl12 claude
924
+ before=$(cat "$dir/home/state/rl12.meta")
925
+ rm -rf "$dir/wt/.git"
926
+ run_control "$dir" rl12 relaunch --note "x" >/dev/null 2>&1
927
+ after=$(cat "$dir/home/state/rl12.meta")
928
+ [ "$before" = "$after" ] || fail "a refused relaunch must leave the durable record byte-identical"
929
+ pass "fm-control relaunch: a refusal before the agent is stopped leaves the durable record untouched"
930
+ }
931
+
932
+ test_checkpoint_refuses_uninspectable_head_and_status() {
933
+ local dir out rc real_git
934
+ real_git=$(command -v git)
935
+
936
+ dir=$(new_case badhead rl22)
937
+ add_ship_task "$dir" rl22 claude
938
+ make_git_failure_stub "$dir"
939
+ out=$(FM_REAL_GIT="$real_git" FM_FAKE_GIT_FAILURE=head \
940
+ run_control "$dir" rl22 relaunch --note "x"); rc=$?
941
+ expect_code 1 "$rc" "an uninspectable HEAD should refuse"
942
+ assert_contains "$out" "HEAD cannot be inspected" "the refusal should name the failed HEAD proof"
943
+ [ "$(cat "$dir/fake/command")" = claude ] || fail "HEAD inspection failure must not stop the agent"
944
+
945
+ dir=$(new_case badstatus rl23)
946
+ add_ship_task "$dir" rl23 claude
947
+ make_git_failure_stub "$dir"
948
+ out=$(FM_REAL_GIT="$real_git" FM_FAKE_GIT_FAILURE=status \
949
+ run_control "$dir" rl23 relaunch --note "x"); rc=$?
950
+ expect_code 1 "$rc" "an uninspectable worktree status should refuse"
951
+ assert_contains "$out" "status cannot be inspected" "the refusal should name the failed dirty-state proof"
952
+ [ "$(cat "$dir/fake/command")" = claude ] || fail "status inspection failure must not stop the agent"
953
+ pass "fm-control relaunch: checkpoint inspection failures refuse before stopping"
954
+ }
955
+
956
+ # --- 5. failure after the agent is stopped -----------------------------------
957
+
958
+ test_launch_failure_keeps_the_prior_record_and_reports_it() {
959
+ local dir out rc before
960
+ dir=$(new_case rollback rl13)
961
+ add_ship_task "$dir" rl13 claude
962
+ before=$(cat "$dir/home/state/rl13.meta")
963
+ # The endpoint's shell is not in the recorded worktree, so the launch owner
964
+ # refuses AFTER the previous agent has already been stopped.
965
+ printf '%s' "$dir/proj" > "$dir/fake/cwd"
966
+ out=$(run_control "$dir" rl13 relaunch --harness codex --note "carry this forward"); rc=$?
967
+ expect_code 1 "$rc" "a failed launch should fail closed"$'\n'"$out"
968
+ assert_contains "$out" "no agent is running" "the failure should say no agent is running"
969
+ assert_contains "$out" "$dir/wt" "the failure should say where the work is preserved"
970
+ [ "$(cat "$dir/home/state/rl13.meta")" = "$before" ] \
971
+ || fail "a failed launch must keep the prior durable record"
972
+ [ "$(journal_field "$dir" rl13 phase)" = "failed:launching" ] \
973
+ || fail "the journal should record the failed phase, got '$(journal_field "$dir" rl13 phase)'"
974
+ [ "$(journal_field "$dir" rl13 rollback)" = "prior-record-kept" ] \
975
+ || fail "the journal should record what the rollback did"
976
+ assert_grep "carry this forward" "$dir/home/data/rl13/brief.md" \
977
+ "the progress note must survive so a later recovery still has it"
978
+ pass "fm-control relaunch: a launch failure after the stop keeps the prior record and reports the real state"
979
+ }
980
+
981
+ test_prepublication_failure_keeps_concurrent_durable_metadata() {
982
+ local dir control_pid link_out rc i=0
983
+ dir=$(new_case rollback-race rl30)
984
+ add_ship_task "$dir" rl30 claude
985
+ printf '%s' "$dir/proj" > "$dir/fake/cwd"
986
+ FM_FAKE_CWD_RACE_READY="$dir/cwd-race-ready" \
987
+ run_control "$dir" rl30 relaunch --harness codex --note "preserve concurrent metadata" \
988
+ > "$dir/control.out" &
989
+ control_pid=$!
990
+ while [ ! -e "$dir/cwd-race-ready" ] && [ "$i" -lt 200 ]; do
991
+ /bin/sleep 0.01
992
+ i=$((i + 1))
993
+ done
994
+ [ -e "$dir/cwd-race-ready" ] || {
995
+ kill "$control_pid" 2>/dev/null || true
996
+ wait "$control_pid" 2>/dev/null || true
997
+ fail "relaunch did not reach its pre-publication endpoint check"
998
+ }
999
+ link_out=$(env PATH="$dir/fakebin:$PATH" FM_HOME="$dir/home" FM_ROOT_OVERRIDE="$ROOT" \
1000
+ "$X_LINK" rl30 request-30 --carry-count 2 --carry-ts 1700000000 \
1001
+ --carry-platform x --carry-max 280 2>&1); rc=$?
1002
+ expect_code 0 "$rc" "concurrent durable metadata publication should succeed"$'\n'"$link_out"
1003
+ wait "$control_pid"; rc=$?
1004
+ expect_code 1 "$rc" "the staged pre-publication launch failure should fail closed"
1005
+ [ "$(meta_field "$dir" rl30 x_request)" = request-30 ] \
1006
+ || fail "rollback erased the concurrent X request"
1007
+ [ "$(meta_field "$dir" rl30 x_followups)" = 2 ] \
1008
+ || fail "rollback erased the concurrent follow-up count"
1009
+ [ "$(journal_field "$dir" rl30 rollback)" = prior-record-kept ] \
1010
+ || fail "pre-publication rollback should leave the live record untouched"
1011
+ pass "fm-control relaunch: unpublished rollback keeps concurrent durable metadata"
1012
+ }
1013
+
1014
+ test_post_publication_launch_failure_keeps_the_new_record() {
1015
+ local dir out rc
1016
+ dir=$(new_case published rl24)
1017
+ add_ship_task "$dir" rl24 claude
1018
+ printf 'codex' > "$dir/fake/becomes"
1019
+ out=$(FM_FAKE_LAUNCH_TRANSPORT_FAIL_AFTER_START=1 \
1020
+ run_control "$dir" rl24 relaunch --harness codex --note "keep the published record"); rc=$?
1021
+ expect_code 1 "$rc" "a post-publication launch failure should fail closed"$'\n'"$out"
1022
+ [ "$(meta_field "$dir" rl24 harness)" = codex ] \
1023
+ || fail "a published replacement record must not be rewritten to the prior harness"
1024
+ [ -n "$(meta_field "$dir" rl24 control_relaunch_tx)" ] \
1025
+ || fail "the published replacement record should identify its relaunch transaction"
1026
+ [ "$(journal_field "$dir" rl24 rollback)" = none-new-record-kept ] \
1027
+ || fail "the journal should record that the published replacement record was kept"
1028
+ pass "fm-control relaunch: post-publication failure keeps the new durable record"
1029
+ }
1030
+
1031
+ test_stop_transport_failure_reconciles_a_dead_agent() {
1032
+ local dir out rc
1033
+ dir=$(new_case stopfail rl25)
1034
+ add_ship_task "$dir" rl25 claude
1035
+ out=$(FM_FAKE_EXIT_TRANSPORT_FAIL_AFTER_STOP=1 \
1036
+ run_control "$dir" rl25 relaunch --note "preserve this after stop"); rc=$?
1037
+ expect_code 1 "$rc" "a stop transport failure should fail closed"$'\n'"$out"
1038
+ [ "$(cat "$dir/fake/command")" = zsh ] || fail "the fixture should stop the old agent before reporting transport failure"
1039
+ [ "$(journal_field "$dir" rl25 phase)" = failed:stopping ] \
1040
+ || fail "the journal should retain the pre-stop phase on a partial stop"
1041
+ [ "$(journal_field "$dir" rl25 rollback)" = prior-record-kept-agent-dead ] \
1042
+ || fail "rollback should reconcile the observed dead agent"
1043
+ assert_contains "$out" "no agent is running" "the failure should report the reconciled dead state"
1044
+ assert_grep "preserve this after stop" "$dir/home/data/rl25/brief.md" \
1045
+ "the progress note should survive once the old agent has stopped"
1046
+ pass "fm-control relaunch: partial stop reconciles actual agent state"
1047
+ }
1048
+
1049
+ test_complete_journal_failure_rolls_back_from_durable_phase() {
1050
+ local dir out rc real_mv
1051
+ dir=$(new_case completejournal rl27)
1052
+ add_ship_task "$dir" rl27 claude
1053
+ printf 'codex' > "$dir/fake/becomes"
1054
+ real_mv=$(command -v mv)
1055
+ make_mv_failure_stub "$dir"
1056
+ out=$(FM_REAL_MV="$real_mv" FM_FAKE_COMPLETE_JOURNAL_MV_FAIL=1 \
1057
+ run_control "$dir" rl27 relaunch --harness codex --note "keep durable phase honest"); rc=$?
1058
+ expect_code 1 "$rc" "a failed complete journal replacement should fail closed"$'\n'"$out"
1059
+ [ "$(journal_field "$dir" rl27 phase)" = failed:launching ] \
1060
+ || fail "rollback should start from the last durable launching phase"
1061
+ [ "$(journal_field "$dir" rl27 rollback)" = none-new-agent-confirmed ] \
1062
+ || fail "rollback should retain the confirmed-running replacement"
1063
+ [ "$(meta_field "$dir" rl27 harness)" = codex ] \
1064
+ || fail "journal failure must not rewrite the published replacement record"
1065
+ assert_contains "$out" "replacement is running" \
1066
+ "journal failure should report the confirmed-running replacement"
1067
+ assert_not_contains "$out" "no running agent could be confirmed" \
1068
+ "journal failure should not contradict the confirmed agent state"
1069
+ pass "fm-control relaunch: failed journal replacement preserves durable phase"
1070
+ }
1071
+
1072
+ test_prepublication_abort_retires_replacement_wiring_and_busy_state() {
1073
+ local dir out rc real_mv meta
1074
+ dir=$(new_case prepublishcleanup rl28)
1075
+ add_ship_task "$dir" rl28 claude
1076
+ meta="$dir/home/state/rl28.meta"
1077
+ real_mv=$(command -v mv)
1078
+ make_mv_failure_stub "$dir"
1079
+ out=$(FM_REAL_MV="$real_mv" FM_FAKE_META_PUBLISH_MV_FAIL="$meta" \
1080
+ run_control "$dir" rl28 relaunch --note "clean partial replacement state"); rc=$?
1081
+ expect_code 1 "$rc" "a failed metadata publication should fail closed"$'\n'"$out"
1082
+ [ "$(meta_field "$dir" rl28 harness)" = claude ] \
1083
+ || fail "a failed publication should retain the prior durable record"
1084
+ [ ! -e "$dir/wt/.claude/settings.local.json" ] \
1085
+ || fail "an aborted replacement should remove its harness wiring"
1086
+ [ ! -e "$dir/home/state/rl28.busy-gen" ] \
1087
+ || fail "an aborted replacement should retire its busy generation"
1088
+ [ ! -e "$dir/home/state/rl28.busy-state" ] \
1089
+ || fail "an aborted replacement should remove its seeded busy record"
1090
+ [ "$(journal_field "$dir" rl28 rollback)" = prior-record-kept ] \
1091
+ || fail "the journal should record the unpublished replacement rollback"
1092
+ pass "fm-spawn relaunch: prepublication abort removes replacement state"
1093
+ }
1094
+
1095
+ test_journal_records_the_checkpoint_it_proved() {
1096
+ local dir head
1097
+ dir=$(new_case journal rl14)
1098
+ add_ship_task "$dir" rl14 claude
1099
+ printf 'scratch\n' > "$dir/wt/uncommitted.txt"
1100
+ head=$(git -C "$dir/wt" rev-parse HEAD)
1101
+ run_control "$dir" rl14 relaunch --note "keeping the scratch file" >/dev/null
1102
+ [ "$(journal_field "$dir" rl14 worktree_head)" = "$head" ] \
1103
+ || fail "the checkpoint should record the head it preserved"
1104
+ [ "$(journal_field "$dir" rl14 worktree_dirty)" = yes ] \
1105
+ || fail "the checkpoint should record that uncommitted work was present"
1106
+ [ -f "$dir/wt/uncommitted.txt" ] || fail "uncommitted work must survive a relaunch"
1107
+ pass "fm-control relaunch: the checkpoint records the exact unlanded work it preserved"
1108
+ }
1109
+
1110
+ # --- secondmate child-work safety -------------------------------------------
1111
+
1112
+ test_secondmate_relaunch_checkpoints_child_work_and_spares_the_charter() {
1113
+ local dir home out rc
1114
+ dir=$(new_case sm sm1)
1115
+ home="$dir/home"
1116
+ mkdir -p "$home/config"
1117
+ printf 'claude\n' > "$home/config/secondmate-harness"
1118
+ fm_git_worktree "$dir/proj" "$dir/smhome" sm-branch
1119
+ mkdir -p "$dir/smhome/state" "$dir/smhome/data" "$dir/smhome/bin"
1120
+ printf 'sm1\n' > "$dir/smhome/.fm-secondmate-home"
1121
+ printf '# charter\n' > "$dir/smhome/data/charter.md"
1122
+ printf '# agents\n' > "$dir/smhome/AGENTS.md"
1123
+ printf 'window=x:fm-c1\n' > "$dir/smhome/state/c1.meta"
1124
+ printf 'window=x:fm-c2\n' > "$dir/smhome/state/c2.meta"
1125
+ {
1126
+ echo "window=fmses:fm-sm1"
1127
+ echo "endpoint_task_id=sm1"
1128
+ echo "worktree=$dir/smhome"
1129
+ echo "project=$dir/smhome"
1130
+ echo "harness=claude"
1131
+ echo "kind=secondmate"
1132
+ echo "mode=secondmate"
1133
+ echo "yolo=off"
1134
+ echo "model=default"
1135
+ echo "effort=default"
1136
+ echo "home=$dir/smhome"
1137
+ echo "projects="
1138
+ } > "$home/state/sm1.meta"
1139
+ printf '%s\n' "fm-sm1" > "$dir/fake/windows"
1140
+ printf '%s' "$dir/smhome" > "$dir/fake/cwd"
1141
+ # No --note: a secondmate reconciles its own home's records at startup, so
1142
+ # the note is optional there.
1143
+ out=$(run_control "$dir" sm1 relaunch); rc=$?
1144
+ expect_code 0 "$rc" "a checkpointed secondmate should relaunch"$'\n'"$out"
1145
+ [ "$(journal_field "$dir" sm1 children)" = 2 ] \
1146
+ || fail "the checkpoint must account for the secondmate's child work, got '$(journal_field "$dir" sm1 children)'"
1147
+ assert_not_contains "$out" "requires --note" "a secondmate relaunch must not demand a progress note"
1148
+ [ "$(cat "$dir/smhome/data/charter.md")" = "# charter" ] \
1149
+ || fail "a secondmate's standing charter must never be rewritten by a relaunch"
1150
+ assert_present "$dir/smhome/state/c1.meta" "child records must survive the relaunch"
1151
+ assert_present "$dir/smhome/state/c2.meta" "child records must survive the relaunch"
1152
+ pass "fm-control relaunch: a secondmate's child work is accounted for and its charter is left alone"
1153
+ }
1154
+
1155
+ test_secondmate_relaunch_refuses_an_unmarked_home() {
1156
+ local dir home out rc
1157
+ dir=$(new_case smbad sm2)
1158
+ home="$dir/home"
1159
+ mkdir -p "$home/config"
1160
+ printf 'claude\n' > "$home/config/secondmate-harness"
1161
+ fm_git_worktree "$dir/proj" "$dir/smhome" sm-branch
1162
+ mkdir -p "$dir/smhome/state"
1163
+ printf 'someone-else\n' > "$dir/smhome/.fm-secondmate-home"
1164
+ {
1165
+ echo "window=fmses:fm-sm2"
1166
+ echo "endpoint_task_id=sm2"
1167
+ echo "worktree=$dir/smhome"
1168
+ echo "project=$dir/smhome"
1169
+ echo "harness=claude"
1170
+ echo "kind=secondmate"
1171
+ echo "mode=secondmate"
1172
+ echo "yolo=off"
1173
+ } > "$home/state/sm2.meta"
1174
+ printf '%s\n' "fm-sm2" > "$dir/fake/windows"
1175
+ out=$(run_control "$dir" sm2 relaunch); rc=$?
1176
+ expect_code 1 "$rc" "a home marked for another secondmate should refuse"
1177
+ assert_contains "$out" "not marked as its own seeded secondmate home" \
1178
+ "the refusal should name the identity mismatch"
1179
+ [ "$(cat "$dir/fake/command")" = claude ] || fail "a refused relaunch must not stop the agent"
1180
+ pass "fm-control relaunch: a secondmate home that is not this secondmate's is refused"
1181
+ }
1182
+
1183
+ test_secondmate_checkpoint_refuses_unreadable_child_state() {
1184
+ local dir home out rc
1185
+ dir=$(new_case smchildren sm5)
1186
+ home="$dir/home"
1187
+ mkdir -p "$home/config"
1188
+ printf 'claude\n' > "$home/config/secondmate-harness"
1189
+ fm_git_worktree "$dir/proj" "$dir/smhome" sm-branch
1190
+ mkdir -p "$dir/smhome/state/bad.meta"
1191
+ printf 'sm5\n' > "$dir/smhome/.fm-secondmate-home"
1192
+ {
1193
+ echo "window=fmses:fm-sm5"
1194
+ echo "endpoint_task_id=sm5"
1195
+ echo "worktree=$dir/smhome"
1196
+ echo "project=$dir/smhome"
1197
+ echo "harness=claude"
1198
+ echo "kind=secondmate"
1199
+ echo "mode=secondmate"
1200
+ echo "yolo=off"
1201
+ echo "home=$dir/smhome"
1202
+ } > "$home/state/sm5.meta"
1203
+ printf '%s\n' "fm-sm5" > "$dir/fake/windows"
1204
+ printf '%s' "$dir/smhome" > "$dir/fake/cwd"
1205
+ out=$(run_control "$dir" sm5 relaunch); rc=$?
1206
+ expect_code 1 "$rc" "a non-readable child record should refuse"
1207
+ assert_contains "$out" "not a readable regular file" "the refusal should name the unreadable child record"
1208
+ [ "$(cat "$dir/fake/command")" = claude ] || fail "child record failure must not stop the secondmate"
1209
+ rmdir "$dir/smhome/state/bad.meta"
1210
+ cat > "$dir/fakebin/find" <<'SH'
1211
+ #!/usr/bin/env bash
1212
+ exit 1
1213
+ SH
1214
+ chmod +x "$dir/fakebin/find"
1215
+ out=$(run_control "$dir" sm5 relaunch); rc=$?
1216
+ expect_code 1 "$rc" "failed child-state traversal should refuse"
1217
+ assert_contains "$out" "child records cannot be traversed" \
1218
+ "the refusal should preserve a find traversal failure"
1219
+ [ "$(cat "$dir/fake/command")" = claude ] || fail "child traversal failure must not stop the secondmate"
1220
+ pass "fm-control relaunch: unreadable and untraversable child state fails checkpoint"
1221
+ }
1222
+
1223
+ test_concurrent_relaunch_is_refused() {
1224
+ local dir out rc lock holder i
1225
+ dir=$(new_case lock rl19)
1226
+ add_ship_task "$dir" rl19 claude
1227
+ lock="$dir/home/state/.control-rl19.lock"
1228
+ # A live holder of this task's control lock, taken through the same lock
1229
+ # library fm-control uses.
1230
+ (
1231
+ # shellcheck source=/dev/null
1232
+ . "$ROOT/bin/fm-wake-lib.sh"
1233
+ fm_lock_try_acquire "$lock" || exit 1
1234
+ sleep 30
1235
+ ) &
1236
+ holder=$!
1237
+ i=0
1238
+ while [ ! -e "$lock" ] && [ "$i" -lt 100 ]; do
1239
+ sleep 0.1
1240
+ i=$((i + 1))
1241
+ done
1242
+ [ -e "$lock" ] || { kill "$holder" 2>/dev/null; fail "could not stage a held control lock"; }
1243
+ out=$(run_control "$dir" rl19 relaunch --note "concurrent"); rc=$?
1244
+ kill "$holder" 2>/dev/null || true
1245
+ wait "$holder" 2>/dev/null || true
1246
+ expect_code 1 "$rc" "a second concurrent control action should refuse"
1247
+ assert_contains "$out" "another lifecycle action is already running" \
1248
+ "the refusal should name the concurrent action"
1249
+ [ "$(cat "$dir/fake/command")" = claude ] \
1250
+ || fail "a refused concurrent relaunch must not stop the agent"
1251
+ pass "fm-control relaunch: two control actions on one task serialize instead of interleaving"
1252
+ }
1253
+
1254
+ # shellcheck disable=SC2031
1255
+ test_direct_spawn_relaunch_participates_in_the_lifecycle_lock() {
1256
+ local dir out rc lock holder i=0
1257
+ dir=$(new_case spawnlock rl26)
1258
+ add_ship_task "$dir" rl26 claude
1259
+ printf 'zsh' > "$dir/fake/command"
1260
+ lock="$dir/home/state/.control-rl26.lock"
1261
+ (
1262
+ . "$ROOT/bin/fm-wake-lib.sh"
1263
+ fm_lock_try_acquire "$lock" || exit 1
1264
+ sleep 30
1265
+ ) &
1266
+ holder=$!
1267
+ while [ ! -e "$lock" ] && [ "$i" -lt 100 ]; do
1268
+ sleep 0.1
1269
+ i=$((i + 1))
1270
+ done
1271
+ [ -e "$lock" ] || fail "could not stage the lifecycle lock"
1272
+ out=$(run_spawn "$dir" rl26 --relaunch --harness claude); rc=$?
1273
+ kill "$holder" 2>/dev/null || true
1274
+ wait "$holder" 2>/dev/null || true
1275
+ expect_code 1 "$rc" "direct relaunch spawn should refuse a held lifecycle lock"
1276
+ assert_contains "$out" "another lifecycle action is already running" \
1277
+ "direct relaunch spawn should name lifecycle contention"
1278
+ [ -z "$(cat "$dir/fake/literal")" ] || fail "contended direct relaunch spawn must deliver no launch bytes"
1279
+ pass "fm-spawn relaunch: direct entry participates in lifecycle serialization"
1280
+ }
1281
+
1282
+ # shellcheck disable=SC2031
1283
+ test_promotion_participates_in_the_lifecycle_lock_before_metadata_resolution() {
1284
+ local dir out rc lock holder i=0
1285
+ dir=$(new_case promotelock rl29)
1286
+ add_ship_task "$dir" rl29 claude
1287
+ lock="$dir/home/state/.control-rl29.lock"
1288
+ (
1289
+ . "$ROOT/bin/fm-wake-lib.sh"
1290
+ fm_lock_try_acquire "$lock" || exit 1
1291
+ sleep 30
1292
+ ) &
1293
+ holder=$!
1294
+ while [ ! -e "$lock" ] && [ "$i" -lt 100 ]; do
1295
+ sleep 0.1
1296
+ i=$((i + 1))
1297
+ done
1298
+ [ -e "$lock" ] || fail "could not stage the promotion lifecycle lock"
1299
+ out=$(FM_HOME="$dir/home" "$PROMOTE" rl29 --mode direct-PR --yolo on 2>&1); rc=$?
1300
+ kill "$holder" 2>/dev/null || true
1301
+ wait "$holder" 2>/dev/null || true
1302
+ expect_code 1 "$rc" "promotion should refuse a concurrent lifecycle action"
1303
+ assert_contains "$out" "another lifecycle action is already running" \
1304
+ "promotion should lock before interpreting the task metadata"
1305
+ [ "$(meta_field "$dir" rl29 kind)" = ship ] \
1306
+ || fail "a contended promotion must leave task metadata unchanged"
1307
+ pass "fm-promote: promotion participates in lifecycle serialization"
1308
+ }
1309
+
1310
+ # --- 6. fm-spawn --relaunch's own refusals -----------------------------------
1311
+
1312
+ test_spawn_relaunch_refuses_a_live_agent() {
1313
+ local dir out rc
1314
+ dir=$(new_case live rl15)
1315
+ add_ship_task "$dir" rl15 claude
1316
+ out=$(run_spawn "$dir" rl15 --relaunch --harness claude); rc=$?
1317
+ expect_code 1 "$rc" "relaunching into a live endpoint should refuse"
1318
+ assert_contains "$out" "positively agent-free endpoint" "the refusal should demand an agent-free endpoint"
1319
+ assert_contains "$out" "fm-control.sh rl15 exit" "the refusal should point at the way to stop it"
1320
+ pass "fm-spawn --relaunch: refuses to launch a second agent into a live endpoint"
1321
+ }
1322
+
1323
+ test_spawn_relaunch_refuses_a_symlinked_task_record_before_inspection() {
1324
+ local dir meta target out rc
1325
+ dir=$(new_case symlink-meta rl37)
1326
+ add_ship_task "$dir" rl37 claude
1327
+ meta="$dir/home/state/rl37.meta"
1328
+ target="$dir/foreign-task-record"
1329
+ mv "$meta" "$target"
1330
+ ln -s "$target" "$meta"
1331
+ mv "$dir/fakebin/tmux" "$dir/fakebin/tmux-real"
1332
+ cat > "$dir/fakebin/tmux" <<SH
1333
+ #!/usr/bin/env bash
1334
+ : > "$dir/relaunch-endpoint-inspected"
1335
+ exec "$dir/fakebin/tmux-real" "\$@"
1336
+ SH
1337
+ chmod +x "$dir/fakebin/tmux"
1338
+
1339
+ out=$(run_spawn "$dir" rl37 --relaunch --harness claude); rc=$?
1340
+ expect_code 1 "$rc" "relaunching from symlinked metadata should refuse"
1341
+ assert_contains "$out" "task record resolves outside its authorized directory" \
1342
+ "relaunch did not identify the unsafe task record"
1343
+ [ -L "$meta" ] || fail "relaunch replaced or removed the symlinked record"
1344
+ assert_present "$target" "relaunch removed the foreign record target"
1345
+ assert_absent "$dir/relaunch-endpoint-inspected" \
1346
+ "relaunch inspected or acted on an endpoint from unsafe metadata"
1347
+ pass "fm-spawn --relaunch: symlinked records refuse before inspection"
1348
+ }
1349
+
1350
+ test_spawn_relaunch_keeps_its_early_meta_lock_continuous() {
1351
+ local dir lock out rc
1352
+ dir=$(new_case continuous-meta-lock rl38)
1353
+ add_ship_task "$dir" rl38 claude
1354
+ printf 'zsh' > "$dir/fake/command"
1355
+ lock="$dir/home/state/.meta-rl38.lock"
1356
+ mv "$dir/fakebin/tmux" "$dir/fakebin/tmux-real"
1357
+ cat > "$dir/fakebin/tmux" <<SH
1358
+ #!/usr/bin/env bash
1359
+ if [ -d "$lock" ]; then
1360
+ if [ ! -e "$dir/lock-observation-started" ]; then
1361
+ : > "$dir/lock-observation-started"
1362
+ : > "$lock/continuity-sentinel"
1363
+ elif [ ! -e "$lock/continuity-sentinel" ]; then
1364
+ : > "$dir/meta-lock-was-recreated"
1365
+ fi
1366
+ fi
1367
+ exec "$dir/fakebin/tmux-real" "\$@"
1368
+ SH
1369
+ chmod +x "$dir/fakebin/tmux"
1370
+
1371
+ out=$(run_spawn "$dir" rl38 --relaunch --harness claude); rc=$?
1372
+ expect_code 0 "$rc" "relaunch with one continuous meta lock should succeed"$'\n'"$out"
1373
+ assert_present "$dir/lock-observation-started" \
1374
+ "test did not observe the relaunch-held meta lock"
1375
+ assert_absent "$dir/meta-lock-was-recreated" \
1376
+ "relaunch released or recreated its already-held meta lock"
1377
+ pass "fm-spawn --relaunch: keeps its early meta lock continuous"
1378
+ }
1379
+
1380
+ test_spawn_relaunch_refuses_a_pending_authoritative_close() {
1381
+ local dir meta marker out rc
1382
+ dir=$(new_case pending-close rl36)
1383
+ add_ship_task "$dir" rl36 claude
1384
+ meta="$dir/home/state/rl36.meta"
1385
+ printf 'spawn_gen=spawn-pending\n' >> "$meta"
1386
+ cp "$meta" "$dir/meta.before"
1387
+ mkdir -p "$dir/wt/.claude"
1388
+ printf 'prior wiring\n' > "$dir/wt/.claude/settings.local.json"
1389
+ marker="$dir/home/state/rl36.backlog-close"
1390
+ printf 'id=rl36\ndata=%s\nspawn_gen=spawn-pending\narg=--note\narg=local%%20main\n' \
1391
+ "$dir/home/data" > "$marker"
1392
+ printf 'zsh' > "$dir/fake/command"
1393
+
1394
+ out=$(run_spawn "$dir" rl36 --relaunch --harness claude); rc=$?
1395
+ expect_code 1 "$rc" "relaunching over a pending close should refuse"
1396
+ assert_contains "$out" "pending authoritative backlog close" \
1397
+ "the refusal should identify the close that still owns the task"
1398
+ cmp -s "$dir/meta.before" "$meta" \
1399
+ || fail "pending-close refusal replaced the task incarnation"
1400
+ assert_grep 'prior wiring' "$dir/wt/.claude/settings.local.json" \
1401
+ "pending-close refusal cleared the prior worker wiring"
1402
+ assert_present "$marker" "pending-close refusal discarded the authoritative close"
1403
+ pass "fm-spawn --relaunch: pending closes refuse before replacement begins"
1404
+ }
1405
+
1406
+ test_spawn_relaunch_refuses_contradicting_flags() {
1407
+ local dir out rc
1408
+ dir=$(new_case flags rl16)
1409
+ add_ship_task "$dir" rl16 claude
1410
+ printf 'zsh' > "$dir/fake/command"
1411
+ out=$(run_spawn "$dir" rl16 --relaunch --backend herdr); rc=$?
1412
+ expect_code 1 "$rc" "--backend should be refused alongside --relaunch"
1413
+ assert_contains "$out" "recorded backend" "the refusal should name the recorded backend rule"
1414
+ out=$(run_spawn "$dir" rl16 --relaunch --scout); rc=$?
1415
+ expect_code 1 "$rc" "--scout should be refused alongside --relaunch"
1416
+ assert_contains "$out" "recorded kind" "the refusal should name the recorded kind rule"
1417
+ out=$(run_spawn "$dir" rl16 "$dir/proj" --relaunch); rc=$?
1418
+ expect_code 1 "$rc" "a project positional should be refused alongside --relaunch"
1419
+ assert_contains "$out" "takes the task id only" "the refusal should name the positional rule"
1420
+ pass "fm-spawn --relaunch: every identity axis comes from the record, and a contradicting flag refuses"
1421
+ }
1422
+
1423
+ test_spawn_relaunch_refuses_an_unrecorded_task() {
1424
+ local dir out rc
1425
+ dir=$(new_case norecord rl17)
1426
+ add_ship_task "$dir" rl17 claude
1427
+ out=$(run_spawn "$dir" nosuchtask --relaunch); rc=$?
1428
+ expect_code 1 "$rc" "an unrecorded task should refuse"
1429
+ assert_contains "$out" "needs an existing task record" "the refusal should name the missing record"
1430
+ pass "fm-spawn --relaunch: an unrecorded task is refused"
1431
+ }
1432
+
1433
+ test_spawn_relaunch_refuses_a_pane_outside_the_worktree() {
1434
+ local dir out rc
1435
+ dir=$(new_case wrongcwd rl18)
1436
+ add_ship_task "$dir" rl18 claude
1437
+ printf 'zsh' > "$dir/fake/command"
1438
+ printf '%s' "$dir/proj" > "$dir/fake/cwd"
1439
+ out=$(run_spawn "$dir" rl18 --relaunch --harness claude); rc=$?
1440
+ expect_code 1 "$rc" "a pane outside the worktree should refuse"
1441
+ assert_contains "$out" "not its recorded worktree" "the refusal should name the wrong location"
1442
+ pass "fm-spawn --relaunch: refuses to start a replacement outside the copy holding the work"
1443
+ }
1444
+
1445
+ test_relaunch_reverifies_an_already_in_flight_item_instead_of_rewriting_it() {
1446
+ local dir out rc=0
1447
+ command -v tasks-axi >/dev/null 2>&1 || {
1448
+ pass "skipped: tasks-axi is not installed, so the backlog transition is inert"
1449
+ return 0
1450
+ }
1451
+ dir=$(new_case reverify rl40)
1452
+ add_ship_task "$dir" rl40 claude
1453
+ seed_backlog "$dir" rl40 in_flight
1454
+ break_tasks_axi_start "$dir"
1455
+
1456
+ out=$(run_control "$dir" rl40 relaunch --note "picking the work back up") || rc=$?
1457
+ expect_code 0 "$rc" "a relaunch must not re-run a transition the row already reflects"$'\n'"$out"
1458
+ [ "$(backlog_state "$dir" rl40)" = in_flight ] \
1459
+ || fail "a relaunch changed an already In-flight item to $(backlog_state "$dir" rl40)"
1460
+ pass "relaunch re-reads the backlog item instead of blindly re-running the transition"
1461
+ }
1462
+
1463
+ test_relaunch_moves_a_drifted_item_back_in_flight() {
1464
+ local dir out rc=0
1465
+ command -v tasks-axi >/dev/null 2>&1 || {
1466
+ pass "skipped: tasks-axi is not installed, so the backlog transition is inert"
1467
+ return 0
1468
+ }
1469
+ dir=$(new_case drifted rl41)
1470
+ add_ship_task "$dir" rl41 claude
1471
+ seed_backlog "$dir" rl41 queued
1472
+
1473
+ out=$(run_control "$dir" rl41 relaunch --note "picking the work back up") || rc=$?
1474
+ expect_code 0 "$rc" "a relaunch onto a drifted item should succeed"$'\n'"$out"
1475
+ [ "$(backlog_state "$dir" rl41)" = in_flight ] \
1476
+ || fail "a relaunch left its item at $(backlog_state "$dir" rl41)"
1477
+ pass "relaunch heals an item that drifted out of In flight while the task stayed live"
1478
+ }
1479
+
1480
+ test_same_harness_relaunch_keeps_identity_and_reuses_the_endpoint
1481
+ test_relaunch_preserves_durable_task_metadata
1482
+ test_relaunch_serializes_concurrent_durable_metadata_publication
1483
+ test_disabled_relaunch_clears_prior_trace_context
1484
+ test_relaunch_appends_the_progress_note_to_the_instructions
1485
+ test_relaunch_requires_a_note_for_a_ship_task
1486
+ test_harness_switch_moves_the_record_and_clears_prior_wiring
1487
+ test_harness_switch_does_not_carry_the_old_profile_axes
1488
+ test_harness_switch_resolves_a_prefixed_recorded_harness
1489
+ test_prefixed_recorded_harness_requires_explicit_replacement
1490
+ test_same_harness_relaunch_keeps_the_profile_axes
1491
+ test_explicit_model_wins_over_the_recorded_one
1492
+ test_relaunch_onto_an_unverified_harness_is_refused
1493
+ test_prior_harness_turnend_registry_entry_is_cleared
1494
+ test_wiring_removal_failure_refuses_before_replacement_arm
1495
+ test_turnend_auth_paths_are_owned_by_the_control_adapter
1496
+ test_secondmate_relaunch_picks_up_the_configured_harness_pin
1497
+ test_secondmate_relaunch_ignores_invalid_configured_effort_before_stop
1498
+ test_secondmate_relaunch_onto_a_crewmate_only_adapter_refuses_before_stop
1499
+ test_explicit_secondmate_harness_ignores_configured_profile_axes
1500
+ test_ship_relaunch_ignores_the_crew_harness_config
1501
+ test_spawn_relaunch_without_a_harness_reuses_the_recorded_one
1502
+ test_prefixed_prior_harness_wiring_is_still_retired
1503
+ test_muse_session_binding_is_retired_on_a_harness_switch
1504
+ test_cursor_session_binding_is_retired_on_a_harness_switch
1505
+ test_missing_worktree_refuses_before_stopping_anything
1506
+ test_missing_instructions_refuse_before_stopping_anything
1507
+ test_checkpoint_refusal_leaves_the_record_byte_identical
1508
+ test_checkpoint_refuses_uninspectable_head_and_status
1509
+ test_launch_failure_keeps_the_prior_record_and_reports_it
1510
+ test_prepublication_failure_keeps_concurrent_durable_metadata
1511
+ test_post_publication_launch_failure_keeps_the_new_record
1512
+ test_stop_transport_failure_reconciles_a_dead_agent
1513
+ test_complete_journal_failure_rolls_back_from_durable_phase
1514
+ test_prepublication_abort_retires_replacement_wiring_and_busy_state
1515
+ test_journal_records_the_checkpoint_it_proved
1516
+ test_secondmate_relaunch_checkpoints_child_work_and_spares_the_charter
1517
+ test_secondmate_relaunch_refuses_an_unmarked_home
1518
+ test_secondmate_checkpoint_refuses_unreadable_child_state
1519
+ test_concurrent_relaunch_is_refused
1520
+ test_direct_spawn_relaunch_participates_in_the_lifecycle_lock
1521
+ test_promotion_participates_in_the_lifecycle_lock_before_metadata_resolution
1522
+ test_spawn_relaunch_refuses_a_live_agent
1523
+ test_spawn_relaunch_refuses_a_symlinked_task_record_before_inspection
1524
+ test_spawn_relaunch_keeps_its_early_meta_lock_continuous
1525
+ test_spawn_relaunch_refuses_a_pending_authoritative_close
1526
+ test_spawn_relaunch_refuses_contradicting_flags
1527
+ test_spawn_relaunch_refuses_an_unrecorded_task
1528
+ test_spawn_relaunch_refuses_a_pane_outside_the_worktree
1529
+ test_relaunch_reverifies_an_already_in_flight_item_instead_of_rewriting_it
1530
+ test_relaunch_moves_a_drifted_item_back_in_flight