@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,1288 @@
1
+ #!/usr/bin/env bash
2
+ # Parent-owned secondmate pending-reply guards (bin/fm-pending-reply-lib.sh).
3
+ #
4
+ # Reproduces the missed-report experience: a marked request is delivered, the
5
+ # target turn completes, and no correlated parent report arrives. The parent
6
+ # must notice without scraping conversation, send exactly one recovery repost,
7
+ # and escalate once if that recovery turn is also missed.
8
+ #
9
+ # Coverage:
10
+ # 1. Normal correlated reply resolves once
11
+ # 2. Completed turn with no report triggers one recovery only
12
+ # 3. Recovery reply resolves the original expectation
13
+ # 4. Second missed turn escalates once and remains durable
14
+ # 5. Transport success cannot masquerade as reply success
15
+ # 6. Unrelated events and stale correlation ids cannot resolve a request
16
+ # 7. Restart/compaction preserves the expectation and exact parent destination
17
+ # 8. Wrong-home reports are detected but do not silently acknowledge
18
+ # 9. Direct unmarked captain input creates no expectation
19
+ # 10. fm-send secondmate path embeds corr and creates durable pending records
20
+ # 11. Backend busy/idle observation works through the shared busy abstraction
21
+ # used by Pi/Claude secondmate backends (no conversation scrape)
22
+ # 12. A remote mate's repost waits for its asynchronous reply mirror to be read
23
+ # past the turn, so a mirrored reply is never nagged and a real miss still
24
+ # gets its one repost
25
+ set -u
26
+
27
+ # shellcheck source=tests/lib.sh
28
+ . "$(dirname "${BASH_SOURCE[0]}")/lib.sh"
29
+ # shellcheck source=bin/fm-marker-lib.sh
30
+ . "$ROOT/bin/fm-marker-lib.sh"
31
+ # shellcheck source=bin/fm-pending-reply-lib.sh
32
+ . "$ROOT/bin/fm-pending-reply-lib.sh"
33
+
34
+ SEND="$ROOT/bin/fm-send.sh"
35
+ REPORT="$ROOT/bin/fm-secondmate-report.sh"
36
+ TMP_ROOT=$(fm_test_tmproot fm-pending-reply)
37
+
38
+ export FM_PENDING_REPLY_GRACE_SECS=0
39
+ export FM_SEND_SETTLE=0
40
+
41
+ # --- fixtures ---------------------------------------------------------------
42
+
43
+ make_stubs() { # <dir> -> fakebin
44
+ local dir=$1 fb="$1/fakebin"
45
+ mkdir -p "$fb"
46
+ cat > "$fb/tmux" <<'SH'
47
+ #!/usr/bin/env bash
48
+ set -u
49
+ case "${1:-}" in
50
+ send-keys)
51
+ shift
52
+ literal=0
53
+ while [ $# -gt 0 ]; do
54
+ case "$1" in
55
+ -t) shift 2 ;;
56
+ -l) literal=1; shift ;;
57
+ *) break ;;
58
+ esac
59
+ done
60
+ if [ "$literal" = 1 ]; then
61
+ printf '%s' "${1:-}" >> "$FM_SEND_LOG"
62
+ fi
63
+ exit 0 ;;
64
+ display-message)
65
+ for a in "$@"; do case "$a" in *cursor_y*) printf '1\n'; exit 0 ;; esac; done
66
+ printf 'fakepane\n'; exit 0 ;;
67
+ capture-pane) printf '╭────╮\n│ │\n╰────╯\n'; exit 0 ;;
68
+ list-windows) exit 0 ;;
69
+ esac
70
+ exit 0
71
+ SH
72
+ chmod +x "$fb/tmux"
73
+ cat > "$fb/sleep" <<'SH'
74
+ #!/usr/bin/env bash
75
+ exit 0
76
+ SH
77
+ chmod +x "$fb/sleep"
78
+ printf '%s\n' "$fb"
79
+ }
80
+
81
+ setup_parent() { # <name> -> home
82
+ local home="$TMP_ROOT/$1-$RANDOM"
83
+ mkdir -p "$home/state"
84
+ printf '%s\n' "$home"
85
+ }
86
+
87
+ run_send() {
88
+ local fb=$1 home=$2 log=$3; shift 3
89
+ : > "$log"
90
+ env PATH="$fb:$PATH" \
91
+ FM_ROOT_OVERRIDE="$home" FM_HOME="$home" FM_SEND_LOG="$log" FM_SEND_SETTLE=0 \
92
+ FM_PENDING_REPLY_GRACE_SECS=0 \
93
+ "$SEND" "$@" 2>/dev/null
94
+ }
95
+
96
+ phase_of() { # <state> <corr>
97
+ fm_pending_reply_get "$(fm_pending_reply_path "$1" "$2")" phase
98
+ }
99
+
100
+ # A local steer now rides the durable steering inbox rather than the typed
101
+ # channel, so marker/corr assertions read the latest enqueued record through
102
+ # the production owner (bin/fm-task-inbox-lib.sh).
103
+ latest_record_body() { # <home> <task>
104
+ local rec
105
+ rec=$(find "$1/state/$2.inbox" -maxdepth 1 -name '*.msg' 2>/dev/null | sort | tail -1)
106
+ [ -n "$rec" ] || return 1
107
+ bash -c '. "$1"; fm_task_inbox_body "$2"' _ "$ROOT/bin/fm-task-inbox-lib.sh" "$rec"
108
+ }
109
+
110
+ # --- tests ------------------------------------------------------------------
111
+
112
+ test_normal_correlated_reply_resolves_once() {
113
+ local home state corr status rec
114
+ home=$(setup_parent resolve-once)
115
+ state="$home/state"
116
+ export FM_PENDING_REPLY_NOW=1000
117
+ corr=$(fm_pending_reply_create "$home" "$state" "hibit" "audit the ledger")
118
+ fm_pending_reply_mark_delivered "$state" "$corr"
119
+ status="$state/hibit.status"
120
+ if fm_pending_reply_try_resolve "$state" "$corr"; then
121
+ fail "missing status must not resolve"
122
+ fi
123
+ printf 'done [corr=%s]: ledger clean\n' "$corr" > "$status"
124
+ fm_pending_reply_try_resolve "$state" "$corr" || fail "correlated status should resolve"
125
+ [ "$(phase_of "$state" "$corr")" = resolved ] || fail "phase should be resolved"
126
+ # Idempotent second resolve.
127
+ fm_pending_reply_try_resolve "$state" "$corr" || fail "second resolve must stay successful"
128
+ [ "$(phase_of "$state" "$corr")" = resolved ] || fail "phase must remain resolved"
129
+ rec=$(fm_pending_reply_path "$state" "$corr")
130
+ [ "$(fm_pending_reply_get "$rec" resolved_via)" = status ] \
131
+ || fail "resolved_via should be status"
132
+ pass "normal correlated reply resolves once (idempotent)"
133
+ }
134
+
135
+ test_completed_turn_no_report_triggers_one_recovery() {
136
+ local home state corr hook_log rec
137
+ home=$(setup_parent one-recovery)
138
+ state="$home/state"
139
+ hook_log="$TMP_ROOT/recovery-hook.log"
140
+ : > "$hook_log"
141
+ export FM_PENDING_REPLY_NOW=2000
142
+ export FM_PENDING_REPLY_SEND_HOOK='printf "%s\t%s\n" >>"'"$hook_log"'"'
143
+ # The hook above is wrong for eval form - use a function.
144
+ # Invoked indirectly through FM_PENDING_REPLY_SEND_HOOK.
145
+ # shellcheck disable=SC2329
146
+ recovery_hook() {
147
+ printf '%s\t%s\n' "$1" "$2" >> "$hook_log"
148
+ }
149
+ export -f recovery_hook
150
+ export FM_PENDING_REPLY_SEND_HOOK='recovery_hook'
151
+
152
+ corr=$(fm_pending_reply_create "$home" "$state" "hibit" "status of phase 7")
153
+ fm_pending_reply_mark_delivered "$state" "$corr"
154
+ # Turn completes with no parent report (the Hi Bit missed-report shape).
155
+ fm_pending_reply_observe_busy "$state" "$corr" busy
156
+ fm_pending_reply_observe_busy "$state" "$corr" idle
157
+ fm_pending_reply_send_recovery "$state" "$corr" \
158
+ || fail "recovery should send after completed turn + grace"
159
+ [ "$(phase_of "$state" "$corr")" = recovery_sent ] \
160
+ || fail "phase should be recovery_sent, got $(phase_of "$state" "$corr")"
161
+ [ -s "$hook_log" ] || fail "recovery hook should have been invoked once"
162
+ # Second attempt must not re-send.
163
+ if fm_pending_reply_send_recovery "$state" "$corr" 2>/dev/null; then
164
+ fail "second recovery must refuse"
165
+ fi
166
+ lines=$(wc -l < "$hook_log" | tr -d ' ')
167
+ [ "$lines" = 1 ] || fail "expected exactly one recovery send, got $lines"
168
+ rec=$(fm_pending_reply_path "$state" "$corr")
169
+ case "$(cat "$hook_log")" in
170
+ *"corr=$corr"*) : ;;
171
+ *) fail "recovery message must carry the original corr"$'\n'"$(cat "$hook_log")" ;;
172
+ esac
173
+ case "$(cat "$hook_log")" in
174
+ *REPOST\ REQUIRED*) : ;;
175
+ *) fail "recovery message must ask for a repost"$'\n'"$(cat "$hook_log")" ;;
176
+ esac
177
+ pass "completed turn with no report triggers exactly one recovery"
178
+ }
179
+
180
+ test_recovery_attempt_is_never_reinjected() {
181
+ local home state corr rec hook_log lines live_corr live_rec live_pid live_identity
182
+ home=$(setup_parent recovery-at-most-once)
183
+ state="$home/state"
184
+ hook_log="$TMP_ROOT/recovery-at-most-once.log"
185
+ : > "$hook_log"
186
+ export FM_PENDING_REPLY_NOW=2500
187
+ recovery_fail_hook() {
188
+ printf 'attempted\n' >> "$hook_log"
189
+ return 1
190
+ }
191
+ export -f recovery_fail_hook
192
+ export FM_PENDING_REPLY_SEND_HOOK=recovery_fail_hook
193
+ corr=$(fm_pending_reply_create "$home" "$state" hibit "at most once")
194
+ fm_pending_reply_mark_delivered "$state" "$corr"
195
+ fm_pending_reply_mark_turn_completed "$state" "$corr" request
196
+ if fm_pending_reply_send_recovery "$state" "$corr"; then
197
+ fail "failed recovery transport should report failure"
198
+ fi
199
+ [ "$(phase_of "$state" "$corr")" = recovery_failed ] \
200
+ || fail "failed recovery attempt should preserve failed delivery"
201
+ [ -z "$(fm_pending_reply_get "$(fm_pending_reply_path "$state" "$corr")" recovery_sent_epoch)" ] \
202
+ || fail "failed recovery must not record a sent epoch"
203
+ if fm_pending_reply_send_recovery "$state" "$corr" 2>/dev/null; then
204
+ fail "committed recovery attempt must refuse reinjection"
205
+ fi
206
+ lines=$(wc -l < "$hook_log" | tr -d ' ')
207
+ [ "$lines" = 1 ] || fail "recovery transport should be attempted once, got $lines"
208
+ fm_pending_reply_maybe_escalate "$state" "$corr" \
209
+ || fail "failed recovery delivery should escalate explicitly"
210
+ grep -Fq "pending-reply-recovery-delivery-failed:" "$state/hibit.status" \
211
+ || fail "failed recovery escalation should name delivery failure"
212
+ live_corr=$(fm_pending_reply_create "$home" "$state" hibit "live recovery")
213
+ fm_pending_reply_mark_delivered "$state" "$live_corr"
214
+ fm_pending_reply_mark_turn_completed "$state" "$live_corr" request
215
+ live_rec=$(fm_pending_reply_path "$state" "$live_corr")
216
+ live_pid=${BASHPID:-$$}
217
+ live_identity=$(fm_pending_reply_pid_identity "$live_pid") \
218
+ || fail "live sender identity should be observable"
219
+ fm_pending_reply_set "$live_rec" recovery_attempted_epoch 2500 || fail "live attempt precommit failed"
220
+ fm_pending_reply_set "$live_rec" recovery_sender_pid "$live_pid" || fail "live sender pid commit failed"
221
+ fm_pending_reply_set "$live_rec" recovery_sender_identity "$live_identity" \
222
+ || fail "live sender identity commit failed"
223
+ fm_pending_reply_set "$live_rec" phase recovery_sending || fail "live sending phase failed"
224
+ fm_pending_reply_tick_one "$state" "$live_corr" unknown || fail "live recovery tick failed"
225
+ [ "$(phase_of "$state" "$live_corr")" = recovery_sending ] \
226
+ || fail "live recovery must remain in progress without elapsed-time inference"
227
+ corr=$(fm_pending_reply_create "$home" "$state" hibit "crashed recovery")
228
+ fm_pending_reply_mark_delivered "$state" "$corr"
229
+ fm_pending_reply_mark_turn_completed "$state" "$corr" request
230
+ rec=$(fm_pending_reply_path "$state" "$corr")
231
+ fm_pending_reply_set "$rec" recovery_attempted_epoch 2500 || fail "attempt precommit failed"
232
+ fm_pending_reply_set "$rec" phase recovery_sending || fail "sending phase precommit failed"
233
+ fm_pending_reply_tick_one "$state" "$corr" unknown || fail "recovery reconciliation failed"
234
+ [ "$(phase_of "$state" "$corr")" = escalated ] \
235
+ || fail "interrupted recovery attempt should escalate unknown delivery"
236
+ [ "$(fm_pending_reply_get "$rec" recovery_delivery_outcome)" = unknown ] \
237
+ || fail "interrupted recovery should preserve unknown delivery outcome"
238
+ [ -z "$(fm_pending_reply_get "$rec" recovery_sent_epoch)" ] \
239
+ || fail "unknown recovery must not record a sent epoch"
240
+ grep -Fq "pending-reply-recovery-delivery-unknown:" "$state/hibit.status" \
241
+ || fail "unknown recovery escalation should name delivery uncertainty"
242
+ lines=$(wc -l < "$hook_log" | tr -d ' ')
243
+ [ "$lines" = 1 ] || fail "reconciliation must not call recovery transport, got $lines attempts"
244
+ unset FM_PENDING_REPLY_SEND_HOOK
245
+ pass "recovery attempts reconcile without reinjection"
246
+ }
247
+
248
+ test_recovery_reply_resolves_original() {
249
+ local home state corr hook_log
250
+ home=$(setup_parent recovery-resolve)
251
+ state="$home/state"
252
+ hook_log="$TMP_ROOT/recovery-resolve-hook.log"
253
+ : > "$hook_log"
254
+ # Invoked indirectly through FM_PENDING_REPLY_SEND_HOOK.
255
+ # shellcheck disable=SC2329
256
+ recovery_hook() { printf '%s\n' "$2" >> "$hook_log"; }
257
+ export -f recovery_hook
258
+ export FM_PENDING_REPLY_SEND_HOOK='recovery_hook'
259
+ export FM_PENDING_REPLY_NOW=3000
260
+
261
+ corr=$(fm_pending_reply_create "$home" "$state" "hibit" "phase 7 status")
262
+ fm_pending_reply_mark_delivered "$state" "$corr"
263
+ fm_pending_reply_mark_turn_completed "$state" "$corr" request
264
+ fm_pending_reply_send_recovery "$state" "$corr" || fail "recovery send failed"
265
+ printf 'done [corr=%s]: phase 7 is Done (reposted)\n' "$corr" > "$state/hibit.status"
266
+ fm_pending_reply_try_resolve "$state" "$corr" || fail "recovery reply should resolve original"
267
+ [ "$(phase_of "$state" "$corr")" = resolved ] || fail "expected resolved after recovery reply"
268
+ pass "recovery reply resolves the original expectation"
269
+ }
270
+
271
+ test_second_missed_turn_escalates_once_and_stays_durable() {
272
+ local home state corr hook_log rec status_line escalations
273
+ home=$(setup_parent escalate-once)
274
+ state="$home/state"
275
+ hook_log="$TMP_ROOT/escalate-hook.log"
276
+ : > "$hook_log"
277
+ # Invoked indirectly through FM_PENDING_REPLY_SEND_HOOK.
278
+ # shellcheck disable=SC2329
279
+ recovery_hook() { printf '%s\n' ok >> "$hook_log"; }
280
+ export -f recovery_hook
281
+ export FM_PENDING_REPLY_SEND_HOOK='recovery_hook'
282
+ export FM_PENDING_REPLY_NOW=4000
283
+ # Do not export STATE into the test process: fm-send resolves
284
+ # FM_STATE_OVERRIDE/STATE from the environment and a leak breaks later cases.
285
+
286
+ corr=$(fm_pending_reply_create "$home" "$state" "hibit" "why is phase 7 stuck")
287
+ fm_pending_reply_mark_delivered "$state" "$corr"
288
+ fm_pending_reply_mark_turn_completed "$state" "$corr" request
289
+ fm_pending_reply_send_recovery "$state" "$corr" || fail "recovery send failed"
290
+ # Recovery turn also completes with no correlated report.
291
+ fm_pending_reply_mark_turn_completed "$state" "$corr" recovery
292
+ fm_pending_reply_maybe_escalate "$state" "$corr" || fail "escalation should fire"
293
+ [ "$(phase_of "$state" "$corr")" = escalated ] || fail "phase should be escalated"
294
+ status_line=$(tail -1 "$state/hibit.status")
295
+ case "$status_line" in
296
+ "blocked [key=pending-reply-$corr]:"*pending-reply-missed:*pending-reply-id=$corr*) : ;;
297
+ *) fail "parent status should carry one blocked missed-report line"$'\n'"$status_line" ;;
298
+ esac
299
+ [ ! -s "$state/.wake-queue" ] || fail "direct escalation must not enqueue a duplicate check wake"
300
+ # Second escalate must be a no-op (phase no longer recovery_sent).
301
+ if fm_pending_reply_maybe_escalate "$state" "$corr" 2>/dev/null; then
302
+ # Function returns 1 when phase is not recovery_sent - good.
303
+ :
304
+ fi
305
+ [ "$(phase_of "$state" "$corr")" = escalated ] || fail "phase must stay escalated"
306
+ escalations=$(grep -Fc "blocked [key=pending-reply-$corr]:" "$state/hibit.status")
307
+ [ "$escalations" = 1 ] || fail "missed recovery should publish one escalation, got $escalations"
308
+ # Durable record retained (never silently expired).
309
+ rec=$(fm_pending_reply_path "$state" "$corr")
310
+ [ -f "$rec" ] || fail "escalated record must remain on disk"
311
+ [ "$(fm_pending_reply_get "$rec" parent_status)" = "$state/hibit.status" ] \
312
+ || fail "parent destination must remain exact"
313
+ # Unrelated status activity still does not resolve.
314
+ printf 'working: unrelated churn\n' >> "$state/hibit.status"
315
+ if fm_pending_reply_try_resolve "$state" "$corr"; then
316
+ fail "unrelated status must not resolve an escalated miss"
317
+ fi
318
+ [ "$(phase_of "$state" "$corr")" = escalated ] || fail "must remain escalated after unrelated status"
319
+ pass "second missed turn escalates once and remains durable"
320
+ }
321
+
322
+ # Wake-gate helpers reading the production seen-signature owner directly, so
323
+ # these assertions consume the exact gate the watcher's signal scan uses.
324
+ seen_gate() { # <state> <file>: 0 when every byte is already announced
325
+ FM_STATE_OVERRIDE="$1" bash -c '. "$1"; fm_wake_signal_seen_current "$2" "$3"' \
326
+ _ "$ROOT/bin/fm-wake-lib.sh" "$1" "$2"
327
+ }
328
+ prime_seen() { # <state> <file>
329
+ FM_STATE_OVERRIDE="$1" bash -c '
330
+ . "$1"; fm_wake_status_mark_current "$2" "$3"
331
+ ' _ "$ROOT/bin/fm-wake-lib.sh" "$1" "$2"
332
+ }
333
+
334
+ test_escalation_wakes_and_its_close_stays_quiet() {
335
+ local home state corr
336
+ home=$(setup_parent escalation-wake-gate)
337
+ state="$home/state"
338
+ export FM_PENDING_REPLY_SEND_HOOK='true'
339
+ export FM_PENDING_REPLY_NOW=4200
340
+ corr=$(fm_pending_reply_create "$home" "$state" "hibit" "confirm the notarization")
341
+ fm_pending_reply_mark_delivered "$state" "$corr"
342
+ fm_pending_reply_mark_turn_completed "$state" "$corr" request
343
+ fm_pending_reply_send_recovery "$state" "$corr" || fail "recovery send failed"
344
+ fm_pending_reply_mark_turn_completed "$state" "$corr" recovery
345
+ : > "$state/hibit.status"
346
+ prime_seen "$state" "$state/hibit.status" || fail "could not prime the announced baseline"
347
+ # A NEW blocker must wake: the escalation append leaves unannounced bytes.
348
+ fm_pending_reply_maybe_escalate "$state" "$corr" || fail "escalation should fire"
349
+ if seen_gate "$state" "$state/hibit.status"; then
350
+ fail "a new pending-reply escalation was hidden from the watcher's signal gate"
351
+ fi
352
+ prime_seen "$state" "$state/hibit.status" || fail "could not mark the escalation announced"
353
+ # A genuinely new correlated reply must wake too.
354
+ printf 'done [corr=%s]: notarization confirmed\n' "$corr" >> "$state/hibit.status"
355
+ if seen_gate "$state" "$state/hibit.status"; then
356
+ fail "a new correlated reply was hidden from the watcher's signal gate"
357
+ fi
358
+ prime_seen "$state" "$state/hibit.status" || fail "could not mark the reply announced"
359
+ # The home's own escalation CLOSE is bookkeeping and stays quiet.
360
+ fm_pending_reply_try_resolve "$state" "$corr" || fail "correlated reply should resolve"
361
+ grep -Fq "resolved [key=pending-reply-$corr]" "$state/hibit.status" \
362
+ || fail "resolution did not close the escalation decision"
363
+ seen_gate "$state" "$state/hibit.status" \
364
+ || fail "the home's own escalation close re-woke its own watcher gate"
365
+ pass "escalations and replies wake; the home's own escalation close stays quiet"
366
+ }
367
+
368
+ test_escalation_publication_failure_retries() {
369
+ local home state corr rec target escalations
370
+ home=$(setup_parent escalation-retry)
371
+ state="$home/state"
372
+ export FM_PENDING_REPLY_NOW=4500
373
+ corr=$(fm_pending_reply_create "$home" "$state" "hibit" "retry escalation")
374
+ fm_pending_reply_mark_delivered "$state" "$corr"
375
+ fm_pending_reply_mark_turn_completed "$state" "$corr" request
376
+ export FM_PENDING_REPLY_SEND_HOOK='true'
377
+ fm_pending_reply_send_recovery "$state" "$corr" || fail "recovery send failed"
378
+ fm_pending_reply_mark_turn_completed "$state" "$corr" recovery
379
+ rec=$(fm_pending_reply_path "$state" "$corr")
380
+ target="$state/escalation-target"
381
+ mkdir -p "$target"
382
+ fm_pending_reply_set "$rec" parent_status "$target" || fail "failed to set escalation target"
383
+ if fm_pending_reply_maybe_escalate "$state" "$corr" 2>/dev/null; then
384
+ fail "escalation should fail when its durable status cannot be written"
385
+ fi
386
+ [ "$(phase_of "$state" "$corr")" = recovery_sent ] \
387
+ || fail "publication failure must leave escalation retryable"
388
+ rmdir "$target"
389
+ fm_pending_reply_maybe_escalate "$state" "$corr" || fail "escalation retry should succeed"
390
+ [ "$(phase_of "$state" "$corr")" = escalated ] || fail "successful retry should commit escalation"
391
+ escalations=$(grep -Fc "blocked [key=pending-reply-$corr]:" "$target")
392
+ [ "$escalations" = 1 ] || fail "successful retry should publish exactly once, got $escalations"
393
+ pass "failed escalation publication remains retryable and publishes once"
394
+ }
395
+
396
+ test_legacy_escalation_closes_default_decision() {
397
+ local home state corr rec open
398
+ home=$(setup_parent legacy-close)
399
+ state="$home/state"
400
+ export FM_PENDING_REPLY_NOW=4725
401
+ corr=$(fm_pending_reply_create "$home" "$state" "hibit" "legacy close")
402
+ fm_pending_reply_mark_delivered "$state" "$corr"
403
+ rec=$(fm_pending_reply_path "$state" "$corr")
404
+ fm_pending_reply_set "$rec" phase escalated
405
+ fm_pending_reply_set "$rec" escalated_epoch 4700
406
+ printf 'blocked: pending-reply-missed: task=hibit pending-reply-id=%s request=legacy close\n' "$corr" \
407
+ > "$state/hibit.status"
408
+ printf 'done [corr=%s]: delayed legacy reply\n' "$corr" >> "$state/hibit.status"
409
+
410
+ fm_pending_reply_try_resolve "$state" "$corr" || fail "legacy reply should resolve its record"
411
+ [ "$(grep -Fc "resolved [key=default]: pending-reply-resolved: task=hibit pending-reply-id=$corr" "$state/hibit.status")" -eq 1 ] \
412
+ || fail "legacy escalation did not append one guarded default-key resolution"
413
+ open=$(status_open_decisions "$state/hibit.status")
414
+ [ -z "$open" ] || fail "resolved legacy escalation remained open: $open"
415
+ [ -n "$(fm_pending_reply_get "$rec" escalation_closed_epoch)" ] \
416
+ || fail "legacy escalation closure was not recorded"
417
+ pass "legacy escalation closes under the shared default key"
418
+ }
419
+
420
+ test_legacy_escalation_does_not_close_taken_default_decision() {
421
+ local home state corr rec open
422
+ home=$(setup_parent legacy-escalation)
423
+ state="$home/state"
424
+ export FM_PENDING_REPLY_NOW=4750
425
+ corr=$(fm_pending_reply_create "$home" "$state" "hibit" "legacy escalation")
426
+ fm_pending_reply_mark_delivered "$state" "$corr"
427
+ rec=$(fm_pending_reply_path "$state" "$corr")
428
+ fm_pending_reply_set "$rec" phase escalated
429
+ fm_pending_reply_set "$rec" escalated_epoch 4700
430
+ printf 'blocked: pending-reply-missed: task=hibit pending-reply-id=%s request=legacy escalation\n' "$corr" \
431
+ > "$state/hibit.status"
432
+ printf 'blocked: unrelated operator decision\n' >> "$state/hibit.status"
433
+ printf 'done [corr=%s]: delayed legacy reply\n' "$corr" >> "$state/hibit.status"
434
+
435
+ fm_pending_reply_try_resolve "$state" "$corr" || fail "legacy reply should resolve its record"
436
+ if grep -Fq 'resolved [key=default]: pending-reply-resolved:' "$state/hibit.status"; then
437
+ fail "legacy escalation emitted an unsafe default-key resolution"
438
+ fi
439
+ fm_pending_reply_tick "$state" || fail "legacy close retry failed"
440
+ open=$(status_open_decisions "$state/hibit.status")
441
+ assert_contains "$open" "unrelated operator decision" \
442
+ "legacy escalation closure hid an unrelated default-key decision"
443
+ pass "legacy escalation cannot close an unrelated default-key decision"
444
+ }
445
+
446
+ test_foreign_blocker_is_not_selected_as_escalation() {
447
+ local home state corr rec open
448
+ home=$(setup_parent foreign-blocker)
449
+ state="$home/state"
450
+ export FM_PENDING_REPLY_NOW=4775
451
+ export FM_PENDING_REPLY_SEND_HOOK=true
452
+ corr=$(fm_pending_reply_create "$home" "$state" "hibit" "foreign blocker")
453
+ fm_pending_reply_mark_delivered "$state" "$corr"
454
+ fm_pending_reply_mark_turn_completed "$state" "$corr" request
455
+ fm_pending_reply_send_recovery "$state" "$corr" || fail "recovery send failed"
456
+ fm_pending_reply_mark_turn_completed "$state" "$corr" recovery
457
+ fm_pending_reply_maybe_escalate "$state" "$corr" || fail "genuine escalation failed"
458
+ rec=$(fm_pending_reply_path "$state" "$corr")
459
+ printf 'blocked [key=release]: foreign decision pending-reply-id=%s corr=%s\n' \
460
+ "$corr" "$corr" >> "$state/hibit.status"
461
+
462
+ fm_pending_reply_try_resolve "$state" "$corr" || fail "correlated foreign blocker should resolve the record"
463
+ open=$(status_open_decisions "$state/hibit.status")
464
+ assert_contains "$open" $'release\tblocked\tforeign decision' \
465
+ "pending-reply closure cleared the foreign release decision"
466
+ assert_not_contains "$open" "pending-reply-$corr" \
467
+ "genuine keyed escalation remained open"
468
+ assert_no_grep 'resolved [key=release]: pending-reply-resolved:' "$state/hibit.status" \
469
+ "foreign release decision was selected as the pending-reply escalation"
470
+ [ -n "$(fm_pending_reply_get "$rec" escalation_closed_epoch)" ] \
471
+ || fail "genuine keyed escalation closure was not recorded"
472
+ pass "foreign correlated blocker cannot impersonate a pending-reply escalation"
473
+ }
474
+
475
+ test_concurrent_resolution_closes_escalation_once() {
476
+ local home state corr rec
477
+ home=$(setup_parent concurrent-resolution)
478
+ state="$home/state"
479
+ export FM_PENDING_REPLY_NOW=4800
480
+ corr=$(fm_pending_reply_create "$home" "$state" "hibit" "concurrent resolution")
481
+ fm_pending_reply_mark_delivered "$state" "$corr"
482
+ rec=$(fm_pending_reply_path "$state" "$corr")
483
+ fm_pending_reply_set "$rec" phase escalated
484
+ fm_pending_reply_set "$rec" escalated_epoch 4750
485
+ printf 'blocked [key=pending-reply-%s]: pending-reply-missed: task=hibit pending-reply-id=%s request=concurrent resolution\n' \
486
+ "$corr" "$corr" > "$state/hibit.status"
487
+ printf 'done [corr=%s]: concurrent delayed reply\n' "$corr" >> "$state/hibit.status"
488
+
489
+ for _ in 1 2 3 4 5 6 7 8; do
490
+ fm_pending_reply_try_resolve "$state" "$corr" &
491
+ done
492
+ wait
493
+
494
+ [ "$(phase_of "$state" "$corr")" = resolved ] \
495
+ || fail "concurrent resolvers left the expectation unresolved"
496
+ [ "$(grep -Fc "pending-reply-resolved: task=hibit pending-reply-id=$corr" "$state/hibit.status")" -eq 1 ] \
497
+ || fail "concurrent resolvers did not append exactly one decision close"
498
+ [ -n "$(fm_pending_reply_get "$rec" escalation_closed_epoch)" ] \
499
+ || fail "concurrent resolution did not record the closed escalation"
500
+ pass "concurrent resolution closes one keyed escalation exactly once"
501
+ }
502
+
503
+ test_concurrent_escalation_yields_to_late_reply() {
504
+ local home state corr rec
505
+ home=$(setup_parent concurrent-escalation)
506
+ state="$home/state"
507
+ export FM_PENDING_REPLY_NOW=4900
508
+ corr=$(fm_pending_reply_create "$home" "$state" "hibit" "concurrent escalation")
509
+ fm_pending_reply_mark_delivered "$state" "$corr"
510
+ rec=$(fm_pending_reply_path "$state" "$corr")
511
+ fm_pending_reply_set "$rec" phase recovery_sent
512
+ fm_pending_reply_set "$rec" recovery_turn_completed_epoch 4850
513
+ printf 'done [corr=%s]: late concurrent reply\n' "$corr" > "$state/hibit.status"
514
+
515
+ for _ in 1 2 3 4 5 6 7 8; do
516
+ fm_pending_reply_maybe_escalate "$state" "$corr" &
517
+ fm_pending_reply_try_resolve "$state" "$corr" &
518
+ done
519
+ wait
520
+
521
+ [ "$(phase_of "$state" "$corr")" = resolved ] \
522
+ || fail "concurrent escalation overwrote a resolved expectation"
523
+ assert_no_grep "pending-reply-id=$corr" "$state/hibit.status" \
524
+ "concurrent escalation published a false missed-reply blocker"
525
+ [ -z "$(fm_pending_reply_get "$rec" escalated_epoch)" ] \
526
+ || fail "concurrent escalation committed after the reply resolved"
527
+ pass "concurrent escalation yields to a late correlated reply"
528
+ }
529
+
530
+ test_transport_success_is_not_reply_success() {
531
+ local home state corr
532
+ home=$(setup_parent transport-not-reply)
533
+ state="$home/state"
534
+ export FM_PENDING_REPLY_NOW=5000
535
+ corr=$(fm_pending_reply_create "$home" "$state" "hibit" "ping")
536
+ fm_pending_reply_mark_delivered "$state" "$corr" || fail "mark delivered failed"
537
+ [ "$(phase_of "$state" "$corr")" = awaiting_report ] \
538
+ || fail "delivery must leave phase awaiting_report, got $(phase_of "$state" "$corr")"
539
+ if fm_pending_reply_try_resolve "$state" "$corr"; then
540
+ fail "delivery alone must not resolve"
541
+ fi
542
+ pass "transport success cannot masquerade as reply success"
543
+ }
544
+
545
+ test_undelivered_records_are_scan_immutable() {
546
+ (
547
+ local home state sm_home corr rec before after
548
+ home=$(setup_parent undelivered-scan)
549
+ state="$home/state"
550
+ sm_home="$home/sm"
551
+ mkdir -p "$sm_home/state"
552
+ # This fixture clock is intentionally scoped to the isolated subshell.
553
+ # shellcheck disable=SC2030,SC2031
554
+ export FM_PENDING_REPLY_NOW=5500
555
+ corr=$(fm_pending_reply_create "$home" "$state" hibit "not delivered yet")
556
+ rec=$(fm_pending_reply_path "$state" "$corr")
557
+ printf 'done [corr=%s]: arrived too early\n' "$corr" > "$state/hibit.status"
558
+ printf 'done [corr=%s]: wrong home too early\n' "$corr" > "$sm_home/state/hibit.status"
559
+ fm_write_secondmate_meta "$state/hibit.meta" "$sm_home" "sess:fm-hibit"
560
+ before=$(cat "$rec")
561
+ if fm_pending_reply_try_resolve "$state" "$corr"; then
562
+ fail "undelivered expectation must not resolve"
563
+ fi
564
+ fm_pending_reply_detect_wrong_home "$state" "$corr" "$sm_home" \
565
+ || fail "undelivered wrong-home check should be inert"
566
+ fm_pending_reply_tick_one "$state" "$corr" busy "$sm_home" \
567
+ || fail "undelivered direct tick should be inert"
568
+ fm_backend_busy_state() { fail "undelivered watcher tick must not probe the backend"; }
569
+ fm_backend_capture() { fail "undelivered watcher tick must not capture the backend"; }
570
+ fm_pending_reply_tick "$state" || fail "undelivered watcher tick should succeed"
571
+ after=$(cat "$rec")
572
+ [ "$after" = "$before" ] || fail "scan paths must not mutate an undelivered record"
573
+ fm_pending_reply_mark_delivered "$state" "$corr" || fail "delivery marker should succeed"
574
+ fm_pending_reply_tick_one "$state" "$corr" unknown "$sm_home" \
575
+ || fail "delivered direct tick should succeed"
576
+ [ "$(phase_of "$state" "$corr")" = resolved ] \
577
+ || fail "correlated parent status should resolve after delivery"
578
+ ) || fail "undelivered scan immutability regression failed"
579
+ pass "undelivered records remain immutable across scan paths"
580
+ }
581
+
582
+ test_delivery_confirmation_fallback_reconciles() {
583
+ (
584
+ local home state corr rec marker rc prepared_corr prepared_rec prepared_marker escalations
585
+ local reported_corr reported_rec reported_marker
586
+ home=$(setup_parent delivery-confirmation)
587
+ state="$home/state"
588
+ # This fixture clock is intentionally scoped to the isolated subshell.
589
+ # shellcheck disable=SC2030,SC2031
590
+ export FM_PENDING_REPLY_NOW=5750
591
+ corr=$(fm_pending_reply_create "$home" "$state" hibit "confirmed delivery")
592
+ rec=$(fm_pending_reply_path "$state" "$corr")
593
+ fm_pending_reply_mark_delivered() { return 1; }
594
+ if fm_pending_reply_confirm_delivery "$state" "$corr"; then
595
+ fail "primary delivery commit failure should be reported"
596
+ else
597
+ rc=$?
598
+ fi
599
+ [ "$rc" = 2 ] || fail "durable fallback should return status 2, got $rc"
600
+ marker=$(fm_pending_reply_delivery_confirmation_path "$state" "$corr")
601
+ [ -f "$marker" ] || fail "delivery confirmation fallback marker should persist"
602
+ [ -z "$(fm_pending_reply_get "$rec" delivered_epoch)" ] \
603
+ || fail "failed primary commit should leave delivered_epoch empty"
604
+ . "$ROOT/bin/fm-pending-reply-lib.sh"
605
+ fm_pending_reply_tick_one "$state" "$corr" unknown \
606
+ || fail "watcher should reconcile the delivery marker"
607
+ [ "$(fm_pending_reply_get "$rec" delivered_epoch)" = 5750 ] \
608
+ || fail "watcher should restore the confirmed delivery epoch"
609
+ [ ! -e "$marker" ] || fail "reconciled delivery marker should be removed"
610
+ prepared_corr=$(fm_pending_reply_create "$home" "$state" hibit "prepared delivery")
611
+ prepared_rec=$(fm_pending_reply_path "$state" "$prepared_corr")
612
+ fm_pending_reply_prepare_delivery "$state" "$prepared_corr" \
613
+ || fail "delivery preparation should persist before transport"
614
+ fm_pending_reply_set "$prepared_rec" grace_secs 10 \
615
+ || fail "delivery-unknown grace fixture should persist"
616
+ prepared_marker=$(fm_pending_reply_delivery_confirmation_path "$state" "$prepared_corr")
617
+ [ -f "$prepared_marker" ] || fail "prepared delivery marker should persist"
618
+ fm_pending_reply_tick_one "$state" "$prepared_corr" unknown \
619
+ || fail "watcher should preserve interrupted delivery state"
620
+ [ "$(phase_of "$state" "$prepared_corr")" = awaiting_report ] \
621
+ || fail "attempted delivery should remain pending during bounded grace"
622
+ [ -z "$(fm_pending_reply_get "$prepared_rec" delivered_epoch)" ] \
623
+ || fail "attempted delivery must never be promoted without confirmation"
624
+ [ -e "$prepared_marker" ] || fail "unknown delivery marker should remain durable"
625
+ export FM_PENDING_REPLY_NOW=5760
626
+ fm_pending_reply_write_delivery_confirmation \
627
+ "$state" "$prepared_corr" attempted 5750 \
628
+ || fail "orphaned attempt fixture should persist"
629
+ fm_pending_reply_tick_one "$state" "$prepared_corr" unknown \
630
+ || fail "orphaned delivery attempt should escalate"
631
+ [ "$(phase_of "$state" "$prepared_corr")" = escalated ] \
632
+ || fail "orphaned delivery attempt should become one durable escalation"
633
+ [ -z "$(fm_pending_reply_get "$prepared_rec" delivered_epoch)" ] \
634
+ || fail "delivery-unknown escalation must not manufacture delivery"
635
+ grep -Fq "pending-reply-delivery-unknown:" "$state/hibit.status" \
636
+ || fail "delivery uncertainty should use its distinct escalation"
637
+ fm_pending_reply_tick_one "$state" "$prepared_corr" unknown \
638
+ || fail "repeated delivery-unknown tick should be inert"
639
+ escalations=$(grep -Fc "blocked [key=pending-reply-$prepared_corr]:" "$state/hibit.status")
640
+ [ "$escalations" = 1 ] \
641
+ || fail "delivery-unknown escalation should publish once, got $escalations"
642
+ printf 'done [corr=%s]: late report proves delivery\n' "$prepared_corr" >> "$state/hibit.status"
643
+ fm_pending_reply_tick "$state" || fail "watcher should accept a late delivery report"
644
+ [ "$(phase_of "$state" "$prepared_corr")" = resolved ] \
645
+ || fail "late report should resolve escalated delivery-unknown"
646
+ [ "$(fm_pending_reply_get "$prepared_rec" delivered_epoch)" = 5760 ] \
647
+ || fail "late report should provide delivery evidence"
648
+ escalations=$(grep -Fc "blocked [key=pending-reply-$prepared_corr]:" "$state/hibit.status")
649
+ [ "$escalations" = 1 ] || fail "late report must not re-escalate delivery-unknown"
650
+ fm_pending_reply_tick "$state" || fail "resolved late report should remain idempotent"
651
+ [ "$(phase_of "$state" "$prepared_corr")" = resolved ] \
652
+ || fail "late report resolution should remain durable"
653
+ export FM_PENDING_REPLY_NOW=5800
654
+ reported_corr=$(fm_pending_reply_create "$home" "$state" hibit "reported delivery")
655
+ reported_rec=$(fm_pending_reply_path "$state" "$reported_corr")
656
+ fm_pending_reply_prepare_delivery "$state" "$reported_corr" \
657
+ || fail "reported delivery attempt should persist"
658
+ reported_marker=$(fm_pending_reply_delivery_confirmation_path "$state" "$reported_corr")
659
+ printf 'done [corr=%s]: report proves delivery\n' "$reported_corr" >> "$state/hibit.status"
660
+ fm_pending_reply_try_resolve "$state" "$reported_corr" \
661
+ || fail "attempted delivery with a report should resolve directly"
662
+ [ "$(phase_of "$state" "$reported_corr")" = resolved ] \
663
+ || fail "correlated report should resolve attempted delivery"
664
+ [ "$(fm_pending_reply_get "$reported_rec" delivered_epoch)" = 5800 ] \
665
+ || fail "correlated report should provide delivery evidence"
666
+ [ ! -e "$reported_marker" ] || fail "resolved delivery marker should be removed"
667
+ fm_pending_reply_tick_one "$state" "$reported_corr" unknown \
668
+ || fail "resolved attempted delivery should remain inert in watcher"
669
+ if grep -Fq "pending-reply-id=$reported_corr" "$state/hibit.status"; then
670
+ fail "reported attempted delivery must not escalate as delivery-unknown"
671
+ fi
672
+ ) || fail "delivery confirmation fallback regression failed"
673
+ pass "delivery confirmation fallback reconciles durably"
674
+ }
675
+
676
+ test_delivery_confirmation_serializes_with_reconciliation() {
677
+ (
678
+ local home state corr rec calls entered release confirm_pid reconcile_pid count i
679
+ home=$(setup_parent delivery-confirm-reconcile-race)
680
+ state="$home/state"
681
+ # This fixture clock is intentionally scoped to the isolated subshell.
682
+ # shellcheck disable=SC2030,SC2031
683
+ export FM_PENDING_REPLY_NOW=5900
684
+ corr=$(fm_pending_reply_create "$home" "$state" hibit "serialized delivery")
685
+ rec=$(fm_pending_reply_path "$state" "$corr")
686
+ calls="$home/mark-delivered.calls"
687
+ entered="$home/mark-delivered.entered"
688
+ release="$home/mark-delivered.release"
689
+ fm_pending_reply_mark_delivered() {
690
+ local pending_state=$1 pending_corr=$2 epoch=$3 pending_rec phase
691
+ printf '%s\n' "$BASHPID" >> "$calls"
692
+ : > "$entered"
693
+ while [ ! -e "$release" ]; do /bin/sleep 0.01; done
694
+ pending_rec=$(fm_pending_reply_path "$pending_state" "$pending_corr")
695
+ fm_pending_reply_set "$pending_rec" delivered_epoch "$epoch" || return 1
696
+ phase=$(fm_pending_reply_get "$pending_rec" phase)
697
+ [ "$phase" != delivery_unknown ] \
698
+ || fm_pending_reply_set "$pending_rec" phase awaiting_report
699
+ }
700
+ fm_pending_reply_confirm_delivery "$state" "$corr" &
701
+ # The background PID is consumed within this isolated test subshell.
702
+ # shellcheck disable=SC2031
703
+ confirm_pid=$!
704
+ for i in $(seq 1 100); do
705
+ [ -e "$entered" ] && break
706
+ /bin/sleep 0.01
707
+ done
708
+ [ -e "$entered" ] || fail "delivery confirmation did not reach its commit boundary"
709
+ fm_pending_reply_reconcile_delivery "$state" "$corr" &
710
+ # The background PID is consumed within this isolated test subshell.
711
+ # shellcheck disable=SC2031
712
+ reconcile_pid=$!
713
+ /bin/sleep 0.1
714
+ : > "$release"
715
+ wait "$confirm_pid" || fail "delivery confirmation should commit"
716
+ wait "$reconcile_pid" || fail "reconciliation should observe committed delivery"
717
+ count=$(wc -l < "$calls" | tr -d ' ')
718
+ [ "$count" = 1 ] \
719
+ || fail "confirmation and reconciliation raced through $count delivery commits"
720
+ [ "$(fm_pending_reply_get "$rec" delivered_epoch)" = 5900 ] \
721
+ || fail "serialized confirmation should retain delivered_epoch"
722
+ [ "$(phase_of "$state" "$corr")" = awaiting_report ] \
723
+ || fail "serialized confirmation should retain awaiting_report phase"
724
+ [ ! -e "$(fm_pending_reply_delivery_confirmation_path "$state" "$corr")" ] \
725
+ || fail "serialized delivery marker should be removed"
726
+ ) || fail "delivery confirmation serialization regression failed"
727
+ pass "delivery confirmation serializes with reconciliation"
728
+ }
729
+
730
+ test_unrelated_and_stale_corr_cannot_resolve() {
731
+ local home state corr other
732
+ home=$(setup_parent stale-corr)
733
+ state="$home/state"
734
+ # Reset the fixture clock after isolated subshell tests.
735
+ # shellcheck disable=SC2031
736
+ export FM_PENDING_REPLY_NOW=6000
737
+ corr=$(fm_pending_reply_create "$home" "$state" "hibit" "need answer")
738
+ fm_pending_reply_mark_delivered "$state" "$corr"
739
+ other=$(fm_pending_reply_new_id)
740
+ printf 'done [corr=%s]: wrong token\n' "$other" > "$state/hibit.status"
741
+ if fm_pending_reply_try_resolve "$state" "$corr"; then
742
+ fail "stale/wrong corr must not resolve"
743
+ fi
744
+ printf 'working: still thinking\n' >> "$state/hibit.status"
745
+ if fm_pending_reply_try_resolve "$state" "$corr"; then
746
+ fail "unrelated working line must not resolve"
747
+ fi
748
+ printf 'done: finished without corr\n' >> "$state/hibit.status"
749
+ if fm_pending_reply_try_resolve "$state" "$corr"; then
750
+ fail "status without corr must not resolve"
751
+ fi
752
+ [ "$(phase_of "$state" "$corr")" = awaiting_report ] || fail "phase must stay awaiting_report"
753
+ pass "unrelated events and stale correlation ids cannot resolve"
754
+ }
755
+
756
+ test_restart_preserves_expectation_and_parent_destination() {
757
+ local home state corr rec parent_status parent_home
758
+ home=$(setup_parent restart)
759
+ state="$home/state"
760
+ export FM_PENDING_REPLY_NOW=7000
761
+ corr=$(fm_pending_reply_create "$home" "$state" "hibit" "survive restart")
762
+ fm_pending_reply_mark_delivered "$state" "$corr"
763
+ rec=$(fm_pending_reply_path "$state" "$corr")
764
+ parent_status=$(fm_pending_reply_get "$rec" parent_status)
765
+ parent_home=$(fm_pending_reply_get "$rec" parent_home)
766
+ # Simulate process restart: re-source library and re-read the same record.
767
+ # shellcheck source=bin/fm-pending-reply-lib.sh
768
+ . "$ROOT/bin/fm-pending-reply-lib.sh"
769
+ [ -f "$rec" ] || fail "record must survive restart"
770
+ [ "$(fm_pending_reply_get "$rec" parent_status)" = "$parent_status" ] \
771
+ || fail "parent_status must be stable across restart"
772
+ [ "$(fm_pending_reply_get "$rec" parent_home)" = "$parent_home" ] \
773
+ || fail "parent_home must be stable across restart"
774
+ [ "$(phase_of "$state" "$corr")" = awaiting_report ] || fail "phase preserved"
775
+ # Compaction-safe: destination is absolute path fields, not chat memory.
776
+ case "$parent_status" in
777
+ /*.status) : ;;
778
+ *) fail "parent_status should be an absolute status path, got $parent_status" ;;
779
+ esac
780
+ pass "restart preserves expectation and exact parent destination"
781
+ }
782
+
783
+ test_wrong_home_detected_not_acknowledged() {
784
+ local home state sm_home corr rec hits
785
+ home=$(setup_parent wrong-home)
786
+ state="$home/state"
787
+ sm_home="$TMP_ROOT/sm-home-$RANDOM"
788
+ mkdir -p "$sm_home/state"
789
+ export FM_PENDING_REPLY_NOW=8000
790
+ corr=$(fm_pending_reply_create "$home" "$state" "hibit" "report to parent")
791
+ fm_pending_reply_mark_delivered "$state" "$corr"
792
+ # Historical incident shape: report written under the secondmate home.
793
+ printf 'done [corr=%s]: stranded in self-home\n' "$corr" > "$sm_home/state/hibit.status"
794
+ fm_pending_reply_detect_wrong_home "$state" "$corr" "$sm_home" \
795
+ || fail "wrong-home detect should succeed"
796
+ rec=$(fm_pending_reply_path "$state" "$corr")
797
+ hits=$(fm_pending_reply_get "$rec" wrong_home_hits)
798
+ [ "$hits" = 1 ] || fail "first wrong-home sighting should count once, got $hits"
799
+ fm_pending_reply_detect_wrong_home "$state" "$corr" "$sm_home" \
800
+ || fail "repeated wrong-home detect should succeed"
801
+ hits=$(fm_pending_reply_get "$rec" wrong_home_hits)
802
+ [ "$hits" = 1 ] || fail "unchanged wrong-home history should remain one hit, got $hits"
803
+ printf 'done [corr=%s]: second stranded report\n' "$corr" >> "$sm_home/state/hibit.status"
804
+ fm_pending_reply_detect_wrong_home "$state" "$corr" "$sm_home" \
805
+ || fail "new wrong-home sighting detect should succeed"
806
+ hits=$(fm_pending_reply_get "$rec" wrong_home_hits)
807
+ [ "$hits" = 2 ] || fail "distinct wrong-home reports should each count once, got $hits"
808
+ fm_pending_reply_detect_wrong_home "$state" "$corr" "$sm_home" \
809
+ || fail "repeated distinct wrong-home detect should succeed"
810
+ hits=$(fm_pending_reply_get "$rec" wrong_home_hits)
811
+ [ "$hits" = 2 ] || fail "repeated polling should preserve two distinct hits, got $hits"
812
+ [ "$(phase_of "$state" "$corr")" = awaiting_report ] \
813
+ || fail "wrong-home must not silently acknowledge (phase=$(phase_of "$state" "$corr"))"
814
+ if fm_pending_reply_try_resolve "$state" "$corr"; then
815
+ fail "wrong-home status must not resolve via parent path"
816
+ fi
817
+ pass "wrong-home reports are detected but do not silently acknowledge"
818
+ }
819
+
820
+ test_unmarked_captain_input_creates_no_expectation() {
821
+ local dir fb log home rc pending_count
822
+ dir="$TMP_ROOT/unmarked"; mkdir -p "$dir"
823
+ fb=$(make_stubs "$dir"); log="$dir/send.log"
824
+ home=$(setup_parent unmarked)
825
+ # Crewmate target stays unmarked and creates no pending-reply record.
826
+ fm_write_meta "$home/state/build.meta" \
827
+ "window=sess:fm-build" "worktree=$home/wt" "project=$home/p" \
828
+ "harness=echo" "kind=ship" "mode=no-mistakes" "yolo=off"
829
+ run_send "$fb" "$home" "$log" "build" "captain says hello"; rc=$?
830
+ expect_code 0 "$rc" "unmarked crewmate send should succeed"
831
+ [ "$(latest_record_body "$home" build)" = "captain says hello" ] \
832
+ || fail "crewmate steer should be recorded unmarked"$'\n'"$(latest_record_body "$home" build | od -An -c)"
833
+ pending_count=$(find "$home/state/pending-replies" -type f 2>/dev/null | wc -l | tr -d ' ')
834
+ [ "$pending_count" = 0 ] || fail "unmarked input must create no pending-reply records (got $pending_count)"
835
+ pass "direct unmarked captain input creates no expectation"
836
+ }
837
+
838
+ test_fm_send_marked_secondmate_creates_pending_and_embeds_corr() {
839
+ local dir fb log home rc got corr rec
840
+ dir="$TMP_ROOT/send-pending"; mkdir -p "$dir"
841
+ fb=$(make_stubs "$dir"); log="$dir/send.log"
842
+ home=$(setup_parent send-pending)
843
+ fm_write_secondmate_meta "$home/state/hibit.meta" "$home/sm" "sess:fm-hibit"
844
+ run_send "$fb" "$home" "$log" "hibit" "audit the build"; rc=$?
845
+ expect_code 0 "$rc" "secondmate send should succeed"
846
+ got=$(latest_record_body "$home" hibit)
847
+ case "$got" in
848
+ "$FM_FROMFIRST_MARK"corr=*) : ;;
849
+ *) fail "secondmate steer record must embed marker+corr"$'\n'"$(printf '%s' "$got" | od -An -c)" ;;
850
+ esac
851
+ corr=$(fm_pending_reply_extract_corr "$got")
852
+ [ "${#corr}" -eq 16 ] || fail "corr id should be 16 hex chars, got '$corr'"
853
+ rec=$(fm_pending_reply_path "$home/state" "$corr")
854
+ [ -f "$rec" ] || fail "pending-reply record must exist after marked send"
855
+ [ "$(fm_pending_reply_get "$rec" phase)" = awaiting_report ] \
856
+ || fail "phase should be awaiting_report after delivery"
857
+ [ -n "$(fm_pending_reply_get "$rec" delivered_epoch)" ] \
858
+ || fail "delivered_epoch must be set after successful send"
859
+ [ "$(fm_pending_reply_get "$rec" task_id)" = hibit ] \
860
+ || fail "task_id must match secondmate id"
861
+ pass "fm-send marked secondmate path creates pending and embeds corr"
862
+ }
863
+
864
+ test_document_pointer_resolves() {
865
+ local home state corr
866
+ home=$(setup_parent doc-pointer)
867
+ state="$home/state"
868
+ export FM_PENDING_REPLY_NOW=9000
869
+ corr=$(fm_pending_reply_create "$home" "$state" "hibit" "deep audit")
870
+ fm_pending_reply_mark_delivered "$state" "$corr"
871
+ printf 'done [corr=%s]: see data/hibit/report.md\n' "$corr" > "$state/hibit.status"
872
+ fm_pending_reply_try_resolve "$state" "$corr" || fail "document pointer status should resolve"
873
+ [ "$(fm_pending_reply_get "$(fm_pending_reply_path "$state" "$corr")" resolved_via)" = document ] \
874
+ || fail "resolved_via should be document"
875
+ pass "status-pointed document resolves the expectation"
876
+ }
877
+
878
+ test_helper_report_resolves() {
879
+ local home state corr
880
+ home=$(setup_parent helper)
881
+ state="$home/state"
882
+ export FM_PENDING_REPLY_NOW=9100
883
+ corr=$(fm_pending_reply_create "$home" "$state" "hibit" "quick answer")
884
+ fm_pending_reply_mark_delivered "$state" "$corr"
885
+ "$REPORT" "$state/hibit.status" "done" "$corr" "all good" \
886
+ || fail "helper report failed"
887
+ fm_pending_reply_try_resolve "$state" "$corr" || fail "helper report should resolve"
888
+ [ "$(fm_pending_reply_get "$(fm_pending_reply_path "$state" "$corr")" resolved_via)" = helper ] \
889
+ || fail "resolved_via should be helper"
890
+ pass "optional helper report resolves without being required for correctness"
891
+ }
892
+
893
+ test_busy_idle_observation_via_backend_abstraction() {
894
+ local home state corr
895
+ home=$(setup_parent busy-idle)
896
+ state="$home/state"
897
+ export FM_PENDING_REPLY_NOW=9200
898
+ corr=$(fm_pending_reply_create "$home" "$state" "hibit" "backend turn")
899
+ fm_pending_reply_mark_delivered "$state" "$corr"
900
+ # Simulates Pi/Claude secondmate busy_state from fm_backend_busy_state without
901
+ # reading conversation text (herdr native idle/busy or tmux unknown fallback).
902
+ fm_pending_reply_observe_busy "$state" "$corr" unknown
903
+ [ -z "$(fm_pending_reply_get "$(fm_pending_reply_path "$state" "$corr")" request_turn_completed_epoch)" ] \
904
+ || fail "unknown busy_state must not prove turn completion"
905
+ fm_pending_reply_observe_busy "$state" "$corr" busy
906
+ fm_pending_reply_observe_busy "$state" "$corr" idle
907
+ [ -n "$(fm_pending_reply_get "$(fm_pending_reply_path "$state" "$corr")" request_turn_completed_epoch)" ] \
908
+ || fail "busy->idle must prove turn completion"
909
+ pass "backend busy/idle observation covers Pi/Claude paths without conversation scrape"
910
+ }
911
+
912
+ test_unknown_backend_state_uses_capture_fallback() {
913
+ local backend
914
+ for backend in tmux zellij; do
915
+ (
916
+ local home state corr rec sm_home
917
+ home=$(setup_parent "fallback-$backend")
918
+ state="$home/state"
919
+ sm_home="$home/sm"
920
+ mkdir -p "$sm_home/state"
921
+ export FM_PENDING_REPLY_GRACE_SECS=10
922
+ # These fixture overrides are intentionally scoped to the isolated subshell.
923
+ # shellcheck disable=SC2030,SC2031
924
+ export FM_PENDING_REPLY_NOW=10000
925
+ corr=$(fm_pending_reply_create "$home" "$state" "hibit" "$backend fallback")
926
+ fm_pending_reply_mark_delivered "$state" "$corr"
927
+ fm_write_secondmate_meta "$state/hibit.meta" "$sm_home" "session:fm-hibit" alpha pi
928
+ [ "$backend" = tmux ] || printf 'backend=%s\n' "$backend" >> "$state/hibit.meta"
929
+ fm_backend_busy_state() { printf 'unknown'; }
930
+ fm_backend_capture() { printf '%s' "$FM_PENDING_TEST_CAPTURE"; }
931
+ # Invoked indirectly through FM_PENDING_REPLY_SEND_HOOK.
932
+ # shellcheck disable=SC2329
933
+ recovery_hook() { :; }
934
+ # This hook override is intentionally scoped to the isolated subshell.
935
+ # shellcheck disable=SC2030,SC2031
936
+ export FM_PENDING_REPLY_SEND_HOOK=recovery_hook
937
+ export FM_PENDING_TEST_CAPTURE='idle footer'
938
+ fm_pending_reply_tick "$state"
939
+ rec=$(fm_pending_reply_path "$state" "$corr")
940
+ [ -z "$(fm_pending_reply_get "$rec" request_turn_completed_epoch)" ] \
941
+ || fail "$backend fallback must not accept stale idle before grace"
942
+ # Continue advancing the subshell-local fixture clock.
943
+ # shellcheck disable=SC2030,SC2031
944
+ export FM_PENDING_REPLY_NOW=10010
945
+ fm_pending_reply_tick "$state"
946
+ [ "$(phase_of "$state" "$corr")" = recovery_sent ] \
947
+ || fail "$backend fallback idle should trigger recovery after grace"
948
+ export FM_PENDING_REPLY_NOW=10011
949
+ export FM_PENDING_TEST_CAPTURE='Working...'
950
+ fm_pending_reply_tick "$state"
951
+ export FM_PENDING_REPLY_NOW=10012
952
+ export FM_PENDING_TEST_CAPTURE='idle footer'
953
+ fm_pending_reply_tick "$state"
954
+ [ "$(phase_of "$state" "$corr")" = escalated ] \
955
+ || fail "$backend capture busy-to-idle should complete recovery turn"
956
+ ) || fail "$backend unknown-state capture fallback failed"
957
+ done
958
+ pass "tmux and zellij unknown states use bounded capture fallback"
959
+ }
960
+
961
+ test_kimi_capture_fallback_uses_recorded_harness() (
962
+ local home state corr rec sm_home
963
+ home=$(setup_parent kimi-fallback)
964
+ state="$home/state"
965
+ sm_home="$home/sm"
966
+ mkdir -p "$sm_home/state"
967
+ # This fixture clock is intentionally scoped to the isolated subshell.
968
+ # shellcheck disable=SC2030,SC2031
969
+ export FM_PENDING_REPLY_NOW=10020
970
+ corr=$(fm_pending_reply_create "$home" "$state" hibit "kimi fallback")
971
+ fm_pending_reply_mark_delivered "$state" "$corr"
972
+ fm_write_secondmate_meta "$state/hibit.meta" "$sm_home" "session:fm-hibit" alpha kimi
973
+ fm_backend_busy_state() { printf 'unknown'; }
974
+ fm_backend_capture() { printf '%s' "$FM_PENDING_KIMI_CAPTURE"; }
975
+ export FM_PENDING_KIMI_CAPTURE=' 🌑 · Tip: ask Kimi to schedule tasks, e.g. "remind me at 5pm"'
976
+
977
+ [ "$(fm_pending_reply_backend_observation tmux session:fm-hibit fm-hibit codex)" = fallback-idle ] \
978
+ || fail "Kimi spinner leaked into another harness"
979
+ export FM_PENDING_KIMI_CAPTURE='Ctrl+c:cancel'
980
+ [ "$(fm_pending_reply_backend_observation tmux session:fm-hibit fm-hibit kimi)" = fallback-idle ] \
981
+ || fail "Grok's exact busy token leaked into Kimi pending-reply observation"
982
+ export FM_PENDING_KIMI_CAPTURE=' 🌑 · Tip: ask Kimi to schedule tasks, e.g. "remind me at 5pm"'
983
+ fm_pending_reply_tick "$state"
984
+ rec=$(fm_pending_reply_path "$state" "$corr")
985
+ [ "$(fm_pending_reply_get "$rec" turn_seen_busy)" = 1 ] \
986
+ || fail "recorded Kimi spinner was not observed as busy"
987
+ [ "$(phase_of "$state" "$corr")" = awaiting_report ] \
988
+ || fail "working Kimi secondmate entered recovery"
989
+ pass "pending replies scope Kimi capture fallback by recorded harness"
990
+ )
991
+
992
+ test_tick_skips_terminal_and_reuses_target_observation() {
993
+ (
994
+ local home state open1 open2 resolved escalated rec probe_log probes scan_log scans snapshot
995
+ home=$(setup_parent observation-cache)
996
+ state="$home/state"
997
+ probe_log="$home/backend-probes.log"
998
+ scan_log="$home/status-scans.log"
999
+ : > "$probe_log"
1000
+ : > "$scan_log"
1001
+ # This fixture clock is intentionally scoped to the isolated subshell.
1002
+ # shellcheck disable=SC2030,SC2031
1003
+ export FM_PENDING_REPLY_NOW=10100
1004
+ open1=$(fm_pending_reply_create "$home" "$state" hibit "first open request")
1005
+ open2=$(fm_pending_reply_create "$home" "$state" hibit "second open request")
1006
+ fm_pending_reply_mark_delivered "$state" "$open1"
1007
+ fm_pending_reply_mark_delivered "$state" "$open2"
1008
+ resolved=$(fm_pending_reply_create "$home" "$state" resolved "resolved request")
1009
+ fm_pending_reply_mark_delivered "$state" "$resolved"
1010
+ printf 'done [corr=%s]: complete\n' "$resolved" > "$state/resolved.status"
1011
+ fm_pending_reply_try_resolve "$state" "$resolved" || fail "resolved fixture should resolve"
1012
+ escalated=$(fm_pending_reply_create "$home" "$state" escalated "escalated request")
1013
+ fm_pending_reply_mark_delivered "$state" "$escalated"
1014
+ rec=$(fm_pending_reply_path "$state" "$escalated")
1015
+ fm_pending_reply_set "$rec" phase escalated || fail "escalated fixture should transition"
1016
+ mkdir -p "$home/escalated/state"
1017
+ printf 'done [corr=%s]: wrong home\n' "$escalated" > "$home/escalated/state/escalated.status"
1018
+ fm_write_secondmate_meta "$state/hibit.meta" "$home/hibit" "sess:fm-hibit"
1019
+ fm_write_secondmate_meta "$state/resolved.meta" "$home/resolved" "sess:fm-resolved"
1020
+ fm_write_secondmate_meta "$state/escalated.meta" "$home/escalated" "sess:fm-escalated"
1021
+ # Runtime overrides called indirectly by the pending-reply tick.
1022
+ # shellcheck disable=SC2329
1023
+ fm_backend_busy_state() {
1024
+ printf '%s\t%s\n' "$1" "$2" >> "$probe_log"
1025
+ printf 'busy'
1026
+ }
1027
+ # shellcheck disable=SC2329
1028
+ fm_backend_capture() { fail "native busy observations should not capture"; }
1029
+ # shellcheck disable=SC2329
1030
+ fm_pending_reply_find_resolve_line() {
1031
+ local status_file=$1 corr=$2 line
1032
+ printf '%s\t%s\n' "$status_file" "$corr" >> "$scan_log"
1033
+ [ -f "$status_file" ] || return 0
1034
+ while IFS= read -r line || [ -n "$line" ]; do
1035
+ fm_pending_reply_line_resolves "$line" "$corr" || continue
1036
+ printf '%s' "$line"
1037
+ return 0
1038
+ done < "$status_file"
1039
+ return 0
1040
+ }
1041
+ fm_pending_reply_tick "$state"
1042
+ probes=$(wc -l < "$probe_log" | tr -d ' ')
1043
+ [ "$probes" = 1 ] || fail "two open records for one target should use one probe, got $probes"
1044
+ rec=$(fm_pending_reply_path "$state" "$open1")
1045
+ [ "$(fm_pending_reply_get "$rec" turn_seen_busy)" = 1 ] \
1046
+ || fail "cached observation should update the first open record"
1047
+ rec=$(fm_pending_reply_path "$state" "$open2")
1048
+ [ "$(fm_pending_reply_get "$rec" turn_seen_busy)" = 1 ] \
1049
+ || fail "cached observation should update the second open record"
1050
+ rec=$(fm_pending_reply_path "$state" "$escalated")
1051
+ snapshot=$(fm_pending_reply_get "$rec" wrong_home_scan_signature)
1052
+ [ -n "$snapshot" ] || fail "wrong-home scan should persist its file-set signature"
1053
+ fm_pending_reply_tick "$state"
1054
+ scans=$(wc -l < "$scan_log" | tr -d ' ')
1055
+ [ "$scans" = 3 ] \
1056
+ || fail "unchanged records should scan two open and one escalated status only once, got $scans"
1057
+ [ "$(fm_pending_reply_get "$rec" wrong_home_scan_signature)" = "$snapshot" ] \
1058
+ || fail "unchanged wrong-home logs should retain their scan signature"
1059
+ ) || fail "terminal-skip and observation-cache regression failed"
1060
+ pass "tick skips terminal records and reuses target observations"
1061
+ }
1062
+
1063
+ test_correlations_reuse_only_for_matching_open_task() {
1064
+ local dir fb log home state got corr1 corr2 corr3 rec
1065
+ dir="$TMP_ROOT/corr-reuse"; mkdir -p "$dir"
1066
+ fb=$(make_stubs "$dir"); log="$dir/send.log"
1067
+ home=$(setup_parent corr-reuse)
1068
+ state="$home/state"
1069
+ fm_write_secondmate_meta "$state/domain.meta" "$home/domain" "sess:fm-domain"
1070
+ fm_write_secondmate_meta "$state/other.meta" "$home/other" "sess:fm-other"
1071
+ run_send "$fb" "$home" "$log" domain "first request" || fail "first marked send failed"
1072
+ got=$(latest_record_body "$home" domain)
1073
+ corr1=$(fm_pending_reply_extract_corr "$got")
1074
+ export FM_PENDING_REPLY_EXISTING_CORR=$corr1
1075
+ if run_send "$fb" "$home" "$log" other "forwarded request"; then
1076
+ fail "an explicit cross-task correlation must be refused"
1077
+ fi
1078
+ unset FM_PENDING_REPLY_EXISTING_CORR
1079
+ if latest_record_body "$home" other >/dev/null 2>&1; then
1080
+ fail "a refused cross-task correlation must not enqueue a steer"
1081
+ fi
1082
+ run_send "$fb" "$home" "$log" other "forwarded request" \
1083
+ || fail "fresh cross-task send failed"
1084
+ corr2=$(fm_pending_reply_extract_corr "$(latest_record_body "$home" other)")
1085
+ [ -n "$corr2" ] && [ "$corr2" != "$corr1" ] \
1086
+ || fail "cross-task send must receive a new correlation"
1087
+ rec=$(fm_pending_reply_path "$state" "$corr2")
1088
+ [ "$(fm_pending_reply_get "$rec" task_id)" = other ] \
1089
+ || fail "cross-task expectation must belong to the new target"
1090
+ printf 'done [corr=%s]: complete\n' "$corr1" > "$state/domain.status"
1091
+ fm_pending_reply_try_resolve "$state" "$corr1" || fail "first expectation should resolve"
1092
+ run_send "$fb" "$home" "$log" domain "${FM_FROMFIRST_MARK}corr=${corr1} follow-up" \
1093
+ || fail "resolved-correlation follow-up failed"
1094
+ corr3=$(fm_pending_reply_extract_corr "$(latest_record_body "$home" domain)")
1095
+ [ -n "$corr3" ] && [ "$corr3" != "$corr1" ] \
1096
+ || fail "resolved correlation must not guard a new send"
1097
+ rec=$(fm_pending_reply_path "$state" "$corr3")
1098
+ [ "$(fm_pending_reply_get "$rec" task_id)" = domain ] \
1099
+ || fail "replacement expectation must belong to the current target"
1100
+ pass "correlations are reused only for matching open task records"
1101
+ }
1102
+
1103
+ test_tick_end_to_end_missed_then_escalate() {
1104
+ local home state corr hook_log sm_home
1105
+ home=$(setup_parent tick-e2e)
1106
+ state="$home/state"
1107
+ sm_home="$home/sm"
1108
+ mkdir -p "$sm_home/state"
1109
+ hook_log="$TMP_ROOT/tick-hook.log"
1110
+ : > "$hook_log"
1111
+ recovery_hook() { printf 'recovered\n' >> "$hook_log"; }
1112
+ export -f recovery_hook
1113
+ # Reset hook and clock fixtures after isolated subshell tests.
1114
+ # shellcheck disable=SC2031
1115
+ export FM_PENDING_REPLY_SEND_HOOK='recovery_hook'
1116
+ # shellcheck disable=SC2031
1117
+ export FM_PENDING_REPLY_NOW=9300
1118
+
1119
+ corr=$(fm_pending_reply_create "$home" "$state" "hibit" "e2e miss")
1120
+ fm_pending_reply_mark_delivered "$state" "$corr"
1121
+ fm_write_secondmate_meta "$state/hibit.meta" "$sm_home" "sess:fm-hibit"
1122
+ # Override backend busy via direct tick_one (backend may be unknown in hermetic home).
1123
+ fm_pending_reply_tick_one "$state" "$corr" busy "$sm_home"
1124
+ fm_pending_reply_tick_one "$state" "$corr" idle "$sm_home"
1125
+ [ "$(phase_of "$state" "$corr")" = recovery_sent ] \
1126
+ || fail "tick should send recovery after idle+grace, got $(phase_of "$state" "$corr")"
1127
+ [ -s "$hook_log" ] || fail "recovery should have been sent via tick"
1128
+ # Recovery turn completes empty.
1129
+ fm_pending_reply_tick_one "$state" "$corr" busy "$sm_home"
1130
+ fm_pending_reply_tick_one "$state" "$corr" idle "$sm_home"
1131
+ [ "$(phase_of "$state" "$corr")" = escalated ] \
1132
+ || fail "tick should escalate after second miss, got $(phase_of "$state" "$corr")"
1133
+ # Expired age must not erase the unresolved record.
1134
+ export FM_PENDING_REPLY_NOW=999999
1135
+ fm_pending_reply_tick_one "$state" "$corr" idle "$sm_home"
1136
+ [ -f "$(fm_pending_reply_path "$state" "$corr")" ] \
1137
+ || fail "expiration must never silently erase an unresolved reply"
1138
+ [ "$(phase_of "$state" "$corr")" = escalated ] || fail "must stay escalated"
1139
+ pass "tick end-to-end: miss -> one recovery -> escalate -> durable"
1140
+ }
1141
+
1142
+ test_remote_repost_waits_for_the_reply_channel() {
1143
+ local home state corr hook_log rec lines
1144
+ home=$(setup_parent remote-repost)
1145
+ state="$home/state"
1146
+ hook_log="$TMP_ROOT/remote-repost.log"
1147
+ : > "$hook_log"
1148
+ export FM_PENDING_REPLY_NOW=5000
1149
+ # Invoked indirectly through FM_PENDING_REPLY_SEND_HOOK.
1150
+ # shellcheck disable=SC2329
1151
+ remote_repost_hook() {
1152
+ printf '%s\t%s\n' "$1" "$2" >> "$hook_log"
1153
+ }
1154
+ export -f remote_repost_hook
1155
+ export FM_PENDING_REPLY_SEND_HOOK=remote_repost_hook
1156
+
1157
+ fm_write_meta "$state/ios.meta" \
1158
+ "window=fm-remote:w1:p1" "harness=claude" "kind=secondmate" "mode=secondmate" \
1159
+ "remote_host=remote-mac" "remote_root=/remote/root" "remote_backend=herdr"
1160
+ corr=$(fm_pending_reply_create "$home" "$state" "ios" "status of the iOS build")
1161
+ fm_pending_reply_mark_delivered "$state" "$corr"
1162
+ fm_pending_reply_observe_busy "$state" "$corr" busy
1163
+ fm_pending_reply_observe_busy "$state" "$corr" idle
1164
+ rec=$(fm_pending_reply_path "$state" "$corr")
1165
+
1166
+ # The mate's turn ended, but nothing proves the parent has read the remote
1167
+ # reply log since: a repost here would nag for a reply already written there.
1168
+ if fm_pending_reply_send_recovery "$state" "$corr" 2>/dev/null; then
1169
+ fail "a remote repost must not fire before the reply channel is known caught up"
1170
+ fi
1171
+ [ ! -s "$hook_log" ] || fail "no repost may be sent while the reply channel is behind"
1172
+ [ "$(phase_of "$state" "$corr")" = awaiting_report ] \
1173
+ || fail "the expectation must stay armed while the reply channel is behind"
1174
+
1175
+ # A watermark from BEFORE the turn ended is still not evidence.
1176
+ fm_pending_reply_note_remote_channel_caught_up "$state" ios 4000
1177
+ if fm_pending_reply_send_recovery "$state" "$corr" 2>/dev/null; then
1178
+ fail "a stale reply-channel watermark must not license a repost"
1179
+ fi
1180
+ [ ! -s "$hook_log" ] || fail "a stale watermark must not release a repost"
1181
+
1182
+ # Read through the end of the remote log after the turn: the report really is
1183
+ # missing, so the one recovery repost fires.
1184
+ fm_pending_reply_note_remote_channel_caught_up "$state" ios \
1185
+ "$(fm_pending_reply_get "$rec" request_turn_completed_epoch)"
1186
+ fm_pending_reply_send_recovery "$state" "$corr" \
1187
+ || fail "a genuinely missed remote report must still trigger its recovery repost"
1188
+ [ "$(phase_of "$state" "$corr")" = recovery_sent ] \
1189
+ || fail "phase should be recovery_sent, got $(phase_of "$state" "$corr")"
1190
+ lines=$(wc -l < "$hook_log" | tr -d ' ')
1191
+ [ "$lines" = 1 ] || fail "expected exactly one repost, got $lines"
1192
+ case "$(cat "$hook_log")" in
1193
+ *REPOST\ REQUIRED*) : ;;
1194
+ *) fail "the recovery message must ask for a repost"$'\n'"$(cat "$hook_log")" ;;
1195
+ esac
1196
+ unset FM_PENDING_REPLY_SEND_HOOK
1197
+ pass "a remote repost waits for the reply channel and still fires on a real miss"
1198
+ }
1199
+
1200
+ test_mirrored_remote_reply_never_triggers_a_repost() {
1201
+ local home state corr hook_log
1202
+ home=$(setup_parent remote-mirrored-reply)
1203
+ state="$home/state"
1204
+ hook_log="$TMP_ROOT/remote-mirrored-reply.log"
1205
+ : > "$hook_log"
1206
+ export FM_PENDING_REPLY_NOW=6000
1207
+ # Invoked indirectly through FM_PENDING_REPLY_SEND_HOOK.
1208
+ # shellcheck disable=SC2329
1209
+ mirrored_reply_hook() {
1210
+ printf '%s\t%s\n' "$1" "$2" >> "$hook_log"
1211
+ }
1212
+ export -f mirrored_reply_hook
1213
+ export FM_PENDING_REPLY_SEND_HOOK=mirrored_reply_hook
1214
+
1215
+ fm_write_meta "$state/ios.meta" \
1216
+ "window=fm-remote:w1:p1" "harness=claude" "kind=secondmate" "mode=secondmate" \
1217
+ "remote_host=remote-mac" "remote_root=/remote/root" "remote_backend=herdr"
1218
+ corr=$(fm_pending_reply_create "$home" "$state" "ios" "did the build go green")
1219
+ fm_pending_reply_mark_delivered "$state" "$corr"
1220
+ fm_pending_reply_mark_turn_completed "$state" "$corr" request
1221
+ # The mirror caught up AND carried the mate's correlated answer.
1222
+ printf 'done [corr=%s]: build is green\n' "$corr" > "$state/ios.status"
1223
+ fm_pending_reply_note_remote_channel_caught_up "$state" ios 6000
1224
+
1225
+ fm_pending_reply_tick_one "$state" "$corr" idle || fail "tick should succeed"
1226
+ [ "$(phase_of "$state" "$corr")" = resolved ] \
1227
+ || fail "a mirrored correlated reply must resolve, got $(phase_of "$state" "$corr")"
1228
+ [ ! -s "$hook_log" ] || fail "a correlated remote reply must never trigger a repost"
1229
+ unset FM_PENDING_REPLY_SEND_HOOK
1230
+ pass "a mirrored correlated remote reply resolves without any repost"
1231
+ }
1232
+
1233
+ test_failed_send_discards_undelivered_expectation() {
1234
+ local home state corr
1235
+ home=$(setup_parent discard)
1236
+ state="$home/state"
1237
+ export FM_PENDING_REPLY_NOW=9400
1238
+ corr=$(fm_pending_reply_create "$home" "$state" "hibit" "never lands")
1239
+ # Not delivered: discard is allowed.
1240
+ fm_pending_reply_discard_undelivered "$state" "$corr" || fail "discard undelivered failed"
1241
+ [ ! -f "$(fm_pending_reply_path "$state" "$corr")" ] \
1242
+ || fail "undelivered record should be removed"
1243
+ # Delivered records must not be discarded by this path.
1244
+ corr=$(fm_pending_reply_create "$home" "$state" "hibit" "landed")
1245
+ fm_pending_reply_mark_delivered "$state" "$corr"
1246
+ if fm_pending_reply_discard_undelivered "$state" "$corr" 2>/dev/null; then
1247
+ fail "delivered record must not be discarded"
1248
+ fi
1249
+ [ -f "$(fm_pending_reply_path "$state" "$corr")" ] || fail "delivered record must remain"
1250
+ pass "failed transport discards undelivered expectation only"
1251
+ }
1252
+
1253
+ # --- run --------------------------------------------------------------------
1254
+
1255
+ test_normal_correlated_reply_resolves_once
1256
+ test_completed_turn_no_report_triggers_one_recovery
1257
+ test_recovery_attempt_is_never_reinjected
1258
+ test_recovery_reply_resolves_original
1259
+ test_second_missed_turn_escalates_once_and_stays_durable
1260
+ test_escalation_wakes_and_its_close_stays_quiet
1261
+ test_escalation_publication_failure_retries
1262
+ test_legacy_escalation_closes_default_decision
1263
+ test_legacy_escalation_does_not_close_taken_default_decision
1264
+ test_foreign_blocker_is_not_selected_as_escalation
1265
+ test_concurrent_resolution_closes_escalation_once
1266
+ test_concurrent_escalation_yields_to_late_reply
1267
+ test_transport_success_is_not_reply_success
1268
+ test_undelivered_records_are_scan_immutable
1269
+ test_delivery_confirmation_fallback_reconciles
1270
+ test_delivery_confirmation_serializes_with_reconciliation
1271
+ test_unrelated_and_stale_corr_cannot_resolve
1272
+ test_restart_preserves_expectation_and_parent_destination
1273
+ test_wrong_home_detected_not_acknowledged
1274
+ test_unmarked_captain_input_creates_no_expectation
1275
+ test_fm_send_marked_secondmate_creates_pending_and_embeds_corr
1276
+ test_document_pointer_resolves
1277
+ test_helper_report_resolves
1278
+ test_busy_idle_observation_via_backend_abstraction
1279
+ test_unknown_backend_state_uses_capture_fallback
1280
+ test_kimi_capture_fallback_uses_recorded_harness
1281
+ test_tick_skips_terminal_and_reuses_target_observation
1282
+ test_correlations_reuse_only_for_matching_open_task
1283
+ test_tick_end_to_end_missed_then_escalate
1284
+ test_failed_send_discards_undelivered_expectation
1285
+ test_remote_repost_waits_for_the_reply_channel
1286
+ test_mirrored_remote_reply_never_triggers_a_repost
1287
+
1288
+ printf 'ok - all pending-reply tests passed\n'