@zerwiz/ymir 0.1.3 → 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 -39
  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,2302 @@
1
+ #!/usr/bin/env bash
2
+ # Behavior tests for the backlog<->record pairing invariant:
3
+ # `state/<id>.meta` exists <=> this home's backlog row for that id is In flight.
4
+ #
5
+ # bin/fm-backlog-transition-lib.sh states the contract; the three scripts that
6
+ # own a task's physical record enforce it. These tests drive those real scripts
7
+ # against a real backlog file and the real tasks-axi CLI, and assert the
8
+ # resulting RECORD STATE - never the wording of a reminder a later turn was
9
+ # expected to act on, which is exactly what let the two records drift before.
10
+ #
11
+ # dispatch bin/fm-spawn.sh moves the row In flight in the same run that
12
+ # publishes the record, so a live worker the backlog does not own
13
+ # cannot arise on the ordinary path.
14
+ # completion bin/fm-teardown.sh closes the row before it reports success, so
15
+ # a finished task cannot be left showing as running.
16
+ # recovery bin/fm-bootstrap.sh reconciles THIS home's own books at session
17
+ # start, covering the millisecond crash window inside those two
18
+ # scripts and any drift a home was already carrying.
19
+ #
20
+ # The invariant is single-host: a home's backlog and its records live together,
21
+ # so a persistent secondmate keeps its own books through its own copies of these
22
+ # scripts. A parent's view of a mate lagging is a freshness question and is
23
+ # deliberately not asserted here.
24
+ set -u
25
+
26
+ # shellcheck source=tests/lib.sh
27
+ . "$(dirname "${BASH_SOURCE[0]}")/lib.sh"
28
+
29
+ SPAWN="$ROOT/bin/fm-spawn.sh"
30
+ TEARDOWN="$ROOT/bin/fm-teardown.sh"
31
+ BOOTSTRAP="$ROOT/bin/fm-bootstrap.sh"
32
+ TMP_ROOT=$(fm_test_tmproot fm-backlog-atomicity)
33
+
34
+ command -v tasks-axi >/dev/null 2>&1 || {
35
+ printf 'ok - skipped (tasks-axi is not installed; the fused transitions are inert without it)\n'
36
+ exit 0
37
+ }
38
+
39
+ # --- fixture ----------------------------------------------------------------
40
+
41
+ # A home with a real backlog, a real project clone with an origin, a pooled
42
+ # worktree, and stubs for every tool the spawn path shells out to.
43
+ make_home() { # <name> [task-id...]
44
+ local name=$1 case_dir home fakebin id
45
+ shift
46
+ case_dir="$TMP_ROOT/$name"
47
+ home="$case_dir/home"
48
+ fakebin=$(fm_fakebin "$case_dir")
49
+ mkdir -p "$home/state" "$home/config" "$home/data" "$home/projects"
50
+ touch "$home/state/.last-watcher-beat"
51
+ printf '%s\n' claude > "$home/config/crew-harness"
52
+ printf '%s\n' '# Backlog' '' '## In flight' '' '## Queued' '' '## Done' \
53
+ > "$home/data/backlog.md"
54
+ for id in "$@"; do
55
+ mkdir -p "$home/data/$id"
56
+ printf 'Delivery contract: mode=no-mistakes\nbrief for %s\n' "$id" > "$home/data/$id/brief.md"
57
+ done
58
+
59
+ cat > "$fakebin/tmux" <<'SH'
60
+ #!/usr/bin/env bash
61
+ case "$*" in *"#{pane_current_path}"*) printf '%s\n' "${FM_FAKE_PANE_PATH:-}"; exit 0 ;; esac
62
+ case "${1:-}" in display-message) printf 'firstmate\n'; exit 0 ;; esac
63
+ exit 0
64
+ SH
65
+ chmod +x "$fakebin/tmux"
66
+ fm_fake_exit0 "$fakebin" treehouse gh gh-axi no-mistakes
67
+
68
+ fm_git_init_commit "$case_dir/project"
69
+ fm_git_add_origin "$case_dir/project" "$case_dir/project.origin.git"
70
+ git -C "$case_dir/project" worktree add --quiet -b pooled "$case_dir/wt"
71
+
72
+ printf '%s\n' "$case_dir"
73
+ }
74
+
75
+ home_of() { printf '%s/home\n' "$1"; }
76
+ backlog_of() { printf '%s/home/data/backlog.md\n' "$1"; }
77
+
78
+ add_item() { # <case-dir> <id> [kind]
79
+ tasks-axi add "$2" "item for $2" --kind "${3:-ship}" --file "$(backlog_of "$1")" >/dev/null
80
+ }
81
+
82
+ start_item() { # <case-dir> <id>
83
+ tasks-axi start "$2" --file "$(backlog_of "$1")" >/dev/null
84
+ }
85
+
86
+ row_state() { # <case-dir> <id>
87
+ tasks-axi show "$2" --file "$(backlog_of "$1")" 2>/dev/null |
88
+ sed -n 's/^ state: *//p' | head -1
89
+ }
90
+
91
+ # Shadow tasks-axi with a wrapper that fails one verb and delegates every other
92
+ # verb to the real binary, so a test can drive a genuine mid-transition failure
93
+ # without faking the reads around it.
94
+ require_show_cwd() { # <case-dir> <expected-dir>
95
+ local case_dir=$1 expected=$2 real
96
+ real=$(command -v tasks-axi)
97
+ cat > "$case_dir/fakebin/tasks-axi" <<SH
98
+ #!/usr/bin/env bash
99
+ case "\${1:-}" in
100
+ show|start|done)
101
+ if [ "\$PWD" != "$expected" ]; then
102
+ echo "error: wrong tasks root: \$PWD" >&2
103
+ exit 1
104
+ fi
105
+ ;;
106
+ esac
107
+ exec "$real" "\$@"
108
+ SH
109
+ chmod +x "$case_dir/fakebin/tasks-axi"
110
+ }
111
+
112
+ make_tasks_axi_incompatible() { # <case-dir>
113
+ local case_dir=$1 real
114
+ real=$(command -v tasks-axi)
115
+ cat > "$case_dir/fakebin/tasks-axi" <<SH
116
+ #!/usr/bin/env bash
117
+ [ "\${1:-}" != --version ] || exit 1
118
+ exec "$real" "\$@"
119
+ SH
120
+ chmod +x "$case_dir/fakebin/tasks-axi"
121
+ }
122
+
123
+ break_verb() { # <case-dir> <verb>
124
+ local case_dir=$1 verb=$2 real
125
+ real=$(command -v tasks-axi)
126
+ cat > "$case_dir/fakebin/tasks-axi" <<SH
127
+ #!/usr/bin/env bash
128
+ if [ "\${1:-}" = "$verb" ]; then
129
+ echo 'error: "backlog is unwritable"' >&2
130
+ exit 1
131
+ fi
132
+ exec "$real" "\$@"
133
+ SH
134
+ chmod +x "$case_dir/fakebin/tasks-axi"
135
+ }
136
+
137
+ interrupt_spawn_during_start() { # <case-dir> <before|after>
138
+ local case_dir=$1 timing=$2 real
139
+ real=$(command -v tasks-axi)
140
+ cat > "$case_dir/fakebin/tasks-axi" <<SH
141
+ #!/usr/bin/env bash
142
+ if [ "\${1:-}" = start ] && [ ! -f "$case_dir/start-interrupted" ]; then
143
+ : > "$case_dir/start-interrupted"
144
+ spawn_pid=\$(ps -o ppid= -p "\$PPID" | tr -d ' ')
145
+ case "\$spawn_pid" in ''|*[!0-9]*) exit 1 ;; esac
146
+ if [ "$timing" = before ]; then
147
+ kill -TERM "\$spawn_pid"
148
+ kill -TERM "\$\$"
149
+ fi
150
+ "$real" "\$@" || exit \$?
151
+ if [ "$timing" = after ]; then
152
+ kill -TERM "\$spawn_pid"
153
+ kill -TERM "\$\$"
154
+ fi
155
+ exit 0
156
+ fi
157
+ exec "$real" "\$@"
158
+ SH
159
+ chmod +x "$case_dir/fakebin/tasks-axi"
160
+ }
161
+
162
+ change_row_on_second_show() { # <case-dir> <done|rm>
163
+ local case_dir=$1 action=$2 real
164
+ real=$(command -v tasks-axi)
165
+ cat > "$case_dir/fakebin/tasks-axi" <<SH
166
+ #!/usr/bin/env bash
167
+ if [ "\${1:-}" = show ]; then
168
+ count=0
169
+ [ ! -f "$case_dir/show-count" ] || count=\$(cat "$case_dir/show-count")
170
+ count=\$((count + 1))
171
+ printf '%s\n' "\$count" > "$case_dir/show-count"
172
+ if [ "\$count" -eq 2 ]; then
173
+ "$real" "$action" "\$2" --file "\$4" >/dev/null || exit 1
174
+ fi
175
+ fi
176
+ exec "$real" "\$@"
177
+ SH
178
+ chmod +x "$case_dir/fakebin/tasks-axi"
179
+ }
180
+
181
+ break_launch_delivery() { # <case-dir>
182
+ local case_dir=$1
183
+ cat > "$case_dir/fakebin/tmux" <<'SH'
184
+ #!/usr/bin/env bash
185
+ case "$*" in *"#{pane_current_path}"*) printf '%s\n' "${FM_FAKE_PANE_PATH:-}"; exit 0 ;; esac
186
+ case "${1:-}" in
187
+ display-message) printf 'firstmate\n'; exit 0 ;;
188
+ send-keys) exit 1 ;;
189
+ esac
190
+ exit 0
191
+ SH
192
+ chmod +x "$case_dir/fakebin/tmux"
193
+ }
194
+
195
+ track_teardown_resource_actions() { # <case-dir>
196
+ local case_dir=$1
197
+ cat > "$case_dir/fakebin/tmux" <<SH
198
+ #!/usr/bin/env bash
199
+ : > "$case_dir/backend-resource-action"
200
+ exit 0
201
+ SH
202
+ cat > "$case_dir/fakebin/treehouse" <<SH
203
+ #!/usr/bin/env bash
204
+ : > "$case_dir/local-copy-resource-action"
205
+ exit 0
206
+ SH
207
+ chmod +x "$case_dir/fakebin/tmux" "$case_dir/fakebin/treehouse"
208
+ }
209
+
210
+ interrupt_teardown_during_treehouse_return() { # <case-dir>
211
+ local case_dir=$1
212
+ cat > "$case_dir/fakebin/treehouse" <<SH
213
+ #!/usr/bin/env bash
214
+ if [ "\${1:-}" = return ] && [ ! -f "$case_dir/teardown-interrupted" ]; then
215
+ : > "$case_dir/teardown-interrupted"
216
+ teardown_pid=\$(ps -o ppid= -p "\$PPID" | tr -d ' ')
217
+ case "\$teardown_pid" in ''|*[!0-9]*) exit 1 ;; esac
218
+ kill -TERM "\$teardown_pid"
219
+ kill -TERM "\$\$"
220
+ fi
221
+ exit 0
222
+ SH
223
+ chmod +x "$case_dir/fakebin/treehouse"
224
+ }
225
+
226
+ interrupt_kimi_readiness() { # <case-dir>
227
+ local case_dir=$1 home
228
+ home=$(home_of "$case_dir")
229
+ mkdir -p "$home/.kimi-code"
230
+ printf '# test config\n' > "$home/.kimi-code/config.toml"
231
+ fm_fake_exit0 "$case_dir/fakebin" kimi
232
+ cat > "$case_dir/fakebin/tmux" <<SH
233
+ #!/usr/bin/env bash
234
+ case "\$*" in
235
+ *"#{pane_current_path}"*) printf '%s\\n' "\${FM_FAKE_PANE_PATH:-}"; exit 0 ;;
236
+ *"#{cursor_y}"*) printf '1\\n'; exit 0 ;;
237
+ esac
238
+ case "\${1:-}" in
239
+ display-message) printf 'firstmate\\n'; exit 0 ;;
240
+ capture-pane)
241
+ if [ ! -f "$case_dir/kimi-interrupted" ]; then
242
+ : > "$case_dir/kimi-interrupted"
243
+ spawn_pid=\$(ps -o ppid= -p "\$PPID" | tr -d ' ')
244
+ case "\$spawn_pid" in ''|*[!0-9]*) exit 1 ;; esac
245
+ kill -TERM "\$spawn_pid"
246
+ fi
247
+ printf 'shell starting\\n$ \\n'
248
+ exit 0
249
+ ;;
250
+ esac
251
+ exit 0
252
+ SH
253
+ chmod +x "$case_dir/fakebin/tmux"
254
+ }
255
+
256
+ break_meta_removal() { # <case-dir> <meta-path>
257
+ local case_dir=$1 meta=$2 real
258
+ real=$(command -v rm)
259
+ cat > "$case_dir/fakebin/rm" <<SH
260
+ #!/usr/bin/env bash
261
+ for arg in "\$@"; do
262
+ [ "\$arg" != "$meta" ] || exit 1
263
+ done
264
+ exec "$real" "\$@"
265
+ SH
266
+ chmod +x "$case_dir/fakebin/rm"
267
+ }
268
+
269
+ break_busy_removal() { # <case-dir> <id>
270
+ local case_dir=$1 id=$2 real state
271
+ real=$(command -v rm)
272
+ state="$(home_of "$case_dir")/state"
273
+ cat > "$case_dir/fakebin/rm" <<SH
274
+ #!/usr/bin/env bash
275
+ for arg in "\$@"; do
276
+ case "\$arg" in
277
+ "$state/$id.busy-state"|"$state/$id.busy-gen") exit 1 ;;
278
+ esac
279
+ done
280
+ exec "$real" "\$@"
281
+ SH
282
+ chmod +x "$case_dir/fakebin/rm"
283
+ }
284
+
285
+ remove_data_during_startup_budget_check() { # <case-dir>
286
+ local case_dir=$1 real data saved budget
287
+ real=$(command -v stat)
288
+ data="$(home_of "$case_dir")/data"
289
+ saved="$case_dir/bootstrap-data"
290
+ budget="$(home_of "$case_dir")/config/startup-memory-budget"
291
+ printf '7500\n' > "$budget"
292
+ cat > "$case_dir/fakebin/stat" <<SH
293
+ #!/usr/bin/env bash
294
+ for arg in "\$@"; do
295
+ if [ "\$arg" = "$budget" ] && [ ! -e "$case_dir/data-removed" ]; then
296
+ mv "$data" "$saved" || exit 1
297
+ : > "$case_dir/data-removed"
298
+ fi
299
+ done
300
+ exec "$real" "\$@"
301
+ SH
302
+ chmod +x "$case_dir/fakebin/stat"
303
+ }
304
+
305
+ break_meta_publication() { # <case-dir> <meta-path>
306
+ local case_dir=$1 meta=$2 real
307
+ real=$(command -v mv)
308
+ cat > "$case_dir/fakebin/mv" <<SH
309
+ #!/usr/bin/env bash
310
+ for arg in "\$@"; do
311
+ [ "\$arg" != "$meta" ] || exit 1
312
+ done
313
+ exec "$real" "\$@"
314
+ SH
315
+ chmod +x "$case_dir/fakebin/mv"
316
+ }
317
+
318
+ write_task_meta() { # <case-dir> <id> <kind> <mode> [extra-line...]
319
+ local case_dir=$1 id=$2 kind=$3 mode=$4
320
+ shift 4
321
+ fm_write_meta "$(home_of "$case_dir")/state/$id.meta" \
322
+ "window=firstmate:fm-$id" \
323
+ "endpoint_task_id=$id" \
324
+ "worktree=$case_dir/absent-worktree" \
325
+ "project=$case_dir/absent-project" \
326
+ "harness=claude" \
327
+ "kind=$kind" \
328
+ "mode=$mode" \
329
+ "yolo=off" \
330
+ "$@"
331
+ }
332
+
333
+ run_spawn() { # <case-dir> <args...>
334
+ local case_dir=$1
335
+ shift
336
+ FM_ROOT_OVERRIDE="$ROOT" FM_HOME="$(home_of "$case_dir")" \
337
+ FM_SPAWN_NO_GUARD=1 FM_FAKE_PANE_PATH="$case_dir/wt" TMUX="fake,1,0" \
338
+ CLAUDE_CONFIG_DIR='' \
339
+ PATH="$case_dir/fakebin:$PATH" \
340
+ "$SPAWN" "$@" 2>&1
341
+ }
342
+
343
+ run_ship_spawn() { # <case-dir> <id>
344
+ local case_dir=$1 id=$2
345
+ run_spawn "$case_dir" "$id" "$case_dir/project" --mode no-mistakes --yolo off
346
+ }
347
+
348
+ # Teardown against a recorded worktree that no longer exists: the landed-work and
349
+ # worktree-return steps are then no-ops, which keeps these cases about the
350
+ # backlog transition rather than re-testing tests/fm-teardown.test.sh's matrix.
351
+ run_teardown() { # <case-dir> <id> [args...]
352
+ local case_dir=$1
353
+ shift
354
+ FM_ROOT_OVERRIDE="$ROOT" FM_HOME="$(home_of "$case_dir")" \
355
+ PATH="$case_dir/fakebin:$PATH" \
356
+ "$TEARDOWN" "$@" 2>&1
357
+ }
358
+
359
+ run_bootstrap() { # <case-dir>
360
+ local case_dir=$1
361
+ FM_ROOT_OVERRIDE="$ROOT" FM_HOME="$(home_of "$case_dir")" \
362
+ FM_BOOTSTRAP_NETWORK=skip \
363
+ PATH="$case_dir/fakebin:$PATH" \
364
+ "$BOOTSTRAP" 2>&1
365
+ }
366
+
367
+ # --- dispatch ---------------------------------------------------------------
368
+
369
+ test_dispatch_moves_the_item_in_flight_in_the_same_run() {
370
+ local case_dir id out
371
+ id=atomic-dispatch-b1
372
+ case_dir=$(make_home dispatch-ok "$id")
373
+ add_item "$case_dir" "$id"
374
+
375
+ out=$(run_ship_spawn "$case_dir" "$id") || fail "spawn failed: $out"
376
+ assert_contains "$out" "spawned $id" "spawn did not report success"
377
+ assert_present "$(home_of "$case_dir")/state/$id.meta" "spawn published no record"
378
+ [ "$(row_state "$case_dir" "$id")" = in_flight ] \
379
+ || fail "spawn reported success with its backlog item still $(row_state "$case_dir" "$id")"
380
+ pass "dispatch publishes the record and moves the backlog item In flight in one run"
381
+ }
382
+
383
+ test_dispatch_refuses_a_pending_authoritative_close() {
384
+ local case_dir id marker out rc=0
385
+ id=atomic-dispatch-pending-close-b1
386
+ case_dir=$(make_home dispatch-pending-close "$id")
387
+ add_item "$case_dir" "$id"
388
+ start_item "$case_dir" "$id"
389
+ marker="$(home_of "$case_dir")/state/$id.backlog-close"
390
+ printf 'id=%s\ndata=%s\nspawn_gen=spawn-closing\narg=--pr\narg=https://github.com/example/repo/pull/12\n' \
391
+ "$id" "$(home_of "$case_dir")/data" > "$marker"
392
+ cat > "$case_dir/fakebin/tmux" <<SH
393
+ #!/usr/bin/env bash
394
+ case "\$*" in
395
+ *new-window*) : > "$case_dir/task-endpoint-created" ;;
396
+ *treehouse\\ get*) : > "$case_dir/local-copy-requested" ;;
397
+ *"#{pane_current_path}"*) printf '%s\n' "\${FM_FAKE_PANE_PATH:-}"; exit 0 ;;
398
+ esac
399
+ case "\${1:-}" in display-message) printf 'firstmate\n'; exit 0 ;; esac
400
+ exit 0
401
+ SH
402
+ chmod +x "$case_dir/fakebin/tmux"
403
+
404
+ out=$(run_ship_spawn "$case_dir" "$id") || rc=$?
405
+ [ "$rc" -ne 0 ] || fail "spawn accepted work with an authoritative close still pending"
406
+ assert_contains "$out" "pending authoritative backlog close" \
407
+ "spawn did not explain why the pending close blocks dispatch"
408
+ assert_present "$marker" "spawn discarded the pending authoritative close"
409
+ assert_absent "$(home_of "$case_dir")/state/$id.meta" \
410
+ "spawn published a new worker over a pending close"
411
+ assert_absent "$case_dir/task-endpoint-created" \
412
+ "spawn created an unowned endpoint before refusing the pending close"
413
+ assert_absent "$case_dir/local-copy-requested" \
414
+ "spawn requested an unowned local copy before refusing the pending close"
415
+ [ "$(row_state "$case_dir" "$id")" = in_flight ] \
416
+ || fail "refused dispatch changed the pending close's backlog row"
417
+ pass "dispatch refuses to supersede a pending authoritative close"
418
+ }
419
+
420
+ test_dispatch_refuses_a_held_row_before_creating_resources() {
421
+ local case_dir id out rc=0
422
+ id=atomic-dispatch-held-b1
423
+ case_dir=$(make_home dispatch-held "$id")
424
+ add_item "$case_dir" "$id"
425
+ tasks-axi hold "$id" --reason "captain decision pending" --kind captain \
426
+ --file "$(backlog_of "$case_dir")" >/dev/null
427
+ cat > "$case_dir/fakebin/tmux" <<SH
428
+ #!/usr/bin/env bash
429
+ case "\$*" in
430
+ *new-window*) : > "$case_dir/task-endpoint-created" ;;
431
+ *treehouse\\ get*) : > "$case_dir/local-copy-requested" ;;
432
+ *"#{pane_current_path}"*) printf '%s\n' "\${FM_FAKE_PANE_PATH:-}"; exit 0 ;;
433
+ esac
434
+ case "\${1:-}" in display-message) printf 'firstmate\n'; exit 0 ;; esac
435
+ exit 0
436
+ SH
437
+ chmod +x "$case_dir/fakebin/tmux"
438
+
439
+ out=$(run_ship_spawn "$case_dir" "$id") || rc=$?
440
+ [ "$rc" -ne 0 ] || fail "spawn accepted a held backlog row"
441
+ assert_contains "$out" "state queued yes" \
442
+ "held-row refusal did not name the actual ineligible state"
443
+ assert_absent "$(home_of "$case_dir")/state/$id.meta" \
444
+ "held-row refusal published a task record"
445
+ assert_absent "$case_dir/task-endpoint-created" \
446
+ "held-row refusal created an unowned endpoint"
447
+ assert_absent "$case_dir/local-copy-requested" \
448
+ "held-row refusal requested an unowned local copy"
449
+ [ "$(row_state "$case_dir" "$id")" = queued ] \
450
+ || fail "held-row refusal changed the backlog state"
451
+ pass "dispatch refuses held rows before creating resources"
452
+ }
453
+
454
+ test_dispatch_refuses_a_blocked_row_before_creating_resources() {
455
+ local case_dir id blocker out rc=0
456
+ id=atomic-dispatch-blocked-b16
457
+ blocker=atomic-dispatch-blocker-b16
458
+ case_dir=$(make_home dispatch-blocked "$id" "$blocker")
459
+ add_item "$case_dir" "$blocker"
460
+ tasks-axi add "$id" "item for $id" --kind ship --blocked-by "$blocker" \
461
+ --file "$(backlog_of "$case_dir")" >/dev/null
462
+ cat > "$case_dir/fakebin/tmux" <<SH
463
+ #!/usr/bin/env bash
464
+ case "\$*" in
465
+ *new-window*) : > "$case_dir/task-endpoint-created" ;;
466
+ *treehouse\\ get*) : > "$case_dir/local-copy-requested" ;;
467
+ *"#{pane_current_path}"*) printf '%s\n' "\${FM_FAKE_PANE_PATH:-}"; exit 0 ;;
468
+ esac
469
+ case "\${1:-}" in display-message) printf 'firstmate\n'; exit 0 ;; esac
470
+ exit 0
471
+ SH
472
+ chmod +x "$case_dir/fakebin/tmux"
473
+
474
+ out=$(run_ship_spawn "$case_dir" "$id") || rc=$?
475
+ [ "$rc" -ne 0 ] || fail "spawn accepted a dependency-blocked backlog row"
476
+ assert_contains "$out" "state queued no yes" \
477
+ "blocked-row refusal did not name the actual ineligible state"
478
+ assert_absent "$(home_of "$case_dir")/state/$id.meta" \
479
+ "blocked-row refusal published a task record"
480
+ assert_absent "$case_dir/task-endpoint-created" \
481
+ "blocked-row refusal created an unowned endpoint"
482
+ assert_absent "$case_dir/local-copy-requested" \
483
+ "blocked-row refusal requested an unowned local copy"
484
+ [ "$(row_state "$case_dir" "$id")" = queued ] \
485
+ || fail "blocked-row refusal changed the backlog state"
486
+ pass "dispatch refuses dependency-blocked rows before creating resources"
487
+ }
488
+
489
+ test_dispatch_refuses_a_held_in_flight_row_before_relaunch() {
490
+ local case_dir id out rc=0
491
+ id=atomic-dispatch-held-in-flight-b16
492
+ case_dir=$(make_home dispatch-held-in-flight "$id")
493
+ add_item "$case_dir" "$id"
494
+ start_item "$case_dir" "$id"
495
+ tasks-axi hold "$id" --reason "captain decision pending" --kind captain \
496
+ --file "$(backlog_of "$case_dir")" >/dev/null
497
+ cat > "$case_dir/fakebin/tmux" <<SH
498
+ #!/usr/bin/env bash
499
+ case "\$*" in
500
+ *new-window*) : > "$case_dir/task-endpoint-created" ;;
501
+ *treehouse\\ get*) : > "$case_dir/local-copy-requested" ;;
502
+ *"#{pane_current_path}"*) printf '%s\n' "\${FM_FAKE_PANE_PATH:-}"; exit 0 ;;
503
+ esac
504
+ case "\${1:-}" in display-message) printf 'firstmate\n'; exit 0 ;; esac
505
+ exit 0
506
+ SH
507
+ chmod +x "$case_dir/fakebin/tmux"
508
+
509
+ out=$(run_ship_spawn "$case_dir" "$id") || rc=$?
510
+ [ "$rc" -ne 0 ] || fail "spawn accepted a held In-flight backlog row"
511
+ assert_contains "$out" "state in_flight yes no" \
512
+ "held In-flight refusal did not name the actual ineligible state"
513
+ assert_absent "$(home_of "$case_dir")/state/$id.meta" \
514
+ "held In-flight refusal published a task record"
515
+ assert_absent "$case_dir/task-endpoint-created" \
516
+ "held In-flight refusal created a replacement endpoint"
517
+ assert_absent "$case_dir/local-copy-requested" \
518
+ "held In-flight refusal requested a replacement local copy"
519
+ [ "$(row_state "$case_dir" "$id")" = in_flight ] \
520
+ || fail "held In-flight refusal changed the backlog state"
521
+ pass "dispatch refuses held In-flight rows before relaunch"
522
+ }
523
+
524
+ test_dispatch_reads_the_row_from_the_backlog_root() {
525
+ local case_dir id out
526
+ id=atomic-dispatch-root-b2
527
+ case_dir=$(make_home dispatch-root "$id")
528
+ add_item "$case_dir" "$id"
529
+ require_show_cwd "$case_dir" "$(cd "$(home_of "$case_dir")" && pwd -P)"
530
+
531
+ out=$(run_ship_spawn "$case_dir" "$id") || fail "spawn read outside the backlog root: $out"
532
+ [ "$(row_state "$case_dir" "$id")" = in_flight ] \
533
+ || fail "root-addressed dispatch left the backlog row queued"
534
+ assert_present "$(home_of "$case_dir")/state/$id.meta" \
535
+ "root-addressed dispatch did not publish its task record"
536
+ pass "dispatch reads backlog rows from the backlog addressing root"
537
+ }
538
+
539
+ test_recovery_uses_the_parent_of_a_trailing_slash_data_record() {
540
+ local case_dir id relocated backlog marker out
541
+ id=atomic-recovery-relocated-root-b2
542
+ case_dir=$(make_home recovery-relocated-root)
543
+ relocated="$case_dir/fm-records"
544
+ mkdir -p "$relocated"
545
+ backlog="$relocated/backlog.md"
546
+ printf '%s\n' '# Backlog' '' '## In flight' '' '## Queued' '' '## Done' > "$backlog"
547
+ tasks-axi add "$id" "item for $id" --kind ship --file "$backlog" >/dev/null
548
+ tasks-axi start "$id" --file "$backlog" >/dev/null
549
+ marker="$(home_of "$case_dir")/state/$id.backlog-close"
550
+ printf 'id=%s\ndata=%s/\nspawn_gen=spawn-relocated-recovery\narg=--note\narg=local%%20main\n' "$id" "$relocated" > "$marker"
551
+ require_show_cwd "$case_dir" "$(cd "$case_dir" && pwd -P)"
552
+
553
+ out=$(FM_DATA_OVERRIDE="$relocated/" run_bootstrap "$case_dir")
554
+ [ "$(tasks-axi show "$id" --file "$backlog" 2>/dev/null | sed -n 's/^ state: *//p' | head -1)" = "done" ] \
555
+ || fail "relocated-data recovery used the wrong addressing root: $out"
556
+ assert_absent "$marker" "relocated-data recovery retained its close marker"
557
+ pass "recovery uses the parent of a trailing-slash data record"
558
+ }
559
+
560
+ test_completion_targets_a_nested_relative_data_directory() {
561
+ local case_dir id relative_data data data_resolved backlog out
562
+ id=atomic-close-relative-data-b2
563
+ case_dir=$(make_home close-relative-data)
564
+ relative_data=relocated/data
565
+ data="$case_dir/$relative_data"
566
+ mkdir -p "$case_dir/relocated"
567
+ mv "$(home_of "$case_dir")/data" "$data"
568
+ data_resolved=$(cd "$data" && pwd -P)
569
+ backlog="$data/backlog.md"
570
+ tasks-axi add "$id" "item for $id" --kind ship --file "$backlog" >/dev/null
571
+ tasks-axi start "$id" --file "$backlog" >/dev/null
572
+ write_task_meta "$case_dir" "$id" ship local-only "spawn_gen=spawn-relative-data"
573
+
574
+ out=$(cd "$case_dir" && \
575
+ FM_ROOT_OVERRIDE="$ROOT" FM_HOME="$(home_of "$case_dir")" \
576
+ FM_DATA_OVERRIDE="$relative_data" PATH="$case_dir/fakebin:$PATH" \
577
+ "$TEARDOWN" "$id" 2>&1) \
578
+ || fail "relative-data teardown failed: $out"
579
+ [ "$(tasks-axi show "$id" --file "$backlog" 2>/dev/null | sed -n 's/^ state: *//p' | head -1)" = "done" ] \
580
+ || fail "relative-data teardown mutated a different backlog file"
581
+ assert_absent "$(home_of "$case_dir")/state/$id.meta" \
582
+ "relative-data teardown retained its task record"
583
+ assert_absent "$(home_of "$case_dir")/state/$id.backlog-close" \
584
+ "relative-data teardown retained its close marker"
585
+ assert_contains "$out" "closed in $data_resolved/backlog.md" \
586
+ "relative-data completion collapsed the configured backlog path"
587
+ pass "completion targets nested relative data from the caller directory"
588
+ }
589
+
590
+ test_immediate_child_absolute_data_dispatches_and_completes() {
591
+ local case_dir id data data_resolved backlog out
592
+ id=atomic-immediate-child-data-b2
593
+ case_dir=$(make_home immediate-child-data "$id")
594
+ data="$case_dir/fm-records"
595
+ mv "$(home_of "$case_dir")/data" "$data"
596
+ data_resolved=$(cd "$data" && pwd -P)
597
+ backlog="$data/backlog.md"
598
+ tasks-axi add "$id" "item for $id" --kind ship --file "$backlog" >/dev/null
599
+
600
+ out=$(FM_DATA_OVERRIDE="$data" run_ship_spawn "$case_dir" "$id") \
601
+ || fail "immediate-child-data spawn failed: $out"
602
+ [ "$(tasks-axi show "$id" --file "$backlog" 2>/dev/null | sed -n 's/^ state: *//p' | head -1)" = in_flight ] \
603
+ || fail "immediate-child absolute dispatch mutated a different backlog"
604
+ rm -f "$(home_of "$case_dir")/state/$id.meta"
605
+ write_task_meta "$case_dir" "$id" ship local-only "spawn_gen=spawn-immediate-child"
606
+ out=$(FM_DATA_OVERRIDE="$data" run_teardown "$case_dir" "$id") \
607
+ || fail "immediate-child-data teardown failed: $out"
608
+ [ "$(tasks-axi show "$id" --file "$backlog" 2>/dev/null | sed -n 's/^ state: *//p' | head -1)" = "done" ] \
609
+ || fail "immediate-child absolute completion mutated a different backlog"
610
+ assert_contains "$out" "closed in $data_resolved/backlog.md" \
611
+ "relocated completion confirmed the wrong backlog path"
612
+ pass "an immediate-child absolute data path keeps one paired backlog"
613
+ }
614
+
615
+ test_bare_relative_data_dispatches_and_completes() {
616
+ local case_dir id data backlog out
617
+ id=atomic-bare-relative-data-b2
618
+ case_dir=$(make_home bare-relative-data "$id")
619
+ data="$case_dir/records"
620
+ mv "$(home_of "$case_dir")/data" "$data"
621
+ backlog="$data/backlog.md"
622
+ tasks-axi add "$id" "item for $id" --kind ship --file "$backlog" >/dev/null
623
+
624
+ out=$(cd "$case_dir" && FM_DATA_OVERRIDE=records run_ship_spawn "$case_dir" "$id") \
625
+ || fail "bare-relative-data spawn failed: $out"
626
+ [ "$(tasks-axi show "$id" --file "$backlog" 2>/dev/null | sed -n 's/^ state: *//p' | head -1)" = in_flight ] \
627
+ || fail "bare relative dispatch mutated a different backlog"
628
+ rm -f "$(home_of "$case_dir")/state/$id.meta"
629
+ write_task_meta "$case_dir" "$id" ship local-only "spawn_gen=spawn-bare-relative"
630
+ out=$(cd "$case_dir" && FM_DATA_OVERRIDE=records run_teardown "$case_dir" "$id") \
631
+ || fail "bare-relative-data teardown failed: $out"
632
+ [ "$(tasks-axi show "$id" --file "$backlog" 2>/dev/null | sed -n 's/^ state: *//p' | head -1)" = "done" ] \
633
+ || fail "bare relative completion mutated a different backlog"
634
+ pass "bare relative data addresses one backlog through dispatch and completion"
635
+ }
636
+
637
+ test_dispatch_refuses_a_symlinked_backlog_without_crossing_homes() {
638
+ local case_dir foreign_case id local_backlog foreign_backlog out rc=0
639
+ id=atomic-dispatch-symlink-backlog-b2
640
+ case_dir=$(make_home dispatch-symlink-backlog "$id")
641
+ foreign_case=$(make_home dispatch-symlink-backlog-foreign)
642
+ add_item "$foreign_case" "$id"
643
+ local_backlog=$(backlog_of "$case_dir")
644
+ foreign_backlog=$(backlog_of "$foreign_case")
645
+ rm -f "$local_backlog"
646
+ ln -s "$foreign_backlog" "$local_backlog"
647
+
648
+ out=$(run_ship_spawn "$case_dir" "$id") || rc=$?
649
+ [ "$rc" -ne 0 ] || fail "spawn accepted a symlinked backlog"
650
+ assert_contains "$out" "backlog file resolves outside its authorized directory" \
651
+ "spawn did not identify the unsafe backlog boundary"
652
+ [ -L "$local_backlog" ] || fail "spawn replaced the local backlog symlink"
653
+ [ "$(row_state "$foreign_case" "$id")" = queued ] \
654
+ || fail "spawn mutated the foreign backlog row"
655
+ assert_absent "$(home_of "$case_dir")/state/$id.meta" \
656
+ "unsafe backlog dispatch published a local task record"
657
+ pass "dispatch refuses symlinked backlogs without crossing homes"
658
+ }
659
+
660
+ test_automatic_backend_refuses_incompatible_tasks_axi_before_mutation() {
661
+ local spawn_case teardown_case id out rc=0
662
+ id=atomic-incompatible-tasks-axi-b2
663
+ spawn_case=$(make_home incompatible-tasks-axi-spawn "$id")
664
+ add_item "$spawn_case" "$id"
665
+ make_tasks_axi_incompatible "$spawn_case"
666
+
667
+ out=$(run_ship_spawn "$spawn_case" "$id") || rc=$?
668
+ [ "$rc" -ne 0 ] || fail "automatic spawn succeeded without compatible tasks-axi"
669
+ assert_contains "$out" "automatic backlog transitions require tasks-axi" \
670
+ "automatic spawn did not report its unavailable transition tool"
671
+ assert_absent "$(home_of "$spawn_case")/state/$id.meta" \
672
+ "automatic spawn published a record without transition tooling"
673
+ rm -f "$spawn_case/fakebin/tasks-axi"
674
+ [ "$(row_state "$spawn_case" "$id")" = queued ] \
675
+ || fail "automatic spawn changed the row without transition tooling"
676
+
677
+ teardown_case=$(make_home incompatible-tasks-axi-teardown)
678
+ add_item "$teardown_case" "$id"
679
+ start_item "$teardown_case" "$id"
680
+ write_task_meta "$teardown_case" "$id" ship local-only "spawn_gen=spawn-incompatible"
681
+ make_tasks_axi_incompatible "$teardown_case"
682
+ rc=0
683
+ out=$(run_teardown "$teardown_case" "$id") || rc=$?
684
+ [ "$rc" -ne 0 ] || fail "automatic teardown succeeded without compatible tasks-axi"
685
+ assert_contains "$out" "automatic backlog transitions require tasks-axi" \
686
+ "automatic teardown did not report its unavailable transition tool"
687
+ assert_present "$(home_of "$teardown_case")/state/$id.meta" \
688
+ "automatic teardown removed its record without transition tooling"
689
+ rm -f "$teardown_case/fakebin/tasks-axi"
690
+ [ "$(row_state "$teardown_case" "$id")" = in_flight ] \
691
+ || fail "automatic teardown changed the row without transition tooling"
692
+ pass "automatic homes refuse lifecycle mutation without compatible tasks-axi"
693
+ }
694
+
695
+ test_dispatch_refuses_an_unresolvable_data_directory() {
696
+ local case_dir id saved out rc=0
697
+ id=atomic-dispatch-missing-data-b2
698
+ case_dir=$(make_home dispatch-missing-data "$id")
699
+ add_item "$case_dir" "$id"
700
+ saved="$case_dir/backlog-data"
701
+ mv "$(home_of "$case_dir")/data" "$saved"
702
+
703
+ out=$(run_ship_spawn "$case_dir" "$id") || rc=$?
704
+ [ "$rc" -ne 0 ] || fail "spawn succeeded with an unresolvable data directory"
705
+ assert_contains "$out" "task $id" \
706
+ "spawn did not identify the task blocked by fatal backlog addressing"
707
+ assert_contains "$out" "$(home_of "$case_dir")/data" \
708
+ "spawn did not identify the inaccessible data directory"
709
+ assert_absent "$(home_of "$case_dir")/state/$id.meta" \
710
+ "fatal backlog addressing created a task record"
711
+ [ "$(tasks-axi show "$id" --file "$saved/backlog.md" 2>/dev/null | sed -n 's/^ state: *//p' | head -1)" = queued ] \
712
+ || fail "fatal backlog addressing changed the queued row"
713
+ pass "dispatch refuses an unresolvable backlog data directory"
714
+ }
715
+
716
+ test_completion_refuses_an_unresolvable_data_directory() {
717
+ local case_dir id saved meta out rc=0
718
+ id=atomic-close-missing-data-b2
719
+ case_dir=$(make_home close-missing-data)
720
+ add_item "$case_dir" "$id"
721
+ start_item "$case_dir" "$id"
722
+ write_task_meta "$case_dir" "$id" ship local-only "spawn_gen=spawn-missing-data"
723
+ meta="$(home_of "$case_dir")/state/$id.meta"
724
+ saved="$case_dir/backlog-data"
725
+ mv "$(home_of "$case_dir")/data" "$saved"
726
+
727
+ out=$(run_teardown "$case_dir" "$id") || rc=$?
728
+ [ "$rc" -ne 0 ] || fail "teardown succeeded with an unresolvable data directory"
729
+ assert_contains "$out" "task $id cannot be torn down" \
730
+ "teardown did not identify the task blocked by fatal backlog addressing"
731
+ assert_present "$meta" "fatal backlog addressing removed the task record"
732
+ assert_absent "$(home_of "$case_dir")/state/$id.backlog-close" \
733
+ "fatal backlog addressing wrote a close marker"
734
+ [ "$(tasks-axi show "$id" --file "$saved/backlog.md" 2>/dev/null | sed -n 's/^ state: *//p' | head -1)" = in_flight ] \
735
+ || fail "fatal backlog addressing changed the In-flight row"
736
+ pass "completion refuses before mutation when backlog data is unresolvable"
737
+ }
738
+
739
+ test_dispatch_refuses_an_id_this_home_has_no_item_for() {
740
+ local case_dir id out rc=0
741
+ id=atomic-dispatch-b2
742
+ case_dir=$(make_home dispatch-no-item "$id")
743
+
744
+ out=$(run_ship_spawn "$case_dir" "$id") || rc=$?
745
+ [ "$rc" -ne 0 ] || fail "spawn dispatched work no backlog item owns"
746
+ assert_contains "$out" "no backlog item in this home" \
747
+ "spawn refused without naming the missing backlog item"
748
+ assert_absent "$(home_of "$case_dir")/state/$id.meta" \
749
+ "refused dispatch still left a record behind"
750
+ pass "dispatch refuses, before creating anything, when the home has no item for the id"
751
+ }
752
+
753
+ test_dispatch_reports_a_backlog_read_failure() {
754
+ local case_dir id out rc=0
755
+ id=atomic-dispatch-read-failure-b3
756
+ case_dir=$(make_home dispatch-read-failure "$id")
757
+ add_item "$case_dir" "$id"
758
+ break_verb "$case_dir" show
759
+
760
+ out=$(run_ship_spawn "$case_dir" "$id") || rc=$?
761
+ [ "$rc" -ne 0 ] || fail "spawn succeeded though backlog preflight could not read its item"
762
+ assert_contains "$out" "backlog item could not be read before dispatch" \
763
+ "spawn misreported a backlog read failure"
764
+ assert_contains "$out" "backlog is unwritable" \
765
+ "spawn discarded the backlog reader's diagnostic"
766
+ assert_absent "$(home_of "$case_dir")/state/$id.meta" \
767
+ "failed backlog preflight created a task record"
768
+ pass "dispatch distinguishes backlog read failures from missing items"
769
+ }
770
+
771
+ test_dispatch_refuses_a_closed_item() {
772
+ local case_dir id out rc=0
773
+ id=atomic-dispatch-b3
774
+ case_dir=$(make_home dispatch-closed "$id")
775
+ add_item "$case_dir" "$id"
776
+ tasks-axi "done" "$id" --file "$(backlog_of "$case_dir")" >/dev/null
777
+
778
+ out=$(run_ship_spawn "$case_dir" "$id") || rc=$?
779
+ [ "$rc" -ne 0 ] || fail "spawn dispatched onto an item the backlog already closed"
780
+ [ "$(row_state "$case_dir" "$id")" = "done" ] \
781
+ || fail "refused dispatch silently reopened a closed item"
782
+ assert_absent "$(home_of "$case_dir")/state/$id.meta" \
783
+ "refused dispatch onto a closed item still left a record behind"
784
+ pass "dispatch refuses a closed item instead of silently reopening it"
785
+ }
786
+
787
+ test_dispatch_refuses_to_commit_without_a_published_record() {
788
+ local case_dir id meta out rc=0
789
+ id=atomic-dispatch-publish-failure-b4
790
+ case_dir=$(make_home dispatch-publish-failure "$id")
791
+ add_item "$case_dir" "$id"
792
+ meta="$(home_of "$case_dir")/state/$id.meta"
793
+ break_meta_publication "$case_dir" "$meta"
794
+
795
+ out=$(run_ship_spawn "$case_dir" "$id") || rc=$?
796
+ [ "$rc" -ne 0 ] || fail "spawn succeeded without publishing its task record"
797
+ assert_contains "$out" "task record for $id could not be published" \
798
+ "spawn did not report task-record publication failure"
799
+ assert_absent "$meta" "failed publication left a task record"
800
+ assert_absent "$(home_of "$case_dir")/state/$id.busy-state" \
801
+ "failed publication retained its busy state"
802
+ [ "$(row_state "$case_dir" "$id")" = queued ] \
803
+ || fail "failed publication moved the backlog row"
804
+ pass "dispatch cannot commit without a verified task-record publication"
805
+ }
806
+
807
+ test_dispatch_leaves_no_record_when_the_transition_fails() {
808
+ local case_dir id out rc=0
809
+ id=atomic-dispatch-b4
810
+ case_dir=$(make_home dispatch-transition-fails "$id")
811
+ add_item "$case_dir" "$id"
812
+ break_verb "$case_dir" start
813
+
814
+ out=$(run_ship_spawn "$case_dir" "$id") || rc=$?
815
+ [ "$rc" -ne 0 ] || fail "spawn reported success though the backlog transition failed"
816
+ assert_contains "$out" "could not be moved to In flight" \
817
+ "spawn failed without explaining the backlog transition failure"
818
+ assert_absent "$(home_of "$case_dir")/state/$id.meta" \
819
+ "a failed backlog transition left an orphaned record behind"
820
+ assert_absent "$(home_of "$case_dir")/state/$id.busy-state" \
821
+ "a failed backlog transition left the task's armed busy generation behind"
822
+ [ "$(row_state "$case_dir" "$id")" = queued ] \
823
+ || fail "a failed dispatch left the backlog item in $(row_state "$case_dir" "$id")"
824
+ pass "a failed backlog transition fails the dispatch loudly and leaves no record"
825
+ }
826
+
827
+ test_dispatch_reports_an_incomplete_record_rollback() {
828
+ local case_dir id meta out rc=0
829
+ id=atomic-dispatch-remove-failure-b5
830
+ case_dir=$(make_home dispatch-remove-failure "$id")
831
+ add_item "$case_dir" "$id"
832
+ meta="$(home_of "$case_dir")/state/$id.meta"
833
+ break_verb "$case_dir" start
834
+ break_meta_removal "$case_dir" "$meta"
835
+
836
+ out=$(run_ship_spawn "$case_dir" "$id") || rc=$?
837
+ [ "$rc" -ne 0 ] || fail "spawn reported success though transition and rollback failed"
838
+ assert_contains "$out" "failed-dispatch cleanup is incomplete" \
839
+ "spawn did not report that its provisional record remained"
840
+ assert_present "$meta" "failed record removal was reported as successful"
841
+ assert_absent "$(home_of "$case_dir")/state/$id.busy-state" \
842
+ "record-removal failure prevented busy-state rollback"
843
+ [ "$(row_state "$case_dir" "$id")" = queued ] \
844
+ || fail "failed rollback changed the backlog row"
845
+ pass "dispatch reports when failed-transition rollback cannot remove its record"
846
+ }
847
+
848
+ test_dispatch_reports_an_incomplete_busy_rollback() {
849
+ local case_dir id out rc=0
850
+ id=atomic-dispatch-busy-remove-failure-b5
851
+ case_dir=$(make_home dispatch-busy-remove-failure "$id")
852
+ add_item "$case_dir" "$id"
853
+ break_verb "$case_dir" start
854
+ break_busy_removal "$case_dir" "$id"
855
+
856
+ out=$(run_ship_spawn "$case_dir" "$id") || rc=$?
857
+ [ "$rc" -ne 0 ] || fail "spawn succeeded though busy rollback failed"
858
+ assert_contains "$out" "did not remove both task and busy records" \
859
+ "spawn did not report incomplete busy rollback"
860
+ assert_absent "$(home_of "$case_dir")/state/$id.meta" \
861
+ "busy rollback failure retained the provisional task record"
862
+ assert_present "$(home_of "$case_dir")/state/$id.busy-state" \
863
+ "busy removal failure was reported as successful"
864
+ [ "$(row_state "$case_dir" "$id")" = queued ] \
865
+ || fail "failed busy rollback changed the backlog row"
866
+ pass "dispatch verifies both task and busy records during rollback"
867
+ }
868
+
869
+ test_dispatch_rolls_back_before_a_failed_launch_delivery() {
870
+ local case_dir id out rc=0
871
+ id=atomic-dispatch-delivery-fails-b5
872
+ case_dir=$(make_home dispatch-delivery-fails "$id")
873
+ add_item "$case_dir" "$id"
874
+ break_launch_delivery "$case_dir"
875
+
876
+ out=$(run_ship_spawn "$case_dir" "$id") || rc=$?
877
+ [ "$rc" -ne 0 ] || fail "spawn reported success though launch delivery failed"
878
+ assert_absent "$(home_of "$case_dir")/state/$id.meta" \
879
+ "a failed launch delivery left its provisional record behind"
880
+ assert_absent "$(home_of "$case_dir")/state/$id.busy-state" \
881
+ "a failed launch delivery left its provisional busy generation behind"
882
+ [ "$(row_state "$case_dir" "$id")" = queued ] \
883
+ || fail "launch delivery failed after committing backlog state $(row_state "$case_dir" "$id")"
884
+ pass "dispatch commits neither record nor backlog state before launch delivery succeeds"
885
+ }
886
+
887
+ test_dispatch_defers_interruption_across_backlog_commit() {
888
+ local timing case_dir id out rc
889
+ for timing in before after; do
890
+ id="atomic-dispatch-interrupted-$timing-b5"
891
+ case_dir=$(make_home "dispatch-interrupted-$timing" "$id")
892
+ add_item "$case_dir" "$id"
893
+ interrupt_spawn_during_start "$case_dir" "$timing"
894
+
895
+ rc=0
896
+ out=$(run_ship_spawn "$case_dir" "$id") || rc=$?
897
+ [ "$rc" -ne 0 ] || fail "a $timing-commit interruption was reported as success"
898
+ assert_contains "$out" "paired task record and In-flight backlog state were preserved" \
899
+ "a $timing-commit interruption did not report its atomic outcome"
900
+ [ "$(row_state "$case_dir" "$id")" = in_flight ] \
901
+ || fail "a $timing-commit interruption left the backlog row queued"
902
+ assert_present "$(home_of "$case_dir")/state/$id.meta" \
903
+ "a $timing-commit interruption removed the paired task record"
904
+ done
905
+ pass "dispatch retries interrupted transitions before honoring termination"
906
+ }
907
+
908
+ test_dispatch_interruption_during_kimi_readiness_fails_before_commit() {
909
+ local case_dir home id out rc=0
910
+ id=atomic-dispatch-kimi-readiness-signal-b5
911
+ case_dir=$(make_home dispatch-kimi-readiness-signal "$id")
912
+ home=$(home_of "$case_dir")
913
+ add_item "$case_dir" "$id"
914
+ interrupt_kimi_readiness "$case_dir"
915
+
916
+ out=$(HOME="$home" FM_KIMI_READY_POLLS=2 FM_KIMI_POLL_INTERVAL=0 \
917
+ run_spawn "$case_dir" "$id" "$case_dir/project" --harness kimi \
918
+ --mode no-mistakes --yolo off) || rc=$?
919
+ [ "$rc" -ne 0 ] || fail "Kimi readiness interruption was reported as success"
920
+ assert_absent "$home/state/$id.meta" \
921
+ "Kimi readiness interruption retained an unconfirmed task record"
922
+ [ "$(row_state "$case_dir" "$id")" = queued ] \
923
+ || fail "Kimi readiness interruption committed unconfirmed work In flight: $out"
924
+ pass "Kimi readiness interruptions fail before backlog commit"
925
+ }
926
+
927
+ test_dispatch_does_not_resurrect_a_row_closed_after_preflight() {
928
+ local case_dir id out rc=0
929
+ id=atomic-dispatch-closed-race-b5
930
+ case_dir=$(make_home dispatch-closed-race "$id")
931
+ add_item "$case_dir" "$id"
932
+ change_row_on_second_show "$case_dir" "done"
933
+
934
+ out=$(run_ship_spawn "$case_dir" "$id") || rc=$?
935
+ [ "$rc" -ne 0 ] || fail "spawn succeeded after its backlog row was closed"
936
+ assert_contains "$out" "state done" "spawn did not report the row's ineligible state"
937
+ [ "$(row_state "$case_dir" "$id")" = "done" ] \
938
+ || fail "spawn resurrected a row closed after preflight"
939
+ assert_absent "$(home_of "$case_dir")/state/$id.meta" \
940
+ "spawn retained a record after its row was closed"
941
+ pass "dispatch does not resurrect a row closed after preflight"
942
+ }
943
+
944
+ test_dispatch_fails_when_its_row_vanishes_after_preflight() {
945
+ local case_dir id out rc=0
946
+ id=atomic-dispatch-removed-race-b6
947
+ case_dir=$(make_home dispatch-removed-race "$id")
948
+ add_item "$case_dir" "$id"
949
+ change_row_on_second_show "$case_dir" rm
950
+
951
+ out=$(run_ship_spawn "$case_dir" "$id") || rc=$?
952
+ [ "$rc" -ne 0 ] || fail "spawn succeeded after its backlog row vanished"
953
+ assert_contains "$out" "vanished before dispatch commit" \
954
+ "spawn did not report that its backlog row vanished"
955
+ assert_absent "$(home_of "$case_dir")/state/$id.meta" \
956
+ "spawn retained a record after its backlog row vanished"
957
+ [ -z "$(row_state "$case_dir" "$id")" ] || fail "spawn recreated a removed backlog row"
958
+ pass "dispatch fails when its backlog row vanishes after preflight"
959
+ }
960
+
961
+ # --- completion -------------------------------------------------------------
962
+
963
+ test_completion_closes_a_local_only_ship_before_reporting_success() {
964
+ local case_dir id out
965
+ id=atomic-close-b5
966
+ case_dir=$(make_home close-local-only)
967
+ add_item "$case_dir" "$id"
968
+ start_item "$case_dir" "$id"
969
+ write_task_meta "$case_dir" "$id" ship local-only "spawn_gen=spawn-close-local"
970
+
971
+ out=$(run_teardown "$case_dir" "$id") || fail "teardown failed: $out"
972
+ [ "$(row_state "$case_dir" "$id")" = "done" ] \
973
+ || fail "teardown reported success with the item still $(row_state "$case_dir" "$id")"
974
+ assert_grep 'local main' "$(backlog_of "$case_dir")" \
975
+ "a local-only landing was closed without its local-main note"
976
+ pass "completion closes a local-only ship, with its landing note, before reporting success"
977
+ }
978
+
979
+ test_completion_closes_a_scout_with_its_report() {
980
+ local case_dir id out
981
+ id=atomic-close-b6
982
+ case_dir=$(make_home close-scout)
983
+ add_item "$case_dir" "$id" scout
984
+ start_item "$case_dir" "$id"
985
+ write_task_meta "$case_dir" "$id" scout '' "spawn_gen=spawn-close-scout"
986
+ # A scout's deliverable is its report, and teardown also enforces the shared
987
+ # captain-call completion gate; satisfy both the way a real scout does.
988
+ mkdir -p "$(home_of "$case_dir")/data/$id"
989
+ printf 'findings\n' > "$(home_of "$case_dir")/data/$id/report.md"
990
+ FM_ROOT_OVERRIDE="$ROOT" FM_HOME="$(home_of "$case_dir")" \
991
+ PATH="$case_dir/fakebin:$PATH" \
992
+ "$ROOT/bin/fm-captain-hold.sh" complete "$id" --none >/dev/null \
993
+ || fail "could not record the scout's completed captain-call inventory"
994
+
995
+ out=$(run_teardown "$case_dir" "$id") || fail "teardown failed: $out"
996
+ [ "$(row_state "$case_dir" "$id")" = "done" ] \
997
+ || fail "teardown reported success with the scout item still $(row_state "$case_dir" "$id")"
998
+ assert_grep "data/$id/report.md" "$(backlog_of "$case_dir")" \
999
+ "a closed scout item did not record its report"
1000
+ pass "completion closes a scout item against its report"
1001
+ }
1002
+
1003
+ test_completion_refuses_a_legacy_record_without_an_incarnation() {
1004
+ local case_dir id meta out rc=0
1005
+ id=atomic-close-legacy-no-incarnation-b7
1006
+ case_dir=$(make_home close-legacy-no-incarnation)
1007
+ add_item "$case_dir" "$id"
1008
+ start_item "$case_dir" "$id"
1009
+ write_task_meta "$case_dir" "$id" ship local-only
1010
+ meta="$(home_of "$case_dir")/state/$id.meta"
1011
+
1012
+ out=$(run_teardown "$case_dir" "$id") || rc=$?
1013
+ [ "$rc" -ne 0 ] || fail "teardown accepted a record with no durable incarnation"
1014
+ assert_contains "$out" "record has no spawn_gen" \
1015
+ "teardown did not explain why the legacy record cannot close automatically"
1016
+ assert_present "$meta" "legacy-record refusal removed the task record"
1017
+ assert_absent "$(home_of "$case_dir")/state/$id.backlog-close" \
1018
+ "legacy-record refusal wrote an unrecoverable close marker"
1019
+ [ "$(row_state "$case_dir" "$id")" = in_flight ] \
1020
+ || fail "legacy-record refusal changed the backlog row"
1021
+ pass "completion leaves legacy records open when no incarnation can be recorded"
1022
+ }
1023
+
1024
+ test_completion_refuses_ambiguous_incarnation_metadata() {
1025
+ local case_dir id meta marker out rc=0
1026
+ id=atomic-close-ambiguous-incarnation-b7
1027
+ case_dir=$(make_home close-ambiguous-incarnation)
1028
+ add_item "$case_dir" "$id"
1029
+ start_item "$case_dir" "$id"
1030
+ write_task_meta "$case_dir" "$id" ship local-only \
1031
+ "spawn_gen=spawn-old" "spawn_gen=spawn-current"
1032
+ meta="$(home_of "$case_dir")/state/$id.meta"
1033
+ marker="$(home_of "$case_dir")/state/$id.backlog-close"
1034
+
1035
+ out=$(run_teardown "$case_dir" "$id") || rc=$?
1036
+ [ "$rc" -ne 0 ] || fail "teardown accepted ambiguous incarnation metadata"
1037
+ assert_contains "$out" "has 2 spawn generation fields" \
1038
+ "teardown did not report the ambiguous incarnation"
1039
+ assert_present "$meta" "ambiguous-incarnation refusal removed the task record"
1040
+ assert_absent "$marker" "ambiguous-incarnation refusal published a close"
1041
+ [ "$(row_state "$case_dir" "$id")" = in_flight ] \
1042
+ || fail "ambiguous-incarnation refusal changed the backlog row"
1043
+ pass "completion refuses ambiguous task incarnations"
1044
+ }
1045
+
1046
+ test_completion_records_a_relative_report_for_relocated_data() {
1047
+ local case_dir id relocated backlog out
1048
+ id=atomic-close-relocated-scout-b7
1049
+ case_dir=$(make_home close-relocated-scout)
1050
+ relocated="$case_dir/relocated/data"
1051
+ mkdir -p "$case_dir/relocated"
1052
+ mv "$(home_of "$case_dir")/data" "$relocated"
1053
+ backlog="$relocated/backlog.md"
1054
+ tasks-axi add "$id" "item for $id" --kind scout --file "$backlog" >/dev/null
1055
+ tasks-axi start "$id" --file "$backlog" >/dev/null
1056
+ write_task_meta "$case_dir" "$id" scout '' "spawn_gen=spawn-relocated-scout"
1057
+ mkdir -p "$relocated/$id"
1058
+ printf 'findings\n' > "$relocated/$id/report.md"
1059
+ FM_ROOT_OVERRIDE="$ROOT" FM_HOME="$(home_of "$case_dir")" \
1060
+ FM_DATA_OVERRIDE="$relocated////" PATH="$case_dir/fakebin:$PATH" \
1061
+ "$ROOT/bin/fm-captain-hold.sh" complete "$id" --none >/dev/null \
1062
+ || fail "could not record the relocated scout's captain-call inventory"
1063
+
1064
+ out=$(FM_DATA_OVERRIDE="$relocated////" run_teardown "$case_dir" "$id") \
1065
+ || fail "relocated scout teardown failed: $out"
1066
+ [ "$(tasks-axi show "$id" --file "$backlog" 2>/dev/null | sed -n 's/^ state: *//p' | head -1)" = "done" ] \
1067
+ || fail "relocated scout backlog row was not closed"
1068
+ assert_grep "data/$id/report.md" "$backlog" \
1069
+ "relocated scout close did not record a relative report path"
1070
+ pass "completion records relocated scout reports relative to the backlog root"
1071
+ }
1072
+
1073
+ test_space_containing_scout_report_marker_replays() {
1074
+ local case_dir id data backlog marker out rc=0
1075
+ id=atomic-space-report-replay-b7
1076
+ case_dir=$(make_home space-report-replay)
1077
+ data="$case_dir/crew space/data"
1078
+ mkdir -p "$case_dir/crew space"
1079
+ mv "$(home_of "$case_dir")/data" "$data"
1080
+ backlog="$data/backlog.md"
1081
+ tasks-axi add "$id" "item for $id" --kind scout --file "$backlog" >/dev/null
1082
+ tasks-axi start "$id" --file "$backlog" >/dev/null
1083
+ write_task_meta "$case_dir" "$id" scout '' "spawn_gen=spawn-space-report"
1084
+ mkdir -p "$data/$id"
1085
+ printf 'findings\n' > "$data/$id/report.md"
1086
+ FM_ROOT_OVERRIDE="$ROOT" FM_HOME="$(home_of "$case_dir")" \
1087
+ FM_DATA_OVERRIDE="$data" PATH="$case_dir/fakebin:$PATH" \
1088
+ "$ROOT/bin/fm-captain-hold.sh" complete "$id" --none >/dev/null \
1089
+ || fail "could not record the space-path scout's captain-call inventory"
1090
+ break_verb "$case_dir" "done"
1091
+ marker="$(home_of "$case_dir")/state/$id.backlog-close"
1092
+
1093
+ out=$(FM_DATA_OVERRIDE="$data" run_teardown "$case_dir" "$id") || rc=$?
1094
+ [ "$rc" -ne 0 ] || fail "space-path scout teardown unexpectedly completed"
1095
+ assert_present "$marker" "space-path scout teardown recorded no pending close"
1096
+ assert_absent "$(home_of "$case_dir")/state/$id.meta" \
1097
+ "space-path scout teardown retained meta after recording its close"
1098
+ rm -f "$case_dir/fakebin/tasks-axi"
1099
+
1100
+ out=$(FM_DATA_OVERRIDE="$data" run_bootstrap "$case_dir")
1101
+ [ "$(tasks-axi show "$id" --file "$backlog" 2>/dev/null | sed -n 's/^ state: *//p' | head -1)" = "done" ] \
1102
+ || fail "space-containing report marker did not replay: $out"
1103
+ assert_grep "data/$id/report.md" "$backlog" \
1104
+ "report path from a space-containing data directory was lost during replay"
1105
+ assert_absent "$marker" "space-containing report marker remained after replay"
1106
+ pass "space-containing scout report paths round-trip through recovery"
1107
+ }
1108
+
1109
+ test_trailing_newline_data_path_fails_closed() {
1110
+ local case_dir home id data backlog_alias out rc=0
1111
+ id=atomic-newline-data-refusal-c8
1112
+ case_dir=$(make_home newline-data-refusal "$id")
1113
+ home=$(home_of "$case_dir")
1114
+ data="$home/data"$'\n'
1115
+ mv "$home/data" "$data"
1116
+ mkdir -p "$home/data/$id"
1117
+ cp "$data/$id/brief.md" "$home/data/$id/brief.md"
1118
+ ln -s "$data" "$case_dir/data-alias"
1119
+ backlog_alias="$case_dir/data-alias/backlog.md"
1120
+ tasks-axi add "$id" "item for $id" --kind ship --file "$backlog_alias" >/dev/null
1121
+
1122
+ out=$(FM_DATA_OVERRIDE="$data" run_ship_spawn "$case_dir" "$id") || rc=$?
1123
+ [ "$rc" -ne 0 ] || fail "trailing-newline data path bypassed dispatch transition"
1124
+ assert_absent "$home/state/$id.meta" \
1125
+ "trailing-newline dispatch published a task record"
1126
+ [ "$(tasks-axi show "$id" --file "$backlog_alias" 2>/dev/null | sed -n 's/^ state: *//p' | head -1)" = queued ] \
1127
+ || fail "trailing-newline dispatch changed the real backlog row: $out"
1128
+
1129
+ tasks-axi start "$id" --file "$backlog_alias" >/dev/null
1130
+ write_task_meta "$case_dir" "$id" ship local-only "spawn_gen=spawn-newline-data"
1131
+ rc=0
1132
+ out=$(FM_DATA_OVERRIDE="$data" run_teardown "$case_dir" "$id") || rc=$?
1133
+ [ "$rc" -ne 0 ] || fail "trailing-newline data path bypassed completion transition"
1134
+ assert_present "$home/state/$id.meta" \
1135
+ "trailing-newline teardown removed the task record"
1136
+ assert_absent "$home/state/$id.backlog-close" \
1137
+ "trailing-newline teardown published a close marker"
1138
+ [ "$(tasks-axi show "$id" --file "$backlog_alias" 2>/dev/null | sed -n 's/^ state: *//p' | head -1)" = in_flight ] \
1139
+ || fail "trailing-newline teardown changed the real backlog row: $out"
1140
+ pass "control-byte data paths fail closed before paired transitions"
1141
+ }
1142
+
1143
+ test_control_character_data_path_is_refused_before_cleanup() {
1144
+ local case_dir id data backlog marker out rc=0
1145
+ id=atomic-control-data-refusal-b7
1146
+ case_dir=$(make_home control-data-refusal "$id")
1147
+ data="$case_dir/crew"$'\t'"data"
1148
+ mv "$(home_of "$case_dir")/data" "$data"
1149
+ backlog="$data/backlog.md"
1150
+ tasks-axi add "$id" "item for $id" --kind ship --file "$backlog" >/dev/null
1151
+
1152
+ out=$(FM_DATA_OVERRIDE="$data" run_ship_spawn "$case_dir" "$id") || rc=$?
1153
+ [ "$rc" -ne 0 ] || fail "control-character data path passed dispatch preflight"
1154
+ assert_absent "$(home_of "$case_dir")/state/$id.meta" \
1155
+ "control-character dispatch published a task record"
1156
+ [ "$(tasks-axi show "$id" --file "$backlog" 2>/dev/null | sed -n 's/^ state: *//p' | head -1)" = queued ] \
1157
+ || fail "control-character dispatch changed the backlog row: $out"
1158
+
1159
+ tasks-axi start "$id" --file "$backlog" >/dev/null
1160
+ write_task_meta "$case_dir" "$id" ship local-only "spawn_gen=spawn-control-data"
1161
+ marker="$(home_of "$case_dir")/state/$id.backlog-close"
1162
+ rc=0
1163
+ out=$(FM_DATA_OVERRIDE="$data" run_teardown "$case_dir" "$id") || rc=$?
1164
+ [ "$rc" -ne 0 ] || fail "control-character data path passed close preflight"
1165
+ assert_present "$(home_of "$case_dir")/state/$id.meta" \
1166
+ "control-character close preflight removed the task record"
1167
+ assert_absent "$marker" "control-character close preflight published a marker"
1168
+ [ "$(tasks-axi show "$id" --file "$backlog" 2>/dev/null | sed -n 's/^ state: *//p' | head -1)" = in_flight ] \
1169
+ || fail "control-character close preflight changed the backlog row: $out"
1170
+ pass "unreplayable data paths are refused before destructive cleanup"
1171
+ }
1172
+
1173
+ test_completion_preserves_records_when_meta_removal_fails() {
1174
+ local case_dir id meta marker out rc=0
1175
+ id=atomic-close-meta-remove-failure-b7
1176
+ case_dir=$(make_home close-meta-remove-failure)
1177
+ add_item "$case_dir" "$id"
1178
+ start_item "$case_dir" "$id"
1179
+ write_task_meta "$case_dir" "$id" ship local-only "spawn_gen=spawn-one"
1180
+ meta="$(home_of "$case_dir")/state/$id.meta"
1181
+ marker="$(home_of "$case_dir")/state/$id.backlog-close"
1182
+ break_meta_removal "$case_dir" "$meta"
1183
+
1184
+ out=$(run_teardown "$case_dir" "$id") || rc=$?
1185
+ [ "$rc" -ne 0 ] || fail "teardown succeeded though task-record removal failed"
1186
+ assert_contains "$out" "task record could not be removed" \
1187
+ "teardown did not report task-record removal failure"
1188
+ assert_present "$meta" "teardown lost meta after its removal failed"
1189
+ assert_present "$marker" "teardown discarded recovery after meta removal failed"
1190
+ [ "$(row_state "$case_dir" "$id")" = in_flight ] \
1191
+ || fail "teardown closed the row before verifying meta removal"
1192
+ pass "completion preserves recovery state when task-record removal fails"
1193
+ }
1194
+
1195
+ test_completion_fails_loudly_and_records_the_close_it_still_owes() {
1196
+ local case_dir id out rc=0
1197
+ id=atomic-close-b7
1198
+ case_dir=$(make_home close-fails)
1199
+ add_item "$case_dir" "$id"
1200
+ start_item "$case_dir" "$id"
1201
+ write_task_meta "$case_dir" "$id" ship local-only "spawn_gen=spawn-close-fails"
1202
+ break_verb "$case_dir" "done"
1203
+
1204
+ out=$(run_teardown "$case_dir" "$id") || rc=$?
1205
+ [ "$rc" -ne 0 ] || fail "teardown reported success while its item was still In flight"
1206
+ assert_contains "$out" "could not be closed" \
1207
+ "teardown failed without explaining the unclosed backlog item"
1208
+ assert_present "$(home_of "$case_dir")/state/$id.backlog-close" \
1209
+ "teardown lost the close it still owes"
1210
+ pass "completion refuses to report success while its item is still open, and records what it owes"
1211
+ }
1212
+
1213
+ test_interrupted_destructive_cleanup_leaves_a_recoverable_close() {
1214
+ local case_dir home id marker out rc=0
1215
+ id=atomic-close-destructive-interrupt-b8
1216
+ case_dir=$(make_home close-destructive-interrupt "$id")
1217
+ home=$(home_of "$case_dir")
1218
+ add_item "$case_dir" "$id"
1219
+ out=$(run_ship_spawn "$case_dir" "$id") || fail "spawn failed: $out"
1220
+ marker="$home/state/$id.backlog-close"
1221
+ interrupt_teardown_during_treehouse_return "$case_dir"
1222
+
1223
+ out=$(run_teardown "$case_dir" "$id") || rc=$?
1224
+ [ "$rc" -ne 0 ] || fail "interrupted destructive cleanup reported success"
1225
+ assert_present "$marker" \
1226
+ "destructive cleanup began before recording its authoritative close"
1227
+ assert_present "$home/state/$id.meta" \
1228
+ "interrupted destructive cleanup lost the task incarnation"
1229
+ [ "$(row_state "$case_dir" "$id")" = in_flight ] \
1230
+ || fail "interrupted cleanup changed the backlog before recovery"
1231
+
1232
+ out=$(run_bootstrap "$case_dir")
1233
+ [ "$(row_state "$case_dir" "$id")" = "done" ] \
1234
+ || fail "restart left interrupted cleanup In flight: $out"
1235
+ assert_absent "$marker" "restart retained the recovered close marker"
1236
+ assert_absent "$home/state/$id.meta" "restart retained the interrupted task record"
1237
+ assert_contains "$out" "endpoint or local copy may remain" \
1238
+ "restart silently hid potentially incomplete physical cleanup"
1239
+ pass "restart recovers closes recorded before destructive cleanup"
1240
+ }
1241
+
1242
+ test_completion_refuses_a_close_target_symlinked_to_a_directory() {
1243
+ local case_dir home id marker external out rc=0
1244
+ id=atomic-close-target-directory-symlink-b8
1245
+ case_dir=$(make_home close-target-directory-symlink)
1246
+ home=$(home_of "$case_dir")
1247
+ add_item "$case_dir" "$id"
1248
+ start_item "$case_dir" "$id"
1249
+ write_task_meta "$case_dir" "$id" ship local-only "spawn_gen=spawn-target-symlink"
1250
+ marker="$home/state/$id.backlog-close"
1251
+ external="$case_dir/external-directory"
1252
+ mkdir -p "$external"
1253
+ ln -s "$external" "$marker"
1254
+
1255
+ out=$(run_teardown "$case_dir" "$id") || rc=$?
1256
+ [ "$rc" -ne 0 ] || fail "teardown published through a directory symlink"
1257
+ assert_contains "$out" "pending-close record target resolves outside its authorized directory" \
1258
+ "teardown did not report the unsafe publication target"
1259
+ [ -L "$marker" ] || fail "teardown replaced the unsafe close target"
1260
+ [ -z "$(find "$external" -mindepth 1 -maxdepth 1 -print -quit)" ] \
1261
+ || fail "teardown wrote a staged close outside the home"
1262
+ assert_present "$home/state/$id.meta" \
1263
+ "unsafe close publication removed the task record"
1264
+ [ "$(row_state "$case_dir" "$id")" = in_flight ] \
1265
+ || fail "unsafe close publication changed the backlog row"
1266
+ pass "completion refuses directory-symlink close targets"
1267
+ }
1268
+
1269
+ test_completion_fails_when_its_close_marker_cannot_be_removed() {
1270
+ local case_dir id marker out rc=0
1271
+ id=atomic-close-marker-remove-failure-b8
1272
+ case_dir=$(make_home close-marker-remove-failure)
1273
+ add_item "$case_dir" "$id"
1274
+ start_item "$case_dir" "$id"
1275
+ write_task_meta "$case_dir" "$id" ship local-only "spawn_gen=spawn-marker-fails"
1276
+ marker="$(home_of "$case_dir")/state/$id.backlog-close"
1277
+ break_meta_removal "$case_dir" "$marker"
1278
+
1279
+ out=$(run_teardown "$case_dir" "$id") || rc=$?
1280
+ [ "$rc" -ne 0 ] || fail "teardown reported success while its close marker remained"
1281
+ assert_contains "$out" "pending-close record could not be removed" \
1282
+ "teardown did not report its incomplete marker cleanup"
1283
+ assert_present "$marker" "teardown hid a close-marker removal failure"
1284
+ [ "$(row_state "$case_dir" "$id")" = "done" ] \
1285
+ || fail "marker cleanup failure lost the completed backlog transition"
1286
+ pass "completion reports failure until its durable close marker is removed"
1287
+ }
1288
+
1289
+ # --- same-home recovery -----------------------------------------------------
1290
+
1291
+ test_recovery_retries_when_a_close_marker_cannot_be_removed() {
1292
+ local case_dir id marker out
1293
+ id=atomic-heal-marker-remove-failure-b8
1294
+ case_dir=$(make_home heal-marker-remove-failure)
1295
+ add_item "$case_dir" "$id"
1296
+ start_item "$case_dir" "$id"
1297
+ marker="$(home_of "$case_dir")/state/$id.backlog-close"
1298
+ printf 'id=%s\ndata=%s\nspawn_gen=spawn-marker-retry\narg=--note\narg=local%%20main\n' \
1299
+ "$id" "$(home_of "$case_dir")/data" > "$marker"
1300
+ break_meta_removal "$case_dir" "$marker"
1301
+
1302
+ out=$(run_bootstrap "$case_dir")
1303
+ assert_contains "$out" "pending-close record could not be removed" \
1304
+ "session start did not report close-marker removal failure"
1305
+ assert_present "$marker" "recovery hid a close-marker removal failure"
1306
+ [ "$(row_state "$case_dir" "$id")" = "done" ] \
1307
+ || fail "recovery did not land the close before marker cleanup"
1308
+
1309
+ rm -f "$case_dir/fakebin/rm"
1310
+ out=$(run_bootstrap "$case_dir")
1311
+ assert_absent "$marker" "recovery did not retry close-marker cleanup: $out"
1312
+ pass "session start retries a close whose marker could not be removed"
1313
+ }
1314
+
1315
+ test_recovery_reports_an_owned_row_read_failure() {
1316
+ local case_dir id out
1317
+ id=atomic-heal-read-failure-b8
1318
+ case_dir=$(make_home heal-owned-read-failure)
1319
+ add_item "$case_dir" "$id"
1320
+ write_task_meta "$case_dir" "$id" ship no-mistakes
1321
+ break_verb "$case_dir" show
1322
+
1323
+ out=$(run_bootstrap "$case_dir")
1324
+ assert_contains "$out" "worker record exists but its backlog item could not be read" \
1325
+ "session start silently ignored an owned-row read failure"
1326
+ assert_contains "$out" "backlog is unwritable" \
1327
+ "session start discarded the backlog reader's diagnostic"
1328
+ rm -f "$case_dir/fakebin/tasks-axi"
1329
+ [ "$(row_state "$case_dir" "$id")" = queued ] \
1330
+ || fail "owned-row read failure changed the backlog state"
1331
+ assert_present "$(home_of "$case_dir")/state/$id.meta" \
1332
+ "owned-row read failure removed the worker record"
1333
+ pass "session start reports owned backlog rows it cannot read"
1334
+ }
1335
+
1336
+ test_orca_cleanup_recovery_never_transitions_the_backlog() {
1337
+ local case_dir id meta out
1338
+ id=atomic-orca-cleanup-recovery-b8
1339
+ case_dir=$(make_home orca-cleanup-recovery)
1340
+ add_item "$case_dir" "$id"
1341
+ write_task_meta "$case_dir" "$id" ship local-only "cleanup_recovery=orca"
1342
+ meta="$(home_of "$case_dir")/state/$id.meta"
1343
+
1344
+ out=$(run_bootstrap "$case_dir")
1345
+ [ "$(row_state "$case_dir" "$id")" = queued ] \
1346
+ || fail "session start treated cleanup recovery as a launched worker: $out"
1347
+ assert_present "$meta" "session start removed the cleanup recovery record"
1348
+
1349
+ out=$(run_teardown "$case_dir" "$id") \
1350
+ || fail "cleanup recovery teardown failed: $out"
1351
+ [ "$(row_state "$case_dir" "$id")" = queued ] \
1352
+ || fail "cleanup recovery teardown completed work that never launched"
1353
+ assert_absent "$meta" "cleanup recovery teardown retained its task record"
1354
+ pass "Orca cleanup recovery is excluded from backlog lifecycle transitions"
1355
+ }
1356
+
1357
+ test_recovery_marks_an_owned_record_in_flight() {
1358
+ local case_dir id out
1359
+ id=atomic-heal-b8
1360
+ case_dir=$(make_home heal-queued)
1361
+ add_item "$case_dir" "$id"
1362
+ write_task_meta "$case_dir" "$id" ship no-mistakes
1363
+
1364
+ out=$(run_bootstrap "$case_dir")
1365
+ [ "$(row_state "$case_dir" "$id")" = in_flight ] \
1366
+ || fail "session start left an owned record's item at $(row_state "$case_dir" "$id"): $out"
1367
+ pass "session start marks an item In flight when this home already owns a worker for it"
1368
+ }
1369
+
1370
+ test_recovery_rejects_an_internal_worker_record_symlink() {
1371
+ local case_dir home id target_id out rc=0
1372
+ id=atomic-heal-internal-symlink-b8
1373
+ target_id=atomic-heal-internal-target-b8
1374
+ case_dir=$(make_home heal-internal-symlink)
1375
+ home=$(home_of "$case_dir")
1376
+ add_item "$case_dir" "$id"
1377
+ write_task_meta "$case_dir" "$target_id" ship no-mistakes "spawn_gen=internal-target"
1378
+ ln -s "$target_id.meta" "$home/state/$id.meta"
1379
+
1380
+ out=$(run_bootstrap "$case_dir") || rc=$?
1381
+ [ "$rc" -ne 0 ] || fail "session start accepted an internal worker-record symlink"
1382
+ assert_contains "$out" "task record resolves through a different final path" \
1383
+ "session start did not report the aliased worker record"
1384
+ [ "$(row_state "$case_dir" "$id")" = queued ] \
1385
+ || fail "session start paired the aliased worker record with its backlog row"
1386
+ [ -L "$home/state/$id.meta" ] \
1387
+ || fail "session start replaced or removed the aliased worker record"
1388
+ assert_present "$home/state/$target_id.meta" \
1389
+ "session start removed the internal symlink target"
1390
+ pass "session start rejects internal worker-record symlinks"
1391
+ }
1392
+
1393
+ test_recovery_ignores_a_symlinked_worker_record() {
1394
+ local case_dir home id target out rc=0
1395
+ id=atomic-heal-symlink-meta-b8
1396
+ case_dir=$(make_home heal-symlink-meta)
1397
+ home=$(home_of "$case_dir")
1398
+ add_item "$case_dir" "$id"
1399
+ target="$case_dir/symlink-meta-target"
1400
+ printf 'kind=ship\nspawn_gen=unpublished\n' > "$target"
1401
+ ln -s "$target" "$home/state/$id.meta"
1402
+
1403
+ out=$(run_bootstrap "$case_dir") || rc=$?
1404
+ [ "$rc" -ne 0 ] || fail "session start accepted a symlinked worker record"
1405
+ assert_contains "$out" "bootstrap refused unsafe worker record" \
1406
+ "session start did not report the unsafe worker record"
1407
+ [ "$(row_state "$case_dir" "$id")" = queued ] \
1408
+ || fail "session start treated a symlink as an owned worker record: $out"
1409
+ [ -L "$home/state/$id.meta" ] \
1410
+ || fail "session start replaced or removed the inert symlinked record"
1411
+ pass "session start rejects symlinked worker records"
1412
+ }
1413
+
1414
+ test_recovery_replays_a_close_an_interrupted_cleanup_left_open() {
1415
+ local case_dir id out
1416
+ id=atomic-heal-b9
1417
+ case_dir=$(make_home heal-pending-close)
1418
+ add_item "$case_dir" "$id"
1419
+ start_item "$case_dir" "$id"
1420
+ printf 'id=%s\ndata=%s\nspawn_gen=spawn-heal-pr\narg=--pr\narg=https://github.com/example/repo/pull/11\n' \
1421
+ "$id" "$(home_of "$case_dir")/data" \
1422
+ > "$(home_of "$case_dir")/state/$id.backlog-close"
1423
+
1424
+ out=$(run_bootstrap "$case_dir")
1425
+ [ "$(row_state "$case_dir" "$id")" = "done" ] \
1426
+ || fail "session start left an interrupted cleanup's item at $(row_state "$case_dir" "$id"): $out"
1427
+ assert_grep 'https://github.com/example/repo/pull/11' "$(backlog_of "$case_dir")" \
1428
+ "the replayed close dropped the completion link the cleanup had recorded"
1429
+ assert_absent "$(home_of "$case_dir")/state/$id.backlog-close" \
1430
+ "a replayed close left its record behind"
1431
+ assert_not_contains "$out" "endpoint or local copy may remain" \
1432
+ "recovery claimed incomplete cleanup without task metadata"
1433
+ pass "session start finishes a close an interrupted cleanup recorded but never landed"
1434
+ }
1435
+
1436
+ test_recovery_backfills_a_recorded_link_on_an_already_done_item() {
1437
+ local case_dir id marker out
1438
+ id=atomic-heal-done-backfill-b9
1439
+ case_dir=$(make_home heal-done-backfill)
1440
+ add_item "$case_dir" "$id"
1441
+ start_item "$case_dir" "$id"
1442
+ tasks-axi "done" "$id" --file "$(backlog_of "$case_dir")" >/dev/null
1443
+ marker="$(home_of "$case_dir")/state/$id.backlog-close"
1444
+ printf 'id=%s\ndata=%s\nspawn_gen=spawn-heal-done\narg=--pr\narg=https://github.com/example/repo/pull/13\n' \
1445
+ "$id" "$(home_of "$case_dir")/data" > "$marker"
1446
+
1447
+ out=$(run_bootstrap "$case_dir")
1448
+ [ "$(row_state "$case_dir" "$id")" = "done" ] \
1449
+ || fail "replaying a completion link changed the closed row: $out"
1450
+ assert_grep 'https://github.com/example/repo/pull/13' "$(backlog_of "$case_dir")" \
1451
+ "recovery discarded the recorded link because the item was already Done"
1452
+ assert_absent "$marker" "recovery retained an applied completion-link marker"
1453
+ pass "recovery backfills recorded links onto already Done items"
1454
+ }
1455
+
1456
+ test_recovery_preserves_a_close_when_the_backlog_cannot_be_read() {
1457
+ local case_dir id out
1458
+ id=atomic-heal-read-error-b10
1459
+ case_dir=$(make_home heal-read-error)
1460
+ add_item "$case_dir" "$id"
1461
+ start_item "$case_dir" "$id"
1462
+ printf 'id=%s\ndata=%s\nspawn_gen=spawn-heal-read\narg=--note\narg=local%%20main\n' \
1463
+ "$id" "$(home_of "$case_dir")/data" \
1464
+ > "$(home_of "$case_dir")/state/$id.backlog-close"
1465
+ break_verb "$case_dir" show
1466
+
1467
+ out=$(run_bootstrap "$case_dir")
1468
+ assert_present "$(home_of "$case_dir")/state/$id.backlog-close" \
1469
+ "a transient backlog read failure discarded the pending close"
1470
+ rm -f "$case_dir/fakebin/tasks-axi"
1471
+ [ "$(row_state "$case_dir" "$id")" = in_flight ] \
1472
+ || fail "a failed recovery changed the backlog row: $out"
1473
+
1474
+ out=$(run_bootstrap "$case_dir")
1475
+ [ "$(row_state "$case_dir" "$id")" = "done" ] \
1476
+ || fail "the preserved close was not retried after the read recovered: $out"
1477
+ assert_absent "$(home_of "$case_dir")/state/$id.backlog-close" \
1478
+ "a successfully retried close left its marker behind"
1479
+ pass "session start preserves a pending close across a transient backlog read failure"
1480
+ }
1481
+
1482
+ test_recovery_retry_preserves_incomplete_cleanup_warning() {
1483
+ local case_dir home id marker out
1484
+ id=atomic-heal-retry-warning-b10
1485
+ case_dir=$(make_home heal-retry-warning)
1486
+ home=$(home_of "$case_dir")
1487
+ add_item "$case_dir" "$id"
1488
+ start_item "$case_dir" "$id"
1489
+ write_task_meta "$case_dir" "$id" ship no-mistakes "spawn_gen=spawn-warning"
1490
+ marker="$home/state/$id.backlog-close"
1491
+ printf 'id=%s\ndata=%s\nspawn_gen=spawn-warning\narg=--note\narg=local%%20main\n' \
1492
+ "$id" "$home/data" > "$marker"
1493
+ break_verb "$case_dir" show
1494
+
1495
+ out=$(run_bootstrap "$case_dir")
1496
+ assert_absent "$home/state/$id.meta" \
1497
+ "failed replay did not cross the task-record removal boundary"
1498
+ assert_present "$marker" "failed replay discarded its pending close"
1499
+ rm -f "$case_dir/fakebin/tasks-axi"
1500
+
1501
+ out=$(run_bootstrap "$case_dir")
1502
+ [ "$(row_state "$case_dir" "$id")" = "done" ] \
1503
+ || fail "retried recovery left the item In flight: $out"
1504
+ assert_contains "$out" "endpoint or local copy may remain" \
1505
+ "retry lost the incomplete-cleanup evidence after removing metadata"
1506
+ assert_absent "$marker" "retried recovery retained its applied marker"
1507
+ pass "recovery preserves incomplete-cleanup evidence across a failed replay"
1508
+ }
1509
+
1510
+ test_recovery_finishes_a_close_for_the_same_meta_incarnation() {
1511
+ local case_dir id out
1512
+ id=atomic-heal-same-incarnation-b11
1513
+ case_dir=$(make_home heal-same-incarnation)
1514
+ add_item "$case_dir" "$id"
1515
+ start_item "$case_dir" "$id"
1516
+ write_task_meta "$case_dir" "$id" ship no-mistakes "spawn_gen=spawn-one"
1517
+ printf 'id=%s\ndata=%s\nspawn_gen=spawn-one\narg=--note\narg=local%%20main\n' \
1518
+ "$id" "$(home_of "$case_dir")/data" \
1519
+ > "$(home_of "$case_dir")/state/$id.backlog-close"
1520
+
1521
+ out=$(run_bootstrap "$case_dir")
1522
+ [ "$(row_state "$case_dir" "$id")" = "done" ] \
1523
+ || fail "session start did not close the interrupted incarnation: $out"
1524
+ assert_absent "$(home_of "$case_dir")/state/$id.meta" \
1525
+ "session start retained the interrupted incarnation's meta"
1526
+ assert_absent "$(home_of "$case_dir")/state/$id.backlog-close" \
1527
+ "session start retained the completed incarnation's close marker"
1528
+ pass "session start finishes a close for the matching meta incarnation"
1529
+ }
1530
+
1531
+ test_recovery_preserves_a_close_for_ambiguous_incarnation_metadata() {
1532
+ local case_dir home id marker out
1533
+ id=atomic-heal-ambiguous-incarnation-b12
1534
+ case_dir=$(make_home heal-ambiguous-incarnation)
1535
+ home=$(home_of "$case_dir")
1536
+ add_item "$case_dir" "$id"
1537
+ start_item "$case_dir" "$id"
1538
+ write_task_meta "$case_dir" "$id" ship no-mistakes \
1539
+ "spawn_gen=spawn-old" "spawn_gen=spawn-current"
1540
+ marker="$home/state/$id.backlog-close"
1541
+ printf 'id=%s\ndata=%s\nspawn_gen=spawn-current\narg=--note\narg=local%%20main\n' \
1542
+ "$id" "$home/data" > "$marker"
1543
+
1544
+ out=$(run_bootstrap "$case_dir")
1545
+ assert_contains "$out" "has 2 spawn generation fields" \
1546
+ "recovery did not report ambiguous incarnation metadata"
1547
+ assert_present "$marker" "ambiguous metadata caused recovery to discard the close"
1548
+ assert_present "$home/state/$id.meta" \
1549
+ "ambiguous metadata caused recovery to remove the task record"
1550
+ [ "$(row_state "$case_dir" "$id")" = in_flight ] \
1551
+ || fail "ambiguous metadata allowed recovery to close the backlog row"
1552
+ pass "recovery preserves closes for ambiguous task incarnations"
1553
+ }
1554
+
1555
+ test_recovery_preserves_both_records_when_meta_removal_fails() {
1556
+ local case_dir id meta out
1557
+ id=atomic-heal-remove-failure-b12
1558
+ case_dir=$(make_home heal-remove-failure)
1559
+ add_item "$case_dir" "$id"
1560
+ start_item "$case_dir" "$id"
1561
+ meta="$(home_of "$case_dir")/state/$id.meta"
1562
+ write_task_meta "$case_dir" "$id" ship no-mistakes "spawn_gen=spawn-one"
1563
+ printf 'id=%s\ndata=%s\nspawn_gen=spawn-one\narg=--note\narg=local%%20main\n' \
1564
+ "$id" "$(home_of "$case_dir")/data" \
1565
+ > "$(home_of "$case_dir")/state/$id.backlog-close"
1566
+ break_meta_removal "$case_dir" "$meta"
1567
+
1568
+ out=$(run_bootstrap "$case_dir")
1569
+ assert_contains "$out" "the interrupted task record could not be removed" \
1570
+ "session start did not surface the record-removal failure"
1571
+ assert_present "$meta" "failed recovery removed the task record"
1572
+ assert_present "$(home_of "$case_dir")/state/$id.backlog-close" \
1573
+ "failed recovery discarded the pending close"
1574
+ [ "$(row_state "$case_dir" "$id")" = in_flight ] \
1575
+ || fail "failed recovery closed the backlog before removing meta"
1576
+
1577
+ rm -f "$case_dir/fakebin/rm"
1578
+ out=$(run_bootstrap "$case_dir")
1579
+ [ "$(row_state "$case_dir" "$id")" = "done" ] \
1580
+ || fail "recovery did not retry after meta removal recovered: $out"
1581
+ assert_absent "$meta" "successful retry retained the task record"
1582
+ assert_absent "$(home_of "$case_dir")/state/$id.backlog-close" \
1583
+ "successful retry retained the pending close"
1584
+ pass "recovery preserves both records when meta removal fails"
1585
+ }
1586
+
1587
+ test_recovery_preserves_a_close_beside_symlinked_metadata() {
1588
+ local case_dir home id marker target out
1589
+ id=atomic-heal-symlink-meta-close-b12
1590
+ case_dir=$(make_home heal-symlink-meta-close)
1591
+ home=$(home_of "$case_dir")
1592
+ add_item "$case_dir" "$id"
1593
+ start_item "$case_dir" "$id"
1594
+ target="$case_dir/foreign-meta-target"
1595
+ printf 'kind=ship\nspawn_gen=other-incarnation\n' > "$target"
1596
+ ln -s "$target" "$home/state/$id.meta"
1597
+ marker="$home/state/$id.backlog-close"
1598
+ printf 'id=%s\ndata=%s\nspawn_gen=closing-incarnation\narg=--note\narg=local%%20main\n' \
1599
+ "$id" "$home/data" > "$marker"
1600
+
1601
+ out=$(run_bootstrap "$case_dir")
1602
+ assert_contains "$out" "unsafe interrupted task record" \
1603
+ "recovery did not report unsafe metadata beside the close"
1604
+ assert_present "$marker" "unsafe metadata caused recovery to discard the close"
1605
+ [ -L "$home/state/$id.meta" ] \
1606
+ || fail "recovery replaced or removed the unsafe metadata path"
1607
+ [ "$(row_state "$case_dir" "$id")" = in_flight ] \
1608
+ || fail "unsafe metadata allowed recovery to close the backlog row"
1609
+ pass "recovery preserves closes beside symlinked metadata"
1610
+ }
1611
+
1612
+ test_recovery_rejects_a_marker_for_another_task_identity() {
1613
+ local case_dir locked_id target_id marker out
1614
+ locked_id=atomic-marker-lock-owner-b12
1615
+ target_id=atomic-marker-target-b12
1616
+ case_dir=$(make_home marker-identity-mismatch)
1617
+ add_item "$case_dir" "$target_id"
1618
+ start_item "$case_dir" "$target_id"
1619
+ write_task_meta "$case_dir" "$target_id" ship no-mistakes "spawn_gen=spawn-marker-target"
1620
+ marker="$(home_of "$case_dir")/state/$locked_id.backlog-close"
1621
+ printf 'id=%s\ndata=%s\nspawn_gen=spawn-marker-target\narg=--note\narg=local%%20main\n' \
1622
+ "$target_id" "$(home_of "$case_dir")/data" > "$marker"
1623
+
1624
+ out=$(run_bootstrap "$case_dir")
1625
+ assert_present "$marker" "identity-mismatched close marker was consumed"
1626
+ assert_present "$(home_of "$case_dir")/state/$target_id.meta" \
1627
+ "identity-mismatched close marker removed another task record"
1628
+ [ "$(row_state "$case_dir" "$target_id")" = in_flight ] \
1629
+ || fail "identity-mismatched close marker changed another task's row: $out"
1630
+ pass "recovery binds close-marker identity to its locked filename"
1631
+ }
1632
+
1633
+ test_recovery_rejects_a_foreign_data_directory() {
1634
+ local case_dir foreign_case id marker out
1635
+ id=atomic-marker-foreign-data-b12
1636
+ case_dir=$(make_home marker-foreign-data-local)
1637
+ foreign_case=$(make_home marker-foreign-data-remote)
1638
+ add_item "$case_dir" "$id"
1639
+ start_item "$case_dir" "$id"
1640
+ add_item "$foreign_case" "$id"
1641
+ start_item "$foreign_case" "$id"
1642
+ write_task_meta "$case_dir" "$id" ship no-mistakes "spawn_gen=spawn-foreign-data"
1643
+ marker="$(home_of "$case_dir")/state/$id.backlog-close"
1644
+ printf 'id=%s\ndata=%s\nspawn_gen=spawn-foreign-data\narg=--note\narg=local%%20main\n' \
1645
+ "$id" "$(home_of "$foreign_case")/data" > "$marker"
1646
+
1647
+ out=$(run_bootstrap "$case_dir")
1648
+ assert_present "$marker" "foreign-data close marker was consumed"
1649
+ assert_present "$(home_of "$case_dir")/state/$id.meta" \
1650
+ "foreign-data close marker removed the local task record"
1651
+ [ "$(row_state "$case_dir" "$id")" = in_flight ] \
1652
+ || fail "foreign-data close marker changed the local backlog row: $out"
1653
+ [ "$(row_state "$foreign_case" "$id")" = in_flight ] \
1654
+ || fail "foreign-data close marker reached into another home's backlog: $out"
1655
+ pass "recovery rejects close markers targeting another home's data"
1656
+ }
1657
+
1658
+ test_recovery_rejects_an_unterminated_unknown_field() {
1659
+ local case_dir id marker out
1660
+ id=atomic-marker-unterminated-field-b12
1661
+ case_dir=$(make_home marker-unterminated-field)
1662
+ add_item "$case_dir" "$id"
1663
+ start_item "$case_dir" "$id"
1664
+ write_task_meta "$case_dir" "$id" ship no-mistakes "spawn_gen=spawn-unterminated-field"
1665
+ marker="$(home_of "$case_dir")/state/$id.backlog-close"
1666
+ printf 'id=%s\ndata=%s\nspawn_gen=spawn-unterminated-field\narg=--note\narg=local%%20main\nunknown=value' \
1667
+ "$id" "$(home_of "$case_dir")/data" > "$marker"
1668
+
1669
+ out=$(run_bootstrap "$case_dir")
1670
+ assert_present "$marker" "marker with an unterminated unknown field was consumed"
1671
+ assert_present "$(home_of "$case_dir")/state/$id.meta" \
1672
+ "unterminated unknown marker field allowed task-record removal"
1673
+ [ "$(row_state "$case_dir" "$id")" = in_flight ] \
1674
+ || fail "unterminated unknown marker field changed the backlog row: $out"
1675
+ pass "recovery validates an unterminated final marker field"
1676
+ }
1677
+
1678
+ test_recovery_rejects_lexical_data_traversal() {
1679
+ local case_dir id marker data out
1680
+ id=atomic-marker-data-traversal-b12
1681
+ case_dir=$(make_home marker-data-traversal)
1682
+ add_item "$case_dir" "$id"
1683
+ start_item "$case_dir" "$id"
1684
+ write_task_meta "$case_dir" "$id" ship no-mistakes "spawn_gen=spawn-data-traversal"
1685
+ data="$(home_of "$case_dir")/data"
1686
+ mkdir -p "$data/sub"
1687
+ marker="$(home_of "$case_dir")/state/$id.backlog-close"
1688
+ printf 'id=%s\ndata=%s/sub/..\nspawn_gen=spawn-data-traversal\narg=--note\narg=local%%20main\n' \
1689
+ "$id" "$data" > "$marker"
1690
+
1691
+ out=$(run_bootstrap "$case_dir")
1692
+ assert_present "$marker" "marker with lexical data traversal was consumed"
1693
+ assert_present "$(home_of "$case_dir")/state/$id.meta" \
1694
+ "lexical data traversal allowed task-record removal"
1695
+ [ "$(row_state "$case_dir" "$id")" = in_flight ] \
1696
+ || fail "lexical data traversal changed the backlog row: $out"
1697
+ pass "recovery rejects lexical traversal before resolving marker data"
1698
+ }
1699
+
1700
+ test_recovery_rejects_raw_control_bytes() {
1701
+ local case_dir id marker data out
1702
+ id=atomic-marker-nul-byte-b12
1703
+ case_dir=$(make_home marker-nul-byte)
1704
+ add_item "$case_dir" "$id"
1705
+ start_item "$case_dir" "$id"
1706
+ write_task_meta "$case_dir" "$id" ship no-mistakes "spawn_gen=spawn-nul-byte"
1707
+ data="$(home_of "$case_dir")/data"
1708
+ marker="$(home_of "$case_dir")/state/$id.backlog-close"
1709
+ printf 'id=%s\ndata=%s\0\nspawn_gen=spawn-nul-byte\narg=--note\narg=local%%20main\n' \
1710
+ "$id" "$data" > "$marker"
1711
+
1712
+ out=$(run_bootstrap "$case_dir")
1713
+ assert_present "$marker" "NUL-bearing close marker was consumed"
1714
+ assert_present "$(home_of "$case_dir")/state/$id.meta" \
1715
+ "NUL-bearing close marker removed the task record"
1716
+ [ "$(row_state "$case_dir" "$id")" = in_flight ] \
1717
+ || fail "NUL-bearing close marker changed the backlog row: $out"
1718
+ pass "recovery rejects marker control bytes before parsing"
1719
+ }
1720
+
1721
+ test_recovery_rejects_malformed_pr_urls() {
1722
+ local case_dir first_id second_id third_id first_marker second_marker third_marker out
1723
+ first_id=atomic-marker-pr-port-b12
1724
+ second_id=atomic-marker-pr-percent-b12
1725
+ third_id=atomic-marker-pr-host-label-b12
1726
+ case_dir=$(make_home marker-malformed-pr)
1727
+ add_item "$case_dir" "$first_id"
1728
+ start_item "$case_dir" "$first_id"
1729
+ add_item "$case_dir" "$second_id"
1730
+ start_item "$case_dir" "$second_id"
1731
+ add_item "$case_dir" "$third_id"
1732
+ start_item "$case_dir" "$third_id"
1733
+ first_marker="$(home_of "$case_dir")/state/$first_id.backlog-close"
1734
+ second_marker="$(home_of "$case_dir")/state/$second_id.backlog-close"
1735
+ third_marker="$(home_of "$case_dir")/state/$third_id.backlog-close"
1736
+ printf 'id=%s\ndata=%s\nspawn_gen=spawn-pr-port\narg=--pr\narg=https://github.com:abc/pull/1\n' \
1737
+ "$first_id" "$(home_of "$case_dir")/data" > "$first_marker"
1738
+ printf 'id=%s\ndata=%s\nspawn_gen=spawn-pr-percent\narg=--pr\narg=https://github.com/pull/%%ZZ\n' \
1739
+ "$second_id" "$(home_of "$case_dir")/data" > "$second_marker"
1740
+ printf 'id=%s\ndata=%s\nspawn_gen=spawn-pr-host-label\narg=--pr\narg=https://foo.-bar.com/pull/1\n' \
1741
+ "$third_id" "$(home_of "$case_dir")/data" > "$third_marker"
1742
+
1743
+ out=$(run_bootstrap "$case_dir")
1744
+ assert_present "$first_marker" "PR marker with a nonnumeric port was consumed"
1745
+ assert_present "$second_marker" "PR marker with an invalid percent escape was consumed"
1746
+ assert_present "$third_marker" "PR marker with a malformed host label was consumed"
1747
+ [ "$(row_state "$case_dir" "$first_id")" = in_flight ] \
1748
+ || fail "nonnumeric PR port changed the backlog row: $out"
1749
+ [ "$(row_state "$case_dir" "$second_id")" = in_flight ] \
1750
+ || fail "invalid PR percent escape changed the backlog row: $out"
1751
+ [ "$(row_state "$case_dir" "$third_id")" = in_flight ] \
1752
+ || fail "malformed PR host label changed the backlog row: $out"
1753
+ pass "recovery rejects malformed PR URL values"
1754
+ }
1755
+
1756
+ test_failed_close_replay_is_not_started_as_live_work() {
1757
+ local case_dir id marker out
1758
+ id=atomic-pending-close-not-started-b12
1759
+ case_dir=$(make_home pending-close-not-started)
1760
+ add_item "$case_dir" "$id"
1761
+ write_task_meta "$case_dir" "$id" ship no-mistakes "spawn_gen=spawn-pending-close"
1762
+ marker="$(home_of "$case_dir")/state/$id.backlog-close"
1763
+ printf 'id=%s\ndata=%s\nspawn_gen=spawn-pending-close\narg=--pr\narg=https://\n' \
1764
+ "$id" "$(home_of "$case_dir")/data" > "$marker"
1765
+
1766
+ out=$(run_bootstrap "$case_dir")
1767
+ assert_present "$marker" "failed close replay discarded its pending marker"
1768
+ assert_present "$(home_of "$case_dir")/state/$id.meta" \
1769
+ "failed close replay removed its task record"
1770
+ [ "$(row_state "$case_dir" "$id")" = queued ] \
1771
+ || fail "retained pending close was started as live work: $out"
1772
+ pass "a retained pending close is never started by reconciliation"
1773
+ }
1774
+
1775
+ test_recovery_rejects_invalid_close_arguments() {
1776
+ local case_dir id marker out
1777
+ id=atomic-marker-invalid-args-b12
1778
+ case_dir=$(make_home marker-invalid-args)
1779
+ add_item "$case_dir" "$id"
1780
+ start_item "$case_dir" "$id"
1781
+ marker="$(home_of "$case_dir")/state/$id.backlog-close"
1782
+ printf 'id=%s\ndata=%s\nspawn_gen=spawn-invalid-args\narg=--unknown\narg=value\n' \
1783
+ "$id" "$(home_of "$case_dir")/data" > "$marker"
1784
+
1785
+ out=$(run_bootstrap "$case_dir")
1786
+ assert_present "$marker" "invalid-argument close marker was consumed"
1787
+ [ "$(row_state "$case_dir" "$id")" = in_flight ] \
1788
+ || fail "invalid close arguments changed the backlog row: $out"
1789
+ pass "recovery rejects close-marker arguments outside its protocol"
1790
+ }
1791
+
1792
+ test_recovery_rejects_a_symlinked_close_marker() {
1793
+ local case_dir id marker payload out rc=0
1794
+ id=atomic-marker-symlink-b12
1795
+ case_dir=$(make_home marker-symlink)
1796
+ add_item "$case_dir" "$id"
1797
+ start_item "$case_dir" "$id"
1798
+ payload="$(home_of "$case_dir")/state/marker-payload"
1799
+ marker="$(home_of "$case_dir")/state/$id.backlog-close"
1800
+ printf 'id=%s\ndata=%s\nspawn_gen=spawn-symlink\narg=--note\narg=local%%20main\n' \
1801
+ "$id" "$(home_of "$case_dir")/data" > "$payload"
1802
+ ln -s "$payload" "$marker"
1803
+ rm -f "$payload"
1804
+
1805
+ out=$(run_bootstrap "$case_dir") || rc=$?
1806
+ [ "$rc" -ne 0 ] || fail "bootstrap accepted a symlinked close marker"
1807
+ [ -L "$marker" ] || fail "dangling symlink close marker was consumed: $out"
1808
+ assert_contains "$out" "bootstrap refused unsafe pending close" \
1809
+ "dangling symlink close marker was silently skipped"
1810
+ [ "$(row_state "$case_dir" "$id")" = in_flight ] \
1811
+ || fail "symlinked close marker changed the backlog row: $out"
1812
+ pass "recovery reports and rejects dangling symlink close markers"
1813
+ }
1814
+
1815
+ test_recovery_drops_a_close_for_a_newer_meta_incarnation() {
1816
+ local case_dir id out
1817
+ id=atomic-heal-new-incarnation-b12
1818
+ case_dir=$(make_home heal-new-incarnation)
1819
+ add_item "$case_dir" "$id"
1820
+ start_item "$case_dir" "$id"
1821
+ write_task_meta "$case_dir" "$id" ship no-mistakes "spawn_gen=spawn-two"
1822
+ printf 'id=%s\ndata=%s\nspawn_gen=spawn-one\narg=--note\narg=local%%20main\n' \
1823
+ "$id" "$(home_of "$case_dir")/data" \
1824
+ > "$(home_of "$case_dir")/state/$id.backlog-close"
1825
+
1826
+ out=$(run_bootstrap "$case_dir")
1827
+ [ "$(row_state "$case_dir" "$id")" = in_flight ] \
1828
+ || fail "session start closed the newer task incarnation: $out"
1829
+ assert_present "$(home_of "$case_dir")/state/$id.meta" \
1830
+ "session start removed the newer task incarnation's meta"
1831
+ assert_absent "$(home_of "$case_dir")/state/$id.backlog-close" \
1832
+ "a stale recorded close was left to fire on a later restart"
1833
+ pass "session start drops a close recorded for an older meta incarnation"
1834
+ }
1835
+
1836
+ test_recovery_rejects_a_legacy_close_without_an_incarnation() {
1837
+ local case_dir id out
1838
+ id=atomic-heal-legacy-close-b13
1839
+ case_dir=$(make_home heal-legacy-close)
1840
+ add_item "$case_dir" "$id"
1841
+ start_item "$case_dir" "$id"
1842
+ write_task_meta "$case_dir" "$id" ship no-mistakes "spawn_gen=spawn-two"
1843
+ printf 'id=%s\ndata=%s\narg=--note\narg=local%%20main\n' \
1844
+ "$id" "$(home_of "$case_dir")/data" \
1845
+ > "$(home_of "$case_dir")/state/$id.backlog-close"
1846
+
1847
+ out=$(run_bootstrap "$case_dir")
1848
+ [ "$(row_state "$case_dir" "$id")" = in_flight ] \
1849
+ || fail "session start guessed that a legacy close belonged to the current meta: $out"
1850
+ assert_present "$(home_of "$case_dir")/state/$id.meta" \
1851
+ "session start removed meta for an unversioned legacy close"
1852
+ assert_present "$(home_of "$case_dir")/state/$id.backlog-close" \
1853
+ "session start consumed an unversioned close marker"
1854
+ pass "session start rejects an unversioned close marker"
1855
+ }
1856
+
1857
+ test_bootstrap_rechecks_worker_record_boundary_after_locking() {
1858
+ local case_dir foreign_case home foreign_state id real_ln out rc=0
1859
+ id=atomic-bootstrap-state-swap-b13
1860
+ case_dir=$(make_home bootstrap-state-swap)
1861
+ foreign_case=$(make_home bootstrap-state-swap-foreign)
1862
+ home=$(home_of "$case_dir")
1863
+ foreign_state="$(home_of "$foreign_case")/state"
1864
+ add_item "$case_dir" "$id"
1865
+ write_task_meta "$case_dir" "$id" ship no-mistakes "spawn_gen=local-worker"
1866
+ write_task_meta "$foreign_case" "$id" ship no-mistakes "spawn_gen=foreign-worker"
1867
+ real_ln=$(command -v ln)
1868
+ cat > "$case_dir/fakebin/ln" <<SH
1869
+ #!/usr/bin/env bash
1870
+ case "\$*" in
1871
+ *"$home/state/.meta-$id.lock"*)
1872
+ if [ ! -e "$case_dir/state-swapped" ]; then
1873
+ : > "$case_dir/state-swapped"
1874
+ mv "$home/state" "$home/state-original" || exit 1
1875
+ "$real_ln" -s "$foreign_state" "$home/state" || exit 1
1876
+ fi
1877
+ ;;
1878
+ esac
1879
+ exec "$real_ln" "\$@"
1880
+ SH
1881
+ chmod +x "$case_dir/fakebin/ln"
1882
+
1883
+ out=$(run_bootstrap "$case_dir") || rc=$?
1884
+ [ "$rc" -ne 0 ] || fail "bootstrap trusted a worker record after its state boundary changed"
1885
+ assert_contains "$out" "post-lock worker record check refused" \
1886
+ "bootstrap did not report the post-lock state-boundary failure"
1887
+ [ "$(row_state "$case_dir" "$id")" = queued ] \
1888
+ || fail "bootstrap changed the local row after reading through a swapped state path"
1889
+ assert_present "$foreign_state/$id.meta" "bootstrap removed the foreign worker record"
1890
+ pass "bootstrap rechecks worker-record containment after locking"
1891
+ }
1892
+
1893
+ test_lifecycle_refuses_ancestor_symlinks_outside_home_roots() {
1894
+ local backlog_case worker_case close_case home foreign id marker out rc=0
1895
+ id=atomic-ancestor-symlink-b14
1896
+
1897
+ backlog_case=$(make_home ancestor-symlink-backlog "$id")
1898
+ home=$(home_of "$backlog_case")
1899
+ foreign="$backlog_case/foreign-home"
1900
+ mkdir -p "$foreign/data"
1901
+ cp "$(backlog_of "$backlog_case")" "$foreign/data/backlog.md"
1902
+ ln -s "$foreign" "$home/foreign-link"
1903
+ out=$(FM_DATA_OVERRIDE="$home/foreign-link/data" run_ship_spawn "$backlog_case" "$id") || rc=$?
1904
+ [ "$rc" -ne 0 ] || fail "dispatch accepted a backlog through an ancestor symlink"
1905
+ assert_absent "$home/state/$id.meta" "dispatch published through a foreign backlog root"
1906
+
1907
+ worker_case=$(make_home ancestor-symlink-worker)
1908
+ home=$(home_of "$worker_case")
1909
+ foreign="$worker_case/foreign-home"
1910
+ mkdir -p "$foreign/state"
1911
+ add_item "$worker_case" "$id"
1912
+ fm_write_meta "$foreign/state/$id.meta" "kind=ship" "spawn_gen=foreign-worker"
1913
+ ln -s "$foreign" "$home/foreign-link"
1914
+ rc=0
1915
+ out=$(FM_STATE_OVERRIDE="$home/foreign-link/state" run_bootstrap "$worker_case") || rc=$?
1916
+ [ "$rc" -ne 0 ] || fail "bootstrap accepted a worker record through an ancestor symlink"
1917
+ [ "$(row_state "$worker_case" "$id")" = queued ] \
1918
+ || fail "bootstrap paired a foreign worker with the local backlog"
1919
+
1920
+ close_case=$(make_home ancestor-symlink-close)
1921
+ home=$(home_of "$close_case")
1922
+ foreign="$close_case/foreign-home"
1923
+ mkdir -p "$foreign/state"
1924
+ add_item "$close_case" "$id"
1925
+ start_item "$close_case" "$id"
1926
+ marker="$foreign/state/$id.backlog-close"
1927
+ printf 'id=%s\ndata=%s\nspawn_gen=foreign-close\narg=--note\narg=local%%20main\n' \
1928
+ "$id" "$home/data" > "$marker"
1929
+ ln -s "$foreign" "$home/foreign-link"
1930
+ rc=0
1931
+ out=$(FM_STATE_OVERRIDE="$home/foreign-link/state" run_bootstrap "$close_case") || rc=$?
1932
+ [ "$rc" -ne 0 ] || fail "bootstrap accepted a close record through an ancestor symlink"
1933
+ assert_present "$marker" "bootstrap discarded a foreign authoritative close"
1934
+ [ "$(row_state "$close_case" "$id")" = in_flight ] \
1935
+ || fail "bootstrap applied a foreign close to the local backlog"
1936
+ pass "lifecycle files reject ancestor symlinks outside home roots"
1937
+ }
1938
+
1939
+ test_same_home_state_override_remains_supported() {
1940
+ local case_dir home state id out
1941
+ id=atomic-same-home-state-override-b14
1942
+ case_dir=$(make_home same-home-state-override)
1943
+ home=$(home_of "$case_dir")
1944
+ state="$home/runtime-state"
1945
+ add_item "$case_dir" "$id"
1946
+ write_task_meta "$case_dir" "$id" ship no-mistakes "spawn_gen=same-home-override"
1947
+ mv "$home/state" "$state"
1948
+
1949
+ out=$(FM_STATE_OVERRIDE="$state" run_bootstrap "$case_dir") \
1950
+ || fail "same-home state override was refused: $out"
1951
+ [ "$(row_state "$case_dir" "$id")" = in_flight ] \
1952
+ || fail "same-home state override did not reconcile its worker"
1953
+ pass "same-home state overrides remain supported"
1954
+ }
1955
+
1956
+ test_bootstrap_refuses_a_symlinked_state_directory_before_reconciliation() {
1957
+ local case_dir foreign_case home foreign_state id out rc=0
1958
+ id=atomic-bootstrap-symlink-state-b11
1959
+ case_dir=$(make_home bootstrap-symlink-state)
1960
+ foreign_case=$(make_home bootstrap-symlink-state-foreign)
1961
+ home=$(home_of "$case_dir")
1962
+ foreign_state="$(home_of "$foreign_case")/state"
1963
+ add_item "$case_dir" "$id"
1964
+ write_task_meta "$foreign_case" "$id" ship no-mistakes "spawn_gen=foreign-worker"
1965
+ rm -rf "$home/state"
1966
+ ln -s "$foreign_state" "$home/state"
1967
+
1968
+ out=$(run_bootstrap "$case_dir") || rc=$?
1969
+ [ "$rc" -ne 0 ] || fail "bootstrap accepted a symlinked state directory"
1970
+ assert_contains "$out" "state directory is not a real directory" \
1971
+ "bootstrap did not report the unsafe state boundary"
1972
+ [ "$(row_state "$case_dir" "$id")" = queued ] \
1973
+ || fail "bootstrap reconciled a foreign record into the local backlog"
1974
+ assert_present "$foreign_state/$id.meta" \
1975
+ "bootstrap removed the foreign worker record"
1976
+ pass "bootstrap refuses symlinked state before reconciliation"
1977
+ }
1978
+
1979
+ test_bootstrap_stops_when_data_disappears_before_reconciliation() {
1980
+ local case_dir id saved out rc=0
1981
+ id=atomic-bootstrap-data-race-b11
1982
+ case_dir=$(make_home bootstrap-data-race)
1983
+ add_item "$case_dir" "$id"
1984
+ start_item "$case_dir" "$id"
1985
+ write_task_meta "$case_dir" "$id" ship no-mistakes "spawn_gen=spawn-bootstrap-race"
1986
+ remove_data_during_startup_budget_check "$case_dir"
1987
+ saved="$case_dir/bootstrap-data"
1988
+
1989
+ out=$(run_bootstrap "$case_dir") || rc=$?
1990
+ [ "$rc" -ne 0 ] || fail "bootstrap absorbed a fatal reconciliation addressing error: $out"
1991
+ assert_present "$(home_of "$case_dir")/state/$id.meta" \
1992
+ "fatal bootstrap reconciliation removed the task record"
1993
+ [ "$(tasks-axi show "$id" --file "$saved/backlog.md" 2>/dev/null | sed -n 's/^ state: *//p' | head -1)" = in_flight ] \
1994
+ || fail "fatal bootstrap reconciliation changed the backlog row"
1995
+ pass "bootstrap stops when backlog data disappears before reconciliation"
1996
+ }
1997
+
1998
+ test_bootstrap_addressing_exemptions_remain_nonfatal() {
1999
+ local manual_case no_backlog_case secondmate_case secondmate_id out
2000
+ manual_case=$(make_home bootstrap-manual-exempt)
2001
+ printf '%s\n' manual > "$(home_of "$manual_case")/config/backlog-backend"
2002
+ mv "$(home_of "$manual_case")/data" "$manual_case/manual-data"
2003
+ out=$(run_bootstrap "$manual_case") \
2004
+ || fail "manual bootstrap exemption became fatal: $out"
2005
+
2006
+ no_backlog_case=$(make_home bootstrap-no-backlog-exempt)
2007
+ rm -f "$(backlog_of "$no_backlog_case")"
2008
+ out=$(run_bootstrap "$no_backlog_case") \
2009
+ || fail "no-backlog bootstrap exemption became fatal: $out"
2010
+
2011
+ secondmate_id=atomic-bootstrap-secondmate-exempt-b11
2012
+ secondmate_case=$(make_home bootstrap-secondmate-exempt)
2013
+ write_task_meta "$secondmate_case" "$secondmate_id" secondmate '' \
2014
+ "spawn_gen=spawn-secondmate-exempt"
2015
+ mv "$(home_of "$secondmate_case")/data" "$secondmate_case/secondmate-data"
2016
+ out=$(run_bootstrap "$secondmate_case") \
2017
+ || fail "secondmate bootstrap exemption became fatal: $out"
2018
+ assert_present "$(home_of "$secondmate_case")/state/$secondmate_id.meta" \
2019
+ "secondmate bootstrap exemption removed the persistent agent record"
2020
+ pass "bootstrap preserves secondmate, manual, and absent-backlog exemptions"
2021
+ }
2022
+
2023
+ test_recovery_leaves_a_captain_held_item_alone() {
2024
+ local case_dir id out
2025
+ id=atomic-heal-b11
2026
+ case_dir=$(make_home heal-held)
2027
+ add_item "$case_dir" "$id"
2028
+ tasks-axi hold "$id" --reason "captain decision pending" --kind captain \
2029
+ --file "$(backlog_of "$case_dir")" >/dev/null
2030
+ write_task_meta "$case_dir" "$id" ship no-mistakes
2031
+
2032
+ out=$(run_bootstrap "$case_dir")
2033
+ [ "$(row_state "$case_dir" "$id")" = queued ] \
2034
+ || fail "session start moved a captain-held item to $(row_state "$case_dir" "$id"): $out"
2035
+ pass "session start leaves a captain-held item where the captain put it"
2036
+ }
2037
+
2038
+ # --- backend selection and secondmate scope ---------------------------------
2039
+
2040
+ test_no_backlog_teardown_refuses_a_symlinked_task_record_at_entry() {
2041
+ local case_dir home id target target_dir foreign_worktree out rc=0
2042
+ id=atomic-no-backlog-symlink-meta-b12
2043
+ case_dir=$(make_home no-backlog-symlink-meta)
2044
+ home=$(home_of "$case_dir")
2045
+ rm -f "$(backlog_of "$case_dir")"
2046
+ foreign_worktree="$case_dir/foreign-worktree"
2047
+ mkdir -p "$foreign_worktree"
2048
+ target_dir="$home/state-foreign"
2049
+ target="$target_dir/$id.meta"
2050
+ mkdir -p "$target_dir"
2051
+ fm_write_meta "$target" \
2052
+ "window=firstmate:fm-$id" "endpoint_task_id=$id" \
2053
+ "worktree=$foreign_worktree" "project=$case_dir/foreign-project" \
2054
+ "harness=claude" "kind=ship" "mode=local-only" "yolo=off"
2055
+ ln -s "$target" "$home/state/$id.meta"
2056
+ track_teardown_resource_actions "$case_dir"
2057
+
2058
+ out=$(run_teardown "$case_dir" "$id") || rc=$?
2059
+ [ "$rc" -ne 0 ] || fail "no-backlog teardown accepted a symlinked task record"
2060
+ assert_contains "$out" "task record resolves outside its authorized directory" \
2061
+ "teardown did not identify the unsafe task record"
2062
+ [ -L "$home/state/$id.meta" ] || fail "teardown removed the symlinked task record"
2063
+ assert_present "$foreign_worktree" "teardown removed a foreign local copy"
2064
+ assert_absent "$case_dir/backend-resource-action" \
2065
+ "teardown acted on the foreign endpoint"
2066
+ assert_absent "$case_dir/local-copy-resource-action" \
2067
+ "teardown acted on the foreign local copy"
2068
+ pass "no-backlog teardown refuses symlinked records before resource actions"
2069
+ }
2070
+
2071
+ test_teardown_rechecks_record_parent_after_lock_acquisition() {
2072
+ local case_dir home id foreign_state foreign_worktree real_ln out rc=0
2073
+ id=atomic-state-parent-swap-b12
2074
+ case_dir=$(make_home state-parent-swap)
2075
+ home=$(home_of "$case_dir")
2076
+ rm -f "$(backlog_of "$case_dir")"
2077
+ write_task_meta "$case_dir" "$id" ship local-only
2078
+ foreign_state="$case_dir/foreign-state"
2079
+ foreign_worktree="$case_dir/foreign-worktree"
2080
+ mkdir -p "$foreign_state" "$foreign_worktree"
2081
+ fm_write_meta "$foreign_state/$id.meta" \
2082
+ "window=firstmate:fm-$id" "endpoint_task_id=$id" \
2083
+ "worktree=$foreign_worktree" "project=$case_dir/foreign-project" \
2084
+ "harness=claude" "kind=ship" "mode=local-only" "yolo=off"
2085
+ track_teardown_resource_actions "$case_dir"
2086
+ real_ln=$(command -v ln)
2087
+ cat > "$case_dir/fakebin/ln" <<SH
2088
+ #!/usr/bin/env bash
2089
+ case "\$*" in
2090
+ *"$home/state/.meta-$id.lock"*)
2091
+ if [ ! -e "$case_dir/state-swapped" ]; then
2092
+ : > "$case_dir/state-swapped"
2093
+ mv "$home/state" "$home/state-original" || exit 1
2094
+ "$real_ln" -s "$foreign_state" "$home/state" || exit 1
2095
+ fi
2096
+ ;;
2097
+ esac
2098
+ exec "$real_ln" "\$@"
2099
+ SH
2100
+ chmod +x "$case_dir/fakebin/ln"
2101
+
2102
+ out=$(run_teardown "$case_dir" "$id") || rc=$?
2103
+ [ "$rc" -ne 0 ] || fail "teardown trusted a record after its parent was swapped"
2104
+ assert_contains "$out" "task record authorized directory resolves outside this home" \
2105
+ "post-lock record check did not report the swapped parent"
2106
+ assert_present "$foreign_state/$id.meta" "teardown removed the foreign record"
2107
+ assert_present "$foreign_worktree" "teardown removed the foreign local copy"
2108
+ assert_absent "$case_dir/backend-resource-action" \
2109
+ "teardown acted on a foreign endpoint after the parent swap"
2110
+ assert_absent "$case_dir/local-copy-resource-action" \
2111
+ "teardown acted on a foreign local copy after the parent swap"
2112
+ pass "teardown rechecks record parents after locking"
2113
+ }
2114
+
2115
+ test_teardown_refuses_a_symlinked_state_directory_at_entry() {
2116
+ local case_dir home id external_state out rc=0
2117
+ id=atomic-symlink-state-b12
2118
+ case_dir=$(make_home symlink-state)
2119
+ home=$(home_of "$case_dir")
2120
+ external_state="$case_dir/external-state"
2121
+ mv "$home/state" "$external_state"
2122
+ fm_write_meta "$external_state/$id.meta" \
2123
+ "window=firstmate:fm-$id" "endpoint_task_id=$id" \
2124
+ "worktree=$case_dir/foreign-worktree" "project=$case_dir/foreign-project" \
2125
+ "harness=claude" "kind=ship" "mode=local-only" "yolo=off"
2126
+ ln -s "$external_state" "$home/state"
2127
+ track_teardown_resource_actions "$case_dir"
2128
+
2129
+ out=$(run_teardown "$case_dir" "$id") || rc=$?
2130
+ [ "$rc" -ne 0 ] || fail "teardown accepted a symlinked state directory"
2131
+ assert_contains "$out" "state directory is not a real directory" \
2132
+ "teardown did not identify the unsafe state directory"
2133
+ assert_present "$external_state/$id.meta" \
2134
+ "teardown removed metadata through the symlinked state directory"
2135
+ assert_absent "$case_dir/backend-resource-action" \
2136
+ "teardown acted on an endpoint through symlinked state"
2137
+ assert_absent "$case_dir/local-copy-resource-action" \
2138
+ "teardown acted on a local copy through symlinked state"
2139
+ pass "teardown refuses symlinked state before resource actions"
2140
+ }
2141
+
2142
+ test_home_without_a_backlog_dispatches_and_completes() {
2143
+ local case_dir id out
2144
+ id=atomic-no-backlog-b12
2145
+ case_dir=$(make_home no-backlog "$id")
2146
+ rm -f "$(backlog_of "$case_dir")"
2147
+ make_tasks_axi_incompatible "$case_dir"
2148
+
2149
+ out=$(run_ship_spawn "$case_dir" "$id") || fail "no-backlog spawn failed: $out"
2150
+ assert_present "$(home_of "$case_dir")/state/$id.meta" \
2151
+ "no-backlog spawn did not publish its task record"
2152
+ out=$(run_teardown "$case_dir" "$id") || fail "no-backlog teardown failed: $out"
2153
+ assert_absent "$(home_of "$case_dir")/state/$id.meta" \
2154
+ "no-backlog teardown retained its task record"
2155
+ assert_absent "$(home_of "$case_dir")/state/$id.backlog-close" \
2156
+ "no-backlog teardown recorded a close marker"
2157
+ pass "a home with no backlog remains exempt from lifecycle transitions"
2158
+ }
2159
+
2160
+ test_manual_backend_home_dispatches_and_completes_without_touching_the_backlog() {
2161
+ local case_dir id data data_resolved out
2162
+ id=atomic-manual-b12
2163
+ case_dir=$(make_home manual-backend "$id")
2164
+ printf '%s\n' manual > "$(home_of "$case_dir")/config/backlog-backend"
2165
+ data="$case_dir/manual-data"
2166
+ mv "$(home_of "$case_dir")/data" "$data"
2167
+ data_resolved=$(cd "$data" && pwd -P)
2168
+ make_tasks_axi_incompatible "$case_dir"
2169
+ # Deliberately no backlog item: on a manual home the operator owns the file,
2170
+ # so neither half of the lifecycle may hard-fail over its contents.
2171
+ out=$(FM_DATA_OVERRIDE="$data" run_ship_spawn "$case_dir" "$id") \
2172
+ || fail "manual-backend spawn failed: $out"
2173
+ assert_contains "$out" "spawned $id" "manual-backend spawn did not report success"
2174
+
2175
+ out=$(FM_DATA_OVERRIDE="$data" run_teardown "$case_dir" "$id") \
2176
+ || fail "manual-backend teardown failed: $out"
2177
+ assert_contains "$out" "Update $data_resolved/backlog.md" \
2178
+ "manual-backend teardown did not name its configured backlog path"
2179
+ assert_absent "$(home_of "$case_dir")/state/$id.backlog-close" \
2180
+ "manual-backend teardown recorded a close it never owed"
2181
+ pass "a manual-backlog home dispatches and completes without a hard failure"
2182
+ }
2183
+
2184
+ test_a_secondmate_home_keeps_its_own_books() {
2185
+ local case_dir id out
2186
+ id=atomic-mate-b13
2187
+ case_dir=$(make_home mate-own-books "$id")
2188
+ # The mate's home is a firstmate home in its own right; the invariant is
2189
+ # single-host, so its own dispatch and completion keep its own two records
2190
+ # paired with no parent involved.
2191
+ printf '%s\n' mate-h1 > "$(home_of "$case_dir")/.fm-secondmate-home"
2192
+ add_item "$case_dir" "$id"
2193
+
2194
+ out=$(run_ship_spawn "$case_dir" "$id") || fail "mate-home spawn failed: $out"
2195
+ [ "$(row_state "$case_dir" "$id")" = in_flight ] \
2196
+ || fail "a mate's own dispatch left its item at $(row_state "$case_dir" "$id")"
2197
+
2198
+ rm -f "$(home_of "$case_dir")/state/$id.meta"
2199
+ write_task_meta "$case_dir" "$id" ship local-only "spawn_gen=spawn-mate-close"
2200
+ out=$(run_teardown "$case_dir" "$id") || fail "mate-home teardown failed: $out"
2201
+ [ "$(row_state "$case_dir" "$id")" = "done" ] \
2202
+ || fail "a mate's own completion left its item at $(row_state "$case_dir" "$id")"
2203
+ pass "a secondmate home keeps its own books paired through dispatch and completion"
2204
+ }
2205
+
2206
+ test_a_persistent_secondmate_is_never_a_backlog_item() {
2207
+ local case_dir id out mate
2208
+ id=atomic-mate-b14
2209
+ case_dir=$(make_home mate-not-an-item)
2210
+ mate="$case_dir/mate-home"
2211
+ mkdir -p "$mate/bin" "$mate/data"
2212
+ printf '# Firstmate\n' > "$mate/AGENTS.md"
2213
+ printf '%s\n' "$id" > "$mate/.fm-secondmate-home"
2214
+ printf 'charter for %s\n' "$id" > "$mate/data/charter.md"
2215
+
2216
+ # No backlog item exists for the mate, and none should be required: agents are
2217
+ # not work items. The dispatch must succeed anyway.
2218
+ out=$(run_spawn "$case_dir" "$id" "$mate" --secondmate) \
2219
+ || fail "secondmate spawn failed: $out"
2220
+ assert_contains "$out" "spawned $id" "secondmate spawn did not report success"
2221
+ assert_present "$(home_of "$case_dir")/state/$id.meta" "secondmate spawn published no record"
2222
+ pass "dispatching a persistent secondmate needs no backlog item"
2223
+ }
2224
+
2225
+ test_dispatch_moves_the_item_in_flight_in_the_same_run
2226
+ test_dispatch_refuses_a_pending_authoritative_close
2227
+ test_dispatch_refuses_a_held_row_before_creating_resources
2228
+ test_dispatch_refuses_a_blocked_row_before_creating_resources
2229
+ test_dispatch_refuses_a_held_in_flight_row_before_relaunch
2230
+ test_dispatch_reads_the_row_from_the_backlog_root
2231
+ test_recovery_uses_the_parent_of_a_trailing_slash_data_record
2232
+ test_completion_targets_a_nested_relative_data_directory
2233
+ test_immediate_child_absolute_data_dispatches_and_completes
2234
+ test_bare_relative_data_dispatches_and_completes
2235
+ test_dispatch_refuses_a_symlinked_backlog_without_crossing_homes
2236
+ test_automatic_backend_refuses_incompatible_tasks_axi_before_mutation
2237
+ test_dispatch_refuses_an_unresolvable_data_directory
2238
+ test_completion_refuses_an_unresolvable_data_directory
2239
+ test_dispatch_refuses_an_id_this_home_has_no_item_for
2240
+ test_dispatch_reports_a_backlog_read_failure
2241
+ test_dispatch_refuses_a_closed_item
2242
+ test_dispatch_refuses_to_commit_without_a_published_record
2243
+ test_dispatch_leaves_no_record_when_the_transition_fails
2244
+ test_dispatch_reports_an_incomplete_record_rollback
2245
+ test_dispatch_reports_an_incomplete_busy_rollback
2246
+ test_dispatch_rolls_back_before_a_failed_launch_delivery
2247
+ test_dispatch_defers_interruption_across_backlog_commit
2248
+ test_dispatch_interruption_during_kimi_readiness_fails_before_commit
2249
+ test_dispatch_does_not_resurrect_a_row_closed_after_preflight
2250
+ test_dispatch_fails_when_its_row_vanishes_after_preflight
2251
+ test_completion_closes_a_local_only_ship_before_reporting_success
2252
+ test_completion_closes_a_scout_with_its_report
2253
+ test_completion_refuses_a_legacy_record_without_an_incarnation
2254
+ test_completion_refuses_ambiguous_incarnation_metadata
2255
+ test_completion_records_a_relative_report_for_relocated_data
2256
+ test_space_containing_scout_report_marker_replays
2257
+ test_trailing_newline_data_path_fails_closed
2258
+ test_control_character_data_path_is_refused_before_cleanup
2259
+ test_completion_preserves_records_when_meta_removal_fails
2260
+ test_completion_fails_loudly_and_records_the_close_it_still_owes
2261
+ test_interrupted_destructive_cleanup_leaves_a_recoverable_close
2262
+ test_completion_refuses_a_close_target_symlinked_to_a_directory
2263
+ test_completion_fails_when_its_close_marker_cannot_be_removed
2264
+ test_recovery_retries_when_a_close_marker_cannot_be_removed
2265
+ test_recovery_reports_an_owned_row_read_failure
2266
+ test_orca_cleanup_recovery_never_transitions_the_backlog
2267
+ test_recovery_marks_an_owned_record_in_flight
2268
+ test_recovery_rejects_an_internal_worker_record_symlink
2269
+ test_recovery_ignores_a_symlinked_worker_record
2270
+ test_recovery_replays_a_close_an_interrupted_cleanup_left_open
2271
+ test_recovery_backfills_a_recorded_link_on_an_already_done_item
2272
+ test_recovery_preserves_a_close_when_the_backlog_cannot_be_read
2273
+ test_recovery_retry_preserves_incomplete_cleanup_warning
2274
+ test_recovery_finishes_a_close_for_the_same_meta_incarnation
2275
+ test_recovery_preserves_a_close_for_ambiguous_incarnation_metadata
2276
+ test_recovery_preserves_both_records_when_meta_removal_fails
2277
+ test_recovery_preserves_a_close_beside_symlinked_metadata
2278
+ test_recovery_rejects_a_marker_for_another_task_identity
2279
+ test_recovery_rejects_a_foreign_data_directory
2280
+ test_recovery_rejects_an_unterminated_unknown_field
2281
+ test_recovery_rejects_lexical_data_traversal
2282
+ test_recovery_rejects_raw_control_bytes
2283
+ test_recovery_rejects_malformed_pr_urls
2284
+ test_failed_close_replay_is_not_started_as_live_work
2285
+ test_recovery_rejects_invalid_close_arguments
2286
+ test_recovery_rejects_a_symlinked_close_marker
2287
+ test_recovery_drops_a_close_for_a_newer_meta_incarnation
2288
+ test_recovery_rejects_a_legacy_close_without_an_incarnation
2289
+ test_bootstrap_rechecks_worker_record_boundary_after_locking
2290
+ test_lifecycle_refuses_ancestor_symlinks_outside_home_roots
2291
+ test_same_home_state_override_remains_supported
2292
+ test_bootstrap_refuses_a_symlinked_state_directory_before_reconciliation
2293
+ test_bootstrap_stops_when_data_disappears_before_reconciliation
2294
+ test_bootstrap_addressing_exemptions_remain_nonfatal
2295
+ test_recovery_leaves_a_captain_held_item_alone
2296
+ test_no_backlog_teardown_refuses_a_symlinked_task_record_at_entry
2297
+ test_teardown_rechecks_record_parent_after_lock_acquisition
2298
+ test_teardown_refuses_a_symlinked_state_directory_at_entry
2299
+ test_home_without_a_backlog_dispatches_and_completes
2300
+ test_manual_backend_home_dispatches_and_completes_without_touching_the_backlog
2301
+ test_a_secondmate_home_keeps_its_own_books
2302
+ test_a_persistent_secondmate_is_never_a_backlog_item