@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,1466 @@
1
+ #!/usr/bin/env bash
2
+ # Generic process-to-event runner: supervise a registered long-polling child
3
+ # outside the agent's foreground turn and turn completed results into normalized
4
+ # durable wakes.
5
+ #
6
+ # Usage:
7
+ # fm-procevent.sh register <adapter> <source-id> -- <argv>...
8
+ # fm-procevent.sh register-extension <adapter> <source-id> --config-ref <reference>
9
+ # fm-procevent.sh start <source-id>
10
+ # fm-procevent.sh reconcile
11
+ # fm-procevent.sh classify <result-file>
12
+ # fm-procevent.sh handled <source-id> <sequence>
13
+ # fm-procevent.sh retire <source-id> [--if-absent|--if-matches <adapter> -- <argv>...|--if-owner <registration-token>]
14
+ # fm-procevent.sh sweep-home [--preflight]
15
+ # fm-procevent.sh binding-retirement-preflight <binding-digest>
16
+ # fm-procevent.sh extension-retirement <binding|transfer> <retirement-arguments...>
17
+ # fm-procevent.sh extension-bind <bind|receive-transfer-bind> <binding-arguments...>
18
+ # fm-procevent.sh extension-process-event <process-event-arguments...>
19
+ # fm-procevent.sh list
20
+ #
21
+ # register Record a built-in source: its adapter, its canonical id, and the
22
+ # exact argv to execute. argv is stored one argument per line and
23
+ # executed directly, so there is no shell surface and no argument
24
+ # splitting. Built-in adapters register sources; nothing here parses
25
+ # user text.
26
+ # register-extension
27
+ # Resolve an explicitly enabled home-local process-event-adapter/1
28
+ # binding, verify its package and handshake, and record the source
29
+ # configuration reference with the exact extension id/version,
30
+ # capability version, package digest, binding digest, and a fresh
31
+ # registration token. The tracked extension host constructs every
32
+ # invocation; no package argv or shell command is stored.
33
+ # classify Ask the immutable adapter owner captured beside <result-file> for a
34
+ # bounded classification. Built-in results keep their existing
35
+ # script command; extension results must still match the exact bound
36
+ # package identity captured with them.
37
+ # start Claim the source, run its child to completion, durably capture the
38
+ # output, publish normalized wakes for pending results, then release
39
+ # the claim. It blocks for as long as the source blocks and is meant
40
+ # to run as a supervised background process, never in a conversational
41
+ # turn. After publishing, it asks the source's own adapter whether the
42
+ # captured result ends the source and retires the registration when it
43
+ # says so, so a source that has ended stops being restarted.
44
+ # reconcile Idempotent liveness entry the watcher calls on its ordinary cycle:
45
+ # republish every durably captured result with no handled
46
+ # acknowledgement yet - regardless of any earlier publication - and
47
+ # start a runner for any registered source that has no live owner.
48
+ # This is liveness repair only - it never discovers results by
49
+ # polling the source, because the child blocks on the source itself.
50
+ # handled Durably and idempotently record that a captured result has been
51
+ # fully handled: <source-id> <sequence>. Prints "handled: id seq"
52
+ # the first time for that exact source-and-sequence generation and
53
+ # "already-handled: id seq" on every repeat call, atomically
54
+ # deduplicated so a paired external effect is never authorized
55
+ # twice. Until this is called, the result stays eligible for
56
+ # bounded re-announcement on every reconcile. Marking a result
57
+ # handled does not retire its source registration or claim.
58
+ # retire Drop a registration, stop a runner this home owns, release the claim.
59
+ # Idempotent, and still the supported explicit path after a source has
60
+ # already retired itself on its adapter's terminal verdict. Existing
61
+ # unconditional built-in retirement remains compatible. An external
62
+ # registration requires --if-owner. --if-matches compares a complete
63
+ # built-in registration, --if-absent refuses while any registration
64
+ # exists, and --if-owner removes only the exact extension registration
65
+ # token printed by register-extension, so a stale owner cannot retire
66
+ # a replacement generation.
67
+ # sweep-home Retire a bounded snapshot of this home's registrations and owned
68
+ # claims, then refuse unless no registration, runner record, or owned
69
+ # claim remains. Used by supported Firstmate home retirement.
70
+ # binding-retirement-preflight
71
+ # Refuse while an extension registration or unhandled captured result
72
+ # still owns the exact enabled binding digest. Called by the tracked
73
+ # extension host before identity-conditional binding retirement.
74
+ # extension-retirement
75
+ # Serialize one tracked binding or transfer retirement against
76
+ # extension resolution and registration publication in this home.
77
+ # extension-bind
78
+ # Serialize tracked binding publication against extension resolution,
79
+ # registration publication, and retirement in this home.
80
+ # list Show registered sources, owners, and pending captured results.
81
+ #
82
+ # Terminal knowledge is adapter-owned. This runner never inspects a result and
83
+ # never names an adapter-specific status: built-ins keep the existing
84
+ # `bin/fm-procevent-<adapter>.sh terminal <result-file>` path, while an external
85
+ # result uses the exact process-event-adapter/1 package identity captured beside
86
+ # it. Exit 0 is the only terminal verdict. A missing command, an error, or any
87
+ # other exit keeps the registration armed, so an adapter that has no notion of
88
+ # ending needs no change.
89
+ #
90
+ # Routine no-op knowledge is adapter-owned through the same kind of seam. Some
91
+ # sources produce a result that carries no news at all - a review surface that
92
+ # simply closed with nothing said - and announcing it makes the handler read a
93
+ # wake to learn that nothing happened. So before publishing, this runner asks
94
+ # the immutable captured adapter owner - the built-in `silent` command or the
95
+ # bound extension operation - and treats exit 0 as the only silence verdict: the
96
+ # result is recorded handled and never announced, so it neither wakes a handler
97
+ # now nor returns on a later reconcile. A missing command, an error, or any other
98
+ # exit publishes the wake exactly as before, so an adapter with no notion of a
99
+ # no-op needs no change and an unknown or degraded result always reaches its
100
+ # handler. This runner still inspects nothing and still names no adapter-specific
101
+ # condition. For built-ins, silence remains independent of the keyed-answer feed
102
+ # below: suppressing an announcement never suppresses the captain's own answer.
103
+ #
104
+ # Applying a built-in result is adapter-owned through the same kind of seam. Some results
105
+ # carry no judgement at all - they must simply be applied idempotently to the
106
+ # home's own durable state - and leaving that to an agent that has to remember
107
+ # means it silently does not happen. So after publishing, `start` calls
108
+ # `bin/fm-procevent-<adapter>.sh autohandle <source-id> <sequence> <result-file>`
109
+ # and lets the adapter apply and acknowledge its own result. Exit 0 means the
110
+ # adapter fully handled it. A missing command, an error, or any other exit is not
111
+ # a failure of capture: the result stays unacknowledged and therefore eligible
112
+ # for re-announcement, so the handler still receives it exactly as before. This
113
+ # runner still inspects nothing and still names no adapter-specific condition.
114
+ # External bindings deliberately receive no autohandle operation.
115
+ #
116
+ # Built-in announcement is adapter-owned through one more seam of the same kind. An
117
+ # adapter that answers exit 0 to `bin/fm-procevent-<adapter>.sh self-announcing`
118
+ # declares that every result its autohandle fully applies is announced through a
119
+ # durable downstream channel of its own (for remote-reply, the mirrored parent
120
+ # status append the watcher's signal scan detects). For such an adapter, `start`
121
+ # runs autohandle FIRST and publishes a check wake only for what remains
122
+ # unhandled afterwards, so a fully autohandled capture never produces a second
123
+ # announcement and a byte-identical replay produces none at all. Every other
124
+ # adapter keeps the strict publish-before-apply order, because without a
125
+ # declared downstream channel an applied-and-acknowledged result would otherwise
126
+ # go silent. An unhandled result stays eligible for bounded re-announcement on
127
+ # every reconcile in both modes, exactly as before.
128
+ #
129
+ # Keyed captain answers from built-in adapters use one more seam of the same kind,
130
+ # and this runner still decides nothing about them. Some sources carry the
131
+ # captain's answer to a captain-held task. What such an answer MEANS is owned
132
+ # once, by bin/fm-captain-hold.sh's keyed-answer intake, and reaching it must not
133
+ # depend on an agent remembering. So after capture, a bound source
134
+ # has its result passed to
135
+ # `bin/fm-procevent-<adapter>.sh answers <result-file>`, and whatever that prints
136
+ # is piped straight into that one intake. The adapter reports only what the
137
+ # captain chose; the intake owns every rule about what happens next. This runner
138
+ # names no adapter, parses no result, and knows no decision rule, so a future
139
+ # built-in source needs nothing here beyond an `answers` command and a binding.
140
+ # External binding responses never enter this authority-bearing intake.
141
+ #
142
+ # Feeding is deliberately independent of handling: it never acknowledges a result
143
+ # and never suppresses a wake. Recording the captain's answer is transcription,
144
+ # while ACTING on it is firstmate's judgement, so the capture stays unacknowledged
145
+ # and its `check` wake reaches the handler exactly as it would have anyway.
146
+ #
147
+ # Ownership is machine-wide per canonical source, because separate Firstmate
148
+ # homes can share one underlying source store. A live owner is never displaced;
149
+ # only a claim whose whole generation is gone is reclaimed. A runner leads its
150
+ # own process group, so a crashed leader whose group still has members is not
151
+ # stale: reconcile stops that surviving group and releases its generation before
152
+ # any replacement starts, and keeps the claim for a later retry when it cannot.
153
+ #
154
+ # Durability boundary: see bin/fm-procevent-lib.sh. This runner proves capture
155
+ # before publication and bounded re-announcement until handled, and nothing
156
+ # about the source side of the handoff.
157
+ set -u
158
+
159
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
160
+ FM_ROOT="${FM_ROOT_OVERRIDE:-$(cd "$SCRIPT_DIR/.." && pwd)}"
161
+ FM_HOME="${FM_HOME:-${FM_ROOT_OVERRIDE:-$FM_ROOT}}"
162
+ STATE="${FM_STATE_OVERRIDE:-$FM_HOME/state}"
163
+
164
+ # shellcheck source=bin/fm-pr-lib.sh
165
+ . "$SCRIPT_DIR/fm-pr-lib.sh"
166
+ # shellcheck source=bin/fm-wake-lib.sh
167
+ . "$SCRIPT_DIR/fm-wake-lib.sh"
168
+ # shellcheck source=bin/fm-procevent-lib.sh
169
+ . "$SCRIPT_DIR/fm-procevent-lib.sh"
170
+
171
+ REG=$(fm_procevent_registry_dir "$STATE")
172
+ MAX_OUTPUT_BYTES=${FM_PROCEVENT_MAX_OUTPUT_BYTES:-1048576}
173
+ EXTENSION_HOST="$SCRIPT_DIR/fm-extension.mjs"
174
+ EXTENSION_LIFECYCLE_LOCK="$REG/.extension-binding-lifecycle.lock"
175
+
176
+ die() { printf 'error: %s\n' "$1" >&2; exit 1; }
177
+ usage() { sed -n '2,/^set -u$/p' "${BASH_SOURCE[0]}" | sed '$d; s/^# \{0,1\}//'; exit 2; }
178
+
179
+ adapter_script() { printf '%s/bin/fm-procevent-%s.sh\n' "$FM_ROOT" "$1"; }
180
+
181
+ extension_lifecycle_lock_acquire() {
182
+ (umask 077; mkdir -p "$REG") || return 1
183
+ [ -d "$REG" ] && [ ! -L "$REG" ] || return 1
184
+ fm_lock_acquire_wait "$EXTENSION_LIFECYCLE_LOCK"
185
+ }
186
+
187
+ extension_lifecycle_lock_release() {
188
+ fm_lock_release "$EXTENSION_LIFECYCLE_LOCK"
189
+ }
190
+
191
+ run_extension_invocation_cleanup() { # [cleanup selector...]
192
+ [ -x "$EXTENSION_HOST" ] && [ ! -L "$EXTENSION_HOST" ] || return 1
193
+ if [ -n "${FM_STATE_OVERRIDE:-}" ]; then
194
+ FM_HOME="$FM_HOME" FM_STATE_OVERRIDE="$STATE" \
195
+ "$EXTENSION_HOST" cleanup-invocations "$@" >/dev/null 2>&1
196
+ else
197
+ FM_HOME="$FM_HOME" "$EXTENSION_HOST" cleanup-invocations "$@" >/dev/null 2>&1
198
+ fi
199
+ }
200
+
201
+ cleanup_extension_binding_invocations() { # <binding-digest>
202
+ run_extension_invocation_cleanup --binding-digest "$1"
203
+ }
204
+
205
+ cleanup_extension_registration_invocations_locked() { # <source-id>
206
+ local owner_state
207
+ fm_procevent_extension_registration_load_locked "$STATE" "$1"
208
+ owner_state=$?
209
+ case "$owner_state" in
210
+ 0) cleanup_extension_binding_invocations "$FM_PROCEVENT_EXTENSION_BINDING_DIGEST" ;;
211
+ 1) return 0 ;;
212
+ *) return 1 ;;
213
+ esac
214
+ }
215
+
216
+ # Invoke one captured result through its exact extension owner. The immutable
217
+ # sidecar, not the current adapter name alone, supplies every expected binding
218
+ # field, so replacing a binding cannot reinterpret old evidence.
219
+ extension_result_command() { # <adapter> <operation> <result-file>
220
+ local adapter=$1 operation=$2 result=$3 owner_state reservation='' owner claim_path handoff_status
221
+ fm_procevent_result_extension_load "$result"
222
+ owner_state=$?
223
+ [ "$owner_state" -eq 0 ] || return 1
224
+ [ -x "$EXTENSION_HOST" ] && [ ! -L "$EXTENSION_HOST" ] || return 1
225
+ case "$operation" in
226
+ result.terminal) reservation=${FM_PROCEVENT_CAPTURE_RESERVATION_TERMINAL:-} ;;
227
+ result.silent) reservation=${FM_PROCEVENT_CAPTURE_RESERVATION_SILENT:-} ;;
228
+ esac
229
+ local -a command=("$EXTENSION_HOST" process-event "$adapter" "$operation"
230
+ --result-file "$result"
231
+ --expect-extension "$FM_PROCEVENT_RESULT_EXTENSION_ID"
232
+ --expect-version "$FM_PROCEVENT_RESULT_EXTENSION_VERSION"
233
+ --expect-capability-version "$FM_PROCEVENT_RESULT_EXTENSION_CAPABILITY_VERSION"
234
+ --expect-package-digest "$FM_PROCEVENT_RESULT_EXTENSION_PACKAGE_DIGEST"
235
+ --expect-binding-digest "$FM_PROCEVENT_RESULT_EXTENSION_BINDING_DIGEST")
236
+ if [ -n "$reservation" ]; then
237
+ extension_lifecycle_lock_acquire || return 1
238
+ owner=${FM_LOCK_OWNER_DIR:-}
239
+ [ -n "$owner" ] || { extension_lifecycle_lock_release; return 1; }
240
+ claim_path=$(fm_procevent_claim_path "$CLAIM_ID") || { extension_lifecycle_lock_release; return 1; }
241
+ FM_EXTENSION_RETIREMENT_MODE=process-event \
242
+ FM_EXTENSION_LIFECYCLE_LOCK="$EXTENSION_LIFECYCLE_LOCK" \
243
+ FM_EXTENSION_LIFECYCLE_OWNER="$owner" \
244
+ perl "$SCRIPT_DIR/fm-procevent-extension-capture.pl" handoff \
245
+ 8 6 "$claim_path" "$CLAIM_HOME" "$CLAIM_ID" "$CLAIM_TOKEN" "$CLAIM_PID" \
246
+ "$(fm_pid_identity "$CLAIM_PID")" "$FM_PROCEVENT_RESULT_EXTENSION_BINDING_DIGEST" "$reservation" \
247
+ "$operation" "$result" "$EXTENSION_HOST" -- "${command[@]:1}"
248
+ handoff_status=$?
249
+ extension_lifecycle_lock_release
250
+ return "$handoff_status"
251
+ fi
252
+ "${command[@]}"
253
+ }
254
+
255
+ # Ask the source's own adapter whether a captured result ends the source. Exit 0
256
+ # is the only terminal verdict; everything else - including a missing adapter
257
+ # command - keeps the registration armed. See the terminal-knowledge note in the
258
+ # header: no adapter-specific condition may appear in this runner.
259
+ adapter_result_is_terminal() { # <adapter> <result-file>
260
+ local script owner_state
261
+ fm_procevent_result_extension_load "$2"
262
+ owner_state=$?
263
+ case "$owner_state" in
264
+ 0) extension_result_command "$1" result.terminal "$2" >/dev/null 2>&1; return $? ;;
265
+ 2) return 1 ;;
266
+ esac
267
+ script=$(adapter_script "$1")
268
+ [ -f "$script" ] && [ ! -L "$script" ] || return 1
269
+ "$script" terminal "$2" >/dev/null 2>&1
270
+ }
271
+
272
+ # Ask the source's own adapter whether a captured result is a routine no-op that
273
+ # needs no wake at all. This mirrors the terminal seam above exactly: exit 0 is
274
+ # the only silence verdict, and everything else - including a missing adapter
275
+ # command - publishes the wake. See the routine-no-op note in the header: no
276
+ # adapter-specific condition may appear in this runner.
277
+ adapter_result_is_silent() { # <adapter> <result-file>
278
+ local script owner_state
279
+ fm_procevent_result_extension_load "$2"
280
+ owner_state=$?
281
+ case "$owner_state" in
282
+ 0) extension_result_command "$1" result.silent "$2" >/dev/null 2>&1; return $? ;;
283
+ 2) return 1 ;;
284
+ esac
285
+ script=$(adapter_script "$1")
286
+ [ -f "$script" ] && [ ! -L "$script" ] || return 1
287
+ "$script" silent "$2" >/dev/null 2>&1
288
+ }
289
+
290
+ # Ask the adapter whether its autohandled results announce themselves through a
291
+ # durable downstream channel of their own (see the announcement-ownership note
292
+ # in the header). Exit 0 is the only declaration; everything else - including a
293
+ # missing adapter or an adapter without the command - keeps the strict
294
+ # publish-before-apply order.
295
+ adapter_self_announcing() { # <adapter>
296
+ local script
297
+ script=$(adapter_script "$1")
298
+ [ -f "$script" ] && [ ! -L "$script" ] || return 1
299
+ "$script" self-announcing >/dev/null 2>&1
300
+ }
301
+
302
+ source_file() { printf '%s/%s.source\n' "$REG" "$1"; }
303
+ runner_file() { printf '%s/%s.runner\n' "$REG" "$1"; }
304
+ staging_file() { printf '%s/.%s.%s.output\n' "$REG" "$1" "$2"; }
305
+
306
+ # Let the source's own adapter apply and acknowledge one captured result. See
307
+ # the header for why this exists and what each exit means. An already
308
+ # acknowledged result is skipped, so this is safe to call more than once for the
309
+ # same generation. The adapter runs OUTSIDE any source-lock hold here, because a
310
+ # handling adapter is expected to re-arm its own next source, which takes that
311
+ # same lock.
312
+ adapter_autohandle() { # <adapter> <source-id> <result-file>
313
+ local adapter=$1 id=$2 result=$3 script seq
314
+ script=$(adapter_script "$adapter")
315
+ [ -f "$script" ] && [ ! -L "$script" ] || return 1
316
+ seq=$(fm_procevent_result_sequence "$result") || return 1
317
+ case "$seq" in ''|*[!0-9]*) return 1 ;; esac
318
+ fm_procevent_is_handled "$STATE" "$id" "$seq" && return 0
319
+ # Silenced exactly like the terminal seam above, so an adapter that has no
320
+ # such command is a quiet no-op rather than runner noise. This runner's own
321
+ # one-line outcome is the interface; an adapter that failed keeps its result
322
+ # announced, and the handler's own call reproduces the diagnostics in full.
323
+ "$script" autohandle "$id" "$seq" "$result" >/dev/null 2>&1
324
+ }
325
+
326
+ # Pass a bound source's captured result to the one keyed-answer intake. The
327
+ # adapter turns its own format into keyed lines; the intake owns everything those
328
+ # lines mean. Silenced and best-effort exactly like the seams above: an unbound
329
+ # source, an adapter with no `answers` command, and a failure on either side all
330
+ # leave the capture untouched and still announced, because this never
331
+ # acknowledges anything (see the keyed-answer note in the header).
332
+ feed_keyed_answers() { # <adapter> <source-id> <result-file>
333
+ local adapter=$1 id=$2 result=$3 script origin seq
334
+ script=$(adapter_script "$adapter")
335
+ [ -f "$script" ] && [ ! -L "$script" ] || return 1
336
+ origin=$("$SCRIPT_DIR/fm-captain-hold.sh" binding "$id" 2>/dev/null) || return 1
337
+ [ -n "$origin" ] || return 1
338
+ seq=$(fm_procevent_result_sequence "$result") || return 1
339
+ "$script" answers "$result" 2>/dev/null \
340
+ | "$SCRIPT_DIR/fm-captain-hold.sh" answers "$origin" \
341
+ --source "the captured result $id sequence $seq" >/dev/null 2>&1
342
+ }
343
+
344
+ read_adapter() { # <source-id>
345
+ local f; f=$(source_file "$1")
346
+ [ -f "$f" ] && [ ! -L "$f" ] || return 1
347
+ sed -n 's/^adapter=//p' "$f" | head -1
348
+ }
349
+
350
+ # Read the stored argv into the ARGV array. One argument per line after the
351
+ # argv= count, so an argument containing spaces is not re-split.
352
+ read_argv() { # <source-id>
353
+ local f n; f=$(source_file "$1")
354
+ ARGV=()
355
+ [ -f "$f" ] && [ ! -L "$f" ] || return 1
356
+ n=$(sed -n 's/^argc=//p' "$f" | head -1)
357
+ case "$n" in ''|*[!0-9]*) return 1 ;; esac
358
+ local i=0 line
359
+ while IFS= read -r line; do
360
+ i=$((i + 1))
361
+ [ "$i" -le "$n" ] && ARGV+=("$line")
362
+ done < <(sed -n '/^argv:$/,$p' "$f" | tail -n +2)
363
+ [ "${#ARGV[@]}" -eq "$n" ]
364
+ }
365
+
366
+ extension_registration_replacement_safe_locked() { # <source-id>
367
+ local id=$1 owner_state claim_state
368
+ if [ ! -e "$(source_file "$id")" ] && [ ! -L "$(source_file "$id")" ]; then
369
+ return 0
370
+ fi
371
+ fm_procevent_extension_registration_load_locked "$STATE" "$id"
372
+ owner_state=$?
373
+ [ "$owner_state" -eq 0 ] || return 0
374
+ fm_procevent_claim_state_locked "$id"
375
+ claim_state=$?
376
+ case "$claim_state" in
377
+ 0|2|3|4) return 1 ;;
378
+ *) return 0 ;;
379
+ esac
380
+ }
381
+
382
+ cmd_register() {
383
+ local adapter=${1-} id=${2-} sep=${3-}
384
+ shift 3 2>/dev/null || usage
385
+ fm_procevent_adapter_valid "$adapter" || die "adapter name must be lowercase alphanumeric or dash: $adapter"
386
+ fm_procevent_source_id_valid "$id" || die "source id must be path-safe and at most 64 characters: $id"
387
+ [ "$sep" = -- ] || usage
388
+ [ "$#" -ge 1 ] || die "register needs at least one argv element after --"
389
+ local arg
390
+ for arg in "$@"; do
391
+ case "$arg" in *$'\n'*) die "argv elements cannot contain newlines" ;; esac
392
+ done
393
+ [ -f "$(adapter_script "$adapter")" ] || die "no installed adapter for: $adapter"
394
+ fm_procevent_source_lock_acquire "$id" || die "cannot lock the source"
395
+ if ! extension_registration_replacement_safe_locked "$id"; then
396
+ fm_procevent_source_lock_release "$id"
397
+ die "cannot replace extension registration while its prior runner remains active: $id"
398
+ fi
399
+ if ! fm_procevent_registration_publish_locked "$STATE" "$adapter" "$id" "$@"; then
400
+ fm_procevent_source_lock_release "$id"
401
+ die "cannot publish the registration"
402
+ fi
403
+ fm_procevent_source_lock_release "$id"
404
+ printf 'registered: %s (%s)\n' "$id" "$adapter"
405
+ }
406
+
407
+ new_extension_registration_token() {
408
+ local hex
409
+ hex=$(LC_ALL=C od -An -v -tx1 -N 32 /dev/urandom 2>/dev/null | tr -d ' \n') || return 1
410
+ [ "${#hex}" -eq 64 ] || return 1
411
+ printf 'sha256:%s\n' "$hex"
412
+ }
413
+
414
+ extension_source_request_id() { # <adapter> <source-id> <next-sequence> <registration-token> <package-digest>
415
+ local digest
416
+ if command -v shasum >/dev/null 2>&1; then
417
+ digest=$(printf 'firstmate-process-event-request-v1\n%s\n%s\n%s\n%s\n%s\n' "$@" \
418
+ | shasum -a 256 | awk '{print $1}') || return 1
419
+ elif command -v sha256sum >/dev/null 2>&1; then
420
+ digest=$(printf 'firstmate-process-event-request-v1\n%s\n%s\n%s\n%s\n%s\n' "$@" \
421
+ | sha256sum | awk '{print $1}') || return 1
422
+ else
423
+ return 1
424
+ fi
425
+ [ "${#digest}" -eq 64 ] || return 1
426
+ printf 'sha256:%s\n' "$digest"
427
+ }
428
+
429
+ next_result_sequence() { # <source-id>
430
+ local id=$1 inbox seq=1
431
+ inbox=$(fm_procevent_inbox_dir "$STATE")
432
+ while [ -e "$inbox/$id.$seq.result" ]; do seq=$((seq + 1)); done
433
+ printf '%s\n' "$seq"
434
+ }
435
+
436
+ cmd_register_extension() {
437
+ local adapter=${1-} id=${2-} option=${3-} config_ref=${4-} resolution schema extension_id
438
+ local extension_version capability_version package_digest binding_digest extra registration_token
439
+ [ "$#" -eq 4 ] || usage
440
+ fm_procevent_adapter_valid "$adapter" || die "adapter name must be lowercase alphanumeric or dash: $adapter"
441
+ fm_procevent_source_id_valid "$id" || die "source id must be path-safe and at most 64 characters: $id"
442
+ [ "$option" = --config-ref ] || usage
443
+ fm_procevent_extension_config_ref_valid "$config_ref" \
444
+ || die "source configuration reference must be one bounded line"
445
+ if [ ! -x "$EXTENSION_HOST" ] || [ -L "$EXTENSION_HOST" ]; then
446
+ die "the tracked extension host is unavailable"
447
+ fi
448
+ extension_lifecycle_lock_acquire || die "cannot lock the extension lifecycle"
449
+ if ! resolution=$("$EXTENSION_HOST" resolve-process-event "$adapter"); then
450
+ extension_lifecycle_lock_release
451
+ die "extension adapter verification failed: $adapter"
452
+ fi
453
+ if [ "$(printf '%s\n' "$resolution" | wc -l | tr -d ' ')" != 1 ]; then
454
+ extension_lifecycle_lock_release
455
+ die "extension adapter resolution was malformed: $adapter"
456
+ fi
457
+ IFS=$'\t' read -r schema extension_id extension_version capability_version \
458
+ package_digest binding_digest extra <<< "$resolution"
459
+ if [ "$schema" != fm-extension-process-event-resolution.v1 ] || [ -n "$extra" ]; then
460
+ extension_lifecycle_lock_release
461
+ die "extension adapter resolution was malformed: $adapter"
462
+ fi
463
+ if ! fm_procevent_extension_id_valid "$extension_id" \
464
+ || ! fm_procevent_extension_version_valid "$extension_version" \
465
+ || [ "$capability_version" != 1 ] \
466
+ || ! fm_procevent_digest_valid "$package_digest" \
467
+ || ! fm_procevent_digest_valid "$binding_digest"; then
468
+ extension_lifecycle_lock_release
469
+ die "extension adapter identity was malformed: $adapter"
470
+ fi
471
+ if ! registration_token=$(new_extension_registration_token); then
472
+ extension_lifecycle_lock_release
473
+ die "cannot create an extension registration identity"
474
+ fi
475
+ if ! fm_procevent_source_lock_acquire "$id"; then
476
+ extension_lifecycle_lock_release
477
+ die "cannot lock the source"
478
+ fi
479
+ if ! extension_registration_replacement_safe_locked "$id"; then
480
+ fm_procevent_source_lock_release "$id"
481
+ extension_lifecycle_lock_release
482
+ die "cannot replace extension registration while its prior runner remains active: $id"
483
+ fi
484
+ if ! fm_procevent_extension_registration_publish_locked "$STATE" "$adapter" "$id" \
485
+ "$extension_id" "$extension_version" "$capability_version" "$package_digest" \
486
+ "$binding_digest" "$config_ref" "$registration_token"; then
487
+ fm_procevent_source_lock_release "$id"
488
+ extension_lifecycle_lock_release
489
+ die "cannot publish the extension registration"
490
+ fi
491
+ fm_procevent_source_lock_release "$id"
492
+ extension_lifecycle_lock_release
493
+ printf 'registered: %s (%s from %s@%s)\n' "$id" "$adapter" "$extension_id" "$extension_version"
494
+ printf 'owner-token: %s\n' "$registration_token"
495
+ printf 'retire: bin/fm-procevent.sh retire %s --if-owner %s\n' "$id" "$registration_token"
496
+ }
497
+
498
+ # Publish every durably captured result with no handled acknowledgement yet.
499
+ # Capture already happened, so this only turns durable state into durable
500
+ # events - and it republishes on every call regardless of any earlier
501
+ # publication, so a result stays eligible for re-announcement across restarts
502
+ # and drains until `fm_procevent_mark_handled` records it.
503
+ publish_result() { # <result-file>
504
+ local result=$1 id seq adapter line status=1
505
+ id=$(fm_procevent_result_source_id "$result")
506
+ seq=$(fm_procevent_result_sequence "$result")
507
+ fm_procevent_source_id_valid "$id" || return 1
508
+ adapter=$(fm_procevent_result_adapter "$result" 2>/dev/null || true)
509
+ [ -n "$adapter" ] || return 1
510
+ line=$(fm_procevent_event_line "$adapter" "$id" "$seq") || return 1
511
+ fm_procevent_source_lock_acquire "$id" || return 1
512
+ if ! fm_procevent_is_handled "$STATE" "$id" "$seq"; then
513
+ # A result its own adapter declares a routine no-op is recorded as handled
514
+ # and never announced, so it neither wakes a handler now nor comes back on
515
+ # a later reconcile's re-announcement. Recording it is what makes that
516
+ # silence durable, so both a newly written marker (0) and one a concurrent
517
+ # caller already wrote (1) settle it; only an unrecordable silence (2)
518
+ # falls through and announces, because a silence nothing remembers would
519
+ # otherwise be re-evaluated on every reconcile forever.
520
+ export FM_PROCEVENT_CAPTURE_SOURCE_LOCK_HELD=1
521
+ if adapter_result_is_silent "$adapter" "$result"; then
522
+ unset FM_PROCEVENT_CAPTURE_SOURCE_LOCK_HELD
523
+ fm_procevent_mark_handled "$STATE" "$id" "$seq"
524
+ case "$?" in
525
+ 0|1)
526
+ fm_procevent_source_lock_release "$id"
527
+ return 1
528
+ ;;
529
+ esac
530
+ fi
531
+ unset FM_PROCEVENT_CAPTURE_SOURCE_LOCK_HELD
532
+ if fm_wake_append check "procevent:$id:$seq" "check: $line"; then
533
+ status=0
534
+ fi
535
+ fi
536
+ fm_procevent_source_lock_release "$id"
537
+ return "$status"
538
+ }
539
+
540
+ publish_pending() { # [result-file-to-skip]
541
+ local skip=${1-} result published=0
542
+ while IFS= read -r result; do
543
+ [ -n "$result" ] || continue
544
+ [ "$result" = "$skip" ] && continue
545
+ if publish_result "$result"; then
546
+ published=$((published + 1))
547
+ fi
548
+ done < <(fm_procevent_pending "$STATE")
549
+ printf '%s\n' "$published"
550
+ }
551
+
552
+ isolate_runner() { # <wait|detach> <source-id>
553
+ local mode=$1 id=$2 program
554
+ # shellcheck disable=SC2016 # Perl owns every $ expression in this literal program.
555
+ program='my $mode = shift @ARGV;
556
+ defined(my $pid = fork) or exit 125;
557
+ if ($pid == 0) {
558
+ setpgrp(0, 0) or exit 125;
559
+ $ENV{FM_PROCEVENT_RUNNER_GROUP} = $$;
560
+ exec @ARGV;
561
+ exit 125;
562
+ }
563
+ exit 0 if $mode eq "detach";
564
+ waitpid($pid, 0) == $pid or exit 125;
565
+ my $status = $?;
566
+ exit(128 + ($status & 127)) if $status & 127;
567
+ exit($status >> 8);'
568
+ if [ "$mode" = wait ]; then
569
+ exec perl -e "$program" "$mode" "$SCRIPT_DIR/fm-procevent.sh" _start "$id"
570
+ fi
571
+ perl -e "$program" "$mode" "$SCRIPT_DIR/fm-procevent.sh" _start "$id" >/dev/null 2>&1 &
572
+ }
573
+
574
+ require_runner_group() {
575
+ local pgid
576
+ [ "${FM_PROCEVENT_RUNNER_GROUP:-}" = "$$" ] \
577
+ || die "runner process group was not isolated"
578
+ pgid=$(ps -o pgid= -p "$$" 2>/dev/null | tr -d '[:space:]') \
579
+ || die "cannot inspect runner process group"
580
+ [ -n "$pgid" ] || die "cannot inspect runner process group"
581
+ [ "$pgid" = "$$" ] || die "runner does not lead its process group"
582
+ unset FM_PROCEVENT_RUNNER_GROUP
583
+ }
584
+
585
+ cmd_start_public() {
586
+ local id=${1-}
587
+ [ "$#" -eq 1 ] || usage
588
+ fm_procevent_source_id_valid "$id" || die "source id must be path-safe: $id"
589
+ isolate_runner wait "$id"
590
+ }
591
+
592
+ cmd_start() {
593
+ local id=${1-} adapter out rc claimed bound_rc published_capture=0 handled_capture=0 self_announcing=0
594
+ local extension_owner=0 extension_load_state extension_sequence='' extension_request_id=''
595
+ fm_procevent_source_id_valid "$id" || die "source id must be path-safe: $id"
596
+ require_runner_group
597
+ fm_procevent_source_lock_acquire "$id" || die "cannot lock source: $id"
598
+ if [ ! -f "$(source_file "$id")" ] || [ -L "$(source_file "$id")" ]; then
599
+ fm_procevent_source_lock_release "$id"
600
+ die "source is not registered: $id"
601
+ fi
602
+ if ! adapter=$(read_adapter "$id"); then
603
+ fm_procevent_source_lock_release "$id"
604
+ die "registration is unreadable: $id"
605
+ fi
606
+ if ! fm_procevent_adapter_valid "$adapter"; then
607
+ fm_procevent_source_lock_release "$id"
608
+ die "registration names an invalid adapter"
609
+ fi
610
+ fm_procevent_extension_registration_load_locked "$STATE" "$id"
611
+ extension_load_state=$?
612
+ case "$extension_load_state" in
613
+ 0)
614
+ extension_owner=1
615
+ [ "$FM_PROCEVENT_EXTENSION_ADAPTER" = "$adapter" ] || {
616
+ fm_procevent_source_lock_release "$id"
617
+ die "extension registration adapter identity is inconsistent: $id"
618
+ }
619
+ [ -x "$EXTENSION_HOST" ] && [ ! -L "$EXTENSION_HOST" ] || {
620
+ fm_procevent_source_lock_release "$id"
621
+ die "the tracked extension host is unavailable"
622
+ }
623
+ extension_sequence=$(next_result_sequence "$id") \
624
+ || { fm_procevent_source_lock_release "$id"; die "cannot derive extension request sequence: $id"; }
625
+ extension_request_id=$(extension_source_request_id "$adapter" "$id" "$extension_sequence" \
626
+ "$FM_PROCEVENT_EXTENSION_REGISTRATION_TOKEN" "$FM_PROCEVENT_EXTENSION_PACKAGE_DIGEST") \
627
+ || { fm_procevent_source_lock_release "$id"; die "cannot derive extension request identity: $id"; }
628
+ ARGV=("$EXTENSION_HOST" process-event "$adapter" source.poll \
629
+ --source-id "$id" --config-ref "$FM_PROCEVENT_EXTENSION_CONFIG_REF" \
630
+ --request-id "$extension_request_id" \
631
+ --expect-extension "$FM_PROCEVENT_EXTENSION_ID" \
632
+ --expect-version "$FM_PROCEVENT_EXTENSION_VERSION" \
633
+ --expect-capability-version "$FM_PROCEVENT_EXTENSION_CAPABILITY_VERSION" \
634
+ --expect-package-digest "$FM_PROCEVENT_EXTENSION_PACKAGE_DIGEST" \
635
+ --expect-binding-digest "$FM_PROCEVENT_EXTENSION_BINDING_DIGEST")
636
+ ;;
637
+ 1)
638
+ if ! read_argv "$id"; then
639
+ fm_procevent_source_lock_release "$id"
640
+ die "registration argv is unreadable: $id"
641
+ fi
642
+ ;;
643
+ *)
644
+ fm_procevent_source_lock_release "$id"
645
+ die "extension registration owner is unreadable: $id"
646
+ ;;
647
+ esac
648
+ fm_procevent_claim_acquire_locked "$id" "$FM_HOME" "$$" "$(source_file "$id")"
649
+ claimed=$?
650
+ fm_procevent_source_lock_release "$id"
651
+ case "$claimed" in
652
+ 0) ;;
653
+ 2) printf 'already owned: %s\n' "$id"; exit 0 ;;
654
+ *) die "cannot claim source: $id" ;;
655
+ esac
656
+ CLAIM_ID=$id
657
+ CLAIM_HOME=$FM_HOME
658
+ CLAIM_PID=$$
659
+ CLAIM_TOKEN=$FM_PROCEVENT_CLAIM_TOKEN
660
+ CLAIM_REG_IDENTITY=$FM_PROCEVENT_CLAIM_REG_IDENTITY
661
+ STAGED_OUTPUT=
662
+ release_start_claim() {
663
+ extension_lifecycle_lock_release 2>/dev/null || true
664
+ [ -z "$STAGED_OUTPUT" ] || rm -f -- "$STAGED_OUTPUT"
665
+ fm_procevent_source_lock_acquire "$CLAIM_ID" 2>/dev/null || return 0
666
+ if fm_procevent_claim_load_locked "$CLAIM_ID" 2>/dev/null \
667
+ && [ "$FM_PROCEVENT_CLAIM_HOME" = "$CLAIM_HOME" ] \
668
+ && [ "$FM_PROCEVENT_CLAIM_PID" = "$CLAIM_PID" ] \
669
+ && [ "$FM_PROCEVENT_CLAIM_TOKEN" = "$CLAIM_TOKEN" ] \
670
+ && [ "$FM_PROCEVENT_CLAIM_TERMINAL" = terminal ]; then
671
+ fm_procevent_source_lock_release "$CLAIM_ID" 2>/dev/null || true
672
+ return 0
673
+ fi
674
+ fm_procevent_claim_release_locked "$CLAIM_ID" "$CLAIM_HOME" "$CLAIM_PID" "$CLAIM_TOKEN" 2>/dev/null || true
675
+ fm_procevent_source_lock_release "$CLAIM_ID" 2>/dev/null || true
676
+ }
677
+ trap release_start_claim EXIT
678
+ local runner inbox reservation_dir
679
+ if [ "$extension_owner" -eq 1 ]; then
680
+ fm_procevent_extension_staging_prepare "$STATE" \
681
+ || die "cannot safely prepare the external registry staging boundary"
682
+ inbox=$(fm_procevent_capture_inbox_prepare "$STATE") \
683
+ || die "cannot durably capture the extension result"
684
+ CDPATH='' cd -- "$REG" 2>/dev/null \
685
+ || die "cannot safely prepare the external registry staging boundary"
686
+ [ "$(pwd -P)" = "$REG" ] \
687
+ || die "cannot safely prepare the external registry staging boundary"
688
+ exec 9<. || die "cannot retain the external registry staging boundary"
689
+ CDPATH='' cd -- "$inbox" 2>/dev/null \
690
+ || die "cannot durably capture the extension result"
691
+ [ "$(pwd -P)" = "$inbox" ] \
692
+ || die "cannot durably capture the extension result"
693
+ exec 8<. || die "cannot retain the external capture boundary"
694
+ reservation_dir=$(fm_procevent_capture_reservation_prepare "$STATE") \
695
+ || die "cannot retain the external capture reservation boundary"
696
+ exec 6<"$reservation_dir" || die "cannot retain the external capture reservation boundary"
697
+ FM_PROCEVENT_CAPTURE_PINNED_INBOX=1
698
+ export FM_PROCEVENT_CAPTURE_INBOX_FD=8
699
+ runner="$id.runner"
700
+ else
701
+ runner=$(runner_file "$id")
702
+ fi
703
+
704
+ case "$MAX_OUTPUT_BYTES" in ''|*[!0-9]*) die "FM_PROCEVENT_MAX_OUTPUT_BYTES must be a nonnegative integer" ;; esac
705
+ if [ "$extension_owner" -eq 1 ]; then
706
+ out=".$id.$CLAIM_TOKEN.output"
707
+ else
708
+ out=$(staging_file "$id" "$CLAIM_TOKEN")
709
+ printf '%s\n' "$$" > "$runner" 2>/dev/null || true
710
+ chmod 0600 "$runner" 2>/dev/null || true
711
+ fi
712
+ # Built-in adapters do not run the extension capture helper, so keep this
713
+ # sentinel defined while sharing the no-result branch below under `set -u`.
714
+ local truncated=0 capture_state='' durable='' reservation_terminal='' reservation_silent=''
715
+ if [ "$extension_owner" -eq 1 ]; then
716
+ capture_state=$(perl "$SCRIPT_DIR/fm-procevent-extension-capture.pl" \
717
+ 9 8 6 "$id" "$adapter" "$FM_PROCEVENT_EXTENSION_ID" \
718
+ "$FM_PROCEVENT_EXTENSION_VERSION" "$FM_PROCEVENT_EXTENSION_CAPABILITY_VERSION" \
719
+ "$FM_PROCEVENT_EXTENSION_PACKAGE_DIGEST" "$FM_PROCEVENT_EXTENSION_BINDING_DIGEST" \
720
+ "$CLAIM_TOKEN" "$runner" "$out" "$$" "$(fm_pid_identity "$$")" "$MAX_OUTPUT_BYTES" -- "${ARGV[@]}") \
721
+ || die "cannot safely stage the extension result"
722
+ IFS=$'\t' read -r capture_state durable rc truncated reservation_terminal reservation_silent <<EOF
723
+ $capture_state
724
+ EOF
725
+ exec 9<&-
726
+ case "$capture_state" in
727
+ captured|no-result) ;;
728
+ failure) die "external source invocation failed: $id" ;;
729
+ *) die "cannot safely stage the extension result" ;;
730
+ esac
731
+ if [ "$capture_state" = captured ]; then
732
+ FM_PROCEVENT_CAPTURE_RESERVATION_TERMINAL=$reservation_terminal
733
+ FM_PROCEVENT_CAPTURE_RESERVATION_SILENT=$reservation_silent
734
+ fi
735
+ else
736
+ [ ! -e "$out" ] && [ ! -L "$out" ] || die "cannot safely stage output"
737
+ (umask 077; : > "$out") || die "cannot stage output"
738
+ STAGED_OUTPUT=$out
739
+ "${ARGV[@]}" 2>/dev/null | perl -e '
740
+ use strict;
741
+ use warnings;
742
+ my $limit = shift;
743
+ my ($written, $truncated) = (0, 0);
744
+ while (1) {
745
+ my $count = sysread(STDIN, my $buffer, 65536);
746
+ exit 2 unless defined $count;
747
+ last if $count == 0;
748
+ my $take = $written < $limit ? $limit - $written : 0;
749
+ $take = $count if $take > $count;
750
+ if ($take > 0) {
751
+ my $offset = 0;
752
+ while ($offset < $take) {
753
+ my $count_written = syswrite(STDOUT, $buffer, $take - $offset, $offset);
754
+ exit 2 unless defined $count_written;
755
+ $offset += $count_written;
756
+ }
757
+ $written += $take;
758
+ }
759
+ $truncated = 1 if $take < $count;
760
+ }
761
+ exit($truncated ? 3 : 0);
762
+ ' "$MAX_OUTPUT_BYTES" > "$out"
763
+ local pipe_status=("${PIPESTATUS[@]}")
764
+ rc=${pipe_status[0]}
765
+ bound_rc=${pipe_status[1]}
766
+ case "$bound_rc" in
767
+ 0) ;;
768
+ 3) truncated=1 ;;
769
+ *) die "cannot bound source output" ;;
770
+ esac
771
+ fi
772
+
773
+ if [ "$capture_state" = no-result ] || { [ "$extension_owner" -eq 0 ] && [ "$rc" -ne 0 ] && [ ! -s "$out" ]; }; then
774
+ # No usable result. Leave the registration armed; the adapter decides
775
+ # whether a nonzero exit is terminal when it handles the next result.
776
+ if [ "$extension_owner" -eq 0 ]; then
777
+ rm -f -- "$out" "$runner"
778
+ fi
779
+ printf 'no-result: %s (exit %s)\n' "$id" "$rc"
780
+ exit 0
781
+ fi
782
+
783
+ if [ "$extension_owner" -eq 1 ]; then
784
+ durable="./$durable"
785
+ fi
786
+
787
+ if [ "$extension_owner" -eq 1 ]; then
788
+ :
789
+ else
790
+ durable=$(fm_procevent_capture "$STATE" "$id" "$adapter" "$out") \
791
+ || { rm -f -- "$out"; die "cannot durably capture the result"; }
792
+ fi
793
+ [ "$extension_owner" -eq 1 ] || rm -f -- "$out"
794
+ STAGED_OUTPUT=
795
+ [ "$truncated" -eq 1 ] && printf 'truncated: %s at %s bytes\n' "$id" "$MAX_OUTPUT_BYTES" >&2
796
+
797
+ # Independent of publication and acknowledgement, so it runs once per capture
798
+ # for every adapter and cannot change what the handler receives.
799
+ if [ "$extension_owner" -eq 0 ] \
800
+ && feed_keyed_answers "$adapter" "$id" "$durable"; then
801
+ printf 'answers-fed: %s\n' "$id"
802
+ fi
803
+
804
+ # A self-announcing adapter's autohandle announces through its own durable
805
+ # downstream channel, so publication waits until after application and covers
806
+ # only what remains unhandled; every other adapter keeps the strict
807
+ # publish-before-apply order (announcement-ownership note in the header).
808
+ if [ "$extension_owner" -eq 0 ] && adapter_self_announcing "$adapter"; then
809
+ self_announcing=1
810
+ else
811
+ if publish_result "$durable"; then
812
+ published_capture=1
813
+ elif fm_procevent_is_handled "$STATE" "$id" "$(fm_procevent_result_sequence "$durable")"; then
814
+ handled_capture=1
815
+ fi
816
+ publish_pending "$durable" >/dev/null
817
+ fi
818
+ [ "$extension_owner" -eq 1 ] || rm -f -- "$runner"
819
+ if [ "$self_announcing" -eq 1 ]; then
820
+ if adapter_autohandle "$adapter" "$id" "$durable"; then
821
+ printf 'autohandled: %s\n' "$id"
822
+ else
823
+ printf 'not-autohandled: %s (left for the handler; still unacknowledged)\n' "$id" >&2
824
+ fi
825
+ # publish_result's own handled guard keeps a fully autohandled capture
826
+ # quiet here; anything the adapter left unhandled is announced exactly as
827
+ # before, and a crash above leaves it to reconcile's re-announcement.
828
+ if publish_result "$durable"; then
829
+ published_capture=1
830
+ fi
831
+ publish_pending "$durable" >/dev/null
832
+ elif [ "$handled_capture" -eq 1 ]; then
833
+ :
834
+ elif [ "$extension_owner" -eq 0 ] \
835
+ && [ "$published_capture" -eq 1 ] \
836
+ && adapter_autohandle "$adapter" "$id" "$durable"; then
837
+ printf 'autohandled: %s\n' "$id"
838
+ else
839
+ printf 'not-autohandled: %s (left for the handler; still unacknowledged)\n' "$id" >&2
840
+ fi
841
+ if adapter_result_is_terminal "$adapter" "$durable"; then
842
+ if retire_owned_terminal_source "$id"; then
843
+ printf 'retired: %s (adapter classified the captured result terminal)\n' "$id"
844
+ else
845
+ printf 'cannot retire terminal source; it remains registered: %s\n' "$id" >&2
846
+ fi
847
+ fi
848
+ printf 'captured: %s\n' "$durable"
849
+ if [ "$extension_owner" -eq 1 ]; then
850
+ fm_procevent_claim_capture_reservation_remove_locked || true
851
+ exec 6<&-
852
+ fi
853
+ }
854
+
855
+ # Retire a source this runner owns because its adapter classified the captured
856
+ # result terminal. Ownership is re-proved, the registration is dropped, and this
857
+ # runner's own claim is released under ONE source-lock hold, so no concurrent
858
+ # reconcile can observe a registered source with no owner (and start a
859
+ # replacement) or an owned claim with no registration (and signal this runner
860
+ # mid-exit), and a generation this runner no longer owns is never unregistered.
861
+ # The EXIT trap's own release then no-ops, because the generation is already gone.
862
+ retire_owned_terminal_source() { # <source-id>
863
+ local id=$1 status=0 registration current_identity
864
+ registration=$(source_file "$id")
865
+ fm_procevent_source_lock_acquire "$id" || return 1
866
+ if fm_procevent_claim_load_locked "$id" 2>/dev/null \
867
+ && [ "$FM_PROCEVENT_CLAIM_HOME" = "$CLAIM_HOME" ] \
868
+ && [ "$FM_PROCEVENT_CLAIM_PID" = "$CLAIM_PID" ] \
869
+ && [ "$FM_PROCEVENT_CLAIM_TOKEN" = "$CLAIM_TOKEN" ] \
870
+ && [ "$FM_PROCEVENT_CLAIM_REG_IDENTITY" = "$CLAIM_REG_IDENTITY" ] \
871
+ && current_identity=$(fm_pr_file_identity "$registration" 2>/dev/null) \
872
+ && [ "$current_identity" = "$CLAIM_REG_IDENTITY" ] \
873
+ && fm_procevent_claim_mark_terminal_locked "$id" "$CLAIM_HOME" "$CLAIM_PID" "$CLAIM_TOKEN"; then
874
+ if rm -f -- "$registration" && [ ! -e "$registration" ] && [ ! -L "$registration" ]; then
875
+ fm_procevent_claim_release_locked "$id" "$CLAIM_HOME" "$CLAIM_PID" "$CLAIM_TOKEN" || status=1
876
+ else
877
+ status=1
878
+ fi
879
+ else
880
+ status=1
881
+ fi
882
+ fm_procevent_source_lock_release "$id"
883
+ return "$status"
884
+ }
885
+
886
+ # Start a runner outside the watcher cycle that noticed it was missing. The
887
+ # public start boundary establishes its own process group before claiming.
888
+ detach_runner() { # <source-id>
889
+ isolate_runner detach "$1"
890
+ }
891
+
892
+ cmd_reconcile() {
893
+ local rec id published started=0 stopped=0 uncertain=0 claim owner pid token identity claim_state stop_state
894
+ published=$(publish_pending)
895
+
896
+ # Stop a runner this home owns whose source is no longer registered. Without
897
+ # this, unregistering a source that never completes leaves its child blocked
898
+ # forever with nothing left to reap it.
899
+ for claim in "$(fm_procevent_claim_root)"/*.claim; do
900
+ [ -e "$claim" ] || continue
901
+ id=${claim##*/}; id=${id%.claim}
902
+ fm_procevent_source_id_valid "$id" || continue
903
+ fm_procevent_source_lock_acquire "$id" || continue
904
+ if [ -f "$(source_file "$id")" ] && [ ! -L "$(source_file "$id")" ]; then
905
+ fm_procevent_source_lock_release "$id"
906
+ continue
907
+ fi
908
+ if ! fm_procevent_claim_load_locked "$id" 2>/dev/null; then
909
+ uncertain=$((uncertain + 1))
910
+ fm_procevent_source_lock_release "$id"
911
+ continue
912
+ fi
913
+ owner=$FM_PROCEVENT_CLAIM_HOME
914
+ pid=$FM_PROCEVENT_CLAIM_PID
915
+ token=$FM_PROCEVENT_CLAIM_TOKEN
916
+ identity=$FM_PROCEVENT_CLAIM_IDENTITY
917
+ if [ "$owner" != "$FM_HOME" ]; then
918
+ fm_procevent_source_lock_release "$id"
919
+ continue
920
+ fi
921
+ stop_runner_pid "$pid" "$identity"
922
+ stop_state=$?
923
+ case "$stop_state" in
924
+ 0|1)
925
+ if fm_procevent_claim_release_locked "$id" "$owner" "$pid" "$token" 2>/dev/null; then
926
+ rm -f -- "$(staging_file "$id" "$token")"
927
+ rm -f -- "$(runner_file "$id")"
928
+ stopped=$((stopped + 1))
929
+ else
930
+ uncertain=$((uncertain + 1))
931
+ fi
932
+ ;;
933
+ *) uncertain=$((uncertain + 1)) ;;
934
+ esac
935
+ fm_procevent_source_lock_release "$id"
936
+ done
937
+
938
+ if [ -d "$REG" ]; then
939
+ for rec in "$REG"/*.source; do
940
+ [ -e "$rec" ] || continue
941
+ id=${rec##*/}; id=${id%.source}
942
+ fm_procevent_source_id_valid "$id" || continue
943
+ fm_procevent_source_lock_acquire "$id" || continue
944
+ if [ -f "$(source_file "$id")" ] && [ ! -L "$(source_file "$id")" ]; then
945
+ fm_procevent_claim_state_locked "$id"
946
+ claim_state=$?
947
+ if [ "$claim_state" -eq 1 ]; then
948
+ if ! cleanup_extension_registration_invocations_locked "$id"; then
949
+ uncertain=$((uncertain + 1))
950
+ fm_procevent_source_lock_release "$id"
951
+ continue
952
+ fi
953
+ fm_procevent_source_lock_release "$id"
954
+ detach_runner "$id"
955
+ started=$((started + 1))
956
+ continue
957
+ elif [ "$claim_state" -eq 4 ]; then
958
+ owner=$FM_PROCEVENT_CLAIM_HOME
959
+ pid=$FM_PROCEVENT_CLAIM_PID
960
+ token=$FM_PROCEVENT_CLAIM_TOKEN
961
+ if [ "$owner" = "$FM_HOME" ] \
962
+ && rm -f -- "$(source_file "$id")" \
963
+ && [ ! -e "$(source_file "$id")" ] \
964
+ && [ ! -L "$(source_file "$id")" ] \
965
+ && fm_procevent_claim_release_locked "$id" "$owner" "$pid" "$token" 2>/dev/null; then
966
+ stopped=$((stopped + 1))
967
+ else
968
+ uncertain=$((uncertain + 1))
969
+ fi
970
+ elif [ "$claim_state" -eq 3 ]; then
971
+ # The leader crashed but its owned group is still consuming the
972
+ # source. Never start a replacement alongside it: stop that group and
973
+ # release its generation first, and if either cannot be proved, keep
974
+ # the claim and retry on a later cycle rather than adding a second
975
+ # poller. Only the owning home may signal its own group.
976
+ owner=$FM_PROCEVENT_CLAIM_HOME
977
+ pid=$FM_PROCEVENT_CLAIM_PID
978
+ token=$FM_PROCEVENT_CLAIM_TOKEN
979
+ identity=$FM_PROCEVENT_CLAIM_IDENTITY
980
+ stop_state=2
981
+ if [ "$owner" = "$FM_HOME" ]; then
982
+ stop_runner_pid "$pid" "$identity"
983
+ stop_state=$?
984
+ fi
985
+ if [ "$stop_state" -eq 0 ] \
986
+ && cleanup_extension_registration_invocations_locked "$id" \
987
+ && fm_procevent_claim_release_locked "$id" "$owner" "$pid" "$token" 2>/dev/null; then
988
+ rm -f -- "$(staging_file "$id" "$token")"
989
+ rm -f -- "$(runner_file "$id")"
990
+ fm_procevent_source_lock_release "$id"
991
+ detach_runner "$id"
992
+ started=$((started + 1))
993
+ continue
994
+ fi
995
+ uncertain=$((uncertain + 1))
996
+ elif [ "$claim_state" -eq 2 ]; then
997
+ uncertain=$((uncertain + 1))
998
+ fi
999
+ fi
1000
+ fm_procevent_source_lock_release "$id"
1001
+ done
1002
+ fi
1003
+ printf 'reconciled: published=%s started=%s stopped=%s uncertain=%s\n' "$published" "$started" "$stopped" "$uncertain"
1004
+ }
1005
+
1006
+ # Stop a runner and the child it is blocked on. A runner started by reconcile is
1007
+ # its own process group leader, so the group signal is what actually reaches the
1008
+ # blocking child - signalling only the runner would leave that child alive and
1009
+ # reparented, which is exactly how a source that never completes leaks.
1010
+ stop_runner_pid() { # <pid> <identity>
1011
+ local pid=${1-} identity=${2-} state pgid i=0
1012
+ case "$pid" in ''|*[!0-9]*) return 2 ;; esac
1013
+ [ -n "$identity" ] || return 2
1014
+ fm_procevent_pid_state "$pid" "$identity"
1015
+ state=$?
1016
+ case "$state" in
1017
+ 0)
1018
+ # A live identity-matched leader still owns its group, so prove the group
1019
+ # really is the one this pid leads before signalling it.
1020
+ pgid=$(ps -o pgid= -p "$pid" 2>/dev/null | tr -d '[:space:]') || return 2
1021
+ [ "$pgid" = "$pid" ] || return 2
1022
+ ;;
1023
+ 3)
1024
+ # The leader crashed but its owned group is still running. Its pgid cannot
1025
+ # be read from the dead leader, and it does not need to be: only an absent
1026
+ # leader reaches this state, so the group cannot belong to a reused pid.
1027
+ ;;
1028
+ *) return "$state" ;;
1029
+ esac
1030
+ kill -TERM -"$pid" 2>/dev/null || return 2
1031
+ while [ "$i" -lt 20 ]; do
1032
+ kill -0 -"$pid" 2>/dev/null || return 0
1033
+ if kill -0 "$pid" 2>/dev/null; then
1034
+ fm_procevent_pid_state "$pid" "$identity"
1035
+ state=$?
1036
+ [ "$state" -eq 2 ] && return 2
1037
+ fi
1038
+ sleep 0.1
1039
+ i=$((i + 1))
1040
+ done
1041
+ kill -KILL -"$pid" 2>/dev/null || return 2
1042
+ i=0
1043
+ while [ "$i" -lt 20 ]; do
1044
+ kill -0 -"$pid" 2>/dev/null || return 0
1045
+ sleep 0.1
1046
+ i=$((i + 1))
1047
+ done
1048
+ return 2
1049
+ }
1050
+
1051
+ # The owned handling interface: durably and idempotently record that a
1052
+ # captured result has been fully handled, keyed by the exact source id and
1053
+ # sequence generation. Serialized under the same per-source boundary as every
1054
+ # other mutation here, on top of the marker's own atomic O_EXCL create, so a
1055
+ # caller can trust the reported first-time/repeat distinction to authorize a
1056
+ # paired external effect at most once.
1057
+ cmd_classify() {
1058
+ local result=${1-} adapter script owner_state
1059
+ [ "$#" -eq 1 ] || usage
1060
+ adapter=$(fm_procevent_result_adapter "$result" 2>/dev/null) \
1061
+ || die "captured result has no readable adapter identity: $result"
1062
+ fm_procevent_result_extension_load "$result"
1063
+ owner_state=$?
1064
+ case "$owner_state" in
1065
+ 0) extension_result_command "$adapter" result.classify "$result"; return $? ;;
1066
+ 2) die "captured extension result has an unreadable owner identity: $result" ;;
1067
+ esac
1068
+ script=$(adapter_script "$adapter")
1069
+ [ -f "$script" ] && [ ! -L "$script" ] \
1070
+ || die "captured result adapter is unavailable: $adapter"
1071
+ "$script" classify "$result"
1072
+ }
1073
+
1074
+ cmd_handled() {
1075
+ local id=${1-} seq=${2-} status
1076
+ fm_procevent_source_id_valid "$id" || die "source id must be path-safe: $id"
1077
+ case "$seq" in ''|*[!0-9]*) die "sequence must be a nonnegative integer: $seq" ;; esac
1078
+ fm_procevent_source_lock_acquire "$id" || die "cannot lock source: $id"
1079
+ fm_procevent_mark_handled "$STATE" "$id" "$seq"
1080
+ status=$?
1081
+ fm_procevent_source_lock_release "$id"
1082
+ case "$status" in
1083
+ 0) printf 'handled: %s %s\n' "$id" "$seq" ;;
1084
+ 1) printf 'already-handled: %s %s\n' "$id" "$seq" ;;
1085
+ *) die "cannot durably record handling: $id $seq" ;;
1086
+ esac
1087
+ }
1088
+
1089
+ cmd_retire() {
1090
+ local id=${1-} condition=${2-} adapter='' sep='' expected_owner='' owner='' pid='' token='' identity='' stop_state owner_state
1091
+ local extension_binding_digest=''
1092
+ fm_procevent_source_id_valid "$id" || die "source id must be path-safe: $id"
1093
+ case "$condition" in
1094
+ '') [ "$#" -eq 1 ] || usage ;;
1095
+ --if-absent) [ "$#" -eq 2 ] || usage ;;
1096
+ --if-owner)
1097
+ [ "$#" -eq 3 ] || usage
1098
+ expected_owner=${3-}
1099
+ fm_procevent_extension_registration_token_valid "$expected_owner" \
1100
+ || die "extension registration owner token is invalid"
1101
+ ;;
1102
+ --if-matches)
1103
+ adapter=${3-}
1104
+ sep=${4-}
1105
+ shift 4 2>/dev/null || usage
1106
+ fm_procevent_adapter_valid "$adapter" \
1107
+ || die "adapter name must be lowercase alphanumeric or dash: $adapter"
1108
+ [ "$sep" = -- ] && [ "$#" -ge 1 ] || usage
1109
+ ;;
1110
+ *) usage ;;
1111
+ esac
1112
+ fm_procevent_source_lock_acquire "$id" || die "cannot lock source: $id"
1113
+ if [ -e "$(source_file "$id")" ] || [ -L "$(source_file "$id")" ]; then
1114
+ if [ -z "$condition" ]; then
1115
+ fm_procevent_extension_registration_load_locked "$STATE" "$id"
1116
+ owner_state=$?
1117
+ case "$owner_state" in
1118
+ 0)
1119
+ fm_procevent_source_lock_release "$id"
1120
+ die "extension registration requires its exact --if-owner token: $id"
1121
+ ;;
1122
+ 2)
1123
+ fm_procevent_source_lock_release "$id"
1124
+ die "cannot safely read extension registration ownership: $id"
1125
+ ;;
1126
+ esac
1127
+ fi
1128
+ case "$condition" in
1129
+ --if-absent)
1130
+ fm_procevent_source_lock_release "$id"
1131
+ die "source registration does not match the expected owner: $id"
1132
+ ;;
1133
+ --if-matches)
1134
+ if ! fm_procevent_registration_matches_locked "$STATE" "$adapter" "$id" "$@"; then
1135
+ fm_procevent_source_lock_release "$id"
1136
+ die "source registration does not match the expected owner: $id"
1137
+ fi
1138
+ ;;
1139
+ --if-owner)
1140
+ fm_procevent_extension_registration_load_locked "$STATE" "$id"
1141
+ owner_state=$?
1142
+ if [ "$owner_state" -ne 0 ] \
1143
+ || [ "$FM_PROCEVENT_EXTENSION_REGISTRATION_TOKEN" != "$expected_owner" ]; then
1144
+ fm_procevent_source_lock_release "$id"
1145
+ die "source registration does not match the expected owner: $id"
1146
+ fi
1147
+ extension_binding_digest=$FM_PROCEVENT_EXTENSION_BINDING_DIGEST
1148
+ ;;
1149
+ esac
1150
+ elif [ "$condition" = --if-owner ] \
1151
+ && { [ -e "$(fm_procevent_claim_path "$id")" ] || [ -L "$(fm_procevent_claim_path "$id")" ]; }; then
1152
+ fm_procevent_source_lock_release "$id"
1153
+ die "source owner cannot be proved after its registration disappeared: $id"
1154
+ fi
1155
+ if [ -e "$(fm_procevent_claim_path "$id")" ]; then
1156
+ if ! fm_procevent_claim_load_locked "$id" 2>/dev/null; then
1157
+ fm_procevent_source_lock_release "$id"
1158
+ die "cannot safely read source ownership: $id"
1159
+ fi
1160
+ if [ "$FM_PROCEVENT_CLAIM_HOME" = "$FM_HOME" ]; then
1161
+ owner=$FM_PROCEVENT_CLAIM_HOME
1162
+ pid=$FM_PROCEVENT_CLAIM_PID
1163
+ token=$FM_PROCEVENT_CLAIM_TOKEN
1164
+ identity=$FM_PROCEVENT_CLAIM_IDENTITY
1165
+ stop_runner_pid "$pid" "$identity"
1166
+ stop_state=$?
1167
+ if [ "$stop_state" -eq 2 ]; then
1168
+ fm_procevent_source_lock_release "$id"
1169
+ die "cannot confirm runner identity; source remains registered: $id"
1170
+ fi
1171
+ if [ -n "$extension_binding_digest" ] \
1172
+ && ! cleanup_extension_binding_invocations "$extension_binding_digest"; then
1173
+ fm_procevent_source_lock_release "$id"
1174
+ die "cannot prove external adapter cleanup; source remains registered: $id"
1175
+ fi
1176
+ if ! fm_procevent_claim_release_locked "$id" "$owner" "$pid" "$token"; then
1177
+ fm_procevent_source_lock_release "$id"
1178
+ die "cannot release source ownership: $id"
1179
+ fi
1180
+ rm -f -- "$(staging_file "$id" "$token")"
1181
+ fi
1182
+ elif [ -n "$extension_binding_digest" ] \
1183
+ && ! cleanup_extension_binding_invocations "$extension_binding_digest"; then
1184
+ fm_procevent_source_lock_release "$id"
1185
+ die "cannot prove external adapter cleanup; source remains registered: $id"
1186
+ fi
1187
+ rm -f -- "$(source_file "$id")"
1188
+ rm -f -- "$(runner_file "$id")"
1189
+ fm_procevent_source_lock_release "$id"
1190
+ # A retired source produces no further answer, so drop any decision binding it
1191
+ # carried. Generic and idempotent: the binding owner is asked to forget this
1192
+ # source id, and an unbound source is unaffected.
1193
+ "$SCRIPT_DIR/fm-captain-hold.sh" unbind "$id" >/dev/null 2>&1 || true
1194
+ printf 'retired: %s\n' "$id"
1195
+ }
1196
+
1197
+ sweep_add_id() {
1198
+ local id=$1
1199
+ case "$SWEEP_IDS" in
1200
+ *$'\n'"$id"$'\n'*) ;;
1201
+ *) SWEEP_IDS+="$id"$'\n' ;;
1202
+ esac
1203
+ }
1204
+
1205
+ sweep_relevant_state() {
1206
+ local path owner
1207
+ for path in "$STATE/extension-invocations"/*.owner.json; do
1208
+ [ -e "$path" ] && return 0
1209
+ done
1210
+ for path in "$REG"/*.source "$REG"/*.runner; do
1211
+ if [ -e "$path" ] || [ -L "$path" ]; then
1212
+ return 0
1213
+ fi
1214
+ done
1215
+ for path in "$(fm_procevent_claim_root)"/*.claim; do
1216
+ [ -f "$path" ] && [ ! -L "$path" ] || continue
1217
+ IFS= read -r owner < "$path" 2>/dev/null || continue
1218
+ [ "$owner" = "$FM_HOME" ] && return 0
1219
+ done
1220
+ return 1
1221
+ }
1222
+
1223
+ sweep_source_preflight() {
1224
+ local id=$1 state
1225
+ fm_procevent_source_lock_acquire "$id" || return 1
1226
+ if [ -e "$(fm_procevent_claim_path "$id")" ] || [ -L "$(fm_procevent_claim_path "$id")" ]; then
1227
+ if ! fm_procevent_claim_load_locked "$id" 2>/dev/null; then
1228
+ fm_procevent_source_lock_release "$id"
1229
+ return 1
1230
+ fi
1231
+ if [ "$FM_PROCEVENT_CLAIM_HOME" = "$FM_HOME" ]; then
1232
+ fm_procevent_pid_state "$FM_PROCEVENT_CLAIM_PID" "$FM_PROCEVENT_CLAIM_IDENTITY"
1233
+ state=$?
1234
+ if [ "$state" -eq 2 ]; then
1235
+ fm_procevent_source_lock_release "$id"
1236
+ return 1
1237
+ fi
1238
+ fi
1239
+ fi
1240
+ fm_procevent_source_lock_release "$id"
1241
+ }
1242
+
1243
+ sweep_retire_source() { # <source-id>
1244
+ local id=$1 owner_state expected_owner=''
1245
+ if [ -e "$(source_file "$id")" ] || [ -L "$(source_file "$id")" ]; then
1246
+ fm_procevent_source_lock_acquire "$id" || return 1
1247
+ fm_procevent_extension_registration_load_locked "$STATE" "$id"
1248
+ owner_state=$?
1249
+ case "$owner_state" in
1250
+ 0) expected_owner=$FM_PROCEVENT_EXTENSION_REGISTRATION_TOKEN ;;
1251
+ 1) ;;
1252
+ *) fm_procevent_source_lock_release "$id"; return 1 ;;
1253
+ esac
1254
+ fm_procevent_source_lock_release "$id"
1255
+ fi
1256
+ if [ -n "$expected_owner" ]; then
1257
+ FM_HOME="$FM_HOME" FM_STATE_OVERRIDE="$STATE" \
1258
+ "$SCRIPT_DIR/fm-procevent.sh" retire "$id" --if-owner "$expected_owner"
1259
+ else
1260
+ FM_HOME="$FM_HOME" FM_STATE_OVERRIDE="$STATE" \
1261
+ "$SCRIPT_DIR/fm-procevent.sh" retire "$id"
1262
+ fi
1263
+ }
1264
+
1265
+ cmd_sweep_home() {
1266
+ local preflight_only=${1-} path id owner attempted=0 failed=0
1267
+ [ -z "$preflight_only" ] || [ "$preflight_only" = --preflight ] || usage
1268
+ SWEEP_IDS=$'\n'
1269
+ for path in "$REG"/*.source; do
1270
+ if [ -e "$path" ] || [ -L "$path" ]; then
1271
+ id=${path##*/}; id=${id%.source}
1272
+ if fm_procevent_source_id_valid "$id"; then
1273
+ sweep_add_id "$id"
1274
+ else
1275
+ failed=$((failed + 1))
1276
+ fi
1277
+ fi
1278
+ done
1279
+ for path in "$(fm_procevent_claim_root)"/*.claim; do
1280
+ [ -f "$path" ] && [ ! -L "$path" ] || continue
1281
+ IFS= read -r owner < "$path" 2>/dev/null || continue
1282
+ [ "$owner" = "$FM_HOME" ] || continue
1283
+ id=${path##*/}; id=${id%.claim}
1284
+ if fm_procevent_source_id_valid "$id"; then
1285
+ sweep_add_id "$id"
1286
+ else
1287
+ failed=$((failed + 1))
1288
+ fi
1289
+ done
1290
+ for path in "$REG"/*.runner; do
1291
+ if [ -e "$path" ] || [ -L "$path" ]; then
1292
+ id=${path##*/}; id=${id%.runner}
1293
+ if ! fm_procevent_source_id_valid "$id"; then
1294
+ failed=$((failed + 1))
1295
+ else
1296
+ case "$SWEEP_IDS" in
1297
+ *$'\n'"$id"$'\n'*) ;;
1298
+ *) failed=$((failed + 1)) ;;
1299
+ esac
1300
+ fi
1301
+ fi
1302
+ done
1303
+ while IFS= read -r id; do
1304
+ [ -n "$id" ] || continue
1305
+ sweep_source_preflight "$id" || failed=$((failed + 1))
1306
+ done <<< "$SWEEP_IDS"
1307
+ if [ "$failed" -ne 0 ]; then
1308
+ printf 'error: process-event home sweep preflight failed: attempted=0 failed=%s\n' "$failed" >&2
1309
+ return 1
1310
+ fi
1311
+ if [ "$preflight_only" = --preflight ]; then
1312
+ printf 'sweep preflight: ready\n'
1313
+ return 0
1314
+ fi
1315
+ while IFS= read -r id; do
1316
+ [ -n "$id" ] || continue
1317
+ attempted=$((attempted + 1))
1318
+ if ! sweep_retire_source "$id"; then
1319
+ failed=$((failed + 1))
1320
+ fi
1321
+ done <<< "$SWEEP_IDS"
1322
+ if ! run_extension_invocation_cleanup; then
1323
+ failed=$((failed + 1))
1324
+ fi
1325
+ if [ "$failed" -ne 0 ] || sweep_relevant_state; then
1326
+ printf 'error: process-event home sweep incomplete: attempted=%s failed=%s\n' "$attempted" "$failed" >&2
1327
+ return 1
1328
+ fi
1329
+ printf 'swept: attempted=%s\n' "$attempted"
1330
+ }
1331
+
1332
+ cmd_list() {
1333
+ local rec id adapter owner pending
1334
+ if ! fm_procevent_any_registered "$STATE"; then
1335
+ printf 'no sources registered\n'
1336
+ return 0
1337
+ fi
1338
+ printf '%-28s %-12s %-10s %s\n' SOURCE ADAPTER OWNER PENDING
1339
+ for rec in "$REG"/*.source; do
1340
+ [ -e "$rec" ] || continue
1341
+ id=${rec##*/}; id=${id%.source}
1342
+ adapter=$(read_adapter "$id" 2>/dev/null || echo '?')
1343
+ fm_procevent_source_lock_acquire "$id" || continue
1344
+ fm_procevent_claim_state_locked "$id"
1345
+ case "$?" in 0) owner=live ;; 1) owner=none ;; 3) owner=orphaned ;; *) owner=uncertain ;; esac
1346
+ fm_procevent_source_lock_release "$id"
1347
+ pending=$(fm_procevent_pending "$STATE" | grep -c "/$id\." || true)
1348
+ printf '%-28s %-12s %-10s %s\n' "$id" "$adapter" "$owner" "$pending"
1349
+ done
1350
+ }
1351
+
1352
+ cmd_binding_retirement_preflight() {
1353
+ local digest=${1-} rec id owner_state result
1354
+ if [ "$#" -ne 1 ] || ! fm_procevent_digest_valid "$digest"; then
1355
+ die "binding-retirement-preflight requires one binding digest"
1356
+ fi
1357
+ for rec in "$REG"/*.source; do
1358
+ [ -e "$rec" ] || continue
1359
+ [ -f "$rec" ] && [ ! -L "$rec" ] || die "binding retirement found unsafe registration state"
1360
+ id=${rec##*/}; id=${id%.source}
1361
+ fm_procevent_source_id_valid "$id" || die "binding retirement found malformed registration state"
1362
+ fm_lock_try_acquire "$(fm_procevent_source_lock_path "$id")" \
1363
+ || die "binding still owns process-event registration: $id"
1364
+ fm_procevent_extension_registration_load_locked "$STATE" "$id"
1365
+ owner_state=$?
1366
+ fm_lock_release "$(fm_procevent_source_lock_path "$id")"
1367
+ case "$owner_state" in
1368
+ 0) [ "$FM_PROCEVENT_EXTENSION_BINDING_DIGEST" != "$digest" ] \
1369
+ || die "binding still owns process-event registration: $id" ;;
1370
+ 1) ;;
1371
+ *) die "binding retirement found malformed extension registration: $id" ;;
1372
+ esac
1373
+ done
1374
+ for result in "$(fm_procevent_inbox_dir "$STATE")"/*.result; do
1375
+ [ -e "$result" ] || continue
1376
+ if [ -e "${result%.result}.handled" ] || [ -L "${result%.result}.handled" ]; then
1377
+ [ -f "${result%.result}.handled" ] && [ ! -L "${result%.result}.handled" ] \
1378
+ || die "binding retirement found unsafe handled-result state: ${result##*/}"
1379
+ continue
1380
+ fi
1381
+ fm_procevent_result_extension_load "$result"
1382
+ owner_state=$?
1383
+ case "$owner_state" in
1384
+ 0) [ "$FM_PROCEVENT_RESULT_EXTENSION_BINDING_DIGEST" != "$digest" ] \
1385
+ || die "binding still owns unhandled process-event result: ${result##*/}" ;;
1386
+ 1) ;;
1387
+ *) die "binding retirement found malformed extension result: ${result##*/}" ;;
1388
+ esac
1389
+ done
1390
+ printf 'binding retirement preflight: ready\n'
1391
+ }
1392
+
1393
+ cmd_extension_retirement() {
1394
+ local mode=${1-} owner
1395
+ [ "$#" -ge 1 ] || die "extension-retirement requires a retirement mode"
1396
+ shift
1397
+ case "$mode" in binding|transfer) ;; *) die "unsupported extension retirement mode: $mode" ;; esac
1398
+ extension_lifecycle_lock_acquire || die "cannot lock the extension lifecycle"
1399
+ owner=${FM_LOCK_OWNER_DIR:-}
1400
+ [ -n "$owner" ] || die "extension lifecycle lock has no owner identity"
1401
+ export FM_EXTENSION_RETIREMENT_MODE="$mode"
1402
+ export FM_EXTENSION_LIFECYCLE_LOCK="$EXTENSION_LIFECYCLE_LOCK"
1403
+ export FM_EXTENSION_LIFECYCLE_OWNER="$owner"
1404
+ exec "$EXTENSION_HOST" "$@"
1405
+ }
1406
+
1407
+ cmd_extension_bind() {
1408
+ local binding_command=${1-} owner
1409
+ case "$binding_command" in bind|receive-transfer-bind) ;; *) die "unsupported extension binding command: $binding_command" ;; esac
1410
+ extension_lifecycle_lock_acquire || die "cannot lock the extension lifecycle"
1411
+ owner=${FM_LOCK_OWNER_DIR:-}
1412
+ [ -n "$owner" ] || die "extension lifecycle lock has no owner identity"
1413
+ export FM_EXTENSION_RETIREMENT_MODE=bind
1414
+ export FM_EXTENSION_LIFECYCLE_LOCK="$EXTENSION_LIFECYCLE_LOCK"
1415
+ export FM_EXTENSION_LIFECYCLE_OWNER="$owner"
1416
+ exec "$EXTENSION_HOST" "$@"
1417
+ }
1418
+
1419
+ cmd_extension_process_event() {
1420
+ local owner arg
1421
+ [ "$#" -ge 2 ] || die "extension-process-event requires process-event arguments"
1422
+ for arg in "$@"; do
1423
+ [ "$arg" != --capture-reservation ] || die "capture reservation is internal"
1424
+ done
1425
+ extension_lifecycle_lock_acquire || die "cannot lock the extension lifecycle"
1426
+ owner=${FM_LOCK_OWNER_DIR:-}
1427
+ [ -n "$owner" ] || die "extension lifecycle lock has no owner identity"
1428
+ export FM_EXTENSION_RETIREMENT_MODE=process-event
1429
+ export FM_EXTENSION_LIFECYCLE_LOCK="$EXTENSION_LIFECYCLE_LOCK"
1430
+ export FM_EXTENSION_LIFECYCLE_OWNER="$owner"
1431
+ # These descriptors are reserved for the direct, internal capture handoff.
1432
+ # The public lifecycle path must not let unrelated descriptors acquired while
1433
+ # obtaining its lock look like a malformed handoff to the host.
1434
+ { exec 6<&-; } 2>/dev/null || true
1435
+ { exec 7<&-; } 2>/dev/null || true
1436
+ { exec 8<&-; } 2>/dev/null || true
1437
+ { exec 9<&-; } 2>/dev/null || true
1438
+ exec "$EXTENSION_HOST" process-event "$@"
1439
+ }
1440
+
1441
+ unset FM_PROCEVENT_CAPTURE_PINNED_INBOX FM_PROCEVENT_CAPTURE_ABSOLUTE_INBOX \
1442
+ FM_PROCEVENT_CAPTURE_RESERVATION_TERMINAL \
1443
+ FM_PROCEVENT_CAPTURE_RESERVATION_SILENT
1444
+ { exec 7<&-; } 2>/dev/null || true
1445
+ { exec 6<&-; } 2>/dev/null || true
1446
+ { exec 8<&-; } 2>/dev/null || true
1447
+ { exec 9<&-; } 2>/dev/null || true
1448
+
1449
+ case "${1-}" in
1450
+ register) shift; cmd_register "$@" ;;
1451
+ register-extension) shift; cmd_register_extension "$@" ;;
1452
+ start) shift; cmd_start_public "$@" ;;
1453
+ _start) shift; cmd_start "$@" ;;
1454
+ reconcile) shift; cmd_reconcile "$@" ;;
1455
+ classify) shift; cmd_classify "$@" ;;
1456
+ handled) shift; cmd_handled "$@" ;;
1457
+ retire) shift; cmd_retire "$@" ;;
1458
+ sweep-home) shift; cmd_sweep_home "$@" ;;
1459
+ binding-retirement-preflight) shift; cmd_binding_retirement_preflight "$@" ;;
1460
+ extension-retirement) shift; cmd_extension_retirement "$@" ;;
1461
+ extension-bind) shift; cmd_extension_bind "$@" ;;
1462
+ extension-process-event) shift; cmd_extension_process_event "$@" ;;
1463
+ list) shift; cmd_list "$@" ;;
1464
+ ''|-h|--help|help) usage ;;
1465
+ *) die "unknown command: $1" ;;
1466
+ esac