@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,1849 @@
1
+ #!/usr/bin/env bash
2
+ # Shared durable wake queue and portable lock helpers.
3
+
4
+ BROKK_WAKE_LIB_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
5
+ BROKK_WAKE_DEFAULT_ROOT="$(cd "$BROKK_WAKE_LIB_DIR/.." && pwd)"
6
+ BROKK_ROOT="${BROKK_ROOT_OVERRIDE:-${BROKK_ROOT:-$BROKK_WAKE_DEFAULT_ROOT}}"
7
+ BROKK_HOME="${BROKK_HOME:-${BROKK_ROOT_OVERRIDE:-$BROKK_ROOT}}"
8
+ STATE="${BROKK_STATE_OVERRIDE:-${STATE:-$BROKK_HOME/state}}"
9
+ BROKK_WAKE_QUEUE="${BROKK_WAKE_QUEUE:-$STATE/.wake-queue}"
10
+ BROKK_WAKE_QUEUE_LOCK="${BROKK_WAKE_QUEUE_LOCK:-$STATE/.wake-queue.lock}"
11
+ BROKK_LOCK_STALE_AFTER="${BROKK_LOCK_STALE_AFTER:-2}"
12
+ # Resolved once at source time: fm_pid_identity and fm_path_mtime run inside 0.2s
13
+ # confirm and 0.5s attach polls, and forking uname per call is a measurable cost on
14
+ # the platform (Git Bash/MSYS) that already pays the highest fork price.
15
+ _BROKK_UNAME=$(uname 2>/dev/null || echo unknown)
16
+ mkdir -p "$STATE"
17
+
18
+ # Most wake-library consumers need only queue and lock primitives, including
19
+ # deliberately minimal recovery fixtures and remote installations.
20
+ # Load the classifier only when a status presentation helper is actually used.
21
+ _fm_wake_require_classify() {
22
+ command -v status_observed_signature >/dev/null 2>&1 && return 0
23
+ # shellcheck source=bin/brokk-classify-lib.sh
24
+ . "$BROKK_WAKE_LIB_DIR/brokk-classify-lib.sh"
25
+ }
26
+
27
+ fm_current_pid() {
28
+ printf '%s\n' "${BASHPID:-$$}"
29
+ }
30
+
31
+ fm_pid_alive() {
32
+ local pid=$1
33
+ case "$pid" in
34
+ ''|*[!0-9]*) return 1 ;;
35
+ esac
36
+ kill -0 "$pid" 2>/dev/null
37
+ }
38
+
39
+ fm_pid_identity() {
40
+ local pid=$1 out proc_root stat_line starttime cmdline_hex identity_key
41
+ local -a stat_fields
42
+ case "$pid" in
43
+ ''|*[!0-9]*) return 1 ;;
44
+ esac
45
+ proc_root=${BROKK_PROC_ROOT_OVERRIDE:-/proc}
46
+ # Prefer a Linux-compatible /proc when present: stat field 22 (starttime, clock ticks since boot) is
47
+ # immune to the wall-clock steps that re-render the ps lstart fallback's date
48
+ # (observed as WSL2 btime drift) and would evict a live watcher; combining the
49
+ # full NUL-separated cmdline keeps PID reuse a mismatch even on a tick collision.
50
+ # Git Bash/MSYS exposes these compatible files but its Cygwin ps rejects the
51
+ # portable fallback's -o fields, so capability detection must not key on uname.
52
+ if [ -r "$proc_root/$pid/stat" ] && [ -r "$proc_root/$pid/cmdline" ]; then
53
+ stat_line=$(cat "$proc_root/$pid/stat" 2>/dev/null) || return 1
54
+ # After the final comm delimiter, array index 19 is proc stat field 22.
55
+ read -r -a stat_fields <<< "${stat_line##*)}"
56
+ [ "${#stat_fields[@]}" -ge 20 ] || return 1
57
+ starttime=${stat_fields[19]}
58
+ case "$starttime" in
59
+ ''|*[!0-9]*) return 1 ;;
60
+ esac
61
+ cmdline_hex=$(od -An -v -tx1 "$proc_root/$pid/cmdline" 2>/dev/null | tr -d '[:space:]') || return 1
62
+ [ -n "$cmdline_hex" ] || return 1
63
+ identity_key=proc-starttime
64
+ [ "$_BROKK_UNAME" != Linux ] || identity_key=linux-starttime
65
+ printf '%s=%s cmdline-hex=%s\n' "$identity_key" "$starttime" "$cmdline_hex"
66
+ return 0
67
+ fi
68
+ # Pin LC_ALL=C so lstart's date format is locale-invariant: the identity is
69
+ # written under one locale but re-read under the machine's ambient locale, which
70
+ # would otherwise mismatch on a non-C locale (e.g. ko_KR) and reject a live watcher.
71
+ out=$(LC_ALL=C ps -p "$pid" -o lstart= -o command= 2>/dev/null) || return 1
72
+ [ -n "$out" ] || return 1
73
+ printf '%s\n' "$out" | sed 's/^[[:space:]]*//'
74
+ }
75
+
76
+ fm_path_mtime() {
77
+ if [ "$_BROKK_UNAME" = Darwin ]; then
78
+ stat -f %m "$1" 2>/dev/null
79
+ else
80
+ stat -c %Y "$1" 2>/dev/null
81
+ fi
82
+ }
83
+
84
+ fm_path_age() {
85
+ local path=$1 m
86
+ m=$(fm_path_mtime "$path") || { echo 999999; return; }
87
+ echo $(( $(date +%s) - m ))
88
+ }
89
+
90
+ # fm_watcher_lock_unheld <state>
91
+ # True when the watcher lock or its symlinked owner directory is absent, or when
92
+ # the existing lock records no pid at all. Any non-empty pid remains held here;
93
+ # its syntax, liveness, ownership metadata, and identity are health concerns.
94
+ fm_watcher_lock_unheld() {
95
+ local state=$1 lockdir pid
96
+ lockdir="$state/.watch.lock"
97
+ [ ! -e "$lockdir" ] && return 0
98
+ [ ! -e "$lockdir/pid" ] && return 0
99
+ pid=$(cat "$lockdir/pid" 2>/dev/null) || return 1
100
+ [ -z "$pid" ]
101
+ }
102
+
103
+ BROKK_WATCHER_MATCHED_IDENTITY=
104
+ fm_watcher_lock_matches_pid() {
105
+ local state=$1 watch_path=$2 pid=$3 home=${4:-$BROKK_HOME} lockdir lock_home lock_path lock_identity current_identity
106
+ BROKK_WATCHER_MATCHED_IDENTITY=
107
+ lockdir="$state/.watch.lock"
108
+ lock_home=$(cat "$lockdir/brokk-home" 2>/dev/null || true)
109
+ lock_path=$(cat "$lockdir/watcher-path" 2>/dev/null || true)
110
+ lock_identity=$(cat "$lockdir/pid-identity" 2>/dev/null || true)
111
+ [ "$lock_home" = "$home" ] || return 1
112
+ [ "$lock_path" = "$watch_path" ] || return 1
113
+ [ -n "$lock_identity" ] || return 1
114
+ current_identity=$(fm_pid_identity "$pid") || return 1
115
+ [ "$current_identity" = "$lock_identity" ] || return 1
116
+ BROKK_WATCHER_MATCHED_IDENTITY=$lock_identity
117
+ }
118
+
119
+ BROKK_WATCHER_HEALTHY_PID=
120
+ BROKK_WATCHER_HEALTHY_IDENTITY=
121
+ fm_watcher_healthy() {
122
+ local state=$1 watch_path=$2 grace=${3:-${BROKK_GUARD_GRACE:-300}} home=${4:-$BROKK_HOME} lockdir beat pid identity age
123
+ BROKK_WATCHER_HEALTHY_PID=
124
+ BROKK_WATCHER_HEALTHY_IDENTITY=
125
+ lockdir="$state/.watch.lock"
126
+ beat="$state/.last-watcher-beat"
127
+ pid=$(cat "$lockdir/pid" 2>/dev/null || true)
128
+ fm_pid_alive "$pid" || return 1
129
+ fm_watcher_lock_matches_pid "$state" "$watch_path" "$pid" "$home" || return 1
130
+ identity=$BROKK_WATCHER_MATCHED_IDENTITY
131
+ age=$(fm_path_age "$beat")
132
+ [ "$age" -lt "$grace" ] || return 1
133
+ # shellcheck disable=SC2034 # Read by callers after fm_watcher_healthy returns.
134
+ BROKK_WATCHER_HEALTHY_PID=$pid
135
+ # shellcheck disable=SC2034 # Read by callers after fm_watcher_healthy returns.
136
+ BROKK_WATCHER_HEALTHY_IDENTITY=$identity
137
+ return 0
138
+ }
139
+
140
+ # fm_watcher_healthy above is the PID-STRICT primitive: true only when a live,
141
+ # identity-matched watcher PROCESS holds this home's lock with a fresh beacon. The
142
+ # arm layer (bin/syn-watch-arm.sh, bin/brokk-claude-stop-autoarm.sh) needs exactly
143
+ # that - it decides whether to start, attach to, or replace a real watcher
144
+ # process, so a leftover beacon must never satisfy it. bin/syn-turnend-guard.sh
145
+ # also keeps this strict check because it fires at the turn boundary where the
146
+ # auto-arm brings a fresh watcher up. The pull warning (bin/brokk-guard.sh) fires
147
+ # mid-turn, where the auto-arm model runs no watcher at all, so it wants a
148
+ # different, model-aware question:
149
+
150
+ # fm_supervision_model
151
+ # Print the supervision model of this home's PRIMARY harness:
152
+ # autoarm Claude's Stop-hook auto-arm and Cursor's stop-hook park: the
153
+ # watcher is armed at each turn end and exits on its wake, so it
154
+ # runs only BETWEEN turns. Mid-turn a fresh beacon with no live
155
+ # watcher process is the healthy state.
156
+ # extension Pi (and pi-signed): .pi/extensions/brokk-primary-pi-watch.ts owns
157
+ # continuity. It tears the watcher down on every actionable wake and
158
+ # spawns the replacement itself, so a genuinely unheld singleton lock
159
+ # is healthy during that hand-off only with extension ownership and a
160
+ # fresh beacon. Any held but unhealthy lock remains down.
161
+ # persistent every other harness (codex foreground checkpoint, opencode/grok
162
+ # background arm, tmux, unknown): the watcher runs as a tracked live
163
+ # process, so a live identity-matched pid is the real liveness signal.
164
+ # BROKK_SUPERVISION_MODEL overrides detection (tests, and callers that already know
165
+ # the harness). Otherwise bin/hamr-harness.sh is the single detection owner, so this
166
+ # stays consistent with the harness-specific repair line the guards already emit.
167
+ fm_supervision_model() {
168
+ local harness
169
+ case "${BROKK_SUPERVISION_MODEL:-}" in
170
+ autoarm|extension|persistent) printf '%s\n' "$BROKK_SUPERVISION_MODEL"; return 0 ;;
171
+ esac
172
+ harness=$("$BROKK_WAKE_LIB_DIR/hamr-harness.sh" 2>/dev/null || printf unknown)
173
+ case "$harness" in
174
+ claude|cursor) printf 'autoarm\n' ;;
175
+ pi|pi-signed) printf 'extension\n' ;;
176
+ *) printf 'persistent\n' ;;
177
+ esac
178
+ }
179
+
180
+ # Pi primary supervision evidence. The Pi extensions record, in their state
181
+ # markers, the exact build they loaded and the session process that loaded it, so
182
+ # "a live Pi session owns supervision" is provable from durable state without a
183
+ # watcher process and without reading any vendor-rendered surface.
184
+ #
185
+ # fm_pi_extension_version <file>
186
+ # Print the marker version string the Pi extensions record for <file>. Must stay
187
+ # byte-identical to the "sha256:<hex>" digest .pi/extensions/brokk-primary-pi-watch.ts
188
+ # and .pi/extensions/brokk-primary-turnend-guard.ts compute for themselves; a host
189
+ # with no SHA-256 tool falls back to a form no marker can match, which keeps every
190
+ # consumer loud rather than silently satisfied.
191
+ fm_pi_extension_version() {
192
+ local file=$1
193
+ [ -f "$file" ] || return 1
194
+ if command -v shasum >/dev/null 2>&1; then
195
+ shasum -a 256 "$file" | awk '{print "sha256:" $1}'
196
+ elif command -v sha256sum >/dev/null 2>&1; then
197
+ sha256sum "$file" | awk '{print "sha256:" $1}'
198
+ else
199
+ cksum "$file" | awk '{print "cksum:" $1 ":" $2}'
200
+ fi
201
+ }
202
+
203
+ # fm_pi_extension_loaded <marker> <expected-version> <session-lock>
204
+ # True when <marker> records <expected-version> and names the session process in
205
+ # <session-lock>, i.e. the session holding this home loaded exactly this build.
206
+ fm_pi_extension_loaded() {
207
+ local marker=$1 expected_version=$2 lock=$3 marker_version marker_pid lock_pid
208
+ [ -f "$marker" ] && [ -f "$lock" ] && [ -n "$expected_version" ] || return 1
209
+ marker_version=$(sed -n '1p' "$marker")
210
+ marker_pid=$(sed -n '2p' "$marker")
211
+ lock_pid=$(sed -n '1p' "$lock")
212
+ [ -n "$marker_pid" ] || return 1
213
+ [ "$marker_version" = "$expected_version" ] && [ "$marker_pid" = "$lock_pid" ]
214
+ }
215
+
216
+ # fm_pi_extension_owns_supervision <state> <root>
217
+ # True when a LIVE Pi session owns supervision continuity for this home: both
218
+ # primary extensions are loaded at their current on-disk builds by the process
219
+ # recorded in this home's session lock, and that process is still alive.
220
+ # Requiring the turn-end guard extension too is deliberate - it is the structural
221
+ # backstop that catches a cycle the watch extension failed to restore, so a home
222
+ # missing it has no benign hand-off to tolerate.
223
+ fm_pi_extension_owns_supervision() {
224
+ local state=$1 root=$2 lock session_pid pair source marker version
225
+ lock="$state/.lock"
226
+ for pair in \
227
+ "brokk-primary-pi-watch.ts:.pi-watch-extension-loaded" \
228
+ "brokk-primary-turnend-guard.ts:.pi-turnend-extension-loaded"; do
229
+ source=${pair%%:*}
230
+ marker=${pair#*:}
231
+ version=$(fm_pi_extension_version "$root/.pi/extensions/$source") || return 1
232
+ fm_pi_extension_loaded "$state/$marker" "$version" "$lock" || return 1
233
+ done
234
+ session_pid=$(sed -n '1p' "$lock" 2>/dev/null)
235
+ fm_pid_alive "$session_pid"
236
+ }
237
+
238
+ # fm_watcher_supervision_verdict <state> <watch-path> [grace] [home] [root]
239
+ # Model-aware "is supervision healthy right now" verdict for the pull warning
240
+ # guard (bin/brokk-guard.sh), NOT the arm layer or the turn-end guard. Sets:
241
+ # BROKK_WATCHER_VERDICT_OK true when supervision is healthy for this model
242
+ # BROKK_WATCHER_VERDICT_REASON when not ok, the true failing condition:
243
+ # no-watcher - a live watcher process is the real
244
+ # signal for this model but none holds
245
+ # the lock (the beacon is still fresh)
246
+ # stale-beacon - the beacon is stale beyond grace or
247
+ # absent (a genuine supervision lapse)
248
+ # autoarm: a fresh beacon within grace is healthy even with no live watcher,
249
+ # because the watcher only runs between turns; only a stale beacon is a lapse.
250
+ # extension: a live identity-matched watcher is the ordinary healthy state, but a
251
+ # genuinely unheld lock is also healthy while the beacon is fresh AND a live Pi
252
+ # session provably owns continuity (fm_pi_extension_owns_supervision) - that is the
253
+ # extension's own tear-down-and-respawn hand-off, which it retries and escalates
254
+ # itself. A lock with any recorded pid remains down if the strict health check fails.
255
+ # Without ownership proof an unheld lock is down exactly as before, so an unloaded,
256
+ # version-drifted, or exited Pi session still alarms immediately, and a cycle the
257
+ # extension never restores still alarms once the beacon passes grace.
258
+ # persistent: require a live identity-matched watcher with a fresh beacon
259
+ # (fm_watcher_healthy); a fresh leftover beacon with no live watcher is still down.
260
+ # shellcheck disable=SC2034 # Read by callers after the function returns.
261
+ BROKK_WATCHER_VERDICT_OK=false
262
+ # shellcheck disable=SC2034 # Read by callers after the function returns.
263
+ BROKK_WATCHER_VERDICT_REASON=stale-beacon
264
+ fm_watcher_supervision_verdict() {
265
+ local state=$1 watch=$2 grace=${3:-${BROKK_GUARD_GRACE:-300}} home=${4:-$BROKK_HOME}
266
+ local root=${5:-$BROKK_ROOT}
267
+ local beat age fresh=false model
268
+ BROKK_WATCHER_VERDICT_OK=false
269
+ BROKK_WATCHER_VERDICT_REASON=stale-beacon
270
+ beat="$state/.last-watcher-beat"
271
+ age=$(fm_path_age "$beat")
272
+ case "$age" in
273
+ ''|*[!0-9]*) ;;
274
+ *) [ "$age" -lt "$grace" ] && fresh=true ;;
275
+ esac
276
+ model=$(fm_supervision_model)
277
+ if [ "$model" = autoarm ]; then
278
+ [ "$fresh" = true ] && BROKK_WATCHER_VERDICT_OK=true
279
+ return 0
280
+ fi
281
+ if fm_watcher_healthy "$state" "$watch" "$grace" "$home"; then
282
+ # shellcheck disable=SC2034 # Read by callers after the function returns.
283
+ BROKK_WATCHER_VERDICT_OK=true
284
+ elif [ "$fresh" = true ]; then
285
+ if [ "$model" = extension ] && fm_watcher_lock_unheld "$state" \
286
+ && fm_pi_extension_owns_supervision "$state" "$root"; then
287
+ # shellcheck disable=SC2034 # Read by callers after the function returns.
288
+ BROKK_WATCHER_VERDICT_OK=true
289
+ else
290
+ # shellcheck disable=SC2034 # Read by callers after the function returns.
291
+ BROKK_WATCHER_VERDICT_REASON=no-watcher
292
+ fi
293
+ fi
294
+ return 0
295
+ }
296
+
297
+ fm_lock_clean_known_files() {
298
+ local lockdir=$1
299
+ rm -f \
300
+ "$lockdir/pid" \
301
+ "$lockdir/brokk-home" \
302
+ "$lockdir/pid-identity" \
303
+ "$lockdir/role" \
304
+ "$lockdir/watcher-path" \
305
+ 2>/dev/null || true
306
+ }
307
+
308
+ fm_lock_set_role() {
309
+ local lockdir=$1 role=$2 current pid back
310
+ case "$role" in
311
+ autoarm|terminal-check) : ;;
312
+ *) return 1 ;;
313
+ esac
314
+ current=${BASHPID:-$$}
315
+ pid=$(cat "$lockdir/pid" 2>/dev/null || true)
316
+ [ "$pid" = "$current" ] || return 1
317
+ printf '%s\n' "$role" > "$lockdir/role" 2>/dev/null || return 1
318
+ back=$(cat "$lockdir/role" 2>/dev/null || true)
319
+ [ "$back" = "$role" ]
320
+ }
321
+
322
+ fm_lock_role() {
323
+ cat "$1/role" 2>/dev/null
324
+ }
325
+
326
+ fm_lock_abs_path() {
327
+ local path=$1 dir base
328
+ dir=$(dirname "$path")
329
+ base=$(basename "$path")
330
+ dir=$(cd "$dir" 2>/dev/null && pwd -P) || return 1
331
+ printf '%s/%s\n' "$dir" "$base"
332
+ }
333
+
334
+ fm_lock_owner_dir() {
335
+ local lockdir=$1 lock_abs
336
+ lock_abs=$(fm_lock_abs_path "$lockdir") || return 1
337
+ mktemp -d "${lock_abs}.owner.XXXXXX" 2>/dev/null
338
+ }
339
+
340
+ fm_lock_prepare_owner() {
341
+ local ownerdir=$1 mypid back
342
+ mypid=${BASHPID:-$$}
343
+ printf '%s\n' "$mypid" > "$ownerdir/pid" 2>/dev/null || return 1
344
+ back=$(cat "$ownerdir/pid" 2>/dev/null || true)
345
+ [ "$back" = "$mypid" ]
346
+ }
347
+
348
+ fm_lock_link_owner() {
349
+ local lockdir=$1 owner
350
+ owner=$(readlink "$lockdir" 2>/dev/null) || return 1
351
+ [ -n "$owner" ] || return 1
352
+ case "$owner" in
353
+ /*) printf '%s\n' "$owner" ;;
354
+ *) printf '%s/%s\n' "$(dirname "$lockdir")" "$owner" ;;
355
+ esac
356
+ }
357
+
358
+ fm_lock_points_to_owner() {
359
+ local lockdir=$1 ownerdir=$2 actual
360
+ actual=$(readlink "$lockdir" 2>/dev/null) || return 1
361
+ [ "$actual" = "$ownerdir" ]
362
+ }
363
+
364
+ fm_lock_discard_owner() {
365
+ local ownerdir=$1
366
+ [ -n "$ownerdir" ] || return 0
367
+ fm_lock_clean_known_files "$ownerdir"
368
+ rmdir "$ownerdir" 2>/dev/null || true
369
+ }
370
+
371
+ fm_lock_remove_stray_owner_link() {
372
+ local lockdir=$1 ownerdir=$2 stray
373
+ stray="$lockdir/$(basename "$ownerdir")"
374
+ if [ -L "$stray" ] && [ "$(readlink "$stray" 2>/dev/null || true)" = "$ownerdir" ]; then
375
+ rm -f "$stray" 2>/dev/null || true
376
+ fi
377
+ }
378
+
379
+ fm_lock_claim_blocked_by_steal() {
380
+ local lockdir=$1 allowed_steal_owner=${2:-} steal
381
+ steal="$lockdir.steal"
382
+ [ -e "$steal" ] || [ -L "$steal" ] || return 1
383
+ if [ -n "$allowed_steal_owner" ] && fm_lock_points_to_owner "$steal" "$allowed_steal_owner"; then
384
+ return 1
385
+ fi
386
+ return 0
387
+ }
388
+
389
+ fm_lock_claim() {
390
+ local lockdir=$1 ownerdir=$2 allowed_steal_owner=${3:-} mypid back
391
+ mypid=${BASHPID:-$$}
392
+ if ! { printf '%s\n' "$mypid" > "$ownerdir/pid"; } 2>/dev/null; then
393
+ fm_lock_discard_owner "$ownerdir"
394
+ return 1
395
+ fi
396
+ back=$(cat "$ownerdir/pid" 2>/dev/null || true)
397
+ if [ "$back" != "$mypid" ]; then
398
+ fm_lock_discard_owner "$ownerdir"
399
+ return 1
400
+ fi
401
+ if ! fm_lock_points_to_owner "$lockdir" "$ownerdir"; then
402
+ fm_lock_discard_owner "$ownerdir"
403
+ return 1
404
+ fi
405
+ if fm_lock_claim_blocked_by_steal "$lockdir" "$allowed_steal_owner"; then
406
+ if fm_lock_points_to_owner "$lockdir" "$ownerdir"; then
407
+ rm -f "$lockdir" 2>/dev/null || true
408
+ fi
409
+ fm_lock_discard_owner "$ownerdir"
410
+ return 1
411
+ fi
412
+ return 0
413
+ }
414
+
415
+ fm_lock_try_create() {
416
+ local lockdir=$1 allowed_steal_owner=${2:-} ownerdir
417
+ BROKK_LOCK_OWNER_DIR=
418
+ ownerdir=$(fm_lock_owner_dir "$lockdir") || return 1
419
+ if [ -e "$lockdir" ] || [ -L "$lockdir" ]; then
420
+ fm_lock_discard_owner "$ownerdir"
421
+ return 1
422
+ fi
423
+ if ! fm_lock_prepare_owner "$ownerdir"; then
424
+ fm_lock_discard_owner "$ownerdir"
425
+ return 1
426
+ fi
427
+ if ln -s "$ownerdir" "$lockdir" 2>/dev/null && fm_lock_points_to_owner "$lockdir" "$ownerdir"; then
428
+ if fm_lock_claim "$lockdir" "$ownerdir" "$allowed_steal_owner"; then
429
+ BROKK_LOCK_OWNER_DIR=$ownerdir
430
+ return 0
431
+ fi
432
+ if fm_lock_points_to_owner "$lockdir" "$ownerdir"; then
433
+ rm -f "$lockdir" 2>/dev/null || true
434
+ fi
435
+ else
436
+ fm_lock_remove_stray_owner_link "$lockdir" "$ownerdir"
437
+ fi
438
+ fm_lock_discard_owner "$ownerdir"
439
+ return 1
440
+ }
441
+
442
+ fm_lock_remove_path() {
443
+ local lockdir=$1 ownerdir
444
+ if [ -L "$lockdir" ]; then
445
+ ownerdir=$(fm_lock_link_owner "$lockdir" 2>/dev/null || true)
446
+ rm -f "$lockdir" 2>/dev/null || return 1
447
+ [ -n "$ownerdir" ] && fm_lock_discard_owner "$ownerdir"
448
+ return 0
449
+ fi
450
+ fm_lock_clean_known_files "$lockdir"
451
+ rmdir "$lockdir" 2>/dev/null
452
+ }
453
+
454
+ fm_lock_mid_acquire_is_fresh() {
455
+ local lockdir=$1 pid=$2 mid_acquire_stale
456
+ case "$pid" in
457
+ ''|*[!0-9]*)
458
+ mid_acquire_stale=$BROKK_LOCK_STALE_AFTER
459
+ [ "$mid_acquire_stale" -lt 2 ] && mid_acquire_stale=2
460
+ [ "$(fm_path_age "$lockdir")" -lt "$mid_acquire_stale" ]
461
+ return
462
+ ;;
463
+ esac
464
+ return 1
465
+ }
466
+
467
+ fm_lock_recheck_stale_owner() {
468
+ local lockdir=$1 expected_owner=$2 expected_pid=$3 actual_pid
469
+ if [ -n "$expected_owner" ]; then
470
+ fm_lock_points_to_owner "$lockdir" "$expected_owner" || return 1
471
+ elif [ -e "$lockdir" ] || [ -L "$lockdir" ]; then
472
+ [ -d "$lockdir" ] && [ ! -L "$lockdir" ] || return 1
473
+ fi
474
+ actual_pid=$(cat "$lockdir/pid" 2>/dev/null || true)
475
+ [ "$actual_pid" = "$expected_pid" ] || return 1
476
+ if fm_pid_alive "$actual_pid"; then
477
+ return 1
478
+ fi
479
+ if fm_lock_mid_acquire_is_fresh "$lockdir" "$actual_pid"; then
480
+ return 1
481
+ fi
482
+ return 0
483
+ }
484
+
485
+ BROKK_RECOVERY_MARKER_TOKEN=
486
+ BROKK_RECOVERY_MARKER_ACTION='none'
487
+
488
+ # Token grammar (one owner): <pending|announced|acked>:<handling|downtime>:<generation>
489
+ # docs/watcher-continuity.md owns the recovery-episode contract, including the
490
+ # once-per-generation announcement rule for unacknowledged downtime.
491
+ fm_recovery_marker_read() {
492
+ local marker=$1 line count
493
+ BROKK_RECOVERY_MARKER_TOKEN=
494
+ [ -f "$marker" ] && [ ! -L "$marker" ] || return 1
495
+ count=$(wc -l < "$marker" 2>/dev/null | tr -d '[:space:]') || return 1
496
+ [ "$count" = 1 ] || return 1
497
+ IFS= read -r line < "$marker" || return 1
498
+ case "$line" in
499
+ pending:handling:*|pending:downtime:*|announced:handling:*|announced:downtime:*|acked:handling:*|acked:downtime:*) ;;
500
+ *) return 1 ;;
501
+ esac
502
+ case "${line##*:}" in
503
+ ''|*[!A-Za-z0-9._-]*) return 1 ;;
504
+ esac
505
+ BROKK_RECOVERY_MARKER_TOKEN=$line
506
+ }
507
+
508
+ _fm_atomic_replace() {
509
+ mv -f -- "$1" "$2"
510
+ }
511
+
512
+ _fm_recovery_marker_write_locked() {
513
+ local marker=$1 kind=$2 generation=${3:-} status=${4:-pending} tmp
514
+ case "$kind" in handling|downtime) ;; *) return 1 ;; esac
515
+ case "$status" in pending|announced) ;; *) return 1 ;; esac
516
+ tmp=$(mktemp "${marker}.tmp.XXXXXX") || return 1
517
+ [ -n "$generation" ] || generation="$(fm_current_pid).$(date +%s).${tmp##*.}"
518
+ if ! printf '%s:%s:%s\n' "$status" "$kind" "$generation" > "$tmp" \
519
+ || ! chmod 0600 "$tmp" \
520
+ || ! _fm_atomic_replace "$tmp" "$marker"; then
521
+ rm -f -- "$tmp"
522
+ return 1
523
+ fi
524
+ }
525
+
526
+ # Preserve a pending or announced episode's generation across downtime
527
+ # republication so its outstanding acknowledgement remains usable, and keep an
528
+ # already-announced generation announced so it cannot be re-presented until a
529
+ # new down stretch mints a new generation.
530
+ # docs/watcher-continuity.md owns the recovery contract and sequence-safety rationale.
531
+ _fm_recovery_marker_publish() {
532
+ local marker=$1 kind=${2:-downtime} lock saved_token generation='' status=pending
533
+ case "$kind" in handling|downtime) ;; *) return 1 ;; esac
534
+ lock="${marker}.lock"
535
+ fm_lock_acquire_wait "$lock" || return 1
536
+ if [ -d "$marker" ] && [ ! -L "$marker" ]; then
537
+ fm_lock_release "$lock"
538
+ return 1
539
+ fi
540
+ if [ "$kind" = downtime ]; then
541
+ # Read inline rather than in a command substitution: this runs inside the
542
+ # marker-lock critical section, so it must not add a subshell fork there.
543
+ # The token is restored because publishing owns no snapshot of its own.
544
+ saved_token=$BROKK_RECOVERY_MARKER_TOKEN
545
+ if fm_recovery_marker_read "$marker"; then
546
+ case "$BROKK_RECOVERY_MARKER_TOKEN" in
547
+ pending:handling:*|pending:downtime:*)
548
+ generation=${BROKK_RECOVERY_MARKER_TOKEN##*:}
549
+ status=pending
550
+ ;;
551
+ announced:handling:*|announced:downtime:*)
552
+ generation=${BROKK_RECOVERY_MARKER_TOKEN##*:}
553
+ status=announced
554
+ ;;
555
+ esac
556
+ fi
557
+ BROKK_RECOVERY_MARKER_TOKEN=$saved_token
558
+ fi
559
+ if ! _fm_recovery_marker_write_locked "$marker" "$kind" "$generation" "$status"; then
560
+ fm_lock_release "$lock"
561
+ return 1
562
+ fi
563
+ fm_lock_release "$lock"
564
+ }
565
+
566
+ _fm_recovery_marker_begin_handling() {
567
+ local marker=$1 expected_generation=${2:-} lock line generation
568
+ lock="${marker}.lock"
569
+ fm_lock_acquire_wait "$lock" || return 1
570
+ if ! fm_recovery_marker_read "$marker"; then
571
+ fm_lock_release "$lock"
572
+ return 1
573
+ fi
574
+ line=$BROKK_RECOVERY_MARKER_TOKEN
575
+ generation=${line##*:}
576
+ if [ -n "$expected_generation" ] && [ "$generation" != "$expected_generation" ]; then
577
+ fm_lock_release "$lock"
578
+ return 3
579
+ fi
580
+ case "$line" in
581
+ pending:handling:*|announced:handling:*) ;;
582
+ pending:downtime:*)
583
+ if ! _fm_recovery_marker_write_locked "$marker" handling "$generation"; then
584
+ fm_lock_release "$lock"
585
+ return 1
586
+ fi
587
+ BROKK_RECOVERY_MARKER_TOKEN="pending:handling:$generation"
588
+ ;;
589
+ announced:downtime:*)
590
+ if ! _fm_recovery_marker_write_locked "$marker" handling "$generation" announced; then
591
+ fm_lock_release "$lock"
592
+ return 1
593
+ fi
594
+ BROKK_RECOVERY_MARKER_TOKEN="announced:handling:$generation"
595
+ ;;
596
+ *) fm_lock_release "$lock"; return 1 ;;
597
+ esac
598
+ fm_lock_release "$lock"
599
+ }
600
+
601
+ fm_recovery_marker_snapshot() {
602
+ local marker=$1 lock
603
+ BROKK_RECOVERY_MARKER_TOKEN=
604
+ lock="${marker}.lock"
605
+ fm_lock_acquire_wait "$lock" || return 1
606
+ fm_recovery_marker_read "$marker" || true
607
+ fm_lock_release "$lock"
608
+ }
609
+
610
+ _fm_recovery_marker_ack() {
611
+ local marker=$1 expected_generation=$2 lock tmp line
612
+ [ -n "$expected_generation" ] || return 2
613
+ lock="${marker}.lock"
614
+ fm_lock_acquire_wait "$lock" || return 1
615
+ if ! fm_recovery_marker_read "$marker" \
616
+ || [ "${BROKK_RECOVERY_MARKER_TOKEN##*:}" != "$expected_generation" ]; then
617
+ fm_lock_release "$lock"
618
+ return 3
619
+ fi
620
+ line=$BROKK_RECOVERY_MARKER_TOKEN
621
+ case "$line" in
622
+ pending:*|announced:*) line="acked:${line#*:}" ;;
623
+ acked:*) fm_lock_release "$lock"; return 0 ;;
624
+ *) fm_lock_release "$lock"; return 1 ;;
625
+ esac
626
+ tmp=$(mktemp "${marker}.tmp.XXXXXX") || { fm_lock_release "$lock"; return 1; }
627
+ if ! printf '%s\n' "$line" > "$tmp" \
628
+ || ! chmod 0600 "$tmp" \
629
+ || ! mv -f -- "$tmp" "$marker"; then
630
+ rm -f -- "$tmp"
631
+ fm_lock_release "$lock"
632
+ return 1
633
+ fi
634
+ fm_lock_release "$lock"
635
+ }
636
+
637
+ _fm_recovery_marker_arm_check() {
638
+ local marker=$1 lock line quarantine
639
+ BROKK_RECOVERY_MARKER_ACTION='none'
640
+ lock="${marker}.lock"
641
+ fm_lock_acquire_wait "$BROKK_WAKE_QUEUE_LOCK" || return 1
642
+ if ! fm_lock_acquire_wait "$lock"; then
643
+ fm_lock_release "$BROKK_WAKE_QUEUE_LOCK"
644
+ return 1
645
+ fi
646
+ if [ ! -e "$marker" ] && [ ! -L "$marker" ]; then
647
+ if [ -s "$BROKK_WAKE_QUEUE" ]; then
648
+ if ! _fm_recovery_marker_write_locked "$marker" downtime "" announced; then
649
+ fm_lock_release "$lock"
650
+ fm_lock_release "$BROKK_WAKE_QUEUE_LOCK"
651
+ return 1
652
+ fi
653
+ BROKK_RECOVERY_MARKER_ACTION='recover'
654
+ fi
655
+ fm_lock_release "$lock"
656
+ fm_lock_release "$BROKK_WAKE_QUEUE_LOCK"
657
+ return 0
658
+ fi
659
+ if ! fm_recovery_marker_read "$marker"; then
660
+ quarantine=$(mktemp -d "${marker}.invalid.XXXXXX") \
661
+ || {
662
+ fm_lock_release "$lock"
663
+ fm_lock_release "$BROKK_WAKE_QUEUE_LOCK"
664
+ return 1
665
+ }
666
+ if ! mv -- "$marker" "$quarantine/marker" \
667
+ || ! _fm_recovery_marker_write_locked "$marker" downtime "" announced; then
668
+ rmdir "$quarantine" 2>/dev/null || true
669
+ fm_lock_release "$lock"
670
+ fm_lock_release "$BROKK_WAKE_QUEUE_LOCK"
671
+ return 1
672
+ fi
673
+ BROKK_RECOVERY_MARKER_ACTION='recover'
674
+ fm_lock_release "$lock"
675
+ fm_lock_release "$BROKK_WAKE_QUEUE_LOCK"
676
+ return 0
677
+ fi
678
+ line=$BROKK_RECOVERY_MARKER_TOKEN
679
+ case "$line" in
680
+ pending:handling:*|announced:handling:*|announced:downtime:*)
681
+ BROKK_RECOVERY_MARKER_ACTION='wait'
682
+ fm_lock_release "$lock"
683
+ fm_lock_release "$BROKK_WAKE_QUEUE_LOCK"
684
+ return 0
685
+ ;;
686
+ pending:downtime:*)
687
+ if ! _fm_recovery_marker_write_locked "$marker" downtime "${line##*:}" announced; then
688
+ fm_lock_release "$lock"
689
+ fm_lock_release "$BROKK_WAKE_QUEUE_LOCK"
690
+ return 1
691
+ fi
692
+ BROKK_RECOVERY_MARKER_TOKEN="announced:downtime:${line##*:}"
693
+ BROKK_RECOVERY_MARKER_ACTION='recover'
694
+ ;;
695
+ acked:*)
696
+ if [ -s "$BROKK_WAKE_QUEUE" ]; then
697
+ if ! _fm_recovery_marker_write_locked "$marker" downtime "" announced; then
698
+ fm_lock_release "$lock"
699
+ fm_lock_release "$BROKK_WAKE_QUEUE_LOCK"
700
+ return 1
701
+ fi
702
+ # shellcheck disable=SC2034 # Output read by callers after this function returns.
703
+ BROKK_RECOVERY_MARKER_ACTION='recover'
704
+ fi
705
+ ;;
706
+ esac
707
+ fm_lock_release "$lock"
708
+ fm_lock_release "$BROKK_WAKE_QUEUE_LOCK"
709
+ }
710
+
711
+ # A non-successor watcher start after an announced-but-unacked episode is a new
712
+ # down stretch: mint a fresh pending generation so a still-open decision or
713
+ # buried note can be presented once more. Handling successors must not call
714
+ # this, because Option B re-arm is not a new down stretch.
715
+ _fm_recovery_marker_reopen_announced() {
716
+ local marker=$1 lock
717
+ lock="${marker}.lock"
718
+ fm_lock_acquire_wait "$lock" || return 1
719
+ if ! fm_recovery_marker_read "$marker"; then
720
+ fm_lock_release "$lock"
721
+ return 0
722
+ fi
723
+ case "$BROKK_RECOVERY_MARKER_TOKEN" in
724
+ announced:*)
725
+ if ! _fm_recovery_marker_write_locked "$marker" downtime ""; then
726
+ fm_lock_release "$lock"
727
+ return 1
728
+ fi
729
+ ;;
730
+ esac
731
+ fm_lock_release "$lock"
732
+ }
733
+
734
+ fm_recovery_transition() {
735
+ local marker=$1 action=$2 target=${3:-} value=${4:-}
736
+ case "$action" in
737
+ publish)
738
+ _fm_recovery_marker_publish "$marker" "${target:-downtime}"
739
+ ;;
740
+ acknowledge)
741
+ _fm_recovery_marker_ack "$marker" "$target"
742
+ ;;
743
+ arm-check)
744
+ _fm_recovery_marker_arm_check "$marker"
745
+ ;;
746
+ reopen-announced)
747
+ _fm_recovery_marker_reopen_announced "$marker"
748
+ ;;
749
+ release-lock)
750
+ [ -n "$target" ] || return 1
751
+ _fm_recovery_marker_publish "$marker" "${value:-downtime}" || return 1
752
+ fm_lock_release "$target"
753
+ ;;
754
+ release-lock-existing)
755
+ [ -n "$target" ] || return 1
756
+ local lock="${marker}.lock"
757
+ fm_lock_acquire_wait "$lock" || return 1
758
+ if ! fm_recovery_marker_read "$marker"; then
759
+ fm_lock_release "$lock"
760
+ return 1
761
+ fi
762
+ fm_lock_release "$target"
763
+ fm_lock_release "$lock"
764
+ ;;
765
+ clear-stale-lock)
766
+ [ -n "$target" ] || return 1
767
+ _fm_recovery_marker_publish "$marker" "${value:-downtime}" || return 1
768
+ fm_lock_remove_path "$target"
769
+ ;;
770
+ *) return 2 ;;
771
+ esac
772
+ }
773
+
774
+ fm_recovery_marker_publish() {
775
+ fm_recovery_transition "$1" publish "${2:-downtime}"
776
+ }
777
+
778
+ fm_recovery_marker_ack() {
779
+ fm_recovery_transition "$1" acknowledge "$2"
780
+ }
781
+
782
+ fm_recovery_marker_begin_handling() {
783
+ _fm_recovery_marker_begin_handling "$1" "${2:-}"
784
+ }
785
+
786
+ fm_recovery_marker_arm_check() {
787
+ fm_recovery_transition "$1" arm-check
788
+ }
789
+
790
+ fm_recovery_marker_reopen_announced() {
791
+ fm_recovery_transition "$1" reopen-announced
792
+ }
793
+
794
+ fm_lock_try_acquire() {
795
+ local lockdir=$1 pid steal cur rc steal_owner primary_owner
796
+ BROKK_LOCK_HELD_PID=
797
+ BROKK_LOCK_OWNER_DIR=
798
+ BROKK_LOCK_RECOVERED_PID=
799
+
800
+ if fm_lock_try_create "$lockdir"; then
801
+ return 0
802
+ fi
803
+
804
+ # Compare against ${BASHPID:-$$} inline, never via a command substitution:
805
+ # $() forks a subshell whose BASHPID is not this frame's pid.
806
+ pid=$(cat "$lockdir/pid" 2>/dev/null || true)
807
+ if [ -n "$pid" ] && [ "$pid" = "${BASHPID:-$$}" ]; then
808
+ # The recorded holder is THIS very process. Single-threaded bash can only
809
+ # observe that when an interrupting trap abandoned the frame that held the
810
+ # lock mid-critical-section (e.g. TERM inside a recovery-marker section,
811
+ # with the EXIT path then re-acquiring the same lock), and every
812
+ # lock-taking trap path in this repo exits rather than resuming the
813
+ # interrupted frame. Spinning here deadlocks the exit path against itself
814
+ # - the hang reproduced by the self-held reclaim regression in
815
+ # tests/brokk-wake-queue.test.sh - so reclaim the abandoned hold instead.
816
+ fm_lock_remove_path "$lockdir" || true
817
+ if fm_lock_try_create "$lockdir"; then
818
+ return 0
819
+ fi
820
+ BROKK_LOCK_HELD_PID=$(cat "$lockdir/pid" 2>/dev/null || true)
821
+ return 1
822
+ fi
823
+ if fm_pid_alive "$pid"; then
824
+ BROKK_LOCK_HELD_PID=$pid
825
+ return 1
826
+ fi
827
+ if fm_lock_mid_acquire_is_fresh "$lockdir" "$pid"; then
828
+ BROKK_LOCK_HELD_PID=$pid
829
+ return 1
830
+ fi
831
+
832
+ steal="$lockdir.steal"
833
+ if ! fm_lock_try_acquire "$steal"; then
834
+ BROKK_LOCK_HELD_PID=$(cat "$lockdir/pid" 2>/dev/null || true)
835
+ BROKK_LOCK_OWNER_DIR=
836
+ return 1
837
+ fi
838
+ steal_owner=${BROKK_LOCK_OWNER_DIR:-}
839
+
840
+ cur=$(cat "$lockdir/pid" 2>/dev/null || true)
841
+ if fm_pid_alive "$cur"; then
842
+ fm_lock_release "$steal"
843
+ BROKK_LOCK_HELD_PID=$cur
844
+ BROKK_LOCK_OWNER_DIR=
845
+ return 1
846
+ fi
847
+ if fm_lock_mid_acquire_is_fresh "$lockdir" "$cur"; then
848
+ fm_lock_release "$steal"
849
+ BROKK_LOCK_HELD_PID=$cur
850
+ BROKK_LOCK_OWNER_DIR=
851
+ return 1
852
+ fi
853
+ if ! fm_lock_points_to_owner "$steal" "$steal_owner"; then
854
+ fm_lock_release "$steal"
855
+ BROKK_LOCK_HELD_PID=$(cat "$lockdir/pid" 2>/dev/null || true)
856
+ BROKK_LOCK_OWNER_DIR=
857
+ return 1
858
+ fi
859
+
860
+ primary_owner=
861
+ if [ -L "$lockdir" ]; then
862
+ primary_owner=$(fm_lock_link_owner "$lockdir" 2>/dev/null || true)
863
+ fi
864
+ cur=$(cat "$lockdir/pid" 2>/dev/null || true)
865
+ if ! fm_lock_recheck_stale_owner "$lockdir" "$primary_owner" "$cur"; then
866
+ fm_lock_release "$steal"
867
+ BROKK_LOCK_HELD_PID=$(cat "$lockdir/pid" 2>/dev/null || true)
868
+ BROKK_LOCK_OWNER_DIR=
869
+ return 1
870
+ fi
871
+
872
+ if [ "$lockdir" = "$STATE/.watch.lock" ] \
873
+ && ! _fm_recovery_marker_publish "$STATE/.watcher-down" downtime; then
874
+ fm_lock_release "$steal"
875
+ BROKK_LOCK_HELD_PID=$cur
876
+ BROKK_LOCK_OWNER_DIR=
877
+ return 1
878
+ fi
879
+ fm_lock_remove_path "$lockdir" || true
880
+ rc=1
881
+ if fm_lock_try_create "$lockdir" "$steal_owner"; then
882
+ rc=0
883
+ # shellcheck disable=SC2034 # Read by sourcing callers after lock acquisition.
884
+ BROKK_LOCK_RECOVERED_PID=$cur
885
+ fi
886
+ if [ "$rc" -ne 0 ]; then
887
+ # shellcheck disable=SC2034 # Read by callers after fm_lock_try_acquire returns.
888
+ BROKK_LOCK_HELD_PID=$(cat "$lockdir/pid" 2>/dev/null || true)
889
+ BROKK_LOCK_OWNER_DIR=
890
+ fi
891
+ fm_lock_release "$steal"
892
+ return "$rc"
893
+ }
894
+
895
+ fm_lock_acquire_wait() {
896
+ local lockdir=$1
897
+ while ! fm_lock_try_acquire "$lockdir"; do
898
+ sleep 0.1
899
+ done
900
+ }
901
+
902
+ fm_lock_release() {
903
+ local lockdir=$1 pid current ownerdir
904
+ current=${BASHPID:-$$}
905
+ if [ -L "$lockdir" ]; then
906
+ ownerdir=$(fm_lock_link_owner "$lockdir" 2>/dev/null || true)
907
+ [ -n "$ownerdir" ] || return 0
908
+ pid=$(cat "$ownerdir/pid" 2>/dev/null || true)
909
+ [ "$pid" = "$current" ] || return 0
910
+ fm_lock_points_to_owner "$lockdir" "$ownerdir" || return 0
911
+ rm -f "$lockdir" 2>/dev/null || return 0
912
+ fm_lock_discard_owner "$ownerdir"
913
+ return 0
914
+ fi
915
+ pid=$(cat "$lockdir/pid" 2>/dev/null || true)
916
+ [ "$pid" = "$current" ] || return 0
917
+ fm_lock_clean_known_files "$lockdir"
918
+ rmdir "$lockdir" 2>/dev/null || true
919
+ }
920
+
921
+ fm_meta_lock_path() {
922
+ local meta=$1 dir base id
923
+ dir=${meta%/*}
924
+ base=${meta##*/}
925
+ [ "$dir" != "$meta" ] || dir=.
926
+ case "$base" in
927
+ *.meta) id=${base%.meta} ;;
928
+ *) return 1 ;;
929
+ esac
930
+ case "$id" in
931
+ ''|*[!A-Za-z0-9._-]*) return 1 ;;
932
+ esac
933
+ printf '%s/.meta-%s.lock\n' "$dir" "$id"
934
+ }
935
+
936
+ # fm_task_set_lock_path: the per-home lock guarding WHICH tasks exist in a home,
937
+ # as opposed to fm_meta_lock_path, which guards one task's record.
938
+ #
939
+ # A per-task lock cannot protect a task that does not exist yet. Forced
940
+ # eindri-home teardown enumerates a home's task set, locks what it found, and
941
+ # then re-enumerates while removing; a fresh spawn publishing a record inside
942
+ # that window is invisible to the first enumeration and visible to the second,
943
+ # so it gets destructively processed while never lifecycle-locked (reproduced
944
+ # with real agents: a record published 0.249s after teardown began was removed
945
+ # and its worktree returned to the pool, with both commands reporting success).
946
+ # Holding this lock from enumeration through cleanup makes the two operations
947
+ # serialize: either the spawn publishes first and the teardown's preflight
948
+ # covers it, or the teardown owns the set and the spawn refuses. Both directions
949
+ # fail closed.
950
+ fm_task_set_lock_path() { # <state-dir>
951
+ local state=$1
952
+ [ -n "$state" ] || return 1
953
+ case "$state" in *[$'\n\r\t']*) return 1 ;; esac
954
+ printf '%s/.task-set.lock\n' "$state"
955
+ }
956
+
957
+ fm_failure_episode_reset() {
958
+ local state=$1 mode=${2:-acquire} lock current pid acquired=0 path
959
+ lock="$state/.turnend-claude-blocks.lock"
960
+ case "$mode" in
961
+ acquire)
962
+ fm_lock_try_acquire "$lock" || return 1
963
+ acquired=1
964
+ ;;
965
+ held)
966
+ current=${BASHPID:-$$}
967
+ pid=$(cat "$lock/pid" 2>/dev/null || true)
968
+ [ "$pid" = "$current" ] || return 1
969
+ ;;
970
+ *) return 1 ;;
971
+ esac
972
+ for path in \
973
+ "$state/.turnend-claude-blocks" \
974
+ "$state/.claude-autoarm-failure-notified" \
975
+ "$state/.claude-autoarm-failure-alarmed"
976
+ do
977
+ if [ -d "$path" ] && [ ! -L "$path" ]; then
978
+ [ "$acquired" -eq 0 ] || fm_lock_release "$lock"
979
+ return 1
980
+ fi
981
+ done
982
+ if ! rm -f \
983
+ "$state/.turnend-claude-blocks" \
984
+ "$state/.claude-autoarm-failure-notified" \
985
+ "$state/.claude-autoarm-failure-alarmed" \
986
+ 2>/dev/null; then
987
+ [ "$acquired" -eq 0 ] || fm_lock_release "$lock"
988
+ return 1
989
+ fi
990
+ [ "$acquired" -eq 0 ] || fm_lock_release "$lock"
991
+ return 0
992
+ }
993
+
994
+ # --- Claude Stop auto-arm generation claims -----------------------------------
995
+ # Both Stop-event participants (bin/brokk-claude-stop-autoarm.sh and
996
+ # bin/syn-turnend-guard.sh --claude) coordinate through the epoch ledger
997
+ # state/.claude-autoarm-epoch, whose monotonic epoch sequence IS the claim
998
+ # generation. This is an optimistic, generation-based single-flight design:
999
+ #
1000
+ # - The CURRENT claim is the ledger's latest entry: line 1 is the classic
1001
+ # "epoch=N owner_pid=P outcome=O updated_at=T" record, and line 2 is the
1002
+ # claiming process's pid-identity, the same identity every other
1003
+ # supervision lock in this repo records (fm_pid_identity above). The
1004
+ # identity is MANDATORY: a claimant that cannot record it does not claim
1005
+ # (continuity falls to the synchronous guard), and the identity is read
1006
+ # from the ledger entry alone - never substituted from any lock - so a
1007
+ # reused pid can never authenticate someone else's stale entry.
1008
+ # - A claim is OPEN (fm_autoarm_claim_open) while its outcome is "arming",
1009
+ # its owner pid is alive, its recorded identity successfully recomputes
1010
+ # and matches that pid, and it is not STUCK - stuck meaning both the
1011
+ # ledger entry and the watcher beacon (state/.last-watcher-beat) are older
1012
+ # than the guard grace, which proves the owner hung mid-arm with nothing
1013
+ # supervising (every legitimate arming phase with no watcher is bounded in
1014
+ # seconds, while a healthy hours-long cycle keeps the beacon beating).
1015
+ # - Every firing DEFERS (exits 0) to an open claim; anything else - a
1016
+ # terminal outcome, a dead or identity-mismatched owner, a stuck owner, an
1017
+ # identityless entry, or no claim at all - lets the next firing take
1018
+ # generation N+1 (fm_autoarm_claim_next). Taking a newer generation IS the
1019
+ # reclaim: a steady-state predecessor is never signalled or revoked.
1020
+ # - NO mutex is ever held across a blocking step. The owner lock
1021
+ # state/.claude-autoarm.lock survives only as a micro-mutex serializing
1022
+ # individual ledger reads-then-writes (a few non-blocking file
1023
+ # operations); a holder that dies inside the hold is reclaimed by
1024
+ # fm_lock_try_acquire's ordinary dead-owner steal.
1025
+ # - A superseded owner goes COMPLETELY silent - cleanup only. Ownership is
1026
+ # re-verified before every side effect: each arm invocation, each
1027
+ # episode-state mutation, each ledger write, and each continuation.
1028
+ # - The irrevocable commit point of a translation is the EXIT STATUS: the
1029
+ # harness delivers the collected stderr banner only on exit 2 and discards
1030
+ # it on exit 0. Markerless outcomes commit with the owned terminal ledger
1031
+ # write. The once-per-episode failure notice commits only when its marker is
1032
+ # created after the winning "failed" write in the same owned critical
1033
+ # section. A superseded generation or failed required-marker creation is
1034
+ # refused and exits 0 silently even after printing; a later generation
1035
+ # supersedes the terminal entry and retries the notice.
1036
+ #
1037
+ # This structurally removes the failure classes the lock-held-across-arm
1038
+ # design produced: a hung owner deferring every later firing forever (observed
1039
+ # 2026-08-26: a hook hung mid-arm with its ledger frozen at "arming" kept the
1040
+ # watcher from ever being auto-re-armed again; and 2026-08-14: a finished
1041
+ # claim whose leftover lock silenced both participants for 40 beacon-less
1042
+ # minutes), a reclaim mutex held across a blocking banner write recreating the
1043
+ # same unreclaimable-live-owner shape, and a reclaimed-but-alive owner racing
1044
+ # its replacement to translate one close twice.
1045
+ #
1046
+ # Two bounded residuals are ACCEPTED INTENT, because closing them absolutely
1047
+ # would require a mutex held across output or steady-state revocation, both
1048
+ # deliberately rejected: (1) an owner that dies between its owned terminal
1049
+ # write and its own process exit leaves a committed outcome whose banner was
1050
+ # never delivered (process-death territory; the durable wake queue retains the
1051
+ # underlying event), and (2) a hung old-build owner that resumes during the
1052
+ # one legacy upgrade window may add one duplicate continuation. Each residual
1053
+ # costs at most one extra exit-2 continuation turn absorbed by the durable
1054
+ # idempotent wake queue. A claim misread as stuck in a pathological race
1055
+ # (e.g. a beacon read right at system wake) likewise yields at most one extra
1056
+ # arm that the watcher singleton dedupes, while the superseded owner still
1057
+ # goes silent.
1058
+ #
1059
+ # fm_autoarm_claim_abandoned / fm_autoarm_release_abandoned below survive as
1060
+ # the LEGACY shim for a lock-holding claim from a pre-generation build (the
1061
+ # lock carries a role file only in that legacy shape, and in the guard's own
1062
+ # short terminal-check hold): a live legacy owner still defers per the legacy
1063
+ # proof, and a proven-abandoned one is reclaimed once through the steal mutex
1064
+ # - with an identity-verified live owner retired via TERM first, because old
1065
+ # code cannot re-check generations - so an upgrade mid-session can neither
1066
+ # double-arm nor deadlock behind a hung legacy hook.
1067
+ _fm_autoarm_epoch_field() { # <epoch-file> <field>
1068
+ local file=$1 field=$2 tok
1069
+ local -a toks=()
1070
+ [ -r "$file" ] || return 1
1071
+ # 2> before <: a failed input redirection reports through whatever stderr is
1072
+ # current when it runs, so the suppression has to be established first.
1073
+ IFS=' ' read -r -a toks 2>/dev/null < "$file" || return 1
1074
+ for tok in ${toks[@]+"${toks[@]}"}; do
1075
+ case "$tok" in
1076
+ "$field="?*) printf '%s\n' "${tok#*=}"; return 0 ;;
1077
+ esac
1078
+ done
1079
+ return 1
1080
+ }
1081
+
1082
+ # Parse the current ledger claim. Sets BROKK_AUTOARM_GEN, BROKK_AUTOARM_OWNER,
1083
+ # BROKK_AUTOARM_OUTCOME, and BROKK_AUTOARM_IDENTITY (line 2 of the entry, and ONLY
1084
+ # line 2 - identity is never substituted from a lock, so a transient
1085
+ # micro-mutex hold or a reused pid can never authenticate a stale entry).
1086
+ fm_autoarm_ledger_read() { # <state-dir>
1087
+ local state=$1 epoch
1088
+ epoch="$state/.claude-autoarm-epoch"
1089
+ BROKK_AUTOARM_GEN=
1090
+ BROKK_AUTOARM_OWNER=
1091
+ BROKK_AUTOARM_OUTCOME=
1092
+ BROKK_AUTOARM_IDENTITY=
1093
+ BROKK_AUTOARM_GEN=$(_fm_autoarm_epoch_field "$epoch" epoch) || return 1
1094
+ BROKK_AUTOARM_OWNER=$(_fm_autoarm_epoch_field "$epoch" owner_pid) || return 1
1095
+ BROKK_AUTOARM_OUTCOME=$(_fm_autoarm_epoch_field "$epoch" outcome) || return 1
1096
+ case "$BROKK_AUTOARM_GEN" in
1097
+ ''|*[!0-9]*) return 1 ;;
1098
+ esac
1099
+ BROKK_AUTOARM_IDENTITY=$(sed -n '2p' "$epoch" 2>/dev/null || true)
1100
+ return 0
1101
+ }
1102
+
1103
+ # True while the CURRENT ledger claim is open and healthy - the defer predicate
1104
+ # both Stop participants use. Open means: outcome "arming", a live owner whose
1105
+ # mandatory recorded identity recomputes and matches its pid, and not stuck
1106
+ # (the contract comment above owns the stuck proof). fm_path_age reports an
1107
+ # absent beacon as ancient, which is exactly right: arming for a full grace
1108
+ # window without producing a first beat is the same hang. An identityless
1109
+ # entry is never open: real generation claims always record identity, a legacy
1110
+ # build's entry gets its deference from its held role-carrying lock through
1111
+ # the legacy shim, and anything else must not defer.
1112
+ fm_autoarm_claim_open() { # <state-dir> [grace]
1113
+ local state=$1 grace=${2:-${BROKK_GUARD_GRACE:-300}} epoch current
1114
+ epoch="$state/.claude-autoarm-epoch"
1115
+ case "$grace" in
1116
+ ''|*[!0-9]*|0) grace=300 ;;
1117
+ esac
1118
+ fm_autoarm_ledger_read "$state" || return 1
1119
+ [ "$BROKK_AUTOARM_OUTCOME" = arming ] || return 1
1120
+ fm_pid_alive "$BROKK_AUTOARM_OWNER" || return 1
1121
+ [ -n "$BROKK_AUTOARM_IDENTITY" ] || return 1
1122
+ current=$(fm_pid_identity "$BROKK_AUTOARM_OWNER" 2>/dev/null) || return 1
1123
+ [ -n "$current" ] || return 1
1124
+ [ "$current" = "$BROKK_AUTOARM_IDENTITY" ] || return 1
1125
+ if [ "$(fm_path_age "$epoch")" -ge "$grace" ] \
1126
+ && [ "$(fm_path_age "$state/.last-watcher-beat")" -ge "$grace" ]; then
1127
+ return 1
1128
+ fi
1129
+ return 0
1130
+ }
1131
+
1132
+ # Atomically publish this process as the owner of generation N+1, under one
1133
+ # short micro-mutex hold. Returns 0 with BROKK_AUTOARM_MY_GEN set on success, 2
1134
+ # when a competing claimant won the race (the ledger holds an open claim), and
1135
+ # 1 when the micro-mutex is contended, the mandatory identity cannot be
1136
+ # computed, or the write failed.
1137
+ fm_autoarm_claim_next() { # <state-dir> [grace]
1138
+ local state=$1 grace=${2:-${BROKK_GUARD_GRACE:-300}} lock epoch pid gen identity tmp
1139
+ lock="$state/.claude-autoarm.lock"
1140
+ epoch="$state/.claude-autoarm-epoch"
1141
+ BROKK_AUTOARM_MY_GEN=
1142
+ # Resolve the pid into a variable FIRST: expanding ${BASHPID:-$$} inside a
1143
+ # command substitution would resolve it in that subshell, recording the
1144
+ # identity of a process that exits immediately.
1145
+ pid=${BASHPID:-$$}
1146
+ identity=$(fm_pid_identity "$pid" 2>/dev/null) || return 1
1147
+ [ -n "$identity" ] || return 1
1148
+ fm_lock_try_acquire "$lock" || return 1
1149
+ if fm_autoarm_claim_open "$state" "$grace"; then
1150
+ fm_lock_release "$lock"
1151
+ return 2
1152
+ fi
1153
+ gen=$(_fm_autoarm_epoch_field "$epoch" epoch 2>/dev/null || true)
1154
+ case "$gen" in
1155
+ ''|*[!0-9]*) gen=0 ;;
1156
+ esac
1157
+ gen=$((gen + 1))
1158
+ tmp="$epoch.tmp.$pid"
1159
+ if ! printf 'epoch=%s owner_pid=%s outcome=arming updated_at=%s\n%s\n' \
1160
+ "$gen" "$pid" "$(date +%s)" "$identity" > "$tmp" 2>/dev/null \
1161
+ || ! mv -f "$tmp" "$epoch" 2>/dev/null; then
1162
+ rm -f "$tmp" 2>/dev/null || true
1163
+ fm_lock_release "$lock"
1164
+ return 1
1165
+ fi
1166
+ fm_lock_release "$lock"
1167
+ # shellcheck disable=SC2034 # Read by callers after the claim succeeds.
1168
+ BROKK_AUTOARM_MY_GEN=$gen
1169
+ return 0
1170
+ }
1171
+
1172
+ # Write a new outcome for a generation this process still owns, re-verified
1173
+ # under the micro-mutex so a superseded owner can never clobber a newer claim.
1174
+ # With a fourth argument, create that marker after the ledger rename in the same
1175
+ # owned critical section (the once-per-episode failure notice). A marker failure
1176
+ # refuses the commit even though its terminal ledger entry remains; marker-first
1177
+ # ordering could permanently suppress a notice whose ledger write never won.
1178
+ # Returns 0 committed, 2 refused (superseded or required-marker failure), and 1
1179
+ # unable (bounded contention or ledger-write failure).
1180
+ fm_autoarm_write_owned() { # <state-dir> <gen> <outcome> [marker-file]
1181
+ local state=$1 gen=$2 outcome=$3 marker=${4:-} lock epoch pid identity tmp i
1182
+ lock="$state/.claude-autoarm.lock"
1183
+ epoch="$state/.claude-autoarm-epoch"
1184
+ pid=${BASHPID:-$$}
1185
+ i=0
1186
+ while ! fm_lock_try_acquire "$lock"; do
1187
+ [ "$i" -lt 20 ] || return 1
1188
+ sleep 0.02
1189
+ i=$((i + 1))
1190
+ done
1191
+ if ! fm_autoarm_ledger_read "$state" \
1192
+ || [ "$BROKK_AUTOARM_GEN" != "$gen" ] || [ "$BROKK_AUTOARM_OWNER" != "$pid" ]; then
1193
+ fm_lock_release "$lock"
1194
+ return 2
1195
+ fi
1196
+ identity=$BROKK_AUTOARM_IDENTITY
1197
+ tmp="$epoch.tmp.$pid"
1198
+ if ! {
1199
+ printf 'epoch=%s owner_pid=%s outcome=%s updated_at=%s\n' \
1200
+ "$gen" "$pid" "$outcome" "$(date +%s)"
1201
+ [ -z "$identity" ] || printf '%s\n' "$identity"
1202
+ } > "$tmp" 2>/dev/null || ! mv -f "$tmp" "$epoch" 2>/dev/null; then
1203
+ rm -f "$tmp" 2>/dev/null || true
1204
+ fm_lock_release "$lock"
1205
+ return 1
1206
+ fi
1207
+ if [ -n "$marker" ] && ! : > "$marker" 2>/dev/null; then
1208
+ fm_lock_release "$lock"
1209
+ return 2
1210
+ fi
1211
+ fm_lock_release "$lock"
1212
+ return 0
1213
+ }
1214
+
1215
+ # Lockless pre-side-effect ownership check: true while the ledger still names
1216
+ # <gen> owned by this process. A superseded owner must go silent instead of
1217
+ # arming, mutating shared state, or emitting.
1218
+ fm_autoarm_still_owner() { # <state-dir> <gen>
1219
+ local state=$1 gen=$2 pid
1220
+ pid=${BASHPID:-$$}
1221
+ fm_autoarm_ledger_read "$state" || return 1
1222
+ [ "$BROKK_AUTOARM_GEN" = "$gen" ] && [ "$BROKK_AUTOARM_OWNER" = "$pid" ]
1223
+ }
1224
+
1225
+ fm_autoarm_reset_owned() { # <state-dir> <gen>
1226
+ local state=$1 gen=$2 lock pid
1227
+ lock="$state/.claude-autoarm.lock"
1228
+ pid=${BASHPID:-$$}
1229
+ fm_lock_try_acquire "$lock" || return 2
1230
+ if ! fm_autoarm_ledger_read "$state" \
1231
+ || [ "$BROKK_AUTOARM_GEN" != "$gen" ] || [ "$BROKK_AUTOARM_OWNER" != "$pid" ]; then
1232
+ fm_lock_release "$lock"
1233
+ return 2
1234
+ fi
1235
+ if ! fm_failure_episode_reset "$state"; then
1236
+ fm_lock_release "$lock"
1237
+ return 1
1238
+ fi
1239
+ fm_lock_release "$lock"
1240
+ return 0
1241
+ }
1242
+
1243
+ # LEGACY shim (see the contract comment above): the abandonment proof for a
1244
+ # lock-holding claim from a pre-generation build, recognizable by the role
1245
+ # file only such claims and the guard's short terminal-check hold publish.
1246
+ # A live legacy owner defers per this proof; a finished, identity-mismatched,
1247
+ # or stuck one is abandoned:
1248
+ #
1249
+ # 1. the owner lock exists and carries the auto-arm role,
1250
+ # 2. its recorded pid is numeric,
1251
+ # 3. a recorded pid-identity that no longer matches the live pid is
1252
+ # abandonment on its own (pid reuse after a group kill), and otherwise
1253
+ # 4. the ledger's owner_pid is exactly that pid and its outcome is present
1254
+ # and either is not "arming", or is "arming" while both the ledger entry
1255
+ # and the watcher beacon are older than the guard grace (the same stuck
1256
+ # proof as fm_autoarm_claim_open).
1257
+ fm_autoarm_claim_abandoned() { # <state-dir> [grace]
1258
+ local state=$1 grace=${2:-${BROKK_GUARD_GRACE:-300}} epoch lock role pid owner outcome recorded current
1259
+ lock="$state/.claude-autoarm.lock"
1260
+ epoch="$state/.claude-autoarm-epoch"
1261
+ case "$grace" in
1262
+ ''|*[!0-9]*|0) grace=300 ;;
1263
+ esac
1264
+ [ -e "$lock" ] || [ -L "$lock" ] || return 1
1265
+ role=$(fm_lock_role "$lock")
1266
+ [ "$role" = autoarm ] || return 1
1267
+ pid=$(cat "$lock/pid" 2>/dev/null || true)
1268
+ case "$pid" in
1269
+ ''|*[!0-9]*) return 1 ;;
1270
+ esac
1271
+ recorded=$(cat "$lock/pid-identity" 2>/dev/null || true)
1272
+ if [ -n "$recorded" ] && current=$(fm_pid_identity "$pid" 2>/dev/null) \
1273
+ && [ -n "$current" ] && [ "$current" != "$recorded" ]; then
1274
+ return 0
1275
+ fi
1276
+ owner=$(_fm_autoarm_epoch_field "$epoch" owner_pid) || return 1
1277
+ [ "$owner" = "$pid" ] || return 1
1278
+ outcome=$(_fm_autoarm_epoch_field "$epoch" outcome) || return 1
1279
+ case "$outcome" in
1280
+ '') return 1 ;;
1281
+ arming)
1282
+ [ "$(fm_path_age "$epoch")" -ge "$grace" ] || return 1
1283
+ [ "$(fm_path_age "$state/.last-watcher-beat")" -ge "$grace" ] || return 1
1284
+ return 0
1285
+ ;;
1286
+ esac
1287
+ return 0
1288
+ }
1289
+
1290
+ # Remove a proven-abandoned legacy claim so the next claimant can arm. The
1291
+ # proof is re-verified while holding the lock's steal mutex, the same
1292
+ # serialization fm_lock_try_acquire uses for stale-owner reclaim: while it is
1293
+ # held no other process can publish the primary lock, so the window between
1294
+ # proving abandonment and removing the lock cannot swallow a genuine new
1295
+ # claim.
1296
+ #
1297
+ # Old-build code cannot re-check generations, so a LIVE proven-abandoned
1298
+ # legacy owner whose recorded identity is verified to match its pid is retired
1299
+ # with TERM before the lock is removed: once the TERM is successfully queued
1300
+ # the process can never resume normal execution (delivery precedes any further
1301
+ # user code when it continues), so a short bounded wait for observed exit is a
1302
+ # courtesy, not a requirement. A pid is never signalled without a verified
1303
+ # matching identity; when the kill itself fails or the identity stops matching
1304
+ # mid-procedure (pid reuse), the reclaim refuses. Missing identity evidence
1305
+ # never blocks the reclaim of a proven-abandoned claim - it only disables the
1306
+ # TERM and the ledger graft below, keeping the documented bounded
1307
+ # upgrade-window residual instead of the deadlock.
1308
+ fm_autoarm_release_abandoned() { # <state-dir> [grace]
1309
+ local state=$1 grace=${2:-${BROKK_GUARD_GRACE:-300}} lock steal epoch lock_pid recorded current owner line1 tmp i
1310
+ lock="$state/.claude-autoarm.lock"
1311
+ steal="$lock.steal"
1312
+ epoch="$state/.claude-autoarm-epoch"
1313
+ fm_autoarm_claim_abandoned "$state" "$grace" || return 1
1314
+ fm_lock_try_acquire "$steal" || return 1
1315
+ if ! fm_autoarm_claim_abandoned "$state" "$grace"; then
1316
+ fm_lock_release "$steal"
1317
+ return 1
1318
+ fi
1319
+ lock_pid=$(cat "$lock/pid" 2>/dev/null || true)
1320
+ recorded=$(cat "$lock/pid-identity" 2>/dev/null || true)
1321
+ if [ -n "$recorded" ] && fm_pid_alive "$lock_pid" \
1322
+ && current=$(fm_pid_identity "$lock_pid" 2>/dev/null) \
1323
+ && [ -n "$current" ] && [ "$current" = "$recorded" ]; then
1324
+ # A live pid still answering to the recorded identity IS the genuine
1325
+ # legacy owner (proven stuck or blocked after a terminal write): retire it
1326
+ # before removing its lock, because old-build code cannot re-check
1327
+ # generations. A pid the recorded identity does NOT verify - reused,
1328
+ # unverifiable, or never recorded - is NEVER signalled; those shapes are
1329
+ # reclaimed as-is, which is safe exactly because the recorded owner is
1330
+ # gone or was never provably this process.
1331
+ if ! kill -TERM "$lock_pid" 2>/dev/null; then
1332
+ fm_lock_release "$steal"
1333
+ return 1
1334
+ fi
1335
+ i=0
1336
+ while [ "$i" -lt 20 ] && fm_pid_alive "$lock_pid"; do
1337
+ sleep 0.05
1338
+ i=$((i + 1))
1339
+ done
1340
+ fi
1341
+ # Preserve the legacy lock's identity evidence in the ledger before the lock
1342
+ # disappears, keeping the ledger's original mtime so the stuck proof's age
1343
+ # window is not silently reopened. Best effort.
1344
+ if [ -n "$recorded" ] && [ -n "$lock_pid" ] \
1345
+ && owner=$(_fm_autoarm_epoch_field "$epoch" owner_pid 2>/dev/null) \
1346
+ && [ "$owner" = "$lock_pid" ] \
1347
+ && [ -z "$(sed -n '2p' "$epoch" 2>/dev/null)" ]; then
1348
+ line1=$(sed -n '1p' "$epoch" 2>/dev/null || true)
1349
+ tmp="$epoch.tmp.${BASHPID:-$$}"
1350
+ if [ -n "$line1" ] \
1351
+ && printf '%s\n%s\n' "$line1" "$recorded" > "$tmp" 2>/dev/null \
1352
+ && touch -r "$epoch" "$tmp" 2>/dev/null \
1353
+ && mv -f "$tmp" "$epoch" 2>/dev/null; then
1354
+ :
1355
+ fi
1356
+ rm -f "$tmp" 2>/dev/null || true
1357
+ fi
1358
+ fm_lock_remove_path "$lock" || true
1359
+ fm_lock_release "$steal"
1360
+ [ -e "$lock" ] || [ -L "$lock" ] || return 0
1361
+ return 1
1362
+ }
1363
+
1364
+ fm_wake_clean_field() {
1365
+ LC_ALL=C tr '\t\r\n' ' '
1366
+ }
1367
+
1368
+ fm_wake_append() {
1369
+ local kind=$1 key=$2 payload=$3 clean_key clean_payload epoch seq seq_file status
1370
+ local recovery_marker
1371
+ case "$kind" in
1372
+ signal|stale|check|heartbeat) ;;
1373
+ *) printf 'fm_wake_append: invalid wake kind: %s\n' "$kind" >&2; return 2 ;;
1374
+ esac
1375
+
1376
+ clean_key=$(printf '%s' "$key" | fm_wake_clean_field)
1377
+ clean_payload=$(printf '%s' "$payload" | fm_wake_clean_field)
1378
+ epoch=$(date +%s)
1379
+ seq_file="$STATE/.wake-queue.seq"
1380
+ recovery_marker="$STATE/.watcher-down"
1381
+ status=0
1382
+
1383
+ fm_lock_acquire_wait "$BROKK_WAKE_QUEUE_LOCK"
1384
+ _fm_recovery_marker_publish "$recovery_marker" downtime || status=$?
1385
+ if [ "$status" -eq 0 ]; then
1386
+ seq=$(cat "$seq_file" 2>/dev/null || echo 0)
1387
+ case "$seq" in
1388
+ ''|*[!0-9]*) seq=0 ;;
1389
+ esac
1390
+ seq=$((seq + 1))
1391
+ printf '%s\n' "$seq" > "$seq_file" || status=$?
1392
+ fi
1393
+ if [ "$status" -eq 0 ]; then
1394
+ printf '%s\t%s\t%s\t%s\t%s\n' "$epoch" "$seq" "$kind" "$clean_key" "$clean_payload" >> "$BROKK_WAKE_QUEUE" || status=$?
1395
+ fi
1396
+ fm_lock_release "$BROKK_WAKE_QUEUE_LOCK"
1397
+ return "$status"
1398
+ }
1399
+
1400
+ # fm_wake_queued_keys <kind>
1401
+ # Print the distinct keys currently queued for <kind>, oldest first. Read under
1402
+ # the append lock so a concurrent append is never observed half-written. The
1403
+ # durable queue stays the authority: a key appears here exactly while a record
1404
+ # for it is queued and unacknowledged, and disappears only after post-handling
1405
+ # acknowledgement consumes it.
1406
+ fm_wake_queued_keys() {
1407
+ local kind=$1
1408
+ case "$kind" in
1409
+ signal|stale|check|heartbeat) ;;
1410
+ *) printf 'fm_wake_queued_keys: invalid wake kind: %s\n' "$kind" >&2; return 2 ;;
1411
+ esac
1412
+ fm_lock_acquire_wait "$BROKK_WAKE_QUEUE_LOCK"
1413
+ fm_wake_queued_keys_locked "$kind"
1414
+ fm_lock_release "$BROKK_WAKE_QUEUE_LOCK"
1415
+ }
1416
+
1417
+ fm_wake_queued_keys_locked() {
1418
+ local kind=$1
1419
+ awk -F '\t' -v kind="$kind" 'NF >= 5 && $3 == kind && !seen[$4]++ { print $4 }' \
1420
+ "$BROKK_WAKE_QUEUE" 2>/dev/null || true
1421
+ }
1422
+
1423
+ fm_wake_eindri-home_stall_marker_write() { # <task> <row-key>
1424
+ local task=$1 row_key=$2 marker tmp
1425
+ case "$task" in ''|*[!A-Za-z0-9._-]*) return 1 ;; esac
1426
+ case "$row_key" in ''|*[!0-9-]*) return 1 ;; esac
1427
+ marker="$STATE/.eindri-home-wake-stall-$task"
1428
+ if [ -e "$marker" ] || [ -L "$marker" ]; then
1429
+ [ -f "$marker" ] && [ ! -L "$marker" ] || return 1
1430
+ fi
1431
+ tmp=$(mktemp "$STATE/.eindri-home-wake-stall.XXXXXX") || return 1
1432
+ if ! printf '%s\n' "$row_key" > "$tmp" || ! chmod 0600 "$tmp" \
1433
+ || ! _fm_atomic_replace "$tmp" "$marker"; then
1434
+ rm -f -- "$tmp"
1435
+ return 1
1436
+ fi
1437
+ }
1438
+
1439
+ fm_wake_eindri-home_stall_receipt_write() { # <task> <row-key>
1440
+ local task=$1 row_key=$2 root task_dir receipt tmp
1441
+ case "$task" in ''|*[!A-Za-z0-9._-]*) return 1 ;; esac
1442
+ case "$row_key" in ''|*[!0-9-]*) return 1 ;; esac
1443
+ root="$STATE/.eindri-home-wake-stall-receipts"
1444
+ task_dir="$root/$task"
1445
+ if [ -e "$root" ] || [ -L "$root" ]; then
1446
+ [ -d "$root" ] && [ ! -L "$root" ] || return 1
1447
+ else
1448
+ mkdir "$root" || return 1
1449
+ chmod 0700 "$root" || return 1
1450
+ fi
1451
+ if [ -e "$task_dir" ] || [ -L "$task_dir" ]; then
1452
+ [ -d "$task_dir" ] && [ ! -L "$task_dir" ] || return 1
1453
+ else
1454
+ mkdir "$task_dir" || return 1
1455
+ chmod 0700 "$task_dir" || return 1
1456
+ fi
1457
+ receipt="$task_dir/$row_key"
1458
+ [ "$(cat "$receipt" 2>/dev/null || true)" != "$row_key" ] || return 0
1459
+ tmp=$(mktemp "$task_dir/.receipt.XXXXXX") || return 1
1460
+ if ! printf '%s\n' "$row_key" > "$tmp" || ! chmod 0600 "$tmp" \
1461
+ || ! _fm_atomic_replace "$tmp" "$receipt"; then
1462
+ rm -f -- "$tmp"
1463
+ return 1
1464
+ fi
1465
+ }
1466
+
1467
+ fm_wake_commit_eindri-home_stall_receipts_through() { # <cutoff> [<rows-file>]
1468
+ local cutoff=$1 rows=${2:-} key seq rest epoch task row_key
1469
+ while IFS= read -r key; do
1470
+ seq=${key##*-}
1471
+ rest=${key%-*}
1472
+ epoch=${rest##*-}
1473
+ task=${rest#eindri-home-wake-loop-}
1474
+ task=${task%-"$epoch"}
1475
+ case "$seq" in ''|*[!0-9]*) return 1 ;; esac
1476
+ case "$epoch" in ''|*[!0-9]*) return 1 ;; esac
1477
+ case "$task" in ''|*[!A-Za-z0-9._-]*) return 1 ;; esac
1478
+ row_key="$epoch-$seq"
1479
+ fm_wake_eindri-home_stall_receipt_write "$task" "$row_key" || return 1
1480
+ done < <(awk -F '\t' -v cutoff="$cutoff" -v rows="$rows" '
1481
+ BEGIN { if (rows != "") while ((getline line < rows) > 0) owned[line]=1 }
1482
+ NF >= 5 && $2 ~ /^[0-9]+$/ && $2 <= cutoff \
1483
+ && (rows == "" || ($2 in owned)) && $3 == "check" \
1484
+ && $4 ~ /^eindri-home-wake-loop-[A-Za-z0-9._-]+-[0-9]+-[0-9]+$/ { print $4 }
1485
+ ' "$BROKK_WAKE_QUEUE" 2>/dev/null)
1486
+ }
1487
+
1488
+ fm_wake_restore_queue() {
1489
+ local drained=$1 restore
1490
+ restore="$STATE/.wake-queue.restore.$(fm_current_pid)"
1491
+ if [ -e "$BROKK_WAKE_QUEUE" ]; then
1492
+ cat "$drained" "$BROKK_WAKE_QUEUE" > "$restore" && mv "$restore" "$BROKK_WAKE_QUEUE"
1493
+ else
1494
+ mv "$drained" "$BROKK_WAKE_QUEUE"
1495
+ fi
1496
+ }
1497
+
1498
+ fm_wake_print_deduped() {
1499
+ local file=$1
1500
+ awk -F '\t' '
1501
+ NF >= 5 {
1502
+ dedupe = $3 SUBSEP $4
1503
+ if ($3 == "heartbeat") {
1504
+ dedupe = "heartbeat"
1505
+ }
1506
+ if (!(dedupe in seen)) {
1507
+ order[++count] = dedupe
1508
+ seen[dedupe] = 1
1509
+ }
1510
+ line[dedupe] = $0
1511
+ }
1512
+ END {
1513
+ for (i = 1; i <= count; i++) {
1514
+ print line[order[i]]
1515
+ }
1516
+ }
1517
+ ' "$file"
1518
+ }
1519
+
1520
+ # --- signal announcement signatures -----------------------------------------
1521
+ #
1522
+ # The watcher's per-file signal scan (bin/brokk-watch.sh scan_signals) detects a
1523
+ # status or turn-ended change by comparing a file signature against a persisted
1524
+ # state/.seen-* marker.
1525
+ # brokk-classify-lib.sh's header owns the status marker contract, including its
1526
+ # independent reported signature and classified position.
1527
+ # These helpers own wake-facing marker routing, the legacy turn-ended signature,
1528
+ # drain-time staleness checks, and guarded bookkeeping writes.
1529
+
1530
+ fm_wake_signal_sig() { # <file> -> reported-state signature
1531
+ case "$1" in
1532
+ *.status)
1533
+ _fm_wake_require_classify || return 1
1534
+ status_observed_signature "$1"
1535
+ ;;
1536
+ *)
1537
+ if [ "$_BROKK_UNAME" = Darwin ]; then stat -f '%z:%Fm' "$1" 2>/dev/null; else stat -c '%s:%Y' "$1" 2>/dev/null; fi
1538
+ ;;
1539
+ esac
1540
+ }
1541
+
1542
+ fm_wake_signal_seen_path() { # <state> <file>
1543
+ local task
1544
+ case "$2" in
1545
+ *.status)
1546
+ task=$(basename "$2"); task=${task%.status}
1547
+ printf '%s/.seen-%s' "$1" "$(printf '%s.status' "$task" | tr '.' '_')"
1548
+ ;;
1549
+ *) printf '%s/.seen-%s' "$1" "$(basename "$2" | tr '.' '_')" ;;
1550
+ esac
1551
+ }
1552
+
1553
+ # The byte size recorded in <file>'s seen marker, or 0 when no marker exists, it
1554
+ # cannot be read, or it does not hold the supported presentation-marker format.
1555
+ # That size is the position the watcher has already classified, independently of
1556
+ # the file signature it has already reported. A 0 means "classify the whole
1557
+ # file", which surfaces events rather than losing them.
1558
+ fm_wake_signal_seen_size() { # <state> <file>
1559
+ local marker sig size
1560
+ marker=$(fm_wake_signal_seen_path "$1" "$2")
1561
+ case "$2" in
1562
+ *.status)
1563
+ _fm_wake_require_classify || { printf '0'; return 0; }
1564
+ status_presentation_marker_offset "$marker" "$2"
1565
+ ;;
1566
+ *)
1567
+ sig=$(cat "$marker" 2>/dev/null) || { printf '0'; return 0; }
1568
+ case "$sig" in *:*) size=${sig%%:*} ;; *) size=0 ;; esac
1569
+ case "$size" in ''|*[!0-9]*) printf '0' ;; *) printf '%s' "$size" ;; esac
1570
+ ;;
1571
+ esac
1572
+ }
1573
+
1574
+ # 0 when <file>'s current signature matches its recorded reported state.
1575
+ # For a status file this means the current state was already reported, not that
1576
+ # every byte was successfully classified; the separate classified position owns
1577
+ # that fact.
1578
+ # A missing marker or unreadable signature is not a match, so uncertainty reads
1579
+ # as an unreported state.
1580
+ fm_wake_signal_seen_current() { # <state> <file>
1581
+ local sig marker
1582
+ sig=$(fm_wake_signal_sig "$2") || return 1
1583
+ [ -n "$sig" ] || return 1
1584
+ marker=$(fm_wake_signal_seen_path "$1" "$2")
1585
+ case "$2" in
1586
+ *.status)
1587
+ _fm_wake_require_classify || return 1
1588
+ status_presentation_marker_reported_matches "$marker" "$sig"
1589
+ ;;
1590
+ *) [ "$(cat "$marker" 2>/dev/null)" = "$sig" ] ;;
1591
+ esac
1592
+ }
1593
+
1594
+ fm_wake_status_reported_commit() { # <state> <status-file> <reported-signature>
1595
+ _fm_wake_require_classify || return 1
1596
+ status_presentation_marker_report "$(fm_wake_signal_seen_path "$1" "$2")" "$3"
1597
+ }
1598
+
1599
+ fm_wake_status_seen_commit() { # <state> <status-file> <captured-end> <captured-identity>
1600
+ _fm_wake_require_classify || return 1
1601
+ status_presentation_marker_commit "$(fm_wake_signal_seen_path "$1" "$2")" "$2" "$3" "$4"
1602
+ }
1603
+
1604
+ # Mark the current complete status snapshot as both reported and classified.
1605
+ # This is the public setup primitive for consumers that adopt an existing log.
1606
+ fm_wake_status_mark_current() { # <state> <status-file>
1607
+ local size ident
1608
+ _fm_wake_require_classify || return 1
1609
+ size=$(_fm_status_file_size "$2") || return 1
1610
+ ident=$(_fm_open_decisions_file_ident "$2") || return 1
1611
+ fm_wake_status_seen_commit "$1" "$2" "$size" "$ident"
1612
+ }
1613
+
1614
+ # Guarded self-announced status append - the one dedup primitive for a status
1615
+ # line THIS home's own machinery writes as bookkeeping it has already presented
1616
+ # in the very turn or tick that writes it (an answerer-closes resolved line, a
1617
+ # pending-reply escalation close, a Allfather-held transfer). Such a close must
1618
+ # not wake the session that wrote it, so this appends the line and then
1619
+ # advances the watcher's seen marker to cover exactly the appended bytes and
1620
+ # nothing else. The advance is provenance-gated and fails toward waking:
1621
+ # - the marker advances ONLY when the file's pre-append signature matched the
1622
+ # recorded seen marker (every earlier byte was already announced or
1623
+ # deliberately absorbed), AND the post-append size equals the pre-append
1624
+ # size plus exactly the appended bytes (no foreign write interleaved);
1625
+ # - on ANY other condition - missing marker, pending foreign bytes, an
1626
+ # interleaved writer, an unreadable signature - the line is still appended
1627
+ # but the marker is left alone, so the watcher surfaces the file normally.
1628
+ # A later, different line from any other writer grows the size past the marker
1629
+ # and wakes as before: task identity alone can never suppress new content.
1630
+ # Returns 0 appended and self-announced, 1 appended but left for the watcher
1631
+ # (the safe direction), 2 the append itself failed.
1632
+ fm_wake_status_append_self_announced() { # <state> <status-file> <line>
1633
+ local state=$1 file=$2 line=$3 marker pre_sig='' pre_size='' pre_ident='' post_size post_ident
1634
+ local LC_ALL=C
1635
+ _fm_wake_require_classify || return 1
1636
+ marker=$(fm_wake_signal_seen_path "$state" "$file")
1637
+ if [ -e "$file" ]; then
1638
+ pre_sig=$(fm_wake_signal_sig "$file") || pre_sig=''
1639
+ pre_size=$(_fm_status_file_size "$file") || pre_size=''
1640
+ pre_ident=$(_fm_open_decisions_file_ident "$file") || pre_ident=''
1641
+ fi
1642
+ printf '%s\n' "$line" >> "$file" || return 2
1643
+ [ -n "$pre_sig" ] || return 1
1644
+ status_presentation_marker_reported_matches "$marker" "$pre_sig" || return 1
1645
+ [ "$(status_presentation_marker_offset "$marker" "$file")" = "$pre_size" ] || return 1
1646
+ post_size=$(_fm_status_file_size "$file") || return 1
1647
+ post_ident=$(_fm_open_decisions_file_ident "$file") || return 1
1648
+ case "$pre_size$post_size" in ''|*[!0-9]*) return 1 ;; esac
1649
+ [ -n "$pre_ident" ] && [ "$post_ident" = "$pre_ident" ] || return 1
1650
+ [ "$post_size" -eq $((pre_size + ${#line} + 1)) ] || return 1
1651
+ fm_wake_status_seen_commit "$state" "$file" "$post_size" "$post_ident" || return 1
1652
+ return 0
1653
+ }
1654
+
1655
+ # Map one structurally valid signal key to its home-local status filename.
1656
+ # Queue payload text is intentionally ignored: it is display data, not a path
1657
+ # authority. The caller still verifies the resulting regular file immediately
1658
+ # before its bounded read.
1659
+ BROKK_WAKE_STATUS_KEY=
1660
+ BROKK_WAKE_STATUS_HISTORICAL=false
1661
+ fm_wake_status_key_map() { # <queue-key>
1662
+ local key=$1 id
1663
+ BROKK_WAKE_STATUS_KEY=
1664
+ BROKK_WAKE_STATUS_HISTORICAL=false
1665
+ case "$key" in
1666
+ *.status)
1667
+ id=${key%.status}
1668
+ ;;
1669
+ *.turn-ended)
1670
+ id=${key%.turn-ended}
1671
+ BROKK_WAKE_STATUS_HISTORICAL=true
1672
+ ;;
1673
+ *)
1674
+ return 1
1675
+ ;;
1676
+ esac
1677
+ case "$id" in
1678
+ ''|.*|*[!A-Za-z0-9._-]*) return 1 ;;
1679
+ esac
1680
+ [ "${#id}" -le 64 ] || return 1
1681
+ BROKK_WAKE_STATUS_KEY="$id.status"
1682
+ }
1683
+
1684
+ fm_wake_annotation_manifest() { # <deduped-raw-rows>
1685
+ local rows=$1 epoch seq kind key payload
1686
+ while IFS=$(printf '\t') read -r epoch seq kind key payload; do
1687
+ [ "$kind" = signal ] || continue
1688
+ fm_wake_status_key_map "$key" || continue
1689
+ if [ "$BROKK_WAKE_STATUS_HISTORICAL" = true ]; then
1690
+ printf '%s\thistorical\n' "$BROKK_WAKE_STATUS_KEY"
1691
+ else
1692
+ printf '%s\tdirect\n' "$BROKK_WAKE_STATUS_KEY"
1693
+ fi
1694
+ done <<EOF
1695
+ $rows
1696
+ EOF
1697
+ }
1698
+
1699
+ BROKK_WAKE_EVENT_LINE=
1700
+ BROKK_WAKE_UNREAD_LINES=
1701
+ fm_wake_status_cursor_offset() { # <validated-status-path> -> already-presented byte offset
1702
+ local path=$1 offset
1703
+ command -v status_presentation_cursor_offset >/dev/null 2>&1 || return 1
1704
+ offset=$(status_presentation_cursor_offset "$path" 2>/dev/null) || return 1
1705
+ case "$offset" in ''|*[!0-9]*) return 1 ;; esac
1706
+ printf '%s' "$offset"
1707
+ }
1708
+
1709
+ # O_NOFOLLOW read of every still-unread status byte. min-offset is the
1710
+ # already-presented cursor from classify-lib. Lines whose bytes begin before
1711
+ # that offset are not replayed. Prints nothing and returns 1 when no unread
1712
+ # non-blank line exists.
1713
+ fm_wake_unread_events() { # <validated-status-path> <unused-tail-byte-cap> <min-offset> [<end-offset>]
1714
+ local path=$1 min_offset=$3 end_offset=${4:-} result size chunk chunk_start
1715
+ local LC_ALL=C
1716
+ BROKK_WAKE_EVENT_LINE=
1717
+ BROKK_WAKE_UNREAD_LINES=
1718
+ case "$min_offset" in ''|*[!0-9]*) min_offset=0 ;; esac
1719
+ result=$(perl -MFcntl=:DEFAULT -e '
1720
+ my ($path, $start, $end) = @ARGV;
1721
+ sysopen(my $file, $path, O_RDONLY | O_NOFOLLOW) or exit 1;
1722
+ my @stat = stat $file or exit 1;
1723
+ exit 1 unless -f _;
1724
+ my $size = $stat[7];
1725
+ exit 1 unless $size =~ /\A\d+\z/ && $start =~ /\A\d+\z/ && $start <= $size;
1726
+ $end = $size unless length $end;
1727
+ exit 1 unless $end =~ /\A\d+\z/ && $start <= $end && $end <= $size;
1728
+ seek($file, $start, 0) or exit 1;
1729
+ printf "%s\t", $end or exit 1;
1730
+ my $remaining = $end - $start;
1731
+ while ($remaining > 0) {
1732
+ my $read = read($file, my $buffer, $remaining);
1733
+ exit 1 unless defined $read;
1734
+ last unless $read;
1735
+ print $buffer or exit 1;
1736
+ $remaining -= $read;
1737
+ }
1738
+ ' "$path" "$min_offset" "$end_offset" 2>/dev/null) || return 1
1739
+ size=${result%%$'\t'*}
1740
+ chunk=${result#*$'\t'}
1741
+ case "$size" in ''|*[!0-9]*) return 1 ;; esac
1742
+ [ -n "$chunk" ] || return 1
1743
+ [ "$min_offset" -lt "$size" ] || return 1
1744
+ chunk_start=$min_offset
1745
+ BROKK_WAKE_UNREAD_LINES=$(printf '%s' "$chunk" | LC_ALL=C awk -v start="$chunk_start" -v min="$min_offset" '
1746
+ BEGIN { pos = start + 0 }
1747
+ {
1748
+ line_start = pos
1749
+ pos += length($0) + 1
1750
+ if ($0 ~ /[^[:space:]]/ && line_start >= min) print $0
1751
+ }
1752
+ ') || return 1
1753
+ [ -n "$BROKK_WAKE_UNREAD_LINES" ] || return 1
1754
+ BROKK_WAKE_EVENT_LINE=$(printf '%s\n' "$BROKK_WAKE_UNREAD_LINES" | tail -1)
1755
+ BROKK_WAKE_EVENT_LINE=$(printf '%s' "$BROKK_WAKE_EVENT_LINE" | LC_ALL=C tr '\t\r' ' ')
1756
+ }
1757
+
1758
+ fm_wake_latest_event() { # <validated-status-path> <tail-byte-cap>
1759
+ fm_wake_unread_events "$1" "$2" 0
1760
+ }
1761
+
1762
+ # Print supplemental drain-time context only after the caller has committed the
1763
+ # raw queue consumption and released the append lock.
1764
+ fm_wake_print_annotations() { # <deduped-raw-rows> [<presentation-snapshot>]
1765
+ local rows=$1 snapshot=${2:-} manifest status_key mode path prefix line task endpoint
1766
+ local snapshot_task snapshot_endpoint _snapshot_ident offset last_event event_line
1767
+ local LC_ALL=C
1768
+
1769
+ manifest=$(fm_wake_annotation_manifest "$rows" | awk -F '\t' '
1770
+ {
1771
+ key = $1
1772
+ if (!(key in seen)) {
1773
+ order[++count] = key
1774
+ seen[key] = 1
1775
+ mode[key] = $2
1776
+ } else if ($2 == "direct") {
1777
+ mode[key] = "direct"
1778
+ }
1779
+ }
1780
+ END {
1781
+ for (i = 1; i <= count; i++) print order[i] "\t" mode[order[i]]
1782
+ }
1783
+ ') || return 0
1784
+
1785
+ # Test-only latency seam for proving that queue appends remain independent of
1786
+ # a slow best-effort annotation phase.
1787
+ case "${BROKK_WAKE_ENRICH_TEST_DELAY:-0}" in
1788
+ 0) ;;
1789
+ ''|*[!0-9]*) ;;
1790
+ *) sleep "$BROKK_WAKE_ENRICH_TEST_DELAY" ;;
1791
+ esac
1792
+
1793
+ while IFS=$(printf '\t') read -r status_key mode; do
1794
+ [ -n "$status_key" ] || continue
1795
+ path="$STATE/$status_key"
1796
+ # A turn-ended-only (historical) row's annotation would show unread status
1797
+ # lines even when those bytes are fully covered by the seen marker - already
1798
+ # surfaced to brokk or deliberately absorbed by the signal triage.
1799
+ # Presenting such an already-announced line again makes a bare turn-end look
1800
+ # like fresh progress, so skip the annotation when the status file's
1801
+ # signature still matches its marker (a proven replay). Any uncertainty -
1802
+ # missing marker, unreadable signature - keeps the annotation with its
1803
+ # existing historical caveat. A direct status row is annotated for every
1804
+ # still-unread line since the last drain presentation; already-presented
1805
+ # bytes are not replayed.
1806
+ if [ "$mode" = historical ] && fm_wake_signal_seen_current "$STATE" "$path"; then
1807
+ continue
1808
+ fi
1809
+ offset=$(fm_wake_status_cursor_offset "$path") || return 1
1810
+ endpoint=
1811
+ if [ -n "$snapshot" ]; then
1812
+ task=${status_key%.status}
1813
+ while IFS=$(printf '\t') read -r snapshot_task snapshot_endpoint _snapshot_ident; do
1814
+ if [ "$snapshot_task" = "$task" ]; then endpoint=$snapshot_endpoint; break; fi
1815
+ done <<EOF
1816
+ $snapshot
1817
+ EOF
1818
+ [ -n "$endpoint" ] || continue
1819
+ fi
1820
+ if [ -n "$endpoint" ] && [ "$offset" -ge "$endpoint" ]; then continue; fi
1821
+ if ! fm_wake_unread_events "$path" 0 "$offset" "$endpoint"; then
1822
+ # Annotation enrichment is supplemental to the already-printed durable
1823
+ # wake rows. A file that disappears, rotates, or becomes unreadable after
1824
+ # the snapshot must not suppress annotations for other status files; the
1825
+ # presentation commit will reject a changed snapshot identity.
1826
+ continue
1827
+ fi
1828
+ last_event=$BROKK_WAKE_EVENT_LINE
1829
+ while IFS= read -r event_line || [ -n "$event_line" ]; do
1830
+ [ -n "$event_line" ] || continue
1831
+ event_line=$(printf '%s' "$event_line" | LC_ALL=C tr '\t\r' ' ')
1832
+ prefix="wake annotation: latest wake-EVENT observed at drain, not current state"
1833
+ if [ "$event_line" != "$last_event" ]; then
1834
+ prefix="wake annotation: unread wake-EVENT since last drain, not current state"
1835
+ fi
1836
+ if [ "$mode" = historical ]; then
1837
+ prefix="$prefix; historical / not necessarily the triggering event"
1838
+ fi
1839
+ line="$prefix: $status_key: $event_line"
1840
+ printf '%s\n' "$line" || return 1
1841
+ done <<EOF
1842
+ $BROKK_WAKE_UNREAD_LINES
1843
+ EOF
1844
+ done <<EOF
1845
+ $manifest
1846
+ EOF
1847
+
1848
+ return 0
1849
+ }