@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,1224 @@
1
+ #!/usr/bin/env bash
2
+ # tests/fm-wake-queue.test.sh - wake-queue losslessness (the queue safety matrix):
3
+ # concurrent append/drain, bounded structural enrichment, interruption safety,
4
+ # signal catch-up while no watcher runs, stale/check enqueue-before-suppressor
5
+ # ordering, atomic double-drain, duplicate collapse, and liveness assertion.
6
+ # Nothing is lost and nothing is double-consumed. General watcher/lock liveness
7
+ # lives in fm-watcher-lock.test.sh; daemon classification/injection in
8
+ # fm-daemon.test.sh.
9
+ set -u
10
+
11
+ # shellcheck source=tests/wake-helpers.sh
12
+ . "$(dirname "${BASH_SOURCE[0]}")/wake-helpers.sh"
13
+
14
+ WATCH="$ROOT/bin/fm-watch.sh"
15
+ DRAIN="$ROOT/bin/fm-wake-drain.sh"
16
+ GRANT="$ROOT/bin/fm-wake-grant.sh"
17
+
18
+ TMP_ROOT=$(fm_test_tmproot fm-wake-tests)
19
+
20
+
21
+ test_concurrent_append_and_drain() {
22
+ local dir state out1 out2 pids i pid count unique malformed sequence generation
23
+ dir=$(make_case concurrent)
24
+ state="$dir/state"
25
+ out1="$dir/drain-one.out"
26
+ out2="$dir/drain-two.out"
27
+ pids=
28
+ i=1
29
+ while [ "$i" -le 40 ]; do
30
+ append_wake "$state" signal "status-$i" "signal: $state/status-$i.status" &
31
+ pids="$pids $!"
32
+ i=$((i + 1))
33
+ done
34
+ FM_STATE_OVERRIDE="$state" "$DRAIN" > "$out1" &
35
+ pids="$pids $!"
36
+ for pid in $pids; do
37
+ wait "$pid" || fail "concurrent append/drain subprocess failed"
38
+ done
39
+ FM_STATE_OVERRIDE="$state" "$DRAIN" > "$out2" 2> "$dir/drain-two.err" || fail "final drain failed"
40
+ count=$(awk -F '\t' 'NF == 5 { count++ } END { print count + 0 }' "$out2")
41
+ [ "$count" -eq 40 ] || fail "expected final replay of 40 durable records, got $count"
42
+ malformed=$(awk -F '\t' 'NF && NF != 5 { bad++ } END { print bad + 0 }' "$out2")
43
+ [ "$malformed" -eq 0 ] || fail "drained records had malformed fields"
44
+ unique=$(awk -F '\t' 'NF == 5 { keys[$4] = 1 } END { for (k in keys) count++; print count + 0 }' "$out2")
45
+ [ "$unique" -eq 40 ] || fail "expected 40 unique keys, got $unique"
46
+ [ -s "$state/.wake-queue" ] || fail "concurrent drain consumed records before handling acknowledgement"
47
+ sequence=$(sed -n 's/^WAKE_ACK_REQUIRED:.*--ack-through \([0-9][0-9]*\) --recovery-generation [A-Za-z0-9._-][A-Za-z0-9._-]*$/\1/p' "$dir/drain-two.err")
48
+ generation=$(sed -n 's/^WAKE_ACK_REQUIRED:.*--ack-through [0-9][0-9]* --recovery-generation \([A-Za-z0-9._-][A-Za-z0-9._-]*\)$/\1/p' "$dir/drain-two.err")
49
+ [ -n "$sequence" ] && [ -n "$generation" ] || fail "final replay omitted its acknowledgement boundary"
50
+ FM_STATE_OVERRIDE="$state" "$DRAIN" --ack-through "$sequence" --recovery-generation "$generation" \
51
+ || fail "concurrent records could not be acknowledged"
52
+ [ ! -s "$state/.wake-queue" ] || fail "acknowledged concurrent records remained queued"
53
+ pass "concurrent append plus drain preserves durable records through acknowledgement"
54
+ }
55
+
56
+ test_signal_catchup_without_running_watcher() {
57
+ local dir state fakebin out drain_out drain_err status_file sequence generation
58
+ dir=$(make_case signal)
59
+ state="$dir/state"
60
+ fakebin="$dir/fakebin"
61
+ out="$dir/watch.out"
62
+ drain_out="$dir/drain.out"
63
+ drain_err="$dir/drain.err"
64
+ status_file="$state/task.status"
65
+ # The durable-queue catch-up contract applies to ACTIONABLE wakes (the always-on
66
+ # watcher can absorb no-verb working: notes when the crew is provably working).
67
+ # Use a captain-relevant verb so the wake is surfaced and the catch-up path is
68
+ # tested.
69
+ printf 'blocked: first\n' > "$status_file"
70
+ PATH="$fakebin:$PATH" FM_STATE_OVERRIDE="$state" FM_POLL=1 FM_SIGNAL_GRACE=1 FM_CHECK_INTERVAL=999999 FM_HEARTBEAT=999999 "$WATCH" > "$out" &
71
+ wait_for_exit "$!" 40 || fail "watcher did not exit for first signal"
72
+ grep -F "signal: $status_file" "$out" >/dev/null || fail "watcher did not print first signal"
73
+ FM_STATE_OVERRIDE="$state" "$DRAIN" > "$drain_out" 2> "$drain_err" || fail "drain after first signal failed"
74
+ grep "$(printf '\tsignal\t')" "$drain_out" | grep -F "$status_file" >/dev/null || fail "first signal was not queued"
75
+ sequence=$(sed -n 's/^WAKE_ACK_REQUIRED:.*--ack-through \([0-9][0-9]*\) --recovery-generation [A-Za-z0-9._-][A-Za-z0-9._-]*$/\1/p' "$drain_err")
76
+ generation=$(sed -n 's/^WAKE_ACK_REQUIRED:.*--ack-through [0-9][0-9]* --recovery-generation \([A-Za-z0-9._-][A-Za-z0-9._-]*\)$/\1/p' "$drain_err")
77
+ FM_STATE_OVERRIDE="$state" "$DRAIN" --ack-through "$sequence" --recovery-generation "$generation" \
78
+ || fail "first signal handling acknowledgement failed"
79
+
80
+ printf 'done: second\n' >> "$status_file"
81
+ : > "$out"
82
+ PATH="$fakebin:$PATH" FM_STATE_OVERRIDE="$state" FM_POLL=1 FM_SIGNAL_GRACE=1 FM_CHECK_INTERVAL=999999 FM_HEARTBEAT=999999 "$WATCH" > "$out" &
83
+ wait_for_exit "$!" 40 || fail "watcher did not exit for second signal"
84
+ grep -F "signal: $status_file" "$out" >/dev/null || fail "signal written with no watcher was not caught"
85
+ pass "signal written while no watcher runs is caught on next run"
86
+ }
87
+
88
+ test_stale_enqueue_before_suppressor() {
89
+ local dir state fakebin out drain_out capture_file window key pane_hash
90
+ dir=$(make_case stale)
91
+ state="$dir/state"
92
+ fakebin="$dir/fakebin"
93
+ out="$dir/watch.out"
94
+ drain_out="$dir/drain.out"
95
+ capture_file="$dir/pane.txt"
96
+ window="test:fm-stale"
97
+ printf 'idle prompt' > "$capture_file"
98
+ printf 'window=%s\nkind=ship\n' "$window" > "$state/stale.meta"
99
+ # A stale pane sitting on a captain-relevant status is actionable when the crew
100
+ # is not provably working, so give the window one and prime the .seen-* marker
101
+ # to its current signature so the per-poll signal scan does not pre-empt the
102
+ # stale wake with a signal wake.
103
+ printf 'done: ready in branch fm/stale\n' > "$state/stale.status"
104
+ prime_status_seen "$state" "$state/stale.status"
105
+ key=$(printf '%s' "$window" | tr ':/.' '___')
106
+ pane_hash=$(hash_text "idle prompt")
107
+ printf '%s' "$pane_hash" > "$state/.hash-$key"
108
+ printf '1\n' > "$state/.count-$key"
109
+ PATH="$fakebin:$PATH" FM_FAKE_TMUX_WINDOW="$window" FM_FAKE_TMUX_CAPTURE="$capture_file" FM_STATE_OVERRIDE="$state" FM_POLL=1 FM_SIGNAL_GRACE=1 FM_CHECK_INTERVAL=999999 FM_HEARTBEAT=999999 "$WATCH" > "$out" &
110
+ wait_for_exit "$!" 40 || fail "watcher did not exit for stale pane"
111
+ grep -Fx "stale: $window" "$out" >/dev/null || fail "watcher did not print stale wake"
112
+ FM_STATE_OVERRIDE="$state" "$DRAIN" > "$drain_out" || fail "drain after stale wake failed"
113
+ grep "$(printf '\tstale\t')" "$drain_out" | grep -F "$window" >/dev/null || fail "stale wake was not queued"
114
+ [ "$(cat "$state/.stale-$key" 2>/dev/null || true)" = "$pane_hash" ] || fail "stale suppressor was not written"
115
+ pass "stale wake is queued before suppressor state is advanced"
116
+ }
117
+
118
+ # Absorb-only-when-provably-working adds a new actionable wake: a non-terminal stale
119
+ # whose crew is NOT provably working is surfaced immediately. That new path must keep
120
+ # the queue-safety invariant - enqueue the stale wake BEFORE advancing the .stale-*
121
+ # suppressor - so a watcher killed between the two never swallows the surfaced finish.
122
+ test_not_working_stale_enqueue_before_suppressor() {
123
+ local dir state fakebin out drain_out capture_file window key pane_hash
124
+ dir=$(make_case stale-stopped)
125
+ state="$dir/state"
126
+ fakebin="$dir/fakebin"
127
+ out="$dir/watch.out"
128
+ drain_out="$dir/drain.out"
129
+ capture_file="$dir/pane.txt"
130
+ window="test:fm-stopped"
131
+ printf 'idle prompt, finished' > "$capture_file"
132
+ printf 'window=%s\nkind=ship\n' "$window" > "$state/stopped.meta"
133
+ # Non-terminal status (no captain-relevant verb); prime .seen-* so the per-poll
134
+ # signal scan does not pre-empt the stale path.
135
+ printf 'working: implementing\n' > "$state/stopped.status"
136
+ prime_status_seen "$state" "$state/stopped.status"
137
+ key=$(printf '%s' "$window" | tr ':/.' '___')
138
+ pane_hash=$(hash_text "idle prompt, finished")
139
+ printf '%s' "$pane_hash" > "$state/.hash-$key"
140
+ printf '1\n' > "$state/.count-$key"
141
+ # NOT provably working: no running pipeline, idle pane. (make_case installed the
142
+ # fake fm-crew-state.sh the watcher reads via FM_CREW_STATE_BIN.)
143
+ export FM_FAKE_CREW_STATE='state: unknown · source: none · no current-state source available'
144
+ PATH="$fakebin:$PATH" FM_FAKE_TMUX_WINDOW="$window" FM_FAKE_TMUX_CAPTURE="$capture_file" \
145
+ FM_STATE_OVERRIDE="$state" FM_CREW_STATE_BIN="$fakebin/fm-crew-state.sh" \
146
+ FM_STALE_ESCALATE_SECS=999 FM_POLL=1 FM_SIGNAL_GRACE=1 FM_CHECK_INTERVAL=999999 FM_HEARTBEAT=999999 "$WATCH" > "$out" &
147
+ wait_for_exit "$!" 40 || fail "watcher did not surface a not-provably-working stale"
148
+ grep -Fx "stale: $window" "$out" >/dev/null || fail "watcher did not print the immediate stale wake"
149
+ FM_STATE_OVERRIDE="$state" "$DRAIN" > "$drain_out" || fail "drain after the immediate stale wake failed"
150
+ grep "$(printf '\tstale\t')" "$drain_out" | grep -F "$window" >/dev/null || fail "immediate stale wake was not queued"
151
+ [ "$(cat "$state/.stale-$key" 2>/dev/null || true)" = "$pane_hash" ] || fail "stale suppressor was not advanced after the enqueue"
152
+ unset FM_FAKE_CREW_STATE
153
+ pass "a not-provably-working stale wake is queued before its suppressor is advanced"
154
+ }
155
+
156
+ test_check_output_is_queued() {
157
+ local dir state fakebin out drain_out check_file
158
+ dir=$(make_case check)
159
+ state="$dir/state"
160
+ fakebin="$dir/fakebin"
161
+ out="$dir/watch.out"
162
+ drain_out="$dir/drain.out"
163
+ check_file="$state/task.check.sh"
164
+ cat > "$check_file" <<'SH'
165
+ #!/usr/bin/env bash
166
+ printf 'merged: https://example.test/pr/1\n'
167
+ SH
168
+ chmod 0700 "$check_file"
169
+ FM_STATE_OVERRIDE="$state" "$ROOT/bin/fm-check-register.sh" task >/dev/null \
170
+ || fail "could not register queue custom check"
171
+ PATH="$fakebin:$PATH" FM_STATE_OVERRIDE="$state" FM_POLL=1 FM_SIGNAL_GRACE=1 FM_CHECK_INTERVAL=0 FM_HEARTBEAT=999999 "$WATCH" > "$out" &
172
+ wait_for_exit "$!" 40 || fail "watcher did not exit for check output"
173
+ grep -F "check: $check_file: merged: https://example.test/pr/1" "$out" >/dev/null || fail "watcher did not print check wake"
174
+ FM_STATE_OVERRIDE="$state" "$DRAIN" > "$drain_out" || fail "drain after check wake failed"
175
+ grep "$(printf '\tcheck\t')" "$drain_out" | grep -F "$check_file" | grep -F 'merged: https://example.test/pr/1' >/dev/null || fail "check wake was not queued"
176
+ [ -e "$state/.last-check" ] || fail "check cadence marker was not written after queue append"
177
+ pass "registered custom check output is queued before cadence suppression"
178
+ }
179
+
180
+ test_atomic_double_drain() {
181
+ local dir state out1 out2 count1 count2 sequence generation leftover
182
+ dir=$(make_case double-drain)
183
+ state="$dir/state"
184
+ out1="$dir/drain-one.out"
185
+ out2="$dir/drain-two.out"
186
+ append_wake "$state" heartbeat heartbeat heartbeat || fail "heartbeat append failed"
187
+ append_wake "$state" signal task "signal: $state/task.status" || fail "signal append failed"
188
+ append_wake "$state" stale 's:fm-task' 'stale: s:fm-task' || fail "stale append failed"
189
+ FM_STATE_OVERRIDE="$state" "$DRAIN" > "$out1" 2> "$dir/drain-one.err" &
190
+ pid1=$!
191
+ FM_STATE_OVERRIDE="$state" "$DRAIN" > "$out2" 2> "$dir/drain-two.err" &
192
+ pid2=$!
193
+ wait "$pid1" || fail "first drain failed"
194
+ wait "$pid2" || fail "second drain failed"
195
+ count1=$(awk -F '\t' 'NF == 5 { count++ } END { print count + 0 }' "$out1")
196
+ count2=$(awk -F '\t' 'NF == 5 { count++ } END { print count + 0 }' "$out2")
197
+ [ "$count1" -eq 3 ] && [ "$count2" -eq 3 ] \
198
+ || fail "unacknowledged concurrent drains did not replay all three records"
199
+ cmp -s "$out1" "$out2" || fail "concurrent pre-ack replays were not deterministic"
200
+ [ -s "$state/.wake-queue" ] || fail "concurrent drains consumed records before acknowledgement"
201
+ sequence=$(sed -n 's/^WAKE_ACK_REQUIRED:.*--ack-through \([0-9][0-9]*\) --recovery-generation [A-Za-z0-9._-][A-Za-z0-9._-]*$/\1/p' "$dir/drain-two.err")
202
+ generation=$(sed -n 's/^WAKE_ACK_REQUIRED:.*--ack-through [0-9][0-9]* --recovery-generation \([A-Za-z0-9._-][A-Za-z0-9._-]*\)$/\1/p' "$dir/drain-two.err")
203
+ [ -n "$sequence" ] && [ -n "$generation" ] || fail "concurrent replay omitted its acknowledgement boundary"
204
+ FM_STATE_OVERRIDE="$state" "$DRAIN" --ack-through "$sequence" --recovery-generation "$generation" \
205
+ || fail "concurrent replay acknowledgement failed"
206
+ [ ! -s "$state/.wake-queue" ] || fail "acknowledgement did not consume replayed records"
207
+ leftover=$(FM_STATE_OVERRIDE="$state" "$DRAIN" | awk -F '\t' 'NF == 5 { count++ } END { print count + 0 }')
208
+ [ "$leftover" -eq 0 ] || fail "acknowledged records replayed again"
209
+ pass "concurrent drains replay until one post-handling acknowledgement consumes records"
210
+ }
211
+
212
+ test_drain_dedupes_obvious_duplicates() {
213
+ local dir state out count
214
+ dir=$(make_case dedupe)
215
+ state="$dir/state"
216
+ out="$dir/drain.out"
217
+ append_wake "$state" heartbeat heartbeat heartbeat || fail "first heartbeat append failed"
218
+ append_wake "$state" signal task.status "signal: $state/task.status" || fail "first signal append failed"
219
+ append_wake "$state" heartbeat heartbeat heartbeat || fail "second heartbeat append failed"
220
+ append_wake "$state" signal task.status "signal: $state/task.status $state/task.turn-ended" || fail "second signal append failed"
221
+ FM_STATE_OVERRIDE="$state" "$DRAIN" > "$out" || fail "dedupe drain failed"
222
+ count=$(awk 'NF { count++ } END { print count + 0 }' "$out")
223
+ [ "$count" -eq 2 ] || fail "expected 2 deduped records, got $count"
224
+ grep "$(printf '\theartbeat\theartbeat\theartbeat')" "$out" >/dev/null || fail "heartbeat was not preserved"
225
+ grep "$(printf '\tsignal\ttask.status\t')" "$out" | grep -F "$state/task.turn-ended" >/dev/null || fail "latest signal payload was not preserved"
226
+ pass "drain collapses obvious duplicate heartbeat and signal records"
227
+ }
228
+
229
+ # The drain runs at the top of every wake-handling turn, so it also asserts
230
+ # watcher liveness via fm-guard.sh: a lapsed re-arm chain then surfaces even on a
231
+ # plain drain-and-handle turn that runs no other supervision script. It must warn
232
+ # when work is in flight with no live watcher, and stay silent right after a
233
+ # normal fire from a live watcher with a fresh beacon, so it never false-alarms.
234
+ test_secondmate_foreign_queue_stall_is_one_shot_and_read_only() {
235
+ local dir state sub fakebin out row_before row_after stall_count
236
+ dir=$(make_case secondmate-foreign-stall)
237
+ state="$dir/state"
238
+ sub="$dir/secondmate"
239
+ mkdir -p "$sub/state" "$sub/data" "$sub/bin"
240
+ printf '# Firstmate\n' > "$sub/AGENTS.md"
241
+ printf 'mate\n' > "$sub/.fm-secondmate-home"
242
+ printf 'window=firstmate:fm-mate\nkind=secondmate\nharness=claude\nbackend=tmux\nhome=%s\n' \
243
+ "$sub" > "$state/mate.meta"
244
+ printf '%s\t7\tcheck\trouted\tcheck: routed row\n' "$(( $(date +%s) - 10 ))" > "$sub/state/.wake-queue"
245
+ row_before="$dir/foreign-before"
246
+ row_after="$dir/foreign-after"
247
+ cp "$sub/state/.wake-queue" "$row_before"
248
+ fakebin="$dir/fakebin"
249
+ cat > "$fakebin/tmux" <<'SH'
250
+ #!/usr/bin/env bash
251
+ case "${1:-}" in
252
+ list-windows) printf '%s\n' "${FM_FAKE_TMUX_WINDOW:-}" ;;
253
+ capture-pane) cat "${FM_FAKE_TMUX_CAPTURE:-/dev/null}" ;;
254
+ display-message) printf '0\n' ;;
255
+ *) exit 0 ;;
256
+ esac
257
+ SH
258
+ chmod +x "$fakebin/tmux"
259
+ out="$dir/watch.out"
260
+
261
+ PATH="$fakebin:$PATH" FM_HOME="$dir" FM_ROOT_OVERRIDE="$ROOT" \
262
+ FM_STATE_OVERRIDE="$state" FM_FAKE_TMUX_WINDOW='firstmate:fm-mate' \
263
+ FM_FAKE_TMUX_LOG="$dir/tmux.log" FM_FAKE_TMUX_CAPTURE="$dir/fake-tmux/pane.txt" \
264
+ FM_SECONDMATE_WAKE_STALL_SECS=1 FM_POLL=1 FM_SIGNAL_GRACE=0 \
265
+ FM_CHECK_INTERVAL=999999 FM_HEARTBEAT=999999 \
266
+ "$ROOT/bin/fm-watch-checkpoint.sh" --seconds 3 > "$out" 2> "$dir/watch.err" || true
267
+ grep -F 'check: secondmate wake-loop stalled: mate=mate row=7' "$out" >/dev/null \
268
+ || fail "an aged foreign row did not wake the parent checkpoint: $(cat "$out"); err=$(cat "$dir/watch.err"); meta=$(cat "$state/mate.meta"); foreign=$(cat "$sub/state/.wake-queue")"
269
+ [ -s "$state/.wake-queue" ] || fail "the parent notification was not durable"
270
+ stall_count=$(grep -c 'secondmate-wake-loop-mate-' "$state/.wake-queue" || true)
271
+ [ "$stall_count" -eq 1 ] || fail "the first parent checkpoint did not publish exactly one stall notification"
272
+
273
+ cmp -s "$row_before" "$sub/state/.wake-queue" \
274
+ || fail "foreign queue row changed during read-only stall detection"
275
+ FM_STATE_OVERRIDE="$state" "$DRAIN" > "$dir/drain.out" 2> "$dir/drain.err" \
276
+ || fail "parent drain failed after the stall notification"
277
+ ack_drain_err "$state" "$dir/drain.err" \
278
+ || fail "parent stall notification could not be acknowledged"
279
+
280
+ sleep 1
281
+ PATH="$fakebin:$PATH" FM_HOME="$dir" FM_ROOT_OVERRIDE="$ROOT" \
282
+ FM_STATE_OVERRIDE="$state" FM_FAKE_TMUX_WINDOW='firstmate:fm-mate' \
283
+ FM_FAKE_TMUX_LOG="$dir/tmux.log" FM_FAKE_TMUX_CAPTURE="$dir/fake-tmux/pane.txt" \
284
+ FM_SECONDMATE_WAKE_STALL_SECS=1 FM_POLL=1 FM_SIGNAL_GRACE=0 \
285
+ FM_CHECK_INTERVAL=999999 FM_HEARTBEAT=999999 \
286
+ "$ROOT/bin/fm-watch-checkpoint.sh" --seconds 2 > "$dir/watch-second.out" 2> "$dir/watch-second.err" || true
287
+ [ ! -s "$state/.wake-queue" ] || {
288
+ stall_count=$(grep -c 'secondmate-wake-loop-mate-' "$state/.wake-queue" || true)
289
+ [ "$stall_count" -eq 0 ] || fail "repeated checkpoint re-published the same stall notification"
290
+ }
291
+ cp "$sub/state/.wake-queue" "$row_after"
292
+ cmp -s "$row_before" "$row_after" || fail "foreign queue changed after idempotent re-check"
293
+
294
+ : > "$sub/state/.wake-queue"
295
+ PATH="$fakebin:$PATH" FM_HOME="$dir" FM_ROOT_OVERRIDE="$ROOT" \
296
+ FM_STATE_OVERRIDE="$state" FM_FAKE_TMUX_WINDOW='firstmate:fm-mate' \
297
+ FM_FAKE_TMUX_LOG="$dir/tmux.log" FM_FAKE_TMUX_CAPTURE="$dir/fake-tmux/pane.txt" \
298
+ FM_SECONDMATE_WAKE_STALL_SECS=1 FM_POLL=1 FM_SIGNAL_GRACE=0 \
299
+ FM_CHECK_INTERVAL=999999 FM_HEARTBEAT=999999 \
300
+ "$ROOT/bin/fm-watch-checkpoint.sh" --seconds 2 > "$dir/watch-empty.out" 2> "$dir/watch-empty.err" || true
301
+ ! grep -F 'secondmate wake-loop stalled' "$dir/watch-empty.out" >/dev/null \
302
+ || fail "an empty foreign queue produced a stall notification"
303
+
304
+ printf '%s\t8\tcheck\thealthy\tcheck: healthy row\n' "$(date +%s)" > "$sub/state/.wake-queue"
305
+ PATH="$fakebin:$PATH" FM_HOME="$dir" FM_ROOT_OVERRIDE="$ROOT" \
306
+ FM_STATE_OVERRIDE="$state" FM_FAKE_TMUX_WINDOW='firstmate:fm-mate' \
307
+ FM_FAKE_TMUX_LOG="$dir/tmux.log" FM_FAKE_TMUX_CAPTURE="$dir/fake-tmux/pane.txt" \
308
+ FM_SECONDMATE_WAKE_STALL_SECS=60 FM_POLL=1 FM_SIGNAL_GRACE=0 \
309
+ FM_CHECK_INTERVAL=999999 FM_HEARTBEAT=999999 \
310
+ "$ROOT/bin/fm-watch-checkpoint.sh" --seconds 2 > "$dir/watch-healthy.out" 2> "$dir/watch-healthy.err" || true
311
+ ! grep -F 'secondmate wake-loop stalled' "$dir/watch-healthy.out" >/dev/null \
312
+ || fail "a healthy foreign queue produced a stall notification"
313
+ pass "foreign secondmate queue stalls notify once, remain byte-stable, and stay quiet when empty or healthy"
314
+ }
315
+
316
+ test_secondmate_stall_marker_rejects_symlink() {
317
+ local dir state sub fakebin marker outside expected
318
+ dir=$(make_case secondmate-stall-marker-symlink)
319
+ state="$dir/state"
320
+ sub="$dir/secondmate"
321
+ mkdir -p "$sub/state"
322
+ printf 'mate\n' > "$sub/.fm-secondmate-home"
323
+ printf 'window=firstmate:fm-mate\nkind=secondmate\nhome=%s\n' "$sub" > "$state/mate.meta"
324
+ printf '%s\t7\tcheck\trouted\tcheck: routed row\n' "$(( $(date +%s) - 10 ))" > "$sub/state/.wake-queue"
325
+ outside="$dir/outside"
326
+ expected='must remain unchanged'
327
+ printf '%s\n' "$expected" > "$outside"
328
+ marker="$state/.secondmate-wake-stall-mate"
329
+ ln -s "$outside" "$marker"
330
+ fakebin="$dir/fakebin"
331
+ cat > "$fakebin/tmux" <<'SH'
332
+ #!/usr/bin/env bash
333
+ case "${1:-}" in
334
+ list-windows) printf '%s\n' 'firstmate:fm-mate' ;;
335
+ capture-pane) : ;;
336
+ display-message) printf '0\n' ;;
337
+ *) exit 0 ;;
338
+ esac
339
+ SH
340
+ chmod +x "$fakebin/tmux"
341
+
342
+ PATH="$fakebin:$PATH" FM_HOME="$dir" FM_ROOT_OVERRIDE="$ROOT" \
343
+ FM_STATE_OVERRIDE="$state" FM_SECONDMATE_WAKE_STALL_SECS=1 FM_POLL=1 \
344
+ FM_SIGNAL_GRACE=0 FM_CHECK_INTERVAL=999999 FM_HEARTBEAT=999999 \
345
+ "$ROOT/bin/fm-watch-checkpoint.sh" --seconds 2 \
346
+ > "$dir/watch.out" 2> "$dir/watch.err" || true
347
+ [ "$(cat "$outside")" = "$expected" ] || fail "stall marker write followed an unsafe symlink"
348
+ [ -L "$marker" ] || fail "stall marker write replaced rather than rejected an unsafe path"
349
+ [ ! -s "$state/.wake-queue" ] || fail "unsafe stall marker path still published a parent notification"
350
+ pass "secondmate stall markers reject symlinks without touching their targets"
351
+ }
352
+
353
+ test_acknowledged_stall_publication_survives_pre_marker_crash() {
354
+ local dir state sub fakebin out epoch row_before
355
+ dir=$(make_case secondmate-stall-crash)
356
+ state="$dir/state"
357
+ sub="$dir/secondmate"
358
+ mkdir -p "$sub/state" "$sub/data"
359
+ printf 'mate\n' > "$sub/.fm-secondmate-home"
360
+ printf 'window=firstmate:fm-mate\nkind=secondmate\nharness=claude\nbackend=tmux\nhome=%s\n' \
361
+ "$sub" > "$state/mate.meta"
362
+ epoch=$(( $(date +%s) - 10 ))
363
+ printf '%s\t7\tcheck\trouted\tcheck: routed row\n' "$epoch" > "$sub/state/.wake-queue"
364
+ row_before="$dir/foreign-before"
365
+ cp "$sub/state/.wake-queue" "$row_before"
366
+ append_wake "$state" check "secondmate-wake-loop-mate-$epoch-7" \
367
+ "check: secondmate wake-loop stalled: mate=mate row=7 age=10s" \
368
+ || fail "could not seed the pre-marker crash publication"
369
+ FM_STATE_OVERRIDE="$state" "$DRAIN" > "$dir/drain.out" 2> "$dir/drain.err" \
370
+ || fail "pre-marker crash publication could not be drained"
371
+ ack_drain_err "$state" "$dir/drain.err" \
372
+ || fail "pre-marker crash publication could not be acknowledged"
373
+
374
+ fakebin="$dir/fakebin"
375
+ out="$dir/watch.out"
376
+ PATH="$fakebin:$PATH" FM_HOME="$dir" FM_ROOT_OVERRIDE="$ROOT" \
377
+ FM_STATE_OVERRIDE="$state" FM_FAKE_TMUX_WINDOW='firstmate:fm-mate' \
378
+ FM_FAKE_TMUX_LOG="$dir/tmux.log" FM_FAKE_TMUX_CAPTURE="$dir/fake-tmux/pane.txt" \
379
+ FM_SECONDMATE_WAKE_STALL_SECS=1 FM_POLL=1 FM_SIGNAL_GRACE=0 \
380
+ FM_CHECK_INTERVAL=999999 FM_HEARTBEAT=999999 \
381
+ "$ROOT/bin/fm-watch-checkpoint.sh" --seconds 2 > "$out" 2> "$dir/watch.err" || true
382
+ ! grep -F 'secondmate wake-loop stalled' "$out" >/dev/null \
383
+ || fail "an acknowledged publication was duplicated after the pre-marker crash state"
384
+ [ ! -s "$state/.wake-queue" ] \
385
+ || fail "the replacement watcher re-published an acknowledged stall notification"
386
+ cmp -s "$row_before" "$sub/state/.wake-queue" \
387
+ || fail "pre-marker crash recovery changed the foreign queue row"
388
+ pass "stall publication acknowledgement closes the pre-marker crash window"
389
+ }
390
+
391
+ test_empty_prefix_mate_preserves_other_mate_receipt() {
392
+ local dir state empty stalled fakebin epoch row_before round
393
+ dir=$(make_case secondmate-prefix-receipt)
394
+ state="$dir/state"
395
+ empty="$dir/ios"
396
+ stalled="$dir/ios-ui"
397
+ mkdir -p "$empty/state" "$stalled/state"
398
+ printf 'ios\n' > "$empty/.fm-secondmate-home"
399
+ printf 'ios-ui\n' > "$stalled/.fm-secondmate-home"
400
+ printf 'window=firstmate:fm-ios\nkind=secondmate\nhome=%s\n' "$empty" > "$state/ios.meta"
401
+ printf 'window=firstmate:fm-ios-ui\nkind=secondmate\nhome=%s\n' "$stalled" > "$state/ios-ui.meta"
402
+ : > "$empty/state/.wake-queue"
403
+ epoch=$(( $(date +%s) - 10 ))
404
+ printf '%s\t9\tcheck\trouted\tcheck: routed row\n' "$epoch" > "$stalled/state/.wake-queue"
405
+ row_before="$dir/foreign-before"
406
+ cp "$stalled/state/.wake-queue" "$row_before"
407
+ append_wake "$state" check "secondmate-wake-loop-ios-ui-$epoch-9" \
408
+ "check: secondmate wake-loop stalled: mate=ios-ui row=9 age=10s" \
409
+ || fail "could not seed the ios-ui stall publication"
410
+ FM_STATE_OVERRIDE="$state" "$DRAIN" > "$dir/drain.out" 2> "$dir/drain.err" \
411
+ || fail "ios-ui stall publication could not be drained"
412
+ ack_drain_err "$state" "$dir/drain.err" \
413
+ || fail "ios-ui stall publication could not be acknowledged"
414
+
415
+ fakebin="$dir/fakebin"
416
+ round=1
417
+ while [ "$round" -le 2 ]; do
418
+ PATH="$fakebin:$PATH" FM_HOME="$dir" FM_ROOT_OVERRIDE="$ROOT" \
419
+ FM_STATE_OVERRIDE="$state" FM_FAKE_TMUX_WINDOW='' \
420
+ FM_FAKE_TMUX_LOG="$dir/tmux.log" FM_FAKE_TMUX_CAPTURE="$dir/fake-tmux/pane.txt" \
421
+ FM_SECONDMATE_WAKE_STALL_SECS=1 FM_POLL=1 FM_SIGNAL_GRACE=0 \
422
+ FM_CHECK_INTERVAL=999999 FM_HEARTBEAT=999999 \
423
+ "$ROOT/bin/fm-watch-checkpoint.sh" --seconds 2 \
424
+ > "$dir/watch-$round.out" 2> "$dir/watch-$round.err" || true
425
+ ! grep -F 'secondmate wake-loop stalled' "$dir/watch-$round.out" >/dev/null \
426
+ || fail "empty ios queue erased ios-ui idempotency on checkpoint $round"
427
+ round=$((round + 1))
428
+ done
429
+ [ ! -s "$state/.wake-queue" ] \
430
+ || fail "overlapping mate ids re-published the acknowledged ios-ui stall"
431
+ cmp -s "$row_before" "$stalled/state/.wake-queue" \
432
+ || fail "overlapping mate receipt checks changed the foreign row"
433
+ pass "empty prefix mate cleanup preserves another mate's stall receipt"
434
+ }
435
+
436
+ test_drain_asserts_watcher_liveness() {
437
+ local dir state err identity
438
+ dir=$(make_case drain-liveness)
439
+ state="$dir/state"
440
+ err="$dir/drain.err"
441
+ printf 'window=test:fm-x\nkind=ship\n' > "$state/x.meta"
442
+ FM_STATE_OVERRIDE="$state" "$DRAIN" >/dev/null 2> "$err" || fail "drain failed while asserting liveness"
443
+ grep -F 'WATCHER DOWN' "$err" >/dev/null || fail "drain did not surface the watcher-down banner with work in flight and no live watcher"
444
+ : > "$err"
445
+ identity=$(FM_STATE_OVERRIDE="$state" bash -c '. "$1"; fm_pid_identity "$2"' _ "$ROOT/bin/fm-wake-lib.sh" "$$") \
446
+ || fail "could not identify the live watcher fixture"
447
+ mkdir "$state/.watch.lock"
448
+ printf '%s\n' "$$" > "$state/.watch.lock/pid"
449
+ printf '%s\n' "$dir" > "$state/.watch.lock/fm-home"
450
+ printf '%s\n' "$WATCH" > "$state/.watch.lock/watcher-path"
451
+ printf '%s\n' "$identity" > "$state/.watch.lock/pid-identity"
452
+ touch "$state/.last-watcher-beat"
453
+ FM_HOME="$dir" FM_STATE_OVERRIDE="$state" FM_GUARD_GRACE=300 "$DRAIN" >/dev/null 2> "$err" \
454
+ || fail "drain failed with a live watcher and fresh beacon"
455
+ if grep -F 'WATCHER DOWN' "$err" >/dev/null; then
456
+ fail "drain false-alarmed with a live watcher and fresh beacon"
457
+ fi
458
+ pass "drain asserts watcher liveness: warns on a lapse, stays silent for a live watcher with a fresh beacon"
459
+ }
460
+
461
+ test_structural_signal_enrichment_preserves_raw_rows() {
462
+ local dir state out expected actual annotation_count outside perl_bin
463
+ dir=$(make_case enrichment)
464
+ state="$dir/state"
465
+ out="$dir/drain.out"
466
+ expected="$dir/expected.out"
467
+ actual="$dir/actual.out"
468
+ outside="$dir/outside-secret"
469
+ printf 'working: first\n\ndone: latest event\n' > "$state/task.status"
470
+ printf 'working: old turn-end context\n' > "$state/turn-only.status"
471
+ printf 'must-not-be-read\n' > "$outside"
472
+ ln -s "$outside" "$state/escape.status"
473
+ perl_bin=$(command -v perl) || fail "perl is required for safe status reads"
474
+ cat > "$dir/fakebin/perl" <<'SH'
475
+ #!/usr/bin/env bash
476
+ if [ "${1:-}" = -MFcntl=:DEFAULT ]; then
477
+ for arg in "$@"; do
478
+ if [ "$arg" = "${FM_WAKE_ENRICH_SWAP_PATH:-}" ]; then
479
+ rm -f "$arg"
480
+ ln -s "$FM_WAKE_ENRICH_SWAP_TARGET" "$arg"
481
+ break
482
+ fi
483
+ done
484
+ fi
485
+ exec "$FM_WAKE_ENRICH_REAL_PERL" "$@"
486
+ SH
487
+ chmod +x "$dir/fakebin/perl"
488
+
489
+ append_wake "$state" signal task.status "signal: $outside" || fail "direct status wake append failed"
490
+ append_wake "$state" signal task.turn-ended "signal: $outside" || fail "coalesced turn-end wake append failed"
491
+ append_wake "$state" signal turn-only.turn-ended "signal: $outside" || fail "bare turn-end wake append failed"
492
+ append_wake "$state" signal escape.status "signal: $outside" || fail "symlink status wake append failed"
493
+ append_wake "$state" signal arbitrary-key "signal: $outside" || fail "non-status signal wake append failed"
494
+ append_wake "$state" check task.check.sh "check: complete payload" || fail "check wake append failed"
495
+ append_wake "$state" stale test:fm-task "stale: test:fm-task" || fail "stale wake append failed"
496
+ append_wake "$state" heartbeat heartbeat heartbeat || fail "heartbeat wake append failed"
497
+
498
+ FM_STATE_OVERRIDE="$state" bash -c '. "$1"; fm_wake_print_deduped "$2"' _ \
499
+ "$ROOT/bin/fm-wake-lib.sh" "$state/.wake-queue" > "$expected"
500
+ PATH="$dir/fakebin:$PATH" FM_STATE_OVERRIDE="$state" FM_WAKE_ENRICH_SWAP_PATH="$state/task.status" \
501
+ FM_WAKE_ENRICH_SWAP_TARGET="$outside" FM_WAKE_ENRICH_REAL_PERL="$perl_bin" "$DRAIN" > "$out" \
502
+ || fail "structural enrichment drain failed"
503
+ awk -F '\t' 'NF == 5 { print }' "$out" > "$actual"
504
+ cmp -s "$expected" "$actual" || fail "enrichment changed or reordered an authoritative raw row"
505
+
506
+ annotation_count=$(grep -c '^wake annotation:' "$out" || true)
507
+ [ "$annotation_count" -eq 1 ] || fail "expected only the unreadable-race-safe status annotation, got $annotation_count"
508
+ if grep -E '^wake annotation:.*: task\.status:' "$out" >/dev/null; then
509
+ fail "replaced status file produced an annotation"
510
+ fi
511
+ grep -F 'latest wake-EVENT observed at drain, not current state; historical / not necessarily the triggering event: turn-only.status:' "$out" >/dev/null \
512
+ || fail "bare turn-end mapping did not carry the historical warning"
513
+ if grep -F 'must-not-be-read' "$out" >/dev/null; then
514
+ fail "drain trusted a payload path or followed an out-of-state status symlink"
515
+ fi
516
+ pass "structural signal enrichment is separate, deduped, home-local, and tier-zero for other wakes"
517
+ }
518
+
519
+ test_enrichment_preserves_all_unread_lines_and_status_file_failures() {
520
+ local dir state out i raw_count expected
521
+ dir=$(make_case complete-enrichment)
522
+ state="$dir/state"
523
+ out="$dir/drain.out"
524
+ awk 'BEGIN { printf "done: "; for (i = 0; i < 20000; i++) printf "x"; printf "\n" }' > "$state/huge.status"
525
+ append_wake "$state" signal huge.status "signal: huge" || fail "huge status wake append failed"
526
+ i=1
527
+ while [ "$i" -le 8 ]; do
528
+ awk -v n="$i" 'BEGIN { printf "working-%d: ", n; for (j = 0; j < 3000; j++) printf "y"; printf "\n" }' > "$state/many-$i.status"
529
+ append_wake "$state" signal "many-$i.status" "signal: many-$i" || fail "many-status wake append failed"
530
+ i=$((i + 1))
531
+ done
532
+ : > "$state/empty.status"
533
+ append_wake "$state" signal empty.status "signal: empty" || fail "empty status wake append failed"
534
+ append_wake "$state" signal missing.status "signal: missing" || fail "missing status wake append failed"
535
+ mkdir "$state/malformed.status"
536
+ append_wake "$state" signal malformed.status "signal: malformed" || fail "malformed status wake append failed"
537
+ printf 'done: unreadable\n' > "$state/unreadable.status"
538
+ chmod 000 "$state/unreadable.status"
539
+ append_wake "$state" signal unreadable.status "signal: unreadable" || fail "unreadable status wake append failed"
540
+
541
+ FM_STATE_OVERRIDE="$state" "$DRAIN" > "$out" \
542
+ || fail "complete enrichment drain failed"
543
+ raw_count=$(awk -F '\t' 'NF == 5 { count++ } END { print count + 0 }' "$out")
544
+ [ "$raw_count" -eq 13 ] || fail "missing, unreadable, malformed, empty, or oversized status input hid a raw row"
545
+
546
+ expected="wake annotation: latest wake-EVENT observed at drain, not current state: huge.status: $(cat "$state/huge.status")"
547
+ grep -Fx "$expected" "$out" >/dev/null \
548
+ || fail "the oversized unread status line was truncated or omitted"
549
+ i=1
550
+ while [ "$i" -le 8 ]; do
551
+ expected="wake annotation: latest wake-EVENT observed at drain, not current state: many-$i.status: $(cat "$state/many-$i.status")"
552
+ grep -Fx "$expected" "$out" >/dev/null \
553
+ || fail "readable status many-$i was truncated or omitted"
554
+ i=$((i + 1))
555
+ done
556
+ if grep -E '^wake annotation:.*(truncated|omitted)' "$out" >/dev/null; then
557
+ fail "complete unread annotation output still reported dropped content"
558
+ fi
559
+ if grep -E ': (empty|missing|malformed|unreadable)\.status:' "$out" >/dev/null; then
560
+ fail "missing, unreadable, malformed, or empty status file produced an annotation"
561
+ fi
562
+ pass "every readable unread status line is annotated in full while invalid status files preserve their raw wakes"
563
+ }
564
+
565
+ wait_for_file_text() { # <file> <fixed-text>
566
+ local file=$1 expected=$2 i=0
567
+ while [ "$i" -lt 100 ]; do
568
+ grep -F "$expected" "$file" >/dev/null 2>&1 && return 0
569
+ sleep 0.05
570
+ i=$((i + 1))
571
+ done
572
+ return 1
573
+ }
574
+
575
+ test_slow_annotation_does_not_block_append_and_deleted_file_fails_open() {
576
+ local dir state out1 out2 pid
577
+ dir=$(make_case slow-annotation)
578
+ state="$dir/state"
579
+ out1="$dir/drain-one.out"
580
+ out2="$dir/drain-two.out"
581
+ printf 'done: disappears before bounded read\n' > "$state/slow.status"
582
+ append_wake "$state" signal slow.status "signal: slow" || fail "slow status wake append failed"
583
+
584
+ FM_STATE_OVERRIDE="$state" FM_WAKE_ENRICH_TEST_DELAY=3 "$DRAIN" > "$out1" &
585
+ pid=$!
586
+ wait_for_file_text "$out1" "$(printf '\tsignal\tslow.status\t')" \
587
+ || { kill "$pid" 2>/dev/null || true; fail "slow drain did not commit its raw row"; }
588
+ printf 'done: appended while first drain annotates\n' > "$state/next.status"
589
+ append_wake "$state" signal next.status "signal: next" || fail "append blocked or failed during annotation"
590
+ kill -0 "$pid" 2>/dev/null || fail "slow annotation finished before the concurrent append proved lock independence"
591
+ rm -f "$state/slow.status"
592
+ wait "$pid" || fail "deleted status file made the committed drain fail"
593
+ grep -F "$(printf '\tsignal\tslow.status\t')" "$out1" >/dev/null || fail "deleted status file hid the committed raw row"
594
+ if grep -F ': slow.status:' "$out1" >/dev/null; then
595
+ fail "status deleted during annotation still produced an annotation"
596
+ fi
597
+ FM_STATE_OVERRIDE="$state" "$DRAIN" > "$out2" || fail "follow-up drain after concurrent append failed"
598
+ grep -F "$(printf '\tsignal\tnext.status\t')" "$out2" >/dev/null || fail "concurrent append was not left for the next drain"
599
+ pass "slow annotation releases the append lock and a deleted status file fails open"
600
+ }
601
+
602
+ # Per-actor consume (docs/watcher-continuity.md "Per-actor acknowledgement").
603
+ # Drives a MIXED queue snapshot - an unacked main-only check row alongside two
604
+ # task-local rows the Pi supervision branch was granted - directly against
605
+ # the real bin/fm-wake-drain.sh, independent of the Pi SDK. This is the core
606
+ # safety property: a scoped actor's ack must never remove a row outside its
607
+ # own eligible snapshot, no matter that row's sequence number relative to
608
+ # what the actor presents or acks itself. Do not regress it.
609
+ test_branch_actor_scoped_ack_never_swallows_a_main_owned_row() {
610
+ local dir state out err sequence generation count
611
+ dir=$(make_case actor-scope)
612
+ state="$dir/state"
613
+
614
+ append_wake "$state" check "some-poll.check.sh" "check: some-poll.check.sh: merged" \
615
+ || fail "main-only append failed"
616
+ append_wake "$state" signal "task-a.status" "signal: task-a" || fail "signal append failed"
617
+ append_wake "$state" stale "fm-window" "stale: fm-window" || fail "stale append failed"
618
+
619
+ # The extension's own job (fm-branch-dispatch.ts) is granting exactly the
620
+ # two task-local rows; this test drives the bash consume contract those
621
+ # sequence numbers gate, independent of the Pi SDK.
622
+ FM_STATE_OVERRIDE="$state" "$GRANT" activate "$$" actor-scope || fail "branch owner activation failed"
623
+ FM_STATE_OVERRIDE="$state" "$GRANT" publish actor-scope 2 3 || fail "branch grant publication failed"
624
+
625
+ out="$dir/branch-drain.out"
626
+ err="$dir/branch-drain.err"
627
+ FM_STATE_OVERRIDE="$state" FM_SUPERVISION_ACTOR=branch "$DRAIN" > "$out" 2> "$err" \
628
+ || fail "branch-scoped drain failed: $(cat "$err")"
629
+ grep -Fq "$(printf '\tsignal\ttask-a.status\t')" "$out" || fail "branch drain omitted its eligible signal row"
630
+ grep -Fq "$(printf '\tstale\tfm-window\t')" "$out" || fail "branch drain omitted its eligible stale row"
631
+ grep -Fq "$(printf '\tcheck\tsome-poll.check.sh\t')" "$out" && fail "branch drain presented the main-owned row"
632
+
633
+ sequence=$(sed -n 's/^WAKE_ACK_REQUIRED:.*--ack-through \([0-9][0-9]*\) --recovery-generation [A-Za-z0-9._-][A-Za-z0-9._-]*$/\1/p' "$err")
634
+ generation=$(sed -n 's/^WAKE_ACK_REQUIRED:.*--ack-through [0-9][0-9]* --recovery-generation \([A-Za-z0-9._-][A-Za-z0-9._-]*\)$/\1/p' "$err")
635
+ [ -n "$sequence" ] && [ -n "$generation" ] || fail "branch drain omitted its acknowledgement boundary"
636
+ [ "$sequence" -eq 3 ] || fail "branch ack cutoff must be the max ELIGIBLE seq (3), got $sequence"
637
+
638
+ FM_STATE_OVERRIDE="$state" FM_SUPERVISION_ACTOR=branch "$DRAIN" --ack-through "$sequence" --recovery-generation "$generation" \
639
+ || fail "branch-scoped ack failed"
640
+
641
+ # The core no-swallow property: the main-only row - seq 1, BELOW the
642
+ # branch's own ack cutoff of 3 - must still be there.
643
+ grep -Fq "$(printf '\tcheck\tsome-poll.check.sh\t')" "$state/.wake-queue" \
644
+ || fail "branch's scoped ack swallowed a main-owned row below its own cutoff"
645
+ grep -Fq "$(printf '\tsignal\ttask-a.status\t')" "$state/.wake-queue" \
646
+ && fail "branch's own eligible signal row was not consumed"
647
+ grep -Fq "$(printf '\tstale\tfm-window\t')" "$state/.wake-queue" \
648
+ && fail "branch's own eligible stale row was not consumed"
649
+
650
+ # Main's own later, ordinary (unscoped) drain sees exactly what remains.
651
+ out="$dir/main-drain.out"
652
+ err="$dir/main-drain.err"
653
+ FM_STATE_OVERRIDE="$state" "$DRAIN" > "$out" 2> "$err" || fail "main drain failed: $(cat "$err")"
654
+ count=$(awk -F '\t' 'NF == 5 { count++ } END { print count + 0 }' "$out")
655
+ [ "$count" -eq 1 ] || fail "main's later drain should see exactly the one remaining main-owned row: $(cat "$out")"
656
+ grep -Fq "$(printf '\tcheck\tsome-poll.check.sh\t')" "$out" || fail "main's later drain lost the main-owned row"
657
+ sequence=$(sed -n 's/^WAKE_ACK_REQUIRED:.*--ack-through \([0-9][0-9]*\) --recovery-generation [A-Za-z0-9._-][A-Za-z0-9._-]*$/\1/p' "$err")
658
+ generation=$(sed -n 's/^WAKE_ACK_REQUIRED:.*--ack-through [0-9][0-9]* --recovery-generation \([A-Za-z0-9._-][A-Za-z0-9._-]*\)$/\1/p' "$err")
659
+ [ -n "$sequence" ] && [ -n "$generation" ] || fail "main's drain omitted its acknowledgement boundary"
660
+ FM_STATE_OVERRIDE="$state" "$DRAIN" --ack-through "$sequence" --recovery-generation "$generation" \
661
+ || fail "main's ack failed"
662
+ [ ! -s "$state/.wake-queue" ] || fail "the main-owned row survived main's own ack"
663
+
664
+ pass "a branch-actor scoped ack never swallows an unacked main-owned row, and main's later drain sees exactly what remains"
665
+ }
666
+
667
+ test_main_drain_excludes_rows_already_granted_to_branch() {
668
+ local dir state out err sequence generation
669
+ dir=$(make_case main-excludes-branch-grant)
670
+ state="$dir/state"
671
+
672
+ append_wake "$state" check "some-poll.check.sh" "check: some-poll.check.sh: merged" \
673
+ || fail "main-only append failed"
674
+ append_wake "$state" signal "task-a.status" "signal: task-a" || fail "signal append failed"
675
+ FM_STATE_OVERRIDE="$state" "$GRANT" activate "$$" main-excludes || fail "branch owner activation failed"
676
+ FM_STATE_OVERRIDE="$state" "$GRANT" publish main-excludes 2 || fail "branch grant publication failed"
677
+
678
+ out="$dir/main-drain.out"
679
+ err="$dir/main-drain.err"
680
+ FM_STATE_OVERRIDE="$state" "$DRAIN" > "$out" 2> "$err" || fail "main drain failed: $(cat "$err")"
681
+ grep -Fq "$(printf '\tcheck\tsome-poll.check.sh\t')" "$out" || fail "main drain omitted its main-owned row"
682
+ ! grep -Fq "$(printf '\tsignal\ttask-a.status\t')" "$out" || fail "main drain presented a branch-granted row"
683
+ sequence=$(sed -n 's/^WAKE_ACK_REQUIRED:.*--ack-through \([0-9][0-9]*\) --recovery-generation [A-Za-z0-9._-][A-Za-z0-9._-]*$/\1/p' "$err")
684
+ generation=$(sed -n 's/^WAKE_ACK_REQUIRED:.*--ack-through [0-9][0-9]* --recovery-generation \([A-Za-z0-9._-][A-Za-z0-9._-]*\)$/\1/p' "$err")
685
+ [ "$sequence" = 1 ] && [ -n "$generation" ] || fail "main acknowledgement did not bind only its presented row"
686
+ FM_STATE_OVERRIDE="$state" "$DRAIN" --ack-through "$sequence" --recovery-generation "$generation" \
687
+ || fail "main acknowledgement failed"
688
+ grep -Fq "$(printf '\tsignal\ttask-a.status\t')" "$state/.wake-queue" \
689
+ || fail "main acknowledgement consumed the branch-granted row"
690
+
691
+ out="$dir/branch-drain.out"
692
+ err="$dir/branch-drain.err"
693
+ FM_STATE_OVERRIDE="$state" FM_SUPERVISION_ACTOR=branch "$DRAIN" > "$out" 2> "$err" \
694
+ || fail "branch drain failed: $(cat "$err")"
695
+ grep -Fq "$(printf '\tsignal\ttask-a.status\t')" "$out" || fail "branch lost its granted row"
696
+ sequence=$(sed -n 's/^WAKE_ACK_REQUIRED:.*--ack-through \([0-9][0-9]*\) --recovery-generation [A-Za-z0-9._-][A-Za-z0-9._-]*$/\1/p' "$err")
697
+ generation=$(sed -n 's/^WAKE_ACK_REQUIRED:.*--ack-through [0-9][0-9]* --recovery-generation \([A-Za-z0-9._-][A-Za-z0-9._-]*\)$/\1/p' "$err")
698
+ FM_STATE_OVERRIDE="$state" FM_SUPERVISION_ACTOR=branch "$DRAIN" --ack-through "$sequence" --recovery-generation "$generation" \
699
+ || fail "branch acknowledgement failed"
700
+ [ ! -s "$state/.wake-queue" ] || fail "branch acknowledgement left its handled row queued"
701
+ [ ! -e "$state/.branch-eligible-rows" ] || fail "branch acknowledgement retained its completed grant"
702
+
703
+ pass "main drain and acknowledgement exclude an active branch grant"
704
+ }
705
+
706
+ test_branch_grant_refuses_rows_already_claimed_by_main() {
707
+ local dir state rc
708
+ dir=$(make_case branch-refuses-main-claim)
709
+ state="$dir/state"
710
+
711
+ append_wake "$state" signal "task-a.status" "signal: task-a" || fail "signal append failed"
712
+ FM_STATE_OVERRIDE="$state" "$DRAIN" > "$dir/main.out" 2> "$dir/main.err" \
713
+ || fail "main presentation failed"
714
+ FM_STATE_OVERRIDE="$state" "$GRANT" activate "$$" branch-refuses || fail "branch owner activation failed"
715
+ rc=0
716
+ FM_STATE_OVERRIDE="$state" "$GRANT" publish branch-refuses 1 || rc=$?
717
+ [ "$rc" -eq 3 ] || fail "branch grant did not report the existing main ownership: rc=$rc"
718
+ [ ! -e "$state/.branch-eligible-rows" ] || fail "refused branch grant published an ownership snapshot"
719
+ grep -Fq "$(printf '\tsignal\ttask-a.status\t')" "$dir/main.out" \
720
+ || fail "the main owner did not present its claimed row"
721
+
722
+ pass "branch grant cannot take a row already claimed by main"
723
+ }
724
+
725
+ test_actor_filter_precedes_same_key_deduplication() {
726
+ local dir state main_sequence main_generation branch_sequence branch_generation
727
+ dir=$(make_case actor-dedup-order)
728
+ state="$dir/state"
729
+
730
+ append_wake "$state" signal "task-a.status" "signal: branch version" || fail "branch row append failed"
731
+ FM_STATE_OVERRIDE="$state" "$GRANT" activate "$$" actor-dedup || fail "branch owner activation failed"
732
+ FM_STATE_OVERRIDE="$state" "$GRANT" publish actor-dedup 1 || fail "branch grant publication failed"
733
+ append_wake "$state" signal "task-a.status" "signal: main version" || fail "main row append failed"
734
+
735
+ FM_STATE_OVERRIDE="$state" "$DRAIN" > "$dir/main.out" 2> "$dir/main.err" || fail "main drain failed"
736
+ [ "$(awk -F '\t' '$3 == "signal" { print $2 }' "$dir/main.out")" = 2 ] \
737
+ || fail "main did not present its same-key claimed row"
738
+ FM_STATE_OVERRIDE="$state" FM_SUPERVISION_ACTOR=branch "$DRAIN" > "$dir/branch.out" 2> "$dir/branch.err" \
739
+ || fail "branch drain failed"
740
+ [ "$(awk -F '\t' '$3 == "signal" { print $2 }' "$dir/branch.out")" = 1 ] \
741
+ || fail "global deduplication hid the branch's older same-key row"
742
+
743
+ main_sequence=$(sed -n 's/^WAKE_ACK_REQUIRED:.*--ack-through \([0-9][0-9]*\) --recovery-generation [A-Za-z0-9._-][A-Za-z0-9._-]*$/\1/p' "$dir/main.err")
744
+ main_generation=$(sed -n 's/^WAKE_ACK_REQUIRED:.*--ack-through [0-9][0-9]* --recovery-generation \([A-Za-z0-9._-][A-Za-z0-9._-]*\)$/\1/p' "$dir/main.err")
745
+ branch_sequence=$(sed -n 's/^WAKE_ACK_REQUIRED:.*--ack-through \([0-9][0-9]*\) --recovery-generation [A-Za-z0-9._-][A-Za-z0-9._-]*$/\1/p' "$dir/branch.err")
746
+ branch_generation=$(sed -n 's/^WAKE_ACK_REQUIRED:.*--ack-through [0-9][0-9]* --recovery-generation \([A-Za-z0-9._-][A-Za-z0-9._-]*\)$/\1/p' "$dir/branch.err")
747
+ FM_STATE_OVERRIDE="$state" "$DRAIN" --ack-through "$main_sequence" --recovery-generation "$main_generation" \
748
+ || fail "main same-key acknowledgement failed"
749
+ FM_STATE_OVERRIDE="$state" FM_SUPERVISION_ACTOR=branch "$DRAIN" --ack-through "$branch_sequence" --recovery-generation "$branch_generation" \
750
+ || fail "branch same-key acknowledgement failed"
751
+ [ ! -s "$state/.wake-queue" ] || fail "same-key actor rows remained stranded"
752
+
753
+ pass "actor ownership filtering precedes same-key deduplication"
754
+ }
755
+
756
+ test_main_reclaims_a_grant_whose_branch_owner_exited() {
757
+ local dir state owner sequence generation
758
+ dir=$(make_case stale-branch-owner)
759
+ state="$dir/state"
760
+
761
+ append_wake "$state" signal "task-a.status" "signal: task-a" || fail "signal append failed"
762
+ sleep 30 &
763
+ owner=$!
764
+ FM_STATE_OVERRIDE="$state" "$GRANT" activate "$owner" stale-owner || {
765
+ kill "$owner" 2>/dev/null || true
766
+ fail "branch owner activation failed"
767
+ }
768
+ FM_STATE_OVERRIDE="$state" "$GRANT" publish stale-owner 1 || {
769
+ kill "$owner" 2>/dev/null || true
770
+ fail "branch grant publication failed"
771
+ }
772
+ kill "$owner" 2>/dev/null || true
773
+ wait "$owner" 2>/dev/null || true
774
+
775
+ FM_STATE_OVERRIDE="$state" "$DRAIN" > "$dir/main.out" 2> "$dir/main.err" || fail "main reclaim drain failed"
776
+ grep -Fq "$(printf '\tsignal\ttask-a.status\t')" "$dir/main.out" \
777
+ || fail "main did not reclaim the dead branch owner's row"
778
+ [ ! -e "$state/.branch-eligible-rows" ] && [ ! -e "$state/.branch-eligible-owner" ] \
779
+ || fail "dead branch ownership evidence survived reclaim"
780
+ sequence=$(sed -n 's/^WAKE_ACK_REQUIRED:.*--ack-through \([0-9][0-9]*\) --recovery-generation [A-Za-z0-9._-][A-Za-z0-9._-]*$/\1/p' "$dir/main.err")
781
+ generation=$(sed -n 's/^WAKE_ACK_REQUIRED:.*--ack-through [0-9][0-9]* --recovery-generation \([A-Za-z0-9._-][A-Za-z0-9._-]*\)$/\1/p' "$dir/main.err")
782
+ FM_STATE_OVERRIDE="$state" "$DRAIN" --ack-through "$sequence" --recovery-generation "$generation" \
783
+ || fail "reclaimed row acknowledgement failed"
784
+ [ ! -s "$state/.wake-queue" ] || fail "reclaimed branch row remained queued"
785
+
786
+ pass "main reclaims rows granted to an exited branch owner"
787
+ }
788
+
789
+ # A branch-actor drain or ack without a snapshot is a wiring bug, never
790
+ # "nothing eligible": it must refuse loudly rather than silently draining or
791
+ # acking nothing.
792
+ test_branch_actor_without_eligible_snapshot_refuses() {
793
+ local dir state
794
+ dir=$(make_case actor-no-snapshot)
795
+ state="$dir/state"
796
+ append_wake "$state" signal "task-a.status" "signal: task-a" || fail "append failed"
797
+ if FM_STATE_OVERRIDE="$state" FM_SUPERVISION_ACTOR=branch "$DRAIN" >/dev/null 2>"$dir/err"; then
798
+ fail "a branch-actor drain with no eligible-row snapshot must refuse, not silently drain"
799
+ fi
800
+ grep -q "no branch-eligible row snapshot" "$dir/err" || fail "the refusal did not name the missing snapshot: $(cat "$dir/err")"
801
+ [ -s "$state/.wake-queue" ] || fail "the refused drain must leave the queue untouched"
802
+ pass "a branch-actor drain with no eligible-row snapshot refuses loudly instead of draining nothing"
803
+ }
804
+
805
+ test_wake_publish_requires_atomic_recovery_evidence() {
806
+ local dir state fakebin real_mv rc out
807
+ dir=$(make_case wake-publish-recovery-evidence)
808
+ state="$dir/state"
809
+ fakebin="$dir/fakebin"
810
+ real_mv=$(command -v mv) || fail "could not locate mv for recovery publication fixture"
811
+ printf 'pending:handling:existing\n' > "$state/.watcher-down"
812
+ cat > "$fakebin/mv" <<'SH'
813
+ #!/usr/bin/env bash
814
+ last=${!#}
815
+ if [ "$last" = "${FM_TEST_PUBLISH_MARKER:-}" ]; then
816
+ exit 1
817
+ fi
818
+ exec "$FM_TEST_REAL_MV" "$@"
819
+ SH
820
+ chmod +x "$fakebin/mv"
821
+
822
+ set +e
823
+ PATH="$fakebin:$PATH" FM_TEST_REAL_MV="$real_mv" FM_TEST_PUBLISH_MARKER="$state/.watcher-down" \
824
+ append_wake "$state" signal task.status "signal: publish failure"
825
+ rc=$?
826
+ set -e
827
+ [ "$rc" -ne 0 ] || fail "recovery publication failure allowed wake append to succeed"
828
+ [ "$(cat "$state/.watcher-down")" = 'pending:handling:existing' ] \
829
+ || fail "failed atomic publication erased existing recovery evidence"
830
+ [ ! -s "$state/.wake-queue" ] \
831
+ || fail "wake became durable before its recovery evidence"
832
+
833
+ PATH="$fakebin:$PATH" FM_TEST_REAL_MV="$real_mv" \
834
+ append_wake "$state" signal task.status "signal: recovered retry" \
835
+ || fail "wake retry did not publish durable recovery evidence"
836
+ out="$dir/drain.out"
837
+ FM_STATE_OVERRIDE="$state" "$DRAIN" > "$out" \
838
+ || fail "wake retry did not drain"
839
+ grep -F "signal: recovered retry" "$out" >/dev/null \
840
+ || fail "retried wake was not recovered by the durable drain"
841
+ pass "wake append publishes atomic recovery evidence before durable rows"
842
+ }
843
+
844
+ test_legacy_generationless_wake_is_adopted() {
845
+ local dir state row sequence generation
846
+ dir=$(make_case legacy-generationless-wake)
847
+ state="$dir/state"
848
+ row=$(printf '1700000000\t7\tcheck\tlegacy-process-event\tcheck: legacy process-event')
849
+ printf '%s\n' "$row" > "$state/.wake-queue"
850
+
851
+ FM_STATE_OVERRIDE="$state" "$DRAIN" > "$dir/first.out" 2> "$dir/first.err" \
852
+ || fail "generation-less legacy wake could not be adopted"
853
+ grep -F "$row" "$dir/first.out" >/dev/null \
854
+ || fail "adopted legacy wake was not presented"
855
+ sequence=$(sed -n 's/^WAKE_ACK_REQUIRED:.*--ack-through \([0-9][0-9]*\) --recovery-generation [A-Za-z0-9._-][A-Za-z0-9._-]*$/\1/p' "$dir/first.err")
856
+ generation=$(sed -n 's/^WAKE_ACK_REQUIRED:.*--ack-through [0-9][0-9]* --recovery-generation \([A-Za-z0-9._-][A-Za-z0-9._-]*\)$/\1/p' "$dir/first.err")
857
+ [ "$sequence" = 7 ] && [ -n "$generation" ] \
858
+ || fail "legacy wake adoption omitted its generation-bound acknowledgement"
859
+ [ "$(cat "$state/.watcher-down" 2>/dev/null || true)" = "pending:handling:$generation" ] \
860
+ || fail "legacy wake was not adopted into durable handling recovery"
861
+
862
+ FM_STATE_OVERRIDE="$state" "$DRAIN" > "$dir/replay.out" 2> "$dir/replay.err" \
863
+ || fail "unacknowledged adopted wake could not be re-drained"
864
+ grep -F "$row" "$dir/replay.out" >/dev/null \
865
+ || fail "unacknowledged adopted wake was lost"
866
+ FM_STATE_OVERRIDE="$state" "$DRAIN" --ack-through "$sequence" \
867
+ --recovery-generation "$generation" \
868
+ || fail "adopted legacy wake could not be acknowledged"
869
+ [ ! -s "$state/.wake-queue" ] || fail "acknowledged legacy wake remained queued"
870
+ FM_STATE_OVERRIDE="$state" "$DRAIN" > "$dir/after-ack.out" 2> "$dir/after-ack.err" \
871
+ || fail "post-acknowledgement legacy drain failed"
872
+ ! grep -F "$row" "$dir/after-ack.out" >/dev/null \
873
+ || fail "acknowledged legacy wake was consumed more than once"
874
+ pass "wake drain: generation-less legacy wakes are adopted and acknowledged"
875
+ }
876
+
877
+ # Pin the recovery acknowledgement contract from docs/watcher-continuity.md at
878
+ # the queue-library boundary.
879
+ test_stale_recovery_generation_cannot_touch_a_newer_episode() {
880
+ local dir state first_err replay_err sequence generation handling_marker
881
+ local newer_marker newer_sequence newer_generation rc
882
+ dir=$(make_case stale-recovery-generation)
883
+ state="$dir/state"
884
+
885
+ append_wake "$state" check first 'check: first generation' \
886
+ || fail "first generation wake append failed"
887
+ FM_STATE_OVERRIDE="$state" "$DRAIN" > "$dir/first.out" 2> "$dir/first.err" \
888
+ || fail "first generation drain failed"
889
+ first_err="$dir/first.err"
890
+ sequence=$(sed -n 's/^WAKE_ACK_REQUIRED:.*--ack-through \([0-9][0-9]*\) --recovery-generation [A-Za-z0-9._-][A-Za-z0-9._-]*$/\1/p' "$first_err")
891
+ generation=$(sed -n 's/^WAKE_ACK_REQUIRED:.*--ack-through [0-9][0-9]* --recovery-generation \([A-Za-z0-9._-][A-Za-z0-9._-]*\)$/\1/p' "$first_err")
892
+ [ -n "$sequence" ] && [ -n "$generation" ] \
893
+ || fail "first drain did not emit a generation-bound acknowledgement"
894
+
895
+ append_wake "$state" check second 'check: same episode' \
896
+ || fail "first same-episode wake append failed"
897
+ append_wake "$state" check third 'check: same episode again' \
898
+ || fail "second same-episode wake append failed"
899
+ handling_marker=$(cat "$state/.watcher-down")
900
+ [ "${handling_marker##*:}" = "$generation" ] \
901
+ || fail "repeated publications replaced the outstanding recovery generation"
902
+
903
+ FM_STATE_OVERRIDE="$state" "$DRAIN" --ack-through "$sequence" \
904
+ --recovery-generation "$generation" > "$dir/handled-ack.out" 2> "$dir/handled-ack.err" \
905
+ || fail "a publication during handling invalidated the printed acknowledgement"
906
+ ! grep "$(printf '\tcheck\tfirst\t')" "$state/.wake-queue" >/dev/null \
907
+ || fail "the handled row was not consumed"
908
+ grep "$(printf '\tcheck\tsecond\t')" "$state/.wake-queue" >/dev/null \
909
+ || fail "a row above the acknowledged sequence was consumed"
910
+ grep "$(printf '\tcheck\tthird\t')" "$state/.wake-queue" >/dev/null \
911
+ || fail "the second row above the acknowledged sequence was consumed"
912
+ case "$(cat "$state/.watcher-down")" in
913
+ pending:*) ;;
914
+ *) fail "an episode with rows still queued was retired" ;;
915
+ esac
916
+
917
+ # Retire that episode, then let a genuinely newer one open.
918
+ FM_STATE_OVERRIDE="$state" "$DRAIN" > "$dir/replay.out" 2> "$dir/replay.err" \
919
+ || fail "remaining wake could not be re-drained"
920
+ replay_err="$dir/replay.err"
921
+ grep "$(printf '\tcheck\tsecond\t')" "$dir/replay.out" >/dev/null \
922
+ || fail "remaining wake did not re-surface"
923
+ grep "$(printf '\tcheck\tthird\t')" "$dir/replay.out" >/dev/null \
924
+ || fail "second remaining wake did not re-surface"
925
+ newer_sequence=$(sed -n 's/^WAKE_ACK_REQUIRED:.*--ack-through \([0-9][0-9]*\) --recovery-generation [A-Za-z0-9._-][A-Za-z0-9._-]*$/\1/p' "$replay_err")
926
+ newer_generation=$(sed -n 's/^WAKE_ACK_REQUIRED:.*--ack-through [0-9][0-9]* --recovery-generation \([A-Za-z0-9._-][A-Za-z0-9._-]*\)$/\1/p' "$replay_err")
927
+ FM_STATE_OVERRIDE="$state" "$DRAIN" --ack-through "$newer_sequence" \
928
+ --recovery-generation "$newer_generation" \
929
+ || fail "the handled episode could not be acknowledged"
930
+ [ ! -s "$state/.wake-queue" ] || fail "acknowledgement left durable wakes queued"
931
+
932
+ append_wake "$state" check fourth 'check: newer recovery generation' \
933
+ || fail "newer generation wake append failed"
934
+ newer_marker=$(cat "$state/.watcher-down")
935
+ [ "${newer_marker##*:}" != "$generation" ] \
936
+ || fail "a retired episode did not open a new recovery generation"
937
+
938
+ rc=0
939
+ FM_STATE_OVERRIDE="$state" "$DRAIN" --ack-through "$sequence" \
940
+ --recovery-generation "$generation" > "$dir/stale-ack.out" 2> "$dir/stale-ack.err" || rc=$?
941
+ [ "$rc" -eq 0 ] \
942
+ || fail "a stale acknowledgement failed instead of degrading safely: $(cat "$dir/stale-ack.err")"
943
+ if ! grep -F 'WAKE_ACK_REQUIRED' "$dir/stale-ack.err" >/dev/null \
944
+ || ! grep -F 're-run' "$dir/stale-ack.err" >/dev/null; then
945
+ fail "a stale acknowledgement did not name its own remedy: $(cat "$dir/stale-ack.err")"
946
+ fi
947
+ [ "$(cat "$state/.watcher-down")" = "$newer_marker" ] \
948
+ || fail "a stale acknowledgement retired the newer recovery episode"
949
+ grep "$(printf '\tcheck\tfourth\t')" "$state/.wake-queue" >/dev/null \
950
+ || fail "a stale acknowledgement consumed the newer durable wake"
951
+ pass "wake drain: a stale acknowledgement cannot retire or consume a newer recovery episode"
952
+ }
953
+
954
+ test_recovery_ack_failure_is_reported() {
955
+ local dir state fakebin real_mv rc generation
956
+ dir=$(make_case recovery-ack-failure)
957
+ state="$dir/state"
958
+ fakebin="$dir/fakebin"
959
+ real_mv=$(command -v mv) || fail "could not locate mv for recovery acknowledgement fixture"
960
+ printf 'pending:handling:fixture\n' > "$state/.watcher-down"
961
+ FM_STATE_OVERRIDE="$state" "$DRAIN" > "$dir/initial.out" 2> "$dir/initial.err" \
962
+ || fail "initial recovery drain failed"
963
+ generation=$(sed -n 's/^WAKE_ACK_REQUIRED:.*--ack-through 0 --recovery-generation \([A-Za-z0-9._-][A-Za-z0-9._-]*\)$/\1/p' "$dir/initial.err")
964
+ [ -n "$generation" ] || fail "initial recovery drain omitted its generation"
965
+ cat > "$fakebin/mv" <<'SH'
966
+ #!/usr/bin/env bash
967
+ last=${!#}
968
+ if [ "$last" = "${FM_TEST_ACK_MARKER:-}" ]; then
969
+ exit 1
970
+ fi
971
+ exec "$FM_TEST_REAL_MV" "$@"
972
+ SH
973
+ chmod +x "$fakebin/mv"
974
+
975
+ set +e
976
+ PATH="$fakebin:$PATH" FM_TEST_REAL_MV="$real_mv" FM_TEST_ACK_MARKER="$state/.watcher-down" \
977
+ FM_STATE_OVERRIDE="$state" "$DRAIN" --ack-through 0 --recovery-generation "$generation" \
978
+ > "$dir/drain.out" 2> "$dir/drain.err"
979
+ rc=$?
980
+ set -e
981
+ [ "$rc" -ne 0 ] || fail "recovery acknowledgement failure was reported as success"
982
+ grep -F 'recovery episode could not be retired safely' "$dir/drain.err" >/dev/null \
983
+ || fail "recovery acknowledgement failure had no explicit diagnostic"
984
+ grep -F 'WAKE_ACK_REQUIRED' "$dir/drain.err" >/dev/null \
985
+ || fail "recovery acknowledgement failure did not name its own remedy"
986
+ [ "$(cat "$state/.watcher-down")" = "pending:handling:$generation" ] \
987
+ || fail "failed acknowledgement corrupted the pending recovery marker"
988
+
989
+ FM_STATE_OVERRIDE="$state" "$DRAIN" --ack-through 0 --recovery-generation "$generation" \
990
+ > "$dir/retry.out" 2> "$dir/retry.err" \
991
+ || fail "recovery acknowledgement did not succeed on retry"
992
+ [ "$(cat "$state/.watcher-down")" = "acked:handling:$generation" ] \
993
+ || fail "successful retry did not acknowledge pending recovery state"
994
+ pass "wake drain: recovery acknowledgement failures are explicit and retryable"
995
+ }
996
+
997
+ test_interruption_before_and_after_raw_commit() {
998
+ local dir state before_out after_out replay_out empty_out pid rc count i sequence generation
999
+ dir=$(make_case interruption)
1000
+ state="$dir/state"
1001
+ before_out="$dir/before.out"
1002
+ after_out="$dir/after.out"
1003
+ replay_out="$dir/replay.out"
1004
+ empty_out="$dir/empty.out"
1005
+ printf 'done: interruption fixture\n' > "$state/task.status"
1006
+ append_wake "$state" signal task.status "signal: task" || fail "pre-commit interruption wake append failed"
1007
+
1008
+ FM_STATE_OVERRIDE="$state" FM_WAKE_DRAIN_TEST_DELAY_BEFORE_COMMIT=5 "$DRAIN" > "$before_out" &
1009
+ pid=$!
1010
+ i=0
1011
+ while [ "$i" -lt 100 ] && [ ! -e "$state/.wake-queue.lock" ]; do
1012
+ sleep 0.05
1013
+ i=$((i + 1))
1014
+ done
1015
+ [ -e "$state/.wake-queue.lock" ] || { kill "$pid" 2>/dev/null || true; fail "pre-commit drain never entered its serialized read boundary"; }
1016
+ kill -TERM "$pid" 2>/dev/null || fail "could not interrupt drain before raw commitment"
1017
+ set +e
1018
+ wait "$pid"
1019
+ rc=$?
1020
+ set -e
1021
+ [ "$rc" -ne 0 ] || fail "pre-commit interruption unexpectedly succeeded"
1022
+ FM_STATE_OVERRIDE="$state" "$DRAIN" > "$replay_out" 2> "$dir/replay.err" || fail "restored pre-commit wake did not drain"
1023
+ count=$(awk -F '\t' 'NF == 5 { count++ } END { print count + 0 }' "$replay_out")
1024
+ [ "$count" -eq 1 ] || fail "pre-commit interruption lost or duplicated the durable row"
1025
+ sequence=$(sed -n 's/^WAKE_ACK_REQUIRED:.*--ack-through \([0-9][0-9]*\) --recovery-generation [A-Za-z0-9._-][A-Za-z0-9._-]*$/\1/p' "$dir/replay.err")
1026
+ generation=$(sed -n 's/^WAKE_ACK_REQUIRED:.*--ack-through [0-9][0-9]* --recovery-generation \([A-Za-z0-9._-][A-Za-z0-9._-]*\)$/\1/p' "$dir/replay.err")
1027
+ FM_STATE_OVERRIDE="$state" "$DRAIN" --ack-through "$sequence" --recovery-generation "$generation" \
1028
+ || fail "pre-commit replay acknowledgement failed"
1029
+
1030
+ append_wake "$state" signal task.status "signal: task after commit" || fail "post-commit interruption wake append failed"
1031
+ FM_STATE_OVERRIDE="$state" FM_WAKE_ENRICH_TEST_DELAY=5 "$DRAIN" > "$after_out" &
1032
+ pid=$!
1033
+ wait_for_file_text "$after_out" "$(printf '\tsignal\ttask.status\t')" \
1034
+ || { kill "$pid" 2>/dev/null || true; fail "post-commit drain did not print its raw row"; }
1035
+ [ -s "$state/.wake-queue" ] \
1036
+ || { kill "$pid" 2>/dev/null || true; fail "post-commit drain consumed its raw row before handling acknowledgement"; }
1037
+ kill -TERM "$pid" 2>/dev/null || fail "could not interrupt drain after raw presentation"
1038
+ set +e
1039
+ wait "$pid"
1040
+ set -e
1041
+ FM_STATE_OVERRIDE="$state" "$DRAIN" > "$empty_out" 2> "$dir/after-replay.err" \
1042
+ || fail "drain after post-presentation interruption failed"
1043
+ count=$(awk -F '\t' 'NF == 5 { count++ } END { print count + 0 }' "$empty_out")
1044
+ [ "$count" -eq 1 ] || fail "interrupted handling did not replay its durable row exactly once"
1045
+ sequence=$(sed -n 's/^WAKE_ACK_REQUIRED:.*--ack-through \([0-9][0-9]*\) --recovery-generation [A-Za-z0-9._-][A-Za-z0-9._-]*$/\1/p' "$dir/after-replay.err")
1046
+ generation=$(sed -n 's/^WAKE_ACK_REQUIRED:.*--ack-through [0-9][0-9]* --recovery-generation \([A-Za-z0-9._-][A-Za-z0-9._-]*\)$/\1/p' "$dir/after-replay.err")
1047
+ FM_STATE_OVERRIDE="$state" "$DRAIN" --ack-through "$sequence" --recovery-generation "$generation" \
1048
+ || fail "post-interruption replay acknowledgement failed"
1049
+ [ ! -s "$state/.wake-queue" ] || fail "acknowledged interrupted wake remained durable"
1050
+ pass "interruptions preserve durable rows until post-handling acknowledgement"
1051
+ }
1052
+
1053
+ # The guarded self-announced status append (fm_wake_status_append_self_announced)
1054
+ # and the seen-signature gate it shares with the watcher's signal scan. Both
1055
+ # directions of the dedup contract are pinned through the real library
1056
+ # functions: a fully announced file plus the home's own bookkeeping close stays
1057
+ # announced (no wake), while ANY unannounced byte - a pending foreign line, a
1058
+ # missing marker, a later different note - reads as wake-worthy.
1059
+ test_self_announced_append_guards() {
1060
+ local dir state status
1061
+ dir=$(make_case self-announced-append)
1062
+ state="$dir/state"
1063
+ status="$state/t.status"
1064
+
1065
+ run_wake_lib() {
1066
+ FM_STATE_OVERRIDE="$state" bash -c '
1067
+ . "$1"; shift; "$@"
1068
+ ' _ "$ROOT/bin/fm-wake-lib.sh" "$@"
1069
+ }
1070
+
1071
+ # FIRST status change: a fresh file with no marker is unannounced (wakes).
1072
+ printf 'working: first line\n' > "$status"
1073
+ run_wake_lib fm_wake_signal_seen_current "$state" "$status" \
1074
+ && fail "a never-announced status file read as already announced"
1075
+
1076
+ # Prime the marker to current (the watcher just surfaced/absorbed everything).
1077
+ prime_status_seen "$state" "$status" || fail "could not prime the seen marker"
1078
+
1079
+ # A self-announced bookkeeping close on a fully announced file is suppressed.
1080
+ run_wake_lib fm_wake_status_append_self_announced "$state" "$status" \
1081
+ 'resolved [key=k1]: answered: closed by this home' \
1082
+ || fail "self-announced append on an announced file was not suppressed (rc=$?)"
1083
+ grep -Fq 'resolved [key=k1]: answered: closed by this home' "$status" \
1084
+ || fail "the suppressed close was not appended"
1085
+ run_wake_lib fm_wake_signal_seen_current "$state" "$status" \
1086
+ || fail "the self-announced close left unannounced bytes behind"
1087
+
1088
+ # A later DIFFERENT note from any other writer still wakes.
1089
+ printf 'needs-decision [key=k2]: a new decision\n' >> "$status"
1090
+ run_wake_lib fm_wake_signal_seen_current "$state" "$status" \
1091
+ && fail "a later different note on the same task read as already announced"
1092
+
1093
+ # With that foreign line pending, a bookkeeping close must NOT advance the
1094
+ # marker over it: the close appends but the file stays wake-worthy.
1095
+ local rc=0
1096
+ run_wake_lib fm_wake_status_append_self_announced "$state" "$status" \
1097
+ 'resolved [key=k1]: answered: second close' || rc=$?
1098
+ [ "$rc" -eq 1 ] || fail "a close over pending foreign bytes did not fail toward waking (rc=$rc)"
1099
+ grep -Fq 'resolved [key=k1]: answered: second close' "$status" \
1100
+ || fail "the fail-toward-waking close was not appended"
1101
+ run_wake_lib fm_wake_signal_seen_current "$state" "$status" \
1102
+ && fail "a close over pending foreign bytes swallowed the pending wake"
1103
+
1104
+ # UTF-8 close on an announced file: byte accounting must hold for multibyte.
1105
+ prime_status_seen "$state" "$status" || fail "could not re-prime the seen marker"
1106
+ run_wake_lib fm_wake_status_append_self_announced "$state" "$status" \
1107
+ "$(printf 'resolved [key=k2]: answered: caf\xc3\xa9 rentr\xc3\xa9e')" \
1108
+ || fail "a multibyte self-announced close was not suppressed (rc=$?)"
1109
+ run_wake_lib fm_wake_signal_seen_current "$state" "$status" \
1110
+ || fail "multibyte byte accounting broke the self-announce guard"
1111
+
1112
+ pass "self-announced appends suppress only their own bytes and fail toward waking"
1113
+ }
1114
+
1115
+ # A trap that fires inside a lock's critical section abandons the holding
1116
+ # frame, and the exit path then re-acquires the same lock (a TERM inside a
1117
+ # recovery-marker section is the reproduced case: the watcher's reap wedged
1118
+ # forever spinning against its own pid). The same-process re-acquire must
1119
+ # reclaim the abandoned hold, while a SUBSHELL still waits on its parent's
1120
+ # live hold exactly as before.
1121
+ test_self_held_lock_reclaims_instead_of_deadlocking() {
1122
+ local dir state rc
1123
+ dir=$(make_case self-held-lock)
1124
+ state="$dir/state"
1125
+ rc=0
1126
+ FM_STATE_OVERRIDE="$state" bash -c '
1127
+ . "$1"
1128
+ lock="$2/.fixture.lock"
1129
+ fm_lock_acquire_wait "$lock" || exit 10
1130
+ fm_lock_try_acquire "$lock" || exit 11
1131
+ fm_lock_release "$lock"
1132
+ [ ! -e "$lock" ] && [ ! -L "$lock" ] || exit 12
1133
+ ' _ "$ROOT/bin/fm-wake-lib.sh" "$state" || rc=$?
1134
+ [ "$rc" -eq 0 ] || fail "self-held lock was not reclaimed cleanly (rc=$rc)"
1135
+ rc=0
1136
+ FM_STATE_OVERRIDE="$state" bash -c '
1137
+ . "$1"
1138
+ lock="$2/.fixture2.lock"
1139
+ fm_lock_acquire_wait "$lock" || exit 10
1140
+ ( fm_lock_try_acquire "$lock" && exit 13; exit 0 ) || exit 13
1141
+ fm_lock_release "$lock"
1142
+ ' _ "$ROOT/bin/fm-wake-lib.sh" "$state" || rc=$?
1143
+ [ "$rc" -eq 0 ] || fail "a subshell reclaimed its parent's live hold (rc=$rc)"
1144
+ pass "an abandoned same-process lock hold is reclaimed; a parent's live hold is not"
1145
+ }
1146
+
1147
+ # Drain-time historical annotation staleness: a turn-ended-only wake row must
1148
+ # not present an already-announced status line as a new update, while a status
1149
+ # file with unannounced bytes keeps its annotation and a direct status row is
1150
+ # always annotated. Driven through the real drain executable.
1151
+ test_historical_annotation_skips_announced_status() {
1152
+ local dir state out err
1153
+ dir=$(make_case historical-annotation)
1154
+ state="$dir/state"
1155
+ out="$dir/drain.out"
1156
+ err="$dir/drain.err"
1157
+
1158
+ printf 'working: long scout still going\n' > "$state/scout.status"
1159
+ prime_status_seen "$state" "$state/scout.status" \
1160
+ || fail "could not prime the scout seen marker"
1161
+ : > "$state/scout.turn-ended"
1162
+ append_wake "$state" signal scout.turn-ended "signal: $state/scout.turn-ended" \
1163
+ || fail "turn-ended wake append failed"
1164
+ FM_STATE_OVERRIDE="$state" "$DRAIN" > "$out" 2> "$err" || fail "drain failed"
1165
+ if grep -F 'scout.status: working: long scout still going' "$out" >/dev/null; then
1166
+ fail "a fully announced status line was replayed as a historical annotation"
1167
+ fi
1168
+ grep -F 'scout.turn-ended' "$out" >/dev/null \
1169
+ || fail "suppressing the stale annotation dropped the turn-ended wake row itself"
1170
+ ack_drain_err "$state" "$err" || fail "could not acknowledge the first drain"
1171
+
1172
+ # Unannounced status bytes: the historical annotation is genuinely new
1173
+ # information and must stay.
1174
+ printf 'working: fresh unannounced progress\n' >> "$state/scout.status"
1175
+ : > "$state/scout.turn-ended"
1176
+ append_wake "$state" signal scout.turn-ended "signal: $state/scout.turn-ended" \
1177
+ || fail "second turn-ended wake append failed"
1178
+ FM_STATE_OVERRIDE="$state" "$DRAIN" > "$out" 2> "$err" || fail "second drain failed"
1179
+ grep -F 'historical / not necessarily the triggering event: scout.status: working: fresh unannounced progress' "$out" >/dev/null \
1180
+ || fail "an unannounced status line lost its historical annotation"
1181
+ ack_drain_err "$state" "$err" || fail "could not acknowledge the second drain"
1182
+
1183
+ # A direct status row is the announcement itself and is always annotated,
1184
+ # even when the seen marker already covers the file.
1185
+ printf 'done: scout finished\n' >> "$state/scout.status"
1186
+ prime_status_seen "$state" "$state/scout.status" \
1187
+ || fail "could not prime the marker for the direct-row leg"
1188
+ append_wake "$state" signal scout.status "signal: $state/scout.status" \
1189
+ || fail "direct status wake append failed"
1190
+ FM_STATE_OVERRIDE="$state" "$DRAIN" > "$out" 2> "$err" || fail "third drain failed"
1191
+ grep -F 'scout.status: done: scout finished' "$out" >/dev/null \
1192
+ || fail "a direct status row lost its annotation"
1193
+ pass "historical annotations replay nothing already announced and keep everything new"
1194
+ }
1195
+
1196
+ test_self_held_lock_reclaims_instead_of_deadlocking
1197
+ test_secondmate_foreign_queue_stall_is_one_shot_and_read_only
1198
+ test_secondmate_stall_marker_rejects_symlink
1199
+ test_acknowledged_stall_publication_survives_pre_marker_crash
1200
+ test_empty_prefix_mate_preserves_other_mate_receipt
1201
+ test_self_announced_append_guards
1202
+ test_historical_annotation_skips_announced_status
1203
+ test_concurrent_append_and_drain
1204
+ test_signal_catchup_without_running_watcher
1205
+ test_stale_enqueue_before_suppressor
1206
+ test_not_working_stale_enqueue_before_suppressor
1207
+ test_check_output_is_queued
1208
+ test_atomic_double_drain
1209
+ test_drain_dedupes_obvious_duplicates
1210
+ test_drain_asserts_watcher_liveness
1211
+ test_structural_signal_enrichment_preserves_raw_rows
1212
+ test_enrichment_preserves_all_unread_lines_and_status_file_failures
1213
+ test_slow_annotation_does_not_block_append_and_deleted_file_fails_open
1214
+ test_branch_actor_scoped_ack_never_swallows_a_main_owned_row
1215
+ test_main_drain_excludes_rows_already_granted_to_branch
1216
+ test_branch_grant_refuses_rows_already_claimed_by_main
1217
+ test_actor_filter_precedes_same_key_deduplication
1218
+ test_main_reclaims_a_grant_whose_branch_owner_exited
1219
+ test_branch_actor_without_eligible_snapshot_refuses
1220
+ test_wake_publish_requires_atomic_recovery_evidence
1221
+ test_legacy_generationless_wake_is_adopted
1222
+ test_stale_recovery_generation_cannot_touch_a_newer_episode
1223
+ test_recovery_ack_failure_is_reported
1224
+ test_interruption_before_and_after_raw_commit