@zerwiz/ymir 0.1.4 → 0.1.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (850) hide show
  1. package/.agents/README.md +291 -0
  2. package/.agents/agents/bragi-marketer.md +111 -0
  3. package/.agents/agents/brokk.md +352 -0
  4. package/.agents/agents/forseti-reviewer.md +93 -0
  5. package/.agents/agents/frigg-consent.md +77 -0
  6. package/.agents/agents/groa-update.md +77 -0
  7. package/.agents/agents/hnoss-designer.md +69 -0
  8. package/.agents/agents/huginn-researcher.md +99 -0
  9. package/.agents/agents/jord-projects.md +78 -0
  10. package/.agents/agents/kvasir-scout.md +91 -0
  11. package/.agents/agents/mimir-planner.md +92 -0
  12. package/.agents/agents/muninn-stow.md +77 -0
  13. package/.agents/agents/saga-bearings.md +78 -0
  14. package/.agents/agents/sindri-developer.md +114 -0
  15. package/.agents/agents/snotra-documenter.md +105 -0
  16. package/.agents/agents/syn-recovery.md +78 -0
  17. package/.agents/agents/tyr-check.md +78 -0
  18. package/.agents/agents/urdh-hold.md +77 -0
  19. package/.agents/agents/volundr-smith.md +80 -0
  20. package/.agents/agents/vor-diagnostics.md +77 -0
  21. package/.agents/assets/agents/naming.md +50 -0
  22. package/.agents/assets/agents/registry.md +93 -0
  23. package/.agents/assets/agents/runtime.md +36 -0
  24. package/.agents/assets/agents/toon-tasks-cli.md +42 -0
  25. package/.agents/assets/templates/company_entity.template.md +37 -0
  26. package/.agents/backend/backends/cmux.sh +649 -0
  27. package/.agents/backend/backends/herdr-eventwait.py +157 -0
  28. package/.agents/backend/backends/herdr-workspace-move.py +114 -0
  29. package/.agents/backend/backends/herdr.sh +3386 -0
  30. package/.agents/backend/backends/orca.sh +290 -0
  31. package/.agents/backend/backends/tmux.sh +357 -0
  32. package/.agents/backend/backends/zellij.sh +700 -0
  33. package/.agents/backend/fm-afk-launch.sh +654 -0
  34. package/.agents/backend/fm-afk-return.sh +241 -0
  35. package/.agents/backend/fm-afk-start.sh +172 -0
  36. package/.agents/backend/fm-arm-command-policy.mjs +960 -0
  37. package/.agents/backend/fm-arm-pretool-check.sh +200 -0
  38. package/.agents/backend/fm-backend-hometag-lib.sh +52 -0
  39. package/.agents/backend/fm-backend.sh +981 -0
  40. package/.agents/backend/fm-backlog-handoff.sh +909 -0
  41. package/.agents/backend/fm-backlog-receive.sh +187 -0
  42. package/.agents/backend/fm-backlog-transition-lib.sh +779 -0
  43. package/.agents/backend/fm-bearings-board.sh +204 -0
  44. package/.agents/backend/fm-bearings-snapshot.sh +546 -0
  45. package/.agents/backend/fm-bootstrap.sh +1568 -0
  46. package/.agents/backend/fm-branch-outcome.sh +203 -0
  47. package/.agents/backend/fm-branch-prompt.sh +101 -0
  48. package/.agents/backend/fm-brief.sh +452 -0
  49. package/.agents/backend/fm-busy-event.sh +234 -0
  50. package/.agents/backend/fm-busy-lib.sh +986 -0
  51. package/.agents/backend/fm-captain-hold.sh +1001 -0
  52. package/.agents/backend/fm-cd-command-policy.mjs +153 -0
  53. package/.agents/backend/fm-cd-pretool-check.sh +190 -0
  54. package/.agents/backend/fm-check-lib.sh +74 -0
  55. package/.agents/backend/fm-check-register.sh +42 -0
  56. package/.agents/backend/fm-check-unregister.sh +52 -0
  57. package/.agents/backend/fm-classify-lib.sh +1770 -0
  58. package/.agents/backend/fm-claude-stop-autoarm.sh +334 -0
  59. package/.agents/backend/fm-composer-lib.sh +1416 -0
  60. package/.agents/backend/fm-config-inherit-lib.sh +1202 -0
  61. package/.agents/backend/fm-config-push.sh +254 -0
  62. package/.agents/backend/fm-control-lib.sh +251 -0
  63. package/.agents/backend/fm-control.sh +878 -0
  64. package/.agents/backend/fm-crew-state.sh +625 -0
  65. package/.agents/backend/fm-cursor-lib.sh +243 -0
  66. package/.agents/backend/fm-decision-hold.sh +232 -0
  67. package/.agents/backend/fm-doc-audience-check.sh +269 -0
  68. package/.agents/backend/fm-dod-lib.sh +67 -0
  69. package/.agents/backend/fm-ensure-agents-md.sh +253 -0
  70. package/.agents/backend/fm-extension-launch-barrier.mjs +129 -0
  71. package/.agents/backend/fm-extension.mjs +2577 -0
  72. package/.agents/backend/fm-extension.sh +16 -0
  73. package/.agents/backend/fm-ff-lib.sh +420 -0
  74. package/.agents/backend/fm-fleet-snapshot.sh +1463 -0
  75. package/.agents/backend/fm-fleet-sync.sh +458 -0
  76. package/.agents/backend/fm-fleet-view.sh +96 -0
  77. package/.agents/backend/fm-gate-refuse-lib.sh +102 -0
  78. package/.agents/backend/fm-guard.sh +242 -0
  79. package/.agents/backend/fm-harness.sh +195 -0
  80. package/.agents/backend/fm-herdr-ci-cleanup.sh +115 -0
  81. package/.agents/backend/fm-herdr-lab.sh +346 -0
  82. package/.agents/backend/fm-herdr-session-cleanup.sh +337 -0
  83. package/.agents/backend/fm-home-seed.sh +984 -0
  84. package/.agents/backend/fm-home-summary-refresh.sh +255 -0
  85. package/.agents/backend/fm-hook-host-lib.sh +36 -0
  86. package/.agents/backend/fm-inactive-reconcile.sh +531 -0
  87. package/.agents/backend/fm-inbox.sh +399 -0
  88. package/.agents/backend/fm-install-actionlint.sh +84 -0
  89. package/.agents/backend/fm-install-herdr.sh +96 -0
  90. package/.agents/backend/fm-install-shellcheck.sh +84 -0
  91. package/.agents/backend/fm-install-treehouse.sh +95 -0
  92. package/.agents/backend/fm-kimi-turnend-hook.sh +276 -0
  93. package/.agents/backend/fm-lease-lib.sh +218 -0
  94. package/.agents/backend/fm-lease.sh +189 -0
  95. package/.agents/backend/fm-line-cap-lib.sh +51 -0
  96. package/.agents/backend/fm-lint-workflows.sh +137 -0
  97. package/.agents/backend/fm-lint.sh +591 -0
  98. package/.agents/backend/fm-lock-lib.sh +104 -0
  99. package/.agents/backend/fm-lock.sh +107 -0
  100. package/.agents/backend/fm-marker-lib.sh +12 -0
  101. package/.agents/backend/fm-merge-local.sh +74 -0
  102. package/.agents/backend/fm-merge-outcome-lib.sh +138 -0
  103. package/.agents/backend/fm-nm-run-lib.sh +124 -0
  104. package/.agents/backend/fm-on.sh +125 -0
  105. package/.agents/backend/fm-operational-input.sh +253 -0
  106. package/.agents/backend/fm-peek.sh +46 -0
  107. package/.agents/backend/fm-pending-reply-lib.sh +1409 -0
  108. package/.agents/backend/fm-pr-check.sh +135 -0
  109. package/.agents/backend/fm-pr-lib.sh +1016 -0
  110. package/.agents/backend/fm-pr-merge.sh +707 -0
  111. package/.agents/backend/fm-pr-poll.sh +110 -0
  112. package/.agents/backend/fm-primary-scope-lib.sh +33 -0
  113. package/.agents/backend/fm-procevent-extension-capture.pl +259 -0
  114. package/.agents/backend/fm-procevent-lavish.sh +628 -0
  115. package/.agents/backend/fm-procevent-lib.sh +892 -0
  116. package/.agents/backend/fm-procevent-quota.sh +290 -0
  117. package/.agents/backend/fm-procevent-remote-reply.sh +577 -0
  118. package/.agents/backend/fm-procevent-when.sh +504 -0
  119. package/.agents/backend/fm-procevent.sh +1466 -0
  120. package/.agents/backend/fm-project-mode.sh +92 -0
  121. package/.agents/backend/fm-project-origin-lib.sh +180 -0
  122. package/.agents/backend/fm-promote.sh +282 -0
  123. package/.agents/backend/fm-public-followup-emit.sh +260 -0
  124. package/.agents/backend/fm-public-followup-lib.sh +398 -0
  125. package/.agents/backend/fm-public-followup.sh +1330 -0
  126. package/.agents/backend/fm-push-transition-lib.sh +168 -0
  127. package/.agents/backend/fm-quota-axi-lib.sh +93 -0
  128. package/.agents/backend/fm-quota-choose.sh +384 -0
  129. package/.agents/backend/fm-remote-delta-read.sh +189 -0
  130. package/.agents/backend/fm-remote-doctor.sh +800 -0
  131. package/.agents/backend/fm-remote-entrypoint.sh +193 -0
  132. package/.agents/backend/fm-remote-file.sh +234 -0
  133. package/.agents/backend/fm-remote-home-provision.sh +259 -0
  134. package/.agents/backend/fm-remote-home-seed.sh +259 -0
  135. package/.agents/backend/fm-remote-inherit-push.sh +90 -0
  136. package/.agents/backend/fm-remote-inherit.sh +179 -0
  137. package/.agents/backend/fm-remote-job-lib.sh +1241 -0
  138. package/.agents/backend/fm-remote-job-reap-orphans.sh +137 -0
  139. package/.agents/backend/fm-remote-job-worker.sh +1124 -0
  140. package/.agents/backend/fm-remote-readiness-lib.sh +44 -0
  141. package/.agents/backend/fm-remote-secondmate-control.sh +340 -0
  142. package/.agents/backend/fm-review-diff.sh +158 -0
  143. package/.agents/backend/fm-secondmate-charter-lib.sh +44 -0
  144. package/.agents/backend/fm-secondmate-nudge-lib.sh +68 -0
  145. package/.agents/backend/fm-secondmate-parent-lib.sh +70 -0
  146. package/.agents/backend/fm-secondmate-reconcile.sh +371 -0
  147. package/.agents/backend/fm-secondmate-registry-lib.sh +311 -0
  148. package/.agents/backend/fm-secondmate-report.sh +88 -0
  149. package/.agents/backend/fm-send.sh +1059 -0
  150. package/.agents/backend/fm-session-lock-lib.sh +176 -0
  151. package/.agents/backend/fm-session-start.sh +970 -0
  152. package/.agents/backend/fm-sessionstart-cursor.sh +40 -0
  153. package/.agents/backend/fm-sessionstart-nudge.sh +45 -0
  154. package/.agents/backend/fm-sessionstart-run.sh +145 -0
  155. package/.agents/backend/fm-spawn.sh +3153 -0
  156. package/.agents/backend/fm-startup-memory-budget-lib.sh +224 -0
  157. package/.agents/backend/fm-startup-memory-budget.sh +94 -0
  158. package/.agents/backend/fm-startup-network.sh +642 -0
  159. package/.agents/backend/fm-stow-cascade.sh +251 -0
  160. package/.agents/backend/fm-subagent-pretool-check.sh +207 -0
  161. package/.agents/backend/fm-supervise-daemon.sh +1732 -0
  162. package/.agents/backend/fm-supervision-instructions.sh +215 -0
  163. package/.agents/backend/fm-supervision-lib.sh +90 -0
  164. package/.agents/backend/fm-supervisor-target-lib.sh +78 -0
  165. package/.agents/backend/fm-tangle-lib.sh +53 -0
  166. package/.agents/backend/fm-task-inbox-lib.sh +402 -0
  167. package/.agents/backend/fm-tasks-axi-lib.sh +123 -0
  168. package/.agents/backend/fm-teardown.sh +2922 -0
  169. package/.agents/backend/fm-test-isolation-proof.sh +588 -0
  170. package/.agents/backend/fm-test-run.sh +2191 -0
  171. package/.agents/backend/fm-timeout-lib.sh +141 -0
  172. package/.agents/backend/fm-timing-lib.sh +179 -0
  173. package/.agents/backend/fm-tmux-lib.sh +291 -0
  174. package/.agents/backend/fm-tool-update-check.sh +898 -0
  175. package/.agents/backend/fm-trace-context-lib.sh +227 -0
  176. package/.agents/backend/fm-transition-lib.sh +103 -0
  177. package/.agents/backend/fm-turnend-guard-cursor.sh +391 -0
  178. package/.agents/backend/fm-turnend-guard-grok.sh +90 -0
  179. package/.agents/backend/fm-turnend-guard.sh +432 -0
  180. package/.agents/backend/fm-update.sh +110 -0
  181. package/.agents/backend/fm-vendor-auth-probe.sh +190 -0
  182. package/.agents/backend/fm-voice-client.py +1373 -0
  183. package/.agents/backend/fm-voice-relay.py +1256 -0
  184. package/.agents/backend/fm-wake-drain.sh +592 -0
  185. package/.agents/backend/fm-wake-grant.sh +127 -0
  186. package/.agents/backend/fm-wake-lib.sh +1849 -0
  187. package/.agents/backend/fm-watch-arm.sh +602 -0
  188. package/.agents/backend/fm-watch-checkpoint.sh +109 -0
  189. package/.agents/backend/fm-watch.sh +1962 -0
  190. package/.agents/backend/fm-x-dismiss.sh +116 -0
  191. package/.agents/backend/fm-x-followup.sh +286 -0
  192. package/.agents/backend/fm-x-lib.sh +1000 -0
  193. package/.agents/backend/fm-x-link.sh +234 -0
  194. package/.agents/backend/fm-x-poll.sh +185 -0
  195. package/.agents/backend/fm-x-reply.sh +385 -0
  196. package/.agents/backend/fm_voice_frame.py +166 -0
  197. package/.agents/backend/fm_voice_records.py +574 -0
  198. package/.agents/backend/model-bridge.py +290 -0
  199. package/.agents/backend/opencode-go-bridge.py +30 -0
  200. package/.agents/bus/README.md +12 -0
  201. package/.agents/bus/messages.json +1 -0
  202. package/.agents/config/agents.machine.example.yaml +16 -0
  203. package/.agents/config/agents.yaml +50 -0
  204. package/.agents/config/agents.yaml.example +72 -0
  205. package/.agents/config/crew-dispatch.json +22 -0
  206. package/.agents/config/cron.yaml +9 -0
  207. package/.agents/config/eindri-dispatch.json +67 -0
  208. package/.agents/config/eindri-harness +1 -0
  209. package/.agents/config/startup-memory-budget +1 -0
  210. package/.agents/config/tailscale-sync.yaml +27 -0
  211. package/.agents/filebrowser/.filebrowser.json +6 -0
  212. package/.agents/filebrowser/README.md +11 -0
  213. package/.agents/gateway/README.md +12 -0
  214. package/.agents/github/README.md +15 -0
  215. package/.agents/github/webhooks/.gitkeep +0 -0
  216. package/.agents/github/workflows/.gitkeep +0 -0
  217. package/.agents/harness/opencode/plugins/lib/rodd-operational-input.js +37 -0
  218. package/.agents/harness/opencode/plugins/package.json +4 -0
  219. package/.agents/harness/opencode/plugins/saga-sessionstart.js +77 -0
  220. package/.agents/harness/opencode/plugins/syn-cd-check.js +61 -0
  221. package/.agents/harness/opencode/plugins/syn-pretool-check.js +68 -0
  222. package/.agents/harness/opencode/plugins/syn-turnend-guard.js +105 -0
  223. package/.agents/harness/opencode/plugins/syn-watch-arm.js +521 -0
  224. package/.agents/memory/README.md +46 -0
  225. package/.agents/memory/kaia.engram +0 -0
  226. package/.agents/memory/kaia.engram-shm +0 -0
  227. package/.agents/memory/kaia.engram-wal +0 -0
  228. package/.agents/memory/runes_audit.md +5 -0
  229. package/.agents/memory/well/episodes.jsonl +367 -0
  230. package/.agents/memory/well/workspace.jsonl +27 -0
  231. package/.agents/migrations/0001-hodd-layout.sh +38 -0
  232. package/.agents/migrations/0002-a2a-mcp.sh +27 -0
  233. package/.agents/migrations/0003-private-data-separation.sh +111 -0
  234. package/.agents/migrations/0004-hoard-and-realms.sh +104 -0
  235. package/.agents/sandbox/Dockerfile.utgard +19 -0
  236. package/.agents/sandbox/README.md +14 -0
  237. package/.agents/sandbox/utgard.config.json +14 -0
  238. package/.agents/skills/README.md +59 -0
  239. package/.agents/skills/bragi-marketing/README.md +3 -0
  240. package/.agents/skills/bragi-marketing/SKILL.md +82 -0
  241. package/.agents/skills/eindri-homes/README.md +3 -0
  242. package/.agents/skills/eindri-homes/SKILL.md +291 -0
  243. package/.agents/skills/eindri-homes/assets/control-plane.md +122 -0
  244. package/.agents/skills/eindri-homes/assets/dispatch.md +80 -0
  245. package/.agents/skills/frigg-consent/README.md +3 -0
  246. package/.agents/skills/frigg-consent/SKILL.md +56 -0
  247. package/.agents/skills/galdr-ymirsystem/README.md +3 -0
  248. package/.agents/skills/galdr-ymirsystem/SKILL.md +158 -0
  249. package/.agents/skills/galdr-ymirsystem/assets/README.md +74 -0
  250. package/.agents/skills/galdr-ymirsystem/assets/brokk-distro-runtime.md +390 -0
  251. package/.agents/skills/galdr-ymirsystem/assets/build-method.md +113 -0
  252. package/.agents/skills/galdr-ymirsystem/assets/build-tool-categories.md +178 -0
  253. package/.agents/skills/galdr-ymirsystem/assets/eindri-orchestration.md +518 -0
  254. package/.agents/skills/galdr-ymirsystem/assets/eindri-profiles.md +136 -0
  255. package/.agents/skills/galdr-ymirsystem/assets/harness-integration/README.md +673 -0
  256. package/.agents/skills/galdr-ymirsystem/assets/harness-integration/claude-code.md +197 -0
  257. package/.agents/skills/galdr-ymirsystem/assets/harness-integration/codex.md +191 -0
  258. package/.agents/skills/galdr-ymirsystem/assets/harness-integration/cursor.md +193 -0
  259. package/.agents/skills/galdr-ymirsystem/assets/harness-integration/opencode.md +262 -0
  260. package/.agents/skills/galdr-ymirsystem/assets/harness-integration/pi.md +295 -0
  261. package/.agents/skills/galdr-ymirsystem/assets/hlidskjalf-ui.md +496 -0
  262. package/.agents/skills/galdr-ymirsystem/assets/installation.md +571 -0
  263. package/.agents/skills/galdr-ymirsystem/assets/local-models/HOST-DATA.example.md +72 -0
  264. package/.agents/skills/galdr-ymirsystem/assets/local-models.md +274 -0
  265. package/.agents/skills/galdr-ymirsystem/assets/memory-well.md +161 -0
  266. package/.agents/skills/galdr-ymirsystem/assets/nornir-jobs.md +377 -0
  267. package/.agents/skills/galdr-ymirsystem/assets/norse-naming.md +325 -0
  268. package/.agents/skills/galdr-ymirsystem/assets/odrerir-hall.md +129 -0
  269. package/.agents/skills/galdr-ymirsystem/assets/pi-boot/einherjar-spawn.schema.json +81 -0
  270. package/.agents/skills/galdr-ymirsystem/assets/pi-boot/herdr-profile.toml +75 -0
  271. package/.agents/skills/galdr-ymirsystem/assets/pi-boot/pi-profile.yml +96 -0
  272. package/.agents/skills/galdr-ymirsystem/assets/pi-boot/supervision-tree.yml +163 -0
  273. package/.agents/skills/galdr-ymirsystem/assets/pi-boot-guide.md +58 -0
  274. package/.agents/skills/galdr-ymirsystem/assets/porting-upstream-to-norse.md +301 -0
  275. package/.agents/skills/galdr-ymirsystem/assets/principles.md +385 -0
  276. package/.agents/skills/galdr-ymirsystem/assets/reference-adoption.md +99 -0
  277. package/.agents/skills/galdr-ymirsystem/assets/registry.md +379 -0
  278. package/.agents/skills/galdr-ymirsystem/assets/runtime-compliance.md +463 -0
  279. package/.agents/skills/galdr-ymirsystem/assets/runtime-components.md +553 -0
  280. package/.agents/skills/galdr-ymirsystem/assets/smidja.md +175 -0
  281. package/.agents/skills/galdr-ymirsystem/schemas/toon-schemas.md +190 -0
  282. package/.agents/skills/galdr-ymirsystem/scripts/README.md +45 -0
  283. package/.agents/skills/galdr-ymirsystem/scripts/bench-one.sh +248 -0
  284. package/.agents/skills/galdr-ymirsystem/scripts/compliance-check.sh +360 -0
  285. package/.agents/skills/galdr-ymirsystem/scripts/gpu-sample.sh +43 -0
  286. package/.agents/skills/galdr-ymirsystem/scripts/stop-all.sh +34 -0
  287. package/.agents/skills/galdr-ymirsystem/scripts/toon-check.py +188 -0
  288. package/.agents/skills/gjallarhorn-relay/README.md +3 -0
  289. package/.agents/skills/gjallarhorn-relay/SKILL.md +283 -0
  290. package/.agents/skills/groa-update/SKILL.md +70 -0
  291. package/.agents/skills/hamr-adapters/README.md +3 -0
  292. package/.agents/skills/hamr-adapters/SKILL.md +123 -0
  293. package/.agents/skills/hamr-adapters/references/common/control-and-recovery.md +37 -0
  294. package/.agents/skills/hamr-adapters/references/common/dispatch.md +32 -0
  295. package/.agents/skills/hamr-adapters/references/common/model-and-effort.md +42 -0
  296. package/.agents/skills/hamr-adapters/references/common/primary-hooks.md +40 -0
  297. package/.agents/skills/hamr-adapters/references/harness/claude.md +55 -0
  298. package/.agents/skills/hamr-adapters/references/harness/codex.md +43 -0
  299. package/.agents/skills/hamr-adapters/references/harness/cursor.md +75 -0
  300. package/.agents/skills/hamr-adapters/references/harness/grok.md +69 -0
  301. package/.agents/skills/hamr-adapters/references/harness/kimi.md +51 -0
  302. package/.agents/skills/hamr-adapters/references/harness/muse.md +70 -0
  303. package/.agents/skills/hamr-adapters/references/harness/opencode.md +42 -0
  304. package/.agents/skills/hamr-adapters/references/harness/pi.md +56 -0
  305. package/.agents/skills/herdr-panes/README.md +3 -0
  306. package/.agents/skills/herdr-panes/SKILL.md +92 -0
  307. package/.agents/skills/herdr-panes/assets/herdr-backend.md +346 -0
  308. package/.agents/skills/herdr-panes/assets/tmux-backend.md +115 -0
  309. package/.agents/skills/hnoss-design/DESIGN.md +19 -0
  310. package/.agents/skills/hnoss-design/README.md +3 -0
  311. package/.agents/skills/hnoss-design/SKILL.md +83 -0
  312. package/.agents/skills/hvild-afk/README.md +3 -0
  313. package/.agents/skills/hvild-afk/SKILL.md +241 -0
  314. package/.agents/skills/jord-projects/README.md +3 -0
  315. package/.agents/skills/jord-projects/SKILL.md +92 -0
  316. package/.agents/skills/lifecycle/SKILL.md +56 -0
  317. package/.agents/skills/lifecycle/smoke_test.sh +52 -0
  318. package/.agents/skills/lifecycle/start.sh +21 -0
  319. package/.agents/skills/lifecycle/status.sh +35 -0
  320. package/.agents/skills/lifecycle/stop.sh +16 -0
  321. package/.agents/skills/muninn-stow/README.md +3 -0
  322. package/.agents/skills/muninn-stow/SKILL.md +309 -0
  323. package/.agents/skills/no-mistakes/SKILL.md +379 -0
  324. package/.agents/skills/nornir-schedule/README.md +3 -0
  325. package/.agents/skills/nornir-schedule/SKILL.md +25 -0
  326. package/.agents/skills/nornir-schedule/assets/events.md +137 -0
  327. package/.agents/skills/nornir-schedule/assets/quota.md +131 -0
  328. package/.agents/skills/nsr-compliance/README.md +3 -0
  329. package/.agents/skills/nsr-compliance/SKILL.md +29 -0
  330. package/.agents/skills/nsr-compliance/assets/nsr/README.md +40 -0
  331. package/.agents/skills/nsr-compliance/assets/nsr/assets/RULES/README.md +15 -0
  332. package/.agents/skills/nsr-compliance/assets/nsr/assets/RULES/git-workflow.md +18 -0
  333. package/.agents/skills/nsr-compliance/assets/nsr/assets/RULES/security.md +22 -0
  334. package/.agents/skills/nsr-compliance/assets/nsr/assets/RULES/tech-stack.md +8 -0
  335. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/SKILL.md +71 -0
  336. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/config/core_four.yaml +16 -0
  337. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/gates/check_danger.sh +102 -0
  338. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/gates/check_wiring.sh +69 -0
  339. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/gates/validate_code.sh +29 -0
  340. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/gates/verify_docs.py +66 -0
  341. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/generate.sh +109 -0
  342. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/harness/envelopes/result_envelope.json +14 -0
  343. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/harness/envelopes/task_envelope.json +20 -0
  344. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/harness/runner.py +303 -0
  345. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/telemetry/logger.py +112 -0
  346. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/templates/compliance/README.md +18 -0
  347. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/templates/compliance/config/README.md +10 -0
  348. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/templates/compliance/gates/README.md +12 -0
  349. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/templates/compliance/gates/check_env.sh +23 -0
  350. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/templates/compliance/gates/check_paths.sh +20 -0
  351. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/templates/compliance/gates/check_platform.sh +23 -0
  352. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/templates/compliance/harness/README.md +11 -0
  353. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/templates/compliance/harness/envelopes/README.md +10 -0
  354. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/templates/compliance/installer/README.md +10 -0
  355. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/templates/compliance/installer/install.sh +125 -0
  356. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/templates/compliance/telemetry/README.md +11 -0
  357. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/README.md +20 -0
  358. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/features/README.md +36 -0
  359. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/features/SKILL-template.md +14 -0
  360. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/git_ops/README.md +25 -0
  361. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/git_ops/SKILL.md +13 -0
  362. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/git_ops/create_branch.sh +6 -0
  363. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/git_ops/safe_commit.sh +9 -0
  364. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/git_ops/sync_upstream.sh +6 -0
  365. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/lifecycle/README.md +27 -0
  366. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/lifecycle/SKILL.md +15 -0
  367. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/lifecycle/smoke_test.sh +12 -0
  368. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/lifecycle/start.sh +19 -0
  369. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/lifecycle/status.sh +12 -0
  370. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/lifecycle/stop.sh +15 -0
  371. package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/README.md +18 -0
  372. package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/config/README.md +10 -0
  373. package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/config/core_four.yaml +13 -0
  374. package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/gates/README.md +12 -0
  375. package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/gates/check_danger.sh +102 -0
  376. package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/gates/check_env.sh +23 -0
  377. package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/gates/check_paths.sh +20 -0
  378. package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/gates/check_platform.sh +23 -0
  379. package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/gates/check_wiring.sh +69 -0
  380. package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/harness/README.md +11 -0
  381. package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/harness/envelopes/README.md +10 -0
  382. package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/harness/envelopes/result_envelope.json +14 -0
  383. package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/harness/envelopes/task_envelope.json +20 -0
  384. package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/harness/runner.py +303 -0
  385. package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/installer/README.md +10 -0
  386. package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/installer/install.sh +125 -0
  387. package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/telemetry/README.md +11 -0
  388. package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/telemetry/logger.py +112 -0
  389. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/BEST_PRACTICES/README.md +14 -0
  390. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/BEST_PRACTICES/api.md +17 -0
  391. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/BEST_PRACTICES/compliance-wiring.md +155 -0
  392. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/BEST_PRACTICES/frontend.md +15 -0
  393. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/CI_CD/README.md +11 -0
  394. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/CI_CD/deployment/README.md +23 -0
  395. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/CI_CD/deployment/clients/README.md +16 -0
  396. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/CI_CD/deployment/clients/template-README.md +11 -0
  397. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/CI_CD/deployment/deploy.sh +49 -0
  398. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/CI_CD/deployment/envs/README.md +12 -0
  399. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/CI_CD/deployment/envs/development.env.example +17 -0
  400. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/CI_CD/deployment/envs/production.env.example +17 -0
  401. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/CI_CD/deployment/envs/staging.env.example +17 -0
  402. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/CI_CD/deployment/tenants/README.md +11 -0
  403. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/CI_CD/deployment/tenants/template.env.example +8 -0
  404. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/DEVELOPER_SETUP/README.md +26 -0
  405. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/DEVELOPER_SETUP/agent.md +7 -0
  406. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/DEVELOPER_SETUP/developers/dev-1.md +20 -0
  407. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/DEVELOPER_SETUP/linux.md +9 -0
  408. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/DEVELOPER_SETUP/macos.md +10 -0
  409. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/DEVELOPER_SETUP/shared.md +19 -0
  410. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/DEVELOPER_SETUP/windows.md +14 -0
  411. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/HOSTING/README.md +25 -0
  412. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/HOSTING/_template.md +41 -0
  413. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/HOSTING/hosting-1.md +41 -0
  414. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/README.md +21 -0
  415. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/RUNBOOK/README.md +15 -0
  416. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/RUNBOOK/deployment.md +19 -0
  417. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/RUNBOOK/disaster-recovery.md +15 -0
  418. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/RUNBOOK/ops-tasks.md +14 -0
  419. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/features/README.md +16 -0
  420. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/project-skills.md +146 -0
  421. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/research/README.md +11 -0
  422. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/research/software-compliance-research.md +269 -0
  423. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/working-with-agents.md +260 -0
  424. package/.agents/skills/nsr-compliance/assets/nsr/assets/root/AGENTS.md +38 -0
  425. package/.agents/skills/nsr-compliance/assets/nsr/assets/root/ARCHITECTURE.md +36 -0
  426. package/.agents/skills/nsr-compliance/assets/nsr/assets/root/BEST_PRACTICES.md +27 -0
  427. package/.agents/skills/nsr-compliance/assets/nsr/assets/root/CI_CD.md +23 -0
  428. package/.agents/skills/nsr-compliance/assets/nsr/assets/root/FEATURES.md +15 -0
  429. package/.agents/skills/nsr-compliance/assets/nsr/assets/root/README.md +22 -0
  430. package/.agents/skills/nsr-compliance/assets/nsr/assets/root/RULES.md +37 -0
  431. package/.agents/skills/nsr-compliance/assets/nsr/assets/root/STRUCTURE.md +45 -0
  432. package/.agents/skills/nsr-compliance/assets/nsr/assets/root/TECH_STACK.md +28 -0
  433. package/.agents/skills/nsr-compliance/assets/nsr/scaffold-spec.md +63 -0
  434. package/.agents/skills/nsr-compliance/assets/nsr/scripts/scaffold.sh +103 -0
  435. package/.agents/skills/nsr-compliance/assets/nsr/scripts/validate.sh +52 -0
  436. package/.agents/skills/nsr-compliance/assets/nsrcompliance/SKILL.md +71 -0
  437. package/.agents/skills/nsr-compliance/assets/nsrcompliance/config/core_four.yaml +16 -0
  438. package/.agents/skills/nsr-compliance/assets/nsrcompliance/gates/check_danger.sh +102 -0
  439. package/.agents/skills/nsr-compliance/assets/nsrcompliance/gates/check_wiring.sh +69 -0
  440. package/.agents/skills/nsr-compliance/assets/nsrcompliance/gates/validate_code.sh +29 -0
  441. package/.agents/skills/nsr-compliance/assets/nsrcompliance/gates/verify_docs.py +66 -0
  442. package/.agents/skills/nsr-compliance/assets/nsrcompliance/generate.sh +109 -0
  443. package/.agents/skills/nsr-compliance/assets/nsrcompliance/harness/envelopes/result_envelope.json +14 -0
  444. package/.agents/skills/nsr-compliance/assets/nsrcompliance/harness/envelopes/task_envelope.json +20 -0
  445. package/.agents/skills/nsr-compliance/assets/nsrcompliance/harness/runner.py +303 -0
  446. package/.agents/skills/nsr-compliance/assets/nsrcompliance/telemetry/logger.py +112 -0
  447. package/.agents/skills/nsr-compliance/assets/nsrcompliance/templates/compliance/README.md +18 -0
  448. package/.agents/skills/nsr-compliance/assets/nsrcompliance/templates/compliance/config/README.md +10 -0
  449. package/.agents/skills/nsr-compliance/assets/nsrcompliance/templates/compliance/gates/README.md +12 -0
  450. package/.agents/skills/nsr-compliance/assets/nsrcompliance/templates/compliance/gates/check_env.sh +23 -0
  451. package/.agents/skills/nsr-compliance/assets/nsrcompliance/templates/compliance/gates/check_paths.sh +20 -0
  452. package/.agents/skills/nsr-compliance/assets/nsrcompliance/templates/compliance/gates/check_platform.sh +23 -0
  453. package/.agents/skills/nsr-compliance/assets/nsrcompliance/templates/compliance/harness/README.md +11 -0
  454. package/.agents/skills/nsr-compliance/assets/nsrcompliance/templates/compliance/harness/envelopes/README.md +10 -0
  455. package/.agents/skills/nsr-compliance/assets/nsrcompliance/templates/compliance/installer/README.md +10 -0
  456. package/.agents/skills/nsr-compliance/assets/nsrcompliance/templates/compliance/installer/install.sh +125 -0
  457. package/.agents/skills/nsr-compliance/assets/nsrcompliance/templates/compliance/telemetry/README.md +11 -0
  458. package/.agents/skills/pr-ops/README.md +3 -0
  459. package/.agents/skills/pr-ops/SKILL.md +109 -0
  460. package/.agents/skills/ratatoskr-a2a/README.md +3 -0
  461. package/.agents/skills/ratatoskr-a2a/SKILL.md +148 -0
  462. package/.agents/skills/rules-check-drift/SKILL.md +81 -0
  463. package/.agents/skills/saga-bearings/README.md +3 -0
  464. package/.agents/skills/saga-bearings/SKILL.md +26 -0
  465. package/.agents/skills/saga-bearings/assets/bearings.md +161 -0
  466. package/.agents/skills/saga-bearings/assets/board-template.html +731 -0
  467. package/.agents/skills/saga-bearings/assets/recap.md +56 -0
  468. package/.agents/skills/syn-recovery/README.md +3 -0
  469. package/.agents/skills/syn-recovery/SKILL.md +54 -0
  470. package/.agents/skills/tyr-check/README.md +3 -0
  471. package/.agents/skills/tyr-check/SKILL.md +218 -0
  472. package/.agents/skills/urdh-hold/README.md +3 -0
  473. package/.agents/skills/urdh-hold/SKILL.md +25 -0
  474. package/.agents/skills/urdh-hold/assets/decisions.md +15 -0
  475. package/.agents/skills/urdh-hold/assets/hold.md +54 -0
  476. package/.agents/skills/vor-diagnostics/README.md +3 -0
  477. package/.agents/skills/vor-diagnostics/SKILL.md +68 -0
  478. package/.agents/skills/ymir-host/README.md +3 -0
  479. package/.agents/skills/ymir-host/SKILL.md +34 -0
  480. package/.agents/skills/ymir-host/assets/desktop.md +36 -0
  481. package/.agents/skills/ymir-host/assets/omarchy.md +297 -0
  482. package/.agents/skills/ymir-host/assets/runbooks.md +15 -0
  483. package/.agents/skills/ymir-host/assets/thjazi.md +220 -0
  484. package/.agents/skills/ymir-host/assets/update.md +64 -0
  485. package/.agents/tests/assets/board-render-harness.mjs +117 -0
  486. package/.agents/tests/cmux-test-safety.sh +51 -0
  487. package/.agents/tests/fixtures.sh +291 -0
  488. package/.agents/tests/fm-afk-inject-e2e.test.sh +428 -0
  489. package/.agents/tests/fm-afk-inject-herdr-e2e.test.sh +536 -0
  490. package/.agents/tests/fm-afk-launch.test.sh +961 -0
  491. package/.agents/tests/fm-afk-pi-herdr-return-e2e.test.sh +297 -0
  492. package/.agents/tests/fm-afk-return.test.sh +283 -0
  493. package/.agents/tests/fm-arm-pretool-check.test.sh +482 -0
  494. package/.agents/tests/fm-ask-user-authority.test.sh +42 -0
  495. package/.agents/tests/fm-backend-autodetect-smoke.test.sh +176 -0
  496. package/.agents/tests/fm-backend-cmux-smoke.test.sh +188 -0
  497. package/.agents/tests/fm-backend-cmux.test.sh +1166 -0
  498. package/.agents/tests/fm-backend-herdr-eventwait-smoke.test.sh +136 -0
  499. package/.agents/tests/fm-backend-herdr-eventwait.test.py +106 -0
  500. package/.agents/tests/fm-backend-herdr-focus-flash-e2e.test.sh +409 -0
  501. package/.agents/tests/fm-backend-herdr-launcher-workspace-e2e.test.sh +437 -0
  502. package/.agents/tests/fm-backend-herdr-presentation-e2e.test.sh +1424 -0
  503. package/.agents/tests/fm-backend-herdr-prune-safety-e2e.test.sh +180 -0
  504. package/.agents/tests/fm-backend-herdr-respawn-idem-e2e.test.sh +182 -0
  505. package/.agents/tests/fm-backend-herdr-smoke.test.sh +355 -0
  506. package/.agents/tests/fm-backend-herdr-workspace-per-home-e2e.test.sh +251 -0
  507. package/.agents/tests/fm-backend-herdr.test.sh +4606 -0
  508. package/.agents/tests/fm-backend-orca.test.sh +1357 -0
  509. package/.agents/tests/fm-backend-tmux-smoke.test.sh +173 -0
  510. package/.agents/tests/fm-backend-zellij-smoke.test.sh +205 -0
  511. package/.agents/tests/fm-backend-zellij.test.sh +1360 -0
  512. package/.agents/tests/fm-backend.test.sh +1142 -0
  513. package/.agents/tests/fm-backlog-atomicity.test.sh +2302 -0
  514. package/.agents/tests/fm-backlog-handoff.test.sh +1360 -0
  515. package/.agents/tests/fm-bearings-board-render.test.sh +135 -0
  516. package/.agents/tests/fm-bearings-board.test.sh +414 -0
  517. package/.agents/tests/fm-bearings-snapshot.test.sh +1990 -0
  518. package/.agents/tests/fm-bootstrap-network-parallel.test.sh +316 -0
  519. package/.agents/tests/fm-bootstrap.test.sh +1178 -0
  520. package/.agents/tests/fm-branch-supervision.test.sh +552 -0
  521. package/.agents/tests/fm-brief.test.sh +785 -0
  522. package/.agents/tests/fm-busy-adapter-wiring.test.sh +324 -0
  523. package/.agents/tests/fm-busy-state.test.sh +465 -0
  524. package/.agents/tests/fm-calm-pi-extension.test.sh +3979 -0
  525. package/.agents/tests/fm-captain-hold-lifecycle.test.sh +1187 -0
  526. package/.agents/tests/fm-cd-pretool-check.test.sh +400 -0
  527. package/.agents/tests/fm-check-unregister.test.sh +198 -0
  528. package/.agents/tests/fm-classify-corr-token.test.sh +543 -0
  529. package/.agents/tests/fm-classify-decision-key.test.sh +340 -0
  530. package/.agents/tests/fm-claude-stop-autoarm-live-e2e.test.sh +164 -0
  531. package/.agents/tests/fm-claude-stop-autoarm.test.sh +1190 -0
  532. package/.agents/tests/fm-cmux-claude-composer-live-e2e.test.sh +110 -0
  533. package/.agents/tests/fm-codex-continuity-live-e2e.test.sh +55 -0
  534. package/.agents/tests/fm-composer-ghost.test.sh +716 -0
  535. package/.agents/tests/fm-composer-lib.test.sh +666 -0
  536. package/.agents/tests/fm-composer-matrix-live-e2e.test.sh +220 -0
  537. package/.agents/tests/fm-control-herdr-smoke.test.sh +149 -0
  538. package/.agents/tests/fm-control-relaunch.test.sh +1530 -0
  539. package/.agents/tests/fm-control.test.sh +910 -0
  540. package/.agents/tests/fm-crew-state.test.sh +1610 -0
  541. package/.agents/tests/fm-cursor-harness.test.sh +404 -0
  542. package/.agents/tests/fm-cursor-primary-live-e2e.test.sh +217 -0
  543. package/.agents/tests/fm-cursor-primary.test.sh +708 -0
  544. package/.agents/tests/fm-daemon.test.sh +2740 -0
  545. package/.agents/tests/fm-documentation-audiences.test.sh +141 -0
  546. package/.agents/tests/fm-ensure-agents-md.test.sh +371 -0
  547. package/.agents/tests/fm-extension-binding.test.sh +2187 -0
  548. package/.agents/tests/fm-fleet-snapshot-view.test.sh +816 -0
  549. package/.agents/tests/fm-fleet-sync.test.sh +721 -0
  550. package/.agents/tests/fm-gate-refuse.test.sh +356 -0
  551. package/.agents/tests/fm-gitignore-config.test.sh +89 -0
  552. package/.agents/tests/fm-gotmp.test.sh +230 -0
  553. package/.agents/tests/fm-grok-continuity-live-e2e.test.sh +112 -0
  554. package/.agents/tests/fm-grok-harness.test.sh +117 -0
  555. package/.agents/tests/fm-grok-stop-live-e2e.test.sh +219 -0
  556. package/.agents/tests/fm-guard-stale-banner.test.sh +709 -0
  557. package/.agents/tests/fm-harness-adapter-instructions-live-e2e.test.sh +134 -0
  558. package/.agents/tests/fm-harness-adapter-references.test.sh +30 -0
  559. package/.agents/tests/fm-harness-liveness-drift-live-e2e.test.sh +151 -0
  560. package/.agents/tests/fm-herdr-lab.test.sh +243 -0
  561. package/.agents/tests/fm-herdr-session-cleanup-e2e.test.sh +146 -0
  562. package/.agents/tests/fm-herdr-session-cleanup.test.sh +329 -0
  563. package/.agents/tests/fm-herdr-submit-confirm-live-e2e.test.sh +125 -0
  564. package/.agents/tests/fm-herdr-version-floor-live-e2e.test.sh +144 -0
  565. package/.agents/tests/fm-home-summary-refresh.test.sh +971 -0
  566. package/.agents/tests/fm-inactive-reconcile.test.sh +485 -0
  567. package/.agents/tests/fm-kimi-harness.test.sh +687 -0
  568. package/.agents/tests/fm-lint-workflows.test.sh +532 -0
  569. package/.agents/tests/fm-lint.test.sh +1023 -0
  570. package/.agents/tests/fm-muse-harness.test.sh +932 -0
  571. package/.agents/tests/fm-muse-signals-live-e2e.test.sh +205 -0
  572. package/.agents/tests/fm-no-mistakes-required.test.sh +72 -0
  573. package/.agents/tests/fm-on.test.sh +507 -0
  574. package/.agents/tests/fm-opencode-primary-live-e2e.test.sh +357 -0
  575. package/.agents/tests/fm-operational-input.test.sh +160 -0
  576. package/.agents/tests/fm-peek-remote.test.sh +110 -0
  577. package/.agents/tests/fm-pending-reply.test.sh +1288 -0
  578. package/.agents/tests/fm-pi-branch-extension.test.sh +3182 -0
  579. package/.agents/tests/fm-pi-branch-live-e2e.test.sh +527 -0
  580. package/.agents/tests/fm-pi-primary-live-e2e.test.sh +345 -0
  581. package/.agents/tests/fm-pi-primary-types.test.sh +68 -0
  582. package/.agents/tests/fm-pi-watch-extension.test.sh +2843 -0
  583. package/.agents/tests/fm-pr-check-security.test.sh +2140 -0
  584. package/.agents/tests/fm-pr-merge.test.sh +2135 -0
  585. package/.agents/tests/fm-procevent-quota.test.sh +225 -0
  586. package/.agents/tests/fm-procevent-when.test.sh +406 -0
  587. package/.agents/tests/fm-procevent.test.sh +1745 -0
  588. package/.agents/tests/fm-project-origin.test.sh +110 -0
  589. package/.agents/tests/fm-public-followup.test.sh +2272 -0
  590. package/.agents/tests/fm-quota-array-dispatch-live-e2e.test.sh +526 -0
  591. package/.agents/tests/fm-quota-choose.test.sh +626 -0
  592. package/.agents/tests/fm-remote-backlog-handoff.test.sh +453 -0
  593. package/.agents/tests/fm-remote-doctor.test.sh +625 -0
  594. package/.agents/tests/fm-remote-entrypoint.test.sh +59 -0
  595. package/.agents/tests/fm-remote-job-orphan-reap.test.sh +205 -0
  596. package/.agents/tests/fm-remote-job.test.sh +768 -0
  597. package/.agents/tests/fm-remote-reply.test.sh +520 -0
  598. package/.agents/tests/fm-remote-secondmate-lifecycle-e2e.test.sh +1256 -0
  599. package/.agents/tests/fm-remote-secondmate-parent-binding.test.sh +326 -0
  600. package/.agents/tests/fm-remote-secondmate-trace-context.test.sh +310 -0
  601. package/.agents/tests/fm-remote-transport-lanes.test.sh +431 -0
  602. package/.agents/tests/fm-review-diff.test.sh +176 -0
  603. package/.agents/tests/fm-secondmate-harness.test.sh +2600 -0
  604. package/.agents/tests/fm-secondmate-lifecycle-e2e.test.sh +259 -0
  605. package/.agents/tests/fm-secondmate-liveness.test.sh +559 -0
  606. package/.agents/tests/fm-secondmate-reconcile.test.sh +732 -0
  607. package/.agents/tests/fm-secondmate-safety.test.sh +2972 -0
  608. package/.agents/tests/fm-secondmate-sync.test.sh +913 -0
  609. package/.agents/tests/fm-send-inbox-doorbell-live-e2e.test.sh +206 -0
  610. package/.agents/tests/fm-send-inbox.test.sh +352 -0
  611. package/.agents/tests/fm-send-popup-settle.test.sh +167 -0
  612. package/.agents/tests/fm-send-remote-delivery.test.sh +796 -0
  613. package/.agents/tests/fm-send-resolve-key.test.sh +551 -0
  614. package/.agents/tests/fm-send-secondmate-marker-herdr-e2e.test.sh +187 -0
  615. package/.agents/tests/fm-send-secondmate-marker.test.sh +278 -0
  616. package/.agents/tests/fm-send-settle.test.sh +147 -0
  617. package/.agents/tests/fm-send-strict.test.sh +241 -0
  618. package/.agents/tests/fm-session-lock-ancestry.test.sh +365 -0
  619. package/.agents/tests/fm-session-start.test.sh +2515 -0
  620. package/.agents/tests/fm-sessionstart-hook-live-e2e.test.sh +631 -0
  621. package/.agents/tests/fm-sessionstart-instruction-refresh-live-e2e.test.sh +230 -0
  622. package/.agents/tests/fm-sessionstart-nudge.test.sh +1039 -0
  623. package/.agents/tests/fm-shared-captain-inheritance.test.sh +404 -0
  624. package/.agents/tests/fm-spawn-batch.test.sh +148 -0
  625. package/.agents/tests/fm-spawn-dispatch-profile.test.sh +827 -0
  626. package/.agents/tests/fm-spawn-pool-base-freshen.test.sh +440 -0
  627. package/.agents/tests/fm-spawn-worktree-settle.test.sh +147 -0
  628. package/.agents/tests/fm-startup-memory-budget.test.sh +333 -0
  629. package/.agents/tests/fm-startup-network.test.sh +712 -0
  630. package/.agents/tests/fm-stow-cascade.test.sh +370 -0
  631. package/.agents/tests/fm-subagent-pretool-check.test.sh +291 -0
  632. package/.agents/tests/fm-supervision-events.test.sh +156 -0
  633. package/.agents/tests/fm-supervision-instructions.test.sh +189 -0
  634. package/.agents/tests/fm-tangle-guard.test.sh +277 -0
  635. package/.agents/tests/fm-task-delivery.test.sh +420 -0
  636. package/.agents/tests/fm-task-inbox.test.sh +513 -0
  637. package/.agents/tests/fm-teardown-endpoint-safety.test.sh +374 -0
  638. package/.agents/tests/fm-teardown.test.sh +2665 -0
  639. package/.agents/tests/fm-test-fixture-cleanup.test.sh +172 -0
  640. package/.agents/tests/fm-test-fixtures.test.sh +132 -0
  641. package/.agents/tests/fm-test-isolation-proof.test.sh +278 -0
  642. package/.agents/tests/fm-test-run.test.sh +1196 -0
  643. package/.agents/tests/fm-tmux-agent-liveness.test.sh +354 -0
  644. package/.agents/tests/fm-tmux-submit-busy.test.sh +340 -0
  645. package/.agents/tests/fm-tool-update-check.test.sh +1044 -0
  646. package/.agents/tests/fm-trace-context-lib.test.sh +253 -0
  647. package/.agents/tests/fm-trace-context-spawn.test.sh +581 -0
  648. package/.agents/tests/fm-transition-lib.test.sh +54 -0
  649. package/.agents/tests/fm-turnend-guard.test.sh +1822 -0
  650. package/.agents/tests/fm-update.test.sh +304 -0
  651. package/.agents/tests/fm-vendor-auth-probe.test.sh +395 -0
  652. package/.agents/tests/fm-voice-relay.test.sh +4741 -0
  653. package/.agents/tests/fm-wake-daemon-lifecycle-e2e.test.sh +171 -0
  654. package/.agents/tests/fm-wake-drain-open-decisions-cursor.test.sh +356 -0
  655. package/.agents/tests/fm-wake-drain-open-decisions.test.sh +226 -0
  656. package/.agents/tests/fm-wake-drain-unread-status.test.sh +379 -0
  657. package/.agents/tests/fm-wake-queue.test.sh +1224 -0
  658. package/.agents/tests/fm-watch-arm.test.sh +821 -0
  659. package/.agents/tests/fm-watch-checkpoint.test.sh +87 -0
  660. package/.agents/tests/fm-watch-recovery-loop.test.sh +223 -0
  661. package/.agents/tests/fm-watch-triage.test.sh +3889 -0
  662. package/.agents/tests/fm-watcher-lock.test.sh +1133 -0
  663. package/.agents/tests/fm-x-mode.test.sh +3042 -0
  664. package/.agents/tests/gleipnir-guard-pretool-check.test.sh +51 -0
  665. package/.agents/tests/gleipnir-machine-lock.test.sh +130 -0
  666. package/.agents/tests/herdr-test-safety.sh +42 -0
  667. package/.agents/tests/lib.sh +315 -0
  668. package/.agents/tests/remote-herdr-fixture.sh +125 -0
  669. package/.agents/tests/secondmate-helpers.sh +193 -0
  670. package/.agents/tests/smoke.test.sh +93 -0
  671. package/.agents/tests/wake-helpers.sh +338 -0
  672. package/.agents/tests/zellij-test-safety.sh +60 -0
  673. package/.agents/tools/README.md +10 -0
  674. package/.agents/tools/bin/tasks-cli +267 -0
  675. package/.agents/tools/tasks-cli.ts +216 -0
  676. package/AGENTS.md +454 -0
  677. package/CHANGELOG.md +881 -0
  678. package/LICENSE +1 -1
  679. package/NOTICE +63 -0
  680. package/README.md +113 -59
  681. package/RULES/01-domains.md +51 -0
  682. package/RULES/02-agents.md +78 -0
  683. package/RULES/03-houses.md +61 -0
  684. package/RULES/04-hoard.md +65 -0
  685. package/RULES/05-platforms.md +67 -0
  686. package/RULES/06-append-only.md +58 -0
  687. package/RULES/07-config.md +45 -0
  688. package/RULES/08-delivery-gate.md +75 -0
  689. package/RULES/README.md +20 -0
  690. package/bin/README.md +107 -0
  691. package/bin/a2a-mcp.sh +98 -0
  692. package/bin/a2a-serve.py +103 -0
  693. package/bin/a2a-serve.sh +8 -0
  694. package/bin/a2a-talk.sh +50 -0
  695. package/bin/a2abridge-ensure.sh +108 -0
  696. package/bin/agent-run.sh +43 -0
  697. package/bin/agents-config.sh +233 -0
  698. package/bin/apodex-smoke-test.sh +92 -0
  699. package/bin/bifrost-bridge.sh +112 -0
  700. package/bin/bifrost-ingress.sh +77 -0
  701. package/bin/bootstrap-macos.sh +107 -0
  702. package/bin/bootstrap-windows.ps1 +112 -0
  703. package/bin/branch-guard.sh +64 -0
  704. package/bin/brokk +73 -0
  705. package/bin/brokk-classify-lib.sh +1770 -0
  706. package/bin/brokk-lease-lib.sh +222 -0
  707. package/bin/brokk-lease.sh +193 -0
  708. package/bin/brokk-lint.sh +65 -0
  709. package/bin/brokk-send.sh +38 -0
  710. package/bin/brokk-update.sh +9 -0
  711. package/bin/brokk-wake-grant.sh +127 -0
  712. package/bin/brokk-wake-lib.sh +1849 -0
  713. package/bin/changelog-guard.sh +165 -0
  714. package/bin/crash-sense.sh +160 -0
  715. package/bin/desktop-place.sh +224 -0
  716. package/bin/docs-guard.sh +39 -0
  717. package/bin/editor-place.sh +207 -0
  718. package/bin/eindri-acclaim.sh +34 -0
  719. package/bin/eindri-control.sh +28 -0
  720. package/bin/eindri-role.sh +83 -0
  721. package/bin/eindri-seen.sh +41 -0
  722. package/bin/eindri-send.sh +15 -0
  723. package/bin/eindri-start.sh +85 -0
  724. package/bin/eindri-watch.sh +57 -0
  725. package/bin/einherjar-spawn.sh +576 -0
  726. package/bin/eir-doctor.sh +112 -0
  727. package/bin/erindi-brief.sh +271 -0
  728. package/bin/fleet-apply.sh +68 -0
  729. package/bin/github-deploy.sh +81 -0
  730. package/bin/gjallarhorn-expose.sh +102 -0
  731. package/bin/gjallarhorn-notify.sh +58 -0
  732. package/bin/gjallarhorn-purge.sh +54 -0
  733. package/bin/gjallarhorn-tunnel.sh +72 -0
  734. package/bin/gleipnir-lock-lib.sh +284 -0
  735. package/bin/groa-update.sh +122 -0
  736. package/bin/hall-snapshot.sh +92 -0
  737. package/bin/hamr-harness.sh +246 -0
  738. package/bin/heimdall-ssh-keys.sh +374 -0
  739. package/bin/herdr-agents.py +44 -0
  740. package/bin/herdr-ensure.sh +104 -0
  741. package/bin/herdr-run.sh +431 -0
  742. package/bin/hermes-ensure.sh +88 -0
  743. package/bin/hoard-lib.sh +25 -0
  744. package/bin/hodd.sh +78 -0
  745. package/bin/host-sense.sh +199 -0
  746. package/bin/huginn-research-worker.sh +108 -0
  747. package/bin/local-model-lock.sh +51 -0
  748. package/bin/mimir-bridge.py +226 -0
  749. package/bin/mimir-bridge.sh +87 -0
  750. package/bin/mimir-ingest.sh +131 -0
  751. package/bin/mimir.sh +113 -0
  752. package/bin/mjollnir-webhook.sh +59 -0
  753. package/bin/mjollnir.sh +85 -0
  754. package/bin/model-hardware.sh +82 -0
  755. package/bin/model-resolve.sh +147 -0
  756. package/bin/models-detect.sh +178 -0
  757. package/bin/nornir-cron-start.sh +162 -0
  758. package/bin/nornir-job-daily-briefing.sh +191 -0
  759. package/bin/nornir-job-git-sync.sh +152 -0
  760. package/bin/nornir-job-memory-housekeeping.sh +114 -0
  761. package/bin/nornir-job-observer.sh +166 -0
  762. package/bin/nornir-job-skillopt-sleep.sh +76 -0
  763. package/bin/omarchy-hook-install.sh +60 -0
  764. package/bin/omarchy-install.sh +148 -0
  765. package/bin/omarchy-plugins.sh +174 -0
  766. package/bin/omarchy-sense.sh +149 -0
  767. package/bin/perm-guard.sh +30 -0
  768. package/bin/pi-agent.sh +68 -0
  769. package/bin/pi-ensure.sh +77 -0
  770. package/bin/pi-local.sh +37 -0
  771. package/bin/pi-seat.sh +63 -0
  772. package/bin/prereq-ensure.sh +166 -0
  773. package/bin/private-guard.sh +38 -0
  774. package/bin/project-git.sh +55 -0
  775. package/bin/public-guard.sh +70 -0
  776. package/bin/ratatoskr.sh +49 -0
  777. package/bin/realm-lib.sh +32 -0
  778. package/bin/repo-scrub.sh +62 -0
  779. package/bin/rodd-operational-input.sh +250 -0
  780. package/bin/runes-append.sh +198 -0
  781. package/bin/saga-session-start.sh +172 -0
  782. package/bin/saga-sessionstart-run.sh +79 -0
  783. package/bin/saga-wake-drain.sh +41 -0
  784. package/bin/secret-guard.sh +63 -0
  785. package/bin/sessrumnir-ensure.sh +100 -0
  786. package/bin/sessrumnir-sync.sh +237 -0
  787. package/bin/sessrumnir.sh +91 -0
  788. package/bin/skillopt-setup.sh +76 -0
  789. package/bin/skuld-branch-outcome.sh +203 -0
  790. package/bin/skuld-branch-prompt.sh +101 -0
  791. package/bin/smidja-bootstrap.sh +90 -0
  792. package/bin/smidja-observe.sh +86 -0
  793. package/bin/syn-arm-pretool-check.sh +57 -0
  794. package/bin/syn-asset-pretool-check.sh +76 -0
  795. package/bin/syn-cd-pretool-check.sh +28 -0
  796. package/bin/syn-guard-pretool-check.sh +83 -0
  797. package/bin/syn-turnend-guard.sh +36 -0
  798. package/bin/syn-watch-arm.sh +99 -0
  799. package/bin/tailscale-sync.sh +190 -0
  800. package/bin/telegram-bot.sh +69 -0
  801. package/bin/toolchain.sh +74 -0
  802. package/bin/utgard.sh +111 -0
  803. package/bin/valhalla.sh +115 -0
  804. package/bin/valknut-load.sh +260 -0
  805. package/bin/vor-crew-state.sh +124 -0
  806. package/bin/wedge-notify.sh +47 -0
  807. package/bin/workspace-provision.sh +80 -0
  808. package/bin/workspace-rag.sh +150 -0
  809. package/bin/wyrd-db.sh +47 -0
  810. package/bin/yggdrasil.sh +152 -0
  811. package/bin/ymir-edit.sh +145 -0
  812. package/bin/ymir-install.sh +641 -0
  813. package/bin/ymir-invite.sh +93 -0
  814. package/bin/ymir-isolation.sh +88 -0
  815. package/bin/ymir-migrate.sh +61 -0
  816. package/bin/ymir-platform.sh +348 -0
  817. package/bin/ymir-say.sh +97 -0
  818. package/bin/ymir-setup-auth.sh +108 -0
  819. package/bin/ymir-validate.sh +173 -0
  820. package/bin/ymir.js +74 -16
  821. package/docs/Architecture.md +339 -0
  822. package/docs/design.md +351 -0
  823. package/docs/installations/README.md +63 -0
  824. package/docs/installations/ymir-home.md +93 -0
  825. package/docs/integration.md +105 -0
  826. package/docs/lore.md +1056 -0
  827. package/docs/research/agent-harnesses.md +290 -0
  828. package/docs/runbooks/README.md +19 -0
  829. package/docs/runbooks/a2a.md +91 -0
  830. package/docs/runbooks/agent-permissions.md +99 -0
  831. package/docs/runbooks/agents.md +57 -0
  832. package/docs/runbooks/models.md +69 -0
  833. package/docs/runbooks/realm-onboarding.md +60 -0
  834. package/docs/runbooks/secrets-and-hoard.md +60 -0
  835. package/docs/runbooks/updates-and-migrations.md +110 -0
  836. package/docs/runbooks/worktrees-isolation.md +72 -0
  837. package/docs/session-start.md +105 -0
  838. package/docs/skillopt-integration.md +321 -0
  839. package/docs/sync-system.md +66 -0
  840. package/docs/workspaces.md +104 -0
  841. package/docs/ymir-rut.md +260 -0
  842. package/package.json +44 -14
  843. package/packaging/build.sh +74 -0
  844. package/packaging/macos/Ymir Installer.command +43 -0
  845. package/packaging/windows/ymir-setup.nsi +74 -0
  846. package/scripts/README.md +22 -0
  847. package/scripts/electron.sh +210 -0
  848. package/scripts/start.sh +167 -0
  849. package/scripts/stop.sh +113 -0
  850. package/install.sh +0 -35
@@ -0,0 +1,1330 @@
1
+ #!/usr/bin/env bash
2
+ # fm-public-followup.sh - the deterministic consumer and delivery owner for
3
+ # public commitments made through the myfirstmate relay (X and Discord).
4
+ #
5
+ # THE PROBLEM THIS SOLVES: firstmate promises a public final reply, routes the
6
+ # work out, and then the conversation compacts or the session restarts. Nothing
7
+ # in memory survives, so the promise is only kept if reconciling it is a disk
8
+ # operation. Every command here reads durable state and nothing else.
9
+ #
10
+ # OWNERSHIP BOUNDARIES (do not re-implement any of these here):
11
+ # tasks-axi public-followup the typed obligation and its state machine.
12
+ # state/x-context/ the private full request context (fm-x-lib.sh).
13
+ # bin/fm-x-reply.sh posting to the relay, thread splitting, dry run.
14
+ # bin/fm-public-followup-lib.sh the activation gate and private transport.
15
+ # This script composes them; it never restates their contracts or schemas.
16
+ #
17
+ # ZERO OVERHEAD FOR HOMES THAT DO NOT USE THE RELAY: every subcommand gates
18
+ # first on the authoritative activation contract (a non-empty FMX_PAIRING_TOKEN
19
+ # in $FM_HOME/.env). Read-side and cleanup paths then use an O(1) presence check
20
+ # for registrations this home actually created. A relay-disabled home therefore
21
+ # runs one [ -f ] test before any backlog work: no tasks-axi call, no backlog scan,
22
+ # and no file created. Silent read-side commands return without output; commands
23
+ # that require an active relay report their configuration error after the same
24
+ # gate. A relay-enabled home with no live commitments stops at the second gate
25
+ # for the same cost.
26
+ #
27
+ # Usage:
28
+ # fm-public-followup.sh active
29
+ # Silent gate probe. Exit 0 when this home has live public-followup work
30
+ # worth looking at, including a delivered open loop, 1 otherwise. Safe to
31
+ # call unconditionally.
32
+ #
33
+ # fm-public-followup.sh register <obligation-id> --relation <relation-id>
34
+ # --work-home <main|secondmate:<id>> --work-id <task-id> --generation <n>
35
+ # [--platform <x|discord>] [--request <request-id>]
36
+ # Record the binding the relay path just created with `tasks-axi
37
+ # public-followup add` + `bind-work`. This is the event-driven
38
+ # registration: it creates this home's private public-followup directories
39
+ # (0700) and the bounded public-safe registration record, which is what
40
+ # later makes the presence checks O(1) and lets bound work report a typed
41
+ # terminal result. Refuses when the relay is not active for this home.
42
+ #
43
+ # fm-public-followup.sh brief <obligation-id>
44
+ # Print the exact fm-public-followup-emit.sh command line the bound worker
45
+ # must run when its work reaches the promised terminal outcome, so the
46
+ # binding is copied into a brief instead of hand-assembled. The
47
+ # --deliverable flags name the obligation's actual required keys.
48
+ #
49
+ # fm-public-followup.sh consume
50
+ # Drain every pending typed terminal event: validate its derived identity,
51
+ # skip anything already accepted, apply `tasks-axi public-followup
52
+ # work-event`, and quarantine what tasks-axi refuses. Prints one
53
+ # "ready <obligation-id> <request-id> <platform>" line per obligation that
54
+ # became delivery-ready, and one "rejected <event-id>: <reason>" line per
55
+ # refusal. Silent when there is nothing to do. Duplicate events and restart
56
+ # replay are no-ops.
57
+ #
58
+ # fm-public-followup.sh pending
59
+ # One bounded public-safe line per open public loop, for the session
60
+ # start digest. Unresolved commitments print as "unresolved" (a reply is
61
+ # still owed). Delivered or settled registrations print as "open-loop"
62
+ # (the thread is still open with nothing owed). Registrations are never
63
+ # pruned here; only `retire` removes one. Silent when nothing is open.
64
+ #
65
+ # fm-public-followup.sh deliver <obligation-id> [--text-file <path>]
66
+ # Post the final public reply into the ORIGINAL thread. Uses the stored
67
+ # platform and opaque context binding, so the destination is never guessed.
68
+ # Without --text-file the accepted terminal event's bounded public-safe
69
+ # outcome is reused exactly, which keeps the common path deterministic.
70
+ # The sequence is begin-delivery with the payload hash, post, then record
71
+ # the posted receipt or a typed error. A validated receipt also clears any
72
+ # bound legacy X link, then stamps the registration state=delivered. Delivery
73
+ # does not close the public loop; `retire` is the only close. Prints a
74
+ # disposition line so the loop is handed on with `rechain` or closed
75
+ # explicitly. An already-posted obligation is an idempotent success
76
+ # without another post; an obligation left in delivery-posting by a crash
77
+ # is REFUSED rather than posted again.
78
+ #
79
+ # fm-public-followup.sh record-posted <obligation-id> --attempt <n> --chunks <n>
80
+ # Record an obligation whose post is known to have landed on exactly
81
+ # attempt <n> with exactly <n> messages, without posting anything. This is
82
+ # the late-receipt path: use it when a post succeeded but its receipt was
83
+ # lost, never to paper over an unknown outcome. Stamps the registration
84
+ # delivered; does not remove it.
85
+ #
86
+ # fm-public-followup.sh guard-work <work-home-id> <work-id>
87
+ # Exit 3 when this home has an unresolved public commitment bound to that
88
+ # exact work, printing one line per blocking obligation. Exit 0 otherwise.
89
+ # Cleanup paths call this so bound work is never treated as finished while
90
+ # its public promise is still open. A delivered registration is not a
91
+ # block: that work's reply already landed.
92
+ #
93
+ # fm-public-followup.sh rechain <new-obligation-id> --from <delivered-id>
94
+ # --work-home <main|secondmate:<id>> --work-id <task-id>
95
+ # --expected <pr-merged|report-ready|local-main>
96
+ # [--deliverable-key <k>]...
97
+ # Hand a delivered public loop on to follow-on work against the same
98
+ # thread. Decodes the retained request context, creates and binds a fresh
99
+ # promised-final obligation, registers it, retires the source with reason
100
+ # "handed on to <new-id>", and prints `brief` for the new obligation.
101
+ # Refuses unless the source is state=delivered, the follow-up window is
102
+ # still open, and the relay is active. A pre-change record without
103
+ # request_context_b64 is un-rechainable.
104
+ #
105
+ # fm-public-followup.sh retire <obligation-id> --reason "<why the loop is done>" [--force]
106
+ # The only close. Drops the registration after recording --reason.
107
+ # --force is the explicit discard-approved escape hatch for an unresolved
108
+ # or missing obligation. --reason is required.
109
+ #
110
+ # Requires jq and a compatible tasks-axi for registration, briefs,
111
+ # reconciliation, delivery, cleanup guards, and retirement; only `active`
112
+ # inspects local state alone.
113
+ # FM_PF_RETRY_BACKOFF_SECS (default 900) sets the next-attempt time recorded with
114
+ # a retryable delivery error.
115
+ set -u
116
+
117
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
118
+ FM_ROOT="${FM_ROOT_OVERRIDE:-$(cd "$SCRIPT_DIR/.." && pwd)}"
119
+ FM_HOME="${FM_HOME:-${FM_ROOT_OVERRIDE:-$FM_ROOT}}"
120
+ STATE="${FM_STATE_OVERRIDE:-$FM_HOME/state}"
121
+ DATA="${FM_DATA_OVERRIDE:-$FM_HOME/data}"
122
+
123
+ # shellcheck source=bin/fm-public-followup-lib.sh
124
+ . "$SCRIPT_DIR/fm-public-followup-lib.sh"
125
+ # shellcheck source=bin/fm-secondmate-registry-lib.sh
126
+ . "$SCRIPT_DIR/fm-secondmate-registry-lib.sh"
127
+
128
+ RETRY_BACKOFF=${FM_PF_RETRY_BACKOFF_SECS:-900}
129
+ case "$RETRY_BACKOFF" in ''|*[!0-9]*) RETRY_BACKOFF=900 ;; esac
130
+
131
+ usage() {
132
+ echo "usage: fm-public-followup.sh <active|register|brief|consume|pending|deliver|record-posted|guard-work|rechain|retire> [args]" >&2
133
+ }
134
+
135
+ # The header comment IS the help text, so the two can never drift apart.
136
+ help() { sed -n '2,/^set -u$/p' "$0" | sed '$d; s/^# \{0,1\}//'; }
137
+
138
+ die() { printf 'fm-public-followup: %s\n' "$1" >&2; exit "${2:-2}"; }
139
+
140
+ PF_TEMP_FILES=()
141
+ PF_REGISTRY_LOCK_IDS=()
142
+ pf_registry_lock_held() {
143
+ local wanted=$1 held
144
+ for held in "${PF_REGISTRY_LOCK_IDS[@]}"; do
145
+ [ "$held" = "$wanted" ] && return 0
146
+ done
147
+ return 1
148
+ }
149
+ pf_registry_lock_acquire() {
150
+ local id=$1
151
+ pf_registry_lock_held "$id" && return 0
152
+ fm_pf_registry_lock_acquire "$STATE" "$id" || return 1
153
+ PF_REGISTRY_LOCK_IDS+=("$id")
154
+ }
155
+ pf_registry_lock_release() {
156
+ local id=$1 held
157
+ local -a remaining=()
158
+ pf_registry_lock_held "$id" || return 0
159
+ fm_pf_registry_lock_release "$STATE" "$id"
160
+ for held in "${PF_REGISTRY_LOCK_IDS[@]}"; do
161
+ [ "$held" = "$id" ] || remaining+=("$held")
162
+ done
163
+ PF_REGISTRY_LOCK_IDS=("${remaining[@]}")
164
+ }
165
+ pf_cleanup() {
166
+ local i
167
+ for ((i=${#PF_REGISTRY_LOCK_IDS[@]}-1; i>=0; i--)); do
168
+ fm_pf_registry_lock_release "$STATE" "${PF_REGISTRY_LOCK_IDS[$i]}" 2>/dev/null || true
169
+ done
170
+ [ "${#PF_TEMP_FILES[@]}" -eq 0 ] || rm -f -- "${PF_TEMP_FILES[@]}"
171
+ }
172
+ trap pf_cleanup EXIT
173
+
174
+ now_rfc3339() { fm_pf_now_rfc3339; }
175
+
176
+ # next_attempt_rfc3339: the retry time recorded with a retryable delivery error.
177
+ # BSD and GNU date disagree on the flag, so try both and print nothing when
178
+ # neither works - the error is still recorded, just without a retry time.
179
+ next_attempt_rfc3339() {
180
+ local at
181
+ at=$(( $(date +%s) + RETRY_BACKOFF ))
182
+ date -u -r "$at" +%Y-%m-%dT%H:%M:%SZ 2>/dev/null \
183
+ || date -u -d "@$at" +%Y-%m-%dT%H:%M:%SZ 2>/dev/null \
184
+ || true
185
+ }
186
+
187
+ require_tools() {
188
+ command -v jq >/dev/null 2>&1 || die "jq is required" 1
189
+ command -v tasks-axi >/dev/null 2>&1 || die "tasks-axi is required" 1
190
+ }
191
+
192
+ # Every tasks-axi call runs from the home whose backlog owns the obligation, the
193
+ # same convention bin/fm-captain-hold.sh uses for typed backlog state.
194
+ tx() { (cd "$FM_HOME" && tasks-axi "$@"); }
195
+
196
+ # obligation_json <id>: the complete typed obligation payload on stdout, empty
197
+ # when the backlog simply has no such public-followup item, and a non-zero exit
198
+ # ONLY when the backlog could not be read at all. Callers depend on that
199
+ # distinction to report the right thing, so jq runs without -e here. tasks-axi
200
+ # stays the single source of truth; the registration record is never consulted
201
+ # for state.
202
+ obligation_json() {
203
+ local id=$1 out
204
+ out=$(tx public-followup list --json 2>/dev/null) || return 1
205
+ [ -n "$out" ] || return 1
206
+ printf '%s' "$out" | jq -c --arg id "$id" \
207
+ '(.public_followups // []) | map(select(.id == $id)) | .[0] // empty' 2>/dev/null \
208
+ || return 1
209
+ }
210
+
211
+ pf_field() { printf '%s' "$1" | jq -r "$2 // empty" 2>/dev/null; }
212
+
213
+ # --- gates ------------------------------------------------------------------
214
+
215
+ # gate_or_exit: the shared silent gate for every read-side subcommand. Exits 0
216
+ # with no output when this home has no public-followup work, so callers can
217
+ # invoke unconditionally without a relay-disabled home paying anything.
218
+ gate_or_exit() {
219
+ fm_pf_relay_active "$FM_HOME" || exit 0
220
+ fm_pf_has_registrations "$STATE" || fm_pf_has_events "$STATE" || exit 0
221
+ }
222
+
223
+ # --- subcommand: active -----------------------------------------------------
224
+
225
+ cmd_active() {
226
+ fm_pf_relay_active "$FM_HOME" || exit 1
227
+ fm_pf_has_registrations "$STATE" || fm_pf_has_events "$STATE" || exit 1
228
+ exit 0
229
+ }
230
+
231
+ # --- subcommand: register ---------------------------------------------------
232
+
233
+ cmd_register() {
234
+ local id=${1:-}
235
+ local relation='' work_home='' work_id='' generation='' platform='' request=''
236
+ [ -n "$id" ] || { usage; exit 2; }
237
+ shift
238
+ while [ "$#" -gt 0 ]; do
239
+ case "$1" in
240
+ --relation) shift; relation=${1:-} ;;
241
+ --work-home) shift; work_home=${1:-} ;;
242
+ --work-id) shift; work_id=${1:-} ;;
243
+ --generation) shift; generation=${1:-} ;;
244
+ --platform) shift; platform=${1:-} ;;
245
+ --request) shift; request=${1:-} ;;
246
+ *) die "unknown argument '$1'" ;;
247
+ esac
248
+ shift || true
249
+ done
250
+
251
+ fm_pf_relay_active "$FM_HOME" \
252
+ || die "this home has not opted into the myfirstmate relay, so it cannot own a public commitment" 1
253
+ require_tools
254
+
255
+ fm_pf_slug_valid "$id" || die "unsafe obligation id: $id"
256
+ fm_pf_slug_valid "$relation" || die "unsafe relation id: $relation"
257
+ fm_pf_slug_valid "$work_id" || die "unsafe work id: $work_id"
258
+ fm_pf_home_id_valid "$work_home" \
259
+ || die "work home must be 'main' or 'secondmate:<stable-id>', got '$work_home'"
260
+ case "$generation" in
261
+ ''|*[!0-9]*) die "generation must be a positive integer, got '$generation'" ;;
262
+ esac
263
+ [ "$generation" -ge 1 ] || die "generation must be >= 1"
264
+
265
+ local payload
266
+ payload=$(obligation_json "$id") \
267
+ || die "could not read the backlog through tasks-axi" 1
268
+ [ -n "$payload" ] \
269
+ || die "no public-followup obligation '$id' in this home's backlog; create it with tasks-axi public-followup add before registering" 1
270
+
271
+ # The relation must already be bound, so a registration can never describe a
272
+ # binding tasks-axi does not have.
273
+ printf '%s' "$payload" | jq -e --arg r "$relation" --arg h "$work_home" --arg w "$work_id" \
274
+ '(.public_followup.work_relations // [])
275
+ | map(select(.relation_id == $r and .work_ref.home_id == $h and .work_ref.task_id == $w))
276
+ | length > 0' >/dev/null 2>&1 \
277
+ || die "obligation '$id' has no bound relation '$relation' for $work_home/$work_id; run tasks-axi public-followup bind-work first" 1
278
+
279
+ [ -n "$platform" ] || platform=$(pf_field "$payload" '.public_followup.request.platform')
280
+ [ -n "$request" ] || request=$(pf_field "$payload" '.public_followup.request.request_id')
281
+ [ -z "$request" ] || fm_pf_slug_valid "$request" || die "unsafe request id: $request"
282
+
283
+ local followup_expires_at request_json request_context_b64 work_home_path
284
+ followup_expires_at=$(pf_field "$payload" '.public_followup.request.followup_expires_at')
285
+ request_json=$(printf '%s' "$payload" | jq -c '.public_followup.request // empty' 2>/dev/null || true)
286
+ request_context_b64=
287
+ if [ -n "$request_json" ]; then
288
+ request_context_b64=$(printf '%s' "$request_json" | fm_pf_b64_encode)
289
+ fi
290
+ work_home_path=
291
+ case "$work_home" in
292
+ secondmate:*)
293
+ work_home_path=$(public_followup_secondmate_home "${work_home#secondmate:}" 2>/dev/null || true)
294
+ case "$work_home_path" in
295
+ *$'\n'*|*$'\r'*) work_home_path= ;;
296
+ esac
297
+ ;;
298
+ esac
299
+
300
+ local mkdir_target registry_state retired_file
301
+ for mkdir_target in "$(fm_pf_registry_dir "$STATE")" "$(fm_pf_events_dir "$STATE")" \
302
+ "$(fm_pf_consumed_dir "$STATE")" "$(fm_pf_rejected_dir "$STATE")"; do
303
+ fmx_private_artifact_dir_prepare "$mkdir_target" >/dev/null \
304
+ || die "could not prepare $mkdir_target" 1
305
+ done
306
+
307
+ pf_registry_lock_acquire "$id" \
308
+ || die "could not lock registration '$id'" 1
309
+ retired_file="$(fm_pf_retired_dir "$STATE")/$id"
310
+ if [ -e "$retired_file" ] || [ -L "$retired_file" ]; then
311
+ die "public loop '$id' has already been retired and cannot be registered again" 1
312
+ fi
313
+ registry_state=$(fm_pf_registry_loop_state "$STATE" "$id")
314
+ if [ "$registry_state" = delivered ]; then
315
+ pf_registry_lock_release "$id"
316
+ printf 'already registered %s state=delivered\n' "$id"
317
+ return 0
318
+ fi
319
+ printf 'obligation_id=%s\nrelation_id=%s\nwork_home=%s\nwork_home_path=%s\nwork_id=%s\ngeneration=%s\nplatform=%s\nrequest_id=%s\nstate=open\nfollowup_expires_at=%s\nrequest_context_b64=%s\n' \
320
+ "$id" "$relation" "$work_home" "$work_home_path" "$work_id" "$generation" "$platform" "$request" \
321
+ "$followup_expires_at" "$request_context_b64" \
322
+ | fmx_private_artifact_publish_stdin "$(fm_pf_registry_dir "$STATE")" "$id" 600 \
323
+ || die "could not write the registration record" 1
324
+ pf_registry_lock_release "$id"
325
+
326
+ printf 'registered %s %s/%s generation=%s platform=%s\n' \
327
+ "$id" "$work_home" "$work_id" "$generation" "${platform:-unknown}"
328
+ }
329
+
330
+ # --- subcommand: brief ------------------------------------------------------
331
+
332
+ cmd_brief() {
333
+ local id=${1:-} relation work_home work_id generation payload outcome keys key deliverable_flags
334
+ [ -n "$id" ] || { usage; exit 2; }
335
+ fm_pf_slug_valid "$id" || die "unsafe obligation id: $id"
336
+ fm_pf_relay_active "$FM_HOME" || die "the relay is not active for this home" 1
337
+ [ -f "$(fm_pf_registry_dir "$STATE")/$id" ] \
338
+ || die "no registration for '$id' in this home" 1
339
+
340
+ relation=$(fm_pf_registry_get "$STATE" "$id" relation_id)
341
+ work_home=$(fm_pf_registry_get "$STATE" "$id" work_home)
342
+ work_id=$(fm_pf_registry_get "$STATE" "$id" work_id)
343
+ generation=$(fm_pf_registry_get "$STATE" "$id" generation)
344
+
345
+ require_tools
346
+ payload=$(obligation_json "$id") \
347
+ || die "could not read public-followup obligation '$id' through tasks-axi" 1
348
+ [ -n "$payload" ] \
349
+ || die "public-followup obligation '$id' is missing from tasks-axi" 1
350
+ outcome=$(pf_field "$payload" '.public_followup.expected_final.type')
351
+ [ -n "$outcome" ] \
352
+ || die "public-followup obligation '$id' has no expected final type" 1
353
+ keys=$(printf '%s' "$payload" \
354
+ | jq -er '.public_followup.expected_final.required_deliverables
355
+ | select(type == "array" and length > 0
356
+ and (map(type == "string" and test("^[a-z0-9_]+$")) | all))
357
+ | .[]' 2>/dev/null) \
358
+ || die "public-followup obligation '$id' has no readable required deliverable keys" 1
359
+ deliverable_flags=
360
+ while IFS= read -r key; do
361
+ [ -n "$key" ] || continue
362
+ deliverable_flags="${deliverable_flags} --deliverable ${key}=<value> \\
363
+ "
364
+ done <<EOF
365
+ $keys
366
+ EOF
367
+
368
+ cat <<EOF
369
+ When this work reaches its promised terminal outcome, report it as typed data
370
+ (never as a sentence for someone to parse) by running exactly:
371
+
372
+ $FM_ROOT/bin/fm-public-followup-emit.sh \\
373
+ --home $FM_HOME \\
374
+ --obligation $id \\
375
+ --relation $relation \\
376
+ --source-home $work_home \\
377
+ --work-id $work_id \\
378
+ --generation $generation \\
379
+ --outcome $outcome \\
380
+ ${deliverable_flags} --outcome-text '<one bounded public-safe sentence>'
381
+
382
+ Do not post anything publicly yourself and do not look for the public thread:
383
+ the home above owns the reply.
384
+ EOF
385
+ }
386
+
387
+ # --- subcommand: consume ----------------------------------------------------
388
+
389
+ # reject_event <file> <event-id> <reason>: quarantine one refused event with an
390
+ # inspectable reason so it is never retried in a loop.
391
+ reject_event() {
392
+ local file=$1 event_id=$2 reason=$3 rejected event_payload
393
+ rejected=$(fm_pf_rejected_dir "$STATE")
394
+ fmx_private_artifact_dir_prepare "$rejected" >/dev/null \
395
+ || { printf 'rejected %s: %s (quarantine failed; event retained)\n' "$event_id" "$reason"; return 1; }
396
+ if ! printf '%s\n' "$reason" \
397
+ | fmx_private_artifact_publish_stdin "$rejected" "$event_id.reason" 600 2>/dev/null; then
398
+ printf 'rejected %s: %s (quarantine failed; event retained)\n' "$event_id" "$reason"
399
+ return 1
400
+ fi
401
+ if ! event_payload=$(cat "$file" 2>/dev/null); then
402
+ printf 'rejected %s: %s (quarantine failed; event retained)\n' "$event_id" "$reason"
403
+ return 1
404
+ fi
405
+ if ! printf '%s' "$event_payload" \
406
+ | fmx_private_artifact_publish_stdin "$rejected" "$event_id.json" 600 2>/dev/null; then
407
+ printf 'rejected %s: %s (quarantine failed; event retained)\n' "$event_id" "$reason"
408
+ return 1
409
+ fi
410
+ if ! rm -f -- "$file" 2>/dev/null; then
411
+ printf 'rejected %s: %s (quarantine cleanup failed; event retained)\n' "$event_id" "$reason"
412
+ return 1
413
+ fi
414
+ printf 'rejected %s: %s\n' "$event_id" "$reason"
415
+ }
416
+
417
+ cmd_consume() {
418
+ gate_or_exit
419
+ fm_pf_has_events "$STATE" || exit 0
420
+ require_tools
421
+
422
+ local events_dir consumed_dir stderr_file file event_id payload derived out rc reason consume_rc=0
423
+ local obligation delivery request platform
424
+ events_dir=$(fm_pf_events_dir "$STATE")
425
+ consumed_dir=$(fm_pf_consumed_dir "$STATE")
426
+ fmx_private_artifact_dir_prepare "$consumed_dir" >/dev/null \
427
+ || die "could not prepare the consumed-event ledger" 1
428
+ stderr_file=$(mktemp "${TMPDIR:-/tmp}/fm-pf-consume.XXXXXX") \
429
+ || die "could not stage the reconciliation log" 1
430
+ PF_TEMP_FILES+=("$stderr_file")
431
+
432
+ for file in "$events_dir"/*.json; do
433
+ [ -f "$file" ] && [ ! -L "$file" ] || continue
434
+ event_id=$(basename "$file" .json)
435
+
436
+ if ! fm_pf_slug_valid "$event_id"; then
437
+ printf 'rejected %s: unsafe event filename (event retained)\n' "$event_id"
438
+ consume_rc=1
439
+ continue
440
+ fi
441
+
442
+ # Already accepted on an earlier pass (duplicate emit, or a replay after
443
+ # restart): drop the copy without touching the state machine.
444
+ if [ -f "$consumed_dir/$event_id" ]; then
445
+ rm -f -- "$file" 2>/dev/null || true
446
+ continue
447
+ fi
448
+
449
+ if [ "$(wc -c < "$file" 2>/dev/null || echo 0)" -gt "$FM_PF_EVENT_BYTES_MAX" ]; then
450
+ reject_event "$file" "$event_id" "event exceeds $FM_PF_EVENT_BYTES_MAX bytes" || consume_rc=1
451
+ continue
452
+ fi
453
+
454
+ if ! payload=$(jq -ce . "$file" 2>/dev/null) || [ -z "$payload" ]; then
455
+ reject_event "$file" "$event_id" "event is not valid JSON" || consume_rc=1
456
+ continue
457
+ fi
458
+
459
+ # The filename, the declared event_id, and the identity tuple must all agree.
460
+ # A mismatch means the file was hand-edited or built by something other than
461
+ # fm-public-followup-emit.sh, so it is refused before tasks-axi sees it.
462
+ if [ "$(pf_field "$payload" '.event_id')" != "$event_id" ]; then
463
+ reject_event "$file" "$event_id" "declared event_id does not match the filename" || consume_rc=1
464
+ continue
465
+ fi
466
+ derived=$(fm_pf_event_id \
467
+ "$(pf_field "$payload" '.obligation_id')" \
468
+ "$(pf_field "$payload" '.relation_id')" \
469
+ "$(pf_field "$payload" '.source_home_id')" \
470
+ "$(pf_field "$payload" '.work_id')" \
471
+ "$(pf_field "$payload" '.generation')" \
472
+ "$(pf_field "$payload" '.outcome_type')" \
473
+ "$(printf '%s' "$payload" | jq -Sc '.deliverables // {}' 2>/dev/null)")
474
+ if [ -z "$derived" ] || [ "$derived" != "$event_id" ]; then
475
+ reject_event "$file" "$event_id" "event id does not match its own identity fields" || consume_rc=1
476
+ continue
477
+ fi
478
+
479
+ obligation=$(pf_field "$payload" '.obligation_id')
480
+ if ! fm_pf_slug_valid "$obligation"; then
481
+ reject_event "$file" "$event_id" "unsafe obligation id in event" || consume_rc=1
482
+ continue
483
+ fi
484
+
485
+ # tasks-axi is the authority on source home, work id, generation, schema,
486
+ # outcome, and deliverables. Anything it refuses is quarantined verbatim.
487
+ # stderr is captured separately so a warning can never corrupt the JSON that
488
+ # the accepted path parses.
489
+ if out=$(tx public-followup work-event "$obligation" --event-file "$file" --json 2>"$stderr_file"); then
490
+ rc=0
491
+ else
492
+ rc=$?
493
+ fi
494
+ if [ "$rc" -ne 0 ]; then
495
+ reason=$( { cat "$stderr_file" 2>/dev/null; printf '%s\n' "$out"; } \
496
+ | grep -v '^[[:space:]]*$' | head -1 | fm_pf_clean_outcome_text | fm_pf_bound_bytes 400)
497
+ reject_event "$file" "$event_id" "${reason:-tasks-axi refused the event}" || consume_rc=1
498
+ continue
499
+ fi
500
+
501
+ if ! printf 'accepted %s\n' "$(now_rfc3339)" \
502
+ | fmx_private_artifact_publish_stdin "$consumed_dir" "$event_id" 600 2>/dev/null; then
503
+ printf 'accepted %s: consumed ledger could not be recorded; event retained for reconciliation\n' "$event_id"
504
+ consume_rc=1
505
+ continue
506
+ fi
507
+ if ! rm -f -- "$file" 2>/dev/null; then
508
+ printf 'accepted %s: consumed ledger recorded but event could not be removed; event retained for reconciliation\n' "$event_id"
509
+ consume_rc=1
510
+ continue
511
+ fi
512
+
513
+ delivery=$(printf '%s' "$out" | jq -r '.task.public_followup.delivery.state // empty' 2>/dev/null)
514
+ if [ "$delivery" = ready ]; then
515
+ request=$(printf '%s' "$out" | jq -r '.task.public_followup.request.request_id // empty' 2>/dev/null)
516
+ platform=$(printf '%s' "$out" | jq -r '.task.public_followup.request.platform // empty' 2>/dev/null)
517
+ printf 'ready %s %s %s\n' "$obligation" "${request:-unknown}" "${platform:-unknown}"
518
+ fi
519
+ done
520
+
521
+ # A fresh event must be able to wake firstmate again, so drop the surfaced
522
+ # signature once the inbox has been worked.
523
+ rm -f -- "$(fm_pf_root "$STATE")/$FM_PF_SURFACED_BASENAME" 2>/dev/null || true
524
+ return "$consume_rc"
525
+ }
526
+
527
+ # --- subcommand: pending ----------------------------------------------------
528
+
529
+ # print_open_loop <id> <payload>: the session-start line for a public loop that
530
+ # is still open after delivery (or whose obligation has left the backlog).
531
+ print_window_escalation() {
532
+ local expires=$1 window
533
+ window=$(fm_pf_followup_window_class "$expires")
534
+ case "$window" in
535
+ expired)
536
+ printf ' DEADLINE: thread can no longer be reached (window closed %s); this needs a captain decision\n' \
537
+ "${expires:-unknown}"
538
+ ;;
539
+ closing)
540
+ printf ' DEADLINE: window closes %s (under 48 hours)\n' "${expires:-unknown}"
541
+ ;;
542
+ esac
543
+ }
544
+
545
+ print_open_loop() {
546
+ local id=$1 payload=$2 request platform summary delivered expires ctx
547
+ request=$(fm_pf_registry_get "$STATE" "$id" request_id)
548
+ [ -n "$request" ] || request=$(pf_field "$payload" '.public_followup.request.request_id')
549
+ platform=$(fm_pf_registry_get "$STATE" "$id" platform)
550
+ [ -n "$platform" ] || platform=$(pf_field "$payload" '.public_followup.request.platform')
551
+ delivered=$(fm_pf_registry_get "$STATE" "$id" delivered_at)
552
+ expires=$(fm_pf_registry_get "$STATE" "$id" followup_expires_at)
553
+ [ -n "$expires" ] || expires=$(pf_field "$payload" '.public_followup.request.followup_expires_at')
554
+ summary=$(pf_field "$payload" '.public_followup.request.public_safe_summary' | fm_pf_clean_outcome_text)
555
+ if [ -z "$summary" ]; then
556
+ ctx=$(fm_pf_registry_get "$STATE" "$id" request_context_b64)
557
+ if [ -n "$ctx" ]; then
558
+ summary=$(printf '%s' "$ctx" | fm_pf_b64_decode | jq -r '.public_safe_summary // empty' 2>/dev/null | fm_pf_clean_outcome_text)
559
+ fi
560
+ fi
561
+ printf 'open-loop %s request=%s platform=%s\n' "$id" "${request:-unknown}" "${platform:-unknown}"
562
+ printf ' delivered=%s window-closes=%s\n' "${delivered:-unknown}" "${expires:-unknown}"
563
+ printf ' summary=%s\n' "$summary"
564
+ if ! fm_pf_registry_rechainable "$STATE" "$id"; then
565
+ printf ' unrechainable: pre-change registration lacks request_context_b64\n'
566
+ fi
567
+ print_window_escalation "$expires"
568
+ printf ' -> bind the follow-on with rechain, or close the loop with retire %s --reason ...\n' "$id"
569
+ }
570
+
571
+ cmd_pending() {
572
+ gate_or_exit
573
+
574
+ local listing id payload delivery task_state summary platform request expires printed=0 loop_state settled stamp_rc
575
+ # An unreadable backlog with registrations present is exactly the silence this
576
+ # whole path exists to prevent, so say so rather than printing nothing.
577
+ if ! command -v jq >/dev/null 2>&1 || ! command -v tasks-axi >/dev/null 2>&1 \
578
+ || ! listing=$(tx public-followup list --json 2>/dev/null) || [ -z "$listing" ] \
579
+ || ! printf '%s' "$listing" | jq -e '
580
+ type == "object"
581
+ and (.public_followups | type == "array")
582
+ and all(.public_followups[];
583
+ type == "object"
584
+ and (.id | type == "string")
585
+ and (.public_followup | type == "object")
586
+ and (.state | type == "string"))
587
+ ' >/dev/null 2>&1; then
588
+ if fm_pf_has_registrations "$STATE"; then
589
+ printf 'cannot read this home'\''s public commitments through tasks-axi; %s registration(s) are still recorded under state/%s/registry\n' \
590
+ "$(fm_pf_registry_ids "$STATE" | grep -c . || true)" "$FM_PF_DIRNAME"
591
+ printed=1
592
+ fi
593
+ if fm_pf_has_events "$STATE"; then
594
+ printf 'unconsumed terminal results are waiting; run %s/bin/fm-public-followup.sh consume\n' "$FM_ROOT"
595
+ printed=1
596
+ fi
597
+ [ "$printed" -eq 1 ] || exit 0
598
+ return 0
599
+ fi
600
+
601
+ while IFS= read -r id; do
602
+ [ -n "$id" ] || continue
603
+ payload=$(printf '%s' "$listing" | jq -ce --arg id "$id" \
604
+ '(.public_followups // []) | map(select(.id == $id)) | .[0] // empty' 2>/dev/null)
605
+ loop_state=$(fm_pf_registry_loop_state "$STATE" "$id")
606
+ delivery=$(pf_field "$payload" '.public_followup.delivery.state')
607
+ task_state=$(pf_field "$payload" '.state')
608
+ settled=0
609
+ if [ -z "$payload" ] || [ "$task_state" = 'done' ] \
610
+ || [ "$delivery" = 'posted' ] || [ "$delivery" = 'waived' ] \
611
+ || [ "$loop_state" = delivered ]; then
612
+ settled=1
613
+ fi
614
+ if [ "$settled" -eq 1 ]; then
615
+ if [ "$loop_state" != delivered ]; then
616
+ stamp_rc=0
617
+ fm_pf_registry_stamp_delivered "$STATE" "$id" "$(now_rfc3339)" || stamp_rc=$?
618
+ if [ "$stamp_rc" -eq 3 ] && fm_pf_retirement_receipt_exists "$STATE" "$id"; then
619
+ continue
620
+ fi
621
+ [ "$stamp_rc" -eq 0 ] \
622
+ || die "could not stamp settled registration '$id' as delivered" 1
623
+ fi
624
+ # Keep the registration. Clearing a leftover legacy link is best-effort
625
+ # and never the close; only retire removes the record.
626
+ if public_followup_registration_valid "$id"; then
627
+ clear_public_followup_link "$id" >/dev/null 2>&1 || true
628
+ fi
629
+ print_open_loop "$id" "$payload"
630
+ printed=1
631
+ continue
632
+ fi
633
+ summary=$(pf_field "$payload" '.public_followup.request.public_safe_summary' | fm_pf_clean_outcome_text)
634
+ platform=$(pf_field "$payload" '.public_followup.request.platform')
635
+ request=$(pf_field "$payload" '.public_followup.request.request_id')
636
+ printf 'unresolved %s state=%s platform=%s request=%s summary=%s\n' \
637
+ "$id" "${delivery:-unknown}" "${platform:-unknown}" "${request:-unknown}" "$summary"
638
+ expires=$(fm_pf_registry_get "$STATE" "$id" followup_expires_at)
639
+ [ -n "$expires" ] || expires=$(pf_field "$payload" '.public_followup.request.followup_expires_at')
640
+ print_window_escalation "$expires"
641
+ if ! fm_pf_registry_rechainable "$STATE" "$id"; then
642
+ printf ' unrechainable: pre-change registration lacks request_context_b64\n'
643
+ fi
644
+ printed=1
645
+ done <<EOF
646
+ $(fm_pf_registry_ids "$STATE")
647
+ EOF
648
+
649
+ # Events that arrived while no agent was present are actionable on their own,
650
+ # so surface them even when every registration currently looks settled.
651
+ if fm_pf_has_events "$STATE"; then
652
+ printf 'unconsumed terminal results are waiting; run %s/bin/fm-public-followup.sh consume\n' "$FM_ROOT"
653
+ printed=1
654
+ fi
655
+ [ "$printed" -eq 1 ] || exit 0
656
+ }
657
+
658
+ # --- subcommand: deliver ----------------------------------------------------
659
+
660
+ public_followup_registration_valid() {
661
+ local id=$1 file relation work_home work_id generation
662
+ file="$(fm_pf_registry_dir "$STATE")/$id"
663
+ [ -f "$file" ] && [ ! -L "$file" ] || return 1
664
+ relation=$(fm_pf_registry_get "$STATE" "$id" relation_id)
665
+ work_home=$(fm_pf_registry_get "$STATE" "$id" work_home)
666
+ work_id=$(fm_pf_registry_get "$STATE" "$id" work_id)
667
+ generation=$(fm_pf_registry_get "$STATE" "$id" generation)
668
+ [ -n "$relation" ] && [ -n "$work_id" ] || return 1
669
+ fm_pf_home_id_valid "$work_home" || return 1
670
+ fm_pf_slug_valid "$work_id" || return 1
671
+ case "$generation" in ''|*[!0-9]*) return 1 ;; esac
672
+ }
673
+
674
+ public_followup_secondmate_home() {
675
+ local id=$1 include_absent=${2:-} meta_home registry_home home marker
676
+ fm_pf_home_id_valid "secondmate:$id" || return 1
677
+ meta_home=$(fmx_meta_get "$STATE/$id.meta" home)
678
+ registry_home=
679
+ if [ -f "$DATA/secondmates.md" ] && [ ! -L "$DATA/secondmates.md" ]; then
680
+ registry_home=$(secondmate_registry_field "$DATA/secondmates.md" "$id" home || true)
681
+ fi
682
+ if [ -n "$meta_home" ] && [ -n "$registry_home" ] && [ "$meta_home" != "$registry_home" ]; then
683
+ return 2
684
+ fi
685
+ home=${meta_home:-$registry_home}
686
+ [ -n "$home" ] || return 4
687
+ case "$home" in /*) ;; *) return 2 ;; esac
688
+ if [ ! -e "$home" ]; then
689
+ [ ! -L "$home" ] || return 2
690
+ [ "$include_absent" = include-absent ] && printf '%s\n' "$home"
691
+ return 3
692
+ fi
693
+ home=$(CDPATH='' cd -- "$home" 2>/dev/null && pwd -P) || return 2
694
+ [ -f "$home/.fm-secondmate-home" ] && [ ! -L "$home/.fm-secondmate-home" ] || return 2
695
+ marker=$(sed -n '1p' "$home/.fm-secondmate-home" 2>/dev/null)
696
+ [ "$marker" = "$id" ] || return 2
697
+ printf '%s\n' "$home"
698
+ }
699
+
700
+ clear_public_followup_link() {
701
+ local id=$1 work_home work_home_path work_id home state rc
702
+ public_followup_registration_valid "$id" || return 1
703
+ work_home=$(fm_pf_registry_get "$STATE" "$id" work_home)
704
+ work_id=$(fm_pf_registry_get "$STATE" "$id" work_id)
705
+ [ -n "$work_home" ] && [ -n "$work_id" ] || return 1
706
+ case "$work_home" in
707
+ main)
708
+ home=$FM_HOME
709
+ state=$STATE
710
+ ;;
711
+ secondmate:*)
712
+ work_home_path=$(fm_pf_registry_get "$STATE" "$id" work_home_path)
713
+ case "$work_home_path" in /*) ;; *) return 1 ;; esac
714
+ case "$work_home_path" in *$'\n'*|*$'\r'*) return 1 ;; esac
715
+ rc=0
716
+ home=$(public_followup_secondmate_home "${work_home#secondmate:}" include-absent) || rc=$?
717
+ if [ "$rc" -eq 3 ]; then
718
+ [ "$home" = "$work_home_path" ] || return 1
719
+ [ ! -e "$work_home_path" ] && [ ! -L "$work_home_path" ] || return 1
720
+ return 0
721
+ fi
722
+ if [ "$rc" -eq 4 ]; then
723
+ [ ! -e "$work_home_path" ] && [ ! -L "$work_home_path" ] || return 1
724
+ return 0
725
+ fi
726
+ [ "$rc" -eq 0 ] || return 1
727
+ [ "$home" = "$work_home_path" ] || return 1
728
+ state="$home/state"
729
+ ;;
730
+ *) return 1 ;;
731
+ esac
732
+ FM_HOME="$home" FM_STATE_OVERRIDE="$state" FM_ROOT_OVERRIDE="$FM_ROOT" \
733
+ "$FM_ROOT/bin/fm-x-followup.sh" --clear "$work_id" >/dev/null
734
+ }
735
+
736
+ public_followup_legacy_link_status() {
737
+ local payload=$1 relations work_home work_id home meta
738
+ if ! printf '%s' "$payload" | jq -e '
739
+ (.public_followup.work_relations | type == "array")
740
+ and all(.public_followup.work_relations[];
741
+ (.work_ref.home_id | type == "string")
742
+ and (.work_ref.task_id | type == "string")
743
+ )
744
+ ' >/dev/null 2>&1; then
745
+ return 2
746
+ fi
747
+ relations=$(printf '%s' "$payload" | jq -r '
748
+ .public_followup.work_relations[]
749
+ | [.work_ref.home_id, .work_ref.task_id]
750
+ | @tsv
751
+ ' 2>/dev/null) || return 2
752
+ [ -n "$relations" ] || return 2
753
+ while IFS=$'\t' read -r work_home work_id; do
754
+ [ -n "$work_home" ] && [ -n "$work_id" ] || return 2
755
+ case "$work_home" in
756
+ main) home=$FM_HOME ;;
757
+ secondmate:*) home=$(public_followup_secondmate_home "${work_home#secondmate:}") || return 2 ;;
758
+ *) return 2 ;;
759
+ esac
760
+ meta="$home/state/$work_id.meta"
761
+ [ -e "$meta" ] || continue
762
+ [ -f "$meta" ] && [ ! -L "$meta" ] || return 2
763
+ [ -n "$(fmx_meta_get "$meta" x_request)" ] && return 0
764
+ done <<EOF
765
+ $relations
766
+ EOF
767
+ return 1
768
+ }
769
+
770
+ record_error() {
771
+ local id=$1 attempt=$2 state=$3 code=$4 next=$5 tmp rc
772
+ tmp=$(mktemp "${TMPDIR:-/tmp}/fm-pf-error.XXXXXX") || return 1
773
+ if [ -n "$next" ]; then
774
+ jq -n --argjson a "$attempt" --arg s "$state" --arg c "$code" \
775
+ --arg o "$(now_rfc3339)" --arg n "$next" \
776
+ '{state:$s, attempt_count:$a, error_code:$c, occurred_at:$o, next_attempt_at:$n}' > "$tmp"
777
+ else
778
+ jq -n --argjson a "$attempt" --arg s "$state" --arg c "$code" --arg o "$(now_rfc3339)" \
779
+ '{state:$s, attempt_count:$a, error_code:$c, occurred_at:$o}' > "$tmp"
780
+ fi
781
+ tx public-followup record-error "$id" --error-file "$tmp" >/dev/null 2>&1
782
+ rc=$?
783
+ rm -f -- "$tmp"
784
+ return "$rc"
785
+ }
786
+
787
+ record_posted() {
788
+ local id=$1 attempt=$2 request=$3 platform=$4 chunks=$5 tmp rc
789
+ tmp=$(mktemp "${TMPDIR:-/tmp}/fm-pf-receipt.XXXXXX") || return 1
790
+ jq -n --argjson a "$attempt" --arg r "$request" --arg p "$platform" \
791
+ --argjson c "$chunks" --arg t "$(now_rfc3339)" \
792
+ '{state:"posted", request_id:$r, platform:$p, attempt_count:$a,
793
+ total_chunks:$c, posted_chunks:$c, posted_at:$t}' > "$tmp"
794
+ tx public-followup record-delivery "$id" --receipt-file "$tmp" >/dev/null 2>&1
795
+ rc=$?
796
+ rm -f -- "$tmp"
797
+ return "$rc"
798
+ }
799
+
800
+ # Delivery keeps the registration. Stamp it delivered and tell the caller the
801
+ # public loop is still open.
802
+ mark_loop_delivered() {
803
+ local id=$1 rc=0
804
+ fm_pf_registry_stamp_delivered "$STATE" "$id" "$(now_rfc3339)" || rc=$?
805
+ case "$rc" in
806
+ 0) return 0 ;;
807
+ 3) return 3 ;;
808
+ *) die "could not stamp registration '$id' as delivered after the public reply landed" 1 ;;
809
+ esac
810
+ }
811
+
812
+ print_loop_open_disposition() {
813
+ local id=$1 request=$2
814
+ printf "thread %s is still OPEN: hand it on with 'rechain ...' or close it with 'retire %s --reason ...'\n" \
815
+ "${request:-unknown}" "$id"
816
+ }
817
+
818
+ cmd_deliver() {
819
+ local id=${1:-} text_file=
820
+ [ -n "$id" ] || { usage; exit 2; }
821
+ shift
822
+ while [ "$#" -gt 0 ]; do
823
+ case "$1" in
824
+ --text-file) shift; text_file=${1:-} ;;
825
+ *) die "unknown argument '$1'" ;;
826
+ esac
827
+ shift || true
828
+ done
829
+
830
+ fm_pf_slug_valid "$id" || die "unsafe obligation id: $id"
831
+ fm_pf_relay_active "$FM_HOME" \
832
+ || die "this home has not opted into the myfirstmate relay, so it cannot post a public reply" 1
833
+ require_tools
834
+
835
+ local payload delivery attempt request platform text tmp_text hash chunks rc receipt receipt_fields receipt_dry_run link_status
836
+ local loop_retained=0
837
+ payload=$(obligation_json "$id") || die "could not read the backlog through tasks-axi" 1
838
+ [ -n "$payload" ] || die "no public-followup obligation '$id' in this home's backlog" 1
839
+
840
+ delivery=$(pf_field "$payload" '.public_followup.delivery.state')
841
+ request=$(pf_field "$payload" '.public_followup.request.request_id')
842
+ platform=$(pf_field "$payload" '.public_followup.request.platform')
843
+ attempt=$(pf_field "$payload" '.public_followup.delivery.attempt_count')
844
+ case "$attempt" in ''|*[!0-9]*) attempt=0 ;; esac
845
+
846
+ case "$delivery" in
847
+ posted|waived)
848
+ if public_followup_registration_valid "$id"; then
849
+ if ! clear_public_followup_link "$id"; then
850
+ die "obligation '$id' is already $delivery, but its legacy X link could not be cleared; the registration was retained for reconciliation" 1
851
+ fi
852
+ else
853
+ link_status=1
854
+ public_followup_legacy_link_status "$payload" || link_status=$?
855
+ case "$link_status" in
856
+ 0) die "obligation '$id' is already $delivery, but its legacy X link cannot be cleared without a valid registration; reconcile it before any later terminal follow-up" 1 ;;
857
+ 1) ;;
858
+ *) die "obligation '$id' is already $delivery, but its registration is missing or invalid and the legacy X link cannot be verified; reconcile it before any later terminal follow-up" 1 ;;
859
+ esac
860
+ fi
861
+ if mark_loop_delivered "$id"; then loop_retained=1; fi
862
+ printf 'already delivered %s state=%s\n' "$id" "$delivery"
863
+ [ "$loop_retained" -eq 0 ] || print_loop_open_disposition "$id" "$request"
864
+ return 0
865
+ ;;
866
+ ready|retry-due|context-blocked|unknown|partial)
867
+ public_followup_registration_valid "$id" \
868
+ || die "public-followup registration for '$id' is missing or invalid; reconcile it before delivery so any legacy X link can be cleared" 1
869
+ ;;
870
+ delivery-posting)
871
+ die "obligation '$id' is mid-delivery on attempt $attempt: a previous post was started and its outcome was never recorded. Confirm whether that post landed, then close it with 'record-posted $id --attempt $attempt --chunks <exact-count>' or reopen it for retry. Posting again here could duplicate the public reply." 1
872
+ ;;
873
+ pending-work)
874
+ die "obligation '$id' is still waiting on its bound work; nothing to deliver yet" 1
875
+ ;;
876
+ *)
877
+ die "obligation '$id' is in delivery state '${delivery:-unknown}', which is not deliverable" 1
878
+ ;;
879
+ esac
880
+
881
+ [ -n "$request" ] || die "obligation '$id' has no relay request id; its thread binding is unusable" 1
882
+
883
+ if [ -n "$text_file" ]; then
884
+ [ -f "$text_file" ] || die "reply text file not found: $text_file"
885
+ text=$(cat "$text_file")
886
+ else
887
+ # Deterministic default: reuse the accepted terminal event's bounded
888
+ # public-safe outcome exactly rather than paraphrasing a landed result.
889
+ text=$(printf '%s' "$payload" | jq -r '
890
+ [(.public_followup.work_relations // [])[]
891
+ | (.accepted_events // [])[]
892
+ | .public_safe_outcome // empty] | last // empty' 2>/dev/null)
893
+ [ -n "$text" ] \
894
+ || die "obligation '$id' carries no accepted public-safe outcome to reuse; pass --text-file with the reply you composed" 1
895
+ fi
896
+ [ -n "$text" ] || die "the reply text is empty" 2
897
+
898
+ tmp_text=$(mktemp "${TMPDIR:-/tmp}/fm-pf-text.XXXXXX") || die "could not stage the reply text" 1
899
+ PF_TEMP_FILES+=("$tmp_text")
900
+ receipt=$(mktemp "${TMPDIR:-/tmp}/fm-pf-postreceipt.XXXXXX") || die "could not stage the post receipt" 1
901
+ PF_TEMP_FILES+=("$receipt")
902
+ printf '%s' "$text" > "$tmp_text"
903
+
904
+ hash=$(fm_pf_sha256 < "$tmp_text") || die "sha256 (shasum or sha256sum) is required" 1
905
+ [ -n "$hash" ] || die "could not hash the reply payload" 1
906
+
907
+ # begin-delivery is what makes a retry safe: it pins the attempt and the exact
908
+ # payload before anything leaves the machine. The attempt is read back rather
909
+ # than assumed, because every later receipt or error must name it exactly.
910
+ local begun
911
+ begun=$(tx public-followup begin-delivery "$id" --payload-hash "$hash" --json 2>/dev/null) \
912
+ || die "tasks-axi refused to begin delivery for '$id'" 1
913
+ attempt=$(printf '%s' "$begun" | jq -r '.task.public_followup.delivery.attempt_count // empty' 2>/dev/null)
914
+ case "$attempt" in
915
+ ''|*[!0-9]*) die "could not read the delivery attempt for '$id' after beginning it; nothing was posted" 1 ;;
916
+ esac
917
+
918
+ rc=0
919
+ FMX_REPLY_PLATFORM="$platform" FM_HOME="$FM_HOME" \
920
+ "$FM_ROOT/bin/fm-x-reply.sh" "$request" --followup --receipt-file "$receipt" \
921
+ --text-file "$tmp_text" >/dev/null || rc=$?
922
+
923
+ if [ "$rc" -eq 0 ]; then
924
+ receipt_fields=$(jq -er --arg request "$request" '
925
+ if type != "object" or .request_id != $request or .endpoint != "followup"
926
+ or (.chunks | type) != "number" or (.chunks < 1) or (.chunks != (.chunks | floor))
927
+ or (.dry_run | type) != "boolean" then error("invalid receipt")
928
+ else [(.chunks | tostring), (.dry_run | tostring)] | @tsv end
929
+ ' "$receipt" 2>/dev/null) \
930
+ || die "the public reply for '$id' POSTED but its receipt is missing or invalid; inspect the relay and close it with 'record-posted $id --attempt $attempt --chunks <exact-count>' before any retry" 1
931
+ IFS=$'\t' read -r chunks receipt_dry_run <<EOF
932
+ $receipt_fields
933
+ EOF
934
+ if [ "$receipt_dry_run" = true ]; then
935
+ if ! record_error "$id" "$attempt" retry-due dry_run_no_post "$(next_attempt_rfc3339)"; then
936
+ die "dry-run for '$id' did not post and its retryable state could not be recorded; the obligation remains mid-delivery and needs explicit reconciliation before retry" 1
937
+ fi
938
+ die "dry-run for '$id' did not post; recorded as retryable and left the obligation open" 1
939
+ fi
940
+ if record_posted "$id" "$attempt" "$request" "$platform" "$chunks"; then
941
+ if ! clear_public_followup_link "$id"; then
942
+ die "the public reply for '$id' POSTED and its receipt was recorded, but its legacy X link could not be cleared; the registration was retained for reconciliation" 1
943
+ fi
944
+ if mark_loop_delivered "$id"; then loop_retained=1; fi
945
+ printf 'delivered %s request=%s platform=%s chunks=%s\n' "$id" "$request" "$platform" "$chunks"
946
+ [ "$loop_retained" -eq 0 ] || print_loop_open_disposition "$id" "$request"
947
+ return 0
948
+ fi
949
+ die "the public reply for '$id' POSTED but its receipt could not be recorded; close it with 'record-posted $id --attempt $attempt --chunks <exact-count>' before any retry, or the thread will get a second reply" 1
950
+ fi
951
+
952
+ case "$rc" in
953
+ 8) if ! record_error "$id" "$attempt" context-blocked reply_context_unresolved ""; then
954
+ die "the public reply for '$id' was not posted, and its held state could not be recorded; the obligation remains mid-delivery and needs explicit reconciliation before retry" 1
955
+ fi
956
+ die "held '$id': the original thread's platform or size budget could not be resolved, so nothing was posted. Retry once the request context is recoverable." 1 ;;
957
+ 9) if ! record_error "$id" "$attempt" expired-action-required followup_binding_exhausted ""; then
958
+ die "the relay rejected '$id', and its expired state could not be recorded; the obligation remains mid-delivery and needs explicit reconciliation before retry" 1
959
+ fi
960
+ die "the relay no longer accepts a follow-up for '$id' (window or cap exhausted); nothing was posted and this needs a captain decision" 1 ;;
961
+ *) if ! record_error "$id" "$attempt" retry-due relay_post_failed "$(next_attempt_rfc3339)"; then
962
+ die "posting the public reply for '$id' failed, and its retryable state could not be recorded; the obligation remains mid-delivery and needs explicit reconciliation before retry" 1
963
+ fi
964
+ die "posting the public reply for '$id' failed (exit $rc); recorded as retryable, nothing was delivered" 1 ;;
965
+ esac
966
+ }
967
+
968
+ # --- subcommand: record-posted ---------------------------------------------
969
+
970
+ cmd_record_posted() {
971
+ local id=${1:-} attempt='' chunks=''
972
+ [ -n "$id" ] || { usage; exit 2; }
973
+ shift
974
+ while [ "$#" -gt 0 ]; do
975
+ case "$1" in
976
+ --attempt) shift; attempt=${1:-} ;;
977
+ --chunks) shift; chunks=${1:-} ;;
978
+ *) die "unknown argument '$1'" ;;
979
+ esac
980
+ shift || true
981
+ done
982
+ fm_pf_slug_valid "$id" || die "unsafe obligation id: $id"
983
+ case "$attempt" in ''|*[!0-9]*) die "--attempt <n> is required and must be an integer" ;; esac
984
+ case "$chunks" in ''|*[!0-9]*) die "--chunks <n> is required and must be a positive integer" ;; esac
985
+ [ "$chunks" -ge 1 ] 2>/dev/null || die "--chunks <n> is required and must be a positive integer"
986
+ fm_pf_relay_active "$FM_HOME" || die "the relay is not active for this home" 1
987
+ public_followup_registration_valid "$id" \
988
+ || die "public-followup registration for '$id' is missing or invalid; reconcile it before recording a receipt so any legacy X link can be cleared" 1
989
+ require_tools
990
+
991
+ local payload request platform loop_retained=0
992
+ payload=$(obligation_json "$id") || die "could not read the backlog through tasks-axi" 1
993
+ [ -n "$payload" ] || die "no public-followup obligation '$id' in this home's backlog" 1
994
+ request=$(pf_field "$payload" '.public_followup.request.request_id')
995
+ platform=$(pf_field "$payload" '.public_followup.request.platform')
996
+
997
+ record_posted "$id" "$attempt" "$request" "$platform" "$chunks" \
998
+ || die "tasks-axi refused the receipt for '$id' attempt $attempt; the recorded attempt must match exactly" 1
999
+ if ! clear_public_followup_link "$id"; then
1000
+ die "the receipt for '$id' was recorded, but its legacy X link could not be cleared; the registration was retained for reconciliation" 1
1001
+ fi
1002
+ if mark_loop_delivered "$id"; then loop_retained=1; fi
1003
+ printf 'recorded %s attempt=%s request=%s\n' "$id" "$attempt" "$request"
1004
+ [ "$loop_retained" -eq 0 ] || print_loop_open_disposition "$id" "$request"
1005
+ }
1006
+
1007
+ # --- subcommand: guard-work -------------------------------------------------
1008
+
1009
+ cmd_guard_work() {
1010
+ local work_home=${1:-} work_id=${2:-} bound id payload delivery task_state blocked=0
1011
+ [ -n "$work_home" ] && [ -n "$work_id" ] || { usage; exit 2; }
1012
+ fm_pf_relay_active "$FM_HOME" || exit 0
1013
+ fm_pf_has_registrations "$STATE" || exit 0
1014
+
1015
+ # Reading the registration records needs no tools, so establish whether this
1016
+ # work is bound to any commitment before deciding anything else.
1017
+ bound=$(fm_pf_registry_ids_for_work "$STATE" "$work_home" "$work_id")
1018
+ [ -n "$bound" ] || exit 0
1019
+
1020
+ # From here the work IS bound to a public promise, so an unreadable state is a
1021
+ # blocking answer, not a pass: cleanup must never proceed on a guess.
1022
+ if ! command -v jq >/dev/null 2>&1 || ! command -v tasks-axi >/dev/null 2>&1; then
1023
+ printf 'cannot verify the public commitments bound to %s/%s: jq and tasks-axi are required\n' \
1024
+ "$work_home" "$work_id"
1025
+ exit 3
1026
+ fi
1027
+
1028
+ while IFS= read -r id; do
1029
+ [ -n "$id" ] || continue
1030
+ if ! payload=$(obligation_json "$id"); then
1031
+ printf 'cannot read the state of public commitment %s for %s/%s\n' "$id" "$work_home" "$work_id"
1032
+ blocked=1
1033
+ continue
1034
+ fi
1035
+ # Gone from the backlog entirely (pruned after Done): nothing left to owe.
1036
+ [ -n "$payload" ] || continue
1037
+ delivery=$(pf_field "$payload" '.public_followup.delivery.state')
1038
+ task_state=$(pf_field "$payload" '.state')
1039
+ case "$task_state:$delivery" in
1040
+ done:*|*:posted|*:waived) continue ;;
1041
+ esac
1042
+ printf 'public commitment %s is still %s for %s/%s\n' "$id" "${delivery:-unknown}" "$work_home" "$work_id"
1043
+ blocked=1
1044
+ done <<EOF
1045
+ $bound
1046
+ EOF
1047
+ [ "$blocked" -eq 0 ] || exit 3
1048
+ }
1049
+
1050
+ # --- subcommand: rechain ----------------------------------------------------
1051
+
1052
+ rechain_default_deliverable_key() {
1053
+ case "$1" in
1054
+ pr-merged) printf 'pr_url\n' ;;
1055
+ report-ready) printf 'report_path\n' ;;
1056
+ *) return 1 ;;
1057
+ esac
1058
+ }
1059
+
1060
+ cmd_rechain() {
1061
+ local new_id=${1:-} from='' work_home='' work_id='' expected=''
1062
+ local -a deliverable_keys=()
1063
+ [ -n "$new_id" ] || { usage; exit 2; }
1064
+ shift
1065
+ while [ "$#" -gt 0 ]; do
1066
+ case "$1" in
1067
+ --from) shift; from=${1:-} ;;
1068
+ --work-home) shift; work_home=${1:-} ;;
1069
+ --work-id) shift; work_id=${1:-} ;;
1070
+ --expected) shift; expected=${1:-} ;;
1071
+ --deliverable-key) shift; deliverable_keys+=("${1:-}") ;;
1072
+ *) die "unknown argument '$1'" ;;
1073
+ esac
1074
+ shift || true
1075
+ done
1076
+
1077
+ fm_pf_relay_active "$FM_HOME" \
1078
+ || die "this home has not opted into the myfirstmate relay, so it cannot own a public commitment" 1
1079
+ require_tools
1080
+ fm_pf_slug_valid "$new_id" || die "unsafe obligation id: $new_id"
1081
+ fm_pf_slug_valid "$from" || die "unsafe source obligation id: $from"
1082
+ fm_pf_slug_valid "$work_id" || die "unsafe work id: $work_id"
1083
+ fm_pf_home_id_valid "$work_home" \
1084
+ || die "work home must be 'main' or 'secondmate:<stable-id>', got '$work_home'"
1085
+ case "$expected" in
1086
+ pr-merged|report-ready|local-main) ;;
1087
+ *) die "--expected must be pr-merged, report-ready, or local-main, got '$expected'" ;;
1088
+ esac
1089
+ [ "$new_id" != "$from" ] || die "the new obligation id must differ from --from" 2
1090
+
1091
+ pf_registry_lock_acquire "$from" \
1092
+ || die "could not lock source registration '$from' for rechain" 1
1093
+ local src_file loop_state expires window ctx rechain_to source_record first_claim=0 existing
1094
+ src_file="$(fm_pf_registry_dir "$STATE")/$from"
1095
+ [ -f "$src_file" ] && [ ! -L "$src_file" ] \
1096
+ || die "no registration for '$from' in this home" 1
1097
+ loop_state=$(fm_pf_registry_loop_state "$STATE" "$from")
1098
+ [ "$loop_state" = delivered ] \
1099
+ || die "source '$from' is not state=delivered (got '$loop_state'); nothing to hand on until that final lands" 1
1100
+ fm_pf_registry_rechainable "$STATE" "$from" \
1101
+ || die "source '$from' is un-rechainable: a pre-change registration has no request_context_b64. Close it with retire --reason or reconstruct the request context by hand." 1
1102
+
1103
+ expires=$(fm_pf_registry_get "$STATE" "$from" followup_expires_at)
1104
+ [ -n "$expires" ] || die "source '$from' has no followup_expires_at; the thread window cannot be checked" 1
1105
+ window=$(fm_pf_followup_window_class "$expires")
1106
+ case "$window" in
1107
+ ok|closing) ;;
1108
+ expired)
1109
+ die "followup_expires_at $expires is in the past: the thread can no longer be reached, so this loop cannot be closed publicly. This is a captain decision." 1
1110
+ ;;
1111
+ *)
1112
+ die "followup_expires_at $expires could not be parsed: the thread window cannot be checked, so this loop cannot be rechained" 1
1113
+ ;;
1114
+ esac
1115
+
1116
+ if [ "${#deliverable_keys[@]}" -eq 0 ]; then
1117
+ local default_key
1118
+ default_key=$(rechain_default_deliverable_key "$expected") \
1119
+ || die "--expected $expected needs --deliverable-key <k> (no default key)"
1120
+ deliverable_keys+=("$default_key")
1121
+ fi
1122
+ local key
1123
+ for key in "${deliverable_keys[@]}"; do
1124
+ case "$key" in
1125
+ ''|*[!a-z0-9_]*) die "deliverable key must be lowercase [a-z0-9_], got '$key'" ;;
1126
+ esac
1127
+ done
1128
+
1129
+ # Claim the delivered baton before publishing its destination. The claim is
1130
+ # retained if any later retirement step fails, so a retry may resume the same
1131
+ # destination but can never fork this thread into a second obligation.
1132
+ rechain_to=$(fm_pf_registry_get "$STATE" "$from" rechain_to)
1133
+ if [ -n "$rechain_to" ] && [ "$rechain_to" != "$new_id" ]; then
1134
+ die "source '$from' is already claimed by rechain destination '$rechain_to'; resume that destination" 1
1135
+ fi
1136
+ if [ -z "$rechain_to" ]; then
1137
+ existing=$(obligation_json "$new_id") \
1138
+ || die "could not check whether rechain destination '$new_id' is unused" 1
1139
+ [ -z "$existing" ] \
1140
+ || die "'$new_id' already exists and was not created by this rechain; choose another id" 1
1141
+ [ ! -e "$(fm_pf_registry_dir "$STATE")/$new_id" ] \
1142
+ && [ ! -L "$(fm_pf_registry_dir "$STATE")/$new_id" ] \
1143
+ && [ ! -e "$(fm_pf_retired_dir "$STATE")/$new_id" ] \
1144
+ && [ ! -L "$(fm_pf_retired_dir "$STATE")/$new_id" ] \
1145
+ || die "'$new_id' already has local public-loop state; choose another id" 1
1146
+ source_record=$(grep -v -E '^rechain_to=' "$src_file" 2>/dev/null) \
1147
+ || die "could not read source registration '$from' while claiming it" 1
1148
+ printf '%s\nrechain_to=%s\n' "$source_record" "$new_id" \
1149
+ | fmx_private_artifact_publish_stdin "$(fm_pf_registry_dir "$STATE")" "$from" 600 \
1150
+ || die "could not claim source registration '$from' for '$new_id'" 1
1151
+ first_claim=1
1152
+ fi
1153
+
1154
+ local ctx_file expected_file relation_file keys_json project src_payload
1155
+ ctx=$(fm_pf_registry_get "$STATE" "$from" request_context_b64)
1156
+ ctx_file=$(mktemp "${TMPDIR:-/tmp}/fm-pf-rechain-ctx.XXXXXX") \
1157
+ || die "could not stage the retained request context" 1
1158
+ expected_file=$(mktemp "${TMPDIR:-/tmp}/fm-pf-rechain-exp.XXXXXX") \
1159
+ || die "could not stage the expected-final document" 1
1160
+ relation_file=$(mktemp "${TMPDIR:-/tmp}/fm-pf-rechain-rel.XXXXXX") \
1161
+ || die "could not stage the relation document" 1
1162
+ PF_TEMP_FILES+=("$ctx_file" "$expected_file" "$relation_file")
1163
+ printf '%s' "$ctx" | fm_pf_b64_decode > "$ctx_file" \
1164
+ || die "could not decode request_context_b64 for '$from'" 1
1165
+ jq -e 'type == "object" and (.request_id | type == "string")' "$ctx_file" >/dev/null 2>&1 \
1166
+ || die "decoded request context for '$from' is not usable" 1
1167
+
1168
+ keys_json=$(printf '%s\n' "${deliverable_keys[@]}" | jq -R . | jq -s -c .)
1169
+ project=
1170
+ if src_payload=$(obligation_json "$from") && [ -n "$src_payload" ]; then
1171
+ project=$(pf_field "$src_payload" '.public_followup.expected_final.project')
1172
+ fi
1173
+ if [ -n "$project" ]; then
1174
+ jq -n --arg t "$expected" --arg p "$project" --argjson keys "$keys_json" \
1175
+ '{type:$t, project:$p, required_deliverables:$keys, completion_policy:"all-required"}' \
1176
+ > "$expected_file"
1177
+ else
1178
+ jq -n --arg t "$expected" --argjson keys "$keys_json" \
1179
+ '{type:$t, required_deliverables:$keys, completion_policy:"all-required"}' \
1180
+ > "$expected_file"
1181
+ fi
1182
+ jq -n --arg h "$work_home" --arg w "$work_id" \
1183
+ '{relation_id:"rel-1", work_ref:{home_id:$h, task_id:$w},
1184
+ role:"fulfills", required:true, generation:1}' > "$relation_file"
1185
+
1186
+ local relation_count new_registry
1187
+ if [ "$first_claim" -eq 1 ]; then
1188
+ existing=
1189
+ else
1190
+ existing=$(obligation_json "$new_id") \
1191
+ || die "could not read the backlog through tasks-axi" 1
1192
+ fi
1193
+ if [ -n "$existing" ]; then
1194
+ printf '%s' "$existing" | jq -e \
1195
+ --slurpfile request "$ctx_file" --slurpfile expected "$expected_file" \
1196
+ --arg expires "$expires" \
1197
+ '.public_followup as $pf
1198
+ | $pf.request == $request[0]
1199
+ and $pf.purpose == "promised-final"
1200
+ and $pf.expected_final == $expected[0]
1201
+ and $pf.obligation_expires_at == $expires' >/dev/null 2>&1 \
1202
+ || die "'$new_id' already exists with different public-followup data; choose another id" 1
1203
+ else
1204
+ tx public-followup add "$new_id" --request-context-file "$ctx_file" \
1205
+ --purpose promised-final --expected-final-file "$expected_file" \
1206
+ --expires-at "$expires" >/dev/null \
1207
+ || die "tasks-axi refused to add '$new_id' on the retained thread binding" 1
1208
+ existing=$(obligation_json "$new_id") \
1209
+ || die "added '$new_id' but could not read it back through tasks-axi; retry this same rechain command" 1
1210
+ fi
1211
+
1212
+ relation_count=$(printf '%s' "$existing" \
1213
+ | jq -r '(.public_followup.work_relations // []) | length' 2>/dev/null) \
1214
+ || die "could not inspect work bindings for '$new_id'" 1
1215
+ if [ "$relation_count" -eq 0 ]; then
1216
+ tx public-followup bind-work "$new_id" --relation-file "$relation_file" >/dev/null \
1217
+ || die "tasks-axi refused to bind '$new_id' to $work_home/$work_id; retry this same rechain command" 1
1218
+ else
1219
+ printf '%s' "$existing" | jq -e --arg h "$work_home" --arg w "$work_id" \
1220
+ '(.public_followup.work_relations // []) as $relations
1221
+ | ($relations | length) == 1
1222
+ and $relations[0].relation_id == "rel-1"
1223
+ and $relations[0].work_ref.home_id == $h
1224
+ and $relations[0].work_ref.task_id == $w
1225
+ and $relations[0].role == "fulfills"
1226
+ and $relations[0].required == true
1227
+ and $relations[0].generation == 1' >/dev/null 2>&1 \
1228
+ || die "'$new_id' already has a different work binding; choose another id" 1
1229
+ fi
1230
+
1231
+ new_registry="$(fm_pf_registry_dir "$STATE")/$new_id"
1232
+ if [ -f "$new_registry" ] && [ ! -L "$new_registry" ]; then
1233
+ [ "$(fm_pf_registry_get "$STATE" "$new_id" relation_id)" = rel-1 ] \
1234
+ && [ "$(fm_pf_registry_get "$STATE" "$new_id" work_home)" = "$work_home" ] \
1235
+ && [ "$(fm_pf_registry_get "$STATE" "$new_id" work_id)" = "$work_id" ] \
1236
+ && [ "$(fm_pf_registry_get "$STATE" "$new_id" generation)" = 1 ] \
1237
+ || die "registration '$new_id' already names different work; choose another id" 1
1238
+ else
1239
+ cmd_register "$new_id" --relation rel-1 --work-home "$work_home" \
1240
+ --work-id "$work_id" --generation 1 >/dev/null \
1241
+ || die "could not register '$new_id'; retry this same rechain command" 1
1242
+ fi
1243
+
1244
+ cmd_retire "$from" --reason "handed on to $new_id" \
1245
+ || die "registered '$new_id' but could not retire '$from'; both loops are open until '$from' is retired" 1
1246
+
1247
+ cmd_brief "$new_id"
1248
+ }
1249
+
1250
+ # --- subcommand: retire -----------------------------------------------------
1251
+
1252
+ cmd_retire() {
1253
+ local id=${1:-} force=0 reason='' payload delivery task_state registry_file retired_dir retired_at
1254
+ local retirement_rc=0
1255
+ [ -n "$id" ] || { usage; exit 2; }
1256
+ shift
1257
+ while [ "$#" -gt 0 ]; do
1258
+ case "$1" in
1259
+ --force) force=1 ;;
1260
+ --reason) shift; reason=${1:-} ;;
1261
+ *) die "unknown argument '$1'" ;;
1262
+ esac
1263
+ shift || true
1264
+ done
1265
+ fm_pf_slug_valid "$id" || die "unsafe obligation id: $id"
1266
+ fm_pf_relay_active "$FM_HOME" || exit 0
1267
+ [ -n "$reason" ] || die "retire requires --reason \"<why the public loop is done>\"" 2
1268
+ reason=$(printf '%s' "$reason" | fm_pf_clean_outcome_text)
1269
+ [ -n "$reason" ] || die "retire requires --reason \"<why the public loop is done>\"" 2
1270
+ require_tools
1271
+ pf_registry_lock_acquire "$id" \
1272
+ || die "could not lock registration '$id' for retirement" 1
1273
+
1274
+ payload=$(obligation_json "$id") || die "could not read the backlog through tasks-axi" 1
1275
+ if [ -n "$payload" ]; then
1276
+ delivery=$(pf_field "$payload" '.public_followup.delivery.state')
1277
+ task_state=$(pf_field "$payload" '.state')
1278
+ case "$task_state:$delivery" in
1279
+ done:*|*:posted|*:waived) ;;
1280
+ *)
1281
+ [ "$force" -eq 1 ] \
1282
+ || die "obligation '$id' is still ${delivery:-unresolved}; retiring its registration now would hide an open public promise. Deliver it, waive it, or pass --force." 1
1283
+ ;;
1284
+ esac
1285
+ fi
1286
+ if ! clear_public_followup_link "$id"; then
1287
+ die "could not clear the legacy X link for '$id'; its registration was retained for reconciliation" 1
1288
+ fi
1289
+ retired_dir=$(fm_pf_retired_dir "$STATE")
1290
+ retired_at=$(now_rfc3339)
1291
+ registry_file="$(fm_pf_registry_dir "$STATE")/$id"
1292
+ printf 'reason=%s\nretired_at=%s\n' "$reason" "$retired_at" \
1293
+ | fmx_private_artifact_publish_stdin "$retired_dir" "$id" 600 \
1294
+ || retirement_rc=1
1295
+ if [ "$retirement_rc" -eq 0 ]; then
1296
+ if ! rm -f -- "$registry_file" 2>/dev/null \
1297
+ || [ -e "$registry_file" ] || [ -L "$registry_file" ]; then
1298
+ retirement_rc=2
1299
+ fi
1300
+ fi
1301
+ pf_registry_lock_release "$id"
1302
+ case "$retirement_rc" in
1303
+ 1) die "could not record the retirement reason for '$id'; the public loop remains open" 1 ;;
1304
+ 2) die "could not remove registration for '$id'; the public loop remains open" 1 ;;
1305
+ esac
1306
+ printf 'retired %s reason=%s\n' "$id" "$reason"
1307
+ }
1308
+
1309
+ # --- dispatch ---------------------------------------------------------------
1310
+
1311
+ CMD=${1:-}
1312
+ case "$CMD" in
1313
+ --help|-h|help) help; exit 0 ;;
1314
+ '') usage; exit 2 ;;
1315
+ esac
1316
+ shift
1317
+
1318
+ case "$CMD" in
1319
+ active) cmd_active "$@" ;;
1320
+ register) cmd_register "$@" ;;
1321
+ brief) cmd_brief "$@" ;;
1322
+ consume) cmd_consume "$@" ;;
1323
+ pending) cmd_pending "$@" ;;
1324
+ deliver) cmd_deliver "$@" ;;
1325
+ record-posted) cmd_record_posted "$@" ;;
1326
+ guard-work) cmd_guard_work "$@" ;;
1327
+ rechain) cmd_rechain "$@" ;;
1328
+ retire) cmd_retire "$@" ;;
1329
+ *) usage; exit 2 ;;
1330
+ esac