@zerwiz/ymir 0.1.3 → 0.1.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (850) hide show
  1. package/.agents/README.md +291 -0
  2. package/.agents/agents/bragi-marketer.md +111 -0
  3. package/.agents/agents/brokk.md +352 -0
  4. package/.agents/agents/forseti-reviewer.md +93 -0
  5. package/.agents/agents/frigg-consent.md +77 -0
  6. package/.agents/agents/groa-update.md +77 -0
  7. package/.agents/agents/hnoss-designer.md +69 -0
  8. package/.agents/agents/huginn-researcher.md +99 -0
  9. package/.agents/agents/jord-projects.md +78 -0
  10. package/.agents/agents/kvasir-scout.md +91 -0
  11. package/.agents/agents/mimir-planner.md +92 -0
  12. package/.agents/agents/muninn-stow.md +77 -0
  13. package/.agents/agents/saga-bearings.md +78 -0
  14. package/.agents/agents/sindri-developer.md +114 -0
  15. package/.agents/agents/snotra-documenter.md +105 -0
  16. package/.agents/agents/syn-recovery.md +78 -0
  17. package/.agents/agents/tyr-check.md +78 -0
  18. package/.agents/agents/urdh-hold.md +77 -0
  19. package/.agents/agents/volundr-smith.md +80 -0
  20. package/.agents/agents/vor-diagnostics.md +77 -0
  21. package/.agents/assets/agents/naming.md +50 -0
  22. package/.agents/assets/agents/registry.md +93 -0
  23. package/.agents/assets/agents/runtime.md +36 -0
  24. package/.agents/assets/agents/toon-tasks-cli.md +42 -0
  25. package/.agents/assets/templates/company_entity.template.md +37 -0
  26. package/.agents/backend/backends/cmux.sh +649 -0
  27. package/.agents/backend/backends/herdr-eventwait.py +157 -0
  28. package/.agents/backend/backends/herdr-workspace-move.py +114 -0
  29. package/.agents/backend/backends/herdr.sh +3386 -0
  30. package/.agents/backend/backends/orca.sh +290 -0
  31. package/.agents/backend/backends/tmux.sh +357 -0
  32. package/.agents/backend/backends/zellij.sh +700 -0
  33. package/.agents/backend/fm-afk-launch.sh +654 -0
  34. package/.agents/backend/fm-afk-return.sh +241 -0
  35. package/.agents/backend/fm-afk-start.sh +172 -0
  36. package/.agents/backend/fm-arm-command-policy.mjs +960 -0
  37. package/.agents/backend/fm-arm-pretool-check.sh +200 -0
  38. package/.agents/backend/fm-backend-hometag-lib.sh +52 -0
  39. package/.agents/backend/fm-backend.sh +981 -0
  40. package/.agents/backend/fm-backlog-handoff.sh +909 -0
  41. package/.agents/backend/fm-backlog-receive.sh +187 -0
  42. package/.agents/backend/fm-backlog-transition-lib.sh +779 -0
  43. package/.agents/backend/fm-bearings-board.sh +204 -0
  44. package/.agents/backend/fm-bearings-snapshot.sh +546 -0
  45. package/.agents/backend/fm-bootstrap.sh +1568 -0
  46. package/.agents/backend/fm-branch-outcome.sh +203 -0
  47. package/.agents/backend/fm-branch-prompt.sh +101 -0
  48. package/.agents/backend/fm-brief.sh +452 -0
  49. package/.agents/backend/fm-busy-event.sh +234 -0
  50. package/.agents/backend/fm-busy-lib.sh +986 -0
  51. package/.agents/backend/fm-captain-hold.sh +1001 -0
  52. package/.agents/backend/fm-cd-command-policy.mjs +153 -0
  53. package/.agents/backend/fm-cd-pretool-check.sh +190 -0
  54. package/.agents/backend/fm-check-lib.sh +74 -0
  55. package/.agents/backend/fm-check-register.sh +42 -0
  56. package/.agents/backend/fm-check-unregister.sh +52 -0
  57. package/.agents/backend/fm-classify-lib.sh +1770 -0
  58. package/.agents/backend/fm-claude-stop-autoarm.sh +334 -0
  59. package/.agents/backend/fm-composer-lib.sh +1416 -0
  60. package/.agents/backend/fm-config-inherit-lib.sh +1202 -0
  61. package/.agents/backend/fm-config-push.sh +254 -0
  62. package/.agents/backend/fm-control-lib.sh +251 -0
  63. package/.agents/backend/fm-control.sh +878 -0
  64. package/.agents/backend/fm-crew-state.sh +625 -0
  65. package/.agents/backend/fm-cursor-lib.sh +243 -0
  66. package/.agents/backend/fm-decision-hold.sh +232 -0
  67. package/.agents/backend/fm-doc-audience-check.sh +269 -0
  68. package/.agents/backend/fm-dod-lib.sh +67 -0
  69. package/.agents/backend/fm-ensure-agents-md.sh +253 -0
  70. package/.agents/backend/fm-extension-launch-barrier.mjs +129 -0
  71. package/.agents/backend/fm-extension.mjs +2577 -0
  72. package/.agents/backend/fm-extension.sh +16 -0
  73. package/.agents/backend/fm-ff-lib.sh +420 -0
  74. package/.agents/backend/fm-fleet-snapshot.sh +1463 -0
  75. package/.agents/backend/fm-fleet-sync.sh +458 -0
  76. package/.agents/backend/fm-fleet-view.sh +96 -0
  77. package/.agents/backend/fm-gate-refuse-lib.sh +102 -0
  78. package/.agents/backend/fm-guard.sh +242 -0
  79. package/.agents/backend/fm-harness.sh +195 -0
  80. package/.agents/backend/fm-herdr-ci-cleanup.sh +115 -0
  81. package/.agents/backend/fm-herdr-lab.sh +346 -0
  82. package/.agents/backend/fm-herdr-session-cleanup.sh +337 -0
  83. package/.agents/backend/fm-home-seed.sh +984 -0
  84. package/.agents/backend/fm-home-summary-refresh.sh +255 -0
  85. package/.agents/backend/fm-hook-host-lib.sh +36 -0
  86. package/.agents/backend/fm-inactive-reconcile.sh +531 -0
  87. package/.agents/backend/fm-inbox.sh +399 -0
  88. package/.agents/backend/fm-install-actionlint.sh +84 -0
  89. package/.agents/backend/fm-install-herdr.sh +96 -0
  90. package/.agents/backend/fm-install-shellcheck.sh +84 -0
  91. package/.agents/backend/fm-install-treehouse.sh +95 -0
  92. package/.agents/backend/fm-kimi-turnend-hook.sh +276 -0
  93. package/.agents/backend/fm-lease-lib.sh +218 -0
  94. package/.agents/backend/fm-lease.sh +189 -0
  95. package/.agents/backend/fm-line-cap-lib.sh +51 -0
  96. package/.agents/backend/fm-lint-workflows.sh +137 -0
  97. package/.agents/backend/fm-lint.sh +591 -0
  98. package/.agents/backend/fm-lock-lib.sh +104 -0
  99. package/.agents/backend/fm-lock.sh +107 -0
  100. package/.agents/backend/fm-marker-lib.sh +12 -0
  101. package/.agents/backend/fm-merge-local.sh +74 -0
  102. package/.agents/backend/fm-merge-outcome-lib.sh +138 -0
  103. package/.agents/backend/fm-nm-run-lib.sh +124 -0
  104. package/.agents/backend/fm-on.sh +125 -0
  105. package/.agents/backend/fm-operational-input.sh +253 -0
  106. package/.agents/backend/fm-peek.sh +46 -0
  107. package/.agents/backend/fm-pending-reply-lib.sh +1409 -0
  108. package/.agents/backend/fm-pr-check.sh +135 -0
  109. package/.agents/backend/fm-pr-lib.sh +1016 -0
  110. package/.agents/backend/fm-pr-merge.sh +707 -0
  111. package/.agents/backend/fm-pr-poll.sh +110 -0
  112. package/.agents/backend/fm-primary-scope-lib.sh +33 -0
  113. package/.agents/backend/fm-procevent-extension-capture.pl +259 -0
  114. package/.agents/backend/fm-procevent-lavish.sh +628 -0
  115. package/.agents/backend/fm-procevent-lib.sh +892 -0
  116. package/.agents/backend/fm-procevent-quota.sh +290 -0
  117. package/.agents/backend/fm-procevent-remote-reply.sh +577 -0
  118. package/.agents/backend/fm-procevent-when.sh +504 -0
  119. package/.agents/backend/fm-procevent.sh +1466 -0
  120. package/.agents/backend/fm-project-mode.sh +92 -0
  121. package/.agents/backend/fm-project-origin-lib.sh +180 -0
  122. package/.agents/backend/fm-promote.sh +282 -0
  123. package/.agents/backend/fm-public-followup-emit.sh +260 -0
  124. package/.agents/backend/fm-public-followup-lib.sh +398 -0
  125. package/.agents/backend/fm-public-followup.sh +1330 -0
  126. package/.agents/backend/fm-push-transition-lib.sh +168 -0
  127. package/.agents/backend/fm-quota-axi-lib.sh +93 -0
  128. package/.agents/backend/fm-quota-choose.sh +384 -0
  129. package/.agents/backend/fm-remote-delta-read.sh +189 -0
  130. package/.agents/backend/fm-remote-doctor.sh +800 -0
  131. package/.agents/backend/fm-remote-entrypoint.sh +193 -0
  132. package/.agents/backend/fm-remote-file.sh +234 -0
  133. package/.agents/backend/fm-remote-home-provision.sh +259 -0
  134. package/.agents/backend/fm-remote-home-seed.sh +259 -0
  135. package/.agents/backend/fm-remote-inherit-push.sh +90 -0
  136. package/.agents/backend/fm-remote-inherit.sh +179 -0
  137. package/.agents/backend/fm-remote-job-lib.sh +1241 -0
  138. package/.agents/backend/fm-remote-job-reap-orphans.sh +137 -0
  139. package/.agents/backend/fm-remote-job-worker.sh +1124 -0
  140. package/.agents/backend/fm-remote-readiness-lib.sh +44 -0
  141. package/.agents/backend/fm-remote-secondmate-control.sh +340 -0
  142. package/.agents/backend/fm-review-diff.sh +158 -0
  143. package/.agents/backend/fm-secondmate-charter-lib.sh +44 -0
  144. package/.agents/backend/fm-secondmate-nudge-lib.sh +68 -0
  145. package/.agents/backend/fm-secondmate-parent-lib.sh +70 -0
  146. package/.agents/backend/fm-secondmate-reconcile.sh +371 -0
  147. package/.agents/backend/fm-secondmate-registry-lib.sh +311 -0
  148. package/.agents/backend/fm-secondmate-report.sh +88 -0
  149. package/.agents/backend/fm-send.sh +1059 -0
  150. package/.agents/backend/fm-session-lock-lib.sh +176 -0
  151. package/.agents/backend/fm-session-start.sh +970 -0
  152. package/.agents/backend/fm-sessionstart-cursor.sh +40 -0
  153. package/.agents/backend/fm-sessionstart-nudge.sh +45 -0
  154. package/.agents/backend/fm-sessionstart-run.sh +145 -0
  155. package/.agents/backend/fm-spawn.sh +3153 -0
  156. package/.agents/backend/fm-startup-memory-budget-lib.sh +224 -0
  157. package/.agents/backend/fm-startup-memory-budget.sh +94 -0
  158. package/.agents/backend/fm-startup-network.sh +642 -0
  159. package/.agents/backend/fm-stow-cascade.sh +251 -0
  160. package/.agents/backend/fm-subagent-pretool-check.sh +207 -0
  161. package/.agents/backend/fm-supervise-daemon.sh +1732 -0
  162. package/.agents/backend/fm-supervision-instructions.sh +215 -0
  163. package/.agents/backend/fm-supervision-lib.sh +90 -0
  164. package/.agents/backend/fm-supervisor-target-lib.sh +78 -0
  165. package/.agents/backend/fm-tangle-lib.sh +53 -0
  166. package/.agents/backend/fm-task-inbox-lib.sh +402 -0
  167. package/.agents/backend/fm-tasks-axi-lib.sh +123 -0
  168. package/.agents/backend/fm-teardown.sh +2922 -0
  169. package/.agents/backend/fm-test-isolation-proof.sh +588 -0
  170. package/.agents/backend/fm-test-run.sh +2191 -0
  171. package/.agents/backend/fm-timeout-lib.sh +141 -0
  172. package/.agents/backend/fm-timing-lib.sh +179 -0
  173. package/.agents/backend/fm-tmux-lib.sh +291 -0
  174. package/.agents/backend/fm-tool-update-check.sh +898 -0
  175. package/.agents/backend/fm-trace-context-lib.sh +227 -0
  176. package/.agents/backend/fm-transition-lib.sh +103 -0
  177. package/.agents/backend/fm-turnend-guard-cursor.sh +391 -0
  178. package/.agents/backend/fm-turnend-guard-grok.sh +90 -0
  179. package/.agents/backend/fm-turnend-guard.sh +432 -0
  180. package/.agents/backend/fm-update.sh +110 -0
  181. package/.agents/backend/fm-vendor-auth-probe.sh +190 -0
  182. package/.agents/backend/fm-voice-client.py +1373 -0
  183. package/.agents/backend/fm-voice-relay.py +1256 -0
  184. package/.agents/backend/fm-wake-drain.sh +592 -0
  185. package/.agents/backend/fm-wake-grant.sh +127 -0
  186. package/.agents/backend/fm-wake-lib.sh +1849 -0
  187. package/.agents/backend/fm-watch-arm.sh +602 -0
  188. package/.agents/backend/fm-watch-checkpoint.sh +109 -0
  189. package/.agents/backend/fm-watch.sh +1962 -0
  190. package/.agents/backend/fm-x-dismiss.sh +116 -0
  191. package/.agents/backend/fm-x-followup.sh +286 -0
  192. package/.agents/backend/fm-x-lib.sh +1000 -0
  193. package/.agents/backend/fm-x-link.sh +234 -0
  194. package/.agents/backend/fm-x-poll.sh +185 -0
  195. package/.agents/backend/fm-x-reply.sh +385 -0
  196. package/.agents/backend/fm_voice_frame.py +166 -0
  197. package/.agents/backend/fm_voice_records.py +574 -0
  198. package/.agents/backend/model-bridge.py +290 -0
  199. package/.agents/backend/opencode-go-bridge.py +30 -0
  200. package/.agents/bus/README.md +12 -0
  201. package/.agents/bus/messages.json +1 -0
  202. package/.agents/config/agents.machine.example.yaml +16 -0
  203. package/.agents/config/agents.yaml +50 -0
  204. package/.agents/config/agents.yaml.example +72 -0
  205. package/.agents/config/crew-dispatch.json +22 -0
  206. package/.agents/config/cron.yaml +9 -0
  207. package/.agents/config/eindri-dispatch.json +67 -0
  208. package/.agents/config/eindri-harness +1 -0
  209. package/.agents/config/startup-memory-budget +1 -0
  210. package/.agents/config/tailscale-sync.yaml +27 -0
  211. package/.agents/filebrowser/.filebrowser.json +6 -0
  212. package/.agents/filebrowser/README.md +11 -0
  213. package/.agents/gateway/README.md +12 -0
  214. package/.agents/github/README.md +15 -0
  215. package/.agents/github/webhooks/.gitkeep +0 -0
  216. package/.agents/github/workflows/.gitkeep +0 -0
  217. package/.agents/harness/opencode/plugins/lib/rodd-operational-input.js +37 -0
  218. package/.agents/harness/opencode/plugins/package.json +4 -0
  219. package/.agents/harness/opencode/plugins/saga-sessionstart.js +77 -0
  220. package/.agents/harness/opencode/plugins/syn-cd-check.js +61 -0
  221. package/.agents/harness/opencode/plugins/syn-pretool-check.js +68 -0
  222. package/.agents/harness/opencode/plugins/syn-turnend-guard.js +105 -0
  223. package/.agents/harness/opencode/plugins/syn-watch-arm.js +521 -0
  224. package/.agents/memory/README.md +46 -0
  225. package/.agents/memory/kaia.engram +0 -0
  226. package/.agents/memory/kaia.engram-shm +0 -0
  227. package/.agents/memory/kaia.engram-wal +0 -0
  228. package/.agents/memory/runes_audit.md +5 -0
  229. package/.agents/memory/well/episodes.jsonl +367 -0
  230. package/.agents/memory/well/workspace.jsonl +27 -0
  231. package/.agents/migrations/0001-hodd-layout.sh +38 -0
  232. package/.agents/migrations/0002-a2a-mcp.sh +27 -0
  233. package/.agents/migrations/0003-private-data-separation.sh +111 -0
  234. package/.agents/migrations/0004-hoard-and-realms.sh +104 -0
  235. package/.agents/sandbox/Dockerfile.utgard +19 -0
  236. package/.agents/sandbox/README.md +14 -0
  237. package/.agents/sandbox/utgard.config.json +14 -0
  238. package/.agents/skills/README.md +59 -0
  239. package/.agents/skills/bragi-marketing/README.md +3 -0
  240. package/.agents/skills/bragi-marketing/SKILL.md +82 -0
  241. package/.agents/skills/eindri-homes/README.md +3 -0
  242. package/.agents/skills/eindri-homes/SKILL.md +291 -0
  243. package/.agents/skills/eindri-homes/assets/control-plane.md +122 -0
  244. package/.agents/skills/eindri-homes/assets/dispatch.md +80 -0
  245. package/.agents/skills/frigg-consent/README.md +3 -0
  246. package/.agents/skills/frigg-consent/SKILL.md +56 -0
  247. package/.agents/skills/galdr-ymirsystem/README.md +3 -0
  248. package/.agents/skills/galdr-ymirsystem/SKILL.md +158 -0
  249. package/.agents/skills/galdr-ymirsystem/assets/README.md +74 -0
  250. package/.agents/skills/galdr-ymirsystem/assets/brokk-distro-runtime.md +390 -0
  251. package/.agents/skills/galdr-ymirsystem/assets/build-method.md +113 -0
  252. package/.agents/skills/galdr-ymirsystem/assets/build-tool-categories.md +178 -0
  253. package/.agents/skills/galdr-ymirsystem/assets/eindri-orchestration.md +518 -0
  254. package/.agents/skills/galdr-ymirsystem/assets/eindri-profiles.md +136 -0
  255. package/.agents/skills/galdr-ymirsystem/assets/harness-integration/README.md +673 -0
  256. package/.agents/skills/galdr-ymirsystem/assets/harness-integration/claude-code.md +197 -0
  257. package/.agents/skills/galdr-ymirsystem/assets/harness-integration/codex.md +191 -0
  258. package/.agents/skills/galdr-ymirsystem/assets/harness-integration/cursor.md +193 -0
  259. package/.agents/skills/galdr-ymirsystem/assets/harness-integration/opencode.md +262 -0
  260. package/.agents/skills/galdr-ymirsystem/assets/harness-integration/pi.md +295 -0
  261. package/.agents/skills/galdr-ymirsystem/assets/hlidskjalf-ui.md +496 -0
  262. package/.agents/skills/galdr-ymirsystem/assets/installation.md +571 -0
  263. package/.agents/skills/galdr-ymirsystem/assets/local-models/HOST-DATA.example.md +72 -0
  264. package/.agents/skills/galdr-ymirsystem/assets/local-models.md +274 -0
  265. package/.agents/skills/galdr-ymirsystem/assets/memory-well.md +161 -0
  266. package/.agents/skills/galdr-ymirsystem/assets/nornir-jobs.md +377 -0
  267. package/.agents/skills/galdr-ymirsystem/assets/norse-naming.md +325 -0
  268. package/.agents/skills/galdr-ymirsystem/assets/odrerir-hall.md +129 -0
  269. package/.agents/skills/galdr-ymirsystem/assets/pi-boot/einherjar-spawn.schema.json +81 -0
  270. package/.agents/skills/galdr-ymirsystem/assets/pi-boot/herdr-profile.toml +75 -0
  271. package/.agents/skills/galdr-ymirsystem/assets/pi-boot/pi-profile.yml +96 -0
  272. package/.agents/skills/galdr-ymirsystem/assets/pi-boot/supervision-tree.yml +163 -0
  273. package/.agents/skills/galdr-ymirsystem/assets/pi-boot-guide.md +58 -0
  274. package/.agents/skills/galdr-ymirsystem/assets/porting-upstream-to-norse.md +301 -0
  275. package/.agents/skills/galdr-ymirsystem/assets/principles.md +385 -0
  276. package/.agents/skills/galdr-ymirsystem/assets/reference-adoption.md +99 -0
  277. package/.agents/skills/galdr-ymirsystem/assets/registry.md +379 -0
  278. package/.agents/skills/galdr-ymirsystem/assets/runtime-compliance.md +463 -0
  279. package/.agents/skills/galdr-ymirsystem/assets/runtime-components.md +553 -0
  280. package/.agents/skills/galdr-ymirsystem/assets/smidja.md +175 -0
  281. package/.agents/skills/galdr-ymirsystem/schemas/toon-schemas.md +190 -0
  282. package/.agents/skills/galdr-ymirsystem/scripts/README.md +45 -0
  283. package/.agents/skills/galdr-ymirsystem/scripts/bench-one.sh +248 -0
  284. package/.agents/skills/galdr-ymirsystem/scripts/compliance-check.sh +360 -0
  285. package/.agents/skills/galdr-ymirsystem/scripts/gpu-sample.sh +43 -0
  286. package/.agents/skills/galdr-ymirsystem/scripts/stop-all.sh +34 -0
  287. package/.agents/skills/galdr-ymirsystem/scripts/toon-check.py +188 -0
  288. package/.agents/skills/gjallarhorn-relay/README.md +3 -0
  289. package/.agents/skills/gjallarhorn-relay/SKILL.md +283 -0
  290. package/.agents/skills/groa-update/SKILL.md +70 -0
  291. package/.agents/skills/hamr-adapters/README.md +3 -0
  292. package/.agents/skills/hamr-adapters/SKILL.md +123 -0
  293. package/.agents/skills/hamr-adapters/references/common/control-and-recovery.md +37 -0
  294. package/.agents/skills/hamr-adapters/references/common/dispatch.md +32 -0
  295. package/.agents/skills/hamr-adapters/references/common/model-and-effort.md +42 -0
  296. package/.agents/skills/hamr-adapters/references/common/primary-hooks.md +40 -0
  297. package/.agents/skills/hamr-adapters/references/harness/claude.md +55 -0
  298. package/.agents/skills/hamr-adapters/references/harness/codex.md +43 -0
  299. package/.agents/skills/hamr-adapters/references/harness/cursor.md +75 -0
  300. package/.agents/skills/hamr-adapters/references/harness/grok.md +69 -0
  301. package/.agents/skills/hamr-adapters/references/harness/kimi.md +51 -0
  302. package/.agents/skills/hamr-adapters/references/harness/muse.md +70 -0
  303. package/.agents/skills/hamr-adapters/references/harness/opencode.md +42 -0
  304. package/.agents/skills/hamr-adapters/references/harness/pi.md +56 -0
  305. package/.agents/skills/herdr-panes/README.md +3 -0
  306. package/.agents/skills/herdr-panes/SKILL.md +92 -0
  307. package/.agents/skills/herdr-panes/assets/herdr-backend.md +346 -0
  308. package/.agents/skills/herdr-panes/assets/tmux-backend.md +115 -0
  309. package/.agents/skills/hnoss-design/DESIGN.md +19 -0
  310. package/.agents/skills/hnoss-design/README.md +3 -0
  311. package/.agents/skills/hnoss-design/SKILL.md +83 -0
  312. package/.agents/skills/hvild-afk/README.md +3 -0
  313. package/.agents/skills/hvild-afk/SKILL.md +241 -0
  314. package/.agents/skills/jord-projects/README.md +3 -0
  315. package/.agents/skills/jord-projects/SKILL.md +92 -0
  316. package/.agents/skills/lifecycle/SKILL.md +56 -0
  317. package/.agents/skills/lifecycle/smoke_test.sh +52 -0
  318. package/.agents/skills/lifecycle/start.sh +21 -0
  319. package/.agents/skills/lifecycle/status.sh +35 -0
  320. package/.agents/skills/lifecycle/stop.sh +16 -0
  321. package/.agents/skills/muninn-stow/README.md +3 -0
  322. package/.agents/skills/muninn-stow/SKILL.md +309 -0
  323. package/.agents/skills/no-mistakes/SKILL.md +379 -0
  324. package/.agents/skills/nornir-schedule/README.md +3 -0
  325. package/.agents/skills/nornir-schedule/SKILL.md +25 -0
  326. package/.agents/skills/nornir-schedule/assets/events.md +137 -0
  327. package/.agents/skills/nornir-schedule/assets/quota.md +131 -0
  328. package/.agents/skills/nsr-compliance/README.md +3 -0
  329. package/.agents/skills/nsr-compliance/SKILL.md +29 -0
  330. package/.agents/skills/nsr-compliance/assets/nsr/README.md +40 -0
  331. package/.agents/skills/nsr-compliance/assets/nsr/assets/RULES/README.md +15 -0
  332. package/.agents/skills/nsr-compliance/assets/nsr/assets/RULES/git-workflow.md +18 -0
  333. package/.agents/skills/nsr-compliance/assets/nsr/assets/RULES/security.md +22 -0
  334. package/.agents/skills/nsr-compliance/assets/nsr/assets/RULES/tech-stack.md +8 -0
  335. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/SKILL.md +71 -0
  336. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/config/core_four.yaml +16 -0
  337. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/gates/check_danger.sh +102 -0
  338. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/gates/check_wiring.sh +69 -0
  339. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/gates/validate_code.sh +29 -0
  340. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/gates/verify_docs.py +66 -0
  341. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/generate.sh +109 -0
  342. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/harness/envelopes/result_envelope.json +14 -0
  343. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/harness/envelopes/task_envelope.json +20 -0
  344. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/harness/runner.py +303 -0
  345. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/telemetry/logger.py +112 -0
  346. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/templates/compliance/README.md +18 -0
  347. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/templates/compliance/config/README.md +10 -0
  348. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/templates/compliance/gates/README.md +12 -0
  349. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/templates/compliance/gates/check_env.sh +23 -0
  350. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/templates/compliance/gates/check_paths.sh +20 -0
  351. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/templates/compliance/gates/check_platform.sh +23 -0
  352. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/templates/compliance/harness/README.md +11 -0
  353. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/templates/compliance/harness/envelopes/README.md +10 -0
  354. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/templates/compliance/installer/README.md +10 -0
  355. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/templates/compliance/installer/install.sh +125 -0
  356. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/templates/compliance/telemetry/README.md +11 -0
  357. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/README.md +20 -0
  358. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/features/README.md +36 -0
  359. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/features/SKILL-template.md +14 -0
  360. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/git_ops/README.md +25 -0
  361. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/git_ops/SKILL.md +13 -0
  362. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/git_ops/create_branch.sh +6 -0
  363. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/git_ops/safe_commit.sh +9 -0
  364. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/git_ops/sync_upstream.sh +6 -0
  365. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/lifecycle/README.md +27 -0
  366. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/lifecycle/SKILL.md +15 -0
  367. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/lifecycle/smoke_test.sh +12 -0
  368. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/lifecycle/start.sh +19 -0
  369. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/lifecycle/status.sh +12 -0
  370. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/lifecycle/stop.sh +15 -0
  371. package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/README.md +18 -0
  372. package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/config/README.md +10 -0
  373. package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/config/core_four.yaml +13 -0
  374. package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/gates/README.md +12 -0
  375. package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/gates/check_danger.sh +102 -0
  376. package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/gates/check_env.sh +23 -0
  377. package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/gates/check_paths.sh +20 -0
  378. package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/gates/check_platform.sh +23 -0
  379. package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/gates/check_wiring.sh +69 -0
  380. package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/harness/README.md +11 -0
  381. package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/harness/envelopes/README.md +10 -0
  382. package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/harness/envelopes/result_envelope.json +14 -0
  383. package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/harness/envelopes/task_envelope.json +20 -0
  384. package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/harness/runner.py +303 -0
  385. package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/installer/README.md +10 -0
  386. package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/installer/install.sh +125 -0
  387. package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/telemetry/README.md +11 -0
  388. package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/telemetry/logger.py +112 -0
  389. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/BEST_PRACTICES/README.md +14 -0
  390. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/BEST_PRACTICES/api.md +17 -0
  391. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/BEST_PRACTICES/compliance-wiring.md +155 -0
  392. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/BEST_PRACTICES/frontend.md +15 -0
  393. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/CI_CD/README.md +11 -0
  394. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/CI_CD/deployment/README.md +23 -0
  395. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/CI_CD/deployment/clients/README.md +16 -0
  396. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/CI_CD/deployment/clients/template-README.md +11 -0
  397. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/CI_CD/deployment/deploy.sh +49 -0
  398. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/CI_CD/deployment/envs/README.md +12 -0
  399. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/CI_CD/deployment/envs/development.env.example +17 -0
  400. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/CI_CD/deployment/envs/production.env.example +17 -0
  401. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/CI_CD/deployment/envs/staging.env.example +17 -0
  402. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/CI_CD/deployment/tenants/README.md +11 -0
  403. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/CI_CD/deployment/tenants/template.env.example +8 -0
  404. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/DEVELOPER_SETUP/README.md +26 -0
  405. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/DEVELOPER_SETUP/agent.md +7 -0
  406. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/DEVELOPER_SETUP/developers/dev-1.md +20 -0
  407. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/DEVELOPER_SETUP/linux.md +9 -0
  408. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/DEVELOPER_SETUP/macos.md +10 -0
  409. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/DEVELOPER_SETUP/shared.md +19 -0
  410. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/DEVELOPER_SETUP/windows.md +14 -0
  411. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/HOSTING/README.md +25 -0
  412. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/HOSTING/_template.md +41 -0
  413. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/HOSTING/hosting-1.md +41 -0
  414. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/README.md +21 -0
  415. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/RUNBOOK/README.md +15 -0
  416. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/RUNBOOK/deployment.md +19 -0
  417. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/RUNBOOK/disaster-recovery.md +15 -0
  418. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/RUNBOOK/ops-tasks.md +14 -0
  419. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/features/README.md +16 -0
  420. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/project-skills.md +146 -0
  421. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/research/README.md +11 -0
  422. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/research/software-compliance-research.md +269 -0
  423. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/working-with-agents.md +260 -0
  424. package/.agents/skills/nsr-compliance/assets/nsr/assets/root/AGENTS.md +38 -0
  425. package/.agents/skills/nsr-compliance/assets/nsr/assets/root/ARCHITECTURE.md +36 -0
  426. package/.agents/skills/nsr-compliance/assets/nsr/assets/root/BEST_PRACTICES.md +27 -0
  427. package/.agents/skills/nsr-compliance/assets/nsr/assets/root/CI_CD.md +23 -0
  428. package/.agents/skills/nsr-compliance/assets/nsr/assets/root/FEATURES.md +15 -0
  429. package/.agents/skills/nsr-compliance/assets/nsr/assets/root/README.md +22 -0
  430. package/.agents/skills/nsr-compliance/assets/nsr/assets/root/RULES.md +37 -0
  431. package/.agents/skills/nsr-compliance/assets/nsr/assets/root/STRUCTURE.md +45 -0
  432. package/.agents/skills/nsr-compliance/assets/nsr/assets/root/TECH_STACK.md +28 -0
  433. package/.agents/skills/nsr-compliance/assets/nsr/scaffold-spec.md +63 -0
  434. package/.agents/skills/nsr-compliance/assets/nsr/scripts/scaffold.sh +103 -0
  435. package/.agents/skills/nsr-compliance/assets/nsr/scripts/validate.sh +52 -0
  436. package/.agents/skills/nsr-compliance/assets/nsrcompliance/SKILL.md +71 -0
  437. package/.agents/skills/nsr-compliance/assets/nsrcompliance/config/core_four.yaml +16 -0
  438. package/.agents/skills/nsr-compliance/assets/nsrcompliance/gates/check_danger.sh +102 -0
  439. package/.agents/skills/nsr-compliance/assets/nsrcompliance/gates/check_wiring.sh +69 -0
  440. package/.agents/skills/nsr-compliance/assets/nsrcompliance/gates/validate_code.sh +29 -0
  441. package/.agents/skills/nsr-compliance/assets/nsrcompliance/gates/verify_docs.py +66 -0
  442. package/.agents/skills/nsr-compliance/assets/nsrcompliance/generate.sh +109 -0
  443. package/.agents/skills/nsr-compliance/assets/nsrcompliance/harness/envelopes/result_envelope.json +14 -0
  444. package/.agents/skills/nsr-compliance/assets/nsrcompliance/harness/envelopes/task_envelope.json +20 -0
  445. package/.agents/skills/nsr-compliance/assets/nsrcompliance/harness/runner.py +303 -0
  446. package/.agents/skills/nsr-compliance/assets/nsrcompliance/telemetry/logger.py +112 -0
  447. package/.agents/skills/nsr-compliance/assets/nsrcompliance/templates/compliance/README.md +18 -0
  448. package/.agents/skills/nsr-compliance/assets/nsrcompliance/templates/compliance/config/README.md +10 -0
  449. package/.agents/skills/nsr-compliance/assets/nsrcompliance/templates/compliance/gates/README.md +12 -0
  450. package/.agents/skills/nsr-compliance/assets/nsrcompliance/templates/compliance/gates/check_env.sh +23 -0
  451. package/.agents/skills/nsr-compliance/assets/nsrcompliance/templates/compliance/gates/check_paths.sh +20 -0
  452. package/.agents/skills/nsr-compliance/assets/nsrcompliance/templates/compliance/gates/check_platform.sh +23 -0
  453. package/.agents/skills/nsr-compliance/assets/nsrcompliance/templates/compliance/harness/README.md +11 -0
  454. package/.agents/skills/nsr-compliance/assets/nsrcompliance/templates/compliance/harness/envelopes/README.md +10 -0
  455. package/.agents/skills/nsr-compliance/assets/nsrcompliance/templates/compliance/installer/README.md +10 -0
  456. package/.agents/skills/nsr-compliance/assets/nsrcompliance/templates/compliance/installer/install.sh +125 -0
  457. package/.agents/skills/nsr-compliance/assets/nsrcompliance/templates/compliance/telemetry/README.md +11 -0
  458. package/.agents/skills/pr-ops/README.md +3 -0
  459. package/.agents/skills/pr-ops/SKILL.md +109 -0
  460. package/.agents/skills/ratatoskr-a2a/README.md +3 -0
  461. package/.agents/skills/ratatoskr-a2a/SKILL.md +148 -0
  462. package/.agents/skills/rules-check-drift/SKILL.md +81 -0
  463. package/.agents/skills/saga-bearings/README.md +3 -0
  464. package/.agents/skills/saga-bearings/SKILL.md +26 -0
  465. package/.agents/skills/saga-bearings/assets/bearings.md +161 -0
  466. package/.agents/skills/saga-bearings/assets/board-template.html +731 -0
  467. package/.agents/skills/saga-bearings/assets/recap.md +56 -0
  468. package/.agents/skills/syn-recovery/README.md +3 -0
  469. package/.agents/skills/syn-recovery/SKILL.md +54 -0
  470. package/.agents/skills/tyr-check/README.md +3 -0
  471. package/.agents/skills/tyr-check/SKILL.md +218 -0
  472. package/.agents/skills/urdh-hold/README.md +3 -0
  473. package/.agents/skills/urdh-hold/SKILL.md +25 -0
  474. package/.agents/skills/urdh-hold/assets/decisions.md +15 -0
  475. package/.agents/skills/urdh-hold/assets/hold.md +54 -0
  476. package/.agents/skills/vor-diagnostics/README.md +3 -0
  477. package/.agents/skills/vor-diagnostics/SKILL.md +68 -0
  478. package/.agents/skills/ymir-host/README.md +3 -0
  479. package/.agents/skills/ymir-host/SKILL.md +34 -0
  480. package/.agents/skills/ymir-host/assets/desktop.md +36 -0
  481. package/.agents/skills/ymir-host/assets/omarchy.md +297 -0
  482. package/.agents/skills/ymir-host/assets/runbooks.md +15 -0
  483. package/.agents/skills/ymir-host/assets/thjazi.md +220 -0
  484. package/.agents/skills/ymir-host/assets/update.md +64 -0
  485. package/.agents/tests/assets/board-render-harness.mjs +117 -0
  486. package/.agents/tests/cmux-test-safety.sh +51 -0
  487. package/.agents/tests/fixtures.sh +291 -0
  488. package/.agents/tests/fm-afk-inject-e2e.test.sh +428 -0
  489. package/.agents/tests/fm-afk-inject-herdr-e2e.test.sh +536 -0
  490. package/.agents/tests/fm-afk-launch.test.sh +961 -0
  491. package/.agents/tests/fm-afk-pi-herdr-return-e2e.test.sh +297 -0
  492. package/.agents/tests/fm-afk-return.test.sh +283 -0
  493. package/.agents/tests/fm-arm-pretool-check.test.sh +482 -0
  494. package/.agents/tests/fm-ask-user-authority.test.sh +42 -0
  495. package/.agents/tests/fm-backend-autodetect-smoke.test.sh +176 -0
  496. package/.agents/tests/fm-backend-cmux-smoke.test.sh +188 -0
  497. package/.agents/tests/fm-backend-cmux.test.sh +1166 -0
  498. package/.agents/tests/fm-backend-herdr-eventwait-smoke.test.sh +136 -0
  499. package/.agents/tests/fm-backend-herdr-eventwait.test.py +106 -0
  500. package/.agents/tests/fm-backend-herdr-focus-flash-e2e.test.sh +409 -0
  501. package/.agents/tests/fm-backend-herdr-launcher-workspace-e2e.test.sh +437 -0
  502. package/.agents/tests/fm-backend-herdr-presentation-e2e.test.sh +1424 -0
  503. package/.agents/tests/fm-backend-herdr-prune-safety-e2e.test.sh +180 -0
  504. package/.agents/tests/fm-backend-herdr-respawn-idem-e2e.test.sh +182 -0
  505. package/.agents/tests/fm-backend-herdr-smoke.test.sh +355 -0
  506. package/.agents/tests/fm-backend-herdr-workspace-per-home-e2e.test.sh +251 -0
  507. package/.agents/tests/fm-backend-herdr.test.sh +4606 -0
  508. package/.agents/tests/fm-backend-orca.test.sh +1357 -0
  509. package/.agents/tests/fm-backend-tmux-smoke.test.sh +173 -0
  510. package/.agents/tests/fm-backend-zellij-smoke.test.sh +205 -0
  511. package/.agents/tests/fm-backend-zellij.test.sh +1360 -0
  512. package/.agents/tests/fm-backend.test.sh +1142 -0
  513. package/.agents/tests/fm-backlog-atomicity.test.sh +2302 -0
  514. package/.agents/tests/fm-backlog-handoff.test.sh +1360 -0
  515. package/.agents/tests/fm-bearings-board-render.test.sh +135 -0
  516. package/.agents/tests/fm-bearings-board.test.sh +414 -0
  517. package/.agents/tests/fm-bearings-snapshot.test.sh +1990 -0
  518. package/.agents/tests/fm-bootstrap-network-parallel.test.sh +316 -0
  519. package/.agents/tests/fm-bootstrap.test.sh +1178 -0
  520. package/.agents/tests/fm-branch-supervision.test.sh +552 -0
  521. package/.agents/tests/fm-brief.test.sh +785 -0
  522. package/.agents/tests/fm-busy-adapter-wiring.test.sh +324 -0
  523. package/.agents/tests/fm-busy-state.test.sh +465 -0
  524. package/.agents/tests/fm-calm-pi-extension.test.sh +3979 -0
  525. package/.agents/tests/fm-captain-hold-lifecycle.test.sh +1187 -0
  526. package/.agents/tests/fm-cd-pretool-check.test.sh +400 -0
  527. package/.agents/tests/fm-check-unregister.test.sh +198 -0
  528. package/.agents/tests/fm-classify-corr-token.test.sh +543 -0
  529. package/.agents/tests/fm-classify-decision-key.test.sh +340 -0
  530. package/.agents/tests/fm-claude-stop-autoarm-live-e2e.test.sh +164 -0
  531. package/.agents/tests/fm-claude-stop-autoarm.test.sh +1190 -0
  532. package/.agents/tests/fm-cmux-claude-composer-live-e2e.test.sh +110 -0
  533. package/.agents/tests/fm-codex-continuity-live-e2e.test.sh +55 -0
  534. package/.agents/tests/fm-composer-ghost.test.sh +716 -0
  535. package/.agents/tests/fm-composer-lib.test.sh +666 -0
  536. package/.agents/tests/fm-composer-matrix-live-e2e.test.sh +220 -0
  537. package/.agents/tests/fm-control-herdr-smoke.test.sh +149 -0
  538. package/.agents/tests/fm-control-relaunch.test.sh +1530 -0
  539. package/.agents/tests/fm-control.test.sh +910 -0
  540. package/.agents/tests/fm-crew-state.test.sh +1610 -0
  541. package/.agents/tests/fm-cursor-harness.test.sh +404 -0
  542. package/.agents/tests/fm-cursor-primary-live-e2e.test.sh +217 -0
  543. package/.agents/tests/fm-cursor-primary.test.sh +708 -0
  544. package/.agents/tests/fm-daemon.test.sh +2740 -0
  545. package/.agents/tests/fm-documentation-audiences.test.sh +141 -0
  546. package/.agents/tests/fm-ensure-agents-md.test.sh +371 -0
  547. package/.agents/tests/fm-extension-binding.test.sh +2187 -0
  548. package/.agents/tests/fm-fleet-snapshot-view.test.sh +816 -0
  549. package/.agents/tests/fm-fleet-sync.test.sh +721 -0
  550. package/.agents/tests/fm-gate-refuse.test.sh +356 -0
  551. package/.agents/tests/fm-gitignore-config.test.sh +89 -0
  552. package/.agents/tests/fm-gotmp.test.sh +230 -0
  553. package/.agents/tests/fm-grok-continuity-live-e2e.test.sh +112 -0
  554. package/.agents/tests/fm-grok-harness.test.sh +117 -0
  555. package/.agents/tests/fm-grok-stop-live-e2e.test.sh +219 -0
  556. package/.agents/tests/fm-guard-stale-banner.test.sh +709 -0
  557. package/.agents/tests/fm-harness-adapter-instructions-live-e2e.test.sh +134 -0
  558. package/.agents/tests/fm-harness-adapter-references.test.sh +30 -0
  559. package/.agents/tests/fm-harness-liveness-drift-live-e2e.test.sh +151 -0
  560. package/.agents/tests/fm-herdr-lab.test.sh +243 -0
  561. package/.agents/tests/fm-herdr-session-cleanup-e2e.test.sh +146 -0
  562. package/.agents/tests/fm-herdr-session-cleanup.test.sh +329 -0
  563. package/.agents/tests/fm-herdr-submit-confirm-live-e2e.test.sh +125 -0
  564. package/.agents/tests/fm-herdr-version-floor-live-e2e.test.sh +144 -0
  565. package/.agents/tests/fm-home-summary-refresh.test.sh +971 -0
  566. package/.agents/tests/fm-inactive-reconcile.test.sh +485 -0
  567. package/.agents/tests/fm-kimi-harness.test.sh +687 -0
  568. package/.agents/tests/fm-lint-workflows.test.sh +532 -0
  569. package/.agents/tests/fm-lint.test.sh +1023 -0
  570. package/.agents/tests/fm-muse-harness.test.sh +932 -0
  571. package/.agents/tests/fm-muse-signals-live-e2e.test.sh +205 -0
  572. package/.agents/tests/fm-no-mistakes-required.test.sh +72 -0
  573. package/.agents/tests/fm-on.test.sh +507 -0
  574. package/.agents/tests/fm-opencode-primary-live-e2e.test.sh +357 -0
  575. package/.agents/tests/fm-operational-input.test.sh +160 -0
  576. package/.agents/tests/fm-peek-remote.test.sh +110 -0
  577. package/.agents/tests/fm-pending-reply.test.sh +1288 -0
  578. package/.agents/tests/fm-pi-branch-extension.test.sh +3182 -0
  579. package/.agents/tests/fm-pi-branch-live-e2e.test.sh +527 -0
  580. package/.agents/tests/fm-pi-primary-live-e2e.test.sh +345 -0
  581. package/.agents/tests/fm-pi-primary-types.test.sh +68 -0
  582. package/.agents/tests/fm-pi-watch-extension.test.sh +2843 -0
  583. package/.agents/tests/fm-pr-check-security.test.sh +2140 -0
  584. package/.agents/tests/fm-pr-merge.test.sh +2135 -0
  585. package/.agents/tests/fm-procevent-quota.test.sh +225 -0
  586. package/.agents/tests/fm-procevent-when.test.sh +406 -0
  587. package/.agents/tests/fm-procevent.test.sh +1745 -0
  588. package/.agents/tests/fm-project-origin.test.sh +110 -0
  589. package/.agents/tests/fm-public-followup.test.sh +2272 -0
  590. package/.agents/tests/fm-quota-array-dispatch-live-e2e.test.sh +526 -0
  591. package/.agents/tests/fm-quota-choose.test.sh +626 -0
  592. package/.agents/tests/fm-remote-backlog-handoff.test.sh +453 -0
  593. package/.agents/tests/fm-remote-doctor.test.sh +625 -0
  594. package/.agents/tests/fm-remote-entrypoint.test.sh +59 -0
  595. package/.agents/tests/fm-remote-job-orphan-reap.test.sh +205 -0
  596. package/.agents/tests/fm-remote-job.test.sh +768 -0
  597. package/.agents/tests/fm-remote-reply.test.sh +520 -0
  598. package/.agents/tests/fm-remote-secondmate-lifecycle-e2e.test.sh +1256 -0
  599. package/.agents/tests/fm-remote-secondmate-parent-binding.test.sh +326 -0
  600. package/.agents/tests/fm-remote-secondmate-trace-context.test.sh +310 -0
  601. package/.agents/tests/fm-remote-transport-lanes.test.sh +431 -0
  602. package/.agents/tests/fm-review-diff.test.sh +176 -0
  603. package/.agents/tests/fm-secondmate-harness.test.sh +2600 -0
  604. package/.agents/tests/fm-secondmate-lifecycle-e2e.test.sh +259 -0
  605. package/.agents/tests/fm-secondmate-liveness.test.sh +559 -0
  606. package/.agents/tests/fm-secondmate-reconcile.test.sh +732 -0
  607. package/.agents/tests/fm-secondmate-safety.test.sh +2972 -0
  608. package/.agents/tests/fm-secondmate-sync.test.sh +913 -0
  609. package/.agents/tests/fm-send-inbox-doorbell-live-e2e.test.sh +206 -0
  610. package/.agents/tests/fm-send-inbox.test.sh +352 -0
  611. package/.agents/tests/fm-send-popup-settle.test.sh +167 -0
  612. package/.agents/tests/fm-send-remote-delivery.test.sh +796 -0
  613. package/.agents/tests/fm-send-resolve-key.test.sh +551 -0
  614. package/.agents/tests/fm-send-secondmate-marker-herdr-e2e.test.sh +187 -0
  615. package/.agents/tests/fm-send-secondmate-marker.test.sh +278 -0
  616. package/.agents/tests/fm-send-settle.test.sh +147 -0
  617. package/.agents/tests/fm-send-strict.test.sh +241 -0
  618. package/.agents/tests/fm-session-lock-ancestry.test.sh +365 -0
  619. package/.agents/tests/fm-session-start.test.sh +2515 -0
  620. package/.agents/tests/fm-sessionstart-hook-live-e2e.test.sh +631 -0
  621. package/.agents/tests/fm-sessionstart-instruction-refresh-live-e2e.test.sh +230 -0
  622. package/.agents/tests/fm-sessionstart-nudge.test.sh +1039 -0
  623. package/.agents/tests/fm-shared-captain-inheritance.test.sh +404 -0
  624. package/.agents/tests/fm-spawn-batch.test.sh +148 -0
  625. package/.agents/tests/fm-spawn-dispatch-profile.test.sh +827 -0
  626. package/.agents/tests/fm-spawn-pool-base-freshen.test.sh +440 -0
  627. package/.agents/tests/fm-spawn-worktree-settle.test.sh +147 -0
  628. package/.agents/tests/fm-startup-memory-budget.test.sh +333 -0
  629. package/.agents/tests/fm-startup-network.test.sh +712 -0
  630. package/.agents/tests/fm-stow-cascade.test.sh +370 -0
  631. package/.agents/tests/fm-subagent-pretool-check.test.sh +291 -0
  632. package/.agents/tests/fm-supervision-events.test.sh +156 -0
  633. package/.agents/tests/fm-supervision-instructions.test.sh +189 -0
  634. package/.agents/tests/fm-tangle-guard.test.sh +277 -0
  635. package/.agents/tests/fm-task-delivery.test.sh +420 -0
  636. package/.agents/tests/fm-task-inbox.test.sh +513 -0
  637. package/.agents/tests/fm-teardown-endpoint-safety.test.sh +374 -0
  638. package/.agents/tests/fm-teardown.test.sh +2665 -0
  639. package/.agents/tests/fm-test-fixture-cleanup.test.sh +172 -0
  640. package/.agents/tests/fm-test-fixtures.test.sh +132 -0
  641. package/.agents/tests/fm-test-isolation-proof.test.sh +278 -0
  642. package/.agents/tests/fm-test-run.test.sh +1196 -0
  643. package/.agents/tests/fm-tmux-agent-liveness.test.sh +354 -0
  644. package/.agents/tests/fm-tmux-submit-busy.test.sh +340 -0
  645. package/.agents/tests/fm-tool-update-check.test.sh +1044 -0
  646. package/.agents/tests/fm-trace-context-lib.test.sh +253 -0
  647. package/.agents/tests/fm-trace-context-spawn.test.sh +581 -0
  648. package/.agents/tests/fm-transition-lib.test.sh +54 -0
  649. package/.agents/tests/fm-turnend-guard.test.sh +1822 -0
  650. package/.agents/tests/fm-update.test.sh +304 -0
  651. package/.agents/tests/fm-vendor-auth-probe.test.sh +395 -0
  652. package/.agents/tests/fm-voice-relay.test.sh +4741 -0
  653. package/.agents/tests/fm-wake-daemon-lifecycle-e2e.test.sh +171 -0
  654. package/.agents/tests/fm-wake-drain-open-decisions-cursor.test.sh +356 -0
  655. package/.agents/tests/fm-wake-drain-open-decisions.test.sh +226 -0
  656. package/.agents/tests/fm-wake-drain-unread-status.test.sh +379 -0
  657. package/.agents/tests/fm-wake-queue.test.sh +1224 -0
  658. package/.agents/tests/fm-watch-arm.test.sh +821 -0
  659. package/.agents/tests/fm-watch-checkpoint.test.sh +87 -0
  660. package/.agents/tests/fm-watch-recovery-loop.test.sh +223 -0
  661. package/.agents/tests/fm-watch-triage.test.sh +3889 -0
  662. package/.agents/tests/fm-watcher-lock.test.sh +1133 -0
  663. package/.agents/tests/fm-x-mode.test.sh +3042 -0
  664. package/.agents/tests/gleipnir-guard-pretool-check.test.sh +51 -0
  665. package/.agents/tests/gleipnir-machine-lock.test.sh +130 -0
  666. package/.agents/tests/herdr-test-safety.sh +42 -0
  667. package/.agents/tests/lib.sh +315 -0
  668. package/.agents/tests/remote-herdr-fixture.sh +125 -0
  669. package/.agents/tests/secondmate-helpers.sh +193 -0
  670. package/.agents/tests/smoke.test.sh +93 -0
  671. package/.agents/tests/wake-helpers.sh +338 -0
  672. package/.agents/tests/zellij-test-safety.sh +60 -0
  673. package/.agents/tools/README.md +10 -0
  674. package/.agents/tools/bin/tasks-cli +267 -0
  675. package/.agents/tools/tasks-cli.ts +216 -0
  676. package/AGENTS.md +454 -0
  677. package/CHANGELOG.md +881 -0
  678. package/LICENSE +1 -1
  679. package/NOTICE +63 -0
  680. package/README.md +113 -39
  681. package/RULES/01-domains.md +51 -0
  682. package/RULES/02-agents.md +78 -0
  683. package/RULES/03-houses.md +61 -0
  684. package/RULES/04-hoard.md +65 -0
  685. package/RULES/05-platforms.md +67 -0
  686. package/RULES/06-append-only.md +58 -0
  687. package/RULES/07-config.md +45 -0
  688. package/RULES/08-delivery-gate.md +75 -0
  689. package/RULES/README.md +20 -0
  690. package/bin/README.md +107 -0
  691. package/bin/a2a-mcp.sh +98 -0
  692. package/bin/a2a-serve.py +103 -0
  693. package/bin/a2a-serve.sh +8 -0
  694. package/bin/a2a-talk.sh +50 -0
  695. package/bin/a2abridge-ensure.sh +108 -0
  696. package/bin/agent-run.sh +43 -0
  697. package/bin/agents-config.sh +233 -0
  698. package/bin/apodex-smoke-test.sh +92 -0
  699. package/bin/bifrost-bridge.sh +112 -0
  700. package/bin/bifrost-ingress.sh +77 -0
  701. package/bin/bootstrap-macos.sh +107 -0
  702. package/bin/bootstrap-windows.ps1 +112 -0
  703. package/bin/branch-guard.sh +64 -0
  704. package/bin/brokk +73 -0
  705. package/bin/brokk-classify-lib.sh +1770 -0
  706. package/bin/brokk-lease-lib.sh +222 -0
  707. package/bin/brokk-lease.sh +193 -0
  708. package/bin/brokk-lint.sh +65 -0
  709. package/bin/brokk-send.sh +38 -0
  710. package/bin/brokk-update.sh +9 -0
  711. package/bin/brokk-wake-grant.sh +127 -0
  712. package/bin/brokk-wake-lib.sh +1849 -0
  713. package/bin/changelog-guard.sh +165 -0
  714. package/bin/crash-sense.sh +160 -0
  715. package/bin/desktop-place.sh +224 -0
  716. package/bin/docs-guard.sh +39 -0
  717. package/bin/editor-place.sh +207 -0
  718. package/bin/eindri-acclaim.sh +34 -0
  719. package/bin/eindri-control.sh +28 -0
  720. package/bin/eindri-role.sh +83 -0
  721. package/bin/eindri-seen.sh +41 -0
  722. package/bin/eindri-send.sh +15 -0
  723. package/bin/eindri-start.sh +85 -0
  724. package/bin/eindri-watch.sh +57 -0
  725. package/bin/einherjar-spawn.sh +576 -0
  726. package/bin/eir-doctor.sh +112 -0
  727. package/bin/erindi-brief.sh +271 -0
  728. package/bin/fleet-apply.sh +68 -0
  729. package/bin/github-deploy.sh +81 -0
  730. package/bin/gjallarhorn-expose.sh +102 -0
  731. package/bin/gjallarhorn-notify.sh +58 -0
  732. package/bin/gjallarhorn-purge.sh +54 -0
  733. package/bin/gjallarhorn-tunnel.sh +72 -0
  734. package/bin/gleipnir-lock-lib.sh +284 -0
  735. package/bin/groa-update.sh +122 -0
  736. package/bin/hall-snapshot.sh +92 -0
  737. package/bin/hamr-harness.sh +246 -0
  738. package/bin/heimdall-ssh-keys.sh +374 -0
  739. package/bin/herdr-agents.py +44 -0
  740. package/bin/herdr-ensure.sh +104 -0
  741. package/bin/herdr-run.sh +431 -0
  742. package/bin/hermes-ensure.sh +88 -0
  743. package/bin/hoard-lib.sh +25 -0
  744. package/bin/hodd.sh +78 -0
  745. package/bin/host-sense.sh +199 -0
  746. package/bin/huginn-research-worker.sh +108 -0
  747. package/bin/local-model-lock.sh +51 -0
  748. package/bin/mimir-bridge.py +226 -0
  749. package/bin/mimir-bridge.sh +87 -0
  750. package/bin/mimir-ingest.sh +131 -0
  751. package/bin/mimir.sh +113 -0
  752. package/bin/mjollnir-webhook.sh +59 -0
  753. package/bin/mjollnir.sh +85 -0
  754. package/bin/model-hardware.sh +82 -0
  755. package/bin/model-resolve.sh +147 -0
  756. package/bin/models-detect.sh +178 -0
  757. package/bin/nornir-cron-start.sh +162 -0
  758. package/bin/nornir-job-daily-briefing.sh +191 -0
  759. package/bin/nornir-job-git-sync.sh +152 -0
  760. package/bin/nornir-job-memory-housekeeping.sh +114 -0
  761. package/bin/nornir-job-observer.sh +166 -0
  762. package/bin/nornir-job-skillopt-sleep.sh +76 -0
  763. package/bin/omarchy-hook-install.sh +60 -0
  764. package/bin/omarchy-install.sh +148 -0
  765. package/bin/omarchy-plugins.sh +174 -0
  766. package/bin/omarchy-sense.sh +149 -0
  767. package/bin/perm-guard.sh +30 -0
  768. package/bin/pi-agent.sh +68 -0
  769. package/bin/pi-ensure.sh +77 -0
  770. package/bin/pi-local.sh +37 -0
  771. package/bin/pi-seat.sh +63 -0
  772. package/bin/prereq-ensure.sh +166 -0
  773. package/bin/private-guard.sh +38 -0
  774. package/bin/project-git.sh +55 -0
  775. package/bin/public-guard.sh +70 -0
  776. package/bin/ratatoskr.sh +49 -0
  777. package/bin/realm-lib.sh +32 -0
  778. package/bin/repo-scrub.sh +62 -0
  779. package/bin/rodd-operational-input.sh +250 -0
  780. package/bin/runes-append.sh +198 -0
  781. package/bin/saga-session-start.sh +172 -0
  782. package/bin/saga-sessionstart-run.sh +79 -0
  783. package/bin/saga-wake-drain.sh +41 -0
  784. package/bin/secret-guard.sh +63 -0
  785. package/bin/sessrumnir-ensure.sh +100 -0
  786. package/bin/sessrumnir-sync.sh +237 -0
  787. package/bin/sessrumnir.sh +91 -0
  788. package/bin/skillopt-setup.sh +76 -0
  789. package/bin/skuld-branch-outcome.sh +203 -0
  790. package/bin/skuld-branch-prompt.sh +101 -0
  791. package/bin/smidja-bootstrap.sh +90 -0
  792. package/bin/smidja-observe.sh +86 -0
  793. package/bin/syn-arm-pretool-check.sh +57 -0
  794. package/bin/syn-asset-pretool-check.sh +76 -0
  795. package/bin/syn-cd-pretool-check.sh +28 -0
  796. package/bin/syn-guard-pretool-check.sh +83 -0
  797. package/bin/syn-turnend-guard.sh +36 -0
  798. package/bin/syn-watch-arm.sh +99 -0
  799. package/bin/tailscale-sync.sh +190 -0
  800. package/bin/telegram-bot.sh +69 -0
  801. package/bin/toolchain.sh +74 -0
  802. package/bin/utgard.sh +111 -0
  803. package/bin/valhalla.sh +115 -0
  804. package/bin/valknut-load.sh +260 -0
  805. package/bin/vor-crew-state.sh +124 -0
  806. package/bin/wedge-notify.sh +47 -0
  807. package/bin/workspace-provision.sh +80 -0
  808. package/bin/workspace-rag.sh +150 -0
  809. package/bin/wyrd-db.sh +47 -0
  810. package/bin/yggdrasil.sh +152 -0
  811. package/bin/ymir-edit.sh +145 -0
  812. package/bin/ymir-install.sh +641 -0
  813. package/bin/ymir-invite.sh +93 -0
  814. package/bin/ymir-isolation.sh +88 -0
  815. package/bin/ymir-migrate.sh +61 -0
  816. package/bin/ymir-platform.sh +348 -0
  817. package/bin/ymir-say.sh +97 -0
  818. package/bin/ymir-setup-auth.sh +108 -0
  819. package/bin/ymir-validate.sh +173 -0
  820. package/bin/ymir.js +74 -16
  821. package/docs/Architecture.md +339 -0
  822. package/docs/design.md +351 -0
  823. package/docs/installations/README.md +63 -0
  824. package/docs/installations/ymir-home.md +93 -0
  825. package/docs/integration.md +105 -0
  826. package/docs/lore.md +1056 -0
  827. package/docs/research/agent-harnesses.md +290 -0
  828. package/docs/runbooks/README.md +19 -0
  829. package/docs/runbooks/a2a.md +91 -0
  830. package/docs/runbooks/agent-permissions.md +99 -0
  831. package/docs/runbooks/agents.md +57 -0
  832. package/docs/runbooks/models.md +69 -0
  833. package/docs/runbooks/realm-onboarding.md +60 -0
  834. package/docs/runbooks/secrets-and-hoard.md +60 -0
  835. package/docs/runbooks/updates-and-migrations.md +110 -0
  836. package/docs/runbooks/worktrees-isolation.md +72 -0
  837. package/docs/session-start.md +105 -0
  838. package/docs/skillopt-integration.md +321 -0
  839. package/docs/sync-system.md +66 -0
  840. package/docs/workspaces.md +104 -0
  841. package/docs/ymir-rut.md +260 -0
  842. package/package.json +44 -14
  843. package/packaging/build.sh +74 -0
  844. package/packaging/macos/Ymir Installer.command +43 -0
  845. package/packaging/windows/ymir-setup.nsi +74 -0
  846. package/scripts/README.md +22 -0
  847. package/scripts/electron.sh +210 -0
  848. package/scripts/start.sh +167 -0
  849. package/scripts/stop.sh +113 -0
  850. package/install.sh +0 -35
@@ -0,0 +1,1409 @@
1
+ #!/usr/bin/env bash
2
+ # fm-pending-reply-lib.sh - parent-owned secondmate missed-report guards.
3
+ #
4
+ # When the main firstmate delivers a reply-bearing marked from-firstmate request
5
+ # to a secondmate, this library records a durable parent-owned pending-reply
6
+ # expectation BEFORE delivery, embeds a privacy-safe correlation id in the
7
+ # outbound message, and later resolves that expectation only from a correlated
8
+ # parent status line or status-pointed document - never from transport success,
9
+ # chat content, or unrelated status activity.
10
+ #
11
+ # Safety property (captain direction 2026-07-22): a secondmate agent may ignore
12
+ # the marker and answer only in its visible conversation. The parent must notice
13
+ # the missing correlated report without scraping that conversation, send exactly
14
+ # one automatic recovery request asking for a repost through the parent channel,
15
+ # and escalate once if the recovery turn also completes without a correlated
16
+ # report. Never loop, never repeatedly inject, never silently expire unresolved
17
+ # records, and never treat wrong-home or structured-home heuristics as
18
+ # acknowledgement.
19
+ #
20
+ # Record location (parent FM_HOME):
21
+ # state/pending-replies/<corr_id>
22
+ # One more durable input, owned by bin/fm-procevent-remote-reply.sh and read
23
+ # here: state/remote-replies/<task_id>.caught-up, the remote reply mirror's
24
+ # watermark (see the remote reply-channel freshness section below).
25
+ # Each record is a key=value file owned by this library. Schema:
26
+ # schema=fm-pending-reply.v1
27
+ # corr_id= privacy-safe correlation token
28
+ # task_id= secondmate task id in the parent home
29
+ # parent_home= absolute parent FM_HOME
30
+ # parent_status= absolute path of parent state/<task_id>.status
31
+ # parent_status_scan_signature=
32
+ # request_summary= short sanitized summary (no secrets by design)
33
+ # created_epoch= when the expectation was created
34
+ # delivered_epoch= when the marked request was confirmed delivered
35
+ # (empty until delivery; delivery never resolves)
36
+ # phase= awaiting_report | delivery_unknown | recovery_sending |
37
+ # recovery_sent | recovery_failed | recovery_unknown |
38
+ # escalated | resolved
39
+ # turn_seen_busy= 0|1 after delivery for the original request turn
40
+ # request_turn_completed_epoch=
41
+ # recovery_attempted_epoch=
42
+ # recovery_sender_pid=
43
+ # recovery_sender_identity=
44
+ # recovery_sent_epoch=
45
+ # recovery_delivery_outcome=
46
+ # recovery_turn_seen_busy=
47
+ # recovery_turn_completed_epoch=
48
+ # escalated_epoch=
49
+ # escalation_closed_epoch=
50
+ # when the durable status decision opened by that
51
+ # escalation was closed again (see the escalation
52
+ # lifecycle note below); empty until then
53
+ # resolved_epoch=
54
+ # resolved_via= status | document | helper | empty
55
+ # wrong_home_hits= count of corr sightings under the secondmate home
56
+ # wrong_home_sightings= comma-separated identities of counted sightings
57
+ # wrong_home_scan_signature=
58
+ # grace_secs= bounded grace before recovery is eligible
59
+ #
60
+ # Escalation lifecycle: an escalation is not just a message, it OPENS a durable
61
+ # keyed decision in the parent status log, and bin/fm-classify-lib.sh's fold is
62
+ # the one owner of what closes it. So this library owns both ends of that
63
+ # decision: fm_pending_reply_maybe_escalate opens it under a per-request key, and
64
+ # fm_pending_reply_close_escalation closes it once the record resolves. Resolving
65
+ # the record alone would leave the decision open forever, re-surfacing a settled
66
+ # request in every later OPEN DECISIONS fold.
67
+ # That per-request key lives in a namespace the fold reserves to this library, so
68
+ # no other writer into the same status stream - a local mate appending directly,
69
+ # or a remote mate's mirrored line - can take the key over or clear it; see the
70
+ # reserved-key rule in bin/fm-classify-lib.sh.
71
+ #
72
+ # Sourced by bin/fm-send.sh, bin/fm-watch.sh, bin/fm-secondmate-report.sh, and
73
+ # tests. No side effects on source. set -u / set -e safe.
74
+ #
75
+ # Tunables (env):
76
+ # FM_PENDING_REPLY_GRACE_SECS default 120
77
+ # FM_PENDING_REPLY_DIR_OVERRIDE override the pending-replies directory (tests)
78
+ # FM_PENDING_REPLY_SEND_HOOK optional command template for recovery delivery
79
+ # (tests); receives task_id and full message as args
80
+ # FM_PENDING_REPLY_NOW optional fixed epoch for deterministic tests
81
+
82
+ # shellcheck source=bin/fm-marker-lib.sh
83
+ _FM_PENDING_REPLY_LIB_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd 2>/dev/null)" || _FM_PENDING_REPLY_LIB_DIR="."
84
+ # shellcheck source=bin/fm-marker-lib.sh
85
+ . "$_FM_PENDING_REPLY_LIB_DIR/fm-marker-lib.sh"
86
+ # shellcheck source=bin/fm-backend.sh
87
+ . "$_FM_PENDING_REPLY_LIB_DIR/fm-backend.sh"
88
+ # shellcheck source=bin/fm-tmux-lib.sh
89
+ . "$_FM_PENDING_REPLY_LIB_DIR/fm-tmux-lib.sh"
90
+ # shellcheck source=bin/fm-classify-lib.sh
91
+ . "$_FM_PENDING_REPLY_LIB_DIR/fm-classify-lib.sh"
92
+
93
+ FM_PENDING_REPLY_SCHEMA='fm-pending-reply.v1'
94
+ FM_PENDING_REPLY_CORR_RE='corr=[A-Fa-f0-9]{16}'
95
+ FM_PENDING_REPLY_GRACE_DEFAULT=120
96
+
97
+ fm_pending_reply_now() {
98
+ if [ -n "${FM_PENDING_REPLY_NOW:-}" ]; then
99
+ printf '%s' "$FM_PENDING_REPLY_NOW"
100
+ return 0
101
+ fi
102
+ date +%s
103
+ }
104
+
105
+ fm_pending_reply_grace_secs() {
106
+ local g=${FM_PENDING_REPLY_GRACE_SECS:-$FM_PENDING_REPLY_GRACE_DEFAULT}
107
+ case "$g" in
108
+ ''|*[!0-9]*) g=$FM_PENDING_REPLY_GRACE_DEFAULT ;;
109
+ esac
110
+ printf '%s' "$g"
111
+ }
112
+
113
+ # Directory holding durable pending-reply records for <state-dir>.
114
+ fm_pending_reply_dir() { # <state-dir>
115
+ local state=$1
116
+ if [ -n "${FM_PENDING_REPLY_DIR_OVERRIDE:-}" ]; then
117
+ printf '%s' "$FM_PENDING_REPLY_DIR_OVERRIDE"
118
+ return 0
119
+ fi
120
+ printf '%s/pending-replies' "$state"
121
+ }
122
+
123
+ fm_pending_reply_path() { # <state-dir> <corr_id>
124
+ printf '%s/%s' "$(fm_pending_reply_dir "$1")" "$2"
125
+ }
126
+
127
+ # Privacy-safe correlation id: 16 lowercase hex chars (64 bits of entropy).
128
+ fm_pending_reply_new_id() {
129
+ local raw hex
130
+ if command -v openssl >/dev/null 2>&1; then
131
+ raw=$(openssl rand -hex 8 2>/dev/null || true)
132
+ fi
133
+ if [ -z "$raw" ]; then
134
+ raw=$(printf '%s' "$$-$(date +%s%N 2>/dev/null || date +%s)-$RANDOM$RANDOM" | cksum 2>/dev/null | awk '{print $1}')
135
+ hex=$(printf '%s' "$raw$RANDOM$RANDOM" | shasum -a 256 2>/dev/null | awk '{print $1}')
136
+ raw=${hex:0:16}
137
+ fi
138
+ printf '%s' "$(printf '%s' "$raw" | tr 'A-F' 'a-f' | tr -cd 'a-f0-9' | cut -c1-16)"
139
+ }
140
+
141
+ fm_pending_reply_corr_token() { # <corr_id>
142
+ printf 'corr=%s' "$1"
143
+ }
144
+
145
+ # Extract the first corr=<16hex> token from free text, or empty.
146
+ fm_pending_reply_extract_corr() { # <text>
147
+ local text=$1
148
+ printf '%s' "$text" | grep -oE "$FM_PENDING_REPLY_CORR_RE" 2>/dev/null | head -1 | cut -d= -f2- | tr 'A-F' 'a-f' || true
149
+ }
150
+
151
+ # 0 if <text> carries the exact correlation token for <corr_id>.
152
+ fm_pending_reply_text_has_corr() { # <text> <corr_id>
153
+ local text=$1 corr=$2 token
154
+ token=$(fm_pending_reply_corr_token "$corr")
155
+ case "$text" in
156
+ *"$token"*) return 0 ;;
157
+ esac
158
+ return 1
159
+ }
160
+
161
+ # Sanitize a short request summary: single line, bounded, no control chars.
162
+ fm_pending_reply_summarize() { # <text>
163
+ local text=$1 cleaned
164
+ cleaned=$(printf '%s' "$text" | tr '\t\r\n' ' ' | tr -cd '\11\12\15\40-\176' | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')
165
+ # Drop an already-present marker/corr prefix so the durable summary stays short.
166
+ cleaned=${cleaned#"$FM_FROMFIRST_MARK"}
167
+ cleaned=$(printf '%s' "$cleaned" | sed -E "s/^corr=[A-Fa-f0-9]{16}[[:space:]]*//")
168
+ if [ "${#cleaned}" -gt 120 ]; then
169
+ cleaned="${cleaned:0:117}..."
170
+ fi
171
+ printf '%s' "$cleaned"
172
+ }
173
+
174
+ fm_pending_reply_get() { # <record-path> <key>
175
+ local rec=$1 key=$2
176
+ [ -f "$rec" ] || return 0
177
+ grep "^${key}=" "$rec" 2>/dev/null | tail -1 | cut -d= -f2- || true
178
+ }
179
+
180
+ fm_pending_reply_corr_reusable() { # <state-dir> <corr_id> <task_id>
181
+ local state=$1 corr=$2 task_id=$3 rec phase delivered
182
+ printf '%s' "$corr" | grep -Eq '^[A-Fa-f0-9]{16}$' || return 1
183
+ rec=$(fm_pending_reply_path "$state" "$corr")
184
+ [ -f "$rec" ] || return 1
185
+ [ "$(fm_pending_reply_get "$rec" task_id)" = "$task_id" ] || return 1
186
+ phase=$(fm_pending_reply_get "$rec" phase)
187
+ case "$phase" in
188
+ awaiting_report|recovery_sending|recovery_sent) return 0 ;;
189
+ delivery_unknown)
190
+ delivered=$(fm_pending_reply_get "$rec" delivered_epoch)
191
+ [ -z "$delivered" ]
192
+ return $?
193
+ ;;
194
+ esac
195
+ return 1
196
+ }
197
+
198
+ # Rewrite one key in a pending-reply record atomically. Other keys preserved.
199
+ fm_pending_reply_set() { # <record-path> <key> <value>
200
+ local rec=$1 key=$2 value=$3 dir base tmp line
201
+ [ -f "$rec" ] || return 1
202
+ dir=$(dirname "$rec")
203
+ base=$(basename "$rec")
204
+ tmp="$dir/.${base}.tmp.$$"
205
+ : > "$tmp" || return 1
206
+ while IFS= read -r line || [ -n "$line" ]; do
207
+ case "$line" in
208
+ "${key}="*) continue ;;
209
+ esac
210
+ printf '%s\n' "$line" >> "$tmp" || return 1
211
+ done < "$rec"
212
+ printf '%s=%s\n' "$key" "$value" >> "$tmp" || return 1
213
+ mv -f "$tmp" "$rec"
214
+ }
215
+
216
+ # Embed or replace a correlation token after the from-firstmate marker.
217
+ # Idempotent for the same corr; replaces a different leading corr token.
218
+ # Result is assigned to <result-var>.
219
+ # Trailing newlines in the request body are preserved: never strip via bare
220
+ # $(...) on the body (command substitution removes trailing newlines).
221
+ fm_pending_reply_embed_corr() { # <message> <corr_id> <result-var>
222
+ local message=$1 corr=$2 result_var=$3 body token marked existing
223
+ [ -n "$result_var" ] || return 2
224
+ token=$(fm_pending_reply_corr_token "$corr")
225
+ fm_message_mark_from_firstmate "$message" marked
226
+ body=${marked#"$FM_FROMFIRST_MARK"}
227
+ # Strip a leading corr=<16hex> plus following blanks (space/tab only).
228
+ existing=${body:0:21}
229
+ case "$existing" in
230
+ corr=[a-fA-F0-9][a-fA-F0-9][a-fA-F0-9][a-fA-F0-9][a-fA-F0-9][a-fA-F0-9][a-fA-F0-9][a-fA-F0-9][a-fA-F0-9][a-fA-F0-9][a-fA-F0-9][a-fA-F0-9][a-fA-F0-9][a-fA-F0-9][a-fA-F0-9][a-fA-F0-9])
231
+ body=${body:21}
232
+ while [ "${body# }" != "$body" ]; do body=${body# }; done
233
+ while [ "${body#$'\t'}" != "$body" ]; do body=${body#$'\t'}; done
234
+ ;;
235
+ esac
236
+ printf -v "$result_var" '%s' "${FM_FROMFIRST_MARK}${token} ${body}"
237
+ }
238
+
239
+ # Create a durable pending-reply expectation. Prints corr_id on success.
240
+ # Does not deliver anything. Fails if parent paths cannot be prepared.
241
+ fm_pending_reply_create() { # <parent-home> <state-dir> <task_id> <request-text>
242
+ local parent_home=$1 state=$2 task_id=$3 request_text=$4
243
+ local dir rec corr now summary status_path tmp
244
+ [ -n "$parent_home" ] && [ -n "$state" ] && [ -n "$task_id" ] || return 2
245
+ dir=$(fm_pending_reply_dir "$state")
246
+ mkdir -p "$dir" || return 1
247
+ chmod 700 "$dir" 2>/dev/null || true
248
+ corr=$(fm_pending_reply_new_id)
249
+ [ "${#corr}" -eq 16 ] || return 1
250
+ rec=$(fm_pending_reply_path "$state" "$corr")
251
+ # Extremely unlikely collision; regenerate once.
252
+ if [ -e "$rec" ]; then
253
+ corr=$(fm_pending_reply_new_id)
254
+ rec=$(fm_pending_reply_path "$state" "$corr")
255
+ [ ! -e "$rec" ] || return 1
256
+ fi
257
+ now=$(fm_pending_reply_now)
258
+ summary=$(fm_pending_reply_summarize "$request_text")
259
+ status_path="$state/${task_id}.status"
260
+ # Prefer absolute parent_status when parent_home/state resolve.
261
+ case "$status_path" in
262
+ /*) ;;
263
+ *) status_path="$(cd "$state" 2>/dev/null && pwd)/${task_id}.status" ;;
264
+ esac
265
+ case "$parent_home" in
266
+ /*) ;;
267
+ *) parent_home=$(cd "$parent_home" 2>/dev/null && pwd) || parent_home=$1 ;;
268
+ esac
269
+ tmp="$dir/.${corr}.tmp.$$"
270
+ cat > "$tmp" <<EOF
271
+ schema=$FM_PENDING_REPLY_SCHEMA
272
+ corr_id=$corr
273
+ task_id=$task_id
274
+ parent_home=$parent_home
275
+ parent_status=$status_path
276
+ parent_status_scan_signature=
277
+ request_summary=$summary
278
+ created_epoch=$now
279
+ delivered_epoch=
280
+ phase=awaiting_report
281
+ turn_seen_busy=0
282
+ request_turn_completed_epoch=
283
+ recovery_attempted_epoch=
284
+ recovery_sender_pid=
285
+ recovery_sender_identity=
286
+ recovery_sent_epoch=
287
+ recovery_delivery_outcome=
288
+ recovery_turn_seen_busy=0
289
+ recovery_turn_completed_epoch=
290
+ escalated_epoch=
291
+ resolved_epoch=
292
+ resolved_via=
293
+ wrong_home_hits=0
294
+ wrong_home_sightings=
295
+ wrong_home_scan_signature=
296
+ grace_secs=$(fm_pending_reply_grace_secs)
297
+ EOF
298
+ chmod 600 "$tmp" 2>/dev/null || true
299
+ mv -f "$tmp" "$rec" || return 1
300
+ printf '%s' "$corr"
301
+ }
302
+
303
+ # Mark delivery success for an existing expectation. Never resolves.
304
+ fm_pending_reply_mark_delivered() { # <state-dir> <corr_id> [confirmed-epoch]
305
+ local state=$1 corr=$2 confirmed_epoch=${3-} rec phase delivered now
306
+ rec=$(fm_pending_reply_path "$state" "$corr")
307
+ [ -f "$rec" ] || return 1
308
+ phase=$(fm_pending_reply_get "$rec" phase)
309
+ case "$phase" in
310
+ awaiting_report|delivery_unknown|recovery_sending|recovery_sent|escalated|resolved) ;;
311
+ *) return 1 ;;
312
+ esac
313
+ delivered=$(fm_pending_reply_get "$rec" delivered_epoch)
314
+ if [ -z "$delivered" ]; then
315
+ now=${confirmed_epoch:-$(fm_pending_reply_now)}
316
+ fm_pending_reply_set "$rec" delivered_epoch "$now" || return 1
317
+ fi
318
+ if [ "$phase" = delivery_unknown ]; then
319
+ fm_pending_reply_set "$rec" phase awaiting_report || return 1
320
+ fi
321
+ return 0
322
+ }
323
+
324
+ fm_pending_reply_delivery_confirmation_path() { # <state-dir> <corr_id>
325
+ printf '%s/.delivery-confirmed-%s' "$(fm_pending_reply_dir "$1")" "$2"
326
+ }
327
+
328
+ fm_pending_reply_write_delivery_confirmation() { # <state-dir> <corr_id> <state> <value>
329
+ local pending_state=$1 corr=$2 delivery_state=$3 value=$4 marker dir tmp
330
+ marker=$(fm_pending_reply_delivery_confirmation_path "$pending_state" "$corr")
331
+ dir=$(dirname "$marker")
332
+ mkdir -p "$dir" || return 1
333
+ tmp="$marker.tmp.$$"
334
+ printf '%s=%s\n' "$delivery_state" "$value" > "$tmp" || return 1
335
+ chmod 600 "$tmp" 2>/dev/null || true
336
+ mv -f "$tmp" "$marker"
337
+ }
338
+
339
+ fm_pending_reply_prepare_delivery() { # <state-dir> <corr_id>
340
+ local state=$1 corr=$2 rec delivered marker now
341
+ rec=$(fm_pending_reply_path "$state" "$corr")
342
+ [ -f "$rec" ] || return 1
343
+ delivered=$(fm_pending_reply_get "$rec" delivered_epoch)
344
+ [ -z "$delivered" ] || return 0
345
+ marker=$(fm_pending_reply_delivery_confirmation_path "$state" "$corr")
346
+ [ -f "$marker" ] && return 0
347
+ now=$(fm_pending_reply_now)
348
+ fm_pending_reply_write_delivery_confirmation "$state" "$corr" attempted "$now"
349
+ }
350
+
351
+ fm_pending_reply_confirm_delivery() { # <state-dir> <corr_id>
352
+ local state=$1 corr=$2 lock rc=0
353
+ local STATE FM_WAKE_QUEUE FM_WAKE_QUEUE_LOCK
354
+ STATE=$state
355
+ lock="$state/.pending-reply-$corr.lock"
356
+ . "$_FM_PENDING_REPLY_LIB_DIR/fm-wake-lib.sh"
357
+ fm_lock_acquire_wait "$lock" || return 1
358
+ _fm_pending_reply_confirm_delivery_locked "$@" || rc=$?
359
+ fm_lock_release "$lock"
360
+ return "$rc"
361
+ }
362
+
363
+ _fm_pending_reply_confirm_delivery_locked() { # <state-dir> <corr_id>
364
+ local state=$1 corr=$2 now marker
365
+ marker=$(fm_pending_reply_delivery_confirmation_path "$state" "$corr")
366
+ if ! fm_pending_reply_prepare_delivery "$state" "$corr"; then
367
+ return 1
368
+ fi
369
+ now=$(fm_pending_reply_now)
370
+ fm_pending_reply_write_delivery_confirmation "$state" "$corr" confirmed "$now" || return 1
371
+ if fm_pending_reply_mark_delivered "$state" "$corr" "$now"; then
372
+ rm -f "$marker" 2>/dev/null || true
373
+ return 0
374
+ fi
375
+ return 2
376
+ }
377
+
378
+ # Preserve an expectation when a remote transport disconnect makes delivery
379
+ # completion unknowable. This never resolves or retries the request; it moves
380
+ # the existing prepared record to the owner's explicit unknown-delivery path.
381
+ fm_pending_reply_mark_delivery_unknown() { # <state-dir> <corr_id>
382
+ local state=$1 corr=$2 rec phase
383
+ rec=$(fm_pending_reply_path "$state" "$corr")
384
+ [ -f "$rec" ] || return 1
385
+ phase=$(fm_pending_reply_get "$rec" phase)
386
+ case "$phase" in awaiting_report|delivery_unknown) ;;
387
+ *) return 1 ;;
388
+ esac
389
+ fm_pending_reply_set "$rec" phase delivery_unknown
390
+ }
391
+
392
+ _fm_pending_reply_reconcile_delivery_locked() { # <state-dir> <corr_id>
393
+ local state=$1 corr=$2 rec delivered marker entry delivery_state value epoch
394
+ local grace now age phase
395
+ rec=$(fm_pending_reply_path "$state" "$corr")
396
+ [ -f "$rec" ] || return 1
397
+ marker=$(fm_pending_reply_delivery_confirmation_path "$state" "$corr")
398
+ delivered=$(fm_pending_reply_get "$rec" delivered_epoch)
399
+ if [ -n "$delivered" ]; then
400
+ rm -f "$marker" 2>/dev/null || true
401
+ return 0
402
+ fi
403
+ [ -f "$marker" ] || return 1
404
+ entry=$(cat "$marker" 2>/dev/null || true)
405
+ delivery_state=${entry%%=*}
406
+ value=${entry#*=}
407
+ case "$delivery_state" in
408
+ confirmed)
409
+ epoch=$value
410
+ case "$epoch" in ''|*[!0-9]*) return 1 ;; esac
411
+ fm_pending_reply_mark_delivered "$state" "$corr" "$epoch" || return 1
412
+ rm -f "$marker" 2>/dev/null || true
413
+ return 0
414
+ ;;
415
+ attempted)
416
+ epoch=$value
417
+ case "$epoch" in ''|*[!0-9]*) return 1 ;; esac
418
+ grace=$(fm_pending_reply_get "$rec" grace_secs)
419
+ case "$grace" in ''|*[!0-9]*) grace=$(fm_pending_reply_grace_secs) ;; esac
420
+ now=$(fm_pending_reply_now)
421
+ age=$((now - epoch))
422
+ [ "$age" -ge "$grace" ] || return 1
423
+ phase=$(fm_pending_reply_get "$rec" phase)
424
+ [ "$phase" = awaiting_report ] || return 1
425
+ fm_pending_reply_set "$rec" phase delivery_unknown || return 1
426
+ return 0
427
+ ;;
428
+ esac
429
+ return 1
430
+ }
431
+
432
+ fm_pending_reply_reconcile_delivery() { # <state-dir> <corr_id>
433
+ local state=$1 corr=$2 lock rc=0
434
+ local STATE FM_WAKE_QUEUE FM_WAKE_QUEUE_LOCK
435
+ STATE=$state
436
+ lock="$state/.pending-reply-$corr.lock"
437
+ . "$_FM_PENDING_REPLY_LIB_DIR/fm-wake-lib.sh"
438
+ fm_lock_acquire_wait "$lock" || return 1
439
+ _fm_pending_reply_reconcile_delivery_locked "$@" || rc=$?
440
+ fm_lock_release "$lock"
441
+ return "$rc"
442
+ }
443
+
444
+ fm_pending_reply_delivery_attempt_unresolved() { # <state-dir> <corr_id>
445
+ local state=$1 corr=$2 rec delivered marker entry
446
+ rec=$(fm_pending_reply_path "$state" "$corr")
447
+ [ -f "$rec" ] && [ ! -L "$rec" ] || return 1
448
+ delivered=$(fm_pending_reply_get "$rec" delivered_epoch)
449
+ [ -z "$delivered" ] || return 1
450
+ marker=$(fm_pending_reply_delivery_confirmation_path "$state" "$corr")
451
+ [ -f "$marker" ] && [ ! -L "$marker" ] || return 1
452
+ entry=$(cat "$marker" 2>/dev/null || true)
453
+ case "$entry" in attempted=*) return 0 ;; esac
454
+ return 1
455
+ }
456
+
457
+ # A definitive backend rejection makes the existing correlation retryable again.
458
+ # Reconciliation may have aged the same attempted sidecar to delivery_unknown
459
+ # while the backend call was in flight, so both undelivered phases converge here
460
+ # under the per-correlation lock; a confirmed delivery can never be reset.
461
+ fm_pending_reply_reset_known_undelivered() { # <state-dir> <corr_id>
462
+ local state=$1 corr=$2 lock rc=0
463
+ local STATE FM_WAKE_QUEUE FM_WAKE_QUEUE_LOCK
464
+ STATE=$state
465
+ lock="$state/.pending-reply-$corr.lock"
466
+ . "$_FM_PENDING_REPLY_LIB_DIR/fm-wake-lib.sh"
467
+ fm_lock_acquire_wait "$lock" || return 1
468
+ _fm_pending_reply_reset_known_undelivered_locked "$@" || rc=$?
469
+ fm_lock_release "$lock"
470
+ return "$rc"
471
+ }
472
+
473
+ _fm_pending_reply_reset_known_undelivered_locked() { # <state-dir> <corr_id>
474
+ local state=$1 corr=$2 rec delivered phase marker entry
475
+ rec=$(fm_pending_reply_path "$state" "$corr")
476
+ [ -f "$rec" ] && [ ! -L "$rec" ] || return 1
477
+ delivered=$(fm_pending_reply_get "$rec" delivered_epoch)
478
+ [ -z "$delivered" ] || return 1
479
+ phase=$(fm_pending_reply_get "$rec" phase)
480
+ case "$phase" in awaiting_report|delivery_unknown) ;; *) return 1 ;; esac
481
+ marker=$(fm_pending_reply_delivery_confirmation_path "$state" "$corr")
482
+ [ -e "$marker" ] || [ -L "$marker" ] || {
483
+ [ "$phase" = awaiting_report ]
484
+ return $?
485
+ }
486
+ [ -f "$marker" ] && [ ! -L "$marker" ] || return 1
487
+ entry=$(cat "$marker" 2>/dev/null || true)
488
+ case "$entry" in attempted=*) ;; *) return 1 ;; esac
489
+ [ "$phase" = awaiting_report ] \
490
+ || fm_pending_reply_set "$rec" phase awaiting_report || return 1
491
+ rm -f -- "$marker"
492
+ }
493
+
494
+ # Drop an undelivered expectation after a failed send so transport failure does
495
+ # not masquerade as a missed report later.
496
+ fm_pending_reply_discard_undelivered() { # <state-dir> <corr_id>
497
+ local state=$1 corr=$2 rec delivered marker
498
+ rec=$(fm_pending_reply_path "$state" "$corr")
499
+ [ -f "$rec" ] || return 0
500
+ delivered=$(fm_pending_reply_get "$rec" delivered_epoch)
501
+ [ -z "$delivered" ] || return 1
502
+ marker=$(fm_pending_reply_delivery_confirmation_path "$state" "$corr")
503
+ rm -f "$marker" 2>/dev/null || true
504
+ rm -f "$rec"
505
+ }
506
+
507
+ # 0 if a status line is a correlated acknowledgement for <corr_id>.
508
+ # Accepts short status replies and status lines that point at a document.
509
+ # Unrelated verbs without the token never match. Stale/wrong corr never match.
510
+ # The parent's own pending-reply-missed escalation line must not self-resolve:
511
+ # it names the request with pending-reply-id= rather than corr=.
512
+ fm_pending_reply_line_resolves() { # <line> <corr_id>
513
+ local line=$1 corr=$2
514
+ [ -n "$line" ] && [ -n "$corr" ] || return 1
515
+ case "$line" in
516
+ *pending-reply-missed*) return 1 ;;
517
+ esac
518
+ fm_pending_reply_text_has_corr "$line" "$corr"
519
+ }
520
+
521
+ # Scan a status file for a correlated resolve. Prints the matching line or empty.
522
+ fm_pending_reply_find_resolve_line() { # <status-file> <corr_id>
523
+ local status_file=$1 corr=$2 line
524
+ [ -f "$status_file" ] || return 0
525
+ while IFS= read -r line || [ -n "$line" ]; do
526
+ [ -n "$line" ] || continue
527
+ if fm_pending_reply_line_resolves "$line" "$corr"; then
528
+ printf '%s' "$line"
529
+ return 0
530
+ fi
531
+ done < "$status_file"
532
+ return 0
533
+ }
534
+
535
+ fm_pending_reply_file_signature() { # <path>
536
+ local path=$1
537
+ [ -f "$path" ] || { printf 'missing'; return 0; }
538
+ if [ "$(uname -s 2>/dev/null)" = Darwin ]; then
539
+ LC_ALL=C stat -f '%d:%i:%z:%m:%c' "$path" 2>/dev/null || printf 'unreadable'
540
+ else
541
+ LC_ALL=C stat -c '%d:%i:%s:%Y:%Z' "$path" 2>/dev/null || printf 'unreadable'
542
+ fi
543
+ }
544
+
545
+ fm_pending_reply_status_set_signature() { # <status-dir>
546
+ local status_dir=$1 status_file signature
547
+ {
548
+ for status_file in "$status_dir"/*.status; do
549
+ [ -f "$status_file" ] || continue
550
+ signature=$(fm_pending_reply_file_signature "$status_file")
551
+ printf '%s:%s:%s\n' "${#status_file}" "$status_file" "$signature"
552
+ done
553
+ } | cksum 2>/dev/null | awk '{printf "%s-%s", $1, $2}'
554
+ }
555
+
556
+ # Classify how a resolving line acknowledged the request.
557
+ fm_pending_reply_resolve_via_of_line() { # <line>
558
+ local line=$1
559
+ case "$line" in
560
+ *data/*report*|*report.md*|*document*|*pointer*)
561
+ printf 'document'
562
+ ;;
563
+ *via-helper*|*fm-secondmate-report*)
564
+ printf 'helper'
565
+ ;;
566
+ *)
567
+ printf 'status'
568
+ ;;
569
+ esac
570
+ }
571
+
572
+ # Idempotently resolve an expectation from a correlated parent report.
573
+ # Returns 0 when the record is resolved after the call (already or newly).
574
+ fm_pending_reply_try_resolve() { # <state-dir> <corr_id> [status-file-override]
575
+ # Serialized per correlation so a resolution and an escalation cannot interleave.
576
+ # bin/fm-wake-lib.sh owns the lock primitives but assigns its own globals when
577
+ # sourced, so they are declared local here: that contains them to this call
578
+ # instead of leaking into every script that sources this library, without the
579
+ # subshell that would make every later use of them read as a lost write.
580
+ # The lock is released explicitly rather than from an EXIT trap, because a trap
581
+ # in a plain function would clobber the caller's own.
582
+ local state=$1 corr=$2 lock rc=0
583
+ local STATE FM_WAKE_QUEUE FM_WAKE_QUEUE_LOCK
584
+ STATE=$state
585
+ lock="$state/.pending-reply-$corr.lock"
586
+ # shellcheck source=bin/fm-wake-lib.sh
587
+ . "$_FM_PENDING_REPLY_LIB_DIR/fm-wake-lib.sh"
588
+ fm_lock_acquire_wait "$lock" || return 1
589
+ _fm_pending_reply_try_resolve_locked "$@" || rc=$?
590
+ fm_lock_release "$lock"
591
+ return "$rc"
592
+ }
593
+
594
+ _fm_pending_reply_try_resolve_locked() { # <state-dir> <corr_id> [status-file-override]
595
+ local state=$1 corr=$2 status_override=${3-}
596
+ local rec phase delivered marker delivery_entry delivery_state status_file signature previous line via now
597
+ local unconfirmed=0
598
+ rec=$(fm_pending_reply_path "$state" "$corr")
599
+ [ -f "$rec" ] || return 1
600
+ phase=$(fm_pending_reply_get "$rec" phase)
601
+ if [ "$phase" = resolved ]; then
602
+ _fm_pending_reply_close_escalation_locked "$state" "$corr" || true
603
+ return 0
604
+ fi
605
+ delivered=$(fm_pending_reply_get "$rec" delivered_epoch)
606
+ if [ -z "$delivered" ]; then
607
+ marker=$(fm_pending_reply_delivery_confirmation_path "$state" "$corr")
608
+ [ -f "$marker" ] || return 1
609
+ delivery_entry=$(cat "$marker" 2>/dev/null || true)
610
+ delivery_state=${delivery_entry%%=*}
611
+ case "$delivery_state" in attempted|confirmed) ;; *) return 1 ;; esac
612
+ unconfirmed=1
613
+ fi
614
+ status_file=${status_override:-$(fm_pending_reply_get "$rec" parent_status)}
615
+ if [ -z "$status_override" ] && [ "$unconfirmed" = 0 ]; then
616
+ signature=$(fm_pending_reply_file_signature "$status_file")
617
+ previous=$(fm_pending_reply_get "$rec" parent_status_scan_signature)
618
+ [ "$signature" != "$previous" ] || return 1
619
+ fi
620
+ line=$(fm_pending_reply_find_resolve_line "$status_file" "$corr")
621
+ if [ -z "$line" ]; then
622
+ if [ -z "$status_override" ] && [ "$unconfirmed" = 0 ]; then
623
+ fm_pending_reply_set "$rec" parent_status_scan_signature "$signature" || return 1
624
+ fi
625
+ return 1
626
+ fi
627
+ via=$(fm_pending_reply_resolve_via_of_line "$line")
628
+ now=$(fm_pending_reply_now)
629
+ fm_pending_reply_set "$rec" phase resolved || return 1
630
+ if [ -z "$delivered" ]; then
631
+ fm_pending_reply_mark_delivered "$state" "$corr" "$now" || return 1
632
+ rm -f "$marker" 2>/dev/null || true
633
+ fi
634
+ fm_pending_reply_set "$rec" resolved_epoch "$now" || return 1
635
+ fm_pending_reply_set "$rec" resolved_via "$via" || return 1
636
+ # The record is resolved either way; a failed close stays retryable from the
637
+ # watcher tick rather than turning a settled request back into a failure.
638
+ _fm_pending_reply_close_escalation_locked "$state" "$corr" || true
639
+ return 0
640
+ }
641
+
642
+ # Observe backend busy/idle evidence for the active turn without reading chat.
643
+ # busy_state must be one of: busy | idle | unknown.
644
+ fm_pending_reply_observe_busy() { # <state-dir> <corr_id> <busy_state>
645
+ local state=$1 corr=$2 busy_state=$3
646
+ local rec phase delivered now seen completed field_seen field_completed
647
+ rec=$(fm_pending_reply_path "$state" "$corr")
648
+ [ -f "$rec" ] || return 1
649
+ phase=$(fm_pending_reply_get "$rec" phase)
650
+ case "$phase" in
651
+ awaiting_report|recovery_sent) ;;
652
+ *) return 0 ;;
653
+ esac
654
+ delivered=$(fm_pending_reply_get "$rec" delivered_epoch)
655
+ [ -n "$delivered" ] || return 0
656
+ if [ "$phase" = awaiting_report ]; then
657
+ field_seen=turn_seen_busy
658
+ field_completed=request_turn_completed_epoch
659
+ else
660
+ field_seen=recovery_turn_seen_busy
661
+ field_completed=recovery_turn_completed_epoch
662
+ fi
663
+ seen=$(fm_pending_reply_get "$rec" "$field_seen")
664
+ completed=$(fm_pending_reply_get "$rec" "$field_completed")
665
+ case "$busy_state" in
666
+ busy)
667
+ if [ "$seen" != 1 ]; then
668
+ fm_pending_reply_set "$rec" "$field_seen" 1 || return 1
669
+ fi
670
+ ;;
671
+ idle)
672
+ if [ -z "$completed" ]; then
673
+ # Prefer a busy->idle transition. Also accept a pure idle after delivery
674
+ # when the first observation already missed the busy window (fast turns).
675
+ if [ "$seen" = 1 ] || [ "$seen" = 0 ]; then
676
+ now=$(fm_pending_reply_now)
677
+ fm_pending_reply_set "$rec" "$field_completed" "$now" || return 1
678
+ fi
679
+ fi
680
+ ;;
681
+ unknown)
682
+ # No independent proof; leave completion unset.
683
+ ;;
684
+ *)
685
+ return 2
686
+ ;;
687
+ esac
688
+ return 0
689
+ }
690
+
691
+ fm_pending_reply_fallback_idle_eligible() { # <record-path>
692
+ local rec=$1 phase start seen grace now age
693
+ phase=$(fm_pending_reply_get "$rec" phase)
694
+ case "$phase" in
695
+ awaiting_report)
696
+ start=$(fm_pending_reply_get "$rec" delivered_epoch)
697
+ seen=$(fm_pending_reply_get "$rec" turn_seen_busy)
698
+ ;;
699
+ recovery_sent)
700
+ start=$(fm_pending_reply_get "$rec" recovery_sent_epoch)
701
+ seen=$(fm_pending_reply_get "$rec" recovery_turn_seen_busy)
702
+ ;;
703
+ *) return 1 ;;
704
+ esac
705
+ [ "$seen" = 1 ] && return 0
706
+ grace=$(fm_pending_reply_get "$rec" grace_secs)
707
+ case "$start" in ''|*[!0-9]*) return 1 ;; esac
708
+ case "$grace" in ''|*[!0-9]*) grace=$(fm_pending_reply_grace_secs) ;; esac
709
+ now=$(fm_pending_reply_now)
710
+ age=$((now - start))
711
+ [ "$age" -ge "$grace" ]
712
+ }
713
+
714
+ # fm_pending_reply_backend_observation: one busy/idle observation of a
715
+ # SECONDMATE endpoint, without ever reading its conversation.
716
+ #
717
+ # Deliberately NOT the semantic busy-state contract (bin/fm-busy-lib.sh).
718
+ # That contract covers ordinary task workers, whose turn lifecycle firstmate
719
+ # wires at spawn; a secondmate has no such wiring because an idle secondmate
720
+ # pane is healthy and it runs no supervised turn sequence of its own. This
721
+ # observation exists only to notice a busy-then-idle transition around one
722
+ # delivered request, so it is a delivery-confirmation signal in the same
723
+ # category as the submit acknowledgement matcher in bin/fm-composer-lib.sh - never task
724
+ # state, and never a source consumers can confuse with semantic state.
725
+ #
726
+ # It stays harness-scoped (fm_busy_lines_match with the recorded harness, no
727
+ # global OR of every vendor signature), so one harness's output cannot make
728
+ # another read busy, and a weak rendered idle degrades to `fallback-idle`,
729
+ # which the caller accepts as idle only after its grace window.
730
+ fm_pending_reply_backend_observation() { # <backend> <target> [expected-label] [harness]
731
+ local backend=$1 target=$2 expected_label=${3-} harness=${4-} native tail40
732
+ native=$(fm_backend_busy_state "$backend" "$target" 2>/dev/null || printf 'unknown')
733
+ case "$native" in
734
+ busy|idle) printf '%s' "$native"; return 0 ;;
735
+ esac
736
+ tail40=$(fm_backend_capture "$backend" "$target" 40 "$expected_label" 2>/dev/null) \
737
+ || { printf 'unknown'; return 0; }
738
+ if printf '%s' "$tail40" | grep -v '^[[:space:]]*$' | tail -6 \
739
+ | fm_busy_lines_match "$harness"; then
740
+ printf 'busy'
741
+ else
742
+ printf 'fallback-idle'
743
+ fi
744
+ }
745
+
746
+ fm_pending_reply_busy_state_from_observation() { # <record-path> <observation>
747
+ local rec=$1 observation=$2
748
+ case "$observation" in
749
+ busy|idle|unknown) printf '%s' "$observation" ;;
750
+ fallback-idle)
751
+ if fm_pending_reply_fallback_idle_eligible "$rec"; then
752
+ printf 'idle'
753
+ else
754
+ printf 'unknown'
755
+ fi
756
+ ;;
757
+ *) printf 'unknown' ;;
758
+ esac
759
+ }
760
+
761
+ # Explicit turn-completion proof (for tests and turn-end backends that surface
762
+ # a completion event without a busy/idle pair).
763
+ fm_pending_reply_mark_turn_completed() { # <state-dir> <corr_id> [which: request|recovery]
764
+ local state=$1 corr=$2 which=${3:-request}
765
+ local rec phase field now
766
+ rec=$(fm_pending_reply_path "$state" "$corr")
767
+ [ -f "$rec" ] || return 1
768
+ phase=$(fm_pending_reply_get "$rec" phase)
769
+ case "$which" in
770
+ request) field=request_turn_completed_epoch ;;
771
+ recovery) field=recovery_turn_completed_epoch ;;
772
+ *) return 2 ;;
773
+ esac
774
+ now=$(fm_pending_reply_now)
775
+ fm_pending_reply_set "$rec" "$field" "$now" || return 1
776
+ # Keep phase consistent with which turn completed.
777
+ if [ "$which" = recovery ] && [ "$phase" = awaiting_report ]; then
778
+ : # recovery completion only meaningful after recovery_sent
779
+ fi
780
+ return 0
781
+ }
782
+
783
+ # --- remote reply-channel freshness -----------------------------------------
784
+ #
785
+ # A LOCAL secondmate appends its report straight into the parent's
786
+ # state/<id>.status, so an absent correlated line there is immediate evidence
787
+ # that no report was written. A REMOTE mate's reports reach that same file only
788
+ # through the asynchronous mirror in bin/fm-procevent-remote-reply.sh, so the
789
+ # same absence proves nothing until that mirror has actually been read past the
790
+ # turn that should have produced the report. Without this distinction the guard
791
+ # nags a REPOST REQUIRED for a reply the mate did write and the parent simply
792
+ # had not received yet - the common case, because the mirror's poll window is
793
+ # comparable to the recovery grace.
794
+ #
795
+ # The mirror therefore publishes one watermark: the epoch at which it last knew
796
+ # it had read the remote log through its end. Only that adapter writes it (it
797
+ # owns the channel), and only this library reads it. A channel that is behind,
798
+ # unarmed, or broken simply never advances the watermark, so the request stays
799
+ # durably open and un-nagged; the mirror escalates its own continuity failures.
800
+ fm_pending_reply_remote_channel_watermark_path() { # <state-dir> <task_id>
801
+ printf '%s/remote-replies/%s.caught-up' "$1" "$2"
802
+ }
803
+
804
+ # Record that the mirrored remote reply log for <task_id> was read through its
805
+ # end at <epoch> (default now). Called only by the remote reply adapter.
806
+ fm_pending_reply_note_remote_channel_caught_up() { # <state-dir> <task_id> [epoch]
807
+ local state=$1 task_id=$2 epoch=${3-} path dir tmp
808
+ [ -n "$state" ] && [ -n "$task_id" ] || return 2
809
+ case "$epoch" in ''|*[!0-9]*) epoch=$(fm_pending_reply_now) ;; esac
810
+ path=$(fm_pending_reply_remote_channel_watermark_path "$state" "$task_id")
811
+ dir=$(dirname "$path")
812
+ mkdir -p "$dir" || return 1
813
+ chmod 700 "$dir" 2>/dev/null || true
814
+ [ ! -L "$path" ] || return 1
815
+ tmp="$dir/.caught-up.$task_id.$$"
816
+ printf 'caught_up_epoch=%s\n' "$epoch" > "$tmp" || { rm -f -- "$tmp"; return 1; }
817
+ chmod 600 "$tmp" 2>/dev/null || true
818
+ mv -f -- "$tmp" "$path"
819
+ }
820
+
821
+ # Print the watermark epoch, or nothing when the channel never reported itself
822
+ # caught up. Never invents a value.
823
+ fm_pending_reply_remote_channel_epoch() { # <state-dir> <task_id>
824
+ local path epoch
825
+ path=$(fm_pending_reply_remote_channel_watermark_path "$1" "$2")
826
+ [ -f "$path" ] && [ ! -L "$path" ] || return 0
827
+ epoch=$(sed -n 's/^caught_up_epoch=//p' "$path" 2>/dev/null | head -1)
828
+ case "$epoch" in ''|*[!0-9]*) return 0 ;; esac
829
+ printf '%s' "$epoch"
830
+ }
831
+
832
+ # 0 when <task_id> is a secondmate whose reports cross a machine boundary.
833
+ fm_pending_reply_target_is_remote() { # <state-dir> <task_id>
834
+ local meta="$1/$2.meta"
835
+ [ -f "$meta" ] || return 1
836
+ [ -n "$(fm_meta_get "$meta" remote_host)" ]
837
+ }
838
+
839
+ # 0 when "no correlated report in the parent status log" is admissible evidence
840
+ # that the mate never reported: always for a local target, and for a remote one
841
+ # only once the mirror has been read through its end at or after <since-epoch>.
842
+ fm_pending_reply_missing_report_is_evidence() { # <state-dir> <task_id> <since-epoch>
843
+ local state=$1 task_id=$2 since=$3 caught
844
+ fm_pending_reply_target_is_remote "$state" "$task_id" || return 0
845
+ case "$since" in ''|*[!0-9]*) return 1 ;; esac
846
+ caught=$(fm_pending_reply_remote_channel_epoch "$state" "$task_id")
847
+ [ -n "$caught" ] || return 1
848
+ [ "$caught" -ge "$since" ]
849
+ }
850
+
851
+ # Build the one automatic recovery message for a pending record.
852
+ fm_pending_reply_recovery_message() { # <record-path>
853
+ local rec=$1 corr summary token msg
854
+ corr=$(fm_pending_reply_get "$rec" corr_id)
855
+ summary=$(fm_pending_reply_get "$rec" request_summary)
856
+ token=$(fm_pending_reply_corr_token "$corr")
857
+ msg="REPOST REQUIRED: previous marked request had no correlated parent report. Reply on the parent status channel including ${token}. Original request: ${summary}"
858
+ fm_pending_reply_embed_corr "$msg" "$corr" msg
859
+ printf '%s' "$msg"
860
+ }
861
+
862
+ # Deliver the recovery message once. Caller must hold phase awaiting_report with
863
+ # turn completed and grace elapsed. Uses FM_PENDING_REPLY_SEND_HOOK when set
864
+ # (tests), otherwise invokes fm-send with FM_PENDING_REPLY_EXISTING_CORR so a
865
+ # second expectation is not created.
866
+ fm_pending_reply_send_recovery() { # <state-dir> <corr_id>
867
+ local state=$1 corr=$2
868
+ local rec phase completed delivered attempted grace now age task_id msg parent_home send_status=0
869
+ local sender_pid sender_identity
870
+ rec=$(fm_pending_reply_path "$state" "$corr")
871
+ [ -f "$rec" ] || return 1
872
+ phase=$(fm_pending_reply_get "$rec" phase)
873
+ [ "$phase" = awaiting_report ] || return 1
874
+ attempted=$(fm_pending_reply_get "$rec" recovery_attempted_epoch)
875
+ if [ -n "$attempted" ]; then
876
+ fm_pending_reply_reconcile_recovery "$state" "$corr" || true
877
+ return 1
878
+ fi
879
+ completed=$(fm_pending_reply_get "$rec" request_turn_completed_epoch)
880
+ [ -n "$completed" ] || return 1
881
+ delivered=$(fm_pending_reply_get "$rec" delivered_epoch)
882
+ [ -n "$delivered" ] || return 1
883
+ grace=$(fm_pending_reply_get "$rec" grace_secs)
884
+ case "$grace" in ''|*[!0-9]*) grace=$(fm_pending_reply_grace_secs) ;; esac
885
+ now=$(fm_pending_reply_now)
886
+ age=$((now - delivered))
887
+ [ "$age" -ge "$grace" ] || return 1
888
+ task_id=$(fm_pending_reply_get "$rec" task_id)
889
+ # A remote mate's report may exist and simply not have been mirrored yet.
890
+ fm_pending_reply_missing_report_is_evidence "$state" "$task_id" "$completed" || return 1
891
+ parent_home=$(fm_pending_reply_get "$rec" parent_home)
892
+ msg=$(fm_pending_reply_recovery_message "$rec")
893
+ sender_pid=${BASHPID:-$$}
894
+ sender_identity=$(fm_pending_reply_pid_identity "$sender_pid") || return 1
895
+ fm_pending_reply_set "$rec" recovery_sender_pid "$sender_pid" || return 1
896
+ fm_pending_reply_set "$rec" recovery_sender_identity "$sender_identity" || return 1
897
+ fm_pending_reply_set "$rec" recovery_attempted_epoch "$now" || return 1
898
+ fm_pending_reply_set "$rec" phase recovery_sending || return 1
899
+ if [ -n "${FM_PENDING_REPLY_SEND_HOOK:-}" ]; then
900
+ # Hook receives: task_id message
901
+ # shellcheck disable=SC2086
902
+ if ! eval "$FM_PENDING_REPLY_SEND_HOOK" "$(printf '%q' "$task_id")" "$(printf '%q' "$msg")"; then
903
+ send_status=1
904
+ fi
905
+ else
906
+ if [ -z "$parent_home" ] || [ ! -d "$parent_home" ]; then
907
+ send_status=1
908
+ elif ! env FM_HOME="$parent_home" FM_PENDING_REPLY_EXISTING_CORR="$corr" \
909
+ "$_FM_PENDING_REPLY_LIB_DIR/fm-send.sh" "$task_id" "$msg"; then
910
+ send_status=1
911
+ fi
912
+ fi
913
+ if [ "$send_status" = 0 ]; then
914
+ fm_pending_reply_finish_recovery "$state" "$corr" confirmed
915
+ return $?
916
+ fi
917
+ fm_pending_reply_finish_recovery "$state" "$corr" failed || return 1
918
+ return 1
919
+ }
920
+
921
+ fm_pending_reply_pid_identity() { # <pid>
922
+ local pid=$1 identity
923
+ case "$pid" in ''|*[!0-9]*) return 1 ;; esac
924
+ identity=$(COLUMNS=10000 LC_ALL=C ps -p "$pid" -o lstart= -o command= 2>/dev/null) || return 1
925
+ [ -n "$identity" ] || return 1
926
+ printf '%s' "$identity"
927
+ }
928
+
929
+ fm_pending_reply_sender_alive() { # <record-path>
930
+ local rec=$1 pid expected actual
931
+ pid=$(fm_pending_reply_get "$rec" recovery_sender_pid)
932
+ expected=$(fm_pending_reply_get "$rec" recovery_sender_identity)
933
+ [ -n "$expected" ] || return 1
934
+ actual=$(fm_pending_reply_pid_identity "$pid") || return 1
935
+ [ "$actual" = "$expected" ]
936
+ }
937
+
938
+ fm_pending_reply_finish_recovery() { # <state-dir> <corr_id> <confirmed|failed>
939
+ local state=$1 corr=$2 outcome=$3 rec phase now sent
940
+ rec=$(fm_pending_reply_path "$state" "$corr")
941
+ [ -f "$rec" ] || return 1
942
+ phase=$(fm_pending_reply_get "$rec" phase)
943
+ [ "$phase" = recovery_sending ] || return 1
944
+ fm_pending_reply_set "$rec" recovery_delivery_outcome "$outcome" || return 1
945
+ if [ "$outcome" = confirmed ]; then
946
+ sent=$(fm_pending_reply_get "$rec" recovery_sent_epoch)
947
+ if [ -z "$sent" ]; then
948
+ now=$(fm_pending_reply_now)
949
+ fm_pending_reply_set "$rec" recovery_sent_epoch "$now" || return 1
950
+ fi
951
+ fm_pending_reply_set "$rec" recovery_turn_seen_busy 0 || return 1
952
+ fm_pending_reply_set "$rec" recovery_turn_completed_epoch "" || return 1
953
+ fm_pending_reply_set "$rec" phase recovery_sent || return 1
954
+ else
955
+ [ "$outcome" = failed ] || return 1
956
+ fm_pending_reply_set "$rec" phase recovery_failed || return 1
957
+ fi
958
+ }
959
+
960
+ fm_pending_reply_reconcile_recovery() { # <state-dir> <corr_id>
961
+ local state=$1 corr=$2 rec phase attempted outcome
962
+ rec=$(fm_pending_reply_path "$state" "$corr")
963
+ [ -f "$rec" ] || return 1
964
+ phase=$(fm_pending_reply_get "$rec" phase)
965
+ case "$phase" in awaiting_report|recovery_sending) ;; *) return 1 ;; esac
966
+ attempted=$(fm_pending_reply_get "$rec" recovery_attempted_epoch)
967
+ [ -n "$attempted" ] || return 1
968
+ case "$attempted" in *[!0-9]*) return 1 ;; esac
969
+ outcome=$(fm_pending_reply_get "$rec" recovery_delivery_outcome)
970
+ case "$outcome" in
971
+ confirmed) fm_pending_reply_finish_recovery "$state" "$corr" confirmed; return $? ;;
972
+ failed) fm_pending_reply_finish_recovery "$state" "$corr" failed; return $? ;;
973
+ unknown)
974
+ fm_pending_reply_set "$rec" phase recovery_unknown || return 1
975
+ return 0
976
+ ;;
977
+ esac
978
+ fm_pending_reply_sender_alive "$rec" && return 1
979
+ fm_pending_reply_set "$rec" recovery_delivery_outcome unknown || return 1
980
+ fm_pending_reply_set "$rec" phase recovery_unknown || return 1
981
+ }
982
+
983
+ # The decision key an escalation for <corr_id> opens in the parent status log.
984
+ # Per-request rather than the bare default key, so one request's escalation
985
+ # neither masks nor is masked by an unrelated decision on the same task.
986
+ fm_pending_reply_escalation_key() { # <corr_id>
987
+ printf 'pending-reply-%s' "$1"
988
+ }
989
+
990
+ fm_pending_reply_escalation_payload() { # <record-path> <kind>
991
+ local rec=$1 kind=$2 task_id corr summary outcome token
992
+ task_id=$(fm_pending_reply_get "$rec" task_id)
993
+ corr=$(fm_pending_reply_get "$rec" corr_id)
994
+ summary=$(fm_pending_reply_get "$rec" request_summary)
995
+ [ -n "$task_id" ] && [ -n "$corr" ] || return 1
996
+ case "$kind" in
997
+ missed)
998
+ token=pending-reply-missed
999
+ ;;
1000
+ delivery-unknown)
1001
+ token=pending-reply-delivery-unknown
1002
+ ;;
1003
+ recovery-delivery)
1004
+ outcome=$(fm_pending_reply_get "$rec" recovery_delivery_outcome)
1005
+ case "$outcome" in failed|unknown) ;; *) return 1 ;; esac
1006
+ token="pending-reply-recovery-delivery-$outcome"
1007
+ ;;
1008
+ *) return 1 ;;
1009
+ esac
1010
+ printf '%s: task=%s pending-reply-id=%s request=%s' "$token" "$task_id" "$corr" "$summary"
1011
+ }
1012
+
1013
+ # The escalation line this library published for <corr_id>, or empty. A legacy
1014
+ # unkeyed escalation is matched and closed under the shared default key while
1015
+ # that exact escalation remains open. If an unrelated decision has since taken
1016
+ # over that key, the close is withheld so the unrelated decision is not cleared.
1017
+ fm_pending_reply_escalation_line() { # <status-file> <record-path> <corr_id>
1018
+ local status_file=$1 rec=$2 corr=$3 line found='' kind payload own_key
1019
+ [ -f "$status_file" ] || return 0
1020
+ [ "$(fm_pending_reply_get "$rec" corr_id)" = "$corr" ] || return 0
1021
+ own_key=$(fm_pending_reply_escalation_key "$corr")
1022
+ while IFS= read -r line || [ -n "$line" ]; do
1023
+ [ "$(status_line_verb "$line")" = blocked ] || continue
1024
+ for kind in missed delivery-unknown recovery-delivery; do
1025
+ payload=$(fm_pending_reply_escalation_payload "$rec" "$kind") || continue
1026
+ case "$line" in
1027
+ "blocked [key=$own_key]: $payload"|"blocked: $payload") found=$line; break ;;
1028
+ esac
1029
+ done
1030
+ done < "$status_file"
1031
+ printf '%s' "$found"
1032
+ }
1033
+
1034
+ # Close the durable status decision a previous escalation opened for <corr_id>.
1035
+ # Idempotent, and safe to retry until it succeeds: it appends the closing line
1036
+ # only while that exact keyed decision is still open in
1037
+ # bin/fm-classify-lib.sh's fold. Records that never escalated are left untouched.
1038
+ fm_pending_reply_close_escalation() { # <state-dir> <corr_id>
1039
+ # Serialized per correlation so a resolution and an escalation cannot interleave.
1040
+ # bin/fm-wake-lib.sh owns the lock primitives but assigns its own globals when
1041
+ # sourced, so they are declared local here: that contains them to this call
1042
+ # instead of leaking into every script that sources this library, without the
1043
+ # subshell that would make every later use of them read as a lost write.
1044
+ # The lock is released explicitly rather than from an EXIT trap, because a trap
1045
+ # in a plain function would clobber the caller's own.
1046
+ local state=$1 corr=$2 lock rc=0
1047
+ local STATE FM_WAKE_QUEUE FM_WAKE_QUEUE_LOCK
1048
+ STATE=$state
1049
+ lock="$state/.pending-reply-$corr.lock"
1050
+ # shellcheck source=bin/fm-wake-lib.sh
1051
+ . "$_FM_PENDING_REPLY_LIB_DIR/fm-wake-lib.sh"
1052
+ fm_lock_acquire_wait "$lock" || return 1
1053
+ _fm_pending_reply_close_escalation_locked "$@" || rc=$?
1054
+ fm_lock_release "$lock"
1055
+ return "$rc"
1056
+ }
1057
+
1058
+ _fm_pending_reply_close_escalation_locked() { # <state-dir> <corr_id>
1059
+ local state=$1 corr=$2 rec escalated closed parent_status escalation key note
1060
+ local open_line open_key open_note now close_line close_rc
1061
+ rec=$(fm_pending_reply_path "$state" "$corr")
1062
+ [ -f "$rec" ] || return 1
1063
+ [ "$(fm_pending_reply_get "$rec" phase)" = resolved ] || return 0
1064
+ escalated=$(fm_pending_reply_get "$rec" escalated_epoch)
1065
+ [ -n "$escalated" ] || return 0
1066
+ closed=$(fm_pending_reply_get "$rec" escalation_closed_epoch)
1067
+ [ -z "$closed" ] || return 0
1068
+ parent_status=$(fm_pending_reply_get "$rec" parent_status)
1069
+ [ -n "$parent_status" ] || return 1
1070
+ escalation=$(fm_pending_reply_escalation_line "$parent_status" "$rec" "$corr")
1071
+ if [ -n "$escalation" ]; then
1072
+ key=$(_fm_decision_key "$escalation") || key=''
1073
+ note=$(status_line_note "$escalation")
1074
+ while IFS= read -r open_line; do
1075
+ [ -n "$open_line" ] || continue
1076
+ open_key=${open_line%%$'\t'*}
1077
+ [ "$open_key" = "$key" ] || continue
1078
+ open_note=${open_line#*$'\t'}
1079
+ open_note=${open_note#*$'\t'}
1080
+ [ "$open_note" = "$note" ] || continue
1081
+ # This close is the home's own bookkeeping, written by the same resolve
1082
+ # or tick that already consumed the reply, so it uses the guarded
1083
+ # self-announced append (bin/fm-wake-lib.sh, sourced by this function's
1084
+ # wrappers) and does not wake the home that wrote it; the escalation
1085
+ # OPEN above stays a plain append because a new blocker must wake.
1086
+ close_line=$(printf 'resolved [key=%s]: pending-reply-resolved: task=%s pending-reply-id=%s via=%s' \
1087
+ "$key" "$(fm_pending_reply_get "$rec" task_id)" "$corr" \
1088
+ "$(fm_pending_reply_get "$rec" resolved_via)")
1089
+ close_rc=0
1090
+ fm_wake_status_append_self_announced "${parent_status%/*}" "$parent_status" "$close_line" \
1091
+ 2>/dev/null || close_rc=$?
1092
+ [ "$close_rc" -ne 2 ] || return 1
1093
+ break
1094
+ done <<EOF
1095
+ $(status_open_decisions "$parent_status")
1096
+ EOF
1097
+ fi
1098
+ now=$(fm_pending_reply_now)
1099
+ fm_pending_reply_set "$rec" escalation_closed_epoch "$now"
1100
+ }
1101
+
1102
+ # Escalate once after a missed recovery report or failed delivery outcome.
1103
+ # Retains the durable unresolved record. Never loops.
1104
+ fm_pending_reply_maybe_escalate() { # <state-dir> <corr_id>
1105
+ # Serialized per correlation so a resolution and an escalation cannot interleave.
1106
+ # bin/fm-wake-lib.sh owns the lock primitives but assigns its own globals when
1107
+ # sourced, so they are declared local here: that contains them to this call
1108
+ # instead of leaking into every script that sources this library, without the
1109
+ # subshell that would make every later use of them read as a lost write.
1110
+ # The lock is released explicitly rather than from an EXIT trap, because a trap
1111
+ # in a plain function would clobber the caller's own.
1112
+ local state=$1 corr=$2 lock rc=0
1113
+ local STATE FM_WAKE_QUEUE FM_WAKE_QUEUE_LOCK
1114
+ STATE=$state
1115
+ lock="$state/.pending-reply-$corr.lock"
1116
+ # shellcheck source=bin/fm-wake-lib.sh
1117
+ . "$_FM_PENDING_REPLY_LIB_DIR/fm-wake-lib.sh"
1118
+ fm_lock_acquire_wait "$lock" || return 1
1119
+ _fm_pending_reply_maybe_escalate_locked "$@" || rc=$?
1120
+ fm_lock_release "$lock"
1121
+ return "$rc"
1122
+ }
1123
+
1124
+ _fm_pending_reply_maybe_escalate_locked() { # <state-dir> <corr_id>
1125
+ local state=$1 corr=$2
1126
+ local rec phase completed now payload parent_status line kind
1127
+ rec=$(fm_pending_reply_path "$state" "$corr")
1128
+ [ -f "$rec" ] || return 1
1129
+ phase=$(fm_pending_reply_get "$rec" phase)
1130
+ if [ "$phase" = delivery_unknown ]; then
1131
+ _fm_pending_reply_reconcile_delivery_locked "$state" "$corr" || true
1132
+ phase=$(fm_pending_reply_get "$rec" phase)
1133
+ [ "$phase" = delivery_unknown ] || return 0
1134
+ fi
1135
+ case "$phase" in
1136
+ recovery_sent)
1137
+ completed=$(fm_pending_reply_get "$rec" recovery_turn_completed_epoch)
1138
+ [ -n "$completed" ] || return 1
1139
+ # Same reply-channel evidence rule the recovery repost obeys: a missing
1140
+ # correlated report is not a missed report until the mirror caught up.
1141
+ fm_pending_reply_missing_report_is_evidence "$state" \
1142
+ "$(fm_pending_reply_get "$rec" task_id)" "$completed" || return 1
1143
+ ;;
1144
+ delivery_unknown|recovery_failed|recovery_unknown) ;;
1145
+ *) return 1 ;;
1146
+ esac
1147
+ # Resolve wins if a late report arrived between completion and this call.
1148
+ if _fm_pending_reply_try_resolve_locked "$state" "$corr"; then
1149
+ return 0
1150
+ fi
1151
+ parent_status=$(fm_pending_reply_get "$rec" parent_status)
1152
+ case "$phase" in
1153
+ delivery_unknown) kind=delivery-unknown ;;
1154
+ recovery_failed|recovery_unknown) kind=recovery-delivery ;;
1155
+ *) kind=missed ;;
1156
+ esac
1157
+ payload=$(fm_pending_reply_escalation_payload "$rec" "$kind") || return 1
1158
+ [ -n "$parent_status" ] || return 1
1159
+ mkdir -p "$(dirname "$parent_status")" 2>/dev/null || return 1
1160
+ line="blocked [key=$(fm_pending_reply_escalation_key "$corr")]: $payload"
1161
+ if ! grep -Fqx "$line" "$parent_status" 2>/dev/null; then
1162
+ printf '%s\n' "$line" >> "$parent_status" 2>/dev/null || return 1
1163
+ fi
1164
+ now=$(fm_pending_reply_now)
1165
+ fm_pending_reply_set "$rec" escalated_epoch "$now" || return 1
1166
+ fm_pending_reply_set "$rec" phase escalated || return 1
1167
+ return 0
1168
+ }
1169
+
1170
+ # Detect a correlated report written under the secondmate home (wrong home)
1171
+ # without treating it as acknowledgement.
1172
+ fm_pending_reply_detect_wrong_home() { # <state-dir> <corr_id> <secondmate-home>
1173
+ local state=$1 corr=$2 sm_home=$3
1174
+ local rec delivered hits sightings snapshot previous status_file line line_no sighting_id phase changed=0
1175
+ rec=$(fm_pending_reply_path "$state" "$corr")
1176
+ [ -f "$rec" ] || return 1
1177
+ [ -n "$sm_home" ] && [ -d "$sm_home" ] || return 0
1178
+ phase=$(fm_pending_reply_get "$rec" phase)
1179
+ [ "$phase" != resolved ] || return 0
1180
+ delivered=$(fm_pending_reply_get "$rec" delivered_epoch)
1181
+ [ -n "$delivered" ] || return 0
1182
+ snapshot=$(fm_pending_reply_status_set_signature "$sm_home/state")
1183
+ previous=$(fm_pending_reply_get "$rec" wrong_home_scan_signature)
1184
+ [ "$snapshot" != "$previous" ] || return 0
1185
+ hits=$(fm_pending_reply_get "$rec" wrong_home_hits)
1186
+ case "$hits" in ''|*[!0-9]*) hits=0 ;; esac
1187
+ sightings=$(fm_pending_reply_get "$rec" wrong_home_sightings)
1188
+ for status_file in "$sm_home"/state/*.status; do
1189
+ [ -e "$status_file" ] || continue
1190
+ line_no=0
1191
+ while IFS= read -r line || [ -n "$line" ]; do
1192
+ line_no=$((line_no + 1))
1193
+ fm_pending_reply_line_resolves "$line" "$corr" || continue
1194
+ sighting_id=$(printf '%s:%s:%s:%s' "${#status_file}" "$status_file" "$line_no" "$line" \
1195
+ | cksum 2>/dev/null | awk '{printf "%s-%s", $1, $2}')
1196
+ [ -n "$sighting_id" ] || continue
1197
+ case ",$sightings," in
1198
+ *",$sighting_id,"*) continue ;;
1199
+ esac
1200
+ if [ -n "$sightings" ]; then
1201
+ sightings="$sightings,$sighting_id"
1202
+ else
1203
+ sightings=$sighting_id
1204
+ fi
1205
+ hits=$((hits + 1))
1206
+ changed=1
1207
+ done < "$status_file"
1208
+ done
1209
+ if [ "$changed" = 1 ]; then
1210
+ fm_pending_reply_set "$rec" wrong_home_sightings "$sightings" || return 1
1211
+ fm_pending_reply_set "$rec" wrong_home_hits "$hits" || return 1
1212
+ fi
1213
+ fm_pending_reply_set "$rec" wrong_home_scan_signature "$snapshot" || return 1
1214
+ return 0
1215
+ }
1216
+
1217
+ # One reconciliation tick for a single record: resolve, observe, recover, escalate.
1218
+ # busy_state is busy|idle|unknown for the secondmate endpoint.
1219
+ # secondmate_home may be empty when unknown.
1220
+ fm_pending_reply_tick_one() { # <state-dir> <corr_id> <busy_state> [secondmate-home]
1221
+ local state=$1 corr=$2 busy_state=$3 sm_home=${4-}
1222
+ local rec phase delivered
1223
+ rec=$(fm_pending_reply_path "$state" "$corr")
1224
+ [ -f "$rec" ] || return 1
1225
+ fm_pending_reply_reconcile_delivery "$state" "$corr" || true
1226
+ phase=$(fm_pending_reply_get "$rec" phase)
1227
+ delivered=$(fm_pending_reply_get "$rec" delivered_epoch)
1228
+ if [ -z "$delivered" ]; then
1229
+ case "$phase" in
1230
+ delivery_unknown) fm_pending_reply_maybe_escalate "$state" "$corr" 2>/dev/null || true ;;
1231
+ escalated) fm_pending_reply_try_resolve "$state" "$corr" >/dev/null 2>&1 || true ;;
1232
+ esac
1233
+ return 0
1234
+ fi
1235
+ # Correlated parent report always wins and is idempotent.
1236
+ if fm_pending_reply_try_resolve "$state" "$corr"; then
1237
+ return 0
1238
+ fi
1239
+ phase=$(fm_pending_reply_get "$rec" phase)
1240
+ case "$phase" in
1241
+ awaiting_report|recovery_sending)
1242
+ if [ -n "$(fm_pending_reply_get "$rec" recovery_attempted_epoch)" ]; then
1243
+ fm_pending_reply_reconcile_recovery "$state" "$corr" || true
1244
+ phase=$(fm_pending_reply_get "$rec" phase)
1245
+ fi
1246
+ ;;
1247
+ esac
1248
+ case "$phase" in
1249
+ resolved) return 0 ;;
1250
+ escalated)
1251
+ # Unresolved durable record retained; never auto-delete.
1252
+ if [ -n "$sm_home" ]; then
1253
+ fm_pending_reply_detect_wrong_home "$state" "$corr" "$sm_home" || true
1254
+ fi
1255
+ return 0
1256
+ ;;
1257
+ recovery_sending) return 0 ;;
1258
+ recovery_failed|recovery_unknown)
1259
+ fm_pending_reply_maybe_escalate "$state" "$corr" 2>/dev/null || true
1260
+ return 0
1261
+ ;;
1262
+ esac
1263
+ if [ -n "$sm_home" ]; then
1264
+ fm_pending_reply_detect_wrong_home "$state" "$corr" "$sm_home" || true
1265
+ fi
1266
+ fm_pending_reply_observe_busy "$state" "$corr" "$busy_state" || true
1267
+ # Re-check resolve after observation in case a concurrent status write landed.
1268
+ if fm_pending_reply_try_resolve "$state" "$corr"; then
1269
+ return 0
1270
+ fi
1271
+ phase=$(fm_pending_reply_get "$rec" phase)
1272
+ if [ "$phase" = awaiting_report ]; then
1273
+ fm_pending_reply_send_recovery "$state" "$corr" 2>/dev/null || true
1274
+ fi
1275
+ phase=$(fm_pending_reply_get "$rec" phase)
1276
+ case "$phase" in
1277
+ recovery_sent|recovery_failed|recovery_unknown)
1278
+ fm_pending_reply_maybe_escalate "$state" "$corr" 2>/dev/null || true
1279
+ ;;
1280
+ esac
1281
+ return 0
1282
+ }
1283
+
1284
+ # Scan every pending record for this parent state. Safe to call every poll.
1285
+ # Never scrapes secondmate conversation; uses only parent status, backend busy
1286
+ # state, and optional secondmate-home wrong-home path checks.
1287
+ fm_pending_reply_tick() { # <state-dir>
1288
+ local state=$1 dir rec corr task_id phase delivered meta backend target label busy sm_home harness remote_host
1289
+ local observation observation_task found i
1290
+ local -a observation_tasks=() observation_values=()
1291
+ dir=$(fm_pending_reply_dir "$state")
1292
+ [ -d "$dir" ] || return 0
1293
+ for rec in "$dir"/*; do
1294
+ [ -f "$rec" ] || continue
1295
+ case "$(basename "$rec")" in
1296
+ .*) continue ;;
1297
+ esac
1298
+ corr=$(fm_pending_reply_get "$rec" corr_id)
1299
+ [ -n "$corr" ] || corr=$(basename "$rec")
1300
+ task_id=$(fm_pending_reply_get "$rec" task_id)
1301
+ phase=$(fm_pending_reply_get "$rec" phase)
1302
+ if [ "$phase" = resolved ]; then
1303
+ # Cheap no-op unless an escalation for this record is still open; this is
1304
+ # the retry that makes the close converge after a transient write failure.
1305
+ fm_pending_reply_close_escalation "$state" "$corr" || true
1306
+ continue
1307
+ fi
1308
+ fm_pending_reply_reconcile_delivery "$state" "$corr" || true
1309
+ phase=$(fm_pending_reply_get "$rec" phase)
1310
+ delivered=$(fm_pending_reply_get "$rec" delivered_epoch)
1311
+ if [ -z "$delivered" ]; then
1312
+ case "$phase" in
1313
+ delivery_unknown|escalated)
1314
+ fm_pending_reply_tick_one "$state" "$corr" unknown "" || true
1315
+ ;;
1316
+ esac
1317
+ continue
1318
+ fi
1319
+ case "$phase" in
1320
+ awaiting_report|recovery_sending)
1321
+ if [ -n "$(fm_pending_reply_get "$rec" recovery_attempted_epoch)" ]; then
1322
+ fm_pending_reply_reconcile_recovery "$state" "$corr" || true
1323
+ phase=$(fm_pending_reply_get "$rec" phase)
1324
+ fi
1325
+ ;;
1326
+ esac
1327
+ meta="$state/${task_id}.meta"
1328
+ if [ "$phase" = escalated ]; then
1329
+ if fm_pending_reply_try_resolve "$state" "$corr"; then
1330
+ continue
1331
+ fi
1332
+ if [ -f "$meta" ]; then
1333
+ sm_home=$(fm_meta_get "$meta" home)
1334
+ if [ -n "$sm_home" ]; then
1335
+ fm_pending_reply_detect_wrong_home "$state" "$corr" "$sm_home" || true
1336
+ fi
1337
+ fi
1338
+ continue
1339
+ fi
1340
+ case "$phase" in
1341
+ recovery_failed|recovery_unknown)
1342
+ fm_pending_reply_tick_one "$state" "$corr" unknown "" || true
1343
+ continue
1344
+ ;;
1345
+ esac
1346
+ case "$phase" in
1347
+ awaiting_report|recovery_sent) ;;
1348
+ *) continue ;;
1349
+ esac
1350
+ backend=tmux
1351
+ target=
1352
+ busy=unknown
1353
+ sm_home=
1354
+ harness=
1355
+ if [ -f "$meta" ]; then
1356
+ remote_host=$(fm_meta_get "$meta" remote_host)
1357
+ backend=$(fm_backend_of_meta "$meta")
1358
+ target=$(fm_backend_target_of_meta "$meta")
1359
+ sm_home=$(fm_meta_get "$meta" home)
1360
+ harness=$(fm_meta_get "$meta" harness)
1361
+ if [ -n "$remote_host" ]; then
1362
+ target="remote:$task_id"
1363
+ sm_home=
1364
+ fi
1365
+ if [ -n "$target" ]; then
1366
+ label="fm-$task_id"
1367
+ observation=
1368
+ found=0
1369
+ for ((i = 0; i < ${#observation_tasks[@]}; i++)); do
1370
+ observation_task=${observation_tasks[$i]}
1371
+ [ "$observation_task" = "$task_id" ] || continue
1372
+ observation=${observation_values[$i]}
1373
+ found=1
1374
+ break
1375
+ done
1376
+ if [ "$found" = 0 ]; then
1377
+ if [ -n "$remote_host" ]; then
1378
+ observation=$("$_FM_PENDING_REPLY_LIB_DIR/fm-on.sh" "$task_id" \
1379
+ fm-remote-secondmate-control.sh observe "$task_id" < /dev/null 2>/dev/null || printf 'unknown')
1380
+ case "$observation" in busy|idle|fallback-idle|unknown) ;; *) observation=unknown ;; esac
1381
+ else
1382
+ observation=$(fm_pending_reply_backend_observation "$backend" "$target" "$label" "$harness")
1383
+ fi
1384
+ observation_tasks+=("$task_id")
1385
+ observation_values+=("$observation")
1386
+ fi
1387
+ busy=$(fm_pending_reply_busy_state_from_observation "$rec" "$observation")
1388
+ fi
1389
+ fi
1390
+ fm_pending_reply_tick_one "$state" "$corr" "$busy" "$sm_home" || true
1391
+ done
1392
+ return 0
1393
+ }
1394
+
1395
+ # True when any open (non-resolved) pending reply exists for a task.
1396
+ fm_pending_reply_task_has_open() { # <state-dir> <task_id>
1397
+ local state=$1 task_id=$2 dir rec phase tid
1398
+ dir=$(fm_pending_reply_dir "$state")
1399
+ [ -d "$dir" ] || return 1
1400
+ for rec in "$dir"/*; do
1401
+ [ -f "$rec" ] || continue
1402
+ tid=$(fm_pending_reply_get "$rec" task_id)
1403
+ [ "$tid" = "$task_id" ] || continue
1404
+ phase=$(fm_pending_reply_get "$rec" phase)
1405
+ [ "$phase" != resolved ] || continue
1406
+ return 0
1407
+ done
1408
+ return 1
1409
+ }