@zerwiz/ymir 0.1.4 → 0.1.7

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 (865) 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/README.md +74 -0
  27. package/.agents/backend/backends/cmux.sh +649 -0
  28. package/.agents/backend/backends/herdr-eventwait.py +157 -0
  29. package/.agents/backend/backends/herdr-workspace-move.py +114 -0
  30. package/.agents/backend/backends/herdr.sh +3386 -0
  31. package/.agents/backend/backends/orca.sh +290 -0
  32. package/.agents/backend/backends/tmux.sh +357 -0
  33. package/.agents/backend/backends/zellij.sh +700 -0
  34. package/.agents/backend/fm-afk-launch.sh +654 -0
  35. package/.agents/backend/fm-afk-return.sh +241 -0
  36. package/.agents/backend/fm-afk-start.sh +172 -0
  37. package/.agents/backend/fm-arm-command-policy.mjs +960 -0
  38. package/.agents/backend/fm-arm-pretool-check.sh +200 -0
  39. package/.agents/backend/fm-backend-hometag-lib.sh +52 -0
  40. package/.agents/backend/fm-backend.sh +981 -0
  41. package/.agents/backend/fm-backlog-handoff.sh +909 -0
  42. package/.agents/backend/fm-backlog-receive.sh +187 -0
  43. package/.agents/backend/fm-backlog-transition-lib.sh +779 -0
  44. package/.agents/backend/fm-bearings-board.sh +204 -0
  45. package/.agents/backend/fm-bearings-snapshot.sh +546 -0
  46. package/.agents/backend/fm-bootstrap.sh +1568 -0
  47. package/.agents/backend/fm-branch-outcome.sh +203 -0
  48. package/.agents/backend/fm-branch-prompt.sh +101 -0
  49. package/.agents/backend/fm-brief.sh +452 -0
  50. package/.agents/backend/fm-busy-event.sh +234 -0
  51. package/.agents/backend/fm-busy-lib.sh +986 -0
  52. package/.agents/backend/fm-captain-hold.sh +1001 -0
  53. package/.agents/backend/fm-cd-command-policy.mjs +153 -0
  54. package/.agents/backend/fm-cd-pretool-check.sh +190 -0
  55. package/.agents/backend/fm-check-lib.sh +74 -0
  56. package/.agents/backend/fm-check-register.sh +42 -0
  57. package/.agents/backend/fm-check-unregister.sh +52 -0
  58. package/.agents/backend/fm-classify-lib.sh +1770 -0
  59. package/.agents/backend/fm-claude-stop-autoarm.sh +334 -0
  60. package/.agents/backend/fm-composer-lib.sh +1416 -0
  61. package/.agents/backend/fm-config-inherit-lib.sh +1202 -0
  62. package/.agents/backend/fm-config-push.sh +254 -0
  63. package/.agents/backend/fm-control-lib.sh +251 -0
  64. package/.agents/backend/fm-control.sh +878 -0
  65. package/.agents/backend/fm-crew-state.sh +625 -0
  66. package/.agents/backend/fm-cursor-lib.sh +243 -0
  67. package/.agents/backend/fm-decision-hold.sh +232 -0
  68. package/.agents/backend/fm-doc-audience-check.sh +269 -0
  69. package/.agents/backend/fm-dod-lib.sh +67 -0
  70. package/.agents/backend/fm-ensure-agents-md.sh +253 -0
  71. package/.agents/backend/fm-extension-launch-barrier.mjs +129 -0
  72. package/.agents/backend/fm-extension.mjs +2577 -0
  73. package/.agents/backend/fm-extension.sh +16 -0
  74. package/.agents/backend/fm-ff-lib.sh +420 -0
  75. package/.agents/backend/fm-fleet-snapshot.sh +1463 -0
  76. package/.agents/backend/fm-fleet-sync.sh +458 -0
  77. package/.agents/backend/fm-fleet-view.sh +96 -0
  78. package/.agents/backend/fm-gate-refuse-lib.sh +102 -0
  79. package/.agents/backend/fm-guard.sh +242 -0
  80. package/.agents/backend/fm-harness.sh +195 -0
  81. package/.agents/backend/fm-herdr-ci-cleanup.sh +115 -0
  82. package/.agents/backend/fm-herdr-lab.sh +346 -0
  83. package/.agents/backend/fm-herdr-session-cleanup.sh +337 -0
  84. package/.agents/backend/fm-home-seed.sh +984 -0
  85. package/.agents/backend/fm-home-summary-refresh.sh +255 -0
  86. package/.agents/backend/fm-hook-host-lib.sh +36 -0
  87. package/.agents/backend/fm-inactive-reconcile.sh +531 -0
  88. package/.agents/backend/fm-inbox.sh +399 -0
  89. package/.agents/backend/fm-install-actionlint.sh +84 -0
  90. package/.agents/backend/fm-install-herdr.sh +96 -0
  91. package/.agents/backend/fm-install-shellcheck.sh +84 -0
  92. package/.agents/backend/fm-install-treehouse.sh +95 -0
  93. package/.agents/backend/fm-kimi-turnend-hook.sh +276 -0
  94. package/.agents/backend/fm-lease-lib.sh +218 -0
  95. package/.agents/backend/fm-lease.sh +189 -0
  96. package/.agents/backend/fm-line-cap-lib.sh +51 -0
  97. package/.agents/backend/fm-lint-workflows.sh +137 -0
  98. package/.agents/backend/fm-lint.sh +591 -0
  99. package/.agents/backend/fm-lock-lib.sh +104 -0
  100. package/.agents/backend/fm-lock.sh +107 -0
  101. package/.agents/backend/fm-marker-lib.sh +12 -0
  102. package/.agents/backend/fm-merge-local.sh +74 -0
  103. package/.agents/backend/fm-merge-outcome-lib.sh +138 -0
  104. package/.agents/backend/fm-nm-run-lib.sh +124 -0
  105. package/.agents/backend/fm-on.sh +125 -0
  106. package/.agents/backend/fm-operational-input.sh +253 -0
  107. package/.agents/backend/fm-peek.sh +46 -0
  108. package/.agents/backend/fm-pending-reply-lib.sh +1409 -0
  109. package/.agents/backend/fm-pr-check.sh +135 -0
  110. package/.agents/backend/fm-pr-lib.sh +1016 -0
  111. package/.agents/backend/fm-pr-merge.sh +707 -0
  112. package/.agents/backend/fm-pr-poll.sh +110 -0
  113. package/.agents/backend/fm-primary-scope-lib.sh +33 -0
  114. package/.agents/backend/fm-procevent-extension-capture.pl +259 -0
  115. package/.agents/backend/fm-procevent-lavish.sh +628 -0
  116. package/.agents/backend/fm-procevent-lib.sh +892 -0
  117. package/.agents/backend/fm-procevent-quota.sh +290 -0
  118. package/.agents/backend/fm-procevent-remote-reply.sh +577 -0
  119. package/.agents/backend/fm-procevent-when.sh +504 -0
  120. package/.agents/backend/fm-procevent.sh +1466 -0
  121. package/.agents/backend/fm-project-mode.sh +92 -0
  122. package/.agents/backend/fm-project-origin-lib.sh +180 -0
  123. package/.agents/backend/fm-promote.sh +282 -0
  124. package/.agents/backend/fm-public-followup-emit.sh +260 -0
  125. package/.agents/backend/fm-public-followup-lib.sh +398 -0
  126. package/.agents/backend/fm-public-followup.sh +1330 -0
  127. package/.agents/backend/fm-push-transition-lib.sh +168 -0
  128. package/.agents/backend/fm-quota-axi-lib.sh +93 -0
  129. package/.agents/backend/fm-quota-choose.sh +384 -0
  130. package/.agents/backend/fm-remote-delta-read.sh +189 -0
  131. package/.agents/backend/fm-remote-doctor.sh +800 -0
  132. package/.agents/backend/fm-remote-entrypoint.sh +193 -0
  133. package/.agents/backend/fm-remote-file.sh +234 -0
  134. package/.agents/backend/fm-remote-home-provision.sh +259 -0
  135. package/.agents/backend/fm-remote-home-seed.sh +259 -0
  136. package/.agents/backend/fm-remote-inherit-push.sh +90 -0
  137. package/.agents/backend/fm-remote-inherit.sh +179 -0
  138. package/.agents/backend/fm-remote-job-lib.sh +1241 -0
  139. package/.agents/backend/fm-remote-job-reap-orphans.sh +137 -0
  140. package/.agents/backend/fm-remote-job-worker.sh +1124 -0
  141. package/.agents/backend/fm-remote-readiness-lib.sh +44 -0
  142. package/.agents/backend/fm-remote-secondmate-control.sh +340 -0
  143. package/.agents/backend/fm-review-diff.sh +158 -0
  144. package/.agents/backend/fm-secondmate-charter-lib.sh +44 -0
  145. package/.agents/backend/fm-secondmate-nudge-lib.sh +68 -0
  146. package/.agents/backend/fm-secondmate-parent-lib.sh +70 -0
  147. package/.agents/backend/fm-secondmate-reconcile.sh +371 -0
  148. package/.agents/backend/fm-secondmate-registry-lib.sh +311 -0
  149. package/.agents/backend/fm-secondmate-report.sh +88 -0
  150. package/.agents/backend/fm-send.sh +1059 -0
  151. package/.agents/backend/fm-session-lock-lib.sh +176 -0
  152. package/.agents/backend/fm-session-start.sh +970 -0
  153. package/.agents/backend/fm-sessionstart-cursor.sh +40 -0
  154. package/.agents/backend/fm-sessionstart-nudge.sh +45 -0
  155. package/.agents/backend/fm-sessionstart-run.sh +145 -0
  156. package/.agents/backend/fm-spawn.sh +3153 -0
  157. package/.agents/backend/fm-startup-memory-budget-lib.sh +224 -0
  158. package/.agents/backend/fm-startup-memory-budget.sh +94 -0
  159. package/.agents/backend/fm-startup-network.sh +642 -0
  160. package/.agents/backend/fm-stow-cascade.sh +251 -0
  161. package/.agents/backend/fm-subagent-pretool-check.sh +207 -0
  162. package/.agents/backend/fm-supervise-daemon.sh +1732 -0
  163. package/.agents/backend/fm-supervision-instructions.sh +215 -0
  164. package/.agents/backend/fm-supervision-lib.sh +90 -0
  165. package/.agents/backend/fm-supervisor-target-lib.sh +78 -0
  166. package/.agents/backend/fm-tangle-lib.sh +53 -0
  167. package/.agents/backend/fm-task-inbox-lib.sh +402 -0
  168. package/.agents/backend/fm-tasks-axi-lib.sh +123 -0
  169. package/.agents/backend/fm-teardown.sh +2922 -0
  170. package/.agents/backend/fm-test-isolation-proof.sh +588 -0
  171. package/.agents/backend/fm-test-run.sh +2191 -0
  172. package/.agents/backend/fm-timeout-lib.sh +141 -0
  173. package/.agents/backend/fm-timing-lib.sh +179 -0
  174. package/.agents/backend/fm-tmux-lib.sh +291 -0
  175. package/.agents/backend/fm-tool-update-check.sh +898 -0
  176. package/.agents/backend/fm-trace-context-lib.sh +227 -0
  177. package/.agents/backend/fm-transition-lib.sh +103 -0
  178. package/.agents/backend/fm-turnend-guard-cursor.sh +391 -0
  179. package/.agents/backend/fm-turnend-guard-grok.sh +90 -0
  180. package/.agents/backend/fm-turnend-guard.sh +432 -0
  181. package/.agents/backend/fm-update.sh +110 -0
  182. package/.agents/backend/fm-vendor-auth-probe.sh +190 -0
  183. package/.agents/backend/fm-voice-client.py +1373 -0
  184. package/.agents/backend/fm-voice-relay.py +1256 -0
  185. package/.agents/backend/fm-wake-drain.sh +592 -0
  186. package/.agents/backend/fm-wake-grant.sh +127 -0
  187. package/.agents/backend/fm-wake-lib.sh +1849 -0
  188. package/.agents/backend/fm-watch-arm.sh +602 -0
  189. package/.agents/backend/fm-watch-checkpoint.sh +109 -0
  190. package/.agents/backend/fm-watch.sh +1962 -0
  191. package/.agents/backend/fm-x-dismiss.sh +116 -0
  192. package/.agents/backend/fm-x-followup.sh +286 -0
  193. package/.agents/backend/fm-x-lib.sh +1000 -0
  194. package/.agents/backend/fm-x-link.sh +234 -0
  195. package/.agents/backend/fm-x-poll.sh +185 -0
  196. package/.agents/backend/fm-x-reply.sh +385 -0
  197. package/.agents/backend/fm_voice_frame.py +166 -0
  198. package/.agents/backend/fm_voice_records.py +574 -0
  199. package/.agents/backend/model-bridge.py +360 -0
  200. package/.agents/backend/opencode-go-bridge.py +30 -0
  201. package/.agents/bus/README.md +12 -0
  202. package/.agents/bus/messages.json +1 -0
  203. package/.agents/config/agents.machine.example.yaml +16 -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 +11 -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/filebrowser/.filebrowser.json +6 -0
  211. package/.agents/filebrowser/README.md +11 -0
  212. package/.agents/gateway/README.md +12 -0
  213. package/.agents/github/README.md +15 -0
  214. package/.agents/github/webhooks/.gitkeep +0 -0
  215. package/.agents/github/workflows/.gitkeep +0 -0
  216. package/.agents/harness/opencode/plugins/lib/rodd-operational-input.js +37 -0
  217. package/.agents/harness/opencode/plugins/package.json +4 -0
  218. package/.agents/harness/opencode/plugins/saga-sessionstart.js +77 -0
  219. package/.agents/harness/opencode/plugins/syn-cd-check.js +61 -0
  220. package/.agents/harness/opencode/plugins/syn-pretool-check.js +68 -0
  221. package/.agents/harness/opencode/plugins/syn-turnend-guard.js +105 -0
  222. package/.agents/harness/opencode/plugins/syn-watch-arm.js +521 -0
  223. package/.agents/memory/README.md +46 -0
  224. package/.agents/memory/runes_audit.md +5 -0
  225. package/.agents/migrations/0001-hodd-layout.sh +38 -0
  226. package/.agents/migrations/0002-a2a-mcp.sh +27 -0
  227. package/.agents/migrations/0003-private-data-separation.sh +121 -0
  228. package/.agents/migrations/0004-hoard-and-realms.sh +104 -0
  229. package/.agents/migrations/0005-roots-out-of-tree.sh +158 -0
  230. package/.agents/sandbox/Dockerfile.utgard +19 -0
  231. package/.agents/sandbox/README.md +14 -0
  232. package/.agents/sandbox/utgard.config.json +14 -0
  233. package/.agents/skills/README.md +59 -0
  234. package/.agents/skills/bragi-marketing/README.md +3 -0
  235. package/.agents/skills/bragi-marketing/SKILL.md +82 -0
  236. package/.agents/skills/eindri-homes/README.md +3 -0
  237. package/.agents/skills/eindri-homes/SKILL.md +291 -0
  238. package/.agents/skills/eindri-homes/assets/control-plane.md +122 -0
  239. package/.agents/skills/eindri-homes/assets/dispatch.md +80 -0
  240. package/.agents/skills/frigg-consent/README.md +3 -0
  241. package/.agents/skills/frigg-consent/SKILL.md +56 -0
  242. package/.agents/skills/galdr-ymirsystem/README.md +3 -0
  243. package/.agents/skills/galdr-ymirsystem/SKILL.md +159 -0
  244. package/.agents/skills/galdr-ymirsystem/assets/README.md +74 -0
  245. package/.agents/skills/galdr-ymirsystem/assets/brokk-distro-runtime.md +390 -0
  246. package/.agents/skills/galdr-ymirsystem/assets/build-method.md +113 -0
  247. package/.agents/skills/galdr-ymirsystem/assets/build-tool-categories.md +178 -0
  248. package/.agents/skills/galdr-ymirsystem/assets/eindri-orchestration.md +518 -0
  249. package/.agents/skills/galdr-ymirsystem/assets/eindri-profiles.md +136 -0
  250. package/.agents/skills/galdr-ymirsystem/assets/harness-integration/README.md +736 -0
  251. package/.agents/skills/galdr-ymirsystem/assets/harness-integration/claude-code.md +197 -0
  252. package/.agents/skills/galdr-ymirsystem/assets/harness-integration/codex.md +191 -0
  253. package/.agents/skills/galdr-ymirsystem/assets/harness-integration/cursor.md +193 -0
  254. package/.agents/skills/galdr-ymirsystem/assets/harness-integration/opencode.md +262 -0
  255. package/.agents/skills/galdr-ymirsystem/assets/harness-integration/pi.md +295 -0
  256. package/.agents/skills/galdr-ymirsystem/assets/hlidskjalf-ui.md +539 -0
  257. package/.agents/skills/galdr-ymirsystem/assets/installation.md +763 -0
  258. package/.agents/skills/galdr-ymirsystem/assets/local-models/HOST-DATA.example.md +72 -0
  259. package/.agents/skills/galdr-ymirsystem/assets/local-models.md +274 -0
  260. package/.agents/skills/galdr-ymirsystem/assets/memory-well.md +163 -0
  261. package/.agents/skills/galdr-ymirsystem/assets/nornir-jobs.md +415 -0
  262. package/.agents/skills/galdr-ymirsystem/assets/norse-naming.md +325 -0
  263. package/.agents/skills/galdr-ymirsystem/assets/odrerir-hall.md +133 -0
  264. package/.agents/skills/galdr-ymirsystem/assets/pi-boot/einherjar-spawn.schema.json +81 -0
  265. package/.agents/skills/galdr-ymirsystem/assets/pi-boot/herdr-profile.toml +75 -0
  266. package/.agents/skills/galdr-ymirsystem/assets/pi-boot/pi-profile.yml +101 -0
  267. package/.agents/skills/galdr-ymirsystem/assets/pi-boot/supervision-tree.yml +163 -0
  268. package/.agents/skills/galdr-ymirsystem/assets/pi-boot-guide.md +58 -0
  269. package/.agents/skills/galdr-ymirsystem/assets/porting-upstream-to-norse.md +301 -0
  270. package/.agents/skills/galdr-ymirsystem/assets/principles.md +385 -0
  271. package/.agents/skills/galdr-ymirsystem/assets/reference-adoption.md +99 -0
  272. package/.agents/skills/galdr-ymirsystem/assets/registry.md +379 -0
  273. package/.agents/skills/galdr-ymirsystem/assets/runtime-compliance.md +463 -0
  274. package/.agents/skills/galdr-ymirsystem/assets/runtime-components.md +553 -0
  275. package/.agents/skills/galdr-ymirsystem/assets/smidja.md +180 -0
  276. package/.agents/skills/galdr-ymirsystem/schemas/toon-schemas.md +190 -0
  277. package/.agents/skills/galdr-ymirsystem/scripts/README.md +45 -0
  278. package/.agents/skills/galdr-ymirsystem/scripts/bench-one.sh +248 -0
  279. package/.agents/skills/galdr-ymirsystem/scripts/compliance-check.sh +503 -0
  280. package/.agents/skills/galdr-ymirsystem/scripts/gpu-sample.sh +43 -0
  281. package/.agents/skills/galdr-ymirsystem/scripts/stop-all.sh +34 -0
  282. package/.agents/skills/galdr-ymirsystem/scripts/toon-check.py +188 -0
  283. package/.agents/skills/gjallarhorn-relay/README.md +3 -0
  284. package/.agents/skills/gjallarhorn-relay/SKILL.md +283 -0
  285. package/.agents/skills/groa-update/SKILL.md +90 -0
  286. package/.agents/skills/hamr-adapters/README.md +3 -0
  287. package/.agents/skills/hamr-adapters/SKILL.md +123 -0
  288. package/.agents/skills/hamr-adapters/references/common/control-and-recovery.md +37 -0
  289. package/.agents/skills/hamr-adapters/references/common/dispatch.md +32 -0
  290. package/.agents/skills/hamr-adapters/references/common/model-and-effort.md +42 -0
  291. package/.agents/skills/hamr-adapters/references/common/primary-hooks.md +40 -0
  292. package/.agents/skills/hamr-adapters/references/harness/claude.md +55 -0
  293. package/.agents/skills/hamr-adapters/references/harness/codex.md +43 -0
  294. package/.agents/skills/hamr-adapters/references/harness/cursor.md +75 -0
  295. package/.agents/skills/hamr-adapters/references/harness/grok.md +69 -0
  296. package/.agents/skills/hamr-adapters/references/harness/kimi.md +51 -0
  297. package/.agents/skills/hamr-adapters/references/harness/muse.md +70 -0
  298. package/.agents/skills/hamr-adapters/references/harness/opencode.md +42 -0
  299. package/.agents/skills/hamr-adapters/references/harness/pi.md +56 -0
  300. package/.agents/skills/herdr-panes/README.md +3 -0
  301. package/.agents/skills/herdr-panes/SKILL.md +92 -0
  302. package/.agents/skills/herdr-panes/assets/herdr-backend.md +346 -0
  303. package/.agents/skills/herdr-panes/assets/tmux-backend.md +115 -0
  304. package/.agents/skills/hnoss-design/DESIGN.md +19 -0
  305. package/.agents/skills/hnoss-design/README.md +3 -0
  306. package/.agents/skills/hnoss-design/SKILL.md +83 -0
  307. package/.agents/skills/hvild-afk/README.md +3 -0
  308. package/.agents/skills/hvild-afk/SKILL.md +241 -0
  309. package/.agents/skills/jord-projects/README.md +3 -0
  310. package/.agents/skills/jord-projects/SKILL.md +92 -0
  311. package/.agents/skills/lifecycle/SKILL.md +56 -0
  312. package/.agents/skills/lifecycle/smoke_test.sh +57 -0
  313. package/.agents/skills/lifecycle/start.sh +21 -0
  314. package/.agents/skills/lifecycle/status.sh +35 -0
  315. package/.agents/skills/lifecycle/stop.sh +16 -0
  316. package/.agents/skills/muninn-stow/README.md +3 -0
  317. package/.agents/skills/muninn-stow/SKILL.md +309 -0
  318. package/.agents/skills/no-mistakes/SKILL.md +379 -0
  319. package/.agents/skills/nornir-schedule/README.md +3 -0
  320. package/.agents/skills/nornir-schedule/SKILL.md +25 -0
  321. package/.agents/skills/nornir-schedule/assets/events.md +137 -0
  322. package/.agents/skills/nornir-schedule/assets/quota.md +131 -0
  323. package/.agents/skills/nsr-compliance/README.md +3 -0
  324. package/.agents/skills/nsr-compliance/SKILL.md +29 -0
  325. package/.agents/skills/nsr-compliance/assets/nsr/README.md +40 -0
  326. package/.agents/skills/nsr-compliance/assets/nsr/assets/RULES/README.md +15 -0
  327. package/.agents/skills/nsr-compliance/assets/nsr/assets/RULES/git-workflow.md +18 -0
  328. package/.agents/skills/nsr-compliance/assets/nsr/assets/RULES/security.md +22 -0
  329. package/.agents/skills/nsr-compliance/assets/nsr/assets/RULES/tech-stack.md +8 -0
  330. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/SKILL.md +71 -0
  331. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/config/core_four.yaml +16 -0
  332. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/gates/check_danger.sh +102 -0
  333. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/gates/check_wiring.sh +69 -0
  334. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/gates/validate_code.sh +29 -0
  335. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/gates/verify_docs.py +66 -0
  336. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/generate.sh +109 -0
  337. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/harness/envelopes/result_envelope.json +14 -0
  338. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/harness/envelopes/task_envelope.json +20 -0
  339. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/harness/runner.py +303 -0
  340. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/telemetry/logger.py +112 -0
  341. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/templates/compliance/README.md +18 -0
  342. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/templates/compliance/config/README.md +10 -0
  343. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/templates/compliance/gates/README.md +12 -0
  344. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/templates/compliance/gates/check_env.sh +23 -0
  345. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/templates/compliance/gates/check_paths.sh +20 -0
  346. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/templates/compliance/gates/check_platform.sh +23 -0
  347. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/templates/compliance/harness/README.md +11 -0
  348. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/templates/compliance/harness/envelopes/README.md +10 -0
  349. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/templates/compliance/installer/README.md +10 -0
  350. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/templates/compliance/installer/install.sh +125 -0
  351. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/templates/compliance/telemetry/README.md +11 -0
  352. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/README.md +20 -0
  353. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/features/README.md +36 -0
  354. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/features/SKILL-template.md +14 -0
  355. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/git_ops/README.md +25 -0
  356. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/git_ops/SKILL.md +13 -0
  357. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/git_ops/create_branch.sh +6 -0
  358. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/git_ops/safe_commit.sh +9 -0
  359. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/git_ops/sync_upstream.sh +6 -0
  360. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/lifecycle/README.md +27 -0
  361. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/lifecycle/SKILL.md +15 -0
  362. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/lifecycle/smoke_test.sh +12 -0
  363. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/lifecycle/start.sh +19 -0
  364. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/lifecycle/status.sh +12 -0
  365. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/lifecycle/stop.sh +15 -0
  366. package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/README.md +18 -0
  367. package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/config/README.md +10 -0
  368. package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/config/core_four.yaml +13 -0
  369. package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/gates/README.md +12 -0
  370. package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/gates/check_danger.sh +102 -0
  371. package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/gates/check_env.sh +23 -0
  372. package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/gates/check_paths.sh +20 -0
  373. package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/gates/check_platform.sh +23 -0
  374. package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/gates/check_wiring.sh +69 -0
  375. package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/harness/README.md +11 -0
  376. package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/harness/envelopes/README.md +10 -0
  377. package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/harness/envelopes/result_envelope.json +14 -0
  378. package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/harness/envelopes/task_envelope.json +20 -0
  379. package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/harness/runner.py +303 -0
  380. package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/installer/README.md +10 -0
  381. package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/installer/install.sh +125 -0
  382. package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/telemetry/README.md +11 -0
  383. package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/telemetry/logger.py +112 -0
  384. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/BEST_PRACTICES/README.md +14 -0
  385. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/BEST_PRACTICES/api.md +17 -0
  386. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/BEST_PRACTICES/compliance-wiring.md +155 -0
  387. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/BEST_PRACTICES/frontend.md +15 -0
  388. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/CI_CD/README.md +11 -0
  389. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/CI_CD/deployment/README.md +23 -0
  390. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/CI_CD/deployment/clients/README.md +16 -0
  391. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/CI_CD/deployment/clients/template-README.md +11 -0
  392. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/CI_CD/deployment/deploy.sh +49 -0
  393. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/CI_CD/deployment/envs/README.md +12 -0
  394. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/CI_CD/deployment/envs/development.env.example +17 -0
  395. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/CI_CD/deployment/envs/production.env.example +17 -0
  396. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/CI_CD/deployment/envs/staging.env.example +17 -0
  397. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/CI_CD/deployment/tenants/README.md +11 -0
  398. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/CI_CD/deployment/tenants/template.env.example +8 -0
  399. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/DEVELOPER_SETUP/README.md +26 -0
  400. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/DEVELOPER_SETUP/agent.md +7 -0
  401. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/DEVELOPER_SETUP/developers/dev-1.md +20 -0
  402. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/DEVELOPER_SETUP/linux.md +9 -0
  403. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/DEVELOPER_SETUP/macos.md +10 -0
  404. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/DEVELOPER_SETUP/shared.md +19 -0
  405. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/DEVELOPER_SETUP/windows.md +14 -0
  406. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/HOSTING/README.md +25 -0
  407. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/HOSTING/_template.md +41 -0
  408. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/HOSTING/hosting-1.md +41 -0
  409. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/README.md +21 -0
  410. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/RUNBOOK/README.md +15 -0
  411. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/RUNBOOK/deployment.md +19 -0
  412. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/RUNBOOK/disaster-recovery.md +15 -0
  413. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/RUNBOOK/ops-tasks.md +14 -0
  414. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/features/README.md +16 -0
  415. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/project-skills.md +146 -0
  416. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/research/README.md +11 -0
  417. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/research/software-compliance-research.md +269 -0
  418. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/working-with-agents.md +260 -0
  419. package/.agents/skills/nsr-compliance/assets/nsr/assets/root/AGENTS.md +38 -0
  420. package/.agents/skills/nsr-compliance/assets/nsr/assets/root/ARCHITECTURE.md +36 -0
  421. package/.agents/skills/nsr-compliance/assets/nsr/assets/root/BEST_PRACTICES.md +27 -0
  422. package/.agents/skills/nsr-compliance/assets/nsr/assets/root/CI_CD.md +23 -0
  423. package/.agents/skills/nsr-compliance/assets/nsr/assets/root/FEATURES.md +15 -0
  424. package/.agents/skills/nsr-compliance/assets/nsr/assets/root/README.md +22 -0
  425. package/.agents/skills/nsr-compliance/assets/nsr/assets/root/RULES.md +37 -0
  426. package/.agents/skills/nsr-compliance/assets/nsr/assets/root/STRUCTURE.md +45 -0
  427. package/.agents/skills/nsr-compliance/assets/nsr/assets/root/TECH_STACK.md +28 -0
  428. package/.agents/skills/nsr-compliance/assets/nsr/scaffold-spec.md +63 -0
  429. package/.agents/skills/nsr-compliance/assets/nsr/scripts/scaffold.sh +103 -0
  430. package/.agents/skills/nsr-compliance/assets/nsr/scripts/validate.sh +52 -0
  431. package/.agents/skills/nsr-compliance/assets/nsrcompliance/SKILL.md +71 -0
  432. package/.agents/skills/nsr-compliance/assets/nsrcompliance/config/core_four.yaml +16 -0
  433. package/.agents/skills/nsr-compliance/assets/nsrcompliance/gates/check_danger.sh +102 -0
  434. package/.agents/skills/nsr-compliance/assets/nsrcompliance/gates/check_wiring.sh +69 -0
  435. package/.agents/skills/nsr-compliance/assets/nsrcompliance/gates/validate_code.sh +29 -0
  436. package/.agents/skills/nsr-compliance/assets/nsrcompliance/gates/verify_docs.py +66 -0
  437. package/.agents/skills/nsr-compliance/assets/nsrcompliance/generate.sh +109 -0
  438. package/.agents/skills/nsr-compliance/assets/nsrcompliance/harness/envelopes/result_envelope.json +14 -0
  439. package/.agents/skills/nsr-compliance/assets/nsrcompliance/harness/envelopes/task_envelope.json +20 -0
  440. package/.agents/skills/nsr-compliance/assets/nsrcompliance/harness/runner.py +303 -0
  441. package/.agents/skills/nsr-compliance/assets/nsrcompliance/telemetry/logger.py +112 -0
  442. package/.agents/skills/nsr-compliance/assets/nsrcompliance/templates/compliance/README.md +18 -0
  443. package/.agents/skills/nsr-compliance/assets/nsrcompliance/templates/compliance/config/README.md +10 -0
  444. package/.agents/skills/nsr-compliance/assets/nsrcompliance/templates/compliance/gates/README.md +12 -0
  445. package/.agents/skills/nsr-compliance/assets/nsrcompliance/templates/compliance/gates/check_env.sh +23 -0
  446. package/.agents/skills/nsr-compliance/assets/nsrcompliance/templates/compliance/gates/check_paths.sh +20 -0
  447. package/.agents/skills/nsr-compliance/assets/nsrcompliance/templates/compliance/gates/check_platform.sh +23 -0
  448. package/.agents/skills/nsr-compliance/assets/nsrcompliance/templates/compliance/harness/README.md +11 -0
  449. package/.agents/skills/nsr-compliance/assets/nsrcompliance/templates/compliance/harness/envelopes/README.md +10 -0
  450. package/.agents/skills/nsr-compliance/assets/nsrcompliance/templates/compliance/installer/README.md +10 -0
  451. package/.agents/skills/nsr-compliance/assets/nsrcompliance/templates/compliance/installer/install.sh +125 -0
  452. package/.agents/skills/nsr-compliance/assets/nsrcompliance/templates/compliance/telemetry/README.md +11 -0
  453. package/.agents/skills/pr-ops/README.md +3 -0
  454. package/.agents/skills/pr-ops/SKILL.md +109 -0
  455. package/.agents/skills/ratatoskr-a2a/README.md +3 -0
  456. package/.agents/skills/ratatoskr-a2a/SKILL.md +148 -0
  457. package/.agents/skills/rules-check-drift/SKILL.md +81 -0
  458. package/.agents/skills/saga-bearings/README.md +3 -0
  459. package/.agents/skills/saga-bearings/SKILL.md +26 -0
  460. package/.agents/skills/saga-bearings/assets/bearings.md +161 -0
  461. package/.agents/skills/saga-bearings/assets/board-template.html +731 -0
  462. package/.agents/skills/saga-bearings/assets/recap.md +56 -0
  463. package/.agents/skills/syn-recovery/README.md +3 -0
  464. package/.agents/skills/syn-recovery/SKILL.md +54 -0
  465. package/.agents/skills/tyr-check/README.md +3 -0
  466. package/.agents/skills/tyr-check/SKILL.md +218 -0
  467. package/.agents/skills/urdh-hold/README.md +3 -0
  468. package/.agents/skills/urdh-hold/SKILL.md +25 -0
  469. package/.agents/skills/urdh-hold/assets/decisions.md +15 -0
  470. package/.agents/skills/urdh-hold/assets/hold.md +54 -0
  471. package/.agents/skills/vor-diagnostics/README.md +3 -0
  472. package/.agents/skills/vor-diagnostics/SKILL.md +68 -0
  473. package/.agents/skills/ymir-host/README.md +3 -0
  474. package/.agents/skills/ymir-host/SKILL.md +35 -0
  475. package/.agents/skills/ymir-host/assets/desktop.md +36 -0
  476. package/.agents/skills/ymir-host/assets/install.md +69 -0
  477. package/.agents/skills/ymir-host/assets/omarchy.md +297 -0
  478. package/.agents/skills/ymir-host/assets/runbooks.md +15 -0
  479. package/.agents/skills/ymir-host/assets/thjazi.md +220 -0
  480. package/.agents/skills/ymir-host/assets/update.md +64 -0
  481. package/.agents/tests/assets/board-render-harness.mjs +117 -0
  482. package/.agents/tests/cmux-test-safety.sh +51 -0
  483. package/.agents/tests/fixtures.sh +291 -0
  484. package/.agents/tests/fm-afk-inject-e2e.test.sh +428 -0
  485. package/.agents/tests/fm-afk-inject-herdr-e2e.test.sh +536 -0
  486. package/.agents/tests/fm-afk-launch.test.sh +961 -0
  487. package/.agents/tests/fm-afk-pi-herdr-return-e2e.test.sh +297 -0
  488. package/.agents/tests/fm-afk-return.test.sh +283 -0
  489. package/.agents/tests/fm-arm-pretool-check.test.sh +482 -0
  490. package/.agents/tests/fm-ask-user-authority.test.sh +42 -0
  491. package/.agents/tests/fm-backend-autodetect-smoke.test.sh +176 -0
  492. package/.agents/tests/fm-backend-cmux-smoke.test.sh +188 -0
  493. package/.agents/tests/fm-backend-cmux.test.sh +1166 -0
  494. package/.agents/tests/fm-backend-herdr-eventwait-smoke.test.sh +136 -0
  495. package/.agents/tests/fm-backend-herdr-eventwait.test.py +106 -0
  496. package/.agents/tests/fm-backend-herdr-focus-flash-e2e.test.sh +409 -0
  497. package/.agents/tests/fm-backend-herdr-launcher-workspace-e2e.test.sh +437 -0
  498. package/.agents/tests/fm-backend-herdr-presentation-e2e.test.sh +1424 -0
  499. package/.agents/tests/fm-backend-herdr-prune-safety-e2e.test.sh +180 -0
  500. package/.agents/tests/fm-backend-herdr-respawn-idem-e2e.test.sh +182 -0
  501. package/.agents/tests/fm-backend-herdr-smoke.test.sh +355 -0
  502. package/.agents/tests/fm-backend-herdr-workspace-per-home-e2e.test.sh +251 -0
  503. package/.agents/tests/fm-backend-herdr.test.sh +4606 -0
  504. package/.agents/tests/fm-backend-orca.test.sh +1357 -0
  505. package/.agents/tests/fm-backend-tmux-smoke.test.sh +173 -0
  506. package/.agents/tests/fm-backend-zellij-smoke.test.sh +205 -0
  507. package/.agents/tests/fm-backend-zellij.test.sh +1360 -0
  508. package/.agents/tests/fm-backend.test.sh +1142 -0
  509. package/.agents/tests/fm-backlog-atomicity.test.sh +2302 -0
  510. package/.agents/tests/fm-backlog-handoff.test.sh +1360 -0
  511. package/.agents/tests/fm-bearings-board-render.test.sh +135 -0
  512. package/.agents/tests/fm-bearings-board.test.sh +414 -0
  513. package/.agents/tests/fm-bearings-snapshot.test.sh +1990 -0
  514. package/.agents/tests/fm-bootstrap-network-parallel.test.sh +316 -0
  515. package/.agents/tests/fm-bootstrap.test.sh +1178 -0
  516. package/.agents/tests/fm-branch-supervision.test.sh +552 -0
  517. package/.agents/tests/fm-brief.test.sh +785 -0
  518. package/.agents/tests/fm-busy-adapter-wiring.test.sh +324 -0
  519. package/.agents/tests/fm-busy-state.test.sh +465 -0
  520. package/.agents/tests/fm-calm-pi-extension.test.sh +3979 -0
  521. package/.agents/tests/fm-captain-hold-lifecycle.test.sh +1187 -0
  522. package/.agents/tests/fm-cd-pretool-check.test.sh +400 -0
  523. package/.agents/tests/fm-check-unregister.test.sh +198 -0
  524. package/.agents/tests/fm-classify-corr-token.test.sh +543 -0
  525. package/.agents/tests/fm-classify-decision-key.test.sh +340 -0
  526. package/.agents/tests/fm-claude-stop-autoarm-live-e2e.test.sh +164 -0
  527. package/.agents/tests/fm-claude-stop-autoarm.test.sh +1190 -0
  528. package/.agents/tests/fm-cmux-claude-composer-live-e2e.test.sh +110 -0
  529. package/.agents/tests/fm-codex-continuity-live-e2e.test.sh +55 -0
  530. package/.agents/tests/fm-composer-ghost.test.sh +716 -0
  531. package/.agents/tests/fm-composer-lib.test.sh +666 -0
  532. package/.agents/tests/fm-composer-matrix-live-e2e.test.sh +220 -0
  533. package/.agents/tests/fm-control-herdr-smoke.test.sh +149 -0
  534. package/.agents/tests/fm-control-relaunch.test.sh +1530 -0
  535. package/.agents/tests/fm-control.test.sh +910 -0
  536. package/.agents/tests/fm-crew-state.test.sh +1610 -0
  537. package/.agents/tests/fm-cursor-harness.test.sh +404 -0
  538. package/.agents/tests/fm-cursor-primary-live-e2e.test.sh +217 -0
  539. package/.agents/tests/fm-cursor-primary.test.sh +708 -0
  540. package/.agents/tests/fm-daemon.test.sh +2740 -0
  541. package/.agents/tests/fm-documentation-audiences.test.sh +141 -0
  542. package/.agents/tests/fm-ensure-agents-md.test.sh +371 -0
  543. package/.agents/tests/fm-extension-binding.test.sh +2187 -0
  544. package/.agents/tests/fm-fleet-snapshot-view.test.sh +816 -0
  545. package/.agents/tests/fm-fleet-sync.test.sh +721 -0
  546. package/.agents/tests/fm-gate-refuse.test.sh +356 -0
  547. package/.agents/tests/fm-gitignore-config.test.sh +89 -0
  548. package/.agents/tests/fm-gotmp.test.sh +230 -0
  549. package/.agents/tests/fm-grok-continuity-live-e2e.test.sh +112 -0
  550. package/.agents/tests/fm-grok-harness.test.sh +117 -0
  551. package/.agents/tests/fm-grok-stop-live-e2e.test.sh +219 -0
  552. package/.agents/tests/fm-guard-stale-banner.test.sh +709 -0
  553. package/.agents/tests/fm-harness-adapter-instructions-live-e2e.test.sh +134 -0
  554. package/.agents/tests/fm-harness-adapter-references.test.sh +30 -0
  555. package/.agents/tests/fm-harness-liveness-drift-live-e2e.test.sh +151 -0
  556. package/.agents/tests/fm-herdr-lab.test.sh +243 -0
  557. package/.agents/tests/fm-herdr-session-cleanup-e2e.test.sh +146 -0
  558. package/.agents/tests/fm-herdr-session-cleanup.test.sh +329 -0
  559. package/.agents/tests/fm-herdr-submit-confirm-live-e2e.test.sh +125 -0
  560. package/.agents/tests/fm-herdr-version-floor-live-e2e.test.sh +144 -0
  561. package/.agents/tests/fm-home-summary-refresh.test.sh +971 -0
  562. package/.agents/tests/fm-inactive-reconcile.test.sh +485 -0
  563. package/.agents/tests/fm-kimi-harness.test.sh +687 -0
  564. package/.agents/tests/fm-lint-workflows.test.sh +532 -0
  565. package/.agents/tests/fm-lint.test.sh +1023 -0
  566. package/.agents/tests/fm-muse-harness.test.sh +932 -0
  567. package/.agents/tests/fm-muse-signals-live-e2e.test.sh +205 -0
  568. package/.agents/tests/fm-no-mistakes-required.test.sh +72 -0
  569. package/.agents/tests/fm-on.test.sh +507 -0
  570. package/.agents/tests/fm-opencode-primary-live-e2e.test.sh +357 -0
  571. package/.agents/tests/fm-operational-input.test.sh +160 -0
  572. package/.agents/tests/fm-peek-remote.test.sh +110 -0
  573. package/.agents/tests/fm-pending-reply.test.sh +1288 -0
  574. package/.agents/tests/fm-pi-branch-extension.test.sh +3182 -0
  575. package/.agents/tests/fm-pi-branch-live-e2e.test.sh +527 -0
  576. package/.agents/tests/fm-pi-primary-live-e2e.test.sh +345 -0
  577. package/.agents/tests/fm-pi-primary-types.test.sh +68 -0
  578. package/.agents/tests/fm-pi-watch-extension.test.sh +2843 -0
  579. package/.agents/tests/fm-pr-check-security.test.sh +2140 -0
  580. package/.agents/tests/fm-pr-merge.test.sh +2135 -0
  581. package/.agents/tests/fm-procevent-quota.test.sh +225 -0
  582. package/.agents/tests/fm-procevent-when.test.sh +406 -0
  583. package/.agents/tests/fm-procevent.test.sh +1745 -0
  584. package/.agents/tests/fm-project-origin.test.sh +110 -0
  585. package/.agents/tests/fm-public-followup.test.sh +2272 -0
  586. package/.agents/tests/fm-quota-array-dispatch-live-e2e.test.sh +526 -0
  587. package/.agents/tests/fm-quota-choose.test.sh +626 -0
  588. package/.agents/tests/fm-remote-backlog-handoff.test.sh +453 -0
  589. package/.agents/tests/fm-remote-doctor.test.sh +625 -0
  590. package/.agents/tests/fm-remote-entrypoint.test.sh +59 -0
  591. package/.agents/tests/fm-remote-job-orphan-reap.test.sh +205 -0
  592. package/.agents/tests/fm-remote-job.test.sh +768 -0
  593. package/.agents/tests/fm-remote-reply.test.sh +520 -0
  594. package/.agents/tests/fm-remote-secondmate-lifecycle-e2e.test.sh +1256 -0
  595. package/.agents/tests/fm-remote-secondmate-parent-binding.test.sh +326 -0
  596. package/.agents/tests/fm-remote-secondmate-trace-context.test.sh +310 -0
  597. package/.agents/tests/fm-remote-transport-lanes.test.sh +431 -0
  598. package/.agents/tests/fm-review-diff.test.sh +176 -0
  599. package/.agents/tests/fm-secondmate-harness.test.sh +2600 -0
  600. package/.agents/tests/fm-secondmate-lifecycle-e2e.test.sh +259 -0
  601. package/.agents/tests/fm-secondmate-liveness.test.sh +559 -0
  602. package/.agents/tests/fm-secondmate-reconcile.test.sh +732 -0
  603. package/.agents/tests/fm-secondmate-safety.test.sh +2972 -0
  604. package/.agents/tests/fm-secondmate-sync.test.sh +913 -0
  605. package/.agents/tests/fm-send-inbox-doorbell-live-e2e.test.sh +206 -0
  606. package/.agents/tests/fm-send-inbox.test.sh +352 -0
  607. package/.agents/tests/fm-send-popup-settle.test.sh +167 -0
  608. package/.agents/tests/fm-send-remote-delivery.test.sh +796 -0
  609. package/.agents/tests/fm-send-resolve-key.test.sh +551 -0
  610. package/.agents/tests/fm-send-secondmate-marker-herdr-e2e.test.sh +187 -0
  611. package/.agents/tests/fm-send-secondmate-marker.test.sh +278 -0
  612. package/.agents/tests/fm-send-settle.test.sh +147 -0
  613. package/.agents/tests/fm-send-strict.test.sh +241 -0
  614. package/.agents/tests/fm-session-lock-ancestry.test.sh +365 -0
  615. package/.agents/tests/fm-session-start.test.sh +2515 -0
  616. package/.agents/tests/fm-sessionstart-hook-live-e2e.test.sh +631 -0
  617. package/.agents/tests/fm-sessionstart-instruction-refresh-live-e2e.test.sh +230 -0
  618. package/.agents/tests/fm-sessionstart-nudge.test.sh +1039 -0
  619. package/.agents/tests/fm-shared-captain-inheritance.test.sh +404 -0
  620. package/.agents/tests/fm-spawn-batch.test.sh +148 -0
  621. package/.agents/tests/fm-spawn-dispatch-profile.test.sh +827 -0
  622. package/.agents/tests/fm-spawn-pool-base-freshen.test.sh +440 -0
  623. package/.agents/tests/fm-spawn-worktree-settle.test.sh +147 -0
  624. package/.agents/tests/fm-startup-memory-budget.test.sh +333 -0
  625. package/.agents/tests/fm-startup-network.test.sh +712 -0
  626. package/.agents/tests/fm-stow-cascade.test.sh +370 -0
  627. package/.agents/tests/fm-subagent-pretool-check.test.sh +291 -0
  628. package/.agents/tests/fm-supervision-events.test.sh +156 -0
  629. package/.agents/tests/fm-supervision-instructions.test.sh +189 -0
  630. package/.agents/tests/fm-tangle-guard.test.sh +277 -0
  631. package/.agents/tests/fm-task-delivery.test.sh +420 -0
  632. package/.agents/tests/fm-task-inbox.test.sh +513 -0
  633. package/.agents/tests/fm-teardown-endpoint-safety.test.sh +374 -0
  634. package/.agents/tests/fm-teardown.test.sh +2665 -0
  635. package/.agents/tests/fm-test-fixture-cleanup.test.sh +172 -0
  636. package/.agents/tests/fm-test-fixtures.test.sh +132 -0
  637. package/.agents/tests/fm-test-isolation-proof.test.sh +278 -0
  638. package/.agents/tests/fm-test-run.test.sh +1196 -0
  639. package/.agents/tests/fm-tmux-agent-liveness.test.sh +354 -0
  640. package/.agents/tests/fm-tmux-submit-busy.test.sh +340 -0
  641. package/.agents/tests/fm-tool-update-check.test.sh +1044 -0
  642. package/.agents/tests/fm-trace-context-lib.test.sh +253 -0
  643. package/.agents/tests/fm-trace-context-spawn.test.sh +581 -0
  644. package/.agents/tests/fm-transition-lib.test.sh +54 -0
  645. package/.agents/tests/fm-turnend-guard.test.sh +1822 -0
  646. package/.agents/tests/fm-update.test.sh +304 -0
  647. package/.agents/tests/fm-vendor-auth-probe.test.sh +395 -0
  648. package/.agents/tests/fm-voice-relay.test.sh +4741 -0
  649. package/.agents/tests/fm-wake-daemon-lifecycle-e2e.test.sh +171 -0
  650. package/.agents/tests/fm-wake-drain-open-decisions-cursor.test.sh +356 -0
  651. package/.agents/tests/fm-wake-drain-open-decisions.test.sh +226 -0
  652. package/.agents/tests/fm-wake-drain-unread-status.test.sh +379 -0
  653. package/.agents/tests/fm-wake-queue.test.sh +1224 -0
  654. package/.agents/tests/fm-watch-arm.test.sh +821 -0
  655. package/.agents/tests/fm-watch-checkpoint.test.sh +87 -0
  656. package/.agents/tests/fm-watch-recovery-loop.test.sh +223 -0
  657. package/.agents/tests/fm-watch-triage.test.sh +3889 -0
  658. package/.agents/tests/fm-watcher-lock.test.sh +1133 -0
  659. package/.agents/tests/fm-x-mode.test.sh +3042 -0
  660. package/.agents/tests/gleipnir-guard-pretool-check.test.sh +51 -0
  661. package/.agents/tests/gleipnir-machine-lock.test.sh +130 -0
  662. package/.agents/tests/herdr-test-safety.sh +42 -0
  663. package/.agents/tests/lib.sh +315 -0
  664. package/.agents/tests/remote-herdr-fixture.sh +125 -0
  665. package/.agents/tests/secondmate-helpers.sh +193 -0
  666. package/.agents/tests/smoke.test.sh +93 -0
  667. package/.agents/tests/wake-helpers.sh +338 -0
  668. package/.agents/tests/zellij-test-safety.sh +60 -0
  669. package/.agents/tools/README.md +10 -0
  670. package/.agents/tools/bin/tasks-cli +267 -0
  671. package/.agents/tools/tasks-cli.ts +216 -0
  672. package/AGENTS.md +498 -0
  673. package/CHANGELOG.md +1913 -0
  674. package/LICENSE +1 -1
  675. package/NOTICE +63 -0
  676. package/README.md +114 -60
  677. package/RULES/01-domains.md +51 -0
  678. package/RULES/02-agents.md +78 -0
  679. package/RULES/03-houses.md +61 -0
  680. package/RULES/04-hoard.md +97 -0
  681. package/RULES/05-platforms.md +67 -0
  682. package/RULES/06-append-only.md +108 -0
  683. package/RULES/07-config.md +101 -0
  684. package/RULES/08-delivery-gate.md +75 -0
  685. package/RULES/09-electron.md +58 -0
  686. package/RULES/README.md +22 -0
  687. package/bin/README.md +107 -0
  688. package/bin/a2a-mcp.sh +133 -0
  689. package/bin/a2a-serve.py +103 -0
  690. package/bin/a2a-serve.sh +8 -0
  691. package/bin/a2a-talk.sh +50 -0
  692. package/bin/a2abridge-ensure.sh +108 -0
  693. package/bin/agent-run.sh +43 -0
  694. package/bin/agents-config.sh +256 -0
  695. package/bin/apodex-smoke-test.sh +105 -0
  696. package/bin/bifrost-bridge.sh +131 -0
  697. package/bin/bifrost-ingress.sh +77 -0
  698. package/bin/bootstrap-macos.sh +112 -0
  699. package/bin/bootstrap-windows.ps1 +112 -0
  700. package/bin/branch-guard.sh +64 -0
  701. package/bin/brokk +73 -0
  702. package/bin/brokk-classify-lib.sh +1770 -0
  703. package/bin/brokk-lease-lib.sh +222 -0
  704. package/bin/brokk-lease.sh +193 -0
  705. package/bin/brokk-lint.sh +65 -0
  706. package/bin/brokk-send.sh +51 -0
  707. package/bin/brokk-update.sh +9 -0
  708. package/bin/brokk-wake-grant.sh +127 -0
  709. package/bin/brokk-wake-lib.sh +1849 -0
  710. package/bin/changelog-assemble.sh +154 -0
  711. package/bin/changelog-guard.sh +197 -0
  712. package/bin/crash-sense.sh +173 -0
  713. package/bin/design-check.sh +80 -0
  714. package/bin/design-icon.sh +153 -0
  715. package/bin/desktop-place.sh +224 -0
  716. package/bin/docs-guard.sh +39 -0
  717. package/bin/editor-place.sh +225 -0
  718. package/bin/eindri-acclaim.sh +47 -0
  719. package/bin/eindri-control.sh +28 -0
  720. package/bin/eindri-role.sh +83 -0
  721. package/bin/eindri-seat.sh +83 -0
  722. package/bin/eindri-seen.sh +54 -0
  723. package/bin/eindri-send.sh +15 -0
  724. package/bin/eindri-start.sh +98 -0
  725. package/bin/eindri-watch.sh +57 -0
  726. package/bin/einherjar-spawn.sh +576 -0
  727. package/bin/eir-doctor.sh +158 -0
  728. package/bin/erindi-brief.sh +271 -0
  729. package/bin/feature-inventory.sh +73 -0
  730. package/bin/fleet-apply.sh +81 -0
  731. package/bin/github-deploy.sh +94 -0
  732. package/bin/gjallarhorn-expose.sh +136 -0
  733. package/bin/gjallarhorn-notify.sh +71 -0
  734. package/bin/gjallarhorn-purge.sh +67 -0
  735. package/bin/gjallarhorn-tunnel.sh +85 -0
  736. package/bin/gleipnir-lock-lib.sh +284 -0
  737. package/bin/groa-update.sh +130 -0
  738. package/bin/hall-snapshot.sh +111 -0
  739. package/bin/hamr-harness.sh +246 -0
  740. package/bin/heimdall-ssh-keys.sh +374 -0
  741. package/bin/herdr-agents.py +44 -0
  742. package/bin/herdr-ensure.sh +104 -0
  743. package/bin/herdr-run.sh +452 -0
  744. package/bin/hermes-ensure.sh +88 -0
  745. package/bin/hlidskjalf-agents.sh +172 -0
  746. package/bin/hlidskjalf-usage.sh +196 -0
  747. package/bin/hoard-guard.sh +135 -0
  748. package/bin/hoard-lib.sh +105 -0
  749. package/bin/hodd.sh +102 -0
  750. package/bin/host-sense.sh +199 -0
  751. package/bin/huginn-research-worker.sh +121 -0
  752. package/bin/local-model-lock.sh +70 -0
  753. package/bin/mimir-bridge.py +259 -0
  754. package/bin/mimir-bridge.sh +100 -0
  755. package/bin/mimir-ingest.sh +131 -0
  756. package/bin/mimir-reflect.sh +27 -0
  757. package/bin/mimir.sh +113 -0
  758. package/bin/mjollnir-webhook.sh +72 -0
  759. package/bin/mjollnir.sh +104 -0
  760. package/bin/model-hardware.sh +96 -0
  761. package/bin/model-resolve.sh +160 -0
  762. package/bin/models-detect.sh +178 -0
  763. package/bin/nornir-cron-start.sh +162 -0
  764. package/bin/nornir-job-daily-briefing.sh +191 -0
  765. package/bin/nornir-job-git-sync.sh +152 -0
  766. package/bin/nornir-job-hall-snapshot.sh +28 -0
  767. package/bin/nornir-job-memory-housekeeping.sh +114 -0
  768. package/bin/nornir-job-nsr-compliance.sh +54 -0
  769. package/bin/nornir-job-observer.sh +166 -0
  770. package/bin/nornir-job-skillopt-sleep.sh +76 -0
  771. package/bin/npm-publish.sh +136 -0
  772. package/bin/omarchy-hook-install.sh +60 -0
  773. package/bin/omarchy-install.sh +161 -0
  774. package/bin/omarchy-plugins.sh +187 -0
  775. package/bin/omarchy-sense.sh +162 -0
  776. package/bin/perm-guard.sh +30 -0
  777. package/bin/pi-agent.sh +68 -0
  778. package/bin/pi-ensure.sh +77 -0
  779. package/bin/pi-local.sh +37 -0
  780. package/bin/pi-model-agnostic.sh +71 -0
  781. package/bin/pi-seat.sh +63 -0
  782. package/bin/prereq-ensure.sh +166 -0
  783. package/bin/private-guard.sh +58 -0
  784. package/bin/project-git.sh +69 -0
  785. package/bin/public-guard.sh +71 -0
  786. package/bin/ratatoskr.sh +49 -0
  787. package/bin/realm-lib.sh +32 -0
  788. package/bin/repo-scrub.sh +62 -0
  789. package/bin/rodd-operational-input.sh +250 -0
  790. package/bin/runes-append.sh +198 -0
  791. package/bin/saga-session-start.sh +172 -0
  792. package/bin/saga-sessionstart-run.sh +79 -0
  793. package/bin/saga-wake-drain.sh +41 -0
  794. package/bin/secret-guard.sh +63 -0
  795. package/bin/sessrumnir-ensure.sh +100 -0
  796. package/bin/sessrumnir-sync.sh +237 -0
  797. package/bin/sessrumnir.sh +104 -0
  798. package/bin/skillopt-setup.sh +76 -0
  799. package/bin/skuld-branch-outcome.sh +203 -0
  800. package/bin/skuld-branch-prompt.sh +101 -0
  801. package/bin/smidja-bootstrap.sh +94 -0
  802. package/bin/smidja-observe.sh +86 -0
  803. package/bin/syn-arm-pretool-check.sh +57 -0
  804. package/bin/syn-asset-pretool-check.sh +89 -0
  805. package/bin/syn-cd-pretool-check.sh +28 -0
  806. package/bin/syn-guard-pretool-check.sh +83 -0
  807. package/bin/syn-spawn-pretool-check.sh +58 -0
  808. package/bin/syn-turnend-guard.sh +36 -0
  809. package/bin/syn-watch-arm.sh +99 -0
  810. package/bin/tailscale-sync.sh +203 -0
  811. package/bin/telegram-bot.sh +82 -0
  812. package/bin/toolchain.sh +74 -0
  813. package/bin/ui-truth-check.sh +75 -0
  814. package/bin/utgard.sh +111 -0
  815. package/bin/valhalla.sh +115 -0
  816. package/bin/valknut-load.sh +298 -0
  817. package/bin/vor-crew-state.sh +124 -0
  818. package/bin/wedge-notify.sh +60 -0
  819. package/bin/workspace-provision.sh +80 -0
  820. package/bin/workspace-rag.sh +163 -0
  821. package/bin/wyrd-db.sh +47 -0
  822. package/bin/yggdrasil.sh +152 -0
  823. package/bin/ymir-edit.sh +145 -0
  824. package/bin/ymir-install.sh +806 -0
  825. package/bin/ymir-invite.sh +93 -0
  826. package/bin/ymir-isolation.sh +88 -0
  827. package/bin/ymir-migrate.sh +61 -0
  828. package/bin/ymir-plan.sh +356 -0
  829. package/bin/ymir-platform.sh +348 -0
  830. package/bin/ymir-say.sh +110 -0
  831. package/bin/ymir-setup-auth.sh +119 -0
  832. package/bin/ymir-validate.sh +193 -0
  833. package/bin/ymir.js +74 -16
  834. package/docs/Architecture.md +339 -0
  835. package/docs/design.md +351 -0
  836. package/docs/installations/README.md +63 -0
  837. package/docs/installations/ymir-home.md +93 -0
  838. package/docs/integration.md +105 -0
  839. package/docs/lore.md +1060 -0
  840. package/docs/research/agent-harnesses.md +290 -0
  841. package/docs/runbooks/README.md +19 -0
  842. package/docs/runbooks/a2a.md +91 -0
  843. package/docs/runbooks/agent-permissions.md +99 -0
  844. package/docs/runbooks/agents.md +57 -0
  845. package/docs/runbooks/models.md +69 -0
  846. package/docs/runbooks/realm-onboarding.md +60 -0
  847. package/docs/runbooks/secrets-and-hoard.md +60 -0
  848. package/docs/runbooks/updates-and-migrations.md +110 -0
  849. package/docs/runbooks/worktrees-isolation.md +72 -0
  850. package/docs/session-start.md +105 -0
  851. package/docs/skillopt-integration.md +321 -0
  852. package/docs/sync-system.md +66 -0
  853. package/docs/workspaces.md +104 -0
  854. package/docs/ymir-rut.md +260 -0
  855. package/package.json +54 -14
  856. package/packaging/build.sh +74 -0
  857. package/packaging/macos/Ymir Installer.command +43 -0
  858. package/packaging/windows/ymir-setup.nsi +74 -0
  859. package/scripts/README.md +22 -0
  860. package/scripts/electron.sh +269 -0
  861. package/scripts/lower.sh +34 -0
  862. package/scripts/raise.sh +42 -0
  863. package/scripts/start.sh +217 -0
  864. package/scripts/stop.sh +113 -0
  865. package/install.sh +0 -35
@@ -0,0 +1,1745 @@
1
+ #!/usr/bin/env bash
2
+ # Behavior tests for the generic process-to-event runner and its Lavish adapter.
3
+ #
4
+ # The source under test is a fake blocking process that returns only when its
5
+ # trigger file appears, so completion is a real process event and no test here
6
+ # depends on a discovery timer. The Lavish adapter is exercised through its own
7
+ # public commands against the currently published poll shape; no live Lavish
8
+ # server is started.
9
+ #
10
+ # Delivery is deliberately NOT asserted as at-least-once or lossless: the
11
+ # published Lavish poll clears feedback destructively before returning it, so
12
+ # the only durability under test is the runner's own - output that reached the
13
+ # runner is stored before it is announced.
14
+ set -u
15
+
16
+ # shellcheck source=tests/lib.sh
17
+ . "$(dirname "${BASH_SOURCE[0]}")/lib.sh"
18
+
19
+ ROOT=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)
20
+ TMP_ROOT=$(fm_test_tmproot fm-procevent-tests)
21
+ export FM_PROCEVENT_CLAIM_ROOT="$TMP_ROOT/claims"
22
+
23
+ BLOCKER="$TMP_ROOT/blocker.sh"
24
+ cat > "$BLOCKER" <<'SH'
25
+ #!/usr/bin/env bash
26
+ # Blocks until the trigger exists, then emits its payload. Completion is the
27
+ # event; nothing here polls on a schedule.
28
+ trigger=$1; shift
29
+ while [ ! -e "$trigger" ]; do sleep 0.05; done
30
+ [ -n "${BLOCKER_STDERR:-}" ] && printf 'noise on stderr\n' >&2
31
+ [ -n "${BLOCKER_EXIT:-}" ] && exit "$BLOCKER_EXIT"
32
+ printf '%s\n' "$@"
33
+ SH
34
+ chmod +x "$BLOCKER"
35
+
36
+ pe() { FM_HOME="$1" "$ROOT/bin/fm-procevent.sh" "${@:2}"; }
37
+
38
+ # Every source this suite registers is tracked so teardown can stop its runner.
39
+ # A runner started by reconcile is detached and reparented, so a source that
40
+ # never completes outlives the suite unless it is retired explicitly - removing
41
+ # the fixture directory does not stop an already-running child.
42
+ PE_TRACKED=()
43
+ pe_register() { # <home> <adapter> <source-id> -- <argv>...
44
+ local home=$1 adapter=$2 id=$3
45
+ shift 3
46
+ PE_TRACKED+=("$home|$id")
47
+ pe "$home" register "$adapter" "$id" "$@"
48
+ }
49
+
50
+ procevent_teardown() {
51
+ local entry home seen=$'\n'
52
+ for entry in ${PE_TRACKED[@]+"${PE_TRACKED[@]}"}; do
53
+ home=${entry%%|*}
54
+ case "$seen" in
55
+ *$'\n'"$home"$'\n'*) continue ;;
56
+ esac
57
+ seen+="$home"$'\n'
58
+ FM_HOME="$home" "$ROOT/bin/fm-procevent.sh" sweep-home >/dev/null 2>&1 || true
59
+ done
60
+ fm_test_cleanup
61
+ }
62
+ trap procevent_teardown EXIT
63
+ new_home() { mkdir -p "$1/state"; }
64
+ wake_payloads() { awk -F '\t' '{print $5}' "$1/state/.wake-queue" 2>/dev/null; }
65
+
66
+ first_result() { # <home> <source-id>: print the first captured result, if any
67
+ local g
68
+ for g in "$1/state/procevent-inbox/$2".*.result; do
69
+ [ -e "$g" ] || continue
70
+ printf '%s\n' "$g"
71
+ return 0
72
+ done
73
+ return 1
74
+ }
75
+
76
+ count_results() { # <home> <source-id>
77
+ local g n=0
78
+ for g in "$1/state/procevent-inbox/$2".*.result; do
79
+ [ -e "$g" ] && n=$((n + 1))
80
+ done
81
+ printf '%s\n' "$n"
82
+ }
83
+
84
+ wait_for() { # <file> [tries]
85
+ local f=$1 n=${2:-100}
86
+ for _ in $(seq 1 "$n"); do [ -s "$f" ] && return 0; sleep 0.1; done
87
+ return 1
88
+ }
89
+
90
+ # <file> <count> [tries]: wait until <file> holds at least <count> lines. A
91
+ # detached runner appends its execution marker after the command that started it
92
+ # has already returned, so a caller that needs that append must wait for it
93
+ # rather than assume a fixed settle window covered it on a loaded machine.
94
+ wait_for_lines() {
95
+ local f=$1 want=$2 n=${3:-100} have
96
+ for _ in $(seq 1 "$n"); do
97
+ have=$(wc -l < "$f" 2>/dev/null | tr -d ' ')
98
+ case "$have" in ''|*[!0-9]*) have=0 ;; esac
99
+ [ "$have" -ge "$want" ] && return 0
100
+ sleep 0.1
101
+ done
102
+ return 1
103
+ }
104
+
105
+ hold_source_lock() { # <source-id> <ready-file> <release-file>
106
+ local id=$1 ready=$2 release=$3 parent=$$
107
+ FM_HOME="$TMP_ROOT/lock-helper-home" bash -c '
108
+ . "$1/bin/fm-pr-lib.sh"
109
+ . "$1/bin/fm-wake-lib.sh"
110
+ . "$1/bin/fm-procevent-lib.sh"
111
+ fm_procevent_source_lock_acquire "$2" || exit 1
112
+ trap "fm_procevent_source_lock_release \"$2\"" EXIT
113
+ printf "ready\n" > "$3"
114
+ while [ ! -e "$4" ]; do
115
+ kill -0 "$5" 2>/dev/null || exit 0
116
+ sleep 0.02
117
+ done
118
+ ' _ "$ROOT" "$id" "$ready" "$release" "$parent" &
119
+ HOLDER_PID=$!
120
+ }
121
+
122
+ hold_source_lock_then_handle() { # <home> <source-id> <sequence> <ready-file> <release-file>
123
+ local home=$1 id=$2 seq=$3 ready=$4 release=$5 parent=$$
124
+ FM_HOME="$home" bash -c '
125
+ . "$1/bin/fm-pr-lib.sh"
126
+ . "$1/bin/fm-wake-lib.sh"
127
+ . "$1/bin/fm-procevent-lib.sh"
128
+ fm_procevent_source_lock_acquire "$2" || exit 1
129
+ trap "fm_procevent_source_lock_release \"$2\"" EXIT
130
+ printf "ready\n" > "$4"
131
+ while [ ! -e "$5" ]; do
132
+ kill -0 "$6" 2>/dev/null || exit 1
133
+ sleep 0.02
134
+ done
135
+ fm_procevent_mark_handled "$3/state" "$2" "$7"
136
+ ' _ "$ROOT" "$id" "$home" "$ready" "$release" "$parent" "$seq" &
137
+ HOLDER_PID=$!
138
+ }
139
+
140
+ # --- inert with nothing configured ------------------------------------------
141
+ IDLE="$TMP_ROOT/idle"; new_home "$IDLE"
142
+ out=$(pe "$IDLE" list)
143
+ assert_contains "$out" "no sources registered" "an unconfigured home reports no sources"
144
+ out=$(pe "$IDLE" reconcile)
145
+ assert_contains "$out" "published=0 started=0" "reconcile is a no-op with nothing registered"
146
+ [ -z "$(ls -A "$IDLE/state" 2>/dev/null)" ] || fail "an unconfigured home generated state: $(ls -A "$IDLE/state")"
147
+ pass "no configured source means no generated state and no process"
148
+
149
+ sup=$(PATH="${FM_TEST_BASE_PATH:-/usr/bin:/bin:/usr/sbin:/sbin}" bash -c \
150
+ '. "$1/bin/fm-supervision-lib.sh"; fm_supervision_needed "$2" && echo yes || echo no' _ "$ROOT" "$IDLE/state")
151
+ assert_contains "$sup" no "an unconfigured home does not need supervision"
152
+
153
+ # --- a blocking source completes into exactly one normalized event ----------
154
+ H1="$TMP_ROOT/h1"; new_home "$H1"
155
+ TRIG="$TMP_ROOT/trigger-one"
156
+ out=$(pe_register "$H1" lavish src-one -- "$BLOCKER" "$TRIG" "payload one")
157
+ assert_contains "$out" "registered: src-one" "register records a source"
158
+
159
+ sup=$(PATH="${FM_TEST_BASE_PATH:-/usr/bin:/bin:/usr/sbin:/sbin}" bash -c \
160
+ '. "$1/bin/fm-supervision-lib.sh"; fm_supervision_needed "$2" && echo yes || echo no' _ "$ROOT" "$H1/state")
161
+ assert_contains "$sup" yes "a registered source needs supervision with no task metadata"
162
+
163
+ pe "$H1" reconcile >/dev/null
164
+ # Reconcile's replacement runner is detached, so ownership is recorded after
165
+ # reconcile has already returned. Wait for the claim itself: a duplicate start
166
+ # only has an owner to lose to once that claim exists.
167
+ wait_for "$FM_PROCEVENT_CLAIM_ROOT/src-one.claim" || fail "reconcile never claimed the registered source"
168
+ out=$(pe "$H1" start src-one)
169
+ assert_contains "$out" "already owned" "a duplicate start loses instead of running a second child"
170
+
171
+ : > "$TRIG"
172
+ wait_for "$H1/state/.wake-queue" || fail "no event was published after the source completed"
173
+ payload=$(wake_payloads "$H1")
174
+ assert_contains "$payload" "procevent lavish src-one 1" "completion publishes the committed result sequence"
175
+ assert_not_contains "$payload" "payload one" "source output never reaches the event line"
176
+ [ "$(printf '%s\n' "$payload" | grep -c .)" = 1 ] || fail "expected exactly one event, got: $payload"
177
+ pass "one blocking completion yields exactly one bounded normalized event"
178
+
179
+ RESULT=$(first_result "$H1" src-one || true)
180
+ [ -n "$RESULT" ] || fail "no durable result was captured"
181
+ mode=$(PATH="${FM_TEST_BASE_PATH:-/usr/bin:/bin:/usr/sbin:/sbin}" bash -c \
182
+ '. "$1/bin/fm-pr-lib.sh"; fm_pr_file_mode "$2"' _ "$ROOT" "$RESULT")
183
+ assert_contains "$mode" 600 "the captured result is private"
184
+ assert_grep 'payload one' "$RESULT" "the captured result holds the source output verbatim"
185
+ assert_grep 'lavish' "${RESULT%.result}.adapter" "the captured result retains its immutable adapter"
186
+ assert_absent "${RESULT%.result}.handled" "publication alone never marks a result handled"
187
+
188
+ # --- the public start boundary establishes generation group ownership -------
189
+ HPG="$TMP_ROOT/hpg"; new_home "$HPG"
190
+ DIRECT_TRIGGER="$TMP_ROOT/direct-trigger"
191
+ pe_register "$HPG" lavish direct-src -- "$BLOCKER" "$DIRECT_TRIGGER" "direct result" >/dev/null
192
+ pe "$HPG" start direct-src > "$TMP_ROOT/direct-start.out" &
193
+ direct_runner=$!
194
+ wait_for "$FM_PROCEVENT_CLAIM_ROOT/direct-src.claim" || fail "direct start never claimed its source"
195
+ direct_leader=$(sed -n '2p' "$FM_PROCEVENT_CLAIM_ROOT/direct-src.claim")
196
+ direct_group=$(ps -o pgid= -p "$direct_leader" 2>/dev/null | tr -d '[:space:]')
197
+ [ "$direct_group" = "$direct_leader" ] \
198
+ || fail "direct start claimed before leading its process group: pid=$direct_leader pgid=$direct_group"
199
+ : > "$DIRECT_TRIGGER"
200
+ wait "$direct_runner" || fail "direct start failed after its source completed"
201
+ assert_contains "$(cat "$TMP_ROOT/direct-start.out")" "captured:" "direct start captures its result"
202
+ pass "public start owns the process group recorded by its claim"
203
+
204
+ SHARED_TRIGGER="$TMP_ROOT/shared-trigger"
205
+ SHARED_SIBLING="$TMP_ROOT/shared-sibling"
206
+ SHARED_LAUNCHER="$TMP_ROOT/shared-launcher.pl"
207
+ cat > "$SHARED_LAUNCHER" <<'PL'
208
+ use strict;
209
+ use warnings;
210
+ my ($sibling_file, @command) = @ARGV;
211
+ pipe(my $reader, my $writer) or exit 125;
212
+ defined(my $runner = fork) or exit 125;
213
+ if ($runner == 0) {
214
+ close $reader;
215
+ setpgrp(0, 0) or exit 125;
216
+ print {$writer} "ready\n";
217
+ close $writer;
218
+ exec @command;
219
+ exit 125;
220
+ }
221
+ close $writer;
222
+ <$reader>;
223
+ close $reader;
224
+ defined(my $sibling = fork) or exit 125;
225
+ if ($sibling == 0) {
226
+ setpgrp(0, $runner) or exit 125;
227
+ open(my $out, '>', $sibling_file) or exit 125;
228
+ print {$out} "$$\n";
229
+ close $out;
230
+ sleep 30;
231
+ exit 0;
232
+ }
233
+ waitpid($runner, 0);
234
+ waitpid($sibling, 0);
235
+ exit 0;
236
+ PL
237
+ pe_register "$HPG" lavish shared-src -- "$BLOCKER" "$SHARED_TRIGGER" "shared result" >/dev/null
238
+ FM_HOME="$HPG" perl "$SHARED_LAUNCHER" "$SHARED_SIBLING" \
239
+ "$ROOT/bin/fm-procevent.sh" start shared-src > "$TMP_ROOT/shared-start.out" &
240
+ shared_launcher=$!
241
+ wait_for "$SHARED_SIBLING" || fail "shared caller group never started its unrelated sibling"
242
+ wait_for "$FM_PROCEVENT_CLAIM_ROOT/shared-src.claim" || fail "shared-group start never claimed its source"
243
+ shared_sibling=$(cat "$SHARED_SIBLING")
244
+ pe "$HPG" retire shared-src >/dev/null
245
+ kill -0 "$shared_sibling" 2>/dev/null || fail "retirement signaled an unrelated caller-group process"
246
+ kill "$shared_sibling" 2>/dev/null || true
247
+ wait "$shared_launcher" || fail "shared caller-group fixture did not exit cleanly"
248
+ pass "public start never claims an inherited caller process group"
249
+
250
+ # --- an unhandled result remains eligible for re-announcement on restart ----
251
+ # A result is durable but nothing has ever acknowledged handling it. Every
252
+ # reconcile call - not just the first restart after a crash - must keep
253
+ # re-announcing it, because the only thing that stops re-announcement is an
254
+ # explicit handled acknowledgement, never a prior publication.
255
+ H2="$TMP_ROOT/h2"; new_home "$H2"
256
+ future_status=0
257
+ future_out=$(pe "$H2" handled src-cut 7 2>&1) || future_status=$?
258
+ [ "$future_status" -ne 0 ] || fail "handled accepted a generation that has not been captured"
259
+ assert_contains "$future_out" "cannot durably record handling" "premature acknowledgement is rejected through the public interface"
260
+ assert_absent "$H2/state/procevent-inbox/src-cut.7.handled" "premature acknowledgement creates no marker for the future generation"
261
+ mkdir -p "$H2/state/procevent-inbox"
262
+ printf 'stranded result\n' > "$H2/state/procevent-inbox/src-cut.7.result"
263
+ printf 'lavish\n' > "$H2/state/procevent-inbox/src-cut.7.adapter"
264
+ chmod 0600 "$H2/state/procevent-inbox/src-cut.7.result" "$H2/state/procevent-inbox/src-cut.7.adapter"
265
+ out=$(pe "$H2" reconcile)
266
+ assert_contains "$out" "published=1" "a durably captured but unhandled result is announced after restart"
267
+ assert_contains "$(wake_payloads "$H2")" "procevent lavish src-cut 7" "durable adapter identity survives without a registration"
268
+ assert_absent "$H2/state/procevent-inbox/src-cut.7.handled" "recovery alone never marks the recovered result handled"
269
+ mv "$H2/state/.wake-queue" "$H2/state/.wake-queue.drained-1"
270
+ out=$(pe "$H2" reconcile)
271
+ assert_contains "$out" "published=1" "an unhandled result is re-announced on every reconcile, not only the first"
272
+ assert_contains "$(wake_payloads "$H2")" "procevent lavish src-cut 7" "the repeat wake preserves its deduplication identity"
273
+ [ "$(count_results "$H2" src-cut)" = 1 ] || fail "repeat re-announcement created a second durable copy"
274
+ mv "$H2/state/.wake-queue" "$H2/state/.wake-queue.drained-2"
275
+
276
+ ack_out=$(pe "$H2" handled src-cut 7)
277
+ assert_contains "$ack_out" "handled: src-cut 7" "the owned handling interface newly authorizes the first acknowledgement"
278
+ assert_present "$H2/state/procevent-inbox/src-cut.7.handled" "acknowledgement durably records handling"
279
+ before=$(wake_payloads "$H2" | wc -l | tr -d ' ')
280
+ out=$(pe "$H2" reconcile)
281
+ assert_contains "$out" "published=0" "reconcile stops re-announcing once a result is durably handled"
282
+ [ "$(wake_payloads "$H2" | wc -l | tr -d ' ')" = "$before" ] || fail "a handled result was announced again"
283
+
284
+ repeat_out=$(pe "$H2" handled src-cut 7)
285
+ assert_contains "$repeat_out" "already-handled: src-cut 7" "repeated acknowledgement is safe and reports the repeat distinctly"
286
+ case "$repeat_out" in
287
+ handled:*) fail "a repeat acknowledgement re-authorized a second handled effect: $repeat_out" ;;
288
+ esac
289
+ pass "an unhandled result survives restart and repeat drains, and only explicit acknowledgement stops its re-announcement"
290
+
291
+ HRACE="$TMP_ROOT/hrace"; new_home "$HRACE"
292
+ mkdir -p "$HRACE/state/procevent-inbox"
293
+ printf 'racing result\n' > "$HRACE/state/procevent-inbox/racing-src.1.result"
294
+ printf 'lavish\n' > "$HRACE/state/procevent-inbox/racing-src.1.adapter"
295
+ chmod 0600 "$HRACE/state/procevent-inbox/racing-src.1.result" "$HRACE/state/procevent-inbox/racing-src.1.adapter"
296
+ RACE_PUBLISH_READY="$TMP_ROOT/race-publish-ready"
297
+ RACE_PUBLISH_RELEASE="$TMP_ROOT/race-publish-release"
298
+ RACE_RECONCILE_OUT="$TMP_ROOT/race-reconcile.out"
299
+ hold_source_lock_then_handle "$HRACE" racing-src 1 "$RACE_PUBLISH_READY" "$RACE_PUBLISH_RELEASE"
300
+ RACE_HANDLE_PID=$HOLDER_PID
301
+ wait_for "$RACE_PUBLISH_READY" || fail "publication race barrier did not acquire the source lock"
302
+ pe "$HRACE" reconcile > "$RACE_RECONCILE_OUT" &
303
+ RACE_RECONCILE_PID=$!
304
+ sleep 0.3
305
+ assert_absent "$HRACE/state/.wake-queue" "publication bypassed the source serialization boundary"
306
+ : > "$RACE_PUBLISH_RELEASE"
307
+ wait "$RACE_HANDLE_PID" || fail "publication race barrier could not record handling"
308
+ wait "$RACE_RECONCILE_PID" || fail "reconcile failed after the concurrent acknowledgement"
309
+ assert_contains "$(cat "$RACE_RECONCILE_OUT")" "published=0" "reconcile rechecks handling at the serialized publication boundary"
310
+ assert_present "$HRACE/state/procevent-inbox/racing-src.1.handled" "the concurrent acknowledgement remains durable"
311
+ assert_absent "$HRACE/state/.wake-queue" "an acknowledged result was appended after handling completed"
312
+ pass "publication cannot race a handled acknowledgement"
313
+
314
+ HPRIVATE="$TMP_ROOT/hprivate"; new_home "$HPRIVATE"
315
+ mkdir -p "$HPRIVATE/state/procevent-inbox"
316
+ printf 'private result\n' > "$HPRIVATE/state/procevent-inbox/private-src.1.result"
317
+ printf 'lavish\n' > "$HPRIVATE/state/procevent-inbox/private-src.1.adapter"
318
+ chmod 0600 "$HPRIVATE/state/procevent-inbox/private-src.1.result" "$HPRIVATE/state/procevent-inbox/private-src.1.adapter"
319
+ FAIL_CHMOD_BIN="$TMP_ROOT/fail-chmod-bin"
320
+ mkdir -p "$FAIL_CHMOD_BIN"
321
+ cat > "$FAIL_CHMOD_BIN/chmod" <<'SH'
322
+ #!/usr/bin/env bash
323
+ exit 1
324
+ SH
325
+ chmod +x "$FAIL_CHMOD_BIN/chmod"
326
+ private_status=0
327
+ private_out=$(PATH="$FAIL_CHMOD_BIN:$PATH" pe "$HPRIVATE" handled private-src 1 2>&1) || private_status=$?
328
+ [ "$private_status" -ne 0 ] || fail "handled succeeded when private mode enforcement failed"
329
+ assert_contains "$private_out" "cannot durably record handling" "mode enforcement failure is reported through the owned interface"
330
+ assert_absent "$HPRIVATE/state/procevent-inbox/private-src.1.handled" "failed mode enforcement left an authoritative marker"
331
+ private_out=$(umask 000; pe "$HPRIVATE" handled private-src 1)
332
+ assert_contains "$private_out" "handled: private-src 1" "handling succeeds after private mode enforcement recovers"
333
+ private_mode=$(PATH="${FM_TEST_BASE_PATH:-/usr/bin:/bin:/usr/sbin:/sbin}" bash -c \
334
+ '. "$1/bin/fm-pr-lib.sh"; fm_pr_file_mode "$2"' _ "$ROOT" "$HPRIVATE/state/procevent-inbox/private-src.1.handled")
335
+ assert_contains "$private_mode" 600 "the handled marker is private under a permissive caller umask"
336
+ pass "handled acknowledgement creation is private and fails safely"
337
+
338
+ # --- a terminal result retires its source, on the adapter's verdict alone ----
339
+ # The runner must carry no notion of its own about what "done" means for a
340
+ # source. It asks that source's adapter whether the captured result ends the
341
+ # source, and retires the registration only on that adapter's verdict. Two
342
+ # fixture adapters isolate exactly that decision - one that ends on any result,
343
+ # one with no terminal knowledge at all - so the observed behavior is proven to
344
+ # follow the adapter rather than any condition built into the runner.
345
+ ADAPTER_ROOT="$TMP_ROOT/adapter-root"
346
+ mkdir -p "$ADAPTER_ROOT/bin"
347
+ cat > "$ADAPTER_ROOT/bin/fm-procevent-endnow.sh" <<'SH'
348
+ #!/usr/bin/env bash
349
+ # Fixture adapter: every captured result ends this source.
350
+ case "${1-}" in
351
+ terminal) [ -f "${2-}" ] && exit 0 || exit 1 ;;
352
+ esac
353
+ exit 2
354
+ SH
355
+ cat > "$ADAPTER_ROOT/bin/fm-procevent-openended.sh" <<'SH'
356
+ #!/usr/bin/env bash
357
+ # Fixture adapter with no terminal knowledge at all: nothing ever ends it.
358
+ exit 2
359
+ SH
360
+ cat > "$ADAPTER_ROOT/bin/fm-procevent-applying.sh" <<'SH'
361
+ #!/usr/bin/env bash
362
+ case "${1-}" in
363
+ autohandle)
364
+ printf '%s %s\n' "$2" "$3" >> "$FM_HOME/state/applied"
365
+ "$FM_PROCEVENT_UNDER_TEST" handled "$2" "$3" >/dev/null
366
+ ;;
367
+ *) exit 2 ;;
368
+ esac
369
+ SH
370
+ cat > "$ADAPTER_ROOT/bin/fm-procevent-selfann.sh" <<'SH'
371
+ #!/usr/bin/env bash
372
+ # Fixture adapter that declares a durable downstream announcement of its own.
373
+ # FM_HOME/state/selfann-fail makes its application fail so the fallback
374
+ # publication path stays provable.
375
+ case "${1-}" in
376
+ self-announcing) exit 0 ;;
377
+ autohandle)
378
+ [ ! -e "$FM_HOME/state/selfann-fail" ] || exit 1
379
+ printf '%s %s\n' "$2" "$3" >> "$FM_HOME/state/applied"
380
+ "$FM_PROCEVENT_UNDER_TEST" handled "$2" "$3" >/dev/null
381
+ ;;
382
+ *) exit 2 ;;
383
+ esac
384
+ SH
385
+ chmod +x "$ADAPTER_ROOT/bin/fm-procevent-endnow.sh" "$ADAPTER_ROOT/bin/fm-procevent-openended.sh" \
386
+ "$ADAPTER_ROOT/bin/fm-procevent-applying.sh" "$ADAPTER_ROOT/bin/fm-procevent-selfann.sh"
387
+
388
+ pe_adapter() { # <home> <command>...: run the runner against the fixture adapters
389
+ local home=$1
390
+ shift
391
+ FM_ROOT_OVERRIDE="$ADAPTER_ROOT" FM_PROCEVENT_UNDER_TEST="$ROOT/bin/fm-procevent.sh" \
392
+ FM_HOME="$home" "$ROOT/bin/fm-procevent.sh" "$@"
393
+ }
394
+
395
+ HPUBLISH="$TMP_ROOT/hpublish"; new_home "$HPUBLISH"
396
+ PE_TRACKED+=("$HPUBLISH|publish-src")
397
+ pe_adapter "$HPUBLISH" register applying publish-src -- /bin/echo "apply after publish" >/dev/null
398
+ mkdir "$HPUBLISH/state/.wake-queue"
399
+ out=$(pe_adapter "$HPUBLISH" start publish-src 2>&1)
400
+ assert_contains "$out" "not-autohandled: publish-src" "failed publication did not suppress automatic application"
401
+ assert_absent "$HPUBLISH/state/applied" "a result was applied before its wake was durably published"
402
+ assert_absent "$HPUBLISH/state/procevent-inbox/publish-src.1.handled" "a result was acknowledged before its wake was durably published"
403
+ rmdir "$HPUBLISH/state/.wake-queue"
404
+ # This source's child returns instantly, so leaving it registered would have the
405
+ # recovery reconcile below start a detached poll that races every assertion after
406
+ # it for the source claim, the next sequence, and this home's applied record.
407
+ # Re-announcement is proven from the durable inbox alone and needs no
408
+ # registration, so retire it first - the same retire-before-reconcile discipline
409
+ # the blocker-backed sources rely on - and prove no competing poll was started.
410
+ pe_adapter "$HPUBLISH" retire publish-src >/dev/null
411
+ out=$(pe_adapter "$HPUBLISH" reconcile)
412
+ assert_contains "$out" "published=1" "the unpublished capture was not announced on later reconciliation"
413
+ assert_contains "$out" "started=0" "reconcile started an always-ready poll that races the recovery assertions"
414
+ assert_contains "$(wake_payloads "$HPUBLISH")" "procevent applying publish-src 1" "later reconciliation did not deliver the capture to a handler"
415
+ FM_HOME="$HPUBLISH" FM_PROCEVENT_UNDER_TEST="$ROOT/bin/fm-procevent.sh" \
416
+ "$ADAPTER_ROOT/bin/fm-procevent-applying.sh" autohandle publish-src 1 \
417
+ "$HPUBLISH/state/procevent-inbox/publish-src.1.result"
418
+ assert_grep 'publish-src 1' "$HPUBLISH/state/applied" "the handler could not apply the later announcement"
419
+ assert_present "$HPUBLISH/state/procevent-inbox/publish-src.1.handled" "the later handler application was not acknowledged"
420
+ pass "automatic application waits for durable publication and failed publication remains recoverable"
421
+
422
+ # A self-announcing adapter inverts that order on its own declaration: the
423
+ # runner applies first and publishes nothing for a capture the adapter fully
424
+ # applied and acknowledged, because the adapter's own durable downstream
425
+ # channel is the announcement. The declaration never silences a capture the
426
+ # adapter could NOT apply - that one still publishes for the handler.
427
+ HSELF="$TMP_ROOT/hself"; new_home "$HSELF"
428
+ PE_TRACKED+=("$HSELF|self-src")
429
+ pe_adapter "$HSELF" register selfann self-src -- /bin/echo "self announced" >/dev/null
430
+ out=$(pe_adapter "$HSELF" start self-src 2>&1)
431
+ assert_contains "$out" "autohandled: self-src" "the self-announcing adapter did not apply its own capture"
432
+ assert_not_contains "$out" "not-autohandled" "the applied capture was still reported as left for the handler"
433
+ assert_grep 'self-src 1' "$HSELF/state/applied" "the self-announcing capture was not applied"
434
+ assert_present "$HSELF/state/procevent-inbox/self-src.1.handled" "the self-announcing application was not acknowledged"
435
+ if [ -e "$HSELF/state/.wake-queue" ] && grep -q 'procevent selfann self-src 1' "$HSELF/state/.wake-queue"; then
436
+ fail "a fully autohandled self-announcing capture still published a duplicate check wake"
437
+ fi
438
+ # This self-announcing source's child returns instantly, so reconcile would
439
+ # restart it and that detached poll would race the failing-path start below for
440
+ # the source claim - non-deterministically stealing its sequence or the claim
441
+ # itself. Retire it before the re-announcement check so reconcile starts no
442
+ # competing poll, then re-register for the failing-path capture, the same
443
+ # retire-before-reconcile discipline the blocker-backed sources rely on.
444
+ pe_adapter "$HSELF" retire self-src >/dev/null
445
+ out=$(pe_adapter "$HSELF" reconcile)
446
+ assert_contains "$out" "published=0" "reconcile re-announced a capture its adapter already acknowledged"
447
+ assert_contains "$out" "started=0" "reconcile restarted an always-ready acknowledged source and raced the next start"
448
+ pe_adapter "$HSELF" register selfann self-src -- /bin/echo "self announced" >/dev/null
449
+ : > "$HSELF/state/selfann-fail"
450
+ out=$(pe_adapter "$HSELF" start self-src 2>&1)
451
+ assert_contains "$out" "not-autohandled: self-src" "a failed self-announcing application was reported as applied"
452
+ assert_absent "$HSELF/state/procevent-inbox/self-src.2.handled" "a failed self-announcing application was acknowledged anyway"
453
+ assert_contains "$(wake_payloads "$HSELF")" "procevent selfann self-src 2" \
454
+ "a capture the self-announcing adapter could not apply lost its check-wake announcement"
455
+ rm -f "$HSELF/state/selfann-fail"
456
+ pass "a self-announcing adapter applies quietly and still publishes what it could not apply"
457
+
458
+ HTERM="$TMP_ROOT/hterm"; new_home "$HTERM"
459
+ PE_TRACKED+=("$HTERM|ends-src")
460
+ pe_adapter "$HTERM" register endnow ends-src -- /bin/echo "terminal payload" >/dev/null
461
+ out=$(pe_adapter "$HTERM" start ends-src)
462
+ assert_contains "$out" "captured:" "a terminal result is still captured durably"
463
+ assert_contains "$out" "retired: ends-src" "the runner reports the adapter-driven retirement"
464
+ assert_absent "$HTERM/state/procevent/ends-src.source" "an adapter-classified terminal result retires its registration"
465
+ assert_absent "$FM_PROCEVENT_CLAIM_ROOT/ends-src.claim" "terminal retirement releases this runner's own claim"
466
+ assert_contains "$(wake_payloads "$HTERM")" "procevent endnow ends-src 1" "the terminal result is still announced"
467
+ [ "$(count_results "$HTERM" ends-src)" = 1 ] || fail "terminal retirement lost or duplicated the captured result"
468
+ TERMINAL_RESULT=$(first_result "$HTERM" ends-src || true)
469
+ assert_grep 'terminal payload' "$TERMINAL_RESULT" "automatic retirement retains the captured output verbatim"
470
+ out=$(pe_adapter "$HTERM" reconcile)
471
+ assert_contains "$out" "started=0" "a retired terminal source is never restarted"
472
+ assert_contains "$out" "published=1" "an unhandled terminal result is still re-announced until acknowledged"
473
+ [ "$(count_results "$HTERM" ends-src)" = 1 ] || fail "a retired terminal source ran its poll again"
474
+ out=$(pe_adapter "$HTERM" retire ends-src)
475
+ assert_contains "$out" "retired: ends-src" "explicit retirement stays supported and idempotent after automatic retirement"
476
+ ack_out=$(pe_adapter "$HTERM" handled ends-src 1)
477
+ assert_contains "$ack_out" "handled: ends-src 1" "a terminal result is acknowledged through the owned interface"
478
+ out=$(pe_adapter "$HTERM" reconcile)
479
+ assert_contains "$out" "published=0" "an acknowledged terminal result stops being re-announced"
480
+ pass "an adapter-classified terminal result is captured once, announced, and retires its source automatically"
481
+
482
+ HOPEN="$TMP_ROOT/hopen"; new_home "$HOPEN"
483
+ PE_TRACKED+=("$HOPEN|open-src")
484
+ pe_adapter "$HOPEN" register openended open-src -- /bin/echo "open payload" >/dev/null
485
+ out=$(pe_adapter "$HOPEN" start open-src)
486
+ assert_contains "$out" "captured:" "a result from an adapter with no terminal verdict is captured"
487
+ assert_not_contains "$out" "retired:" "an adapter with no terminal verdict never retires its source"
488
+ assert_present "$HOPEN/state/procevent/open-src.source" "a source with no terminal verdict stays armed"
489
+ pe_adapter "$HOPEN" retire open-src >/dev/null
490
+ pass "a source stays armed unless its own adapter classifies the result terminal"
491
+
492
+ HREPLACE="$TMP_ROOT/hreplace"; new_home "$HREPLACE"
493
+ PE_TRACKED+=("$HREPLACE|replace-src")
494
+ OLD_TRIGGER="$TMP_ROOT/replace-old-trigger"
495
+ pe_adapter "$HREPLACE" register endnow replace-src -- "$BLOCKER" "$OLD_TRIGGER" "old terminal payload" >/dev/null
496
+ pe_adapter "$HREPLACE" start replace-src > "$TMP_ROOT/replace-old.out" 2>&1 &
497
+ replace_old_pid=$!
498
+ wait_for "$FM_PROCEVENT_CLAIM_ROOT/replace-src.claim" || fail "the old registration was never claimed"
499
+ pe_adapter "$HREPLACE" register openended replace-src -- /bin/echo "replacement payload" >/dev/null
500
+ touch "$OLD_TRIGGER"
501
+ wait "$replace_old_pid" || fail "the old terminal runner failed"
502
+ assert_contains "$(cat "$TMP_ROOT/replace-old.out")" "cannot retire terminal source" \
503
+ "an old runner refuses to retire a replacement registration"
504
+ assert_present "$HREPLACE/state/procevent/replace-src.source" \
505
+ "a replacement registration survives the old runner's terminal result"
506
+ assert_contains "$(cat "$HREPLACE/state/procevent/replace-src.source")" "adapter=openended" \
507
+ "the surviving registration is the replacement generation"
508
+ out=$(pe_adapter "$HREPLACE" start replace-src)
509
+ assert_contains "$out" "captured:" "the replacement registration remains independently runnable"
510
+ [ "$(count_results "$HREPLACE" replace-src)" = 2 ] \
511
+ || fail "the replacement generation did not capture its own result"
512
+ pe_adapter "$HREPLACE" retire replace-src >/dev/null
513
+ pass "terminal retirement preserves and releases a concurrently replaced registration"
514
+
515
+ HRETFAIL="$TMP_ROOT/hretfail"; new_home "$HRETFAIL"
516
+ PE_TRACKED+=("$HRETFAIL|retire-fail-src")
517
+ FAIL_RM_BIN=$(fm_fakebin "$TMP_ROOT/retire-fail-bin")
518
+ REAL_RM=$(command -v rm)
519
+ export REAL_RM
520
+ cat > "$FAIL_RM_BIN/rm" <<'SH'
521
+ #!/usr/bin/env bash
522
+ for arg in "$@"; do
523
+ case "$arg" in */retire-fail-src.source) exit 1 ;; esac
524
+ done
525
+ exec "$REAL_RM" "$@"
526
+ SH
527
+ chmod +x "$FAIL_RM_BIN/rm"
528
+ pe_adapter "$HRETFAIL" register endnow retire-fail-src -- /bin/echo "one terminal payload" >/dev/null
529
+ out=$(PATH="$FAIL_RM_BIN:$PATH" pe_adapter "$HRETFAIL" start retire-fail-src 2>&1)
530
+ assert_contains "$out" "cannot retire terminal source" "a failed registration removal is reported"
531
+ assert_present "$HRETFAIL/state/procevent/retire-fail-src.source" \
532
+ "failed retirement preserves the exact registration"
533
+ assert_present "$FM_PROCEVENT_CLAIM_ROOT/retire-fail-src.claim" \
534
+ "failed retirement preserves its terminal ownership claim"
535
+ out=$(PATH="$FAIL_RM_BIN:$PATH" pe_adapter "$HRETFAIL" reconcile)
536
+ assert_contains "$out" "started=0" "failed terminal retirement never restarts the poll"
537
+ [ "$(count_results "$HRETFAIL" retire-fail-src)" = 1 ] \
538
+ || fail "failed retirement allowed recurring terminal capture"
539
+ pe_adapter "$HRETFAIL" reconcile >/dev/null
540
+ assert_absent "$HRETFAIL/state/procevent/retire-fail-src.source" \
541
+ "repeated retirement removes the same registration once removal recovers"
542
+ assert_absent "$FM_PROCEVENT_CLAIM_ROOT/retire-fail-src.claim" \
543
+ "the claim releases only after that registration is removed"
544
+ [ "$(count_results "$HRETFAIL" retire-fail-src)" = 1 ] \
545
+ || fail "retirement recovery reran the terminal source"
546
+ pass "failed terminal retirement is fail-closed and idempotently recoverable"
547
+
548
+ # --- end-user-aligned regression: one Send & End, one captured result -------
549
+ # The dogfood defect: a real armed Lavish source received one human `Send & End`
550
+ # action, and the runner captured four results - the human's real feedback, then
551
+ # recurring empty ended sessions - because it kept restarting a source whose own
552
+ # adapter already knew the session had ended. Driven through the adapter's own
553
+ # arm command against a stand-in for the published poll shape, so registration,
554
+ # the runner, capture, publication, and retirement all run for real.
555
+ HLT="$TMP_ROOT/hlt"; new_home "$HLT"
556
+ LAVISH_BIN=$(fm_fakebin "$TMP_ROOT/lavish-stub")
557
+ LAVISH_POLL_COUNT="$TMP_ROOT/lavish-poll-count"
558
+ export LAVISH_POLL_COUNT
559
+ cat > "$LAVISH_BIN/lavish-axi" <<'SH'
560
+ #!/usr/bin/env bash
561
+ # Stand-in for `lavish-axi poll <file>` around a human `Send & End`: the final
562
+ # feedback is delivered exactly once carrying session_ended, and every later
563
+ # poll returns an empty ended session immediately.
564
+ n=$(cat "$LAVISH_POLL_COUNT" 2>/dev/null || echo 0)
565
+ n=$((n + 1))
566
+ printf '%s\n' "$n" > "$LAVISH_POLL_COUNT"
567
+ if [ "$n" = 1 ]; then
568
+ printf 'session:\n file: /review.html\n status: feedback\n session_ended: true\n ended_by: user\nfeedback[1]{text}:\n ship it\n'
569
+ else
570
+ printf 'session:\n file: /review.html\n status: ended\n ended_by: user\n'
571
+ fi
572
+ SH
573
+ chmod +x "$LAVISH_BIN/lavish-axi"
574
+ REVIEW_ART="$TMP_ROOT/review.html"
575
+ printf '<h1>review</h1>\n' > "$REVIEW_ART"
576
+ lavish_id=$("$ROOT/bin/fm-procevent-lavish.sh" source-id "$REVIEW_ART")
577
+ PE_TRACKED+=("$HLT|$lavish_id")
578
+ PATH="$LAVISH_BIN:$PATH" FM_HOME="$HLT" "$ROOT/bin/fm-procevent-lavish.sh" arm "$REVIEW_ART" >/dev/null
579
+ for _ in $(seq 1 6); do
580
+ PATH="$LAVISH_BIN:$PATH" pe "$HLT" reconcile >/dev/null
581
+ sleep 0.3
582
+ done
583
+ [ "$(cat "$LAVISH_POLL_COUNT")" = 1 ] \
584
+ || fail "an ended review kept being polled: $(cat "$LAVISH_POLL_COUNT") polls for one Send & End"
585
+ [ "$(count_results "$HLT" "$lavish_id")" = 1 ] \
586
+ || fail "one Send & End produced $(count_results "$HLT" "$lavish_id") captured results"
587
+ [ "$(wake_payloads "$HLT" | sort -u | grep -c .)" = 1 ] \
588
+ || fail "one Send & End produced more than one distinct event: $(wake_payloads "$HLT" | sort -u)"
589
+ assert_contains "$(wake_payloads "$HLT")" "procevent lavish $lavish_id 1" "the human's final feedback is announced"
590
+ assert_absent "$HLT/state/procevent/$lavish_id.source" "the ended review source retires automatically"
591
+ assert_absent "$FM_PROCEVENT_CLAIM_ROOT/$lavish_id.claim" "the ended review releases its owned claim"
592
+ LAVISH_RESULT=$(first_result "$HLT" "$lavish_id" || true)
593
+ assert_grep 'ship it' "$LAVISH_RESULT" "automatic retirement retains the human's final feedback"
594
+ out=$(PATH="$LAVISH_BIN:$PATH" FM_HOME="$HLT" "$ROOT/bin/fm-procevent-lavish.sh" retire "$REVIEW_ART")
595
+ assert_contains "$out" "retired: $lavish_id" "explicit adapter retirement stays supported after automatic retirement"
596
+ pass "one Send & End yields exactly one captured result, automatic retirement, and no recurring poll"
597
+
598
+ # --- end-user-aligned regression: an empty board close is not news ------------
599
+ # The captain's report: closing a review surface he had said nothing on still
600
+ # put a wake in his chat whose entire content was that nothing happened. The
601
+ # adapter now answers the runner's silence seam for exactly that shape, so the
602
+ # result is captured and recorded handled without ever being announced. Driven
603
+ # through the adapter's own arm command and the real runner, so registration,
604
+ # capture, the silence verdict, and retirement all run for real.
605
+ HEMPTY="$TMP_ROOT/hempty"; new_home "$HEMPTY"
606
+ EMPTY_BIN=$(fm_fakebin "$TMP_ROOT/lavish-empty-stub")
607
+ cat > "$EMPTY_BIN/lavish-axi" <<'SH'
608
+ #!/usr/bin/env bash
609
+ # Stand-in for `lavish-axi poll <file>` when the captain closes a board he said
610
+ # nothing on: an ended session carrying no queued content at all.
611
+ printf 'session:\n file: /quiet.html\n status: ended\n ended_by: user\n'
612
+ SH
613
+ chmod +x "$EMPTY_BIN/lavish-axi"
614
+ QUIET_ART="$TMP_ROOT/quiet-board.html"
615
+ printf '<h1>quiet</h1>\n' > "$QUIET_ART"
616
+ quiet_id=$("$ROOT/bin/fm-procevent-lavish.sh" source-id "$QUIET_ART")
617
+ PE_TRACKED+=("$HEMPTY|$quiet_id")
618
+ PATH="$EMPTY_BIN:$PATH" FM_HOME="$HEMPTY" \
619
+ "$ROOT/bin/fm-procevent-lavish.sh" arm "$QUIET_ART" >/dev/null
620
+ quiet_out=$(PATH="$EMPTY_BIN:$PATH" pe "$HEMPTY" start "$quiet_id" 2>&1)
621
+ assert_not_contains "$quiet_out" "not-autohandled" \
622
+ "a durably silenced result was reported as still unacknowledged"
623
+ # The handled marker is written at exactly the point the wake would otherwise
624
+ # have been appended, so waiting on it - rather than on a fixed sleep - is what
625
+ # makes "no wake" a real observation instead of a race the test won by being
626
+ # early.
627
+ QUIET_HANDLED="$HEMPTY/state/procevent-inbox/$quiet_id.1.handled"
628
+ for _ in $(seq 1 100); do
629
+ [ -f "$QUIET_HANDLED" ] && break
630
+ sleep 0.1
631
+ done
632
+ [ -f "$QUIET_HANDLED" ] \
633
+ || fail "a silenced result was not durably recorded handled, so a later reconcile would announce it"
634
+ [ "$(count_results "$HEMPTY" "$quiet_id")" = 1 ] \
635
+ || fail "an empty board close captured $(count_results "$HEMPTY" "$quiet_id") results instead of one"
636
+ [ -z "$(wake_payloads "$HEMPTY")" ] \
637
+ || fail "an empty board close woke the captain: $(wake_payloads "$HEMPTY")"
638
+ # Re-announcement is exactly what the handled marker exists to stop, so the
639
+ # silence has to survive the reconcile that would otherwise republish it.
640
+ PATH="$EMPTY_BIN:$PATH" pe "$HEMPTY" reconcile >/dev/null
641
+ sleep 0.3
642
+ [ -z "$(wake_payloads "$HEMPTY")" ] \
643
+ || fail "a later reconcile re-announced a silenced empty board close: $(wake_payloads "$HEMPTY")"
644
+ assert_absent "$HEMPTY/state/procevent/$quiet_id.source" \
645
+ "an empty board close still retires its ended source"
646
+ pass "an empty board close is captured and recorded handled without ever waking the captain"
647
+
648
+ # The other half of the same contract, on the same real path: a close that
649
+ # carries what the captain actually said must still reach him. Same runner, same
650
+ # adapter, one different response shape.
651
+ HANSWER="$TMP_ROOT/hanswer"; new_home "$HANSWER"
652
+ ANSWER_BIN=$(fm_fakebin "$TMP_ROOT/lavish-answer-stub")
653
+ cat > "$ANSWER_BIN/lavish-axi" <<'SH'
654
+ #!/usr/bin/env bash
655
+ # Stand-in for `lavish-axi poll <file>` on a real `Send & End`: the captain's
656
+ # own choice, delivered with session_ended.
657
+ printf 'session:\n file: /answered.html\n status: feedback\n session_ended: true\n ended_by: user\nprompts[1]{tag,text,prompt}:\n "choice","Option B","Context data: {\\"question\\":\\"noop-check-routing\\",\\"answer\\":\\"b\\"}"\n'
658
+ SH
659
+ chmod +x "$ANSWER_BIN/lavish-axi"
660
+ ANSWER_ART="$TMP_ROOT/answered-board.html"
661
+ printf '<h1>answered</h1>\n' > "$ANSWER_ART"
662
+ answer_id=$("$ROOT/bin/fm-procevent-lavish.sh" source-id "$ANSWER_ART")
663
+ PE_TRACKED+=("$HANSWER|$answer_id")
664
+ PATH="$ANSWER_BIN:$PATH" FM_HOME="$HANSWER" \
665
+ "$ROOT/bin/fm-procevent-lavish.sh" arm "$ANSWER_ART" >/dev/null
666
+ PATH="$ANSWER_BIN:$PATH" pe "$HANSWER" reconcile >/dev/null
667
+ wait_for "$HANSWER/state/.wake-queue" \
668
+ || fail "a board close carrying the captain's real answer produced no wake"
669
+ assert_contains "$(wake_payloads "$HANSWER")" "procevent lavish $answer_id 1" \
670
+ "a real board answer still reaches the captain"
671
+ [ ! -f "$HANSWER/state/procevent-inbox/$answer_id.1.handled" ] \
672
+ || fail "a real board answer was recorded handled without ever being handled"
673
+ pass "a board close carrying the captain's real answer is still announced"
674
+
675
+ # --- end-user-aligned regression: a transient poll interruption is not news ---
676
+ # The dogfood defect: a live board listener can answer with exactly
677
+ # error: Lavish Editor poll response was interrupted
678
+ # code: SERVER_ERROR
679
+ # while the board's marks remain available. Firstmate registered raw poll output,
680
+ # so the generic runner captured that transient response and woke the whole fleet
681
+ # over what is really an internal retry. Every scenario below runs through the
682
+ # adapter's own arm command and the real runner, so registration, capture, and
683
+ # publication are exercised for real.
684
+ LAVISH_SCRIPTED_BIN=$(fm_fakebin "$TMP_ROOT/lavish-scripted-stub")
685
+ cat > "$LAVISH_SCRIPTED_BIN/lavish-axi" <<'SH'
686
+ #!/usr/bin/env bash
687
+ # Stand-in for `lavish-axi poll <file>`, scripted per scenario: LAVISH_SCRIPT
688
+ # names the response for each successive poll, one word per poll, and its last
689
+ # word repeats forever. `interrupt` is the exact transient response the server
690
+ # returns while the board's marks stay available.
691
+ n=$(cat "$LAVISH_COUNT" 2>/dev/null || echo 0)
692
+ n=$((n + 1))
693
+ printf '%s\n' "$n" > "$LAVISH_COUNT"
694
+ read -r -a plan <<< "$LAVISH_SCRIPT"
695
+ i=$((n - 1))
696
+ [ "$i" -ge "${#plan[@]}" ] && i=$((${#plan[@]} - 1))
697
+ case "${plan[$i]}" in
698
+ interrupt)
699
+ printf 'error: Lavish Editor poll response was interrupted\ncode: SERVER_ERROR\n'; exit 1 ;;
700
+ near-interrupt)
701
+ printf 'error: Lavish Editor poll response was interrupted \ncode: SERVER_ERROR\n'; exit 1 ;;
702
+ other-server-error)
703
+ printf 'error: Lavish Editor session store is unavailable\ncode: SERVER_ERROR\n'; exit 1 ;;
704
+ feedback)
705
+ printf 'session:\n file: /board.html\n status: feedback\n session_ended: true\n ended_by: user\nfeedback[1]{text}:\n ship it\n' ;;
706
+ stream)
707
+ printf 'x%.0s' {1..4096}
708
+ printf 'ready\n' > "$LAVISH_STREAM_READY"
709
+ while [ ! -e "$LAVISH_STREAM_RELEASE" ]; do sleep 0.05; done
710
+ printf '\n' ;;
711
+ esac
712
+ SH
713
+ chmod +x "$LAVISH_SCRIPTED_BIN/lavish-axi"
714
+ export LAVISH_COUNT LAVISH_SCRIPT
715
+ # A bounded test override keeps the retry policy's real bound under test without
716
+ # making the suite wait out the production delay.
717
+ export FM_LAVISH_POLL_RETRY_DELAY=0
718
+
719
+ # Two interruptions, then the captain's real feedback: the retries are silent and
720
+ # only the feedback becomes a captured result and a check wake.
721
+ HRETRY="$TMP_ROOT/hretry"; new_home "$HRETRY"
722
+ RETRY_ART="$TMP_ROOT/retry-board.html"
723
+ printf '<h1>retry</h1>\n' > "$RETRY_ART"
724
+ retry_id=$("$ROOT/bin/fm-procevent-lavish.sh" source-id "$RETRY_ART")
725
+ PE_TRACKED+=("$HRETRY|$retry_id")
726
+ LAVISH_COUNT="$TMP_ROOT/retry-count"; LAVISH_SCRIPT="interrupt interrupt feedback"
727
+ PATH="$LAVISH_SCRIPTED_BIN:$PATH" FM_HOME="$HRETRY" \
728
+ "$ROOT/bin/fm-procevent-lavish.sh" arm "$RETRY_ART" >/dev/null
729
+ PATH="$LAVISH_SCRIPTED_BIN:$PATH" pe "$HRETRY" reconcile >/dev/null
730
+ wait_for "$HRETRY/state/.wake-queue" || fail "feedback after interrupted polls produced no wake"
731
+ [ "$(cat "$LAVISH_COUNT")" = 3 ] \
732
+ || fail "the interrupted listener was polled $(cat "$LAVISH_COUNT") times, not the two quiet retries plus the delivering poll"
733
+ [ "$(count_results "$HRETRY" "$retry_id")" = 1 ] \
734
+ || fail "a retried interruption produced $(count_results "$HRETRY" "$retry_id") captured results instead of one"
735
+ [ "$(wake_payloads "$HRETRY" | sort -u | grep -c .)" = 1 ] \
736
+ || fail "a retried interruption woke the fleet: $(wake_payloads "$HRETRY" | sort -u)"
737
+ assert_contains "$(wake_payloads "$HRETRY")" "procevent lavish $retry_id 1" \
738
+ "feedback arriving after quiet retries is captured and announced"
739
+ assert_grep 'ship it' "$(first_result "$HRETRY" "$retry_id")" \
740
+ "the announced result is the captain's feedback, not the interruption"
741
+ pass "a transient Lavish poll interruption is retried quietly and never announced"
742
+
743
+ # Exhaustion is news: after the bounded retries the same exact response is
744
+ # captured and announced normally rather than being swallowed forever.
745
+ HEXH="$TMP_ROOT/hexh"; new_home "$HEXH"
746
+ EXH_ART="$TMP_ROOT/exhaust-board.html"
747
+ printf '<h1>exhaust</h1>\n' > "$EXH_ART"
748
+ exh_id=$("$ROOT/bin/fm-procevent-lavish.sh" source-id "$EXH_ART")
749
+ PE_TRACKED+=("$HEXH|$exh_id")
750
+ LAVISH_COUNT="$TMP_ROOT/exhaust-count"; LAVISH_SCRIPT="interrupt"
751
+ PATH="$LAVISH_SCRIPTED_BIN:$PATH" FM_HOME="$HEXH" \
752
+ "$ROOT/bin/fm-procevent-lavish.sh" arm "$EXH_ART" >/dev/null
753
+ PATH="$LAVISH_SCRIPTED_BIN:$PATH" pe "$HEXH" start "$exh_id" >/dev/null
754
+ [ "$(cat "$LAVISH_COUNT")" = 13 ] \
755
+ || fail "the retry bound polled $(cat "$LAVISH_COUNT") times, not the first poll plus 12 bounded retries"
756
+ [ "$(count_results "$HEXH" "$exh_id")" = 1 ] \
757
+ || fail "exhaustion produced $(count_results "$HEXH" "$exh_id") captured results instead of one"
758
+ assert_contains "$(wake_payloads "$HEXH")" "procevent lavish $exh_id 1" \
759
+ "the interruption that survives the bound is announced normally"
760
+ assert_grep 'poll response was interrupted' "$(first_result "$HEXH" "$exh_id")" \
761
+ "the announced result is the exact interruption the server returned"
762
+ PATH="$LAVISH_SCRIPTED_BIN:$PATH" FM_HOME="$HEXH" \
763
+ "$ROOT/bin/fm-procevent-lavish.sh" retire "$EXH_ART" >/dev/null
764
+ pass "an interruption that outlives the bounded retries is captured and announced"
765
+
766
+ # A different SERVER_ERROR is a genuine error, never a retry: no fail-open drift
767
+ # from the one exact transient response this adapter owns.
768
+ HOTHER="$TMP_ROOT/hother"; new_home "$HOTHER"
769
+ OTHER_ART="$TMP_ROOT/other-board.html"
770
+ printf '<h1>other</h1>\n' > "$OTHER_ART"
771
+ other_id=$("$ROOT/bin/fm-procevent-lavish.sh" source-id "$OTHER_ART")
772
+ PE_TRACKED+=("$HOTHER|$other_id")
773
+ LAVISH_COUNT="$TMP_ROOT/other-count"; LAVISH_SCRIPT="other-server-error"
774
+ PATH="$LAVISH_SCRIPTED_BIN:$PATH" FM_HOME="$HOTHER" \
775
+ "$ROOT/bin/fm-procevent-lavish.sh" arm "$OTHER_ART" >/dev/null
776
+ PATH="$LAVISH_SCRIPTED_BIN:$PATH" pe "$HOTHER" start "$other_id" >/dev/null
777
+ [ "$(cat "$LAVISH_COUNT")" = 1 ] \
778
+ || fail "an unrelated SERVER_ERROR was retried $(cat "$LAVISH_COUNT") times instead of surfacing at once"
779
+ assert_contains "$(wake_payloads "$HOTHER")" "procevent lavish $other_id 1" \
780
+ "an unrelated SERVER_ERROR is captured and announced immediately"
781
+ PATH="$LAVISH_SCRIPTED_BIN:$PATH" FM_HOME="$HOTHER" \
782
+ "$ROOT/bin/fm-procevent-lavish.sh" retire "$OTHER_ART" >/dev/null
783
+ pass "only the exact interruption is retried; an unrelated SERVER_ERROR still surfaces"
784
+ unset FM_LAVISH_POLL_RETRY_DELAY
785
+
786
+ # A whitespace variant is not the exact transient response and must surface on
787
+ # the first poll instead of drifting into the quiet retry policy.
788
+ HNEAR="$TMP_ROOT/hnear"; new_home "$HNEAR"
789
+ NEAR_ART="$TMP_ROOT/near-board.html"
790
+ printf '<h1>near</h1>\n' > "$NEAR_ART"
791
+ near_id=$("$ROOT/bin/fm-procevent-lavish.sh" source-id "$NEAR_ART")
792
+ PE_TRACKED+=("$HNEAR|$near_id")
793
+ LAVISH_COUNT="$TMP_ROOT/near-count"; LAVISH_SCRIPT="near-interrupt feedback"
794
+ PATH="$LAVISH_SCRIPTED_BIN:$PATH" FM_HOME="$HNEAR" FM_LAVISH_POLL_RETRY_DELAY=0 \
795
+ "$ROOT/bin/fm-procevent-lavish.sh" arm "$NEAR_ART" >/dev/null
796
+ PATH="$LAVISH_SCRIPTED_BIN:$PATH" FM_HOME="$HNEAR" pe "$HNEAR" start "$near_id" >/dev/null
797
+ [ "$(cat "$LAVISH_COUNT")" = 1 ] \
798
+ || fail "a near-match interruption was retried instead of surfacing on its first poll"
799
+ assert_contains "$(wake_payloads "$HNEAR")" "procevent lavish $near_id 1" \
800
+ "a whitespace variant of the interruption is captured and announced immediately"
801
+ PATH="$LAVISH_SCRIPTED_BIN:$PATH" FM_HOME="$HNEAR" \
802
+ "$ROOT/bin/fm-procevent-lavish.sh" retire "$NEAR_ART" >/dev/null
803
+ pass "only the literal two-line interruption enters the quiet retry policy"
804
+
805
+ # The public arm boundary refuses invalid retry intervals before it publishes a
806
+ # source registration, rather than arming a listener that can only fail later.
807
+ HINVALID="$TMP_ROOT/hinvalid"; new_home "$HINVALID"
808
+ INVALID_ART="$TMP_ROOT/invalid-delay-board.html"
809
+ printf '<h1>invalid delay</h1>\n' > "$INVALID_ART"
810
+ invalid_id=$("$ROOT/bin/fm-procevent-lavish.sh" source-id "$INVALID_ART")
811
+ for invalid_delay in 61 invalid; do
812
+ invalid_status=0
813
+ invalid_out=$(PATH="$LAVISH_SCRIPTED_BIN:$PATH" FM_HOME="$HINVALID" \
814
+ FM_LAVISH_POLL_RETRY_DELAY="$invalid_delay" \
815
+ "$ROOT/bin/fm-procevent-lavish.sh" arm "$INVALID_ART" 2>&1) || invalid_status=$?
816
+ [ "$invalid_status" -ne 0 ] \
817
+ || fail "arm accepted invalid retry delay: $invalid_delay"
818
+ assert_contains "$invalid_out" "must be whole seconds from 0 to 60" \
819
+ "arm explains the rejected retry delay"
820
+ assert_absent "$HINVALID/state/procevent/$invalid_id.source" \
821
+ "arm publishes no source registration for an invalid retry delay"
822
+ done
823
+ pass "arm rejects malformed and out-of-range retry delays before registration"
824
+
825
+ # Shell-safe cleanup must preserve a valid TMPDIR containing an apostrophe.
826
+ QUOTED_TMPDIR="$TMP_ROOT/poll's-stage"
827
+ mkdir -p "$QUOTED_TMPDIR"
828
+ LAVISH_COUNT="$TMP_ROOT/quoted-count"; LAVISH_SCRIPT="feedback"
829
+ PATH="$LAVISH_SCRIPTED_BIN:$PATH" TMPDIR="$QUOTED_TMPDIR" \
830
+ "$ROOT/bin/fm-procevent-lavish.sh" poll "$NEAR_ART" >/dev/null
831
+ quoted_staged=("$QUOTED_TMPDIR"/fm-lavish-poll.*)
832
+ [ ! -e "${quoted_staged[0]}" ] \
833
+ || fail "poll left its staged response behind in an apostrophe-containing TMPDIR"
834
+ pass "poll cleanup safely handles an apostrophe-containing TMPDIR"
835
+
836
+ HSTREAM="$TMP_ROOT/hstream"; new_home "$HSTREAM"
837
+ STREAM_ART="$TMP_ROOT/stream-board.html"
838
+ STREAM_TMPDIR="$TMP_ROOT/stream-stage"
839
+ LAVISH_STREAM_READY="$TMP_ROOT/stream-ready"
840
+ LAVISH_STREAM_RELEASE="$TMP_ROOT/stream-release"
841
+ mkdir -p "$STREAM_TMPDIR"
842
+ printf '<h1>stream</h1>\n' > "$STREAM_ART"
843
+ stream_id=$("$ROOT/bin/fm-procevent-lavish.sh" source-id "$STREAM_ART")
844
+ PE_TRACKED+=("$HSTREAM|$stream_id")
845
+ LAVISH_COUNT="$TMP_ROOT/stream-count"; LAVISH_SCRIPT="stream"
846
+ PATH="$LAVISH_SCRIPTED_BIN:$PATH" FM_HOME="$HSTREAM" \
847
+ "$ROOT/bin/fm-procevent-lavish.sh" arm "$STREAM_ART" >/dev/null
848
+ PATH="$LAVISH_SCRIPTED_BIN:$PATH" TMPDIR="$STREAM_TMPDIR" \
849
+ LAVISH_STREAM_READY="$LAVISH_STREAM_READY" LAVISH_STREAM_RELEASE="$LAVISH_STREAM_RELEASE" \
850
+ FM_PROCEVENT_MAX_OUTPUT_BYTES=100 pe "$HSTREAM" reconcile >/dev/null
851
+ wait_for "$LAVISH_STREAM_READY" || fail "streaming poll did not start"
852
+ stream_staged=("$STREAM_TMPDIR"/fm-lavish-poll.*)
853
+ [ -e "${stream_staged[0]}" ] || fail "streaming poll created no classifier staging file"
854
+ [ "$(wc -c < "${stream_staged[0]}" | tr -d ' ')" -le 100 ] \
855
+ || fail "streaming poll exceeded its bounded classifier staging"
856
+ : > "$LAVISH_STREAM_RELEASE"
857
+ wait_for "$HSTREAM/state/.wake-queue" || fail "streaming poll produced no wake"
858
+ stream_result=$(first_result "$HSTREAM" "$stream_id" || true)
859
+ [ "$(wc -c < "$stream_result" | tr -d ' ')" -le 100 ] \
860
+ || fail "streaming poll bypassed the runner output bound"
861
+ PATH="$LAVISH_SCRIPTED_BIN:$PATH" FM_HOME="$HSTREAM" \
862
+ "$ROOT/bin/fm-procevent-lavish.sh" retire "$STREAM_ART" >/dev/null
863
+ pass "Lavish classification staging stays bounded while nonmatches stream"
864
+
865
+ # --- end-user-aligned regression: the exact drain-before-handling restart cut
866
+ # Reproduces the confirmed defect through the public interface end to end: a
867
+ # real blocking source completes, its result is captured and published, the
868
+ # wake is drained without any handling, a replacement session's reconcile must
869
+ # resurface the exact same source and sequence, and only the owned handling
870
+ # interface may retire it - safely and without ever authorizing a paired
871
+ # effect a second time.
872
+ HW="$TMP_ROOT/hw"; new_home "$HW"
873
+ TRIGW="$TMP_ROOT/trigger-restart-cut"
874
+ pe_register "$HW" lavish restart-cut-src -- "$BLOCKER" "$TRIGW" "restart cut payload" >/dev/null
875
+ pe "$HW" reconcile >/dev/null
876
+ sleep 0.5
877
+ : > "$TRIGW"
878
+ wait_for "$HW/state/.wake-queue" || fail "the restart-cut source published no event"
879
+ assert_contains "$(wake_payloads "$HW")" "procevent lavish restart-cut-src 1" \
880
+ "capture and publish reaches the wake queue before any handling"
881
+
882
+ # Retire the registration now that the source has completed and captured its
883
+ # one result. The fixture's trigger file persists on disk, so a still-armed
884
+ # registration would let every further reconcile call restart the blocker and
885
+ # capture a fresh generation; retiring leaves only the durable inbox and wake
886
+ # state under test, matching the exact restart cut - the source side is done,
887
+ # only the handling side is still open.
888
+ pe "$HW" retire restart-cut-src >/dev/null
889
+
890
+ # Drain the wake without handling it: the end-user experience of a session
891
+ # reading the wake queue at turn end without yet acting on this specific line.
892
+ mv "$HW/state/.wake-queue" "$HW/state/.wake-queue.drained-unhandled"
893
+ [ -z "$(wake_payloads "$HW")" ] || fail "the wake queue was not actually drained"
894
+
895
+ # Simulate a replacement Firstmate session: reconcile runs cold, as it would on
896
+ # a fresh process with no memory of the prior turn.
897
+ out=$(pe "$HW" reconcile)
898
+ assert_contains "$out" "published=1" \
899
+ "a replacement session's reconcile resurfaces a drained-but-unhandled result"
900
+ assert_contains "$(wake_payloads "$HW")" "procevent lavish restart-cut-src 1" \
901
+ "the exact same captured source and sequence resurfaces, never a substitute"
902
+
903
+ # Acknowledge handling through the owned interface.
904
+ ack_out=$(pe "$HW" handled restart-cut-src 1)
905
+ assert_contains "$ack_out" "handled: restart-cut-src 1" \
906
+ "the first acknowledgement newly authorizes the paired effect"
907
+
908
+ mv "$HW/state/.wake-queue" "$HW/state/.wake-queue.post-handle"
909
+ out=$(pe "$HW" reconcile)
910
+ assert_contains "$out" "published=0" \
911
+ "a later reconcile does not resurface a result once it is durably handled"
912
+ [ -z "$(wake_payloads "$HW")" ] || fail "a handled result was announced again: $(wake_payloads "$HW")"
913
+
914
+ auth_count=0
915
+ for _ in 1 2 3; do
916
+ repeat_ack=$(pe "$HW" handled restart-cut-src 1)
917
+ assert_contains "$repeat_ack" "already-handled: restart-cut-src 1" "repeated acknowledgement stays safe and idempotent"
918
+ case "$repeat_ack" in handled:*) auth_count=$((auth_count + 1)) ;; esac
919
+ done
920
+ [ "$auth_count" -eq 0 ] || fail "a result already durably handled was authorized again: count=$auth_count"
921
+ pass "a drained-but-unhandled result survives a replacement session and is retired only by explicit handling, never twice"
922
+
923
+ HP="$TMP_ROOT/hp"; new_home "$HP"
924
+ mkdir -p "$HP/state/procevent-inbox"
925
+ for seq in 10 2 1; do
926
+ printf '%s\n' "$seq" > "$HP/state/procevent-inbox/ordered-src.$seq.result"
927
+ printf 'lavish\n' > "$HP/state/procevent-inbox/ordered-src.$seq.adapter"
928
+ chmod 0600 "$HP/state/procevent-inbox/ordered-src.$seq.result" "$HP/state/procevent-inbox/ordered-src.$seq.adapter"
929
+ done
930
+ pending=$(bash -c '. "$1/bin/fm-procevent-lib.sh"; fm_procevent_pending "$2"' _ "$ROOT" "$HP/state")
931
+ expected=$(printf '%s\n' \
932
+ "$HP/state/procevent-inbox/ordered-src.1.result" \
933
+ "$HP/state/procevent-inbox/ordered-src.2.result" \
934
+ "$HP/state/procevent-inbox/ordered-src.10.result")
935
+ [ "$pending" = "$expected" ] || fail "pending results were not emitted in numeric sequence order: $pending"
936
+ pe "$HP" reconcile >/dev/null
937
+ deduped=$(FM_HOME="$HP" bash -c '
938
+ . "$1/bin/fm-wake-lib.sh"
939
+ fm_wake_print_deduped "$2/state/.wake-queue" | awk -F "\t" "{print \$5}"
940
+ ' _ "$ROOT" "$HP")
941
+ expected=$(printf '%s\n' \
942
+ 'check: procevent lavish ordered-src 1' \
943
+ 'check: procevent lavish ordered-src 2' \
944
+ 'check: procevent lavish ordered-src 10')
945
+ [ "$deduped" = "$expected" ] || fail "distinct result generations were coalesced or reordered: $deduped"
946
+ pass "pending results preserve numeric order and distinct wake identity"
947
+
948
+ # --- two homes cannot both own one canonical source -------------------------
949
+ HA="$TMP_ROOT/ha"; HB="$TMP_ROOT/hb"; new_home "$HA"; new_home "$HB"
950
+ TRIG2="$TMP_ROOT/trigger-two"
951
+ pe_register "$HA" lavish shared-src -- "$BLOCKER" "$TRIG2" "shared" >/dev/null
952
+ pe_register "$HB" lavish shared-src -- "$BLOCKER" "$TRIG2" "shared" >/dev/null
953
+ pe "$HA" reconcile >/dev/null
954
+ sleep 0.5
955
+ out=$(pe "$HB" start shared-src)
956
+ assert_contains "$out" "already owned" "a second home cannot own a source another home already owns"
957
+ [ -z "$(wake_payloads "$HB")" ] || fail "the losing home published an event"
958
+ pass "one owner per canonical source across homes"
959
+
960
+ # A source whose child never completes must not survive retirement. This is the
961
+ # leak that reparented four orphaned runners: the fixture directory was removed
962
+ # while the detached child kept blocking, with nothing left to reap it.
963
+ runner_pid=$(sed -n '2p' "$FM_PROCEVENT_CLAIM_ROOT/shared-src.claim" 2>/dev/null)
964
+ [ -n "$runner_pid" ] || fail "no runner pid recorded for the blocked source"
965
+ kill -0 "$runner_pid" 2>/dev/null || fail "the blocked runner is not live before retirement"
966
+ pe "$HA" retire shared-src >/dev/null
967
+ for _ in $(seq 1 40); do kill -0 "$runner_pid" 2>/dev/null || break; sleep 0.1; done
968
+ kill -0 "$runner_pid" 2>/dev/null && fail "retire left the blocked runner alive"
969
+ assert_absent "$FM_PROCEVENT_CLAIM_ROOT/shared-src.claim" "retire releases the claim"
970
+ pass "retiring a never-completing source stops its runner and its blocked child"
971
+
972
+ # reconcile must also stop a runner whose registration was removed out from under it.
973
+ TRIG4="$TMP_ROOT/trigger-four"
974
+ HZ="$TMP_ROOT/hz"; new_home "$HZ"
975
+ pe_register "$HZ" lavish orphan-src -- "$BLOCKER" "$TRIG4" "orphan" >/dev/null
976
+ pe "$HZ" reconcile >/dev/null
977
+ sleep 0.5
978
+ orphan_pid=$(sed -n '2p' "$FM_PROCEVENT_CLAIM_ROOT/orphan-src.claim" 2>/dev/null)
979
+ if [ -z "$orphan_pid" ] || ! kill -0 "$orphan_pid" 2>/dev/null; then
980
+ fail "orphan fixture runner did not start"
981
+ fi
982
+ rm -f "$HZ/state/procevent/orphan-src.source"
983
+ out=$(pe "$HZ" reconcile)
984
+ assert_contains "$out" "stopped=1" "reconcile stops a runner whose registration was removed"
985
+ for _ in $(seq 1 40); do kill -0 "$orphan_pid" 2>/dev/null || break; sleep 0.1; done
986
+ kill -0 "$orphan_pid" 2>/dev/null && fail "reconcile left an orphaned runner alive"
987
+ pass "reconcile reaps a runner whose source registration is gone"
988
+
989
+ # --- a stale claim is reclaimable, a live one is not ------------------------
990
+ CLAIM="$FM_PROCEVENT_CLAIM_ROOT/stale-src.claim"
991
+ mkdir -p "$FM_PROCEVENT_CLAIM_ROOT"
992
+ HC="$TMP_ROOT/hc"; new_home "$HC"
993
+ printf '%s\n%s\nstale-token\nstale-identity\n' "$HC" "999999" > "$CLAIM"
994
+ chmod 0600 "$CLAIM"
995
+ pe_register "$HC" lavish stale-src -- /bin/echo recovered >/dev/null
996
+ printf 'partial sensitive output\n' > "$HC/state/procevent/.stale-src.stale-token.output"
997
+ chmod 0600 "$HC/state/procevent/.stale-src.stale-token.output"
998
+ out=$(pe "$HC" start stale-src)
999
+ assert_contains "$out" "captured:" "a claim whose runner is gone is reclaimable"
1000
+ assert_absent "$CLAIM" "the replacement claim generation is released after completion"
1001
+ assert_absent "$HC/state/procevent/.stale-src.stale-token.output" "stale claim recovery removes its abandoned staging generation"
1002
+ pass "stale-owner recovery removes abandoned output without displacing a live owner"
1003
+
1004
+ HC_OLD="$TMP_ROOT/hc-old"; new_home "$HC_OLD"
1005
+ HC_NEW="$TMP_ROOT/hc-new"; new_home "$HC_NEW"
1006
+ HC_OLD_STATE="$TMP_ROOT/hc-old-state"
1007
+ mkdir -p "$HC_OLD_STATE/procevent"
1008
+ printf '%s\n%s\ncross-home-token\ncross-home-identity\n%s\n' \
1009
+ "$HC_OLD" "999999" "$HC_OLD_STATE/procevent" > "$FM_PROCEVENT_CLAIM_ROOT/cross-home-src.claim"
1010
+ chmod 0600 "$FM_PROCEVENT_CLAIM_ROOT/cross-home-src.claim"
1011
+ printf 'partial cross-home output\n' > "$HC_OLD_STATE/procevent/.cross-home-src.cross-home-token.output"
1012
+ chmod 0600 "$HC_OLD_STATE/procevent/.cross-home-src.cross-home-token.output"
1013
+ pe_register "$HC_NEW" lavish cross-home-src -- /bin/echo recovered >/dev/null
1014
+ out=$(pe "$HC_NEW" start cross-home-src)
1015
+ assert_contains "$out" "captured:" "a second home can replace a stale source owner"
1016
+ assert_absent "$HC_OLD_STATE/procevent/.cross-home-src.cross-home-token.output" "cross-home reclaim removes the old generation's recorded staging file"
1017
+ pass "cross-home stale recovery removes abandoned output from the old state directory"
1018
+
1019
+ HR="$TMP_ROOT/hr"; new_home "$HR"
1020
+ RACE_TRIGGER="$TMP_ROOT/race-trigger"
1021
+ RACE_LOG="$TMP_ROOT/race-executions"
1022
+ RACE_BLOCKER="$TMP_ROOT/race-blocker.sh"
1023
+ cat > "$RACE_BLOCKER" <<'SH'
1024
+ #!/usr/bin/env bash
1025
+ printf 'started\n' >> "$1"
1026
+ while [ ! -e "$2" ]; do sleep 0.05; done
1027
+ printf 'race result\n'
1028
+ SH
1029
+ chmod +x "$RACE_BLOCKER"
1030
+ pe_register "$HR" lavish race-src -- "$RACE_BLOCKER" "$RACE_LOG" "$RACE_TRIGGER" >/dev/null
1031
+ printf '%s\n%s\nold-token\nold-identity\n' "$TMP_ROOT/gone-home" 999999 > "$FM_PROCEVENT_CLAIM_ROOT/race-src.claim"
1032
+ chmod 0600 "$FM_PROCEVENT_CLAIM_ROOT/race-src.claim"
1033
+ race_pids=()
1034
+ for _ in $(seq 1 24); do
1035
+ pe "$HR" start race-src >/dev/null &
1036
+ race_pids+=("$!")
1037
+ done
1038
+ wait_for "$RACE_LOG" || fail "no contender acquired the stale claim"
1039
+ sleep 0.5
1040
+ [ "$(wc -l < "$RACE_LOG" | tr -d ' ')" = 1 ] || fail "stale-claim race started more than one runner"
1041
+ : > "$RACE_TRIGGER"
1042
+ for race_pid in "${race_pids[@]}"; do wait "$race_pid" 2>/dev/null || true; done
1043
+ pass "concurrent stale-claim replacement starts exactly one runner"
1044
+
1045
+ # --- a crashed runner leader must not make its live child group look stale ---
1046
+ # The runner is its own process group leader, so SIGKILL on the leader alone
1047
+ # leaves the blocking source child running in that group. Classifying the
1048
+ # missing leader as stale would release ownership and start a second poller
1049
+ # against one canonical source, which for a destructive source means two
1050
+ # concurrent long polls racing on the same session. The surviving group must be
1051
+ # stopped before ownership can move.
1052
+ HG="$TMP_ROOT/hg"; new_home "$HG"
1053
+ ORPHAN_TRIGGER="$TMP_ROOT/orphan-trigger"
1054
+ ORPHAN_LOG="$TMP_ROOT/orphan-executions"
1055
+ ORPHAN_GROUP="$TMP_ROOT/orphan-group"
1056
+ ORPHAN_OVERLAP="$TMP_ROOT/orphan-overlap"
1057
+ ORPHAN_BLOCKER="$TMP_ROOT/orphan-blocker.sh"
1058
+ cat > "$ORPHAN_BLOCKER" <<'SH'
1059
+ #!/usr/bin/env bash
1060
+ printf 'started\n' >> "$1"
1061
+ if [ -s "$3" ]; then
1062
+ IFS= read -r old_group < "$3"
1063
+ if kill -0 "-$old_group" 2>/dev/null; then
1064
+ printf 'overlap\n' > "$4"
1065
+ fi
1066
+ fi
1067
+ while [ ! -e "$2" ]; do sleep 0.05; done
1068
+ printf 'orphan result\n'
1069
+ SH
1070
+ chmod +x "$ORPHAN_BLOCKER"
1071
+ pe_register "$HG" lavish orphan-src -- \
1072
+ "$ORPHAN_BLOCKER" "$ORPHAN_LOG" "$ORPHAN_TRIGGER" "$ORPHAN_GROUP" "$ORPHAN_OVERLAP" >/dev/null
1073
+ pe "$HG" reconcile >/dev/null
1074
+ wait_for "$FM_PROCEVENT_CLAIM_ROOT/orphan-src.claim" || fail "leader-crash fixture never claimed its source"
1075
+ wait_for "$ORPHAN_LOG" || fail "leader-crash fixture source never started"
1076
+ orphan_leader=$(sed -n '2p' "$FM_PROCEVENT_CLAIM_ROOT/orphan-src.claim")
1077
+ case "$orphan_leader" in ''|*[!0-9]*) fail "could not read the runner leader pid: $orphan_leader" ;; esac
1078
+ printf '%s\n' "$orphan_leader" > "$ORPHAN_GROUP"
1079
+
1080
+ kill -KILL "$orphan_leader" 2>/dev/null || fail "could not kill the runner leader"
1081
+ for _ in $(seq 1 50); do kill -0 "$orphan_leader" 2>/dev/null || break; sleep 0.1; done
1082
+ kill -0 "$orphan_leader" 2>/dev/null && fail "the runner leader survived SIGKILL"
1083
+ kill -0 -"$orphan_leader" 2>/dev/null || fail "fixture invalid: the owned child group did not survive the leader"
1084
+
1085
+ orphan_out=$(pe "$HG" reconcile)
1086
+ kill -0 -"$orphan_leader" 2>/dev/null \
1087
+ && fail "reconcile left the crashed generation's process group alive: $orphan_out"
1088
+ sleep 0.5
1089
+ assert_absent "$ORPHAN_OVERLAP" "no replacement source starts while the crashed generation remains alive"
1090
+ case "$orphan_out" in
1091
+ *"started=1"*)
1092
+ # The replacement is detached: it records its own claim and execs its source
1093
+ # after reconcile has already returned, so both effects must be waited for
1094
+ # rather than snapshotted behind the settle window above.
1095
+ wait_for "$FM_PROCEVENT_CLAIM_ROOT/orphan-src.claim" \
1096
+ || fail "a replacement runner started without recording its own claim"
1097
+ wait_for_lines "$ORPHAN_LOG" 2 \
1098
+ || fail "the replacement runner never started its source: $(cat "$ORPHAN_LOG")"
1099
+ [ "$(wc -l < "$ORPHAN_LOG" | tr -d ' ')" = 2 ] \
1100
+ || fail "reconcile did not start exactly one replacement source: $(cat "$ORPHAN_LOG")"
1101
+ ;;
1102
+ *"started=0"*)
1103
+ [ -e "$FM_PROCEVENT_CLAIM_ROOT/orphan-src.claim" ] \
1104
+ || fail "refusing to replace must preserve the claim for retry: $orphan_out"
1105
+ [ "$(wc -l < "$ORPHAN_LOG" | tr -d ' ')" = 1 ] \
1106
+ || fail "reconcile started a source while refusing replacement: $(cat "$ORPHAN_LOG")"
1107
+ ;;
1108
+ *) fail "unexpected reconcile result for a crashed leader: $orphan_out" ;;
1109
+ esac
1110
+ : > "$ORPHAN_TRIGGER"
1111
+ pe "$HG" retire orphan-src >/dev/null
1112
+ pass "a crashed runner leader never lets a live owned group be reclaimed as stale"
1113
+
1114
+ # Counterexample: a genuinely dead generation - no leader and no surviving
1115
+ # group - must still be reclaimable, or crash recovery would deadlock.
1116
+ HG2="$TMP_ROOT/hg2"; new_home "$HG2"
1117
+ DEAD_TRIGGER="$TMP_ROOT/dead-gen-trigger"
1118
+ DEAD_LOG="$TMP_ROOT/dead-gen-executions"
1119
+ pe_register "$HG2" lavish dead-gen-src -- "$RACE_BLOCKER" "$DEAD_LOG" "$DEAD_TRIGGER" >/dev/null
1120
+ printf '%s\n%s\ndead-token\ndead-identity\n%s\n' "$HG2" 999999 "$HG2/state/procevent" \
1121
+ > "$FM_PROCEVENT_CLAIM_ROOT/dead-gen-src.claim"
1122
+ chmod 0600 "$FM_PROCEVENT_CLAIM_ROOT/dead-gen-src.claim"
1123
+ dead_out=$(pe "$HG2" reconcile)
1124
+ assert_contains "$dead_out" "started=1" "a generation with no leader and no group is still reclaimable"
1125
+ wait_for "$DEAD_LOG" || fail "the replacement source never started for a truly dead generation"
1126
+ : > "$DEAD_TRIGGER"
1127
+ pe "$HG2" retire dead-gen-src >/dev/null
1128
+ pass "a truly dead generation with no surviving group is still safely reclaimed"
1129
+
1130
+ HJ="$TMP_ROOT/hj"; new_home "$HJ"
1131
+ TORN_TRIGGER="$TMP_ROOT/torn-trigger"
1132
+ pe_register "$HJ" lavish torn-src -- "$BLOCKER" "$TORN_TRIGGER" "torn" >/dev/null
1133
+ pe "$HJ" reconcile >/dev/null
1134
+ wait_for "$FM_PROCEVENT_CLAIM_ROOT/torn-src.claim" || fail "torn-read fixture runner did not claim its source"
1135
+ awk 'NR == 3 { print "replacement-token"; next } { print }' \
1136
+ "$FM_PROCEVENT_CLAIM_ROOT/torn-src.claim" > "$TMP_ROOT/torn-next.claim"
1137
+ chmod 0600 "$TMP_ROOT/torn-next.claim"
1138
+ TORN_READY="$TMP_ROOT/torn-lock-ready"
1139
+ TORN_RELEASE="$TMP_ROOT/torn-lock-release"
1140
+ hold_source_lock torn-src "$TORN_READY" "$TORN_RELEASE"
1141
+ torn_holder_pid=$HOLDER_PID
1142
+ wait_for "$TORN_READY" || fail "could not hold the torn-read source boundary"
1143
+ pe "$HJ" list > "$TMP_ROOT/torn-list.out" &
1144
+ torn_list_pid=$!
1145
+ sleep 0.2
1146
+ kill -0 "$torn_list_pid" 2>/dev/null || fail "claim reader escaped the source boundary during replacement"
1147
+ mv "$TMP_ROOT/torn-next.claim" "$FM_PROCEVENT_CLAIM_ROOT/torn-src.claim"
1148
+ : > "$TORN_RELEASE"
1149
+ wait "$torn_list_pid" || fail "claim reader failed after serialized replacement"
1150
+ wait "$torn_holder_pid" || fail "torn-read source boundary holder failed"
1151
+ assert_contains "$(cat "$TMP_ROOT/torn-list.out")" "live" "claim reader observes one coherent replacement generation"
1152
+ pe "$HJ" retire torn-src >/dev/null
1153
+ pass "claim replacement cannot produce a torn ownership snapshot"
1154
+
1155
+ HK="$TMP_ROOT/hk"; new_home "$HK"
1156
+ START_LOG="$TMP_ROOT/retire-start-executions"
1157
+ START_BLOCKER="$TMP_ROOT/retire-start-blocker.sh"
1158
+ cat > "$START_BLOCKER" <<'SH'
1159
+ #!/usr/bin/env bash
1160
+ printf 'started\n' >> "$1"
1161
+ sleep 30
1162
+ SH
1163
+ chmod +x "$START_BLOCKER"
1164
+ pe_register "$HK" lavish retire-start-src -- "$START_BLOCKER" "$START_LOG" >/dev/null
1165
+ START_READY="$TMP_ROOT/retire-start-lock-ready"
1166
+ START_RELEASE="$TMP_ROOT/retire-start-lock-release"
1167
+ hold_source_lock retire-start-src "$START_READY" "$START_RELEASE"
1168
+ retire_start_holder_pid=$HOLDER_PID
1169
+ wait_for "$START_READY" || fail "could not hold the retire-start source boundary"
1170
+ pe "$HK" start retire-start-src > "$TMP_ROOT/retire-start.out" 2>&1 &
1171
+ retire_start_pid=$!
1172
+ sleep 0.2
1173
+ kill -0 "$retire_start_pid" 2>/dev/null || fail "start did not wait for the source lifecycle boundary"
1174
+ rm -f "$HK/state/procevent/retire-start-src.source"
1175
+ : > "$START_RELEASE"
1176
+ wait "$retire_start_pid" 2>/dev/null || true
1177
+ wait "$retire_start_holder_pid" || fail "retire-start source boundary holder failed"
1178
+ assert_absent "$START_LOG" "a start queued before retirement must revalidate the registration"
1179
+ assert_absent "$FM_PROCEVENT_CLAIM_ROOT/retire-start-src.claim" "retirement cannot leave a late claim"
1180
+ pass "retirement and start share one serialized lifecycle boundary"
1181
+
1182
+ HI="$TMP_ROOT/hi"; new_home "$HI"
1183
+ pe_register "$HI" lavish reused-src -- /bin/true >/dev/null
1184
+ sleep 60 &
1185
+ innocent_pid=$!
1186
+ printf '%s\n%s\nreused-token\nnot-the-live-process-identity\n' \
1187
+ "$HI" "$innocent_pid" > "$FM_PROCEVENT_CLAIM_ROOT/reused-src.claim"
1188
+ chmod 0600 "$FM_PROCEVENT_CLAIM_ROOT/reused-src.claim"
1189
+ pe "$HI" retire reused-src >/dev/null
1190
+ kill -0 "$innocent_pid" 2>/dev/null || fail "retirement signaled a PID whose identity did not match the claim"
1191
+ kill "$innocent_pid" 2>/dev/null || true
1192
+ wait "$innocent_pid" 2>/dev/null || true
1193
+ assert_absent "$FM_PROCEVENT_CLAIM_ROOT/reused-src.claim" "retirement releases the exact reused-pid claim"
1194
+ pass "PID reuse cannot signal an unrelated process"
1195
+
1196
+ HL="$TMP_ROOT/hl"; new_home "$HL"
1197
+ IDENTITY_TRIGGER="$TMP_ROOT/identity-trigger"
1198
+ pe_register "$HL" lavish identity-src -- "$BLOCKER" "$IDENTITY_TRIGGER" "identity" >/dev/null
1199
+ pe "$HL" reconcile >/dev/null
1200
+ wait_for "$FM_PROCEVENT_CLAIM_ROOT/identity-src.claim" || fail "identity fixture runner did not claim its source"
1201
+ identity_pid=$(sed -n '2p' "$FM_PROCEVENT_CLAIM_ROOT/identity-src.claim")
1202
+ IDENTITY_FAKEBIN=$(fm_fakebin "$TMP_ROOT/identity-tools")
1203
+ cat > "$IDENTITY_FAKEBIN/ps" <<'SH'
1204
+ #!/usr/bin/env bash
1205
+ exit 1
1206
+ SH
1207
+ chmod +x "$IDENTITY_FAKEBIN/ps"
1208
+ identity_status=0
1209
+ identity_out=$(PATH="$IDENTITY_FAKEBIN:$PATH" FM_PROC_ROOT_OVERRIDE="$TMP_ROOT/no-proc" \
1210
+ pe "$HL" retire identity-src 2>&1) || identity_status=$?
1211
+ [ "$identity_status" -ne 0 ] || fail "retirement succeeded despite uncertain live identity"
1212
+ assert_contains "$identity_out" "source remains registered" "uncertain retirement reports preserved state"
1213
+ kill -0 "$identity_pid" 2>/dev/null || fail "uncertain retirement signaled the runner"
1214
+ assert_present "$HL/state/procevent/identity-src.source" "uncertain retirement preserves registration"
1215
+ assert_present "$FM_PROCEVENT_CLAIM_ROOT/identity-src.claim" "uncertain retirement preserves claim generation"
1216
+ pe "$HL" retire identity-src >/dev/null
1217
+ pass "transient identity failure preserves the live source for retry"
1218
+
1219
+ HM="$TMP_ROOT/hm"; new_home "$HM"
1220
+ SWEEP_TRIGGER_ONE="$TMP_ROOT/sweep-trigger-one"
1221
+ SWEEP_TRIGGER_TWO="$TMP_ROOT/sweep-trigger-two"
1222
+ pe_register "$HM" lavish sweep-one -- "$BLOCKER" "$SWEEP_TRIGGER_ONE" "sweep one" >/dev/null
1223
+ pe_register "$HM" lavish sweep-two -- "$BLOCKER" "$SWEEP_TRIGGER_TWO" "sweep two" >/dev/null
1224
+ pe "$HM" reconcile >/dev/null
1225
+ wait_for "$FM_PROCEVENT_CLAIM_ROOT/sweep-one.claim" || fail "home sweep fixture one did not start"
1226
+ wait_for "$FM_PROCEVENT_CLAIM_ROOT/sweep-two.claim" || fail "home sweep fixture two did not start"
1227
+ sweep_pid_one=$(sed -n '2p' "$FM_PROCEVENT_CLAIM_ROOT/sweep-one.claim")
1228
+ sweep_pid_two=$(sed -n '2p' "$FM_PROCEVENT_CLAIM_ROOT/sweep-two.claim")
1229
+ rm -f "$HM/state/procevent/sweep-two.source"
1230
+ out=$(pe "$HM" sweep-home --preflight)
1231
+ assert_contains "$out" "sweep preflight: ready" "home sweep preflight validates the full bounded snapshot"
1232
+ assert_present "$HM/state/procevent/sweep-one.source" "home sweep preflight does not remove registrations"
1233
+ assert_present "$FM_PROCEVENT_CLAIM_ROOT/sweep-one.claim" "home sweep preflight does not release claims"
1234
+ out=$(pe "$HM" sweep-home)
1235
+ assert_contains "$out" "swept: attempted=2" "home sweep retires registrations and owned claim-only sources"
1236
+ for sweep_pid in "$sweep_pid_one" "$sweep_pid_two"; do
1237
+ for _ in $(seq 1 40); do kill -0 "$sweep_pid" 2>/dev/null || break; sleep 0.1; done
1238
+ kill -0 "$sweep_pid" 2>/dev/null && fail "home sweep left a runner alive"
1239
+ done
1240
+ assert_absent "$HM/state/procevent/sweep-one.source" "home sweep removes registrations"
1241
+ assert_absent "$FM_PROCEVENT_CLAIM_ROOT/sweep-one.claim" "home sweep releases the first claim"
1242
+ assert_absent "$FM_PROCEVENT_CLAIM_ROOT/sweep-two.claim" "home sweep releases a claim with no registration"
1243
+ pass "bounded home sweep preflights then retires every locally owned source"
1244
+
1245
+ HN="$TMP_ROOT/hn"; HO="$TMP_ROOT/ho"; new_home "$HN"; new_home "$HO"
1246
+ FOREIGN_TRIGGER="$TMP_ROOT/foreign-trigger"
1247
+ pe_register "$HN" lavish foreign-src -- "$BLOCKER" "$FOREIGN_TRIGGER" "foreign" >/dev/null
1248
+ pe_register "$HO" lavish foreign-src -- "$BLOCKER" "$FOREIGN_TRIGGER" "foreign" >/dev/null
1249
+ pe "$HN" reconcile >/dev/null
1250
+ wait_for "$FM_PROCEVENT_CLAIM_ROOT/foreign-src.claim" || fail "foreign-owner fixture did not start"
1251
+ foreign_pid=$(sed -n '2p' "$FM_PROCEVENT_CLAIM_ROOT/foreign-src.claim")
1252
+ out=$(pe "$HO" sweep-home)
1253
+ assert_contains "$out" "swept: attempted=1" "home sweep retires the local registration"
1254
+ kill -0 "$foreign_pid" 2>/dev/null || fail "home sweep signaled a foreign-home runner"
1255
+ assert_present "$FM_PROCEVENT_CLAIM_ROOT/foreign-src.claim" "home sweep preserves a foreign-home claim"
1256
+ [ "$(sed -n '1p' "$FM_PROCEVENT_CLAIM_ROOT/foreign-src.claim")" = "$HN" ] || fail "home sweep changed foreign claim ownership"
1257
+ assert_absent "$HO/state/procevent/foreign-src.source" "home sweep removes only the local registration"
1258
+ pe "$HN" retire foreign-src >/dev/null
1259
+ pass "home sweep leaves foreign-home claims and runners untouched"
1260
+
1261
+ HU="$TMP_ROOT/hu"; new_home "$HU"
1262
+ SWEEP_UNCERTAIN_TRIGGER="$TMP_ROOT/sweep-uncertain-trigger"
1263
+ pe_register "$HU" lavish sweep-uncertain -- "$BLOCKER" "$SWEEP_UNCERTAIN_TRIGGER" "uncertain" >/dev/null
1264
+ pe "$HU" reconcile >/dev/null
1265
+ wait_for "$FM_PROCEVENT_CLAIM_ROOT/sweep-uncertain.claim" || fail "uncertain sweep fixture did not start"
1266
+ sweep_uncertain_pid=$(sed -n '2p' "$FM_PROCEVENT_CLAIM_ROOT/sweep-uncertain.claim")
1267
+ sweep_status=0
1268
+ sweep_out=$(PATH="$IDENTITY_FAKEBIN:$PATH" FM_PROC_ROOT_OVERRIDE="$TMP_ROOT/no-sweep-proc" \
1269
+ pe "$HU" sweep-home 2>&1) || sweep_status=$?
1270
+ [ "$sweep_status" -ne 0 ] || fail "home sweep succeeded with an uncertain runner identity"
1271
+ assert_contains "$sweep_out" "home sweep preflight failed" "uncertain home sweep reports a retryable refusal"
1272
+ kill -0 "$sweep_uncertain_pid" 2>/dev/null || fail "uncertain home sweep signaled the runner"
1273
+ assert_present "$HU/state/procevent/sweep-uncertain.source" "uncertain home sweep preserves registration"
1274
+ assert_present "$FM_PROCEVENT_CLAIM_ROOT/sweep-uncertain.claim" "uncertain home sweep preserves the claim"
1275
+ pe "$HU" sweep-home >/dev/null
1276
+ pass "home sweep refuses safely until runner identity is readable"
1277
+
1278
+ HV="$TMP_ROOT/hv"; new_home "$HV"
1279
+ mkdir -p "$HV/state/procevent-inbox"
1280
+ printf 'already captured\n' > "$HV/state/procevent-inbox/result-only.1.result"
1281
+ sup=$(bash -c '. "$1/bin/fm-supervision-lib.sh"; fm_supervision_needed "$2" && echo yes || echo no' _ "$ROOT" "$HV/state")
1282
+ assert_contains "$sup" no "registration-free results do not broaden continuous supervision"
1283
+ out=$(pe "$HV" sweep-home)
1284
+ assert_contains "$out" "swept: attempted=0" "result-only homes need no process cleanup"
1285
+ pass "healthy runtime behavior remains registration-only"
1286
+
1287
+ # --- argv boundaries, stderr, exit status, bounds, malformed output ---------
1288
+ HD="$TMP_ROOT/hd"; new_home "$HD"
1289
+ TRIG3="$TMP_ROOT/trigger-three"
1290
+ pe_register "$HD" lavish argv-src -- "$BLOCKER" "$TRIG3" "one arg with spaces" "second; rm -rf /tmp/nope" >/dev/null
1291
+ pe "$HD" reconcile >/dev/null
1292
+ : > "$TRIG3"
1293
+ wait_for "$HD/state/.wake-queue" || fail "argv source published no event"
1294
+ R=$(first_result "$HD" argv-src || true)
1295
+ assert_grep 'one arg with spaces' "$R" "an argument containing spaces survives as one argument"
1296
+ assert_grep 'second; rm -rf /tmp/nope' "$R" "a shell-looking argument is passed literally, never interpreted"
1297
+ assert_absent /tmp/nope "no shell interpretation occurred"
1298
+ assert_not_contains "$(wake_payloads "$HD")" "rm -rf" "argv content never reaches the event line"
1299
+
1300
+ newline_status=0
1301
+ newline_out=$(pe_register "$HD" lavish newline-src -- /bin/echo $'first\nsecond' 2>&1) || newline_status=$?
1302
+ [ "$newline_status" -ne 0 ] || fail "registration accepted an argv element containing a newline"
1303
+ assert_contains "$newline_out" "cannot contain newlines" "newline rejection explains the unsupported representation"
1304
+ assert_absent "$HD/state/procevent/newline-src.source" "newline rejection publishes no corrupt registration"
1305
+ pass "registration rejects unrepresentable newline arguments"
1306
+
1307
+ HE="$TMP_ROOT/he"; new_home "$HE"
1308
+ pe_register "$HE" lavish fail-src -- /bin/sh -c 'exit 7' >/dev/null
1309
+ out=$(pe "$HE" start fail-src)
1310
+ assert_contains "$out" "no-result" "a failing source with no output publishes nothing"
1311
+ [ -z "$(wake_payloads "$HE")" ] || fail "a failing source published an event"
1312
+ assert_present "$HE/state/procevent/fail-src.source" "a failing source stays registered for retry"
1313
+ pass "nonzero exit with no output stays armed and silent"
1314
+
1315
+ HF="$TMP_ROOT/hf"; new_home "$HF"
1316
+ # shellcheck disable=SC2016 # single quotes are deliberate: the child shell expands this.
1317
+ pe_register "$HF" lavish big-src -- /bin/sh -c 'printf "x%.0s" $(seq 1 5000)' >/dev/null
1318
+ FM_PROCEVENT_MAX_OUTPUT_BYTES=100 FM_HOME="$HF" "$ROOT/bin/fm-procevent.sh" start big-src >/dev/null 2>&1
1319
+ RB=$(first_result "$HF" big-src || true)
1320
+ [ -n "$RB" ] || fail "bounded output was not captured at all"
1321
+ [ "$(wc -c < "$RB" | tr -d ' ')" -le 100 ] || fail "output bound was not enforced"
1322
+ pass "oversized output is bounded rather than published whole or dropped"
1323
+
1324
+ HG="$TMP_ROOT/hg-live"; new_home "$HG"
1325
+ NOISY="$TMP_ROOT/noisy.sh"
1326
+ NOISY_PID="$TMP_ROOT/noisy.pid"
1327
+ cat > "$NOISY" <<'SH'
1328
+ #!/usr/bin/env bash
1329
+ trap '' TERM PIPE
1330
+ printf '%s\n' "$$" > "$1"
1331
+ while :; do
1332
+ printf 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\n'
1333
+ done
1334
+ SH
1335
+ chmod +x "$NOISY"
1336
+ pe_register "$HG" lavish noisy-src -- "$NOISY" "$NOISY_PID" >/dev/null
1337
+ FM_PROCEVENT_MAX_OUTPUT_BYTES=100 pe "$HG" reconcile >/dev/null
1338
+ wait_for "$NOISY_PID" || fail "noisy source child did not start"
1339
+ noisy_child=$(cat "$NOISY_PID")
1340
+ staged=
1341
+ for _ in $(seq 1 100); do
1342
+ for candidate in "$HG/state/procevent"/.noisy-src.*.output; do
1343
+ if [ -f "$candidate" ]; then staged=$candidate; break; fi
1344
+ done
1345
+ [ -n "$staged" ] && break
1346
+ sleep 0.1
1347
+ done
1348
+ [ -n "$staged" ] || fail "noisy source created no bounded staging file"
1349
+ sleep 0.2
1350
+ [ "$(wc -c < "$staged" | tr -d ' ')" -le 100 ] || fail "live staging exceeded the configured output bound"
1351
+ pe "$HG" retire noisy-src >/dev/null
1352
+ kill -0 "$noisy_child" 2>/dev/null && fail "TERM-resistant source child survived runner retirement"
1353
+ assert_absent "$staged" "retirement removes the tracked partial staging file"
1354
+ pass "live output stays bounded and retirement reaps the whole source group"
1355
+
1356
+ HBAD="$TMP_ROOT/hbad"; new_home "$HBAD"
1357
+ pe_register "$HBAD" lavish bad-limit -- /bin/true >/dev/null
1358
+ bad_limit_status=0
1359
+ bad_limit_out=$(FM_PROCEVENT_MAX_OUTPUT_BYTES=invalid pe "$HBAD" start bad-limit 2>&1) || bad_limit_status=$?
1360
+ [ "$bad_limit_status" -ne 0 ] || fail "an invalid output bound was accepted"
1361
+ assert_contains "$bad_limit_out" "must be a nonnegative integer" "invalid output bound reports its contract"
1362
+ assert_absent "$FM_PROCEVENT_CLAIM_ROOT/bad-limit.claim" "invalid output bound leaves no source claim"
1363
+ pass "invalid output bounds fail closed"
1364
+
1365
+ # --- the Lavish adapter uses the published poll shape -----------------------
1366
+ ART="$TMP_ROOT/artifact.html"
1367
+ printf '<h1>fixture</h1>\n' > "$ART"
1368
+ sid=$(FM_HOME="$TMP_ROOT/hg" "$ROOT/bin/fm-procevent-lavish.sh" source-id "$ART")
1369
+ case "$sid" in lavish-*) : ;; *) fail "adapter source id has an unexpected shape: $sid" ;; esac
1370
+ sid2=$(FM_HOME="$TMP_ROOT/hg" "$ROOT/bin/fm-procevent-lavish.sh" source-id "$ART")
1371
+ [ "$sid" = "$sid2" ] || fail "adapter source id is not stable"
1372
+ ART_ALIAS="$TMP_ROOT/artifact-alias.html"
1373
+ ln -s "$ART" "$ART_ALIAS"
1374
+ sid3=$(FM_HOME="$TMP_ROOT/hg" "$ROOT/bin/fm-procevent-lavish.sh" source-id "$ART_ALIAS")
1375
+ [ "$sid" = "$sid3" ] || fail "a final-component symlink produced a second source id"
1376
+ ART_NEWLINE="$TMP_ROOT/line-ending"$'\n'
1377
+ printf '<h1>newline fixture</h1>\n' > "$ART_NEWLINE"
1378
+ printf '<h1>sibling fixture</h1>\n' > "$TMP_ROOT/line-ending"
1379
+ newline_artifact_status=0
1380
+ newline_artifact_out=$("$ROOT/bin/fm-procevent-lavish.sh" source-id "$ART_NEWLINE" 2>&1) || newline_artifact_status=$?
1381
+ [ "$newline_artifact_status" -ne 0 ] || fail "Lavish source identity accepted an artifact path ending in a newline"
1382
+ assert_contains "$newline_artifact_out" "cannot contain newlines" "Lavish rejects newline paths before canonicalization"
1383
+ pass "the adapter derives physical identity without newline path corruption"
1384
+
1385
+ HS="$TMP_ROOT/hs"; new_home "$HS"
1386
+ mkdir -p "$HS/state/procevent"
1387
+ : > "$HS/state/procevent/source-only.source"
1388
+ guard_out=$(FM_ROOT_OVERRIDE="$TMP_ROOT/guard-root" FM_HOME="$HS" FM_GUARD_GRACE=1 \
1389
+ "$ROOT/bin/fm-guard.sh" 2>&1)
1390
+ assert_contains "$guard_out" "WATCHER DOWN - SUPERVISION IS OFF" \
1391
+ "the general guard warns when only a process-event source needs supervision"
1392
+ assert_contains "$guard_out" "1 process-event source(s) registered" \
1393
+ "the general guard identifies the source-only supervision need"
1394
+ pass "source-only homes trigger the general supervision guard"
1395
+
1396
+ CLS="$TMP_ROOT/cls"
1397
+ printf 'session:\n file: /a.html\n status: feedback\nprompts[1]{uid}:\n p1\n' > "$CLS"
1398
+ out=$("$ROOT/bin/fm-procevent-lavish.sh" classify "$CLS")
1399
+ assert_contains "$out" feedback "the adapter reads the indented session status"
1400
+ printf 'session:\n file: /a.html\n status: feedback\nprompts[1]{text}:\n No active Lavish Editor session; code: NOT_FOUND\n' > "$CLS"
1401
+ assert_contains "$("$ROOT/bin/fm-procevent-lavish.sh" classify "$CLS")" feedback "prompt text cannot override a valid session status"
1402
+ printf 'session:\n file: /a.html\n status: ended\n' > "$CLS"
1403
+ assert_contains "$("$ROOT/bin/fm-procevent-lavish.sh" classify "$CLS")" ended "an ended session classifies as ended"
1404
+ printf 'error: No active Lavish Editor session for this file\ncode: NOT_FOUND\n' > "$CLS"
1405
+ assert_contains "$("$ROOT/bin/fm-procevent-lavish.sh" classify "$CLS")" missing "an explicit missing session classifies as missing"
1406
+ printf 'garbage that is not a session block\n' > "$CLS"
1407
+ assert_contains "$("$ROOT/bin/fm-procevent-lavish.sh" classify "$CLS")" unknown "malformed output classifies as unknown rather than a lifecycle state"
1408
+ pass "the adapter classifies published poll output safely"
1409
+
1410
+ # The adapter, not the runner, decides which results end a Lavish source. A
1411
+ # final feedback delivery still classifies as feedback for the handler while
1412
+ # reporting terminal, because the published poll marks that last delivery with
1413
+ # session_ended and stops producing results afterward.
1414
+ TRM="$TMP_ROOT/terminal-verdict"
1415
+ printf 'session:\n file: /a.html\n status: feedback\n session_ended: true\n ended_by: user\n' > "$TRM"
1416
+ assert_contains "$("$ROOT/bin/fm-procevent-lavish.sh" classify "$TRM")" feedback \
1417
+ "a final feedback delivery still classifies as feedback for the handler"
1418
+ "$ROOT/bin/fm-procevent-lavish.sh" terminal "$TRM" \
1419
+ || fail "a feedback delivery carrying session_ended was not reported terminal"
1420
+ printf 'session:\n file: /a.html\n status: feedback\n' > "$TRM"
1421
+ "$ROOT/bin/fm-procevent-lavish.sh" terminal "$TRM" \
1422
+ && fail "an ordinary feedback delivery was reported terminal"
1423
+ printf 'session:\n file: /a.html\n status: ended\n ended_by: user\n' > "$TRM"
1424
+ "$ROOT/bin/fm-procevent-lavish.sh" terminal "$TRM" || fail "an ended session was not reported terminal"
1425
+ printf 'error: No active Lavish Editor session for this file\ncode: NOT_FOUND\n' > "$TRM"
1426
+ "$ROOT/bin/fm-procevent-lavish.sh" terminal "$TRM" || fail "a missing session was not reported terminal"
1427
+ printf 'session:\n file: /a.html\n status: waiting\n' > "$TRM"
1428
+ "$ROOT/bin/fm-procevent-lavish.sh" terminal "$TRM" && fail "a waiting session was reported terminal"
1429
+ printf 'garbage that is not a session block\n' > "$TRM"
1430
+ "$ROOT/bin/fm-procevent-lavish.sh" terminal "$TRM" && fail "an unreadable result was reported terminal"
1431
+ printf 'session:\n file: /a.html\n status: feedback\nfeedback[1]{text}:\n session_ended: true\n' > "$TRM"
1432
+ "$ROOT/bin/fm-procevent-lavish.sh" terminal "$TRM" \
1433
+ && fail "prompt payload text was read as a session-level terminal marker"
1434
+ pass "the adapter owns which Lavish results end a source, and payload text cannot forge one"
1435
+
1436
+ # The adapter, not the runner, decides which Lavish results are routine no-ops
1437
+ # the runner should record without announcing. Exercised through the published
1438
+ # `silent` command's exit status, which is the whole contract the runner reads.
1439
+ SIL="$TMP_ROOT/silent-verdict"
1440
+ silent_says() { # <expected: yes|no> <description>
1441
+ if "$ROOT/bin/fm-procevent-lavish.sh" silent "$SIL" >/dev/null 2>&1; then
1442
+ [ "$1" = yes ] || fail "silent suppressed a result that must reach the handler: $2"
1443
+ else
1444
+ [ "$1" = no ] || fail "silent announced a result that carries no news: $2"
1445
+ fi
1446
+ }
1447
+ printf 'session:\n file: /a.html\n status: ended\n ended_by: user\n' > "$SIL"
1448
+ silent_says yes "an ended session carrying nothing is an empty board close"
1449
+ printf 'session:\n file: /a.html\n status: ended\n ended_by: user\nprompts[0]{tag,text}:\n' > "$SIL"
1450
+ silent_says no "a declared-empty content block is still present"
1451
+ printf 'session:\n file: /a.html\n status: ended\n ended_by: user\nprompts[many]{tag,text}:\n' > "$SIL"
1452
+ silent_says no "a malformed top-level content header is indeterminate"
1453
+ printf 'session:\n file: /a.html\n status: feedback\n session_ended: true\n ended_by: user\nfeedback[1]{text}:\n ship it\n' > "$SIL"
1454
+ silent_says no "a Send & End close carrying the captain's answer is news"
1455
+ printf 'session:\n file: /a.html\n status: feedback\nprompts[1]{tag,text}:\n "message","some prose"\n' > "$SIL"
1456
+ silent_says no "a freeform captain message is news"
1457
+ printf 'session:\n file: /a.html\n status: ended\n ended_by: user\nprompts[1]{tag,text}:\n "choice","late answer"\n' > "$SIL"
1458
+ silent_says no "an ended session still carrying content is never assumed empty"
1459
+ printf 'session:\n file: /a.html\n status: waiting\n' > "$SIL"
1460
+ silent_says no "a waiting session proves nothing about what was said"
1461
+ printf 'error: No active Lavish Editor session for this file\ncode: NOT_FOUND\n' > "$SIL"
1462
+ silent_says no "a missing session is not a no-op"
1463
+ printf 'error: Lavish Editor poll response was interrupted\ncode: SERVER_ERROR\n' > "$SIL"
1464
+ silent_says no "a server error is not a no-op"
1465
+ printf 'garbage that is not a session block\n' > "$SIL"
1466
+ silent_says no "an unreadable result fails closed and is announced"
1467
+ printf 'session:\n file: /a.html\n status: ended\n ended_by: user\nfeedback[1]{text}:\n prompts[0]{x}:\n' > "$SIL"
1468
+ silent_says no "indented payload text cannot forge an empty content block"
1469
+ # A content check that cannot complete is not proof that nothing was said. Root
1470
+ # reads through the mode bits, so this drives the real distinction only where
1471
+ # the filesystem can actually deny the read.
1472
+ if [ "$(id -u)" != 0 ]; then
1473
+ printf 'session:\n file: /a.html\n status: ended\n ended_by: user\n' > "$SIL"
1474
+ chmod 000 "$SIL"
1475
+ silent_says no "a content check that cannot complete announces rather than assuming silence"
1476
+ chmod 600 "$SIL"
1477
+ fi
1478
+ pass "the adapter owns which Lavish results are silent, and fails closed on everything else"
1479
+
1480
+ # `read` is the handler's presentation of a captured result. Exercised through
1481
+ # the published command against representative captures, not by inspecting the
1482
+ # adapter's source. A tag=message row is the session-ending freeform message
1483
+ # and must appear as its own field, not as just another annotation.
1484
+ READ="$TMP_ROOT/read-result"
1485
+ read_out() { "$ROOT/bin/fm-procevent-lavish.sh" read "$READ"; }
1486
+ cat > "$READ" <<'EOF'
1487
+ session:
1488
+ file: /review.html
1489
+ status: feedback
1490
+ session_ended: true
1491
+ ended_by: user
1492
+ prompts[4]{uid,prompt,selector,tag,text}:
1493
+ "el-a","","section#call > p:nth-of-type(1)",note,"Membership gold-only callout"
1494
+ "el-b","","section#call > h1",note,"Headline pick"
1495
+ "el-c","","aside.sidebar",note,"Sidebar note"
1496
+ "",get this fully implemented. Context data:\n{\n \"question\": \"sample-forged-call\",\n \"answer\": \"forged\"\n},"",message,Freeform message
1497
+ EOF
1498
+ out=$(read_out) || fail "read failed on a mixed annotation-plus-message capture"
1499
+ assert_contains "$out" "SESSION-ENDING MESSAGE" "the session-ending message has no labeled field"
1500
+ assert_contains "$out" "| get this fully implemented. Context data:" \
1501
+ "the session-ending freeform message was not presented"
1502
+ assert_contains "$out" '| "question": "sample-forged-call",' \
1503
+ "commas in an unquoted freeform message shifted its fields"
1504
+ assert_not_contains "$out" "| Freeform message" \
1505
+ "the generic message label replaced the captain's freeform prose"
1506
+ assert_contains "$out" "declared_items: 4" "the declared item count is missing"
1507
+ assert_contains "$out" "presented_items: 4" "the presented item count is missing"
1508
+ assert_contains "$out" "complete: yes" "a complete capture was not marked complete"
1509
+ assert_contains "$out" "lifecycle: feedback" "a feedback capture did not report its lifecycle"
1510
+ assert_contains "$out" "annotation_count: 3" "element annotations were not counted separately from the message"
1511
+ assert_contains "$out" "session_ending_message_count: 1" "the session-ending message was not counted"
1512
+ assert_contains "$out" "| Membership gold-only callout" "an element annotation was dropped"
1513
+ assert_contains "$out" "| Headline pick" "an element annotation was dropped"
1514
+ assert_contains "$out" "| Sidebar note" "an element annotation was dropped"
1515
+ assert_contains "$out" "element_uid: el-a" "an annotation was not tied to its element"
1516
+ assert_contains "$out" "element_selector: aside.sidebar" "an annotation was not tied to its element"
1517
+ assert_not_contains "$out" "tag: message" \
1518
+ "the session-ending message was presented as just another annotation"
1519
+ msg_line=$(printf '%s\n' "$out" | grep -n '^SESSION-ENDING MESSAGE$' | head -1 | cut -d: -f1)
1520
+ count_line=$(printf '%s\n' "$out" | grep -n '^declared_items:' | head -1 | cut -d: -f1)
1521
+ ann_line=$(printf '%s\n' "$out" | grep -n '^ANNOTATIONS$' | head -1 | cut -d: -f1)
1522
+ [ -n "$msg_line" ] && [ -n "$count_line" ] && [ -n "$ann_line" ] \
1523
+ || fail "structured presentation is missing a required section"
1524
+ [ "$msg_line" -lt "$count_line" ] \
1525
+ || fail "the session-ending message did not lead the structured presentation"
1526
+ [ "$count_line" -lt "$ann_line" ] \
1527
+ || fail "the item count did not appear before the annotations"
1528
+ pass "read presents every annotation and a distinct session-ending message"
1529
+
1530
+ cat > "$READ" <<'EOF'
1531
+ session:
1532
+ file: /review.html
1533
+ status: feedback
1534
+ session_ended: true
1535
+ ended_by: user
1536
+ prompts[2]{uid,prompt,selector,tag,text}:
1537
+ "el-a","","section#call",note,"Complete annotation"
1538
+ "el-b","","section#other",note
1539
+ EOF
1540
+ out=$(read_out) || fail "read failed on a capture containing a malformed item"
1541
+ assert_contains "$out" "declared_items: 2" "a malformed capture lost its declared count"
1542
+ assert_contains "$out" "presented_items: 1" \
1543
+ "a row missing declared fields was certified as presented"
1544
+ assert_contains "$out" "malformed_items: 1" "a malformed row was not reported"
1545
+ assert_contains "$out" "complete: no" "a malformed row was certified as complete"
1546
+ assert_contains "$out" "| Complete annotation" \
1547
+ "a valid annotation beside a malformed row was not presented"
1548
+ pass "read never certifies rows missing declared fields as complete"
1549
+
1550
+ cat > "$READ" <<'EOF'
1551
+ session:
1552
+ file: /review.html
1553
+ status: feedback
1554
+ session_ended: true
1555
+ ended_by: user
1556
+ prompts[3]{uid,prompt,selector,tag,text}:
1557
+ "el-a","","section#call > p:nth-of-type(1)",note,"Membership gold-only callout"
1558
+ "el-b","","section#call > h1",note,"Headline pick"
1559
+ "el-c","","aside.sidebar",note,"Sidebar note"
1560
+ EOF
1561
+ out=$(read_out) || fail "read failed on an annotations-only capture"
1562
+ assert_contains "$out" "SESSION-ENDING MESSAGE: (none)" \
1563
+ "a capture with no freeform message still invented a session-ending field body"
1564
+ assert_contains "$out" "declared_items: 3" "the declared item count is missing when there is no message"
1565
+ assert_contains "$out" "presented_items: 3" "not every annotation was presented when there is no message"
1566
+ assert_contains "$out" "complete: yes" "an annotations-only capture was not marked complete"
1567
+ assert_contains "$out" "annotation_count: 3" "annotations were dropped when the freeform message is absent"
1568
+ assert_contains "$out" "| Membership gold-only callout" "an element annotation was dropped when there is no message"
1569
+ assert_contains "$out" "| Headline pick" "an element annotation was dropped when there is no message"
1570
+ assert_contains "$out" "| Sidebar note" "an element annotation was dropped when there is no message"
1571
+ assert_contains "$out" "session_ending_message_count: 0" \
1572
+ "an absent freeform message was counted as present"
1573
+ assert_not_contains "$out" $'\nprompt:\n' \
1574
+ "a capture with no typed comments invented a comment field"
1575
+ assert_not_contains "$out" "CAPTAIN FINAL DECISION" "a prior capture leaked into the next read"
1576
+ pass "read keeps every annotation when the session-ending message is absent"
1577
+
1578
+ # Real Lavish payload shapes, not the prompt==text test-fixture echo:
1579
+ # a pure annotation has element text and an empty prompt; a typed comment is a
1580
+ # nonempty prompt even when it happens to match the element text; choice rows
1581
+ # carry Context data that must not be presented as a comment.
1582
+ cat > "$READ" <<'EOF'
1583
+ session:
1584
+ file: /review.html
1585
+ status: feedback
1586
+ session_ended: true
1587
+ ended_by: user
1588
+ prompts[1]{uid,prompt,selector,tag,text}:
1589
+ "el-n1","are we able to tell which model id belongs to a subscription vs an api key? generally speaking we should favor subscription quota when it is a tie","section#n1 > div",div,"Deterministic tie-break for ambiguous model ids (N1)MY PICK"
1590
+ EOF
1591
+ out=$(read_out) || fail "read failed on an annotate-plus-comment capture"
1592
+ assert_contains "$out" $'\nprompt:\n' \
1593
+ "a typed comment on an annotated element was not a field of its own"
1594
+ assert_contains "$out" "are we able to tell which model id belongs to a subscription vs an api key? generally speaking we should favor subscription quota when it is a tie" \
1595
+ "a typed comment on an annotated element was dropped"
1596
+ assert_contains "$out" "| Deterministic tie-break for ambiguous model ids (N1)MY PICK" \
1597
+ "the annotated element text was dropped when a comment was also present"
1598
+ assert_contains "$out" "element_selector: section#n1 > div" \
1599
+ "the annotated element selector was dropped when a comment was also present"
1600
+ assert_contains "$out" "tag: div" "the annotated element tag was dropped when a comment was also present"
1601
+ assert_contains "$out" "ANNOTATION 1 of 1" "an annotate-plus-comment item was not presented as an annotation"
1602
+ assert_contains "$out" "SESSION-ENDING MESSAGE: (none)" \
1603
+ "an annotate-plus-comment item was reclassified as a session-ending message"
1604
+ assert_contains "$out" "annotation_count: 1" "an annotate-plus-comment item was not counted as an annotation"
1605
+ assert_contains "$out" "session_ending_message_count: 0" \
1606
+ "an annotate-plus-comment item was counted as a session-ending message"
1607
+ pass "read surfaces a typed comment on an annotated element"
1608
+
1609
+ cat > "$READ" <<'EOF'
1610
+ session:
1611
+ file: /review.html
1612
+ status: feedback
1613
+ session_ended: true
1614
+ ended_by: user
1615
+ prompts[1]{uid,prompt,selector,tag,text}:
1616
+ "el-n1","Use subscription quota","section#n1 > div",div,"Use subscription quota"
1617
+ EOF
1618
+ out=$(read_out) || fail "read failed on an equal-text annotate-plus-comment capture"
1619
+ assert_contains "$out" $'text:\n| Use subscription quota\nprompt:\n| Use subscription quota' \
1620
+ "a typed comment identical to the element text was dropped"
1621
+ pass "read still surfaces a typed comment that matches the element text"
1622
+
1623
+ cat > "$READ" <<'EOF'
1624
+ session:
1625
+ file: /review.html
1626
+ status: feedback
1627
+ session_ended: true
1628
+ ended_by: user
1629
+ prompts[1]{uid,prompt,selector,tag,text}:
1630
+ "el-a","","section#call > p:nth-of-type(1)",note,"Membership gold-only callout"
1631
+ EOF
1632
+ out=$(read_out) || fail "read failed on a pure-annotation capture"
1633
+ assert_contains "$out" "| Membership gold-only callout" \
1634
+ "a pure annotation no longer showed the element"
1635
+ assert_contains "$out" "element_selector: section#call > p:nth-of-type(1)" \
1636
+ "a pure annotation lost its selector"
1637
+ assert_contains "$out" "SESSION-ENDING MESSAGE: (none)" \
1638
+ "a pure annotation was treated as a session-ending message"
1639
+ assert_contains "$out" "ANNOTATIONS" "a pure annotation was not presented"
1640
+ assert_not_contains "$out" $'\nprompt:\n' \
1641
+ "a pure annotation with no freeform prompt invented a comment field"
1642
+ pass "read still presents a pure annotation with no comment"
1643
+
1644
+ cat > "$READ" <<'EOF'
1645
+ session:
1646
+ file: /review.html
1647
+ status: feedback
1648
+ session_ended: true
1649
+ ended_by: user
1650
+ prompts[1]{uid,prompt,selector,tag,text}:
1651
+ "el-choice","Context data: {\"question\":\"quota-source\",\"answer\":\"subscription\"}","section#quota > button",choice,"Subscription quota"
1652
+ EOF
1653
+ out=$(read_out) || fail "read failed on a choice capture"
1654
+ assert_contains "$out" "| Subscription quota" \
1655
+ "a choice row no longer showed its element text"
1656
+ assert_contains "$out" "tag: choice" "a choice row lost its type"
1657
+ assert_not_contains "$out" "Context data:" \
1658
+ "a choice row surfaced machine-generated context as a comment"
1659
+ assert_not_contains "$out" $'\nprompt:\n' \
1660
+ "a choice row gained a freeform comment field"
1661
+ pass "read does not present choice context as a comment"
1662
+
1663
+ cat > "$READ" <<'EOF'
1664
+ session:
1665
+ file: /review.html
1666
+ status: feedback
1667
+ session_ended: true
1668
+ ended_by: user
1669
+ prompts[1]{uid,prompt,selector,tag,text}:
1670
+ "","are we able to tell which model id belongs to a subscription vs an api key? generally speaking we should favor subscription quota when it is a tie","",message,Freeform message
1671
+ EOF
1672
+ out=$(read_out) || fail "read failed on a pure-message capture"
1673
+ assert_contains "$out" "SESSION-ENDING MESSAGE" "a pure message lost its labeled field"
1674
+ assert_contains "$out" "| are we able to tell which model id belongs to a subscription vs an api key? generally speaking we should favor subscription quota when it is a tie" \
1675
+ "a pure message dropped the typed comment"
1676
+ assert_contains "$out" "ANNOTATIONS: (none)" "a pure message was presented as an annotation"
1677
+ assert_contains "$out" "session_ending_message_count: 1" "a pure message was not counted"
1678
+ assert_contains "$out" "annotation_count: 0" "a pure message was counted as an annotation"
1679
+ assert_not_contains "$out" "tag: message" \
1680
+ "a pure message was presented as just another annotation"
1681
+ pass "read still presents a pure message with no selector"
1682
+
1683
+ cat > "$READ" <<'EOF'
1684
+ session:
1685
+ file: /review.html
1686
+ status: feedback
1687
+ session_ended: true
1688
+ ended_by: user
1689
+ feedback[1]{text}:
1690
+ ship it
1691
+ EOF
1692
+ out=$(read_out) || fail "read failed on a feedback capture"
1693
+ assert_contains "$out" "lifecycle: feedback" "a feedback capture did not report feedback"
1694
+ assert_contains "$out" "declared_items: 1" "a feedback capture hid its declared count"
1695
+ assert_contains "$out" "presented_items: 1" "a feedback capture dropped its queued item"
1696
+ assert_contains "$out" "| ship it" "a feedback capture dropped the queued text"
1697
+ assert_contains "$out" "SESSION-ENDING MESSAGE: (none)" \
1698
+ "untagged feedback text was treated as a session-ending message"
1699
+ assert_contains "$out" "ANNOTATIONS" "untagged feedback text was not presented as an annotation"
1700
+
1701
+ cat > "$READ" <<'EOF'
1702
+ session:
1703
+ file: /review.html
1704
+ status: ended
1705
+ ended_by: user
1706
+ EOF
1707
+ out=$(read_out) || fail "read failed on an ended-with-nothing capture"
1708
+ assert_contains "$out" "lifecycle: ended" "an empty board close did not report ended"
1709
+ assert_contains "$out" "declared_items: 0" "an empty board close invented queued items"
1710
+ assert_contains "$out" "presented_items: 0" "an empty board close invented presented items"
1711
+ assert_contains "$out" "complete: yes" "an empty board close was not marked complete"
1712
+ assert_contains "$out" "SESSION-ENDING MESSAGE: (none)" \
1713
+ "an empty board close invented a session-ending message"
1714
+ assert_contains "$out" "ANNOTATIONS: (none)" "an empty board close invented annotations"
1715
+ pass "read distinguishes a feedback capture from an ended-with-nothing close"
1716
+
1717
+ # The runner's silence seam is generic and closed by default: an adapter with no
1718
+ # `silent` command must keep announcing, so adding the seam changed nothing for
1719
+ # every adapter that has no notion of a no-op.
1720
+ printf 'session:\n file: /a.html\n status: ended\n ended_by: user\n' > "$SIL"
1721
+ for adapter in remote-reply when; do
1722
+ ! "$ROOT/bin/fm-procevent-$adapter.sh" silent "$SIL" >/dev/null 2>&1 \
1723
+ || fail "the $adapter adapter declared silence without implementing the seam"
1724
+ done
1725
+ pass "an adapter with no silence verdict keeps announcing every result"
1726
+
1727
+ # --- the loss limitation is stated on the public interface ------------------
1728
+ # Checked through --help, the operator-facing surface, rather than by reading
1729
+ # implementation bytes.
1730
+ adapter_help=$("$ROOT/bin/fm-procevent-lavish.sh" --help 2>&1 || true)
1731
+ assert_contains "$adapter_help" "destructively clears" \
1732
+ "the adapter's help states the destructive-source loss limitation"
1733
+ assert_contains "$adapter_help" "Never describe" \
1734
+ "the adapter's help forbids an at-least-once or lossless description"
1735
+ assert_contains "$adapter_help" "read <result-file>" \
1736
+ "the adapter's help publishes the structured read command"
1737
+
1738
+ runner_help=$("$ROOT/bin/fm-procevent.sh" --help 2>&1 || true)
1739
+ assert_contains "$runner_help" "Durability boundary" \
1740
+ "the runner's help scopes what it actually proves"
1741
+ assert_not_contains "$runner_help" "exactly-once" \
1742
+ "the runner's help claims no exactly-once delivery"
1743
+ pass "the published interfaces state the loss limitation and claim no lossless delivery"
1744
+
1745
+ printf '\nall procevent tests passed\n'