@zerwiz/ymir 0.1.4 → 0.1.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (850) hide show
  1. package/.agents/README.md +291 -0
  2. package/.agents/agents/bragi-marketer.md +111 -0
  3. package/.agents/agents/brokk.md +352 -0
  4. package/.agents/agents/forseti-reviewer.md +93 -0
  5. package/.agents/agents/frigg-consent.md +77 -0
  6. package/.agents/agents/groa-update.md +77 -0
  7. package/.agents/agents/hnoss-designer.md +69 -0
  8. package/.agents/agents/huginn-researcher.md +99 -0
  9. package/.agents/agents/jord-projects.md +78 -0
  10. package/.agents/agents/kvasir-scout.md +91 -0
  11. package/.agents/agents/mimir-planner.md +92 -0
  12. package/.agents/agents/muninn-stow.md +77 -0
  13. package/.agents/agents/saga-bearings.md +78 -0
  14. package/.agents/agents/sindri-developer.md +114 -0
  15. package/.agents/agents/snotra-documenter.md +105 -0
  16. package/.agents/agents/syn-recovery.md +78 -0
  17. package/.agents/agents/tyr-check.md +78 -0
  18. package/.agents/agents/urdh-hold.md +77 -0
  19. package/.agents/agents/volundr-smith.md +80 -0
  20. package/.agents/agents/vor-diagnostics.md +77 -0
  21. package/.agents/assets/agents/naming.md +50 -0
  22. package/.agents/assets/agents/registry.md +93 -0
  23. package/.agents/assets/agents/runtime.md +36 -0
  24. package/.agents/assets/agents/toon-tasks-cli.md +42 -0
  25. package/.agents/assets/templates/company_entity.template.md +37 -0
  26. package/.agents/backend/backends/cmux.sh +649 -0
  27. package/.agents/backend/backends/herdr-eventwait.py +157 -0
  28. package/.agents/backend/backends/herdr-workspace-move.py +114 -0
  29. package/.agents/backend/backends/herdr.sh +3386 -0
  30. package/.agents/backend/backends/orca.sh +290 -0
  31. package/.agents/backend/backends/tmux.sh +357 -0
  32. package/.agents/backend/backends/zellij.sh +700 -0
  33. package/.agents/backend/fm-afk-launch.sh +654 -0
  34. package/.agents/backend/fm-afk-return.sh +241 -0
  35. package/.agents/backend/fm-afk-start.sh +172 -0
  36. package/.agents/backend/fm-arm-command-policy.mjs +960 -0
  37. package/.agents/backend/fm-arm-pretool-check.sh +200 -0
  38. package/.agents/backend/fm-backend-hometag-lib.sh +52 -0
  39. package/.agents/backend/fm-backend.sh +981 -0
  40. package/.agents/backend/fm-backlog-handoff.sh +909 -0
  41. package/.agents/backend/fm-backlog-receive.sh +187 -0
  42. package/.agents/backend/fm-backlog-transition-lib.sh +779 -0
  43. package/.agents/backend/fm-bearings-board.sh +204 -0
  44. package/.agents/backend/fm-bearings-snapshot.sh +546 -0
  45. package/.agents/backend/fm-bootstrap.sh +1568 -0
  46. package/.agents/backend/fm-branch-outcome.sh +203 -0
  47. package/.agents/backend/fm-branch-prompt.sh +101 -0
  48. package/.agents/backend/fm-brief.sh +452 -0
  49. package/.agents/backend/fm-busy-event.sh +234 -0
  50. package/.agents/backend/fm-busy-lib.sh +986 -0
  51. package/.agents/backend/fm-captain-hold.sh +1001 -0
  52. package/.agents/backend/fm-cd-command-policy.mjs +153 -0
  53. package/.agents/backend/fm-cd-pretool-check.sh +190 -0
  54. package/.agents/backend/fm-check-lib.sh +74 -0
  55. package/.agents/backend/fm-check-register.sh +42 -0
  56. package/.agents/backend/fm-check-unregister.sh +52 -0
  57. package/.agents/backend/fm-classify-lib.sh +1770 -0
  58. package/.agents/backend/fm-claude-stop-autoarm.sh +334 -0
  59. package/.agents/backend/fm-composer-lib.sh +1416 -0
  60. package/.agents/backend/fm-config-inherit-lib.sh +1202 -0
  61. package/.agents/backend/fm-config-push.sh +254 -0
  62. package/.agents/backend/fm-control-lib.sh +251 -0
  63. package/.agents/backend/fm-control.sh +878 -0
  64. package/.agents/backend/fm-crew-state.sh +625 -0
  65. package/.agents/backend/fm-cursor-lib.sh +243 -0
  66. package/.agents/backend/fm-decision-hold.sh +232 -0
  67. package/.agents/backend/fm-doc-audience-check.sh +269 -0
  68. package/.agents/backend/fm-dod-lib.sh +67 -0
  69. package/.agents/backend/fm-ensure-agents-md.sh +253 -0
  70. package/.agents/backend/fm-extension-launch-barrier.mjs +129 -0
  71. package/.agents/backend/fm-extension.mjs +2577 -0
  72. package/.agents/backend/fm-extension.sh +16 -0
  73. package/.agents/backend/fm-ff-lib.sh +420 -0
  74. package/.agents/backend/fm-fleet-snapshot.sh +1463 -0
  75. package/.agents/backend/fm-fleet-sync.sh +458 -0
  76. package/.agents/backend/fm-fleet-view.sh +96 -0
  77. package/.agents/backend/fm-gate-refuse-lib.sh +102 -0
  78. package/.agents/backend/fm-guard.sh +242 -0
  79. package/.agents/backend/fm-harness.sh +195 -0
  80. package/.agents/backend/fm-herdr-ci-cleanup.sh +115 -0
  81. package/.agents/backend/fm-herdr-lab.sh +346 -0
  82. package/.agents/backend/fm-herdr-session-cleanup.sh +337 -0
  83. package/.agents/backend/fm-home-seed.sh +984 -0
  84. package/.agents/backend/fm-home-summary-refresh.sh +255 -0
  85. package/.agents/backend/fm-hook-host-lib.sh +36 -0
  86. package/.agents/backend/fm-inactive-reconcile.sh +531 -0
  87. package/.agents/backend/fm-inbox.sh +399 -0
  88. package/.agents/backend/fm-install-actionlint.sh +84 -0
  89. package/.agents/backend/fm-install-herdr.sh +96 -0
  90. package/.agents/backend/fm-install-shellcheck.sh +84 -0
  91. package/.agents/backend/fm-install-treehouse.sh +95 -0
  92. package/.agents/backend/fm-kimi-turnend-hook.sh +276 -0
  93. package/.agents/backend/fm-lease-lib.sh +218 -0
  94. package/.agents/backend/fm-lease.sh +189 -0
  95. package/.agents/backend/fm-line-cap-lib.sh +51 -0
  96. package/.agents/backend/fm-lint-workflows.sh +137 -0
  97. package/.agents/backend/fm-lint.sh +591 -0
  98. package/.agents/backend/fm-lock-lib.sh +104 -0
  99. package/.agents/backend/fm-lock.sh +107 -0
  100. package/.agents/backend/fm-marker-lib.sh +12 -0
  101. package/.agents/backend/fm-merge-local.sh +74 -0
  102. package/.agents/backend/fm-merge-outcome-lib.sh +138 -0
  103. package/.agents/backend/fm-nm-run-lib.sh +124 -0
  104. package/.agents/backend/fm-on.sh +125 -0
  105. package/.agents/backend/fm-operational-input.sh +253 -0
  106. package/.agents/backend/fm-peek.sh +46 -0
  107. package/.agents/backend/fm-pending-reply-lib.sh +1409 -0
  108. package/.agents/backend/fm-pr-check.sh +135 -0
  109. package/.agents/backend/fm-pr-lib.sh +1016 -0
  110. package/.agents/backend/fm-pr-merge.sh +707 -0
  111. package/.agents/backend/fm-pr-poll.sh +110 -0
  112. package/.agents/backend/fm-primary-scope-lib.sh +33 -0
  113. package/.agents/backend/fm-procevent-extension-capture.pl +259 -0
  114. package/.agents/backend/fm-procevent-lavish.sh +628 -0
  115. package/.agents/backend/fm-procevent-lib.sh +892 -0
  116. package/.agents/backend/fm-procevent-quota.sh +290 -0
  117. package/.agents/backend/fm-procevent-remote-reply.sh +577 -0
  118. package/.agents/backend/fm-procevent-when.sh +504 -0
  119. package/.agents/backend/fm-procevent.sh +1466 -0
  120. package/.agents/backend/fm-project-mode.sh +92 -0
  121. package/.agents/backend/fm-project-origin-lib.sh +180 -0
  122. package/.agents/backend/fm-promote.sh +282 -0
  123. package/.agents/backend/fm-public-followup-emit.sh +260 -0
  124. package/.agents/backend/fm-public-followup-lib.sh +398 -0
  125. package/.agents/backend/fm-public-followup.sh +1330 -0
  126. package/.agents/backend/fm-push-transition-lib.sh +168 -0
  127. package/.agents/backend/fm-quota-axi-lib.sh +93 -0
  128. package/.agents/backend/fm-quota-choose.sh +384 -0
  129. package/.agents/backend/fm-remote-delta-read.sh +189 -0
  130. package/.agents/backend/fm-remote-doctor.sh +800 -0
  131. package/.agents/backend/fm-remote-entrypoint.sh +193 -0
  132. package/.agents/backend/fm-remote-file.sh +234 -0
  133. package/.agents/backend/fm-remote-home-provision.sh +259 -0
  134. package/.agents/backend/fm-remote-home-seed.sh +259 -0
  135. package/.agents/backend/fm-remote-inherit-push.sh +90 -0
  136. package/.agents/backend/fm-remote-inherit.sh +179 -0
  137. package/.agents/backend/fm-remote-job-lib.sh +1241 -0
  138. package/.agents/backend/fm-remote-job-reap-orphans.sh +137 -0
  139. package/.agents/backend/fm-remote-job-worker.sh +1124 -0
  140. package/.agents/backend/fm-remote-readiness-lib.sh +44 -0
  141. package/.agents/backend/fm-remote-secondmate-control.sh +340 -0
  142. package/.agents/backend/fm-review-diff.sh +158 -0
  143. package/.agents/backend/fm-secondmate-charter-lib.sh +44 -0
  144. package/.agents/backend/fm-secondmate-nudge-lib.sh +68 -0
  145. package/.agents/backend/fm-secondmate-parent-lib.sh +70 -0
  146. package/.agents/backend/fm-secondmate-reconcile.sh +371 -0
  147. package/.agents/backend/fm-secondmate-registry-lib.sh +311 -0
  148. package/.agents/backend/fm-secondmate-report.sh +88 -0
  149. package/.agents/backend/fm-send.sh +1059 -0
  150. package/.agents/backend/fm-session-lock-lib.sh +176 -0
  151. package/.agents/backend/fm-session-start.sh +970 -0
  152. package/.agents/backend/fm-sessionstart-cursor.sh +40 -0
  153. package/.agents/backend/fm-sessionstart-nudge.sh +45 -0
  154. package/.agents/backend/fm-sessionstart-run.sh +145 -0
  155. package/.agents/backend/fm-spawn.sh +3153 -0
  156. package/.agents/backend/fm-startup-memory-budget-lib.sh +224 -0
  157. package/.agents/backend/fm-startup-memory-budget.sh +94 -0
  158. package/.agents/backend/fm-startup-network.sh +642 -0
  159. package/.agents/backend/fm-stow-cascade.sh +251 -0
  160. package/.agents/backend/fm-subagent-pretool-check.sh +207 -0
  161. package/.agents/backend/fm-supervise-daemon.sh +1732 -0
  162. package/.agents/backend/fm-supervision-instructions.sh +215 -0
  163. package/.agents/backend/fm-supervision-lib.sh +90 -0
  164. package/.agents/backend/fm-supervisor-target-lib.sh +78 -0
  165. package/.agents/backend/fm-tangle-lib.sh +53 -0
  166. package/.agents/backend/fm-task-inbox-lib.sh +402 -0
  167. package/.agents/backend/fm-tasks-axi-lib.sh +123 -0
  168. package/.agents/backend/fm-teardown.sh +2922 -0
  169. package/.agents/backend/fm-test-isolation-proof.sh +588 -0
  170. package/.agents/backend/fm-test-run.sh +2191 -0
  171. package/.agents/backend/fm-timeout-lib.sh +141 -0
  172. package/.agents/backend/fm-timing-lib.sh +179 -0
  173. package/.agents/backend/fm-tmux-lib.sh +291 -0
  174. package/.agents/backend/fm-tool-update-check.sh +898 -0
  175. package/.agents/backend/fm-trace-context-lib.sh +227 -0
  176. package/.agents/backend/fm-transition-lib.sh +103 -0
  177. package/.agents/backend/fm-turnend-guard-cursor.sh +391 -0
  178. package/.agents/backend/fm-turnend-guard-grok.sh +90 -0
  179. package/.agents/backend/fm-turnend-guard.sh +432 -0
  180. package/.agents/backend/fm-update.sh +110 -0
  181. package/.agents/backend/fm-vendor-auth-probe.sh +190 -0
  182. package/.agents/backend/fm-voice-client.py +1373 -0
  183. package/.agents/backend/fm-voice-relay.py +1256 -0
  184. package/.agents/backend/fm-wake-drain.sh +592 -0
  185. package/.agents/backend/fm-wake-grant.sh +127 -0
  186. package/.agents/backend/fm-wake-lib.sh +1849 -0
  187. package/.agents/backend/fm-watch-arm.sh +602 -0
  188. package/.agents/backend/fm-watch-checkpoint.sh +109 -0
  189. package/.agents/backend/fm-watch.sh +1962 -0
  190. package/.agents/backend/fm-x-dismiss.sh +116 -0
  191. package/.agents/backend/fm-x-followup.sh +286 -0
  192. package/.agents/backend/fm-x-lib.sh +1000 -0
  193. package/.agents/backend/fm-x-link.sh +234 -0
  194. package/.agents/backend/fm-x-poll.sh +185 -0
  195. package/.agents/backend/fm-x-reply.sh +385 -0
  196. package/.agents/backend/fm_voice_frame.py +166 -0
  197. package/.agents/backend/fm_voice_records.py +574 -0
  198. package/.agents/backend/model-bridge.py +290 -0
  199. package/.agents/backend/opencode-go-bridge.py +30 -0
  200. package/.agents/bus/README.md +12 -0
  201. package/.agents/bus/messages.json +1 -0
  202. package/.agents/config/agents.machine.example.yaml +16 -0
  203. package/.agents/config/agents.yaml +50 -0
  204. package/.agents/config/agents.yaml.example +72 -0
  205. package/.agents/config/crew-dispatch.json +22 -0
  206. package/.agents/config/cron.yaml +9 -0
  207. package/.agents/config/eindri-dispatch.json +67 -0
  208. package/.agents/config/eindri-harness +1 -0
  209. package/.agents/config/startup-memory-budget +1 -0
  210. package/.agents/config/tailscale-sync.yaml +27 -0
  211. package/.agents/filebrowser/.filebrowser.json +6 -0
  212. package/.agents/filebrowser/README.md +11 -0
  213. package/.agents/gateway/README.md +12 -0
  214. package/.agents/github/README.md +15 -0
  215. package/.agents/github/webhooks/.gitkeep +0 -0
  216. package/.agents/github/workflows/.gitkeep +0 -0
  217. package/.agents/harness/opencode/plugins/lib/rodd-operational-input.js +37 -0
  218. package/.agents/harness/opencode/plugins/package.json +4 -0
  219. package/.agents/harness/opencode/plugins/saga-sessionstart.js +77 -0
  220. package/.agents/harness/opencode/plugins/syn-cd-check.js +61 -0
  221. package/.agents/harness/opencode/plugins/syn-pretool-check.js +68 -0
  222. package/.agents/harness/opencode/plugins/syn-turnend-guard.js +105 -0
  223. package/.agents/harness/opencode/plugins/syn-watch-arm.js +521 -0
  224. package/.agents/memory/README.md +46 -0
  225. package/.agents/memory/kaia.engram +0 -0
  226. package/.agents/memory/kaia.engram-shm +0 -0
  227. package/.agents/memory/kaia.engram-wal +0 -0
  228. package/.agents/memory/runes_audit.md +5 -0
  229. package/.agents/memory/well/episodes.jsonl +367 -0
  230. package/.agents/memory/well/workspace.jsonl +27 -0
  231. package/.agents/migrations/0001-hodd-layout.sh +38 -0
  232. package/.agents/migrations/0002-a2a-mcp.sh +27 -0
  233. package/.agents/migrations/0003-private-data-separation.sh +111 -0
  234. package/.agents/migrations/0004-hoard-and-realms.sh +104 -0
  235. package/.agents/sandbox/Dockerfile.utgard +19 -0
  236. package/.agents/sandbox/README.md +14 -0
  237. package/.agents/sandbox/utgard.config.json +14 -0
  238. package/.agents/skills/README.md +59 -0
  239. package/.agents/skills/bragi-marketing/README.md +3 -0
  240. package/.agents/skills/bragi-marketing/SKILL.md +82 -0
  241. package/.agents/skills/eindri-homes/README.md +3 -0
  242. package/.agents/skills/eindri-homes/SKILL.md +291 -0
  243. package/.agents/skills/eindri-homes/assets/control-plane.md +122 -0
  244. package/.agents/skills/eindri-homes/assets/dispatch.md +80 -0
  245. package/.agents/skills/frigg-consent/README.md +3 -0
  246. package/.agents/skills/frigg-consent/SKILL.md +56 -0
  247. package/.agents/skills/galdr-ymirsystem/README.md +3 -0
  248. package/.agents/skills/galdr-ymirsystem/SKILL.md +158 -0
  249. package/.agents/skills/galdr-ymirsystem/assets/README.md +74 -0
  250. package/.agents/skills/galdr-ymirsystem/assets/brokk-distro-runtime.md +390 -0
  251. package/.agents/skills/galdr-ymirsystem/assets/build-method.md +113 -0
  252. package/.agents/skills/galdr-ymirsystem/assets/build-tool-categories.md +178 -0
  253. package/.agents/skills/galdr-ymirsystem/assets/eindri-orchestration.md +518 -0
  254. package/.agents/skills/galdr-ymirsystem/assets/eindri-profiles.md +136 -0
  255. package/.agents/skills/galdr-ymirsystem/assets/harness-integration/README.md +673 -0
  256. package/.agents/skills/galdr-ymirsystem/assets/harness-integration/claude-code.md +197 -0
  257. package/.agents/skills/galdr-ymirsystem/assets/harness-integration/codex.md +191 -0
  258. package/.agents/skills/galdr-ymirsystem/assets/harness-integration/cursor.md +193 -0
  259. package/.agents/skills/galdr-ymirsystem/assets/harness-integration/opencode.md +262 -0
  260. package/.agents/skills/galdr-ymirsystem/assets/harness-integration/pi.md +295 -0
  261. package/.agents/skills/galdr-ymirsystem/assets/hlidskjalf-ui.md +496 -0
  262. package/.agents/skills/galdr-ymirsystem/assets/installation.md +571 -0
  263. package/.agents/skills/galdr-ymirsystem/assets/local-models/HOST-DATA.example.md +72 -0
  264. package/.agents/skills/galdr-ymirsystem/assets/local-models.md +274 -0
  265. package/.agents/skills/galdr-ymirsystem/assets/memory-well.md +161 -0
  266. package/.agents/skills/galdr-ymirsystem/assets/nornir-jobs.md +377 -0
  267. package/.agents/skills/galdr-ymirsystem/assets/norse-naming.md +325 -0
  268. package/.agents/skills/galdr-ymirsystem/assets/odrerir-hall.md +129 -0
  269. package/.agents/skills/galdr-ymirsystem/assets/pi-boot/einherjar-spawn.schema.json +81 -0
  270. package/.agents/skills/galdr-ymirsystem/assets/pi-boot/herdr-profile.toml +75 -0
  271. package/.agents/skills/galdr-ymirsystem/assets/pi-boot/pi-profile.yml +96 -0
  272. package/.agents/skills/galdr-ymirsystem/assets/pi-boot/supervision-tree.yml +163 -0
  273. package/.agents/skills/galdr-ymirsystem/assets/pi-boot-guide.md +58 -0
  274. package/.agents/skills/galdr-ymirsystem/assets/porting-upstream-to-norse.md +301 -0
  275. package/.agents/skills/galdr-ymirsystem/assets/principles.md +385 -0
  276. package/.agents/skills/galdr-ymirsystem/assets/reference-adoption.md +99 -0
  277. package/.agents/skills/galdr-ymirsystem/assets/registry.md +379 -0
  278. package/.agents/skills/galdr-ymirsystem/assets/runtime-compliance.md +463 -0
  279. package/.agents/skills/galdr-ymirsystem/assets/runtime-components.md +553 -0
  280. package/.agents/skills/galdr-ymirsystem/assets/smidja.md +175 -0
  281. package/.agents/skills/galdr-ymirsystem/schemas/toon-schemas.md +190 -0
  282. package/.agents/skills/galdr-ymirsystem/scripts/README.md +45 -0
  283. package/.agents/skills/galdr-ymirsystem/scripts/bench-one.sh +248 -0
  284. package/.agents/skills/galdr-ymirsystem/scripts/compliance-check.sh +360 -0
  285. package/.agents/skills/galdr-ymirsystem/scripts/gpu-sample.sh +43 -0
  286. package/.agents/skills/galdr-ymirsystem/scripts/stop-all.sh +34 -0
  287. package/.agents/skills/galdr-ymirsystem/scripts/toon-check.py +188 -0
  288. package/.agents/skills/gjallarhorn-relay/README.md +3 -0
  289. package/.agents/skills/gjallarhorn-relay/SKILL.md +283 -0
  290. package/.agents/skills/groa-update/SKILL.md +70 -0
  291. package/.agents/skills/hamr-adapters/README.md +3 -0
  292. package/.agents/skills/hamr-adapters/SKILL.md +123 -0
  293. package/.agents/skills/hamr-adapters/references/common/control-and-recovery.md +37 -0
  294. package/.agents/skills/hamr-adapters/references/common/dispatch.md +32 -0
  295. package/.agents/skills/hamr-adapters/references/common/model-and-effort.md +42 -0
  296. package/.agents/skills/hamr-adapters/references/common/primary-hooks.md +40 -0
  297. package/.agents/skills/hamr-adapters/references/harness/claude.md +55 -0
  298. package/.agents/skills/hamr-adapters/references/harness/codex.md +43 -0
  299. package/.agents/skills/hamr-adapters/references/harness/cursor.md +75 -0
  300. package/.agents/skills/hamr-adapters/references/harness/grok.md +69 -0
  301. package/.agents/skills/hamr-adapters/references/harness/kimi.md +51 -0
  302. package/.agents/skills/hamr-adapters/references/harness/muse.md +70 -0
  303. package/.agents/skills/hamr-adapters/references/harness/opencode.md +42 -0
  304. package/.agents/skills/hamr-adapters/references/harness/pi.md +56 -0
  305. package/.agents/skills/herdr-panes/README.md +3 -0
  306. package/.agents/skills/herdr-panes/SKILL.md +92 -0
  307. package/.agents/skills/herdr-panes/assets/herdr-backend.md +346 -0
  308. package/.agents/skills/herdr-panes/assets/tmux-backend.md +115 -0
  309. package/.agents/skills/hnoss-design/DESIGN.md +19 -0
  310. package/.agents/skills/hnoss-design/README.md +3 -0
  311. package/.agents/skills/hnoss-design/SKILL.md +83 -0
  312. package/.agents/skills/hvild-afk/README.md +3 -0
  313. package/.agents/skills/hvild-afk/SKILL.md +241 -0
  314. package/.agents/skills/jord-projects/README.md +3 -0
  315. package/.agents/skills/jord-projects/SKILL.md +92 -0
  316. package/.agents/skills/lifecycle/SKILL.md +56 -0
  317. package/.agents/skills/lifecycle/smoke_test.sh +52 -0
  318. package/.agents/skills/lifecycle/start.sh +21 -0
  319. package/.agents/skills/lifecycle/status.sh +35 -0
  320. package/.agents/skills/lifecycle/stop.sh +16 -0
  321. package/.agents/skills/muninn-stow/README.md +3 -0
  322. package/.agents/skills/muninn-stow/SKILL.md +309 -0
  323. package/.agents/skills/no-mistakes/SKILL.md +379 -0
  324. package/.agents/skills/nornir-schedule/README.md +3 -0
  325. package/.agents/skills/nornir-schedule/SKILL.md +25 -0
  326. package/.agents/skills/nornir-schedule/assets/events.md +137 -0
  327. package/.agents/skills/nornir-schedule/assets/quota.md +131 -0
  328. package/.agents/skills/nsr-compliance/README.md +3 -0
  329. package/.agents/skills/nsr-compliance/SKILL.md +29 -0
  330. package/.agents/skills/nsr-compliance/assets/nsr/README.md +40 -0
  331. package/.agents/skills/nsr-compliance/assets/nsr/assets/RULES/README.md +15 -0
  332. package/.agents/skills/nsr-compliance/assets/nsr/assets/RULES/git-workflow.md +18 -0
  333. package/.agents/skills/nsr-compliance/assets/nsr/assets/RULES/security.md +22 -0
  334. package/.agents/skills/nsr-compliance/assets/nsr/assets/RULES/tech-stack.md +8 -0
  335. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/SKILL.md +71 -0
  336. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/config/core_four.yaml +16 -0
  337. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/gates/check_danger.sh +102 -0
  338. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/gates/check_wiring.sh +69 -0
  339. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/gates/validate_code.sh +29 -0
  340. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/gates/verify_docs.py +66 -0
  341. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/generate.sh +109 -0
  342. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/harness/envelopes/result_envelope.json +14 -0
  343. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/harness/envelopes/task_envelope.json +20 -0
  344. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/harness/runner.py +303 -0
  345. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/telemetry/logger.py +112 -0
  346. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/templates/compliance/README.md +18 -0
  347. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/templates/compliance/config/README.md +10 -0
  348. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/templates/compliance/gates/README.md +12 -0
  349. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/templates/compliance/gates/check_env.sh +23 -0
  350. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/templates/compliance/gates/check_paths.sh +20 -0
  351. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/templates/compliance/gates/check_platform.sh +23 -0
  352. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/templates/compliance/harness/README.md +11 -0
  353. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/templates/compliance/harness/envelopes/README.md +10 -0
  354. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/templates/compliance/installer/README.md +10 -0
  355. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/templates/compliance/installer/install.sh +125 -0
  356. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/templates/compliance/telemetry/README.md +11 -0
  357. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/README.md +20 -0
  358. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/features/README.md +36 -0
  359. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/features/SKILL-template.md +14 -0
  360. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/git_ops/README.md +25 -0
  361. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/git_ops/SKILL.md +13 -0
  362. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/git_ops/create_branch.sh +6 -0
  363. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/git_ops/safe_commit.sh +9 -0
  364. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/git_ops/sync_upstream.sh +6 -0
  365. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/lifecycle/README.md +27 -0
  366. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/lifecycle/SKILL.md +15 -0
  367. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/lifecycle/smoke_test.sh +12 -0
  368. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/lifecycle/start.sh +19 -0
  369. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/lifecycle/status.sh +12 -0
  370. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/lifecycle/stop.sh +15 -0
  371. package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/README.md +18 -0
  372. package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/config/README.md +10 -0
  373. package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/config/core_four.yaml +13 -0
  374. package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/gates/README.md +12 -0
  375. package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/gates/check_danger.sh +102 -0
  376. package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/gates/check_env.sh +23 -0
  377. package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/gates/check_paths.sh +20 -0
  378. package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/gates/check_platform.sh +23 -0
  379. package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/gates/check_wiring.sh +69 -0
  380. package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/harness/README.md +11 -0
  381. package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/harness/envelopes/README.md +10 -0
  382. package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/harness/envelopes/result_envelope.json +14 -0
  383. package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/harness/envelopes/task_envelope.json +20 -0
  384. package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/harness/runner.py +303 -0
  385. package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/installer/README.md +10 -0
  386. package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/installer/install.sh +125 -0
  387. package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/telemetry/README.md +11 -0
  388. package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/telemetry/logger.py +112 -0
  389. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/BEST_PRACTICES/README.md +14 -0
  390. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/BEST_PRACTICES/api.md +17 -0
  391. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/BEST_PRACTICES/compliance-wiring.md +155 -0
  392. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/BEST_PRACTICES/frontend.md +15 -0
  393. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/CI_CD/README.md +11 -0
  394. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/CI_CD/deployment/README.md +23 -0
  395. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/CI_CD/deployment/clients/README.md +16 -0
  396. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/CI_CD/deployment/clients/template-README.md +11 -0
  397. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/CI_CD/deployment/deploy.sh +49 -0
  398. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/CI_CD/deployment/envs/README.md +12 -0
  399. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/CI_CD/deployment/envs/development.env.example +17 -0
  400. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/CI_CD/deployment/envs/production.env.example +17 -0
  401. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/CI_CD/deployment/envs/staging.env.example +17 -0
  402. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/CI_CD/deployment/tenants/README.md +11 -0
  403. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/CI_CD/deployment/tenants/template.env.example +8 -0
  404. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/DEVELOPER_SETUP/README.md +26 -0
  405. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/DEVELOPER_SETUP/agent.md +7 -0
  406. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/DEVELOPER_SETUP/developers/dev-1.md +20 -0
  407. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/DEVELOPER_SETUP/linux.md +9 -0
  408. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/DEVELOPER_SETUP/macos.md +10 -0
  409. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/DEVELOPER_SETUP/shared.md +19 -0
  410. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/DEVELOPER_SETUP/windows.md +14 -0
  411. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/HOSTING/README.md +25 -0
  412. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/HOSTING/_template.md +41 -0
  413. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/HOSTING/hosting-1.md +41 -0
  414. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/README.md +21 -0
  415. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/RUNBOOK/README.md +15 -0
  416. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/RUNBOOK/deployment.md +19 -0
  417. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/RUNBOOK/disaster-recovery.md +15 -0
  418. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/RUNBOOK/ops-tasks.md +14 -0
  419. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/features/README.md +16 -0
  420. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/project-skills.md +146 -0
  421. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/research/README.md +11 -0
  422. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/research/software-compliance-research.md +269 -0
  423. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/working-with-agents.md +260 -0
  424. package/.agents/skills/nsr-compliance/assets/nsr/assets/root/AGENTS.md +38 -0
  425. package/.agents/skills/nsr-compliance/assets/nsr/assets/root/ARCHITECTURE.md +36 -0
  426. package/.agents/skills/nsr-compliance/assets/nsr/assets/root/BEST_PRACTICES.md +27 -0
  427. package/.agents/skills/nsr-compliance/assets/nsr/assets/root/CI_CD.md +23 -0
  428. package/.agents/skills/nsr-compliance/assets/nsr/assets/root/FEATURES.md +15 -0
  429. package/.agents/skills/nsr-compliance/assets/nsr/assets/root/README.md +22 -0
  430. package/.agents/skills/nsr-compliance/assets/nsr/assets/root/RULES.md +37 -0
  431. package/.agents/skills/nsr-compliance/assets/nsr/assets/root/STRUCTURE.md +45 -0
  432. package/.agents/skills/nsr-compliance/assets/nsr/assets/root/TECH_STACK.md +28 -0
  433. package/.agents/skills/nsr-compliance/assets/nsr/scaffold-spec.md +63 -0
  434. package/.agents/skills/nsr-compliance/assets/nsr/scripts/scaffold.sh +103 -0
  435. package/.agents/skills/nsr-compliance/assets/nsr/scripts/validate.sh +52 -0
  436. package/.agents/skills/nsr-compliance/assets/nsrcompliance/SKILL.md +71 -0
  437. package/.agents/skills/nsr-compliance/assets/nsrcompliance/config/core_four.yaml +16 -0
  438. package/.agents/skills/nsr-compliance/assets/nsrcompliance/gates/check_danger.sh +102 -0
  439. package/.agents/skills/nsr-compliance/assets/nsrcompliance/gates/check_wiring.sh +69 -0
  440. package/.agents/skills/nsr-compliance/assets/nsrcompliance/gates/validate_code.sh +29 -0
  441. package/.agents/skills/nsr-compliance/assets/nsrcompliance/gates/verify_docs.py +66 -0
  442. package/.agents/skills/nsr-compliance/assets/nsrcompliance/generate.sh +109 -0
  443. package/.agents/skills/nsr-compliance/assets/nsrcompliance/harness/envelopes/result_envelope.json +14 -0
  444. package/.agents/skills/nsr-compliance/assets/nsrcompliance/harness/envelopes/task_envelope.json +20 -0
  445. package/.agents/skills/nsr-compliance/assets/nsrcompliance/harness/runner.py +303 -0
  446. package/.agents/skills/nsr-compliance/assets/nsrcompliance/telemetry/logger.py +112 -0
  447. package/.agents/skills/nsr-compliance/assets/nsrcompliance/templates/compliance/README.md +18 -0
  448. package/.agents/skills/nsr-compliance/assets/nsrcompliance/templates/compliance/config/README.md +10 -0
  449. package/.agents/skills/nsr-compliance/assets/nsrcompliance/templates/compliance/gates/README.md +12 -0
  450. package/.agents/skills/nsr-compliance/assets/nsrcompliance/templates/compliance/gates/check_env.sh +23 -0
  451. package/.agents/skills/nsr-compliance/assets/nsrcompliance/templates/compliance/gates/check_paths.sh +20 -0
  452. package/.agents/skills/nsr-compliance/assets/nsrcompliance/templates/compliance/gates/check_platform.sh +23 -0
  453. package/.agents/skills/nsr-compliance/assets/nsrcompliance/templates/compliance/harness/README.md +11 -0
  454. package/.agents/skills/nsr-compliance/assets/nsrcompliance/templates/compliance/harness/envelopes/README.md +10 -0
  455. package/.agents/skills/nsr-compliance/assets/nsrcompliance/templates/compliance/installer/README.md +10 -0
  456. package/.agents/skills/nsr-compliance/assets/nsrcompliance/templates/compliance/installer/install.sh +125 -0
  457. package/.agents/skills/nsr-compliance/assets/nsrcompliance/templates/compliance/telemetry/README.md +11 -0
  458. package/.agents/skills/pr-ops/README.md +3 -0
  459. package/.agents/skills/pr-ops/SKILL.md +109 -0
  460. package/.agents/skills/ratatoskr-a2a/README.md +3 -0
  461. package/.agents/skills/ratatoskr-a2a/SKILL.md +148 -0
  462. package/.agents/skills/rules-check-drift/SKILL.md +81 -0
  463. package/.agents/skills/saga-bearings/README.md +3 -0
  464. package/.agents/skills/saga-bearings/SKILL.md +26 -0
  465. package/.agents/skills/saga-bearings/assets/bearings.md +161 -0
  466. package/.agents/skills/saga-bearings/assets/board-template.html +731 -0
  467. package/.agents/skills/saga-bearings/assets/recap.md +56 -0
  468. package/.agents/skills/syn-recovery/README.md +3 -0
  469. package/.agents/skills/syn-recovery/SKILL.md +54 -0
  470. package/.agents/skills/tyr-check/README.md +3 -0
  471. package/.agents/skills/tyr-check/SKILL.md +218 -0
  472. package/.agents/skills/urdh-hold/README.md +3 -0
  473. package/.agents/skills/urdh-hold/SKILL.md +25 -0
  474. package/.agents/skills/urdh-hold/assets/decisions.md +15 -0
  475. package/.agents/skills/urdh-hold/assets/hold.md +54 -0
  476. package/.agents/skills/vor-diagnostics/README.md +3 -0
  477. package/.agents/skills/vor-diagnostics/SKILL.md +68 -0
  478. package/.agents/skills/ymir-host/README.md +3 -0
  479. package/.agents/skills/ymir-host/SKILL.md +34 -0
  480. package/.agents/skills/ymir-host/assets/desktop.md +36 -0
  481. package/.agents/skills/ymir-host/assets/omarchy.md +297 -0
  482. package/.agents/skills/ymir-host/assets/runbooks.md +15 -0
  483. package/.agents/skills/ymir-host/assets/thjazi.md +220 -0
  484. package/.agents/skills/ymir-host/assets/update.md +64 -0
  485. package/.agents/tests/assets/board-render-harness.mjs +117 -0
  486. package/.agents/tests/cmux-test-safety.sh +51 -0
  487. package/.agents/tests/fixtures.sh +291 -0
  488. package/.agents/tests/fm-afk-inject-e2e.test.sh +428 -0
  489. package/.agents/tests/fm-afk-inject-herdr-e2e.test.sh +536 -0
  490. package/.agents/tests/fm-afk-launch.test.sh +961 -0
  491. package/.agents/tests/fm-afk-pi-herdr-return-e2e.test.sh +297 -0
  492. package/.agents/tests/fm-afk-return.test.sh +283 -0
  493. package/.agents/tests/fm-arm-pretool-check.test.sh +482 -0
  494. package/.agents/tests/fm-ask-user-authority.test.sh +42 -0
  495. package/.agents/tests/fm-backend-autodetect-smoke.test.sh +176 -0
  496. package/.agents/tests/fm-backend-cmux-smoke.test.sh +188 -0
  497. package/.agents/tests/fm-backend-cmux.test.sh +1166 -0
  498. package/.agents/tests/fm-backend-herdr-eventwait-smoke.test.sh +136 -0
  499. package/.agents/tests/fm-backend-herdr-eventwait.test.py +106 -0
  500. package/.agents/tests/fm-backend-herdr-focus-flash-e2e.test.sh +409 -0
  501. package/.agents/tests/fm-backend-herdr-launcher-workspace-e2e.test.sh +437 -0
  502. package/.agents/tests/fm-backend-herdr-presentation-e2e.test.sh +1424 -0
  503. package/.agents/tests/fm-backend-herdr-prune-safety-e2e.test.sh +180 -0
  504. package/.agents/tests/fm-backend-herdr-respawn-idem-e2e.test.sh +182 -0
  505. package/.agents/tests/fm-backend-herdr-smoke.test.sh +355 -0
  506. package/.agents/tests/fm-backend-herdr-workspace-per-home-e2e.test.sh +251 -0
  507. package/.agents/tests/fm-backend-herdr.test.sh +4606 -0
  508. package/.agents/tests/fm-backend-orca.test.sh +1357 -0
  509. package/.agents/tests/fm-backend-tmux-smoke.test.sh +173 -0
  510. package/.agents/tests/fm-backend-zellij-smoke.test.sh +205 -0
  511. package/.agents/tests/fm-backend-zellij.test.sh +1360 -0
  512. package/.agents/tests/fm-backend.test.sh +1142 -0
  513. package/.agents/tests/fm-backlog-atomicity.test.sh +2302 -0
  514. package/.agents/tests/fm-backlog-handoff.test.sh +1360 -0
  515. package/.agents/tests/fm-bearings-board-render.test.sh +135 -0
  516. package/.agents/tests/fm-bearings-board.test.sh +414 -0
  517. package/.agents/tests/fm-bearings-snapshot.test.sh +1990 -0
  518. package/.agents/tests/fm-bootstrap-network-parallel.test.sh +316 -0
  519. package/.agents/tests/fm-bootstrap.test.sh +1178 -0
  520. package/.agents/tests/fm-branch-supervision.test.sh +552 -0
  521. package/.agents/tests/fm-brief.test.sh +785 -0
  522. package/.agents/tests/fm-busy-adapter-wiring.test.sh +324 -0
  523. package/.agents/tests/fm-busy-state.test.sh +465 -0
  524. package/.agents/tests/fm-calm-pi-extension.test.sh +3979 -0
  525. package/.agents/tests/fm-captain-hold-lifecycle.test.sh +1187 -0
  526. package/.agents/tests/fm-cd-pretool-check.test.sh +400 -0
  527. package/.agents/tests/fm-check-unregister.test.sh +198 -0
  528. package/.agents/tests/fm-classify-corr-token.test.sh +543 -0
  529. package/.agents/tests/fm-classify-decision-key.test.sh +340 -0
  530. package/.agents/tests/fm-claude-stop-autoarm-live-e2e.test.sh +164 -0
  531. package/.agents/tests/fm-claude-stop-autoarm.test.sh +1190 -0
  532. package/.agents/tests/fm-cmux-claude-composer-live-e2e.test.sh +110 -0
  533. package/.agents/tests/fm-codex-continuity-live-e2e.test.sh +55 -0
  534. package/.agents/tests/fm-composer-ghost.test.sh +716 -0
  535. package/.agents/tests/fm-composer-lib.test.sh +666 -0
  536. package/.agents/tests/fm-composer-matrix-live-e2e.test.sh +220 -0
  537. package/.agents/tests/fm-control-herdr-smoke.test.sh +149 -0
  538. package/.agents/tests/fm-control-relaunch.test.sh +1530 -0
  539. package/.agents/tests/fm-control.test.sh +910 -0
  540. package/.agents/tests/fm-crew-state.test.sh +1610 -0
  541. package/.agents/tests/fm-cursor-harness.test.sh +404 -0
  542. package/.agents/tests/fm-cursor-primary-live-e2e.test.sh +217 -0
  543. package/.agents/tests/fm-cursor-primary.test.sh +708 -0
  544. package/.agents/tests/fm-daemon.test.sh +2740 -0
  545. package/.agents/tests/fm-documentation-audiences.test.sh +141 -0
  546. package/.agents/tests/fm-ensure-agents-md.test.sh +371 -0
  547. package/.agents/tests/fm-extension-binding.test.sh +2187 -0
  548. package/.agents/tests/fm-fleet-snapshot-view.test.sh +816 -0
  549. package/.agents/tests/fm-fleet-sync.test.sh +721 -0
  550. package/.agents/tests/fm-gate-refuse.test.sh +356 -0
  551. package/.agents/tests/fm-gitignore-config.test.sh +89 -0
  552. package/.agents/tests/fm-gotmp.test.sh +230 -0
  553. package/.agents/tests/fm-grok-continuity-live-e2e.test.sh +112 -0
  554. package/.agents/tests/fm-grok-harness.test.sh +117 -0
  555. package/.agents/tests/fm-grok-stop-live-e2e.test.sh +219 -0
  556. package/.agents/tests/fm-guard-stale-banner.test.sh +709 -0
  557. package/.agents/tests/fm-harness-adapter-instructions-live-e2e.test.sh +134 -0
  558. package/.agents/tests/fm-harness-adapter-references.test.sh +30 -0
  559. package/.agents/tests/fm-harness-liveness-drift-live-e2e.test.sh +151 -0
  560. package/.agents/tests/fm-herdr-lab.test.sh +243 -0
  561. package/.agents/tests/fm-herdr-session-cleanup-e2e.test.sh +146 -0
  562. package/.agents/tests/fm-herdr-session-cleanup.test.sh +329 -0
  563. package/.agents/tests/fm-herdr-submit-confirm-live-e2e.test.sh +125 -0
  564. package/.agents/tests/fm-herdr-version-floor-live-e2e.test.sh +144 -0
  565. package/.agents/tests/fm-home-summary-refresh.test.sh +971 -0
  566. package/.agents/tests/fm-inactive-reconcile.test.sh +485 -0
  567. package/.agents/tests/fm-kimi-harness.test.sh +687 -0
  568. package/.agents/tests/fm-lint-workflows.test.sh +532 -0
  569. package/.agents/tests/fm-lint.test.sh +1023 -0
  570. package/.agents/tests/fm-muse-harness.test.sh +932 -0
  571. package/.agents/tests/fm-muse-signals-live-e2e.test.sh +205 -0
  572. package/.agents/tests/fm-no-mistakes-required.test.sh +72 -0
  573. package/.agents/tests/fm-on.test.sh +507 -0
  574. package/.agents/tests/fm-opencode-primary-live-e2e.test.sh +357 -0
  575. package/.agents/tests/fm-operational-input.test.sh +160 -0
  576. package/.agents/tests/fm-peek-remote.test.sh +110 -0
  577. package/.agents/tests/fm-pending-reply.test.sh +1288 -0
  578. package/.agents/tests/fm-pi-branch-extension.test.sh +3182 -0
  579. package/.agents/tests/fm-pi-branch-live-e2e.test.sh +527 -0
  580. package/.agents/tests/fm-pi-primary-live-e2e.test.sh +345 -0
  581. package/.agents/tests/fm-pi-primary-types.test.sh +68 -0
  582. package/.agents/tests/fm-pi-watch-extension.test.sh +2843 -0
  583. package/.agents/tests/fm-pr-check-security.test.sh +2140 -0
  584. package/.agents/tests/fm-pr-merge.test.sh +2135 -0
  585. package/.agents/tests/fm-procevent-quota.test.sh +225 -0
  586. package/.agents/tests/fm-procevent-when.test.sh +406 -0
  587. package/.agents/tests/fm-procevent.test.sh +1745 -0
  588. package/.agents/tests/fm-project-origin.test.sh +110 -0
  589. package/.agents/tests/fm-public-followup.test.sh +2272 -0
  590. package/.agents/tests/fm-quota-array-dispatch-live-e2e.test.sh +526 -0
  591. package/.agents/tests/fm-quota-choose.test.sh +626 -0
  592. package/.agents/tests/fm-remote-backlog-handoff.test.sh +453 -0
  593. package/.agents/tests/fm-remote-doctor.test.sh +625 -0
  594. package/.agents/tests/fm-remote-entrypoint.test.sh +59 -0
  595. package/.agents/tests/fm-remote-job-orphan-reap.test.sh +205 -0
  596. package/.agents/tests/fm-remote-job.test.sh +768 -0
  597. package/.agents/tests/fm-remote-reply.test.sh +520 -0
  598. package/.agents/tests/fm-remote-secondmate-lifecycle-e2e.test.sh +1256 -0
  599. package/.agents/tests/fm-remote-secondmate-parent-binding.test.sh +326 -0
  600. package/.agents/tests/fm-remote-secondmate-trace-context.test.sh +310 -0
  601. package/.agents/tests/fm-remote-transport-lanes.test.sh +431 -0
  602. package/.agents/tests/fm-review-diff.test.sh +176 -0
  603. package/.agents/tests/fm-secondmate-harness.test.sh +2600 -0
  604. package/.agents/tests/fm-secondmate-lifecycle-e2e.test.sh +259 -0
  605. package/.agents/tests/fm-secondmate-liveness.test.sh +559 -0
  606. package/.agents/tests/fm-secondmate-reconcile.test.sh +732 -0
  607. package/.agents/tests/fm-secondmate-safety.test.sh +2972 -0
  608. package/.agents/tests/fm-secondmate-sync.test.sh +913 -0
  609. package/.agents/tests/fm-send-inbox-doorbell-live-e2e.test.sh +206 -0
  610. package/.agents/tests/fm-send-inbox.test.sh +352 -0
  611. package/.agents/tests/fm-send-popup-settle.test.sh +167 -0
  612. package/.agents/tests/fm-send-remote-delivery.test.sh +796 -0
  613. package/.agents/tests/fm-send-resolve-key.test.sh +551 -0
  614. package/.agents/tests/fm-send-secondmate-marker-herdr-e2e.test.sh +187 -0
  615. package/.agents/tests/fm-send-secondmate-marker.test.sh +278 -0
  616. package/.agents/tests/fm-send-settle.test.sh +147 -0
  617. package/.agents/tests/fm-send-strict.test.sh +241 -0
  618. package/.agents/tests/fm-session-lock-ancestry.test.sh +365 -0
  619. package/.agents/tests/fm-session-start.test.sh +2515 -0
  620. package/.agents/tests/fm-sessionstart-hook-live-e2e.test.sh +631 -0
  621. package/.agents/tests/fm-sessionstart-instruction-refresh-live-e2e.test.sh +230 -0
  622. package/.agents/tests/fm-sessionstart-nudge.test.sh +1039 -0
  623. package/.agents/tests/fm-shared-captain-inheritance.test.sh +404 -0
  624. package/.agents/tests/fm-spawn-batch.test.sh +148 -0
  625. package/.agents/tests/fm-spawn-dispatch-profile.test.sh +827 -0
  626. package/.agents/tests/fm-spawn-pool-base-freshen.test.sh +440 -0
  627. package/.agents/tests/fm-spawn-worktree-settle.test.sh +147 -0
  628. package/.agents/tests/fm-startup-memory-budget.test.sh +333 -0
  629. package/.agents/tests/fm-startup-network.test.sh +712 -0
  630. package/.agents/tests/fm-stow-cascade.test.sh +370 -0
  631. package/.agents/tests/fm-subagent-pretool-check.test.sh +291 -0
  632. package/.agents/tests/fm-supervision-events.test.sh +156 -0
  633. package/.agents/tests/fm-supervision-instructions.test.sh +189 -0
  634. package/.agents/tests/fm-tangle-guard.test.sh +277 -0
  635. package/.agents/tests/fm-task-delivery.test.sh +420 -0
  636. package/.agents/tests/fm-task-inbox.test.sh +513 -0
  637. package/.agents/tests/fm-teardown-endpoint-safety.test.sh +374 -0
  638. package/.agents/tests/fm-teardown.test.sh +2665 -0
  639. package/.agents/tests/fm-test-fixture-cleanup.test.sh +172 -0
  640. package/.agents/tests/fm-test-fixtures.test.sh +132 -0
  641. package/.agents/tests/fm-test-isolation-proof.test.sh +278 -0
  642. package/.agents/tests/fm-test-run.test.sh +1196 -0
  643. package/.agents/tests/fm-tmux-agent-liveness.test.sh +354 -0
  644. package/.agents/tests/fm-tmux-submit-busy.test.sh +340 -0
  645. package/.agents/tests/fm-tool-update-check.test.sh +1044 -0
  646. package/.agents/tests/fm-trace-context-lib.test.sh +253 -0
  647. package/.agents/tests/fm-trace-context-spawn.test.sh +581 -0
  648. package/.agents/tests/fm-transition-lib.test.sh +54 -0
  649. package/.agents/tests/fm-turnend-guard.test.sh +1822 -0
  650. package/.agents/tests/fm-update.test.sh +304 -0
  651. package/.agents/tests/fm-vendor-auth-probe.test.sh +395 -0
  652. package/.agents/tests/fm-voice-relay.test.sh +4741 -0
  653. package/.agents/tests/fm-wake-daemon-lifecycle-e2e.test.sh +171 -0
  654. package/.agents/tests/fm-wake-drain-open-decisions-cursor.test.sh +356 -0
  655. package/.agents/tests/fm-wake-drain-open-decisions.test.sh +226 -0
  656. package/.agents/tests/fm-wake-drain-unread-status.test.sh +379 -0
  657. package/.agents/tests/fm-wake-queue.test.sh +1224 -0
  658. package/.agents/tests/fm-watch-arm.test.sh +821 -0
  659. package/.agents/tests/fm-watch-checkpoint.test.sh +87 -0
  660. package/.agents/tests/fm-watch-recovery-loop.test.sh +223 -0
  661. package/.agents/tests/fm-watch-triage.test.sh +3889 -0
  662. package/.agents/tests/fm-watcher-lock.test.sh +1133 -0
  663. package/.agents/tests/fm-x-mode.test.sh +3042 -0
  664. package/.agents/tests/gleipnir-guard-pretool-check.test.sh +51 -0
  665. package/.agents/tests/gleipnir-machine-lock.test.sh +130 -0
  666. package/.agents/tests/herdr-test-safety.sh +42 -0
  667. package/.agents/tests/lib.sh +315 -0
  668. package/.agents/tests/remote-herdr-fixture.sh +125 -0
  669. package/.agents/tests/secondmate-helpers.sh +193 -0
  670. package/.agents/tests/smoke.test.sh +93 -0
  671. package/.agents/tests/wake-helpers.sh +338 -0
  672. package/.agents/tests/zellij-test-safety.sh +60 -0
  673. package/.agents/tools/README.md +10 -0
  674. package/.agents/tools/bin/tasks-cli +267 -0
  675. package/.agents/tools/tasks-cli.ts +216 -0
  676. package/AGENTS.md +454 -0
  677. package/CHANGELOG.md +881 -0
  678. package/LICENSE +1 -1
  679. package/NOTICE +63 -0
  680. package/README.md +113 -59
  681. package/RULES/01-domains.md +51 -0
  682. package/RULES/02-agents.md +78 -0
  683. package/RULES/03-houses.md +61 -0
  684. package/RULES/04-hoard.md +65 -0
  685. package/RULES/05-platforms.md +67 -0
  686. package/RULES/06-append-only.md +58 -0
  687. package/RULES/07-config.md +45 -0
  688. package/RULES/08-delivery-gate.md +75 -0
  689. package/RULES/README.md +20 -0
  690. package/bin/README.md +107 -0
  691. package/bin/a2a-mcp.sh +98 -0
  692. package/bin/a2a-serve.py +103 -0
  693. package/bin/a2a-serve.sh +8 -0
  694. package/bin/a2a-talk.sh +50 -0
  695. package/bin/a2abridge-ensure.sh +108 -0
  696. package/bin/agent-run.sh +43 -0
  697. package/bin/agents-config.sh +233 -0
  698. package/bin/apodex-smoke-test.sh +92 -0
  699. package/bin/bifrost-bridge.sh +112 -0
  700. package/bin/bifrost-ingress.sh +77 -0
  701. package/bin/bootstrap-macos.sh +107 -0
  702. package/bin/bootstrap-windows.ps1 +112 -0
  703. package/bin/branch-guard.sh +64 -0
  704. package/bin/brokk +73 -0
  705. package/bin/brokk-classify-lib.sh +1770 -0
  706. package/bin/brokk-lease-lib.sh +222 -0
  707. package/bin/brokk-lease.sh +193 -0
  708. package/bin/brokk-lint.sh +65 -0
  709. package/bin/brokk-send.sh +38 -0
  710. package/bin/brokk-update.sh +9 -0
  711. package/bin/brokk-wake-grant.sh +127 -0
  712. package/bin/brokk-wake-lib.sh +1849 -0
  713. package/bin/changelog-guard.sh +165 -0
  714. package/bin/crash-sense.sh +160 -0
  715. package/bin/desktop-place.sh +224 -0
  716. package/bin/docs-guard.sh +39 -0
  717. package/bin/editor-place.sh +207 -0
  718. package/bin/eindri-acclaim.sh +34 -0
  719. package/bin/eindri-control.sh +28 -0
  720. package/bin/eindri-role.sh +83 -0
  721. package/bin/eindri-seen.sh +41 -0
  722. package/bin/eindri-send.sh +15 -0
  723. package/bin/eindri-start.sh +85 -0
  724. package/bin/eindri-watch.sh +57 -0
  725. package/bin/einherjar-spawn.sh +576 -0
  726. package/bin/eir-doctor.sh +112 -0
  727. package/bin/erindi-brief.sh +271 -0
  728. package/bin/fleet-apply.sh +68 -0
  729. package/bin/github-deploy.sh +81 -0
  730. package/bin/gjallarhorn-expose.sh +102 -0
  731. package/bin/gjallarhorn-notify.sh +58 -0
  732. package/bin/gjallarhorn-purge.sh +54 -0
  733. package/bin/gjallarhorn-tunnel.sh +72 -0
  734. package/bin/gleipnir-lock-lib.sh +284 -0
  735. package/bin/groa-update.sh +122 -0
  736. package/bin/hall-snapshot.sh +92 -0
  737. package/bin/hamr-harness.sh +246 -0
  738. package/bin/heimdall-ssh-keys.sh +374 -0
  739. package/bin/herdr-agents.py +44 -0
  740. package/bin/herdr-ensure.sh +104 -0
  741. package/bin/herdr-run.sh +431 -0
  742. package/bin/hermes-ensure.sh +88 -0
  743. package/bin/hoard-lib.sh +25 -0
  744. package/bin/hodd.sh +78 -0
  745. package/bin/host-sense.sh +199 -0
  746. package/bin/huginn-research-worker.sh +108 -0
  747. package/bin/local-model-lock.sh +51 -0
  748. package/bin/mimir-bridge.py +226 -0
  749. package/bin/mimir-bridge.sh +87 -0
  750. package/bin/mimir-ingest.sh +131 -0
  751. package/bin/mimir.sh +113 -0
  752. package/bin/mjollnir-webhook.sh +59 -0
  753. package/bin/mjollnir.sh +85 -0
  754. package/bin/model-hardware.sh +82 -0
  755. package/bin/model-resolve.sh +147 -0
  756. package/bin/models-detect.sh +178 -0
  757. package/bin/nornir-cron-start.sh +162 -0
  758. package/bin/nornir-job-daily-briefing.sh +191 -0
  759. package/bin/nornir-job-git-sync.sh +152 -0
  760. package/bin/nornir-job-memory-housekeeping.sh +114 -0
  761. package/bin/nornir-job-observer.sh +166 -0
  762. package/bin/nornir-job-skillopt-sleep.sh +76 -0
  763. package/bin/omarchy-hook-install.sh +60 -0
  764. package/bin/omarchy-install.sh +148 -0
  765. package/bin/omarchy-plugins.sh +174 -0
  766. package/bin/omarchy-sense.sh +149 -0
  767. package/bin/perm-guard.sh +30 -0
  768. package/bin/pi-agent.sh +68 -0
  769. package/bin/pi-ensure.sh +77 -0
  770. package/bin/pi-local.sh +37 -0
  771. package/bin/pi-seat.sh +63 -0
  772. package/bin/prereq-ensure.sh +166 -0
  773. package/bin/private-guard.sh +38 -0
  774. package/bin/project-git.sh +55 -0
  775. package/bin/public-guard.sh +70 -0
  776. package/bin/ratatoskr.sh +49 -0
  777. package/bin/realm-lib.sh +32 -0
  778. package/bin/repo-scrub.sh +62 -0
  779. package/bin/rodd-operational-input.sh +250 -0
  780. package/bin/runes-append.sh +198 -0
  781. package/bin/saga-session-start.sh +172 -0
  782. package/bin/saga-sessionstart-run.sh +79 -0
  783. package/bin/saga-wake-drain.sh +41 -0
  784. package/bin/secret-guard.sh +63 -0
  785. package/bin/sessrumnir-ensure.sh +100 -0
  786. package/bin/sessrumnir-sync.sh +237 -0
  787. package/bin/sessrumnir.sh +91 -0
  788. package/bin/skillopt-setup.sh +76 -0
  789. package/bin/skuld-branch-outcome.sh +203 -0
  790. package/bin/skuld-branch-prompt.sh +101 -0
  791. package/bin/smidja-bootstrap.sh +90 -0
  792. package/bin/smidja-observe.sh +86 -0
  793. package/bin/syn-arm-pretool-check.sh +57 -0
  794. package/bin/syn-asset-pretool-check.sh +76 -0
  795. package/bin/syn-cd-pretool-check.sh +28 -0
  796. package/bin/syn-guard-pretool-check.sh +83 -0
  797. package/bin/syn-turnend-guard.sh +36 -0
  798. package/bin/syn-watch-arm.sh +99 -0
  799. package/bin/tailscale-sync.sh +190 -0
  800. package/bin/telegram-bot.sh +69 -0
  801. package/bin/toolchain.sh +74 -0
  802. package/bin/utgard.sh +111 -0
  803. package/bin/valhalla.sh +115 -0
  804. package/bin/valknut-load.sh +260 -0
  805. package/bin/vor-crew-state.sh +124 -0
  806. package/bin/wedge-notify.sh +47 -0
  807. package/bin/workspace-provision.sh +80 -0
  808. package/bin/workspace-rag.sh +150 -0
  809. package/bin/wyrd-db.sh +47 -0
  810. package/bin/yggdrasil.sh +152 -0
  811. package/bin/ymir-edit.sh +145 -0
  812. package/bin/ymir-install.sh +641 -0
  813. package/bin/ymir-invite.sh +93 -0
  814. package/bin/ymir-isolation.sh +88 -0
  815. package/bin/ymir-migrate.sh +61 -0
  816. package/bin/ymir-platform.sh +348 -0
  817. package/bin/ymir-say.sh +97 -0
  818. package/bin/ymir-setup-auth.sh +108 -0
  819. package/bin/ymir-validate.sh +173 -0
  820. package/bin/ymir.js +74 -16
  821. package/docs/Architecture.md +339 -0
  822. package/docs/design.md +351 -0
  823. package/docs/installations/README.md +63 -0
  824. package/docs/installations/ymir-home.md +93 -0
  825. package/docs/integration.md +105 -0
  826. package/docs/lore.md +1056 -0
  827. package/docs/research/agent-harnesses.md +290 -0
  828. package/docs/runbooks/README.md +19 -0
  829. package/docs/runbooks/a2a.md +91 -0
  830. package/docs/runbooks/agent-permissions.md +99 -0
  831. package/docs/runbooks/agents.md +57 -0
  832. package/docs/runbooks/models.md +69 -0
  833. package/docs/runbooks/realm-onboarding.md +60 -0
  834. package/docs/runbooks/secrets-and-hoard.md +60 -0
  835. package/docs/runbooks/updates-and-migrations.md +110 -0
  836. package/docs/runbooks/worktrees-isolation.md +72 -0
  837. package/docs/session-start.md +105 -0
  838. package/docs/skillopt-integration.md +321 -0
  839. package/docs/sync-system.md +66 -0
  840. package/docs/workspaces.md +104 -0
  841. package/docs/ymir-rut.md +260 -0
  842. package/package.json +44 -14
  843. package/packaging/build.sh +74 -0
  844. package/packaging/macos/Ymir Installer.command +43 -0
  845. package/packaging/windows/ymir-setup.nsi +74 -0
  846. package/scripts/README.md +22 -0
  847. package/scripts/electron.sh +210 -0
  848. package/scripts/start.sh +167 -0
  849. package/scripts/stop.sh +113 -0
  850. package/install.sh +0 -35
@@ -0,0 +1,1360 @@
1
+ #!/usr/bin/env bash
2
+ # tests/fm-backlog-handoff.test.sh - full item-block handoff (header + indented body).
3
+ #
4
+ # The happy single-line path and broad safety refusals live in the secondmate
5
+ # lifecycle and safety suites. This file owns focused delegated-handoff
6
+ # regressions: the multi-line body contract and registry home parsing edge cases.
7
+ set -u
8
+
9
+ # shellcheck source=tests/secondmate-helpers.sh disable=SC1091
10
+ . "$(dirname "${BASH_SOURCE[0]}")/secondmate-helpers.sh"
11
+
12
+ # The move is delegated to `tasks-axi mv`, so this suite exercises the real
13
+ # binary. Skip cleanly when it is absent (matching the backend smoke suites).
14
+ command -v tasks-axi >/dev/null 2>&1 || { echo "skip: tasks-axi not found (required by the delegated handoff path)"; exit 0; }
15
+
16
+ TMP_ROOT=$(fm_test_tmproot fm-backlog-handoff)
17
+ HANDOFF_FAKEBIN=$(make_fake_tmux "$TMP_ROOT/default-fake")
18
+ export PATH="$HANDOFF_FAKEBIN:$PATH"
19
+ export FM_FAKE_TMUX_WINDOW='firstmate:fm-design'
20
+ export FM_FAKE_TMUX_LOG="$TMP_ROOT/default-tmux.log"
21
+ export FM_FAKE_TMUX_CAPTURE="$TMP_ROOT/default-fake/pane.txt"
22
+ export FM_SEND_SETTLE=0 FM_SEND_SLEEP=0 FM_SEND_RETRIES=1
23
+
24
+ setup_homes() {
25
+ local home=$1 subhome=$2 id=${3:-design}
26
+ mkdir -p "$home/data" "$home/state"
27
+ seed_secondmate_home_marker "$subhome" "$id"
28
+ local sub_abs
29
+ sub_abs=$(cd "$subhome" && pwd -P)
30
+ printf -- '- %s - feature work (home: %s; scope: feature work; projects: alpha; added 2026-07-09)\n' \
31
+ "$id" "$sub_abs" > "$home/data/secondmates.md"
32
+ cat > "$home/state/$id.meta" <<EOF
33
+ window=firstmate:fm-$id
34
+ kind=secondmate
35
+ harness=claude
36
+ backend=tmux
37
+ home=$sub_abs
38
+ worktree=$sub_abs
39
+ EOF
40
+ }
41
+
42
+ inbox_body_stream() { # <state-dir> <task-id>
43
+ local rec
44
+ for rec in "$1/$2.inbox"/*.msg; do
45
+ [ -f "$rec" ] || continue
46
+ bash -c '. "$1"; fm_task_inbox_body "$2"' _ \
47
+ "$ROOT/bin/fm-task-inbox-lib.sh" "$rec"
48
+ done
49
+ }
50
+
51
+ inbox_record_count() { # <state-dir> <task-id>
52
+ find "$1/$2.inbox" -maxdepth 1 -type f -name '*.msg' 2>/dev/null | wc -l | tr -d '[:space:]'
53
+ }
54
+
55
+ doorbell_count() { # <backend-log>
56
+ grep -cF 'Firstmate instruction waiting:' "$1" 2>/dev/null || true
57
+ }
58
+
59
+ # A live local receiver gets the routed-work instruction through its durable
60
+ # inbox record while the endpoint receives only the constant doorbell.
61
+ test_handoff_wakes_live_local_receiver() {
62
+ local home="$TMP_ROOT/live-wake-main" sub="$TMP_ROOT/live-wake-sub" fakebin out wake_count
63
+ setup_homes "$home" "$sub"
64
+ mkdir -p "$sub/state" "$sub/data"
65
+ cat > "$home/state/design.meta" <<EOF
66
+ window=firstmate:fm-design
67
+ kind=secondmate
68
+ harness=claude
69
+ backend=tmux
70
+ home=$sub
71
+ worktree=$sub
72
+ EOF
73
+ cat > "$home/data/backlog.md" <<'EOF'
74
+ ## Queued
75
+ - [ ] wake-item - routed to a live receiver (repo: alpha)
76
+
77
+ ## Done
78
+ EOF
79
+ printf '## Queued\n\n## Done\n' > "$sub/data/backlog.md"
80
+ fakebin=$(make_fake_tmux "$TMP_ROOT/live-wake-fake")
81
+ out="$TMP_ROOT/live-wake.out"
82
+ FM_HOME="$home" FM_ROOT_OVERRIDE="$ROOT" PATH="$fakebin:$PATH" \
83
+ FM_FAKE_TMUX_WINDOW='firstmate:fm-design' \
84
+ FM_FAKE_TMUX_LOG="$TMP_ROOT/live-wake-tmux.log" \
85
+ FM_FAKE_TMUX_CAPTURE="$TMP_ROOT/live-wake-fake/pane.txt" \
86
+ FM_SEND_SETTLE=0 FM_SEND_SLEEP=0 FM_SEND_RETRIES=1 \
87
+ "$ROOT/bin/fm-backlog-handoff.sh" design wake-item > "$out" 2>&1 \
88
+ || fail "handoff to a live receiver failed: $(cat "$out")"
89
+ grep -F 'wake-item' "$sub/data/backlog.md" >/dev/null \
90
+ || fail "live receiver did not receive the routed backlog item"
91
+ grep -F 'send-keys' "$TMP_ROOT/live-wake-tmux.log" >/dev/null \
92
+ || fail "handoff did not ring the live receiver endpoint"
93
+ assert_contains "$(inbox_body_stream "$home/state" design)" \
94
+ 'New routed work is in your backlog.' \
95
+ "receiver inbox did not carry the routed-work instruction"
96
+ wake_count=$(inbox_record_count "$home/state" design)
97
+ [ "$(doorbell_count "$TMP_ROOT/live-wake-tmux.log")" -eq 1 ] \
98
+ || fail "handoff did not ring exactly one constant receiver doorbell"
99
+ FM_HOME="$home" FM_ROOT_OVERRIDE="$ROOT" PATH="$fakebin:$PATH" \
100
+ FM_FAKE_TMUX_WINDOW='firstmate:fm-design' \
101
+ FM_FAKE_TMUX_LOG="$TMP_ROOT/live-wake-tmux.log" \
102
+ FM_FAKE_TMUX_CAPTURE="$TMP_ROOT/live-wake-fake/pane.txt" \
103
+ FM_SEND_SETTLE=0 FM_SEND_SLEEP=0 FM_SEND_RETRIES=1 \
104
+ "$ROOT/bin/fm-backlog-handoff.sh" design wake-item > "$TMP_ROOT/live-wake-rerun.out" 2>&1 \
105
+ || fail "idempotent successful handoff rerun failed: $(cat "$TMP_ROOT/live-wake-rerun.out")"
106
+ [ "$(inbox_record_count "$home/state" design)" -eq "$wake_count" ] \
107
+ || fail "idempotent successful handoff rerun duplicated the receiver inbox record"
108
+ [ "$(doorbell_count "$TMP_ROOT/live-wake-tmux.log")" -eq 1 ] \
109
+ || fail "idempotent successful handoff rerun duplicated the receiver doorbell"
110
+ pass "a routed handoff wakes once and a successful rerun stays idempotent"
111
+ }
112
+
113
+ test_failed_wake_retries_when_the_item_is_already_present() {
114
+ local home="$TMP_ROOT/retry-wake-main" sub="$TMP_ROOT/retry-wake-sub" out corr rc=0
115
+ setup_homes "$home" "$sub"
116
+ rm -f "$home/state/design.meta"
117
+ mkdir -p "$sub/data"
118
+ cat > "$home/data/backlog.md" <<'EOF'
119
+ ## Queued
120
+ - [ ] retry-item - wake must be retried (repo: alpha)
121
+
122
+ ## Done
123
+ EOF
124
+ printf '## Queued\n\n## Done\n' > "$sub/data/backlog.md"
125
+
126
+ out=$(FM_HOME="$home" "$ROOT/bin/fm-backlog-handoff.sh" design retry-item 2>&1) || rc=$?
127
+ [ "$rc" -ne 0 ] || fail "handoff without a receiver endpoint reported success"
128
+ assert_contains "$out" "receiver was not woken" "missing receiver failure was not observable"
129
+ assert_grep 'retry-item' "$sub/data/backlog.md" "failed wake lost the durably handed-off item"
130
+ corr=$(cut -d: -f2- "$home/state/.backlog-handoff-design.wake-pending")
131
+ assert_absent "$home/state/pending-replies/.delivery-confirmed-$corr" \
132
+ "missing endpoint was recorded as an attempted delivery"
133
+
134
+ cat > "$home/state/design.meta" <<EOF
135
+ window=firstmate:fm-design
136
+ kind=secondmate
137
+ harness=claude
138
+ backend=tmux
139
+ home=$sub
140
+ worktree=$sub
141
+ EOF
142
+ : > "$TMP_ROOT/default-tmux.log"
143
+ FM_HOME="$home" "$ROOT/bin/fm-backlog-handoff.sh" design retry-item > "$TMP_ROOT/retry-wake.out" 2>&1 \
144
+ || fail "an already-present handoff did not retry its receiver wake: $(cat "$TMP_ROOT/retry-wake.out")"
145
+ assert_contains "$(inbox_body_stream "$home/state" design)" \
146
+ 'New routed work is in your backlog.' \
147
+ "the recovery handoff did not enqueue the receiver instruction"
148
+ [ "$(doorbell_count "$TMP_ROOT/default-tmux.log")" -eq 1 ] \
149
+ || fail "the recovery handoff did not ring the receiver doorbell"
150
+ pass "a failed receiver wake is loud and retries from an already-present handoff"
151
+ }
152
+
153
+ test_known_receiver_failure_remains_retryable_after_grace() {
154
+ local home="$TMP_ROOT/known-fail-main" sub="$TMP_ROOT/known-fail-sub"
155
+ local basebin rejectbin="$TMP_ROOT/known-fail-reject" out corr rec_count rc=0
156
+ setup_homes "$home" "$sub"
157
+ mkdir -p "$sub/data" "$rejectbin"
158
+ cat > "$home/data/backlog.md" <<'EOF'
159
+ ## Queued
160
+ - [ ] known-fail - retain delivery across a failed doorbell (repo: alpha)
161
+
162
+ ## Done
163
+ EOF
164
+ printf '## Queued\n\n## Done\n' > "$sub/data/backlog.md"
165
+ basebin=$(make_fake_tmux "$TMP_ROOT/known-fail-fake")
166
+ cat > "$rejectbin/tmux" <<'SH'
167
+ #!/usr/bin/env bash
168
+ [ "${1:-}" != send-keys ] || exit 1
169
+ exec "$FM_BASE_TMUX" "$@"
170
+ SH
171
+ chmod +x "$rejectbin/tmux"
172
+
173
+ out=$(PATH="$rejectbin:$basebin:$PATH" FM_BASE_TMUX="$basebin/tmux" \
174
+ FM_HOME="$home" FM_FAKE_TMUX_WINDOW='firstmate:fm-design' \
175
+ FM_FAKE_TMUX_LOG="$TMP_ROOT/known-fail-tmux.log" \
176
+ FM_FAKE_TMUX_CAPTURE="$TMP_ROOT/known-fail-fake/pane.txt" \
177
+ "$ROOT/bin/fm-backlog-handoff.sh" design known-fail 2>&1) || rc=$?
178
+ [ "$rc" -eq 0 ] || fail "failed advisory doorbell negated durable handoff: $out"
179
+ assert_contains "$out" 'doorbell did not reach' \
180
+ "failed advisory doorbell was not reported"
181
+ assert_grep 'known-fail' "$sub/data/backlog.md" "failed doorbell lost the durable item"
182
+ assert_contains "$(inbox_body_stream "$home/state" design)" \
183
+ 'New routed work is in your backlog.' "failed doorbell lost the durable receiver instruction"
184
+ rec_count=$(inbox_record_count "$home/state" design)
185
+ [ "$rec_count" -eq 1 ] || fail "failed doorbell produced $rec_count inbox records"
186
+ corr=$(grep -l '^task_id=design$' "$home/state/pending-replies"/* 2>/dev/null | head -n 1)
187
+ [ -n "$corr" ] || fail "durable receiver instruction lost its reply expectation"
188
+ [ -n "$(grep '^delivered_epoch=' "$corr" | cut -d= -f2-)" ] \
189
+ || fail "durable enqueue was not recorded as delivered"
190
+
191
+ FM_HOME="$home" "$ROOT/bin/fm-backlog-handoff.sh" design known-fail \
192
+ > "$TMP_ROOT/known-fail-retry.out" 2>&1 \
193
+ || fail "idempotent handoff after failed doorbell failed: $(cat "$TMP_ROOT/known-fail-retry.out")"
194
+ [ "$(inbox_record_count "$home/state" design)" -eq "$rec_count" ] \
195
+ || fail "retry duplicated a receiver instruction already delivered by inbox"
196
+ pass "a failed advisory doorbell leaves the inbox delivery successful and idempotent"
197
+ }
198
+
199
+ test_known_failure_restores_retry_after_reconciliation_race() {
200
+ local home="$TMP_ROOT/reconcile-race-main" sub="$TMP_ROOT/reconcile-race-sub"
201
+ local basebin blockbin="$TMP_ROOT/reconcile-race-block" handoff i corr phase
202
+ setup_homes "$home" "$sub"
203
+ mkdir -p "$sub/data" "$blockbin"
204
+ cat > "$home/data/backlog.md" <<'EOF'
205
+ ## Queued
206
+ - [ ] reconcile-race - retry after concurrent reconciliation (repo: alpha)
207
+
208
+ ## Done
209
+ EOF
210
+ printf '## Queued\n\n## Done\n' > "$sub/data/backlog.md"
211
+ basebin=$(make_fake_tmux "$TMP_ROOT/reconcile-race-fake")
212
+ cat > "$blockbin/tmux" <<'SH'
213
+ #!/usr/bin/env bash
214
+ if [ "${1:-}" = send-keys ]; then
215
+ touch "$FM_RECONCILE_RACE_ENTERED"
216
+ while [ ! -f "$FM_RECONCILE_RACE_RELEASE" ]; do sleep 0.02; done
217
+ exit 1
218
+ fi
219
+ exec "$FM_BASE_TMUX" "$@"
220
+ SH
221
+ chmod +x "$blockbin/tmux"
222
+
223
+ PATH="$blockbin:$basebin:$PATH" FM_BASE_TMUX="$basebin/tmux" FM_HOME="$home" \
224
+ FM_RECONCILE_RACE_ENTERED="$TMP_ROOT/reconcile-race.entered" \
225
+ FM_RECONCILE_RACE_RELEASE="$TMP_ROOT/reconcile-race.release" \
226
+ FM_FAKE_TMUX_WINDOW='firstmate:fm-design' \
227
+ FM_FAKE_TMUX_LOG="$TMP_ROOT/reconcile-race-tmux.log" \
228
+ FM_FAKE_TMUX_CAPTURE="$TMP_ROOT/reconcile-race-fake/pane.txt" \
229
+ "$ROOT/bin/fm-backlog-handoff.sh" design reconcile-race \
230
+ > "$TMP_ROOT/reconcile-race.out" 2>&1 &
231
+ handoff=$!
232
+ i=0
233
+ while [ ! -f "$TMP_ROOT/reconcile-race.entered" ]; do
234
+ kill -0 "$handoff" 2>/dev/null || fail "reconciliation-race handoff exited before backend delivery"
235
+ i=$((i + 1))
236
+ [ "$i" -le 250 ] || fail "reconciliation-race handoff never reached backend delivery"
237
+ sleep 0.02
238
+ done
239
+ corr=$(cut -d: -f2- "$home/state/.backlog-handoff-design.wake-pending")
240
+ FM_PENDING_REPLY_NOW=9999999999 bash -c '
241
+ . "$1"
242
+ fm_pending_reply_reconcile_delivery "$2" "$3"
243
+ ' _ "$ROOT/bin/fm-pending-reply-lib.sh" "$home/state" "$corr" \
244
+ || fail "concurrent watcher could not reconcile the durable enqueue"
245
+ phase=$(sed -n 's/^phase=//p' "$home/state/pending-replies/$corr")
246
+ [ "$phase" = awaiting_report ] \
247
+ || fail "advisory ring race changed the delivered expectation to $phase"
248
+ touch "$TMP_ROOT/reconcile-race.release"
249
+ wait "$handoff" || fail "failed advisory ring negated the durable enqueue"
250
+ assert_absent "$home/state/pending-replies/.delivery-confirmed-$corr" \
251
+ "delivered enqueue retained a recovery marker"
252
+ [ "$(inbox_record_count "$home/state" design)" -eq 1 ] \
253
+ || fail "advisory ring race did not retain exactly one inbox record"
254
+
255
+ FM_HOME="$home" "$ROOT/bin/fm-backlog-handoff.sh" design reconcile-race \
256
+ > "$TMP_ROOT/reconcile-race-retry.out" 2>&1 \
257
+ || fail "idempotent handoff after the ring race failed: $(cat "$TMP_ROOT/reconcile-race-retry.out")"
258
+ [ "$(inbox_record_count "$home/state" design)" -eq 1 ] \
259
+ || fail "ring-race retry duplicated the durable receiver instruction"
260
+ pass "concurrent reconciliation treats enqueue as delivery despite a failed advisory ring"
261
+ }
262
+
263
+ test_move_crash_keeps_wake_pending_for_recovery() {
264
+ local home="$TMP_ROOT/move-crash-main" sub="$TMP_ROOT/move-crash-sub"
265
+ local fakebin="$TMP_ROOT/move-crash-fakebin" real_tasks rc=0 prepared_state
266
+ setup_homes "$home" "$sub"
267
+ mkdir -p "$sub/data" "$fakebin"
268
+ cat > "$home/data/backlog.md" <<'EOF'
269
+ ## Queued
270
+ - [ ] crash-item - survive the post-move crash (repo: alpha)
271
+
272
+ ## Done
273
+ EOF
274
+ printf '## Queued\n\n## Done\n' > "$sub/data/backlog.md"
275
+ real_tasks=$(command -v tasks-axi)
276
+ cat > "$fakebin/tasks-axi" <<'SH'
277
+ #!/usr/bin/env bash
278
+ "$FM_REAL_TASKS_AXI" "$@"
279
+ rc=$?
280
+ case " $* " in
281
+ *" --file "*" --to "*)
282
+ if [ "$rc" -eq 0 ] && [ "${1:-}" = mv ]; then
283
+ handoff_pid=$(ps -o ppid= -p "$PPID" | tr -d '[:space:]')
284
+ kill -KILL "$handoff_pid"
285
+ sleep 1
286
+ fi
287
+ ;;
288
+ esac
289
+ exit "$rc"
290
+ SH
291
+ chmod +x "$fakebin/tasks-axi"
292
+
293
+ set +e
294
+ FM_REAL_TASKS_AXI="$real_tasks" PATH="$fakebin:$PATH" FM_HOME="$home" \
295
+ "$ROOT/bin/fm-backlog-handoff.sh" design crash-item > "$TMP_ROOT/move-crash.out" 2>&1
296
+ rc=$?
297
+ set +e
298
+ [ "$rc" -ne 0 ] || fail "post-move crash fixture unexpectedly reported success"
299
+ assert_grep 'crash-item' "$sub/data/backlog.md" "post-move crash did not leave the item durable"
300
+ assert_present "$home/state/.backlog-handoff-design.wake-pending" \
301
+ "post-move crash lost receiver wake intent"
302
+ prepared_state=$(cat "$home/state/.backlog-handoff-design.wake-pending")
303
+ cat > "$home/data/backlog.md" <<'EOF'
304
+ ## Queued
305
+ - [ ] unrelated-move - still waiting in the main backlog (repo: alpha)
306
+
307
+ ## Done
308
+ EOF
309
+ rc=0
310
+ FM_HOME="$home" "$ROOT/bin/fm-backlog-handoff.sh" design unrelated-move \
311
+ > "$TMP_ROOT/move-crash-unrelated.out" 2>&1 || rc=$?
312
+ [ "$rc" -ne 0 ] || fail "unrelated moving handoff discarded a post-move prepared wake"
313
+ assert_contains "$(cat "$TMP_ROOT/move-crash-unrelated.out")" \
314
+ 'belongs to a different routed batch' \
315
+ "unrelated handoff did not surface the unresolved prepared batch"
316
+ [ "$(cat "$home/state/.backlog-handoff-design.wake-pending")" = "$prepared_state" ] \
317
+ || fail "unrelated moving handoff changed the post-move prepared wake"
318
+ assert_grep 'unrelated-move' "$home/data/backlog.md" \
319
+ "unrelated moving handoff changed its source item before resolving the older wake"
320
+ assert_no_grep 'unrelated-move' "$sub/data/backlog.md" \
321
+ "unrelated moving handoff moved work despite the unresolved older wake"
322
+
323
+ : > "$TMP_ROOT/default-tmux.log"
324
+ FM_HOME="$home" "$ROOT/bin/fm-backlog-handoff.sh" design crash-item \
325
+ > "$TMP_ROOT/move-crash-retry.out" 2>&1 \
326
+ || fail "post-move crash recovery failed: $(cat "$TMP_ROOT/move-crash-retry.out")"
327
+ assert_contains "$(inbox_body_stream "$home/state" design)" \
328
+ 'New routed work is in your backlog.' \
329
+ "post-move crash recovery did not enqueue the receiver instruction"
330
+ [ "$(doorbell_count "$TMP_ROOT/default-tmux.log")" -eq 1 ] \
331
+ || fail "post-move crash recovery did not ring the receiver doorbell"
332
+ assert_absent "$home/state/.backlog-handoff-design.wake-pending" \
333
+ "confirmed crash recovery left receiver wake pending"
334
+ pass "a post-move crash preserves wake intent for an idempotent retry"
335
+ }
336
+
337
+ test_pre_move_crash_does_not_wake_until_move_lands() {
338
+ local home="$TMP_ROOT/pre-move-crash-main" sub="$TMP_ROOT/pre-move-crash-sub"
339
+ local fakebin="$TMP_ROOT/pre-move-crash-fakebin" real_tasks rc=0 wake_count
340
+ setup_homes "$home" "$sub"
341
+ mkdir -p "$sub/data" "$fakebin"
342
+ cat > "$home/data/backlog.md" <<'EOF'
343
+ ## Queued
344
+ - [ ] pre-move-crash - wake only after durable move (repo: alpha)
345
+
346
+ ## Done
347
+ EOF
348
+ printf '## Queued\n\n## Done\n' > "$sub/data/backlog.md"
349
+ real_tasks=$(command -v tasks-axi)
350
+ cat > "$fakebin/tasks-axi" <<'SH'
351
+ #!/usr/bin/env bash
352
+ case " $* " in
353
+ *" --file "*" --to "*)
354
+ if [ "${1:-}" = mv ]; then
355
+ handoff_pid=$(ps -o ppid= -p "$PPID" | tr -d '[:space:]')
356
+ kill -KILL "$handoff_pid"
357
+ sleep 1
358
+ fi
359
+ ;;
360
+ esac
361
+ exec "$FM_REAL_TASKS_AXI" "$@"
362
+ SH
363
+ chmod +x "$fakebin/tasks-axi"
364
+ : > "$TMP_ROOT/default-tmux.log"
365
+
366
+ set +e
367
+ FM_REAL_TASKS_AXI="$real_tasks" PATH="$fakebin:$PATH" FM_HOME="$home" \
368
+ "$ROOT/bin/fm-backlog-handoff.sh" design pre-move-crash > "$TMP_ROOT/pre-move-crash.out" 2>&1
369
+ rc=$?
370
+ set -e
371
+ [ "$rc" -ne 0 ] || fail "pre-move crash fixture unexpectedly reported success"
372
+ assert_grep 'pre-move-crash' "$home/data/backlog.md" "pre-move crash changed the source backlog"
373
+ assert_no_grep 'pre-move-crash' "$sub/data/backlog.md" "pre-move crash changed the destination backlog"
374
+ assert_present "$home/state/.backlog-handoff-design.wake-pending" \
375
+ "pre-move crash lost its prepared wake intent"
376
+
377
+ cat > "$sub/data/backlog.md" <<'EOF'
378
+ ## Queued
379
+ - [ ] unrelated-ready - already durable from another handoff (repo: alpha)
380
+
381
+ ## Done
382
+ EOF
383
+ rc=0
384
+ FM_HOME="$home" "$ROOT/bin/fm-backlog-handoff.sh" design unrelated-ready \
385
+ > "$TMP_ROOT/pre-move-unrelated.out" 2>&1 || rc=$?
386
+ [ "$rc" -ne 0 ] || fail "unrelated handoff accepted another batch's prepared wake"
387
+ assert_contains "$(cat "$TMP_ROOT/pre-move-unrelated.out")" \
388
+ 'belongs to a different routed batch' \
389
+ "unrelated handoff did not report the prepared batch conflict"
390
+ [ ! -s "$TMP_ROOT/default-tmux.log" ] \
391
+ || fail "unrelated already-present work promoted another batch's prepared wake"
392
+ assert_grep 'pre-move-crash' "$home/data/backlog.md" \
393
+ "unrelated handoff changed the prepared batch's source item"
394
+ assert_present "$home/state/.backlog-handoff-design.wake-pending" \
395
+ "unrelated handoff discarded another batch's prepared wake"
396
+
397
+ FM_HOME="$home" "$ROOT/bin/fm-backlog-handoff.sh" design pre-move-crash \
398
+ > "$TMP_ROOT/pre-move-crash-retry.out" 2>&1 \
399
+ || fail "pre-move crash recovery failed: $(cat "$TMP_ROOT/pre-move-crash-retry.out")"
400
+ assert_grep 'pre-move-crash' "$sub/data/backlog.md" "pre-move crash recovery did not move the item"
401
+ wake_count=$(inbox_record_count "$home/state" design)
402
+ [ "$wake_count" -eq 1 ] || fail "pre-move crash recovery emitted $wake_count receiver records"
403
+ [ "$(doorbell_count "$TMP_ROOT/default-tmux.log")" -eq 1 ] \
404
+ || fail "pre-move crash recovery did not ring exactly one receiver doorbell"
405
+ pass "a pre-move crash wakes only after retry makes the item durable"
406
+ }
407
+
408
+ test_delivery_confirmation_crash_does_not_resend() {
409
+ local home="$TMP_ROOT/confirm-crash-main" sub="$TMP_ROOT/confirm-crash-sub"
410
+ local fakebin="$TMP_ROOT/confirm-crash-fakebin" real_rm rc=0 wake_count
411
+ setup_homes "$home" "$sub"
412
+ mkdir -p "$sub/data" "$fakebin"
413
+ cat > "$home/data/backlog.md" <<'EOF'
414
+ ## Queued
415
+ - [ ] confirm-crash - preserve confirmed delivery (repo: alpha)
416
+
417
+ ## Done
418
+ EOF
419
+ printf '## Queued\n\n## Done\n' > "$sub/data/backlog.md"
420
+ real_rm=$(command -v rm)
421
+ cat > "$fakebin/rm" <<'SH'
422
+ #!/usr/bin/env bash
423
+ for arg in "$@"; do
424
+ if [ "$arg" = "$FM_CONFIRM_WAKE_MARKER" ] \
425
+ && mkdir "$FM_CONFIRM_CRASH_ONCE" 2>/dev/null; then
426
+ kill -KILL "$PPID"
427
+ exit 0
428
+ fi
429
+ done
430
+ exec "$FM_REAL_RM" "$@"
431
+ SH
432
+ chmod +x "$fakebin/rm"
433
+ : > "$TMP_ROOT/default-tmux.log"
434
+
435
+ set +e
436
+ PATH="$fakebin:$PATH" FM_REAL_RM="$real_rm" \
437
+ FM_CONFIRM_CRASH_ONCE="$TMP_ROOT/confirm-crash.once" \
438
+ FM_CONFIRM_WAKE_MARKER="$home/state/.backlog-handoff-design.wake-pending" \
439
+ FM_HOME="$home" "$ROOT/bin/fm-backlog-handoff.sh" design confirm-crash \
440
+ > "$TMP_ROOT/confirm-crash.out" 2>&1
441
+ rc=$?
442
+ set +e
443
+ [ "$rc" -ne 0 ] || fail "post-confirmation crash fixture unexpectedly reported success"
444
+ wake_count=$(inbox_record_count "$home/state" design)
445
+ [ "$wake_count" -eq 1 ] || fail "post-confirmation crash did not deliver exactly one receiver record"
446
+ [ "$(doorbell_count "$TMP_ROOT/default-tmux.log")" -eq 1 ] \
447
+ || fail "post-confirmation crash did not ring exactly one receiver doorbell"
448
+ case "$(cat "$home/state/.backlog-handoff-design.wake-pending")" in
449
+ pending:*) ;;
450
+ *) fail "post-confirmation crash lost its stable delivery correlation" ;;
451
+ esac
452
+
453
+ # Route different work before explicitly retrying the crashed invocation. The
454
+ # completed old correlation must be reconciled, but must not stand in as the
455
+ # delivery proof for this new durable move.
456
+ cat > "$home/data/backlog.md" <<'EOF'
457
+ ## Queued
458
+ - [ ] after-crash - requires its own receiver wake (repo: alpha)
459
+
460
+ ## Done
461
+ EOF
462
+ FM_HOME="$home" "$ROOT/bin/fm-backlog-handoff.sh" design after-crash \
463
+ > "$TMP_ROOT/after-confirm-crash.out" 2>&1 \
464
+ || fail "new handoff after a confirmation crash failed: $(cat "$TMP_ROOT/after-confirm-crash.out")"
465
+ [ "$(inbox_record_count "$home/state" design)" -eq "$((wake_count + 1))" ] \
466
+ || fail "completed stale correlation suppressed or duplicated the new receiver record"
467
+ [ "$(doorbell_count "$TMP_ROOT/default-tmux.log")" -eq 2 ] \
468
+ || fail "completed stale correlation suppressed or duplicated the new doorbell"
469
+ assert_grep 'after-crash' "$sub/data/backlog.md" \
470
+ "new item after a confirmation crash was not durably handed off"
471
+
472
+ FM_HOME="$home" "$ROOT/bin/fm-backlog-handoff.sh" design confirm-crash \
473
+ > "$TMP_ROOT/confirm-crash-retry.out" 2>&1 \
474
+ || fail "post-confirmation crash recovery failed: $(cat "$TMP_ROOT/confirm-crash-retry.out")"
475
+ [ "$(inbox_record_count "$home/state" design)" -eq "$((wake_count + 1))" ] \
476
+ || fail "post-confirmation crash recovery duplicated the receiver record"
477
+ [ "$(doorbell_count "$TMP_ROOT/default-tmux.log")" -eq 2 ] \
478
+ || fail "post-confirmation crash recovery duplicated the doorbell"
479
+ assert_absent "$home/state/.backlog-handoff-design.wake-pending" \
480
+ "post-confirmation crash recovery left wake state pending"
481
+ pass "a post-confirmation crash reconciles once without suppressing a later handoff wake"
482
+ }
483
+
484
+ test_unresolved_delivery_attempt_refuses_immediate_resend() {
485
+ local home="$TMP_ROOT/attempt-crash-main" sub="$TMP_ROOT/attempt-crash-sub"
486
+ local fakebin="$TMP_ROOT/attempt-crash-fakebin" real_mv rc=0 wake_count out
487
+ setup_homes "$home" "$sub"
488
+ mkdir -p "$sub/data" "$fakebin"
489
+ cat > "$home/data/backlog.md" <<'EOF'
490
+ ## Queued
491
+ - [ ] attempt-crash - do not resend an unresolved delivery (repo: alpha)
492
+
493
+ ## Done
494
+ EOF
495
+ printf '## Queued\n\n## Done\n' > "$sub/data/backlog.md"
496
+ real_mv=$(command -v mv)
497
+ cat > "$fakebin/mv" <<'SH'
498
+ #!/usr/bin/env bash
499
+ for arg in "$@"; do
500
+ if [ -f "$arg" ] && grep -q '^confirmed=' "$arg" 2>/dev/null; then
501
+ kill -KILL "$PPID"
502
+ exit 1
503
+ fi
504
+ done
505
+ exec "$FM_REAL_MV" "$@"
506
+ SH
507
+ chmod +x "$fakebin/mv"
508
+ : > "$TMP_ROOT/default-tmux.log"
509
+
510
+ set +e
511
+ PATH="$fakebin:$PATH" FM_REAL_MV="$real_mv" FM_HOME="$home" \
512
+ "$ROOT/bin/fm-backlog-handoff.sh" design attempt-crash \
513
+ > "$TMP_ROOT/attempt-crash.out" 2>&1
514
+ rc=$?
515
+ set +e
516
+ [ "$rc" -ne 0 ] || fail "unresolved-attempt crash fixture unexpectedly reported success"
517
+ wake_count=$(inbox_record_count "$home/state" design)
518
+ [ "$wake_count" -eq 1 ] || fail "unresolved-attempt crash did not deliver exactly one receiver record"
519
+ [ "$(doorbell_count "$TMP_ROOT/default-tmux.log")" -eq 0 ] \
520
+ || fail "delivery bookkeeping crash unexpectedly reached the later doorbell step"
521
+
522
+ rc=0
523
+ out=$(FM_HOME="$home" "$ROOT/bin/fm-backlog-handoff.sh" design attempt-crash 2>&1) || rc=$?
524
+ [ "$rc" -ne 0 ] || fail "immediate retry resent or accepted an unresolved delivery attempt"
525
+ assert_contains "$out" 'delivery for design is unresolved; refusing to resend correlation' \
526
+ "immediate retry did not report the unresolved delivery boundary"
527
+ [ "$(inbox_record_count "$home/state" design)" -eq "$wake_count" ] \
528
+ || fail "immediate retry duplicated the unresolved receiver record"
529
+ [ "$(doorbell_count "$TMP_ROOT/default-tmux.log")" -eq 0 ] \
530
+ || fail "immediate retry rang for an unresolved delivery correlation"
531
+ pass "an unresolved delivery attempt refuses an immediate duplicate wake"
532
+ }
533
+
534
+ test_concurrent_local_handoffs_serialize_move_and_wake() {
535
+ local home="$TMP_ROOT/concurrent-main" sub="$TMP_ROOT/concurrent-sub"
536
+ local basebin blockbin="$TMP_ROOT/concurrent-blockbin" first second i wake_count
537
+ setup_homes "$home" "$sub"
538
+ mkdir -p "$sub/data" "$blockbin"
539
+ printf '## Queued\n\n## Done\n' > "$sub/data/backlog.md"
540
+ cat > "$home/data/backlog.md" <<'EOF'
541
+ ## Queued
542
+ - [ ] concurrent-a - first routed item (repo: alpha)
543
+
544
+ ## Done
545
+ EOF
546
+ basebin=$(make_fake_tmux "$TMP_ROOT/concurrent-fake")
547
+ cat > "$blockbin/tmux" <<'SH'
548
+ #!/usr/bin/env bash
549
+ case "$*" in
550
+ *"Firstmate instruction waiting:"*)
551
+ if mkdir "$FM_BLOCK_WAKE_ONCE" 2>/dev/null; then
552
+ touch "$FM_BLOCK_WAKE_ENTERED"
553
+ while [ ! -f "$FM_BLOCK_WAKE_RELEASE" ]; do sleep 0.02; done
554
+ fi
555
+ ;;
556
+ esac
557
+ exec "$FM_BASE_TMUX" "$@"
558
+ SH
559
+ chmod +x "$blockbin/tmux"
560
+
561
+ PATH="$blockbin:$basebin:$PATH" FM_HOME="$home" FM_BASE_TMUX="$basebin/tmux" \
562
+ FM_BLOCK_WAKE_ONCE="$TMP_ROOT/concurrent.once" \
563
+ FM_BLOCK_WAKE_ENTERED="$TMP_ROOT/concurrent.entered" \
564
+ FM_BLOCK_WAKE_RELEASE="$TMP_ROOT/concurrent.release" \
565
+ FM_FAKE_TMUX_WINDOW='firstmate:fm-design' \
566
+ FM_FAKE_TMUX_LOG="$TMP_ROOT/concurrent-tmux.log" \
567
+ FM_FAKE_TMUX_CAPTURE="$TMP_ROOT/concurrent-fake/pane.txt" \
568
+ "$ROOT/bin/fm-backlog-handoff.sh" design concurrent-a > "$TMP_ROOT/concurrent-a.out" 2>&1 &
569
+ first=$!
570
+ i=0
571
+ while [ ! -f "$TMP_ROOT/concurrent.entered" ]; do
572
+ kill -0 "$first" 2>/dev/null || fail "first concurrent handoff exited before its blocked wake"
573
+ i=$((i + 1))
574
+ [ "$i" -le 250 ] || fail "first concurrent handoff never reached its receiver wake"
575
+ sleep 0.02
576
+ done
577
+ cat > "$home/data/backlog.md" <<'EOF'
578
+ ## Queued
579
+ - [ ] concurrent-b - second routed item (repo: alpha)
580
+
581
+ ## Done
582
+ EOF
583
+ PATH="$blockbin:$basebin:$PATH" FM_HOME="$home" FM_BASE_TMUX="$basebin/tmux" \
584
+ FM_BLOCK_WAKE_ONCE="$TMP_ROOT/concurrent.once" \
585
+ FM_BLOCK_WAKE_ENTERED="$TMP_ROOT/concurrent.entered" \
586
+ FM_BLOCK_WAKE_RELEASE="$TMP_ROOT/concurrent.release" \
587
+ FM_FAKE_TMUX_WINDOW='firstmate:fm-design' \
588
+ FM_FAKE_TMUX_LOG="$TMP_ROOT/concurrent-tmux.log" \
589
+ FM_FAKE_TMUX_CAPTURE="$TMP_ROOT/concurrent-fake/pane.txt" \
590
+ "$ROOT/bin/fm-backlog-handoff.sh" design concurrent-b > "$TMP_ROOT/concurrent-b.out" 2>&1 &
591
+ second=$!
592
+ sleep 0.2
593
+ assert_grep 'concurrent-b' "$home/data/backlog.md" \
594
+ "second local handoff moved while the first still owned its wake"
595
+ touch "$TMP_ROOT/concurrent.release"
596
+ wait "$first" || fail "first serialized local handoff failed"
597
+ wait "$second" || fail "second serialized local handoff failed"
598
+ assert_grep 'concurrent-a' "$sub/data/backlog.md" "first serialized item was lost"
599
+ assert_grep 'concurrent-b' "$sub/data/backlog.md" "second serialized item was lost"
600
+ wake_count=$(inbox_record_count "$home/state" design)
601
+ [ "$wake_count" -eq 2 ] || fail "serialized local handoffs produced $wake_count receiver records"
602
+ [ "$(doorbell_count "$TMP_ROOT/concurrent-tmux.log")" -eq 2 ] \
603
+ || fail "serialized local handoffs did not produce two receiver doorbells"
604
+ pass "concurrent local handoffs serialize each durable move with its wake"
605
+ }
606
+
607
+ test_local_teardown_waits_for_handoff_wake() {
608
+ local home="$TMP_ROOT/teardown-race-main" sub="$TMP_ROOT/teardown-race-sub"
609
+ local basebin blockbin="$TMP_ROOT/teardown-race-blockbin" handoff teardown i
610
+ setup_homes "$home" "$sub"
611
+ printf 'project=%s\n' "$ROOT" >> "$home/state/design.meta"
612
+ mkdir -p "$sub/data" "$blockbin"
613
+ printf '## Queued\n\n## Done\n' > "$sub/data/backlog.md"
614
+ cat > "$home/data/backlog.md" <<'EOF'
615
+ ## Queued
616
+ - [ ] teardown-race - routed while teardown starts (repo: alpha)
617
+
618
+ ## Done
619
+ EOF
620
+ basebin=$(make_fake_tmux "$TMP_ROOT/teardown-race-fake")
621
+ cat > "$blockbin/tmux" <<'SH'
622
+ #!/usr/bin/env bash
623
+ case "$*" in
624
+ *"Firstmate instruction waiting:"*)
625
+ touch "$FM_BLOCK_WAKE_ENTERED"
626
+ while [ ! -f "$FM_BLOCK_WAKE_RELEASE" ]; do sleep 0.02; done
627
+ ;;
628
+ esac
629
+ exec "$FM_BASE_TMUX" "$@"
630
+ SH
631
+ chmod +x "$blockbin/tmux"
632
+ PATH="$blockbin:$basebin:$PATH" FM_HOME="$home" FM_ROOT_OVERRIDE="$ROOT" \
633
+ FM_BASE_TMUX="$basebin/tmux" FM_BLOCK_WAKE_ENTERED="$TMP_ROOT/teardown-race.entered" \
634
+ FM_BLOCK_WAKE_RELEASE="$TMP_ROOT/teardown-race.release" \
635
+ FM_FAKE_TMUX_WINDOW='firstmate:fm-design' \
636
+ FM_FAKE_TMUX_LOG="$TMP_ROOT/teardown-race-tmux.log" \
637
+ FM_FAKE_TMUX_CAPTURE="$TMP_ROOT/teardown-race-fake/pane.txt" \
638
+ "$ROOT/bin/fm-backlog-handoff.sh" design teardown-race > "$TMP_ROOT/teardown-race-handoff.out" 2>&1 &
639
+ handoff=$!
640
+ i=0
641
+ while [ ! -f "$TMP_ROOT/teardown-race.entered" ]; do
642
+ kill -0 "$handoff" 2>/dev/null || fail "teardown-race handoff exited before its blocked wake"
643
+ i=$((i + 1))
644
+ [ "$i" -le 250 ] || fail "teardown-race handoff never reached its receiver wake"
645
+ sleep 0.02
646
+ done
647
+ PATH="$basebin:$PATH" FM_HOME="$home" FM_ROOT_OVERRIDE="$ROOT" \
648
+ FM_FAKE_TMUX_WINDOW='firstmate:fm-design' \
649
+ FM_FAKE_TMUX_LOG="$TMP_ROOT/teardown-race-tmux.log" \
650
+ FM_FAKE_TMUX_CAPTURE="$TMP_ROOT/teardown-race-fake/pane.txt" \
651
+ "$ROOT/bin/fm-teardown.sh" design --force > "$TMP_ROOT/teardown-race-teardown.out" 2>&1 &
652
+ teardown=$!
653
+ sleep 0.3
654
+ kill -0 "$teardown" 2>/dev/null \
655
+ || fail "local teardown bypassed the in-flight handoff lock: $(cat "$TMP_ROOT/teardown-race-teardown.out")"
656
+ [ -d "$sub" ] || fail "local teardown removed the receiver home before handoff wake completed"
657
+ assert_grep 'teardown-race' "$sub/data/backlog.md" \
658
+ "local teardown removed routed work before handoff wake completed"
659
+ touch "$TMP_ROOT/teardown-race.release"
660
+ wait "$handoff" || fail "teardown-race handoff failed after releasing its wake"
661
+ wait "$teardown" 2>/dev/null || true
662
+ pass "local teardown waits for the routed move and receiver wake"
663
+ }
664
+
665
+ test_local_teardown_preserves_wake_when_home_removal_fails() {
666
+ local home="$TMP_ROOT/teardown-home-fail-main" sub="$TMP_ROOT/teardown-home-fail-sub"
667
+ local fakebin rm_bin="$TMP_ROOT/teardown-home-fail-rm" real_rm corr rc=0 marker rec fail_home
668
+ local marker_before="$TMP_ROOT/teardown-home-fail-marker.before"
669
+ local rec_before="$TMP_ROOT/teardown-home-fail-record.before"
670
+ setup_homes "$home" "$sub"
671
+ printf 'project=%s\n' "$ROOT" >> "$home/state/design.meta"
672
+ mkdir -p "$sub/data" "$rm_bin"
673
+ printf '## Queued\n- [ ] still-routed - preserve its wake (repo: alpha)\n\n## Done\n' > "$sub/data/backlog.md"
674
+ corr=$(FM_HOME="$home" bash -c '
675
+ . "$1"
676
+ fm_pending_reply_create "$2" "$2/state" design "New routed work is in your backlog."
677
+ ' _ "$ROOT/bin/fm-pending-reply-lib.sh" "$home") \
678
+ || fail "could not seed teardown wake state"
679
+ marker="$home/state/.backlog-handoff-design.wake-pending"
680
+ rec="$home/state/pending-replies/$corr"
681
+ printf 'pending:%s\n' "$corr" > "$marker"
682
+ cp -p -- "$marker" "$marker_before"
683
+ cp -p -- "$rec" "$rec_before"
684
+ real_rm=$(command -v rm)
685
+ fail_home=$(cd "$sub" && pwd -P)
686
+ cat > "$rm_bin/rm" <<'SH'
687
+ #!/usr/bin/env bash
688
+ for arg in "$@"; do
689
+ [ "$arg" != "$FM_FAIL_HOME" ] || exit 1
690
+ done
691
+ exec "$FM_REAL_RM" "$@"
692
+ SH
693
+ chmod +x "$rm_bin/rm"
694
+ fakebin=$(make_fake_tmux "$TMP_ROOT/teardown-home-fail-fake")
695
+
696
+ set +e
697
+ PATH="$rm_bin:$fakebin:$PATH" FM_REAL_RM="$real_rm" FM_FAIL_HOME="$fail_home" \
698
+ FM_HOME="$home" FM_ROOT_OVERRIDE="$ROOT" \
699
+ FM_FAKE_TMUX_WINDOW='firstmate:fm-design' \
700
+ FM_FAKE_TMUX_LOG="$TMP_ROOT/teardown-home-fail-tmux.log" \
701
+ FM_FAKE_TMUX_CAPTURE="$TMP_ROOT/teardown-home-fail-fake/pane.txt" \
702
+ "$ROOT/bin/fm-teardown.sh" design --force > "$TMP_ROOT/teardown-home-fail.out" 2>&1
703
+ rc=$?
704
+ set -e
705
+ [ "$rc" -ne 0 ] || fail "teardown ignored the receiver-home removal failure"
706
+ assert_present "$sub" "failed teardown did not preserve the receiver home"
707
+ assert_grep 'still-routed' "$sub/data/backlog.md" "failed teardown lost routed backlog work"
708
+ cmp -s "$marker_before" "$marker" \
709
+ || fail "failed home removal changed the pending wake marker"
710
+ cmp -s "$rec_before" "$rec" \
711
+ || fail "failed home removal changed the pending wake correlation"
712
+ assert_present "$home/state/design.meta" "failed teardown removed route metadata"
713
+ assert_grep '- design ' "$home/data/secondmates.md" "failed teardown removed the registry route"
714
+
715
+ PATH="$fakebin:$PATH" FM_HOME="$home" FM_ROOT_OVERRIDE="$ROOT" \
716
+ FM_FAKE_TMUX_WINDOW='firstmate:fm-design' \
717
+ FM_FAKE_TMUX_LOG="$TMP_ROOT/teardown-home-fail-tmux.log" \
718
+ FM_FAKE_TMUX_CAPTURE="$TMP_ROOT/teardown-home-fail-fake/pane.txt" \
719
+ "$ROOT/bin/fm-teardown.sh" design --force > "$TMP_ROOT/teardown-home-retry.out" 2>&1 \
720
+ || fail "teardown retry did not retire the preserved wake: $(cat "$TMP_ROOT/teardown-home-retry.out")"
721
+ assert_absent "$sub" "teardown retry left the receiver home"
722
+ assert_absent "$marker" "teardown retry left the pending wake marker"
723
+ assert_absent "$rec" "teardown retry left the pending wake correlation"
724
+ assert_no_grep '- design ' "$home/data/secondmates.md" "teardown retry left the registry route"
725
+ pass "failed local home removal preserves its wake and a retry retires both"
726
+ }
727
+
728
+ # Exact multi-line block extract: header matching key plus following body lines
729
+ # (indented lines and blank separators between paragraphs), stopping at the next
730
+ # item header or unindented section heading (column-0 ##).
731
+ extract_item_block() {
732
+ local file=$1 key=$2
733
+ awk -v key="$key" '
734
+ /^- \[[ x]\] / {
735
+ rest = $0
736
+ sub(/^- \[[ x]\] +/, "", rest)
737
+ id = rest
738
+ sub(/[ \t].*/, "", id)
739
+ if (capturing) exit
740
+ if (id == key) { print; capturing = 1; next }
741
+ next
742
+ }
743
+ capturing && /^## / { exit }
744
+ capturing && /^- \[[ x]\] / { exit }
745
+ capturing && /^([ \t].*)?$/ { print; next }
746
+ capturing { exit }
747
+ ' "$file"
748
+ }
749
+
750
+ assert_block_equals() {
751
+ local label=$1 expected=$2 actual=$3
752
+ if [ "$expected" != "$actual" ]; then
753
+ printf 'expected block:\n%s\nactual block:\n%s\n' "$expected" "$actual" >&2
754
+ fail "$label"
755
+ fi
756
+ }
757
+
758
+ # seed_public_commitment <home> <obligation> <work-home> <work-id>: the intake
759
+ # half of a promised public reply - the typed obligation, its bound work, and
760
+ # this home's registration - so a later handoff can be observed against a real
761
+ # unresolved commitment rather than a stub.
762
+ seed_public_commitment() {
763
+ local home=$1 obligation=$2 work_home=$3 work_id=$4
764
+ printf 'FMX_PAIRING_TOKEN=test-token\n' > "$home/.env"
765
+ cp "$ROOT/.tasks.toml" "$home/.tasks.toml"
766
+ jq -n '{request_id:"req-handoff", platform:"x",
767
+ context_binding:{version:"ctx1", value:"ctx1_req-handoff"},
768
+ public_safe_summary:"looking into the sign-in redirect",
769
+ received_at:"2026-07-30T10:00:00Z",
770
+ followup_expires_at:"2026-08-06T10:00:00Z",
771
+ reservation_expires_at:"2026-08-06T10:00:00Z"}' > "$home/request.json"
772
+ jq -n '{type:"pr-merged", project:"alpha",
773
+ required_deliverables:["pr_url"], completion_policy:"all-required"}' \
774
+ > "$home/expected.json"
775
+ jq -n --arg h "$work_home" --arg w "$work_id" \
776
+ '{relation_id:"rel-code", work_ref:{home_id:$h, task_id:$w},
777
+ role:"fulfills", required:true, generation:1}' > "$home/relation.json"
778
+ (cd "$home" && tasks-axi public-followup add "$obligation" \
779
+ --request-context-file "$home/request.json" --purpose promised-final \
780
+ --expected-final-file "$home/expected.json" --expires-at 2026-10-01T00:00:00Z) >/dev/null \
781
+ || fail "could not create the public commitment"
782
+ (cd "$home" && tasks-axi public-followup bind-work "$obligation" \
783
+ --relation-file "$home/relation.json") >/dev/null \
784
+ || fail "could not bind work to the public commitment"
785
+ FM_ROOT_OVERRIDE="$ROOT" FM_HOME="$home" "$ROOT/bin/fm-public-followup.sh" register \
786
+ "$obligation" --relation rel-code --work-home "$work_home" --work-id "$work_id" \
787
+ --generation 1 >/dev/null \
788
+ || fail "could not register the public commitment"
789
+ }
790
+
791
+ # A public promise binds its work by home AND id. Handing that work to a
792
+ # secondmate leaves the binding naming a home that no longer owns it, which used
793
+ # to go unnoticed until the promised reply was never delivered. The move itself
794
+ # stays safe; the staleness must be reported at the moment it is created.
795
+ test_handoff_warns_when_a_moved_item_still_owes_a_public_reply() {
796
+ local home="$TMP_ROOT/pf-stale-main"
797
+ local sub="$TMP_ROOT/pf-stale-sub"
798
+ command -v jq >/dev/null 2>&1 || { echo "skip: jq not found (required by the public-commitment guard)"; return 0; }
799
+ setup_homes "$home" "$sub"
800
+ cat > "$home/data/backlog.md" <<'EOF'
801
+ ## Queued
802
+ - [ ] promised-item - fix the sign-in redirect (repo: alpha)
803
+ - [ ] plain-item - unrelated queued work (repo: alpha)
804
+
805
+ ## Done
806
+ EOF
807
+ seed_public_commitment "$home" pf-handoff main promised-item
808
+
809
+ local out rc=0
810
+ out=$(FM_HOME="$home" "$ROOT/bin/fm-backlog-handoff.sh" design promised-item plain-item 2>&1) || rc=$?
811
+ [ "$rc" -eq 0 ] || fail "handoff must still succeed while reporting the stale binding: $out"
812
+ assert_contains "$out" "handed off 2 item(s)" "the move itself must still be reported"
813
+ assert_grep 'promised-item' "$sub/data/backlog.md" "the promised item did not reach the secondmate backlog"
814
+ assert_contains "$out" "promised-item still owes a public reply bound to main/promised-item" \
815
+ "the stale public-commitment binding was not reported"
816
+ # The report must come from a genuinely unresolved commitment, not from a state
817
+ # the guard merely could not verify.
818
+ assert_contains "$out" "public commitment pf-handoff is still" \
819
+ "the report did not carry the unresolved commitment the guard actually found"
820
+ assert_contains "$out" "--work-home secondmate:design" \
821
+ "the report did not name the rebinding that keeps the promise reachable"
822
+ case "$out" in
823
+ *"plain-item still owes"*) fail "an item with no public commitment must not be reported" ;;
824
+ esac
825
+
826
+ pass "handoff reports a moved item whose public commitment still binds this home"
827
+ }
828
+
829
+ # A home that never opted into the relay must pay nothing and say nothing here.
830
+ test_handoff_is_silent_about_public_commitments_without_the_relay() {
831
+ local home="$TMP_ROOT/pf-silent-main"
832
+ local sub="$TMP_ROOT/pf-silent-sub"
833
+ setup_homes "$home" "$sub"
834
+ cat > "$home/data/backlog.md" <<'EOF'
835
+ ## Queued
836
+ - [ ] quiet-item - ordinary queued work (repo: alpha)
837
+
838
+ ## Done
839
+ EOF
840
+
841
+ local out rc=0
842
+ out=$(FM_HOME="$home" "$ROOT/bin/fm-backlog-handoff.sh" design quiet-item 2>&1) || rc=$?
843
+ [ "$rc" -eq 0 ] || fail "handoff failed in a relay-free home: $out"
844
+ case "$out" in
845
+ *"public reply"*) fail "a relay-free home must not mention public commitments: $out" ;;
846
+ esac
847
+ assert_grep 'quiet-item' "$sub/data/backlog.md" "the item did not reach the secondmate backlog"
848
+ pass "handoff says nothing about public commitments in a relay-free home"
849
+ }
850
+
851
+ test_body_moves_when_followed_by_another_item() {
852
+ local home="$TMP_ROOT/body-next-item-main"
853
+ local sub="$TMP_ROOT/body-next-item-sub"
854
+ setup_homes "$home" "$sub"
855
+
856
+ cat > "$home/data/backlog.md" <<'EOF'
857
+ ## Queued
858
+ - [ ] keep-a - stays first (repo: alpha)
859
+ keep-a body line
860
+ - [ ] body-item - has a body (repo: alpha)
861
+ Spec detail one.
862
+ ## Intent
863
+ Move the full block.
864
+ trailing body line
865
+ - [ ] keep-b - stays after (repo: beta)
866
+ keep-b body stays
867
+
868
+ ## Done
869
+ EOF
870
+
871
+ local expected_block
872
+ expected_block=$(extract_item_block "$home/data/backlog.md" body-item)
873
+
874
+ FM_HOME="$home" "$ROOT/bin/fm-backlog-handoff.sh" design body-item >/dev/null \
875
+ || fail "handoff of body-followed-by-item failed"
876
+
877
+ local dest_block
878
+ dest_block=$(extract_item_block "$sub/data/backlog.md" body-item)
879
+ assert_block_equals "destination body block mismatch after item-boundary handoff" \
880
+ "$expected_block" "$dest_block"
881
+
882
+ assert_no_grep 'body-item' "$home/data/backlog.md" "body-item header still in source"
883
+ assert_no_grep 'Spec detail one' "$home/data/backlog.md" "orphaned body line stayed in source"
884
+ assert_no_grep 'Move the full block' "$home/data/backlog.md" "orphaned body line stayed in source"
885
+ assert_no_grep 'trailing body line' "$home/data/backlog.md" "orphaned trailing body stayed in source"
886
+ # Indented heading must move with the item, not be left or treated as a section.
887
+ assert_no_grep '## Intent' "$home/data/backlog.md" "indented ## Intent left in source as if a section"
888
+ assert_grep ' ## Intent' "$sub/data/backlog.md" "indented ## Intent did not arrive at destination"
889
+
890
+ assert_grep 'keep-a' "$home/data/backlog.md" "keep-a was wrongly removed"
891
+ assert_grep ' keep-a body line' "$home/data/backlog.md" "keep-a body was disturbed"
892
+ assert_grep 'keep-b' "$home/data/backlog.md" "keep-b was wrongly removed"
893
+ assert_grep ' keep-b body stays' "$home/data/backlog.md" "keep-b body was disturbed"
894
+
895
+ # keep-a's body must not have grown the orphaned lines of body-item.
896
+ local keep_a_block
897
+ keep_a_block=$(extract_item_block "$home/data/backlog.md" keep-a)
898
+ assert_block_equals "keep-a block must not absorb orphaned body-item lines" \
899
+ $'- [ ] keep-a - stays first (repo: alpha)\n keep-a body line' \
900
+ "$keep_a_block"
901
+
902
+ pass "body followed by another item moves intact with no source orphans"
903
+ }
904
+
905
+ test_body_moves_when_followed_by_section_heading() {
906
+ local home="$TMP_ROOT/body-section-main"
907
+ local sub="$TMP_ROOT/body-section-sub"
908
+ setup_homes "$home" "$sub"
909
+
910
+ cat > "$home/data/backlog.md" <<'EOF'
911
+ ## Queued
912
+ - [ ] section-tail - body ends at section (repo: alpha)
913
+ last queued body
914
+ ## Intent
915
+ still body until column-0 section
916
+
917
+ ## Done
918
+ - [x] old-task - shipped - local main (merged 2026-07-01)
919
+ EOF
920
+
921
+ local expected_block
922
+ expected_block=$(extract_item_block "$home/data/backlog.md" section-tail)
923
+
924
+ FM_HOME="$home" "$ROOT/bin/fm-backlog-handoff.sh" design section-tail >/dev/null \
925
+ || fail "handoff of body-followed-by-section failed"
926
+
927
+ local dest_block
928
+ dest_block=$(extract_item_block "$sub/data/backlog.md" section-tail)
929
+ assert_block_equals "destination body block mismatch after section-boundary handoff" \
930
+ "$expected_block" "$dest_block"
931
+
932
+ assert_no_grep 'section-tail' "$home/data/backlog.md" "section-tail still in source"
933
+ assert_no_grep 'last queued body' "$home/data/backlog.md" "body orphaned before ## Done"
934
+ assert_no_grep 'still body until' "$home/data/backlog.md" "body after ## Intent orphaned"
935
+ assert_grep 'old-task' "$home/data/backlog.md" "Done section item was disturbed"
936
+ assert_grep '## Done' "$home/data/backlog.md" "Done section heading was disturbed"
937
+
938
+ pass "body followed by section heading moves intact; section stays"
939
+ }
940
+
941
+ test_body_moves_when_last_lines_of_file() {
942
+ local home="$TMP_ROOT/body-eof-main"
943
+ local sub="$TMP_ROOT/body-eof-sub"
944
+ setup_homes "$home" "$sub"
945
+
946
+ # A source item that ends the file with no trailing newline is a valid shape;
947
+ # printf builds that deliberately. It must move whole, indented ## line
948
+ # included, into the destination the handoff seeds.
949
+ {
950
+ printf '%s\n' '## Queued'
951
+ printf '%s\n' '- [ ] eof-item - ends the file (repo: alpha)'
952
+ printf '%s\n' ' eof body line one'
953
+ printf '%s\n' ' ## Intent'
954
+ printf '%s' ' eof body line two'
955
+ } > "$home/data/backlog.md"
956
+ # tasks-axi owns the destination format: the moved block lands under ## Queued
957
+ # in the standard three-section scaffold the handoff seeds for a fresh home.
958
+ local expected_destination="$TMP_ROOT/body-eof-expected.md"
959
+ {
960
+ printf '%s\n' '## In flight'
961
+ printf '%s\n' ''
962
+ printf '%s\n' '## Queued'
963
+ printf '%s\n' '- [ ] eof-item - ends the file (repo: alpha)'
964
+ printf '%s\n' ' eof body line one'
965
+ printf '%s\n' ' ## Intent'
966
+ printf '%s\n' ' eof body line two'
967
+ printf '%s\n' ''
968
+ printf '%s\n' '## Done'
969
+ } > "$expected_destination"
970
+
971
+ local expected_block
972
+ expected_block=$(extract_item_block "$home/data/backlog.md" eof-item)
973
+
974
+ FM_HOME="$home" "$ROOT/bin/fm-backlog-handoff.sh" design eof-item >/dev/null \
975
+ || fail "handoff of EOF body item failed"
976
+
977
+ local dest_block
978
+ dest_block=$(extract_item_block "$sub/data/backlog.md" eof-item)
979
+ assert_block_equals "destination body block mismatch for EOF item" \
980
+ "$expected_block" "$dest_block"
981
+ cmp -s "$expected_destination" "$sub/data/backlog.md" \
982
+ || fail "EOF item did not land byte-exact under the seeded destination scaffold"
983
+
984
+ # Source should have no item residual - only the section heading remains.
985
+ if grep -E 'eof-item|eof body|## Intent' "$home/data/backlog.md" >/dev/null; then
986
+ fail "EOF item left residual header or body lines in source"
987
+ fi
988
+ assert_grep '## Queued' "$home/data/backlog.md" "Queued section heading was lost"
989
+
990
+ pass "body as last lines of the file moves intact"
991
+ }
992
+
993
+ test_eof_body_before_seeded_destination_section_keeps_boundary() {
994
+ local home="$TMP_ROOT/body-eof-seeded-main"
995
+ local sub="$TMP_ROOT/body-eof-seeded-sub"
996
+ setup_homes "$home" "$sub"
997
+
998
+ {
999
+ printf '%s\n' '## Queued'
1000
+ printf '%s\n' '- [ ] seeded-eof-item - ends the file (repo: alpha)'
1001
+ printf '%s\n' ' seeded eof body one'
1002
+ printf '%s' ' seeded eof body two'
1003
+ } > "$home/data/backlog.md"
1004
+ # tasks-axi owns the destination whitespace: the moved block sits directly
1005
+ # under ## Queued with the section separator before the following ## Done, and
1006
+ # the EOF body stays a clean line above that heading (its boundary is kept).
1007
+ local expected_destination="$TMP_ROOT/body-eof-seeded-expected.md"
1008
+ {
1009
+ printf '%s\n' '## In flight'
1010
+ printf '%s\n' ''
1011
+ printf '%s\n' '## Queued'
1012
+ printf '%s\n' '- [ ] seeded-eof-item - ends the file (repo: alpha)'
1013
+ printf '%s\n' ' seeded eof body one'
1014
+ printf '%s\n' ' seeded eof body two'
1015
+ printf '%s\n' ''
1016
+ printf '%s\n' '## Done'
1017
+ } > "$expected_destination"
1018
+
1019
+ FM_HOME="$home" "$ROOT/bin/fm-backlog-handoff.sh" design seeded-eof-item >/dev/null \
1020
+ || fail "handoff of EOF body into seeded backlog failed"
1021
+
1022
+ cmp -s "$expected_destination" "$sub/data/backlog.md" \
1023
+ || fail "EOF body did not remain separate from the seeded ## Done heading"
1024
+
1025
+ pass "EOF body before a seeded destination section keeps its boundary"
1026
+ }
1027
+
1028
+ test_untouched_eof_line_preserves_terminator() {
1029
+ local home="$TMP_ROOT/untouched-eof-main"
1030
+ local sub="$TMP_ROOT/untouched-eof-sub"
1031
+ setup_homes "$home" "$sub"
1032
+
1033
+ {
1034
+ printf '%s\n' '## Queued'
1035
+ printf '%s\n' '- [ ] move-item - remove this block (repo: alpha)'
1036
+ printf '%s\n' ' move body'
1037
+ printf '%s\n' '- [ ] keep-item - retain this block (repo: beta)'
1038
+ printf '%s' ' keep body without a final newline'
1039
+ } > "$home/data/backlog.md"
1040
+ local expected_source="$TMP_ROOT/untouched-eof-expected.md"
1041
+ {
1042
+ printf '%s\n' '## Queued'
1043
+ printf '%s\n' '- [ ] keep-item - retain this block (repo: beta)'
1044
+ printf '%s' ' keep body without a final newline'
1045
+ } > "$expected_source"
1046
+
1047
+ FM_HOME="$home" "$ROOT/bin/fm-backlog-handoff.sh" design move-item >/dev/null \
1048
+ || fail "handoff before untouched EOF preservation check failed"
1049
+
1050
+ cmp -s "$expected_source" "$home/data/backlog.md" \
1051
+ || fail "handoff changed an untouched final-record terminator"
1052
+
1053
+ pass "untouched EOF line preserves its original terminator"
1054
+ }
1055
+
1056
+ test_body_handoff_is_idempotent() {
1057
+ local home="$TMP_ROOT/body-idem-main"
1058
+ local sub="$TMP_ROOT/body-idem-sub"
1059
+ setup_homes "$home" "$sub"
1060
+
1061
+ cat > "$home/data/backlog.md" <<'EOF'
1062
+ ## Queued
1063
+ - [ ] neighbor - untouched (repo: alpha)
1064
+ neighbor body
1065
+ - [ ] idem-item - multi-line for re-run (repo: alpha)
1066
+ ## Intent
1067
+ Idempotent body must not duplicate.
1068
+ final note
1069
+
1070
+ ## Done
1071
+ EOF
1072
+
1073
+ FM_HOME="$home" "$ROOT/bin/fm-backlog-handoff.sh" design idem-item >/dev/null \
1074
+ || fail "first handoff of body-carrying item failed"
1075
+
1076
+ local main_after dest_after
1077
+ main_after=$(cat "$home/data/backlog.md")
1078
+ dest_after=$(cat "$sub/data/backlog.md")
1079
+
1080
+ local out
1081
+ out=$(FM_HOME="$home" "$ROOT/bin/fm-backlog-handoff.sh" design idem-item 2>&1) \
1082
+ || fail "idempotent re-run of body-carrying item failed"
1083
+ assert_contains "$out" "already present" "re-run did not report skip of already-present key"
1084
+
1085
+ [ "$main_after" = "$(cat "$home/data/backlog.md")" ] \
1086
+ || fail "idempotent re-run mutated the main backlog"
1087
+ [ "$dest_after" = "$(cat "$sub/data/backlog.md")" ] \
1088
+ || fail "idempotent re-run mutated the secondmate backlog"
1089
+
1090
+ local count
1091
+ count=$(grep -cF -- '- [ ] idem-item - multi-line for re-run (repo: alpha)' "$sub/data/backlog.md")
1092
+ [ "$count" -eq 1 ] || fail "idempotent re-run duplicated the item header (count=$count)"
1093
+ count=$(grep -cF -- 'Idempotent body must not duplicate.' "$sub/data/backlog.md")
1094
+ [ "$count" -eq 1 ] || fail "idempotent re-run duplicated a body line (count=$count)"
1095
+ count=$(grep -cF -- ' ## Intent' "$sub/data/backlog.md")
1096
+ [ "$count" -eq 1 ] || fail "idempotent re-run duplicated indented ## Intent (count=$count)"
1097
+
1098
+ assert_grep 'neighbor' "$home/data/backlog.md" "neighbor item was disturbed by re-run"
1099
+ assert_grep ' neighbor body' "$home/data/backlog.md" "neighbor body was disturbed by re-run"
1100
+
1101
+ pass "body-carrying handoff is idempotent: re-run changes nothing"
1102
+ }
1103
+
1104
+ test_noncanonical_indented_continuations_refuse_without_changes() {
1105
+ local home="$TMP_ROOT/noncanonical-main"
1106
+ local sub="$TMP_ROOT/noncanonical-sub"
1107
+ setup_homes "$home" "$sub"
1108
+
1109
+ cat > "$home/data/backlog.md" <<'EOF'
1110
+ ## Queued
1111
+ - [ ] malformed-body - must not orphan continuations (repo: alpha)
1112
+ one-space continuation
1113
+ EOF
1114
+ printf '\ttab continuation\n' >> "$home/data/backlog.md"
1115
+ cat >> "$home/data/backlog.md" <<'EOF'
1116
+ - [ ] untouched-item - remains in the main backlog (repo: beta)
1117
+ canonical body
1118
+ EOF
1119
+ cat > "$sub/data/backlog.md" <<'EOF'
1120
+ ## Queued
1121
+ - [ ] resident-item - remains in the secondmate backlog (repo: alpha)
1122
+ resident body
1123
+ EOF
1124
+
1125
+ local source_before="$TMP_ROOT/noncanonical-source-before.md"
1126
+ local destination_before="$TMP_ROOT/noncanonical-destination-before.md"
1127
+ local out
1128
+ cp "$home/data/backlog.md" "$source_before"
1129
+ cp "$sub/data/backlog.md" "$destination_before"
1130
+
1131
+ if out=$(FM_HOME="$home" "$ROOT/bin/fm-backlog-handoff.sh" design malformed-body 2>&1); then
1132
+ fail "handoff accepted a noncanonical indented continuation"
1133
+ fi
1134
+
1135
+ assert_contains "$out" "malformed-body" "refusal did not name the selected item"
1136
+ assert_contains "$out" "one-space continuation" "refusal did not name the one-space continuation"
1137
+ assert_contains "$out" "tab continuation" "refusal did not name the tab continuation"
1138
+ cmp -s "$source_before" "$home/data/backlog.md" \
1139
+ || fail "noncanonical-continuation refusal changed the main backlog"
1140
+ cmp -s "$destination_before" "$sub/data/backlog.md" \
1141
+ || fail "noncanonical-continuation refusal changed the secondmate backlog"
1142
+
1143
+ pass "noncanonical one-space and tab continuations refuse without changes"
1144
+ }
1145
+
1146
+ test_indented_heading_is_not_section_boundary() {
1147
+ # Standalone focus on the tokenizer trap that caused the live incident.
1148
+ local home="$TMP_ROOT/intent-trap-main"
1149
+ local sub="$TMP_ROOT/intent-trap-sub"
1150
+ setup_homes "$home" "$sub" design
1151
+
1152
+ cat > "$home/data/backlog.md" <<'EOF'
1153
+ ## Queued
1154
+ - [ ] ha-codex-fast-default-4e - harness default work (repo: firstmate)
1155
+ Context for the secondmate.
1156
+ ## Intent
1157
+ Deliver the full spec, not the title alone.
1158
+ ## Acceptance
1159
+ - body survives handoff
1160
+ - ## headings inside body stay body
1161
+ - [ ] next-item - after the trap (repo: firstmate)
1162
+ EOF
1163
+
1164
+ local expected_block
1165
+ expected_block=$(extract_item_block "$home/data/backlog.md" ha-codex-fast-default-4e)
1166
+
1167
+ FM_HOME="$home" "$ROOT/bin/fm-backlog-handoff.sh" design ha-codex-fast-default-4e >/dev/null \
1168
+ || fail "handoff of ## Intent body item failed"
1169
+
1170
+ local dest_block
1171
+ dest_block=$(extract_item_block "$sub/data/backlog.md" ha-codex-fast-default-4e)
1172
+ assert_block_equals "tokenizer trap: indented ## lines must move with the item" \
1173
+ "$expected_block" "$dest_block"
1174
+
1175
+ # Source must not treat ## Intent / ## Acceptance as new sections that split the file.
1176
+ if grep -E 'ha-codex-fast-default-4e|Deliver the full spec|body survives handoff' \
1177
+ "$home/data/backlog.md" >/dev/null; then
1178
+ fail "tokenizer trap left item fragments in the source backlog"
1179
+ fi
1180
+ assert_grep 'next-item' "$home/data/backlog.md" "following item was lost after ## Intent body"
1181
+ # Exactly one real Queued section; no spurious column-0 ## Intent section invented.
1182
+ local heading_count
1183
+ heading_count=$(grep -cE '^## ' "$home/data/backlog.md")
1184
+ [ "$heading_count" -eq 1 ] || fail "source gained extra column-0 ## headings (count=$heading_count)"
1185
+ heading_count=$(grep -cE '^## ' "$sub/data/backlog.md")
1186
+ # sub scaffold has In flight / Queued / Done
1187
+ [ "$heading_count" -eq 3 ] || fail "destination has unexpected ## section count (count=$heading_count)"
1188
+
1189
+ pass "indented ## Intent / ## Acceptance are body, not section boundaries"
1190
+ }
1191
+
1192
+ test_multi_paragraph_body_with_internal_blanks_moves_whole() {
1193
+ # The live re-orphan risk: a blank line inside a multi-paragraph body must not
1194
+ # terminate the block and strand the paragraphs after it. Blank lines are body
1195
+ # content and move with the item; only the next item header or a column-0
1196
+ # section heading ends the block. Includes an indented ## after a blank.
1197
+ local home="$TMP_ROOT/multi-para-main"
1198
+ local sub="$TMP_ROOT/multi-para-sub"
1199
+ setup_homes "$home" "$sub"
1200
+
1201
+ cat > "$home/data/backlog.md" <<'EOF'
1202
+ ## Queued
1203
+ - [ ] before-multi - stays put (repo: alpha)
1204
+ before body
1205
+ - [ ] multi-para - multi-paragraph body (repo: alpha)
1206
+ First paragraph line.
1207
+
1208
+ Second paragraph after a blank.
1209
+ ## Intent
1210
+
1211
+ Indented heading then blank then more.
1212
+ final line
1213
+ - [ ] after-multi - subsequent item (repo: alpha)
1214
+ after body
1215
+
1216
+ ## Done
1217
+ EOF
1218
+
1219
+ local expected_block
1220
+ expected_block=$(extract_item_block "$home/data/backlog.md" multi-para)
1221
+
1222
+ FM_HOME="$home" "$ROOT/bin/fm-backlog-handoff.sh" design multi-para >/dev/null \
1223
+ || fail "handoff of multi-paragraph body failed"
1224
+
1225
+ local dest_block
1226
+ dest_block=$(extract_item_block "$sub/data/backlog.md" multi-para)
1227
+ assert_block_equals "multi-paragraph body with internal blanks must move whole" \
1228
+ "$expected_block" "$dest_block"
1229
+
1230
+ # Every body line, including the ones after each internal blank, must leave the source.
1231
+ assert_no_grep 'multi-para' "$home/data/backlog.md" "multi-para header still in source"
1232
+ assert_no_grep 'First paragraph line' "$home/data/backlog.md" "first paragraph orphaned in source"
1233
+ assert_no_grep 'Second paragraph after a blank' "$home/data/backlog.md" "post-blank paragraph orphaned in source"
1234
+ assert_no_grep 'Indented heading then blank then more' "$home/data/backlog.md" "post-blank body orphaned in source"
1235
+ assert_no_grep 'final line' "$home/data/backlog.md" "trailing body orphaned in source"
1236
+ assert_no_grep '## Intent' "$home/data/backlog.md" "indented ## Intent left in source as if a section"
1237
+
1238
+ # The post-blank paragraphs must actually arrive at the destination.
1239
+ assert_grep ' Second paragraph after a blank.' "$sub/data/backlog.md" "post-blank paragraph did not arrive"
1240
+ assert_grep ' Indented heading then blank then more.' "$sub/data/backlog.md" "post-blank body did not arrive"
1241
+ assert_grep ' ## Intent' "$sub/data/backlog.md" "indented ## Intent did not arrive at destination"
1242
+
1243
+ # Neighbors on both sides stay intact.
1244
+ assert_grep 'before-multi' "$home/data/backlog.md" "before-multi was wrongly removed"
1245
+ assert_grep ' before body' "$home/data/backlog.md" "before-multi body was disturbed"
1246
+ assert_grep 'after-multi' "$home/data/backlog.md" "after-multi was wrongly removed"
1247
+ assert_grep ' after body' "$home/data/backlog.md" "after-multi body was disturbed"
1248
+
1249
+ # Idempotent re-run: already present, no duplication, no mutation.
1250
+ local main_after dest_after
1251
+ main_after=$(cat "$home/data/backlog.md")
1252
+ dest_after=$(cat "$sub/data/backlog.md")
1253
+ local out
1254
+ out=$(FM_HOME="$home" "$ROOT/bin/fm-backlog-handoff.sh" design multi-para 2>&1) \
1255
+ || fail "idempotent re-run of multi-paragraph body failed"
1256
+ assert_contains "$out" "already present" "re-run did not report skip of already-present key"
1257
+ [ "$main_after" = "$(cat "$home/data/backlog.md")" ] \
1258
+ || fail "idempotent re-run mutated the main backlog"
1259
+ [ "$dest_after" = "$(cat "$sub/data/backlog.md")" ] \
1260
+ || fail "idempotent re-run mutated the secondmate backlog"
1261
+ local count
1262
+ count=$(grep -cF -- ' Second paragraph after a blank.' "$sub/data/backlog.md")
1263
+ [ "$count" -eq 1 ] || fail "idempotent re-run duplicated a post-blank paragraph (count=$count)"
1264
+
1265
+ pass "multi-paragraph body with internal blank lines moves whole and is idempotent"
1266
+ }
1267
+
1268
+ # Registry lines may carry parentheticals in the summary before the structured
1269
+ # (home: ...) field (e.g. "(id is legacy)"). The home extractor must still find
1270
+ # the field; the old ^[^(]* regex treated those entries as home-less.
1271
+ test_registry_home_with_pre_home_parentheses() {
1272
+ local home="$TMP_ROOT/reg-parens-main"
1273
+ local sub="$TMP_ROOT/reg-parens-sub"
1274
+ local id=oss-triage-t4
1275
+ setup_homes "$home" "$sub" "$id"
1276
+ local sub_abs
1277
+ sub_abs=$(cd "$sub" && pwd -P)
1278
+ # Prose parentheses before (home: ...) and punctuation inside scope match the live registry shape.
1279
+ printf -- '- %s - issue triage (id is legacy) (home: %s; scope: issue triage (child); semicolon is meaningful; projects: alpha; added 2026-07-09)\n' \
1280
+ "$id" "$sub_abs" > "$home/data/secondmates.md"
1281
+ FM_HOME="$home" "$ROOT/bin/fm-home-seed.sh" validate >/dev/null \
1282
+ || fail "home-seed validation rejected punctuation-bearing registry fields"
1283
+
1284
+ cat > "$home/data/backlog.md" <<'EOF'
1285
+ ## Queued
1286
+ - [ ] paren-item - should hand off (repo: alpha)
1287
+ body line
1288
+
1289
+ ## Done
1290
+ EOF
1291
+
1292
+ FM_HOME="$home" "$ROOT/bin/fm-backlog-handoff.sh" "$id" paren-item >/dev/null \
1293
+ || fail "handoff failed for registry entry with parentheses before (home: ...)"
1294
+
1295
+ assert_grep 'paren-item' "$sub/data/backlog.md" \
1296
+ "item did not arrive when registry summary had pre-home parentheses"
1297
+ assert_no_grep 'paren-item' "$home/data/backlog.md" \
1298
+ "item still in source after handoff with pre-home parentheses"
1299
+
1300
+ pass "registry home parses when summary has parentheses before (home: ...)"
1301
+ }
1302
+
1303
+ # An entry that genuinely lacks (home: ...) must still fail cleanly (empty parse
1304
+ # surfaces as "has no home"), not succeed or mis-parse prose.
1305
+ test_registry_home_missing_field_fails_cleanly() {
1306
+ local home="$TMP_ROOT/reg-nohome-main"
1307
+ local sub="$TMP_ROOT/reg-nohome-sub"
1308
+ local id=no-home-mate
1309
+ mkdir -p "$home/data" "$home/state"
1310
+ seed_secondmate_home_marker "$sub" "$id"
1311
+ # Registered, but no structured (home: ...) field at all.
1312
+ printf -- '- %s - charter only (scope prose mentions home: /tmp/ignored-path)\n' \
1313
+ "$id" > "$home/data/secondmates.md"
1314
+
1315
+ cat > "$home/data/backlog.md" <<'EOF'
1316
+ ## Queued
1317
+ - [ ] orphan-item - never moves (repo: alpha)
1318
+
1319
+ ## Done
1320
+ EOF
1321
+
1322
+ local out rc=0
1323
+ out=$(FM_HOME="$home" "$ROOT/bin/fm-backlog-handoff.sh" "$id" orphan-item 2>&1) || rc=$?
1324
+ [ "$rc" -ne 0 ] || fail "handoff succeeded for registry entry with no (home: ...) field"
1325
+ assert_contains "$out" "has no home" \
1326
+ "missing (home: ...) field did not report the clean 'has no home' error"
1327
+ assert_grep 'orphan-item' "$home/data/backlog.md" \
1328
+ "source backlog was mutated despite missing home"
1329
+ [ ! -f "$sub/data/backlog.md" ] || ! grep -q 'orphan-item' "$sub/data/backlog.md" 2>/dev/null \
1330
+ || fail "item appeared in secondmate backlog despite missing home"
1331
+
1332
+ pass "registry entry without (home: ...) fails cleanly with has no home"
1333
+ }
1334
+
1335
+ test_handoff_wakes_live_local_receiver
1336
+ test_failed_wake_retries_when_the_item_is_already_present
1337
+ test_known_receiver_failure_remains_retryable_after_grace
1338
+ test_known_failure_restores_retry_after_reconciliation_race
1339
+ test_move_crash_keeps_wake_pending_for_recovery
1340
+ test_pre_move_crash_does_not_wake_until_move_lands
1341
+ test_delivery_confirmation_crash_does_not_resend
1342
+ test_unresolved_delivery_attempt_refuses_immediate_resend
1343
+ test_concurrent_local_handoffs_serialize_move_and_wake
1344
+ test_local_teardown_waits_for_handoff_wake
1345
+ test_local_teardown_preserves_wake_when_home_removal_fails
1346
+ test_body_moves_when_followed_by_another_item
1347
+ test_body_moves_when_followed_by_section_heading
1348
+ test_multi_paragraph_body_with_internal_blanks_moves_whole
1349
+ test_body_moves_when_last_lines_of_file
1350
+ test_eof_body_before_seeded_destination_section_keeps_boundary
1351
+ test_untouched_eof_line_preserves_terminator
1352
+ test_body_handoff_is_idempotent
1353
+ test_noncanonical_indented_continuations_refuse_without_changes
1354
+ test_indented_heading_is_not_section_boundary
1355
+ test_registry_home_with_pre_home_parentheses
1356
+ test_registry_home_missing_field_fails_cleanly
1357
+ test_handoff_warns_when_a_moved_item_still_owes_a_public_reply
1358
+ test_handoff_is_silent_about_public_commitments_without_the_relay
1359
+
1360
+ echo "ALL TESTS PASSED"