@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,1770 @@
1
+ #!/usr/bin/env bash
2
+ # Shared wake classifier: the common source of truth for Allfather-relevant status
3
+ # tests, declared-external-wait vocabulary, and the working/paused absorb
4
+ # classification that makes no-verb signal and stale-pane wakes safe to absorb.
5
+ # Sourced by BOTH the always-on watcher
6
+ # (bin/brokk-watch.sh) and the away-mode daemon (bin/brokk-supervise-daemon.sh) so the
7
+ # overlapping triage policy lives in one place instead of two copies that can
8
+ # drift apart.
9
+ #
10
+ # Most functions are pure, side-effect-free reads of status files: each takes
11
+ # what it needs as arguments and touches no globals beyond the optional
12
+ # BROKK_ALLFATHER_RE override. Consumers layer their own dedup/marker state on top (the
13
+ # daemon keeps its escalation-digest seen-markers; the watcher keeps its .seen-*
14
+ # signatures).
15
+ # Status-span classification captures one file endpoint and reports every
16
+ # actionable event through that endpoint before the endpoint may be committed.
17
+ # An absent status file is a successful empty span, while an existing status
18
+ # object that cannot be read or identified is a classification failure with no
19
+ # committable endpoint.
20
+ # A presentation marker independently stores the last reported file signature
21
+ # and the last successfully classified position.
22
+ # Successful classification advances both facts through the captured endpoint;
23
+ # after a failure is reported, only the reported signature advances, so the same
24
+ # observed state alarms once while every unclassified byte remains for recovery.
25
+ # The reported signature includes path type, mode, symlink target, and observable
26
+ # failure kind, so a readability change is a new state that triggers another read.
27
+ # A missing, malformed, identity-mismatched, or past-end classified position reads
28
+ # from byte 0, preferring a bounded duplicate over a lost event.
29
+ #
30
+ # There are three documented exceptions. The absorb classification
31
+ # (crew_absorb_class and its working/paused wrappers) is NOT a pure status-file
32
+ # read: it reuses bin/vor-crew-state.sh, which may make a bounded no-mistakes call,
33
+ # to decide whether a crew that just stopped its turn or went stale is working,
34
+ # deliberately paused, or neither. Callers run it ONLY on no-verb signal handling
35
+ # and first sighting of a stale hash, never on every wake, so the per-wake triage
36
+ # stays cheap. status_open_decisions_incremental (see "incremental (cursor-backed)
37
+ # open-decisions fold" below) also writes: it persists a per-status-file byte
38
+ # cursor and folded open-set as a side effect, so a per-drain fleet-wide scan
39
+ # stays bounded by new appends instead of re-reading each task's whole lifetime
40
+ # log every time. crew_worktree_written_since reads the task's meta file and walks
41
+ # a bounded slice of its worktree instead of a status file, so callers run it only
42
+ # at the moment they would otherwise escalate.
43
+
44
+ # Directory of this library, used to locate the sibling vor-crew-state.sh reader.
45
+ # Resolved at source time from BASH_SOURCE so it works whether sourced by a
46
+ # bin/ script (which sets its own SCRIPT_DIR) or directly by a test.
47
+ _BROKK_CLASSIFY_LIB_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd 2>/dev/null)" || _BROKK_CLASSIFY_LIB_DIR="."
48
+
49
+ # The crew current-state reader used for the "provably working" decision.
50
+ # Overridable so tests can replace the run-step/pane verdict without a real worktree
51
+ # or no-mistakes install; absent, it points at the real sibling script.
52
+ BROKK_CREW_STATE_BIN="${BROKK_CREW_STATE_BIN:-$_BROKK_CLASSIFY_LIB_DIR/vor-crew-state.sh}"
53
+
54
+ # fm_run_timed, the shared hard bound the worktree write probe below puts around
55
+ # its one filesystem walk. bin/brokk-timeout-lib.sh owns bounded execution for this
56
+ # repo, so nothing here re-derives the coreutils/BSD/perl selection. That library
57
+ # declares `set -u` for its own hygiene, which a sourced sibling must not impose on
58
+ # THIS library's consumers - several of them deliberately run without it - so the
59
+ # caller's setting is restored around the source.
60
+ case $- in *u*) _fm_classify_nounset=on ;; *) _fm_classify_nounset=off ;; esac
61
+ # shellcheck source=bin/brokk-timeout-lib.sh
62
+ # shellcheck disable=SC1091
63
+ . "$_BROKK_CLASSIFY_LIB_DIR/brokk-timeout-lib.sh"
64
+ [ "$_fm_classify_nounset" = on ] || set +u
65
+ unset _fm_classify_nounset
66
+
67
+ # Allfather-relevant status verbs. A status line carrying any of these is work
68
+ # brokk must see. Lines without these verbs are no-verb signals: the watcher
69
+ # absorbs them only with positive provably-working evidence, while the daemon uses
70
+ # its away-mode classification. BROKK_ALLFATHER_RE overrides the whole set when a home
71
+ # needs a custom verb vocabulary; absent, this default applies.
72
+ #
73
+ # Free-text tokens (PR ready, checks green, ready in branch, merged) exist only for
74
+ # legacy lines that lack a standard terminal verb. status_is_Allfather_relevant is
75
+ # verb-aware: a nonterminal working: or paused: line never becomes Allfather-relevant
76
+ # merely because its prose contains one of those tokens (for example
77
+ # "working: rebased onto merged #76").
78
+ BROKK_CLASSIFY_ALLFATHER_RE_DEFAULT='done:|needs-decision:|blocked:|failed:|PR ready|checks green|ready in branch|merged'
79
+
80
+ # The deliberate-external-wait verb. A crew (or brokk steering it) appends
81
+ # paused: <reason>
82
+ # to declare it is intentionally idling on a KNOWN external dependency - an
83
+ # upstream release, a vendor rate-limit reset, a scheduled window. Unlike
84
+ # `blocked:` (stuck, brokk must help) an idle `paused:` pane is EXPECTED, so
85
+ # the stale path absorbs it instead of escalating a possible wedge. It is
86
+ # deliberately NOT in the Allfather-relevant set above: a pause is a "stop
87
+ # wedge-nagging this idle pane" signal, not work to keep surfacing. This constant
88
+ # is the ONE definition of the verb; both the watcher and the daemon read it here
89
+ # (status_is_paused) rather than hardcoding the literal, so the vocabulary cannot
90
+ # drift between the two consumers. BROKK_CLASSIFY_PAUSED_VERB overrides it.
91
+ BROKK_CLASSIFY_PAUSED_VERB_DEFAULT='paused'
92
+
93
+ # Bounded re-surface cadence for a declared pause or a verified Allfather hold.
94
+ # Far longer than the wedge threshold (BROKK_STALE_ESCALATE_SECS, default 240s), it
95
+ # avoids nagging a deliberate wait while ensuring a forgotten hold cannot rot
96
+ # invisibly - it re-surfaces once for a recheck every window. One hour by default;
97
+ # both consumers read BROKK_PAUSE_RESURFACE_SECS with this default so the cadence has
98
+ # one owner.
99
+ # shellcheck disable=SC2034 # Read by the watcher and daemon (brokk-watch.sh, brokk-supervise-daemon.sh), not this lib.
100
+ BROKK_PAUSE_RESURFACE_SECS_DEFAULT=3600
101
+
102
+ # The resolution verb and durable-backlog-transfer verb that CLOSE a keyed
103
+ # status decision opened by needs-decision or blocked. See status_open_decisions
104
+ # below for the status-fold contract. The transfer verb is written only after
105
+ # brokk-Allfather-hold.sh has verified the corresponding Allfather-held backlog item.
106
+ BROKK_CLASSIFY_RESOLVE_VERB_DEFAULT='resolved'
107
+ BROKK_CLASSIFY_ALLFATHER_HELD_VERB_DEFAULT='Allfather-held'
108
+
109
+ # Return the last non-blank line of a status file (empty if missing/blank).
110
+ last_status_line() {
111
+ local f=$1
112
+ [ -e "$f" ] || return 0
113
+ grep -v '^[[:space:]]*$' "$f" 2>/dev/null | tail -1
114
+ }
115
+
116
+ # 0 if the given (last) status line's leading verb is a real terminal Allfather verb
117
+ # (done, needs-decision, blocked, failed). Free-text tokens alone never count here;
118
+ # callers that need legacy free-text matching use status_is_Allfather_relevant.
119
+ status_is_terminal_verb() {
120
+ local line=$1 verb
121
+ [ -n "$line" ] || return 1
122
+ verb=$(status_line_verb "$line")
123
+ case "$verb" in
124
+ done|needs-decision|blocked|failed) return 0 ;;
125
+ *) return 1 ;;
126
+ esac
127
+ }
128
+
129
+ # 0 if the given (last) status line matches a Allfather-relevant verb.
130
+ # Verb-aware by default: terminal verbs always match; nonterminal progress verbs
131
+ # (working, resolved, Allfather-held) and paused never match from free-text prose;
132
+ # only lines without those leading verbs may still match free-text tokens for
133
+ # legacy bare lines such as "merged" or "PR ready".
134
+ status_is_Allfather_relevant() {
135
+ local line=$1 verb
136
+ [ -n "$line" ] || return 1
137
+ status_is_paused "$line" && return 1
138
+ verb=$(status_line_verb "$line")
139
+ case "$verb" in
140
+ working|resolved|Allfather-held|"${BROKK_CLASSIFY_PAUSED_VERB:-$BROKK_CLASSIFY_PAUSED_VERB_DEFAULT}")
141
+ return 1
142
+ ;;
143
+ esac
144
+ if [ -z "${BROKK_ALLFATHER_RE+x}" ]; then
145
+ case "$verb" in
146
+ done|needs-decision|blocked|failed) return 0 ;;
147
+ esac
148
+ fi
149
+ printf '%s' "$line" | grep -qiE "${BROKK_ALLFATHER_RE:-$BROKK_CLASSIFY_ALLFATHER_RE_DEFAULT}"
150
+ }
151
+
152
+ # 0 if a status line's leading verb is the pause verb (paused: <reason>). A pure
153
+ # read of the line itself, so the daemon's classify_stale can reuse the last line
154
+ # it already read without a vor-crew-state.sh call. Matches only the verb before the
155
+ # first colon, so a reason mentioning "paused" elsewhere does not false-match.
156
+ status_is_paused() { # <status-line>
157
+ local line=$1 verb
158
+ [ -n "$line" ] || return 1
159
+ verb=$(status_line_verb "$line")
160
+ [ "$verb" = "${BROKK_CLASSIFY_PAUSED_VERB:-$BROKK_CLASSIFY_PAUSED_VERB_DEFAULT}" ]
161
+ }
162
+
163
+ # 0 if a status line's leading verb is the verified Allfather-held transfer verb.
164
+ # The same pure verb read as status_is_paused, and the discriminator a supervisor
165
+ # needs once a declared wait has already been recognized: the two declarations get
166
+ # the same bounded cadence, but they block on DIFFERENT humans, so a recheck that
167
+ # names an external dependency for a hold points the Allfather away from the fact
168
+ # that they are the one who can clear it.
169
+ status_is_Allfather_held() { # <status-line>
170
+ local line=$1 verb
171
+ [ -n "$line" ] || return 1
172
+ verb=$(status_line_verb "$line")
173
+ [ "$verb" = "${BROKK_CLASSIFY_ALLFATHER_HELD_VERB:-$BROKK_CLASSIFY_ALLFATHER_HELD_VERB_DEFAULT}" ]
174
+ }
175
+
176
+ # 0 if a status line declares either an external-wait pause or a verified
177
+ # Allfather-held transfer.
178
+ # Both declarations can intentionally leave a crew's endpoint idle, so both
179
+ # supervisors give them one cadence: the away-mode daemon defers the wedge and
180
+ # ages a pause marker instead, and the watcher applies its bounded pause cadence
181
+ # once pause_state_class has admitted the wait (brokk-watch.sh owns which liveness
182
+ # evidence each kind of crew must supply for that).
183
+ status_is_paused_or_Allfather_held() { # <status-line>
184
+ local line=$1
185
+ status_is_paused "$line" || status_is_Allfather_held "$line"
186
+ }
187
+
188
+ # --- durable keyed decisions ------------------------------------------------
189
+ #
190
+ # The status stream is an append-only EVENT log. Reading it last-event-wins
191
+ # (last_status_line above) cannot represent "an earlier decision is still open
192
+ # after a later, unrelated event": a subsequent done/paused/working line silently
193
+ # masks a still-open needs-decision. status_open_decisions is the ONE authoritative
194
+ # statement of the status-fold contract that fixes this - a needs-decision/blocked
195
+ # line OPENS a keyed decision, and only an explicit resolution or a verified
196
+ # Allfather-held backlog transfer referencing that key CLOSES it; a later unrelated
197
+ # terminal line never clears an open Allfather decision.
198
+ # Who WRITES the closing line is owned elsewhere: the answering brokk closes
199
+ # at answer time through brokk-send's --resolve-key (bin/brokk-send.sh header), and a
200
+ # worker self-closes only a blocker that cleared without an answer (bin/erindi-brief.sh
201
+ # rule 6), so closure never depends on a busy worker's discipline.
202
+ #
203
+ # Decision key grammar (backward-compatible with the existing "<verb>: <note>"
204
+ # format): an OPTIONAL "[key=<slug>]" token names the decision. Its documented
205
+ # position sits between the verb and the colon, and a complete token at the
206
+ # head of the note is accepted as an EQUIVALENT position, because that
207
+ # misplaced-colon shape is common real worker output whose stated key must
208
+ # never silently collapse into the shared "default" bucket (issue #2109):
209
+ # needs-decision [key=api-shape]: <summary>
210
+ # needs-decision: [key=api-shape] <summary>
211
+ # resolved [key=api-shape]: <how it was decided>
212
+ # Both positions state the same key and yield the same note (a consumed
213
+ # note-head token is key metadata, stripped from the note); when both positions
214
+ # carry a token, the documented before-colon one wins and the note-head token
215
+ # stays note text. A token deeper inside the note is prose, never a stated key,
216
+ # so a summary merely MENTIONING "[key=x]" cannot open or close that decision.
217
+ # A line with no token in either position uses the key "default", preserving
218
+ # the historical one-open-decision-per-task behavior (a bare "resolved:" closes
219
+ # "default"). A stated key whose slug fails the charset below is rejected (the
220
+ # folds skip the line), never rewritten to "default".
221
+ # The parsers are pure reads of a single line. Status metadata may contain any
222
+ # number of "[name=value]" tags before the colon, in any order, so verb parsing
223
+ # ends at the first tag rather than special-casing "[key=...]".
224
+ #
225
+ # Correlation tokens. That bracket rule already covers every BRACKETED tag,
226
+ # including the "[corr=<16 hex>]" form bin/brokk-eindri-home-report.sh writes. It
227
+ # does not cover the UNBRACKETED token that bin/brokk-pending-reply-lib.sh writes
228
+ # (fm_pending_reply_corr_token), which a eindri-home answering a marked request
229
+ # echoes on its parent status line ahead of the key tag (bin/erindi-brief.sh), so a
230
+ # real transition routinely arrives as
231
+ # needs-decision corr=<16 hex> [key=texte-du-mur]: <summary>
232
+ # resolved corr=<16 hex> [key=texte-du-mur]: <how it was decided>
233
+ # and a recovery turn can leave two such tokens on one line. All of those must
234
+ # read as the bare verb, in BOTH directions: a verb parse that keeps the token
235
+ # glued on matches no arm of _fm_decision_fold_line, so the opener never opens
236
+ # and the closer never closes, and a Allfather decision goes silently missing.
237
+ # Recognition starts only AFTER the retained leading verb: a token-first line
238
+ # keeps that token, so its following word cannot impersonate a transition and
239
+ # close a decision the Allfather is owed.
240
+ #
241
+ # The token grammar is OWNED by bin/brokk-pending-reply-lib.sh
242
+ # (fm_pending_reply_corr_token, BROKK_PENDING_REPLY_CORR_RE). That library sources
243
+ # this one, so it cannot be sourced back here; the pattern below is a deliberate
244
+ # second statement of the SHAPE alone, and tests/brokk-classify-corr-token.test.sh
245
+ # pins the two together through the real writers so they cannot drift.
246
+ #
247
+ # Recognition is deliberately narrow: EXACTLY the token that writer emits, whole
248
+ # word, and nothing else. An arbitrary "<name>=<value>" token is NOT skipped.
249
+ # Skipping unknown tokens would be the permissive road - it would let any
250
+ # free-text word carrying an equals sign ("resolved x=1 [key=k]: ...") reduce to
251
+ # a bare verb and impersonate a transition, which is the takeover the strict
252
+ # parse and _fm_decision_key_transition_allowed exist to prevent. Recognising
253
+ # only what a brokk library actually writes costs one more line here each
254
+ # time a real new token shape is introduced, and that is the intended trade: a
255
+ # new shape is a deliberate, reviewed edit rather than a silent widening. A line
256
+ # whose token is malformed, wrong-length, or merely mentioned in prose keeps its
257
+ # extra words and therefore stays a non-transition, exactly as before.
258
+ #
259
+ # The 16 hex classes are written out literally rather than built from a
260
+ # variable, the same way bin/brokk-eindri-home-report.sh validates the id it is
261
+ # handed: a variable holding a glob is only re-read as a pattern under some
262
+ # shells' expansion rules, and a safety parse must not turn on that.
263
+ #
264
+ # 0 if <word> is, in whole, an unbracketed correlation token this fleet's own
265
+ # tooling writes. The bracketed form never reaches here: the tag rule above has
266
+ # already ended the verb parse at its opening bracket.
267
+ _fm_classify_is_corr_token() { # <word>
268
+ case "$1" in
269
+ corr=[0-9A-Fa-f][0-9A-Fa-f][0-9A-Fa-f][0-9A-Fa-f][0-9A-Fa-f][0-9A-Fa-f][0-9A-Fa-f][0-9A-Fa-f][0-9A-Fa-f][0-9A-Fa-f][0-9A-Fa-f][0-9A-Fa-f][0-9A-Fa-f][0-9A-Fa-f][0-9A-Fa-f][0-9A-Fa-f])
270
+ return 0
271
+ ;;
272
+ esac
273
+ return 1
274
+ }
275
+
276
+ status_line_verb() { # <status-line> -> leading verb word
277
+ local v=${1%%:*} out='' word
278
+ v=${v%%\[*}
279
+ v=${v#"${v%%[![:space:]]*}"}
280
+ v=${v%"${v##*[![:space:]]}"}
281
+ # Fast path, and the whole no-regression guarantee: a prefix that cannot
282
+ # contain a correlation token is returned byte-for-byte as before, so every
283
+ # line without one keeps its exact historical verb, spacing included.
284
+ case "$v" in
285
+ *corr=*) ;;
286
+ *) printf '%s' "$v"; return 0 ;;
287
+ esac
288
+ # Retain the first word, then drop only recognised tokens from the remaining
289
+ # whole words. Anything unrecognised stays, so prose still matches no verb.
290
+ word=${v%%[[:space:]]*}
291
+ out=$word
292
+ v=${v#"$word"}
293
+ v=${v#"${v%%[![:space:]]*}"}
294
+ while [ -n "$v" ]; do
295
+ word=${v%%[[:space:]]*}
296
+ v=${v#"$word"}
297
+ v=${v#"${v%%[![:space:]]*}"}
298
+ _fm_classify_is_corr_token "$word" && continue
299
+ out="$out $word"
300
+ done
301
+ printf '%s' "$out"
302
+ }
303
+ # 0 when a complete "[key=...]" token sits in the documented position before
304
+ # the line's first colon (or anywhere on a line that has no colon at all).
305
+ _fm_key_before_colon() { # <status-line>
306
+ case "${1%%:*}" in
307
+ *\[key=*\]*) return 0 ;;
308
+ *) return 1 ;;
309
+ esac
310
+ }
311
+ # Raw slug of a complete "[key=<slug>]" token at the head of the note (the
312
+ # first thing after the line's first colon, ignoring whitespace). Fails when
313
+ # the line has no colon or no complete token there; slug charset validity is
314
+ # the caller's check via _fm_decision_slug_ok, exactly as for the before-colon
315
+ # position.
316
+ _fm_key_at_note_head() { # <status-line> -> raw slug
317
+ local rest
318
+ case "$1" in
319
+ *:*) rest=${1#*:} ;;
320
+ *) return 1 ;;
321
+ esac
322
+ rest=${rest#"${rest%%[![:space:]]*}"}
323
+ case "$rest" in
324
+ \[key=*\]*) rest=${rest#\[key=}; printf '%s' "${rest%%\]*}" ;;
325
+ *) return 1 ;;
326
+ esac
327
+ }
328
+ # 0 when a stated key slug is well-formed: nonempty, A-Za-z0-9._- only.
329
+ _fm_decision_slug_ok() { # <slug>
330
+ case "$1" in
331
+ ''|*[!A-Za-z0-9._-]*) return 1 ;;
332
+ *) return 0 ;;
333
+ esac
334
+ }
335
+ status_line_note() { # <status-line> -> text after the first colon, trimmed
336
+ local n k
337
+ case "$1" in
338
+ *:*) n=${1#*:}; n=${n#"${n%%[![:space:]]*}"} ;;
339
+ *) printf '%s' "$1"; return 0 ;;
340
+ esac
341
+ # A note-head token that states this line's key (no before-colon token, valid
342
+ # slug) is key metadata, not note text: strip it so both stated-key positions
343
+ # yield the same note.
344
+ if ! _fm_key_before_colon "$1" && k=$(_fm_key_at_note_head "$1") \
345
+ && _fm_decision_slug_ok "$k"; then
346
+ n=${n#"[key=$k]"}
347
+ n=${n#"${n%%[![:space:]]*}"}
348
+ fi
349
+ printf '%s' "$n"
350
+ }
351
+ _fm_decision_key() { # <status-line> -> key slug, or "default" when no token
352
+ local k
353
+ if _fm_key_before_colon "$1"; then
354
+ k=${1%%:*}
355
+ k=${k#*\[key=}
356
+ k=${k%%\]*}
357
+ else
358
+ k=$(_fm_key_at_note_head "$1") || { printf 'default'; return 0; }
359
+ fi
360
+ _fm_decision_slug_ok "$k" || return 1
361
+ printf '%s' "$k"
362
+ }
363
+ # Drop the record for <key> from a newline-terminated "<key>\t<verb>\t<note>" set.
364
+ # Portable (no associative arrays) so the fold runs on bash 3.2 as well as 4+.
365
+ _fm_decision_drop() { # <open-set> <key>
366
+ local set=$1 key=$2 line out=''
367
+ while IFS= read -r line; do
368
+ [ -n "$line" ] || continue
369
+ case "$line" in
370
+ "$key"$'\t'*) : ;;
371
+ *) out="${out}${line}"$'\n' ;;
372
+ esac
373
+ done <<EOF
374
+ $set
375
+ EOF
376
+ printf '%s' "$out"
377
+ }
378
+ # Fold ONE status line into an existing "<key>\t<verb>\t<note>\n"-per-line open
379
+ # set, applying the same needs-decision/blocked-opens, resolved/Allfather-held-closes
380
+ # rule status_open_decisions documents above. Pure text transform, no file I/O.
381
+ # This is the ONE place the per-line open/resolved rule is written; both the
382
+ # whole-file fold (status_open_decisions) and the incremental cursor-backed fold
383
+ # (status_open_decisions_incremental) below call this instead of re-deriving the
384
+ # rule, so the two consumption strategies can never drift apart on semantics.
385
+ # Reserved decision-key namespaces, and the rule that makes them mean something.
386
+ #
387
+ # A key like `pending-reply-<id>` names a decision that one library raises and is
388
+ # the only thing that ever closes it. Every writer reaches this same stream: a
389
+ # local mate appends straight into it, and a remote mate's lines are mirrored
390
+ # into it verbatim. So without a rule here, any writer could claim a reserved
391
+ # key with an unrelated note, take the key over in this fold, and permanently
392
+ # block the owner's close - leaving a decision nothing will ever resolve - or
393
+ # clear the owner's decision with a bare resolution.
394
+ #
395
+ # The rule is deliberately generic, so this fold needs no knowledge of any
396
+ # particular owner: a reserved key may only be opened or closed by a line whose
397
+ # note speaks that namespace's own vocabulary, which its owner states by
398
+ # beginning the note with a `<namespace>...:` token. A line failing that is not a
399
+ # decision transition at all here and is folded as ordinary status. This is a
400
+ # consumer-side rule on purpose - it protects local and remote writers
401
+ # identically, and it can never fail a whole delta or wedge a stream the way a
402
+ # writer-side rejection would.
403
+ BROKK_CLASSIFY_RESERVED_KEY_PREFIXES_DEFAULT='pending-reply-'
404
+
405
+ # 0 when <key> is not reserved, or is reserved and <note> speaks its vocabulary.
406
+ _fm_decision_key_transition_allowed() { # <key> <note>
407
+ local key=$1 note=$2 prefix
408
+ for prefix in ${BROKK_CLASSIFY_RESERVED_KEY_PREFIXES:-$BROKK_CLASSIFY_RESERVED_KEY_PREFIXES_DEFAULT}; do
409
+ case "$key" in
410
+ "$prefix"*)
411
+ case "$note" in
412
+ "$prefix"*:*) return 0 ;;
413
+ *) return 1 ;;
414
+ esac
415
+ ;;
416
+ esac
417
+ done
418
+ return 0
419
+ }
420
+
421
+ _fm_decision_fold_line() { # <open-set> <status-line> <resolve-verb> <held-verb>
422
+ local open=$1 line=$2 resolve=$3 held=$4 verb key note
423
+ # Blank-line guard. A `case` glob answers "does this line hold any non-space
424
+ # character" in one pattern match; the equivalent ${line//[[:space:]]/} costs
425
+ # tens of milliseconds per line under bash 3.2's global bracket-class
426
+ # substitution, which is the whole per-line cost of both folds on a status log
427
+ # of ordinary width. Same verdict, bounded cost.
428
+ case "$line" in
429
+ *[![:space:]]*) ;;
430
+ *) printf '%s' "$open"; return 0 ;;
431
+ esac
432
+ verb=$(status_line_verb "$line")
433
+ key=$(_fm_decision_key "$line") || { printf '%s' "$open"; return 0; }
434
+ _fm_decision_key_transition_allowed "$key" "$(status_line_note "$line")" \
435
+ || { printf '%s' "$open"; return 0; }
436
+ case "$verb" in
437
+ needs-decision|blocked)
438
+ note=$(status_line_note "$line")
439
+ open=$(_fm_decision_drop "$open" "$key")
440
+ [ -n "$open" ] && open="${open}"$'\n'
441
+ open="${open}${key}"$'\t'"${verb}"$'\t'"${note}"$'\n'
442
+ ;;
443
+ "$resolve"|"$held")
444
+ open=$(_fm_decision_drop "$open" "$key")
445
+ [ -n "$open" ] && open="${open}"$'\n'
446
+ ;;
447
+ esac
448
+ printf '%s' "$open"
449
+ }
450
+
451
+ # Fold the WHOLE status stream into the set of decisions still open. Prints one
452
+ # TAB-separated "<key>\t<verb>\t<summary>" line per still-open decision, in
453
+ # most-recently-opened-last order; prints nothing when none are open. Pure read of
454
+ # the file, no globals beyond the optional BROKK_CLASSIFY_RESOLVE_VERB override. This
455
+ # is the durable open-set the fleet snapshot and any point-in-time consumer must use
456
+ # instead of trusting the last status line.
457
+ # The scan_open_decisions wrapper below enumerates a whole directory rather than
458
+ # a single caller-chosen path, so a status file that is itself a symlink (e.g.
459
+ # escaping the state directory) is rejected outright with a plain [ -L ] check
460
+ # before any read - a cheap builtin, unlike fm_wake_latest_event's O_NOFOLLOW
461
+ # subprocess read, which exists for that function's much narrower payload-driven
462
+ # path resolution rather than this directory-local glob.
463
+ status_open_decisions() { # <status-file>
464
+ local f=$1 line resolve held open=''
465
+ [ -f "$f" ] && [ -r "$f" ] && [ ! -L "$f" ] || return 0
466
+ resolve=${BROKK_CLASSIFY_RESOLVE_VERB:-$BROKK_CLASSIFY_RESOLVE_VERB_DEFAULT}
467
+ held=${BROKK_CLASSIFY_ALLFATHER_HELD_VERB:-$BROKK_CLASSIFY_ALLFATHER_HELD_VERB_DEFAULT}
468
+ while IFS= read -r line || [ -n "$line" ]; do
469
+ open=$(_fm_decision_fold_line "$open" "$line" "$resolve" "$held")
470
+ done < "$f"
471
+ printf '%s' "$open"
472
+ }
473
+
474
+ # 0 when <key> has a record in a folded "<key>\t<verb>\t<note>" open set.
475
+ _fm_open_set_has() { # <open-set> <key>
476
+ case "$1" in
477
+ "$2"$'\t'*|*$'\n'"$2"$'\t'*) return 0 ;;
478
+ *) return 1 ;;
479
+ esac
480
+ }
481
+
482
+ # The verb stored for <key> in a folded open set (empty when it has no record).
483
+ _fm_open_set_verb() { # <open-set> <key>
484
+ local line
485
+ while IFS= read -r line; do
486
+ case "$line" in
487
+ "$2"$'\t'*) line=${line#*$'\t'}; printf '%s' "${line%%$'\t'*}"; return 0 ;;
488
+ esac
489
+ done <<EOF
490
+ $1
491
+ EOF
492
+ return 0
493
+ }
494
+
495
+ # The verb that last moved <key> in a status stream, which is what tells a
496
+ # consumer HOW the status side currently reads that key. Prints the opening verb
497
+ # (needs-decision or blocked) while the key is still open, the closing verb
498
+ # (resolved, or the Allfather-held durable-transfer verb) once it is closed, and
499
+ # nothing at all when no line in the stream ever stated a transition for it.
500
+ #
501
+ # The distinction between the two closing verbs is the whole point: a
502
+ # `Allfather-held` close is the VERIFIED handoff to a durable Allfather-held task
503
+ # (brokk-Allfather-hold.sh complete writes it only after verifying that task), so the
504
+ # structured row staying open afterwards is correct. A `resolved` close claims
505
+ # the question is settled outright, so a structured row still open behind it is a
506
+ # contradiction between the two records - see brokk-Allfather-hold.sh's `diverged`.
507
+ #
508
+ # Semantics are not re-derived here: every line goes through the same
509
+ # _fm_decision_fold_line rule the two folds use, and the reported verb is read
510
+ # off the transitions that rule produces. Only lines whose parsed key equals the
511
+ # requested one can move that key, so a caller-supplied key other than "default"
512
+ # lets the scan pre-filter the stream to lines carrying its token and stay cheap
513
+ # on a long log.
514
+ status_key_closing_verb() { # <status-file> <key>
515
+ local f=$1 want=$2 line resolve held open='' was verb='' stream
516
+ [ -f "$f" ] && [ -r "$f" ] && [ ! -L "$f" ] || return 0
517
+ [ -n "$want" ] || return 0
518
+ resolve=${BROKK_CLASSIFY_RESOLVE_VERB:-$BROKK_CLASSIFY_RESOLVE_VERB_DEFAULT}
519
+ held=${BROKK_CLASSIFY_ALLFATHER_HELD_VERB:-$BROKK_CLASSIFY_ALLFATHER_HELD_VERB_DEFAULT}
520
+ if [ "$want" = default ]; then
521
+ stream=$(cat "$f") || return 0
522
+ else
523
+ stream=$(grep -F "[key=$want]" "$f") || stream=''
524
+ fi
525
+ [ -n "$stream" ] || return 0
526
+ while IFS= read -r line || [ -n "$line" ]; do
527
+ was=0
528
+ _fm_open_set_has "$open" "$want" && was=1
529
+ open=$(_fm_decision_fold_line "$open" "$line" "$resolve" "$held")
530
+ if [ "$was" = 1 ] && ! _fm_open_set_has "$open" "$want"; then
531
+ verb=$(status_line_verb "$line")
532
+ fi
533
+ done <<EOF
534
+ $stream
535
+ EOF
536
+ if _fm_open_set_has "$open" "$want"; then
537
+ _fm_open_set_verb "$open" "$want"
538
+ return 0
539
+ fi
540
+ printf '%s' "$verb"
541
+ }
542
+
543
+ # Fleet-wide wrapper around status_open_decisions: scans every task's status
544
+ # log under <state> and prefixes each still-open decision with its owning task
545
+ # id, so a per-wake or per-session surface can print the consolidated open set
546
+ # without re-walking the fold itself. A thin directory scan only - the fold
547
+ # above remains the ONE place the open/resolved semantics are decided. Prints
548
+ # one "<task>\t<key>\t<verb>\t<note>" line per open decision, in glob (task id)
549
+ # order; prints nothing when none are open.
550
+ scan_open_decisions() { # <state>
551
+ local state=$1 f task open line
552
+ for f in "$state"/*.status; do
553
+ [ -e "$f" ] || continue
554
+ task=$(basename "$f"); task="${task%.status}"
555
+ open=$(status_open_decisions "$f") || continue
556
+ [ -n "$open" ] || continue
557
+ while IFS= read -r line; do
558
+ [ -n "$line" ] || continue
559
+ printf '%s\t%s\n' "$task" "$line"
560
+ done <<EOF
561
+ $open
562
+ EOF
563
+ done
564
+ return 0
565
+ }
566
+
567
+ # --- incremental (cursor-backed) open-decisions fold ------------------------
568
+ #
569
+ # status_open_decisions above re-reads and re-folds a status file's ENTIRE
570
+ # lifetime on every call, so its cost grows with total log size. A per-drain
571
+ # fleet-wide scan using that whole-file function would pay that cost for every
572
+ # task on every wake, which grows unbounded as tasks run longer and accumulate
573
+ # status history. status_open_decisions_incremental and scan_open_decisions_incremental
574
+ # below are the bounded-cost siblings used for that per-drain path: each call
575
+ # reads only the bytes appended to a status file since its own last call (a
576
+ # persisted per-file byte cursor) and folds just those new lines into a
577
+ # persisted running open-set, via the exact same _fm_decision_fold_line rule
578
+ # status_open_decisions uses - so the two strategies can never disagree on what
579
+ # is open. Cost is bounded by NEW appends since the last drain, not by the
580
+ # status file's total lifetime size.
581
+ #
582
+ # Correctness invariant (unchanged from the whole-file fold): an open decision
583
+ # is dropped ONLY by an explicit resolved/Allfather-held line for its exact key,
584
+ # never by cursor advancement, age, or being buried under later appends - the
585
+ # persisted open-set carries every still-open key forward across calls
586
+ # regardless of how much new unrelated log content has since been folded in.
587
+ #
588
+ # The cursor format is `version`, `offset`, `ident`, then the folded open set.
589
+ # BROKK_OPEN_DECISIONS_FOLD_VERSION must be bumped whenever
590
+ # _fm_decision_fold_line semantics change, so persisted state from an older
591
+ # interpretation is discarded and rebuilt from byte 0.
592
+ #
593
+ # Cursor invalidation is deliberately minimal, matching how status files are
594
+ # ACTUALLY used in this repo: every one is created once (`>`) and only ever
595
+ # appended to (`>>`) - never replaced, renamed, or rewritten in place. So the
596
+ # ways a cursor can go stale are a fold-version mismatch, a shrink (truncated),
597
+ # or the file at this path being a different file than before
598
+ # (replaced/rotated/recreated), which a changed device+inode makes an O(1) check
599
+ # via a single `stat` call - no content hashing, no re-reading the consumed
600
+ # prefix. Any signal falls back to a full re-fold of the whole current file from
601
+ # byte 0 - byte for byte what status_open_decisions itself would compute - and
602
+ # rewrites the cursor from that clean baseline. A same-inode, same-size,
603
+ # in-place byte edit is NOT detected; that is a deliberately accepted gap
604
+ # because no code path in this repo ever does that to a status file.
605
+ #
606
+ # The other real failure mode is OUR OWN read failing (a stat/wc/tail I/O
607
+ # error), not a malformed writer: every such read here is checked, and on
608
+ # failure this reports the already-trusted persisted set unchanged rather than
609
+ # risking a silent invalidation that would wipe it - never a bare "empty" as if
610
+ # nothing were open.
611
+ #
612
+ # Not a pure status-file read: this writes/rewrites the sibling cursor file as a
613
+ # side effect (state/.<task>.open-decisions-cursor), the library's second
614
+ # documented exception to the pure-read rule after crew_absorb_class. The write
615
+ # is atomic (temp file + rename), so a crash between calls leaves either the
616
+ # prior cursor or the new one, never a partial one. bin/saga-wake-drain.sh calls
617
+ # this only after releasing the wake-queue lock, so a hypothetical race between
618
+ # two overlapping drains can at worst redo a little folding work twice - never
619
+ # drop an open decision - because a losing writer's offset can only ever be
620
+ # equal to or behind an already-recorded byte position, and the next call
621
+ # re-derives from whatever offset actually landed on disk.
622
+ _fm_open_decisions_cursor_path() { # <status-file>
623
+ local f=$1 dir base
624
+ dir=$(dirname "$f")
625
+ base=$(basename "$f")
626
+ printf '%s/.%s.open-decisions-cursor' "$dir" "${base%.status}"
627
+ }
628
+
629
+ # 4: verb parsing ends at the first "[name=value]" tag rather than only at a
630
+ # "[key=...]" one, so lines carrying another bracketed tag first became opens
631
+ # and closes.
632
+ # 5: status_line_verb now also reads through an UNBRACKETED correlation token,
633
+ # so lines that previously folded as ordinary status become opens and closes.
634
+ # Version 4 was already spent on the bracketed-tag parser change above, and a
635
+ # cursor persisted under that reading predates this one, so it must still be
636
+ # discarded and rebuilt from byte 0 under the new reading.
637
+ BROKK_OPEN_DECISIONS_FOLD_VERSION=5
638
+
639
+ # Portable device:inode identity for the rotation/recreation check below.
640
+ _fm_open_decisions_file_ident() { # <file> -> strongest available identity
641
+ local f=$1 epoch birth ident
642
+ if [ -n "${BROKK_STATUS_IDENTITY_READER:-}" ]; then
643
+ "$BROKK_STATUS_IDENTITY_READER" "$f"
644
+ return
645
+ fi
646
+ if [ "$(uname -s 2>/dev/null)" = Darwin ]; then
647
+ ident=$(LC_ALL=C stat -f '%d:%i' "$f" 2>/dev/null) || return 1
648
+ epoch=$(LC_ALL=C stat -f '%B' "$f" 2>/dev/null) || epoch=0
649
+ if [ "$epoch" != 0 ]; then birth=$(LC_ALL=C stat -f '%FB' "$f" 2>/dev/null) || birth=''; else birth=''; fi
650
+ else
651
+ ident=$(LC_ALL=C stat -c '%d:%i' "$f" 2>/dev/null) || return 1
652
+ epoch=$(LC_ALL=C stat -c '%W' "$f" 2>/dev/null) || epoch=0
653
+ if [ "$epoch" != 0 ]; then birth=$(LC_ALL=C stat -c '%w' "$f" 2>/dev/null) || birth=''; else birth=''; fi
654
+ fi
655
+ case "$ident$birth" in *$'\t'*|*$'\n'*|'') return 1 ;; esac
656
+ if [ -n "$birth" ]; then printf 'strong:%s:%s' "$ident" "$birth"; else printf 'weak:%s' "$ident"; fi
657
+ }
658
+
659
+ _fm_status_file_size() { # <status-file>
660
+ local f=$1
661
+ if [ -n "${BROKK_STATUS_SIZE_READER:-}" ]; then
662
+ "$BROKK_STATUS_SIZE_READER" "$f"
663
+ return
664
+ fi
665
+ if [ "$(uname -s 2>/dev/null)" = Darwin ]; then
666
+ LC_ALL=C stat -f '%z' "$f" 2>/dev/null
667
+ else
668
+ LC_ALL=C stat -c '%s' "$f" 2>/dev/null
669
+ fi
670
+ }
671
+
672
+ # Private scratch path for a one-shot span read, alongside the status file the
673
+ # same way the cursor above is, and PID-scoped so concurrent readers of one log
674
+ # (the watcher and the away-mode daemon both classify the same stream) never
675
+ # truncate each other's chunk.
676
+ _fm_status_span_scratch() { # <status-file>
677
+ printf '%s.span.%s' "$(_fm_open_decisions_cursor_path "$1")" "$$"
678
+ }
679
+
680
+ _fm_status_read_span() { # <status-file> <start-offset> <byte-length>
681
+ local f=$1 start=$2 length=$3
682
+ if [ -n "${BROKK_STATUS_SPAN_READER:-}" ]; then
683
+ "$BROKK_STATUS_SPAN_READER" "$f" "$start" "$length"
684
+ return
685
+ fi
686
+ perl -MFcntl=:DEFAULT -e '
687
+ my ($path, $start, $length) = @ARGV;
688
+ sysopen(my $file, $path, O_RDONLY | O_NOFOLLOW) or exit 1;
689
+ sysseek($file, $start, 0) == $start or exit 1;
690
+ while ($length > 0) {
691
+ my $want = $length > 65536 ? 65536 : $length;
692
+ my $read = sysread($file, my $chunk, $want);
693
+ defined($read) && $read > 0 or exit 1;
694
+ print $chunk or exit 1;
695
+ $length -= $read;
696
+ }
697
+ ' "$f" "$start" "$length"
698
+ }
699
+
700
+ status_open_decisions_incremental() { # <status-file> [<captured-end-offset>]
701
+ local f=$1 captured_end=${2:-} cf offset ident open='' trusted_open='' cursor_data first rest offset_line ident_line
702
+ local version='' size actual_size cur_ident resolve held chunk_file chunk_size line cursor_dirty=0
703
+ local target_cursor
704
+ [ -f "$f" ] && [ -r "$f" ] && [ ! -L "$f" ] || return 0
705
+ cf=$(_fm_open_decisions_cursor_path "$f")
706
+ offset=0
707
+ ident=''
708
+ if [ -f "$cf" ] && [ -r "$cf" ] && [ ! -L "$cf" ]; then
709
+ cursor_data=$(LC_ALL=C command cat "$cf" 2>/dev/null) || cursor_data=''
710
+ fi
711
+ if [ -n "${cursor_data:-}" ]; then
712
+ first=${cursor_data%%$'\n'*}
713
+ case "$first" in
714
+ version=*)
715
+ version=${first#version=}
716
+ [ "$version" = "$BROKK_OPEN_DECISIONS_FOLD_VERSION" ] || version=''
717
+ rest=${cursor_data#*$'\n'}
718
+ offset_line=${rest%%$'\n'*}
719
+ case "$offset_line" in
720
+ offset=*) offset=${offset_line#offset=} ;;
721
+ *) offset=0; version='' ;;
722
+ esac
723
+ case "$offset" in
724
+ ''|*[!0-9]*) offset=0; version='' ;;
725
+ *)
726
+ case "$rest" in
727
+ *$'\n'*)
728
+ rest=${rest#*$'\n'}
729
+ ident_line=${rest%%$'\n'*}
730
+ case "$ident_line" in
731
+ ident=*)
732
+ ident=${ident_line#ident=}
733
+ case "$rest" in
734
+ *$'\n'*) open=${rest#*$'\n'} ;;
735
+ esac
736
+ if [ -n "$version" ] && [ -n "$ident" ]; then trusted_open=$open; fi
737
+ ;;
738
+ *) offset=0; version='' ;;
739
+ esac
740
+ ;;
741
+ *) offset=0; version='' ;;
742
+ esac
743
+ ;;
744
+ esac
745
+ ;;
746
+ esac
747
+ fi
748
+
749
+ # A stat/size-read failure is a genuine I/O error, not "the file is empty" -
750
+ # report the already-trusted persisted set unchanged rather than risking a
751
+ # silent invalidation that would wipe it.
752
+ cur_ident=$(_fm_open_decisions_file_ident "$f") || { printf '%s' "$trusted_open"; return 0; }
753
+ [ -n "$cur_ident" ] || { printf '%s' "$trusted_open"; return 0; }
754
+ actual_size=$(_fm_status_file_size "$f") \
755
+ || { printf '%s' "$trusted_open"; return 0; }
756
+ actual_size=${actual_size//[[:space:]]/}
757
+ case "$actual_size" in ''|*[!0-9]*) printf '%s' "$trusted_open"; return 0 ;; esac
758
+ if [ -n "$captured_end" ]; then
759
+ case "$captured_end" in
760
+ ''|*[!0-9]*) printf '%s' "$trusted_open"; return 0 ;;
761
+ esac
762
+ [ "$captured_end" -le "$actual_size" ] || { printf '%s' "$trusted_open"; return 0; }
763
+ size=$captured_end
764
+ else
765
+ size=$actual_size
766
+ fi
767
+
768
+ if [ -z "$version" ] || [ -z "$ident" ] || [ "$ident" != "$cur_ident" ] || [ "$offset" -gt "$actual_size" ]; then
769
+ offset=0
770
+ open=''
771
+ trusted_open=''
772
+ cursor_dirty=1
773
+ fi
774
+
775
+ if [ "$offset" -lt "$size" ]; then
776
+ chunk_file="$cf.read.$$"
777
+ _fm_status_read_span "$f" "$offset" "$((size - offset))" > "$chunk_file" 2>/dev/null \
778
+ || { rm -f "$chunk_file"; printf '%s' "$trusted_open"; return 0; }
779
+ chunk_size=$(LC_ALL=C wc -c < "$chunk_file" 2>/dev/null) \
780
+ || { rm -f "$chunk_file"; printf '%s' "$trusted_open"; return 0; }
781
+ chunk_size=${chunk_size//[[:space:]]/}
782
+ case "$chunk_size" in
783
+ ''|*[!0-9]*) rm -f "$chunk_file"; printf '%s' "$trusted_open"; return 0 ;;
784
+ esac
785
+ # Test-only observability seam (off by default, no production behavior
786
+ # change): when set, records exactly how many bytes THIS call folded, so a
787
+ # test can assert the incremental path stays bounded by new appends rather
788
+ # than re-reading the whole file, without relying on timing or source text.
789
+ [ -n "${BROKK_OPEN_DECISIONS_READ_PROBE:-}" ] \
790
+ && printf '%s\t%s\n' "$f" "$chunk_size" >> "$BROKK_OPEN_DECISIONS_READ_PROBE"
791
+ resolve=${BROKK_CLASSIFY_RESOLVE_VERB:-$BROKK_CLASSIFY_RESOLVE_VERB_DEFAULT}
792
+ held=${BROKK_CLASSIFY_ALLFATHER_HELD_VERB:-$BROKK_CLASSIFY_ALLFATHER_HELD_VERB_DEFAULT}
793
+ while IFS= read -r line || [ -n "$line" ]; do
794
+ open=$(_fm_decision_fold_line "$open" "$line" "$resolve" "$held")
795
+ done < "$chunk_file"
796
+ rm -f "$chunk_file"
797
+ offset=$size
798
+ cursor_dirty=1
799
+ fi
800
+ if [ "$cursor_dirty" -eq 1 ]; then
801
+ target_cursor="$cf.tmp.$$"
802
+ {
803
+ printf 'version=%s\n' "$BROKK_OPEN_DECISIONS_FOLD_VERSION"
804
+ printf 'offset=%s\n' "$offset"
805
+ printf 'ident=%s\n' "$cur_ident"
806
+ if [ -n "$open" ]; then printf '%s' "$open"; fi
807
+ } > "$target_cursor" || return 1
808
+ mv -f "$target_cursor" "$cf" || return 1
809
+ fi
810
+ printf '%s' "$open"
811
+ }
812
+
813
+ # Incremental sibling of scan_open_decisions: same fleet-wide directory walk and
814
+ # output shape ("<task>\t<key>\t<verb>\t<note>" per open decision), but folds
815
+ # each task's status log through status_open_decisions_incremental instead of
816
+ # the whole-file status_open_decisions, so a fleet-wide per-drain scan stays
817
+ # bounded by new appends rather than total lifetime log size across every task.
818
+ scan_open_decisions_incremental() { # <state>
819
+ local state=$1 f task open line
820
+ for f in "$state"/*.status; do
821
+ [ -e "$f" ] || continue
822
+ task=$(basename "$f"); task="${task%.status}"
823
+ open=$(status_open_decisions_incremental "$f") || continue
824
+ [ -n "$open" ] || continue
825
+ while IFS= read -r line; do
826
+ [ -n "$line" ] || continue
827
+ printf '%s\t%s\n' "$task" "$line"
828
+ done <<EOF
829
+ $open
830
+ EOF
831
+ done
832
+ return 0
833
+ }
834
+
835
+ status_presentation_snapshot() { # <state>
836
+ local state=$1 f task size ident
837
+ for f in "$state"/*.status; do
838
+ [ -e "$f" ] || continue
839
+ [ -f "$f" ] && [ -r "$f" ] && [ ! -L "$f" ] || continue
840
+ task=$(basename "$f"); task="${task%.status}"
841
+ size=$(_fm_status_file_size "$f") || return 1
842
+ size=${size//[[:space:]]/}
843
+ ident=$(_fm_open_decisions_file_ident "$f") || return 1
844
+ case "$size" in ''|*[!0-9]*) return 1 ;; esac
845
+ [ -n "$ident" ] || return 1
846
+ printf '%s\t%s\t%s\n' "$task" "$size" "$ident" || return 1
847
+ done
848
+ }
849
+
850
+ status_presentation_cursor_offset() { # <status-file>
851
+ local f=$1 state task manifest data row_task offset ident extra cur_ident size legacy
852
+ [ -f "$f" ] && [ -r "$f" ] && [ ! -L "$f" ] || return 1
853
+ state=${f%/*}
854
+ task=${f##*/}; task=${task%.status}
855
+ manifest="$state/.status-presentation-cursor"
856
+ if [ -e "$manifest" ] || [ -L "$manifest" ]; then
857
+ [ -f "$manifest" ] && [ -r "$manifest" ] && [ ! -L "$manifest" ] || return 1
858
+ data=$(LC_ALL=C command cat "$manifest" 2>/dev/null) || return 1
859
+ offset=
860
+ while IFS=$(printf '\t') read -r row_task ident legacy extra; do
861
+ [ -n "$row_task" ] || continue
862
+ [ -z "$extra" ] || return 1
863
+ case "$legacy" in ''|*[!0-9]*) return 1 ;; esac
864
+ [ -n "$ident" ] || return 1
865
+ if [ "$row_task" = "$task" ]; then
866
+ [ -z "$offset" ] || return 1
867
+ offset=$legacy
868
+ cur_ident=$ident
869
+ fi
870
+ done <<EOF
871
+ $data
872
+ EOF
873
+ if [ -z "$offset" ]; then
874
+ printf '0'
875
+ return 0
876
+ fi
877
+ ident=$cur_ident
878
+ else
879
+ legacy=$(_fm_open_decisions_cursor_path "$f")
880
+ if [ -e "$legacy" ] || [ -L "$legacy" ]; then
881
+ status_open_decisions_cursor_offset "$f"
882
+ return
883
+ fi
884
+ offset=0
885
+ ident=$(_fm_open_decisions_file_ident "$f") || return 1
886
+ fi
887
+ cur_ident=$(_fm_open_decisions_file_ident "$f") || return 1
888
+ size=$(_fm_status_file_size "$f") || return 1
889
+ size=${size//[[:space:]]/}
890
+ case "$size:$offset" in *[!0-9:]*) return 1 ;; esac
891
+ if [ "$ident" != "$cur_ident" ] || [ "$offset" -gt "$size" ]; then offset=0; fi
892
+ printf '%s' "$offset"
893
+ }
894
+
895
+ status_signal_seen_marker_path() { # <state> <task-id>
896
+ printf '%s/.seen-%s' "$1" "$(printf '%s.status' "$2" | tr '.' '_')"
897
+ }
898
+
899
+ status_heartbeat_seen_marker_path() { # <state> <task-id>
900
+ printf '%s/.hb-surfaced-%s' "$1" "$(printf '%s' "$2" | tr ':/.' '___')"
901
+ }
902
+
903
+ status_daemon_seen_marker_path() { # <state> <task-id>
904
+ printf '%s/.subsuper-seen-status-%s' "$1" "$(printf '%s' "$2" | tr ':/.' '___')"
905
+ }
906
+
907
+ _status_presentation_signature_valid() {
908
+ local value=$1 size ident encoded
909
+ [ "$value" = unverifiable ] && return 0
910
+ case "$value" in
911
+ r1:*)
912
+ encoded=${value#r1:}
913
+ case "$encoded" in ''|*[!0-9a-f]*) return 1 ;; esac
914
+ return 0
915
+ ;;
916
+ esac
917
+ case "$value" in *@*) size=${value%%@*}; ident=${value#*@} ;; *) return 1 ;; esac
918
+ case "$size" in ''|*[!0-9]*) return 1 ;; esac
919
+ case "$ident" in ''|*$'\t'*|*$'\n'*) return 1 ;; esac
920
+ }
921
+
922
+ STATUS_PRESENTATION_REPORTED=
923
+ STATUS_PRESENTATION_CLASSIFIED=
924
+ status_presentation_marker_parse() {
925
+ local raw=$1 rest reported classified
926
+ STATUS_PRESENTATION_REPORTED=
927
+ STATUS_PRESENTATION_CLASSIFIED=
928
+ case "$raw" in
929
+ v2$'\t'*)
930
+ rest=${raw#v2$'\t'}
931
+ case "$rest" in *$'\t'*) reported=${rest%%$'\t'*}; classified=${rest#*$'\t'} ;; *) return 1 ;; esac
932
+ case "$classified" in *$'\t'*) return 1 ;; esac
933
+ _status_presentation_signature_valid "$reported" || return 1
934
+ if [ "$classified" != - ]; then
935
+ _status_presentation_signature_valid "$classified" || return 1
936
+ case "$classified" in unverifiable|r1:*) return 1 ;; esac
937
+ fi
938
+ ;;
939
+ *)
940
+ _status_presentation_signature_valid "$raw" || return 1
941
+ case "$raw" in unverifiable|r1:*) return 1 ;; esac
942
+ reported=$raw
943
+ classified=$raw
944
+ ;;
945
+ esac
946
+ STATUS_PRESENTATION_REPORTED=$reported
947
+ STATUS_PRESENTATION_CLASSIFIED=$classified
948
+ }
949
+
950
+ _status_observed_path_state() {
951
+ if [ "$(uname -s 2>/dev/null)" = Darwin ]; then
952
+ LC_ALL=C stat -f '%HT:%p' "$1" 2>/dev/null
953
+ else
954
+ LC_ALL=C stat -c '%F:%f' "$1" 2>/dev/null
955
+ fi
956
+ }
957
+
958
+ status_observed_signature() {
959
+ local f=$1 size=${2-} ident=${3-} path_state link_target=- access kind encoded
960
+ path_state=$(_status_observed_path_state "$f") || path_state=stat-error
961
+ if [ -L "$f" ]; then
962
+ link_target=$(readlink "$f" 2>/dev/null) || link_target=readlink-error
963
+ kind=symlink
964
+ elif [ ! -e "$f" ]; then
965
+ kind=absent
966
+ elif [ ! -f "$f" ]; then
967
+ kind=nonregular
968
+ elif [ -r "$f" ]; then
969
+ kind=readable
970
+ else
971
+ kind=unreadable
972
+ fi
973
+ if [ -z "$size" ]; then
974
+ size=$(_fm_status_file_size "$f") || size='size-error'
975
+ size=${size//[[:space:]]/}
976
+ case "$size" in ''|*[!0-9]*) size='size-error' ;; esac
977
+ fi
978
+ if [ -z "$ident" ]; then
979
+ ident=$(_fm_open_decisions_file_ident "$f") || ident=identity-error
980
+ [ -n "$ident" ] || ident=identity-error
981
+ fi
982
+ if [ -r "$f" ]; then access=readable; else access=unreadable; fi
983
+ encoded=$(printf '%s\0%s\0%s\0%s\0%s\0%s' \
984
+ "$size" "$ident" "$path_state" "$link_target" "$access" "$kind" \
985
+ | LC_ALL=C od -An -v -tx1 | tr -d ' \n') || return 1
986
+ printf 'r1:%s' "$encoded"
987
+ }
988
+
989
+ status_presentation_marker_reported_matches() {
990
+ local raw
991
+ raw=$(cat "$1" 2>/dev/null) || return 1
992
+ status_presentation_marker_parse "$raw" || return 1
993
+ [ "$STATUS_PRESENTATION_REPORTED" = "$2" ]
994
+ }
995
+
996
+ status_presentation_marker_offset() {
997
+ local raw classified offset ident current
998
+ raw=$(cat "$1" 2>/dev/null) || { printf '0'; return 0; }
999
+ status_presentation_marker_parse "$raw" || { printf '0'; return 0; }
1000
+ classified=$STATUS_PRESENTATION_CLASSIFIED
1001
+ [ "$classified" != - ] || { printf '0'; return 0; }
1002
+ offset=${classified%%@*}; ident=${classified#*@}
1003
+ current=$(_fm_open_decisions_file_ident "$2") || { printf '0'; return 0; }
1004
+ [ "$ident" = "$current" ] || { printf '0'; return 0; }
1005
+ printf '%s' "$offset"
1006
+ }
1007
+
1008
+ status_presentation_marker_report() {
1009
+ local marker=$1 reported=$2 raw classified=-
1010
+ _status_presentation_signature_valid "$reported" || return 1
1011
+ if raw=$(cat "$marker" 2>/dev/null) && status_presentation_marker_parse "$raw"; then
1012
+ classified=$STATUS_PRESENTATION_CLASSIFIED
1013
+ fi
1014
+ printf 'v2\t%s\t%s' "$reported" "$classified" > "$marker"
1015
+ }
1016
+
1017
+ status_presentation_marker_commit() {
1018
+ local marker=$1 file=$2 endpoint=$3 ident=$4 current reported classified
1019
+ case "$endpoint" in ''|*[!0-9]*) return 1 ;; esac
1020
+ current=$(_fm_open_decisions_file_ident "$file") || return 1
1021
+ [ -n "$ident" ] && [ "$ident" = "$current" ] || return 1
1022
+ reported=$(status_observed_signature "$file" "$endpoint" "$ident") || return 1
1023
+ classified="${endpoint}@${ident}"
1024
+ printf 'v2\t%s\t%s' "$reported" "$classified" > "$marker"
1025
+ }
1026
+
1027
+ status_retire_presentation_task() { # <state> <task-id>
1028
+ local state=$1 task=$2 lock manifest tmp data row_task ident offset extra rc=0 found=0
1029
+ local signal_marker heartbeat_marker daemon_marker
1030
+ lock="$state/.status-presentation-lock"
1031
+ manifest="$state/.status-presentation-cursor"
1032
+ tmp="$manifest.tmp.$$"
1033
+ signal_marker=$(status_signal_seen_marker_path "$state" "$task")
1034
+ heartbeat_marker=$(status_heartbeat_seen_marker_path "$state" "$task")
1035
+ daemon_marker=$(status_daemon_seen_marker_path "$state" "$task")
1036
+
1037
+ # A remote-home teardown can legitimately retire an endpoint ID that has no
1038
+ # status log in that home. Do not contend with that home's unrelated status
1039
+ # presenter in this no-op case. A concurrent presenter cannot add this task
1040
+ # without its status file, so a valid manifest with no matching row is a
1041
+ # durable proof that there is nothing to retire.
1042
+ if [ ! -e "$state/$task.status" ] && [ ! -L "$state/$task.status" ] \
1043
+ && [ ! -e "$state/.$task.open-decisions-cursor" ] \
1044
+ && [ ! -L "$state/.$task.open-decisions-cursor" ] \
1045
+ && [ ! -e "$signal_marker" ] && [ ! -L "$signal_marker" ] \
1046
+ && [ ! -e "$heartbeat_marker" ] && [ ! -L "$heartbeat_marker" ] \
1047
+ && [ ! -e "$daemon_marker" ] && [ ! -L "$daemon_marker" ]; then
1048
+ if [ ! -e "$manifest" ] && [ ! -L "$manifest" ]; then
1049
+ return 0
1050
+ fi
1051
+ if [ -f "$manifest" ] && [ -r "$manifest" ] && [ ! -L "$manifest" ] \
1052
+ && data=$(LC_ALL=C command cat "$manifest" 2>/dev/null); then
1053
+ while IFS=$(printf '\t') read -r row_task ident offset extra; do
1054
+ [ -n "$row_task" ] || continue
1055
+ if [ -n "$extra" ] || [ -z "$ident" ]; then rc=1; break; fi
1056
+ case "$offset" in ''|*[!0-9]*) rc=1; break ;; esac
1057
+ [ "$row_task" != "$task" ] || found=1
1058
+ done <<EOF
1059
+ $data
1060
+ EOF
1061
+ [ "$rc" -ne 0 ] || [ "$found" -ne 0 ] || return 0
1062
+ rc=0
1063
+ fi
1064
+ fi
1065
+
1066
+ fm_lock_acquire_wait "$lock" || return 1
1067
+ if [ -e "$manifest" ] || [ -L "$manifest" ]; then
1068
+ if [ ! -f "$manifest" ] || [ ! -r "$manifest" ] || [ -L "$manifest" ]; then
1069
+ rc=1
1070
+ elif ! data=$(LC_ALL=C command cat "$manifest" 2>/dev/null); then
1071
+ rc=1
1072
+ elif ! : > "$tmp"; then
1073
+ rc=1
1074
+ else
1075
+ while IFS=$(printf '\t') read -r row_task ident offset extra; do
1076
+ [ -n "$row_task" ] || continue
1077
+ if [ -n "$extra" ] || [ -z "$ident" ]; then rc=1; break; fi
1078
+ case "$offset" in ''|*[!0-9]*) rc=1; break ;; esac
1079
+ if [ "$row_task" != "$task" ]; then
1080
+ printf '%s\t%s\t%s\n' "$row_task" "$ident" "$offset" >> "$tmp" \
1081
+ || { rc=1; break; }
1082
+ fi
1083
+ done <<EOF
1084
+ $data
1085
+ EOF
1086
+ if [ "$rc" -eq 0 ]; then mv -f "$tmp" "$manifest" || rc=1; fi
1087
+ [ "$rc" -eq 0 ] || rm -f "$tmp"
1088
+ fi
1089
+ fi
1090
+ if [ "$rc" -eq 0 ]; then
1091
+ rm -f -- "$state/$task.status" "$state/.$task.open-decisions-cursor" \
1092
+ "$signal_marker" "$heartbeat_marker" "$daemon_marker" || rc=1
1093
+ fi
1094
+ fm_lock_release "$lock" || rc=1
1095
+ return "$rc"
1096
+ }
1097
+
1098
+ status_acknowledge_presented_snapshot() { # <state> <snapshot> [<fully-presented-task-ids>]
1099
+ local state=$1 snapshot=$2 fully_presented=${3:-} task endpoint ident f offset lines line safe
1100
+ while IFS=$(printf '\t') read -r task endpoint ident; do
1101
+ [ -n "$task" ] || continue
1102
+ safe=false
1103
+ case "
1104
+ $fully_presented
1105
+ " in *$'\n'"$task"$'\n'*) safe=true ;; esac
1106
+ if [ "$safe" = false ]; then
1107
+ f="$state/$task.status"
1108
+ offset=$(status_presentation_cursor_offset "$f") || return 1
1109
+ lines=$(status_new_lines_since_cursor "$f" "$endpoint") || return 1
1110
+ # Once any informational line in this span is presented fleet-wide, the
1111
+ # contiguous cursor may advance through the captured endpoint. Routine
1112
+ # lines remain unacknowledged only while they are the sole unread content,
1113
+ # preserving delayed signal annotations without replaying a handled note
1114
+ # that happened to follow a routine line.
1115
+ while IFS= read -r line || [ -n "$line" ]; do
1116
+ case "$line" in
1117
+ *[![:space:]]*)
1118
+ if status_line_is_unread_surface "$line"; then safe=true; break; fi
1119
+ ;;
1120
+ esac
1121
+ done <<EOF
1122
+ $lines
1123
+ EOF
1124
+ if [ "$safe" = false ]; then endpoint=$offset; fi
1125
+ fi
1126
+ printf '%s\t%s\t%s\n' "$task" "$endpoint" "$ident" || return 1
1127
+ done <<EOF
1128
+ $snapshot
1129
+ EOF
1130
+ }
1131
+
1132
+ status_commit_presentation_snapshot() { # <state> <snapshot>
1133
+ local state=$1 snapshot=$2 task endpoint ident f cur_ident size tmp
1134
+ tmp="$state/.status-presentation-cursor.tmp.$$"
1135
+ : > "$tmp" || return 1
1136
+ while IFS=$(printf '\t') read -r task endpoint ident; do
1137
+ [ -n "$task" ] || continue
1138
+ case "$endpoint" in ''|*[!0-9]*) rm -f "$tmp"; return 1 ;; esac
1139
+ [ -n "$ident" ] || { rm -f "$tmp"; return 1; }
1140
+ f="$state/$task.status"
1141
+ [ -f "$f" ] && [ -r "$f" ] && [ ! -L "$f" ] || { rm -f "$tmp"; return 1; }
1142
+ cur_ident=$(_fm_open_decisions_file_ident "$f") || { rm -f "$tmp"; return 1; }
1143
+ size=$(_fm_status_file_size "$f") || { rm -f "$tmp"; return 1; }
1144
+ size=${size//[[:space:]]/}
1145
+ case "$size" in ''|*[!0-9]*) rm -f "$tmp"; return 1 ;; esac
1146
+ [ "$cur_ident" = "$ident" ] && [ "$endpoint" -le "$size" ] \
1147
+ || { rm -f "$tmp"; return 1; }
1148
+ printf '%s\t%s\t%s\n' "$task" "$ident" "$endpoint" >> "$tmp" \
1149
+ || { rm -f "$tmp"; return 1; }
1150
+ done <<EOF
1151
+ $snapshot
1152
+ EOF
1153
+ mv -f "$tmp" "$state/.status-presentation-cursor" || { rm -f "$tmp"; return 1; }
1154
+ }
1155
+
1156
+ scan_open_decisions_snapshot() { # <state> <task-and-endpoint-snapshot>
1157
+ local state=$1 snapshot=$2 task endpoint ident f open line
1158
+ while IFS=$(printf '\t') read -r task endpoint ident; do
1159
+ [ -n "$task" ] || continue
1160
+ f="$state/$task.status"
1161
+ open=$(status_open_decisions_incremental "$f" "$endpoint") || return 1
1162
+ [ -n "$open" ] || continue
1163
+ while IFS= read -r line; do
1164
+ [ -n "$line" ] || continue
1165
+ printf '%s\t%s\n' "$task" "$line"
1166
+ done <<EOF
1167
+ $open
1168
+ EOF
1169
+ done <<EOF
1170
+ $snapshot
1171
+ EOF
1172
+ }
1173
+
1174
+ # --- unread status lines since the presentation cursor ----------------------
1175
+ #
1176
+ # The drain annotation historically printed only the newest status line, so a
1177
+ # substantive `note:` answer immediately followed by a routine `note:` (or a
1178
+ # pending-reply resolution buried under a later unrelated append) never reached
1179
+ # the supervisor. Those verbs also never enter the OPEN DECISIONS fold, so they
1180
+ # had no other surfacing path.
1181
+ # These helpers are the ONE owner of "what is still unread since the last drain
1182
+ # presentation": one fleet manifest records each status identity and last-
1183
+ # presented byte offset, and one atomic replacement commits only the contiguous
1184
+ # status spans that were successfully presented. A quiet fleet scan leaves
1185
+ # routine working/done bytes unacknowledged so a subsequently published signal
1186
+ # can still annotate them. A missing manifest row or changed file identity is
1187
+ # offset 0 for the current file, while malformed or unreadable cursor state
1188
+ # aborts presentation without advancing any offset. A trusted cursor at EOF
1189
+ # prints nothing, so already-presented bytes are not replayed as new. Teardown
1190
+ # retires a task's manifest row with its status file, so reusing a task ID starts
1191
+ # the replacement log unread at byte 0. Informational `note:` lines and
1192
+ # reserved-key pending-reply resolutions are the fleet-wide unread surface;
1193
+ # they are not open decisions and are not persisted in the folded open-set.
1194
+
1195
+ # Read the legacy per-task open-decisions cursor used to seed the presentation
1196
+ # offset before the fleet manifest exists. A fold-version mismatch, identity
1197
+ # mismatch, or offset past the current size falls back to 0. Never writes unless
1198
+ # a caller explicitly requests a migration snapshot.
1199
+ status_open_decisions_cursor_offset() { # <status-file>
1200
+ local f=$1 cf offset=0 ident='' version='' cursor_data first rest open=''
1201
+ local offset_line ident_line cur_ident size
1202
+ [ -f "$f" ] && [ -r "$f" ] && [ ! -L "$f" ] || return 1
1203
+ cf=$(_fm_open_decisions_cursor_path "$f")
1204
+ if [ -e "$cf" ] || [ -L "$cf" ]; then
1205
+ [ -f "$cf" ] && [ -r "$cf" ] && [ ! -L "$cf" ] || return 1
1206
+ if cursor_data=$(LC_ALL=C command cat "$cf" 2>/dev/null); then
1207
+ first=${cursor_data%%$'\n'*}
1208
+ case "$first" in
1209
+ version=*)
1210
+ version=${first#version=}
1211
+ [ "$version" = "$BROKK_OPEN_DECISIONS_FOLD_VERSION" ] || version=''
1212
+ rest=${cursor_data#*$'\n'}
1213
+ offset_line=${rest%%$'\n'*}
1214
+ case "$offset_line" in
1215
+ offset=*) offset=${offset_line#offset=} ;;
1216
+ *) offset=0; version='' ;;
1217
+ esac
1218
+ case "$offset" in
1219
+ ''|*[!0-9]*) offset=0; version='' ;;
1220
+ *)
1221
+ case "$rest" in
1222
+ *$'\n'*)
1223
+ rest=${rest#*$'\n'}
1224
+ ident_line=${rest%%$'\n'*}
1225
+ case "$ident_line" in
1226
+ ident=*)
1227
+ ident=${ident_line#ident=}
1228
+ case "$rest" in *$'\n'*) open=${rest#*$'\n'} ;; esac
1229
+ ;;
1230
+ *) offset=0; version='' ;;
1231
+ esac
1232
+ ;;
1233
+ *) offset=0; version='' ;;
1234
+ esac
1235
+ ;;
1236
+ esac
1237
+ ;;
1238
+ esac
1239
+ else
1240
+ return 1
1241
+ fi
1242
+ fi
1243
+ cur_ident=$(_fm_open_decisions_file_ident "$f") || return 1
1244
+ [ -n "$cur_ident" ] || return 1
1245
+ size=$(_fm_status_file_size "$f") || return 1
1246
+ size=${size//[[:space:]]/}
1247
+ case "$size" in ''|*[!0-9]*) return 1 ;; esac
1248
+ if [ -z "$version" ] || [ -z "$ident" ] || [ "$ident" != "$cur_ident" ] || [ "$offset" -gt "$size" ]; then
1249
+ offset=0
1250
+ open=''
1251
+ fi
1252
+ if [ -n "${BROKK_STATUS_CURSOR_SNAPSHOT_FILE:-}" ]; then
1253
+ {
1254
+ printf 'version=%s\n' "$BROKK_OPEN_DECISIONS_FOLD_VERSION"
1255
+ printf 'offset=%s\n' "$offset"
1256
+ printf 'ident=%s\n' "$cur_ident"
1257
+ if [ -n "$open" ]; then printf '%s' "$open"; fi
1258
+ } > "$BROKK_STATUS_CURSOR_SNAPSHOT_FILE" || return 1
1259
+ fi
1260
+ printf '%s' "$offset"
1261
+ }
1262
+
1263
+ # Print every non-blank status line whose bytes begin at or after the persisted
1264
+ # presentation offset. Does not write the cursor. A missing manifest row or
1265
+ # changed status identity reads the current file from offset 0; malformed or
1266
+ # unreadable cursor state fails the scan. Symlinks and unreadable status files
1267
+ # print nothing.
1268
+ status_new_lines_since_cursor() { # <status-file> [<captured-end-offset>]
1269
+ local f=$1 captured_end=${2:-} cf offset size actual_size chunk_file line rc=0
1270
+ [ -f "$f" ] && [ -r "$f" ] && [ ! -L "$f" ] || return 0
1271
+ cf=$(_fm_open_decisions_cursor_path "$f")
1272
+ chunk_file="$cf.unread.$$"
1273
+ offset=$(status_presentation_cursor_offset "$f") || return 1
1274
+ case "$offset" in ''|*[!0-9]*) return 1 ;; esac
1275
+ actual_size=$(_fm_status_file_size "$f") || return 1
1276
+ actual_size=${actual_size//[[:space:]]/}
1277
+ case "$actual_size" in ''|*[!0-9]*) return 1 ;; esac
1278
+ if [ -n "$captured_end" ]; then
1279
+ case "$captured_end" in ''|*[!0-9]*) return 1 ;; esac
1280
+ [ "$captured_end" -le "$actual_size" ] || return 1
1281
+ size=$captured_end
1282
+ else
1283
+ size=$actual_size
1284
+ fi
1285
+ [ "$offset" -lt "$size" ] || return 0
1286
+ _fm_status_read_span "$f" "$offset" "$((size - offset))" > "$chunk_file" 2>/dev/null \
1287
+ || { rm -f "$chunk_file"; return 1; }
1288
+ while IFS= read -r line || [ -n "$line" ]; do
1289
+ case "$line" in
1290
+ *[![:space:]]*) printf '%s\n' "$line" || { rc=1; break; } ;;
1291
+ esac
1292
+ done < "$chunk_file"
1293
+ rm -f "$chunk_file"
1294
+ return "$rc"
1295
+ }
1296
+
1297
+ # 0 when a status line is an informational `note:` or a reserved-key
1298
+ # pending-reply resolution. Those lines never fold into OPEN DECISIONS, so the
1299
+ # drain's unread-status surface is their only guaranteed presentation.
1300
+ status_line_is_unread_surface() { # <status-line>
1301
+ local line=$1 verb key note resolve held prefix
1302
+ [ -n "$line" ] || return 1
1303
+ verb=$(status_line_verb "$line")
1304
+ [ "$verb" = note ] && return 0
1305
+ resolve=${BROKK_CLASSIFY_RESOLVE_VERB:-$BROKK_CLASSIFY_RESOLVE_VERB_DEFAULT}
1306
+ held=${BROKK_CLASSIFY_ALLFATHER_HELD_VERB:-$BROKK_CLASSIFY_ALLFATHER_HELD_VERB_DEFAULT}
1307
+ case "$verb" in
1308
+ "$resolve"|"$held") ;;
1309
+ *) return 1 ;;
1310
+ esac
1311
+ key=$(_fm_decision_key "$line") || return 1
1312
+ note=$(status_line_note "$line")
1313
+ for prefix in ${BROKK_CLASSIFY_RESERVED_KEY_PREFIXES:-$BROKK_CLASSIFY_RESERVED_KEY_PREFIXES_DEFAULT}; do
1314
+ case "$key" in
1315
+ "$prefix"*)
1316
+ _fm_decision_key_transition_allowed "$key" "$note"
1317
+ return
1318
+ ;;
1319
+ esac
1320
+ done
1321
+ return 1
1322
+ }
1323
+
1324
+ # Fleet-wide unread informational lines: one "<task>\t<status-line>" row per
1325
+ # still-unread `note:` or pending-reply resolution, in glob (task id) order.
1326
+ # Prints nothing when none are unread. Directory scan rejects status symlinks
1327
+ # the same way scan_open_decisions does.
1328
+ scan_unread_surface_lines() { # <state>
1329
+ local state=$1 f task lines line
1330
+ for f in "$state"/*.status; do
1331
+ [ -e "$f" ] || continue
1332
+ task=$(basename "$f"); task="${task%.status}"
1333
+ lines=$(status_new_lines_since_cursor "$f") || return 1
1334
+ [ -n "$lines" ] || continue
1335
+ while IFS= read -r line; do
1336
+ [ -n "$line" ] || continue
1337
+ status_line_is_unread_surface "$line" || continue
1338
+ printf '%s\t%s\n' "$task" "$line"
1339
+ done <<EOF
1340
+ $lines
1341
+ EOF
1342
+ done
1343
+ return 0
1344
+ }
1345
+
1346
+ scan_unread_surface_snapshot() { # <state> <task-and-endpoint-snapshot>
1347
+ local state=$1 snapshot=$2 task endpoint ident f lines line
1348
+ while IFS=$(printf '\t') read -r task endpoint ident; do
1349
+ [ -n "$task" ] || continue
1350
+ f="$state/$task.status"
1351
+ lines=$(status_new_lines_since_cursor "$f" "$endpoint") || return 1
1352
+ [ -n "$lines" ] || continue
1353
+ while IFS= read -r line; do
1354
+ [ -n "$line" ] || continue
1355
+ status_line_is_unread_surface "$line" || continue
1356
+ printf '%s\t%s\n' "$task" "$line"
1357
+ done <<EOF
1358
+ $lines
1359
+ EOF
1360
+ done <<EOF
1361
+ $snapshot
1362
+ EOF
1363
+ }
1364
+
1365
+ # Fold material routed-work phases in the same keyed event stream.
1366
+ # A working or declared-pause event opens or replaces one phase for its key.
1367
+ # A later done, failed, needs-decision, blocked, or resolved event carrying that
1368
+ # key closes the phase, because it has moved to a terminal or separately tracked
1369
+ # state.
1370
+ # A bare legacy event uses the default key, preserving one-phase behavior.
1371
+ # This fold is evidence about whether a parent event was explicitly superseded.
1372
+ # It is never authoritative current crew state, and consumers must not let an open
1373
+ # phase outrank a structured home snapshot or brokk-crew-state result.
1374
+ _fm_status_open_activities_stream() {
1375
+ local line verb key note resolve held open='' pause
1376
+ resolve=${BROKK_CLASSIFY_RESOLVE_VERB:-$BROKK_CLASSIFY_RESOLVE_VERB_DEFAULT}
1377
+ held=${BROKK_CLASSIFY_ALLFATHER_HELD_VERB:-$BROKK_CLASSIFY_ALLFATHER_HELD_VERB_DEFAULT}
1378
+ pause=${BROKK_CLASSIFY_PAUSED_VERB:-$BROKK_CLASSIFY_PAUSED_VERB_DEFAULT}
1379
+ while IFS= read -r line || [ -n "$line" ]; do
1380
+ # Blank-line guard; see _fm_decision_fold_line for why this is a glob.
1381
+ case "$line" in
1382
+ *[![:space:]]*) ;;
1383
+ *) continue ;;
1384
+ esac
1385
+ verb=$(status_line_verb "$line")
1386
+ key=$(_fm_decision_key "$line") || continue
1387
+ case "$verb" in
1388
+ working|"$pause")
1389
+ note=$(status_line_note "$line")
1390
+ open=$(_fm_decision_drop "$open" "$key")
1391
+ [ -n "$open" ] && open="${open}"$'\n'
1392
+ open="${open}${key}"$'\t'"${verb}"$'\t'"${note}"$'\n'
1393
+ ;;
1394
+ done|failed|needs-decision|blocked|"$resolve"|"$held")
1395
+ open=$(_fm_decision_drop "$open" "$key")
1396
+ [ -n "$open" ] && open="${open}"$'\n'
1397
+ ;;
1398
+ esac
1399
+ done
1400
+ printf '%s' "$open"
1401
+ }
1402
+
1403
+ status_open_activities() { # <status-file-or-dash>
1404
+ local f=$1
1405
+ if [ "$f" = - ]; then
1406
+ _fm_status_open_activities_stream
1407
+ return 0
1408
+ fi
1409
+ [ -f "$f" ] || return 0
1410
+ _fm_status_open_activities_stream < "$f"
1411
+ }
1412
+
1413
+ # task id from a recorded window target, falling back to the tmux-shaped
1414
+ # "<session>:fm-<id>" form when no metadata state is available.
1415
+ window_to_task() {
1416
+ local w=$1 state=${2:-${STATE:-${BROKK_STATE_OVERRIDE:-}}} meta mw mt t
1417
+ if [ -n "$state" ]; then
1418
+ for meta in "$state"/*.meta; do
1419
+ [ -e "$meta" ] || continue
1420
+ mw=$(grep '^window=' "$meta" 2>/dev/null | tail -1 | cut -d= -f2- || true)
1421
+ mt=$(grep '^terminal=' "$meta" 2>/dev/null | tail -1 | cut -d= -f2- || true)
1422
+ [ "$mw" = "$w" ] || [ "$mt" = "$w" ] || continue
1423
+ t=$(basename "$meta")
1424
+ t=${t%.meta}
1425
+ printf '%s' "$t"
1426
+ return 0
1427
+ done
1428
+ fi
1429
+ t="${w##*:}"; t="${t#fm-}"; printf '%s' "$t"
1430
+ }
1431
+
1432
+ # Capture the bytes of an append-only status log at or after <start-offset> under
1433
+ # one size-and-identity snapshot.
1434
+ # The record form prints `<endpoint>\t<identity>\t<events>` and returns 0 when
1435
+ # the span has actionable events, joining every such event in source order with
1436
+ # ` ; ` so callers report the complete captured span before committing it.
1437
+ # It returns 1 after a successful classification with no actionable event; an
1438
+ # existing log still prints its committable endpoint and identity, while an absent
1439
+ # log is the ordinary empty case and prints no record.
1440
+ # It returns 2 with no committable endpoint when an existing status object cannot
1441
+ # be classified.
1442
+ # The simpler wrapper prints only the event field, and the predicate discards the
1443
+ # record; all three inherit the library-header contract above.
1444
+ #
1445
+ # A keyed `needs-decision` or `blocked` transition accepted by the whole-file
1446
+ # fold is included only when that fold still names the exact opening as live.
1447
+ # A transition rejected by the reserved-key vocabulary is surfaced instead as a
1448
+ # reconciliation signal and never treated here as an open decision.
1449
+ # status_open_decisions remains the single owner of open/closed semantics,
1450
+ # including same-key reopening and reserved-key handling.
1451
+ # Every other Allfather-relevant event is terminal and always actionable.
1452
+ _fm_decision_origin_drop() { # <origins> <key>
1453
+ local origin
1454
+ while IFS= read -r origin; do
1455
+ case "$origin" in "$2"$'\t'*) ;; *) [ -n "$origin" ] && printf '%s\n' "$origin" ;; esac
1456
+ done <<EOF
1457
+ $1
1458
+ EOF
1459
+ }
1460
+
1461
+ _fm_status_open_decision_origins() { # <status-file>
1462
+ local f=$1 line open='' after key verb note number=0 origins=''
1463
+ local resolve held
1464
+ resolve=${BROKK_CLASSIFY_RESOLVE_VERB:-$BROKK_CLASSIFY_RESOLVE_VERB_DEFAULT}
1465
+ held=${BROKK_CLASSIFY_ALLFATHER_HELD_VERB:-$BROKK_CLASSIFY_ALLFATHER_HELD_VERB_DEFAULT}
1466
+ while IFS= read -r line || [ -n "$line" ]; do
1467
+ number=$((number + 1))
1468
+ after=$(_fm_decision_fold_line "$open" "$line" "$resolve" "$held")
1469
+ key=$(_fm_decision_key "$line") || { open=$after; continue; }
1470
+ verb=$(status_line_verb "$line")
1471
+ note=$(status_line_note "$line")
1472
+ case "$verb" in
1473
+ needs-decision|blocked)
1474
+ if _fm_open_set_has "$after" "$key" \
1475
+ && [ "$(_fm_open_set_verb "$after" "$key")" = "$verb" ]; then
1476
+ case "$after" in
1477
+ "$key"$'\t'"$verb"$'\t'"$note"|*$'\n'"$key"$'\t'"$verb"$'\t'"$note")
1478
+ origins=$(_fm_decision_origin_drop "$origins" "$key")
1479
+ [ -n "$origins" ] && origins="${origins}"$'\n'
1480
+ origins="${origins}${key}"$'\t'"${number}"
1481
+ ;;
1482
+ esac
1483
+ fi
1484
+ ;;
1485
+ "$resolve"|"$held")
1486
+ _fm_open_set_has "$after" "$key" || origins=$(_fm_decision_origin_drop "$origins" "$key")
1487
+ ;;
1488
+ esac
1489
+ open=$after
1490
+ done < "$f"
1491
+ printf '%s' "$origins"
1492
+ }
1493
+
1494
+ status_span_first_actionable_record() { # <status-file> <start-offset>
1495
+ local f=$1 start=${2:-0} size ident cur_ident scratch chunk_file full_file prefix_file
1496
+ local line verb key origins='' folded=0 rc=1 failed=0 prefix_lines=0 line_number=0 live_line='' events='' _line _key
1497
+ [ -e "$f" ] || { [ -L "$f" ] && return 2; return 1; }
1498
+ [ -f "$f" ] && [ -r "$f" ] && [ ! -L "$f" ] || return 2
1499
+ ident=$(_fm_open_decisions_file_ident "$f") || return 2
1500
+ size=$(_fm_status_file_size "$f") || return 2
1501
+ size=${size//[[:space:]]/}
1502
+ case "$size" in ''|*[!0-9]*) return 2 ;; esac
1503
+ case "$start" in ''|*[!0-9]*) start=0 ;; esac
1504
+ [ "$start" -le "$size" ] || start=0
1505
+ [ "$start" -lt "$size" ] || { printf '%s\t%s' "$size" "$ident"; return 1; }
1506
+ scratch=$(_fm_status_span_scratch "$f") || return 2
1507
+ chunk_file="${scratch}.span"; full_file="${scratch}.full"; prefix_file="${scratch}.prefix"
1508
+ _fm_status_read_span "$f" "$start" "$((size - start))" > "$chunk_file" 2>/dev/null \
1509
+ || { rm -f "$chunk_file" "$full_file" "$prefix_file"; return 2; }
1510
+ cur_ident=$(_fm_open_decisions_file_ident "$f") || {
1511
+ rm -f "$chunk_file" "$full_file" "$prefix_file"; return 2;
1512
+ }
1513
+ [ "$cur_ident" = "$ident" ] || { rm -f "$chunk_file" "$full_file" "$prefix_file"; return 2; }
1514
+ while IFS= read -r line || [ -n "$line" ]; do
1515
+ line_number=$((line_number + 1))
1516
+ case "$line" in *[![:space:]]*) ;; *) continue ;; esac
1517
+ status_is_Allfather_relevant "$line" || continue
1518
+ verb=$(status_line_verb "$line")
1519
+ case "$verb" in
1520
+ needs-decision|blocked)
1521
+ key=$(_fm_decision_key "$line") || {
1522
+ [ -n "$events" ] && events="${events} ; "
1523
+ events="${events}${line}"
1524
+ rc=0
1525
+ continue
1526
+ }
1527
+ _fm_decision_key_transition_allowed "$key" "$(status_line_note "$line")" || {
1528
+ [ -n "$events" ] && events="${events} ; "
1529
+ events="${events}reconciliation-required: ${line}"
1530
+ rc=0
1531
+ continue
1532
+ }
1533
+ if [ "$folded" -eq 0 ]; then
1534
+ _fm_status_read_span "$f" 0 "$size" > "$full_file" 2>/dev/null \
1535
+ || { failed=1; break; }
1536
+ if [ "$start" -gt 0 ]; then
1537
+ _fm_status_read_span "$full_file" 0 "$start" > "$prefix_file" 2>/dev/null \
1538
+ || { failed=1; break; }
1539
+ while IFS= read -r _line || [ -n "$_line" ]; do prefix_lines=$((prefix_lines + 1)); done < "$prefix_file"
1540
+ fi
1541
+ origins=$(_fm_status_open_decision_origins "$full_file") || { failed=1; break; }
1542
+ folded=1
1543
+ fi
1544
+ live_line=$(while IFS=$(printf '\t') read -r _key _line; do
1545
+ [ "$_key" = "$key" ] && { printf '%s' "$_line"; break; }
1546
+ done <<EOF
1547
+ $origins
1548
+ EOF
1549
+ )
1550
+ [ -n "$live_line" ] && [ "$((prefix_lines + line_number))" -eq "$live_line" ] || continue
1551
+ [ -n "$events" ] && events="${events} ; "
1552
+ events="${events}${line}"
1553
+ rc=0
1554
+ ;;
1555
+ *)
1556
+ [ -n "$events" ] && events="${events} ; "
1557
+ events="${events}${line}"
1558
+ rc=0
1559
+ ;;
1560
+ esac
1561
+ done < "$chunk_file"
1562
+ rm -f "$chunk_file" "$full_file" "$prefix_file"
1563
+ [ "$failed" -eq 0 ] || return 2
1564
+ if [ "$rc" -eq 0 ]; then printf '%s\t%s\t%s' "$size" "$ident" "$events"; else printf '%s\t%s' "$size" "$ident"; fi
1565
+ return "$rc"
1566
+ }
1567
+
1568
+ status_span_first_actionable() { # <status-file> <start-offset>
1569
+ local record rc rest
1570
+ record=$(status_span_first_actionable_record "$1" "${2:-0}")
1571
+ rc=$?
1572
+ if [ "$rc" -eq 0 ]; then
1573
+ rest=${record#*$'\t'}
1574
+ printf '%s' "${rest#*$'\t'}"
1575
+ fi
1576
+ return "$rc"
1577
+ }
1578
+
1579
+ status_span_has_actionable() { # <status-file> <start-offset>
1580
+ status_span_first_actionable_record "$1" "${2:-0}" > /dev/null
1581
+ }
1582
+
1583
+ # Classify WHY an idle/stale crew MIGHT be safely absorbed instead of surfaced,
1584
+ # from bin/vor-crew-state.sh's one authoritative current-state line
1585
+ # ("state: <s> · source: <src> · <detail>"). Prints exactly one token:
1586
+ # working - an actively-running no-mistakes step (running/fixing/ci) or a busy
1587
+ # pane; the crew is legitimately mid-work on a static-looking pane
1588
+ # (e.g. waiting on CI);
1589
+ # paused - the crew's authoritative current state is a declared external-wait
1590
+ # pause (paused:), which is EXPECTED to idle;
1591
+ # none - neither, so the wake must surface (a stopped/finished/parked/failed/
1592
+ # torn-down/unknown crew, or an unreadable verdict).
1593
+ # One vor-crew-state.sh read serves BOTH absorb reasons at once. Reading the state
1594
+ # authoritatively (not the status log) is what keeps run-step precedence: a crew
1595
+ # that appended paused: but then STARTED a run reports working, never paused.
1596
+ # NOT a pure read: vor-crew-state.sh may make a bounded no-mistakes call, so callers
1597
+ # run it only on no-verb signal and first-sighting stale paths, never every wake.
1598
+ # BROKK_CREW_STATE_BIN lets tests replace the verdict.
1599
+ crew_absorb_class() { # <id>
1600
+ local id=$1 line state src
1601
+ [ -n "$id" ] || { printf 'none'; return; }
1602
+ line=$("$BROKK_CREW_STATE_BIN" "$id" 2>/dev/null) || true
1603
+ case "$line" in state:*) ;; *) printf 'none'; return ;; esac
1604
+ state=${line#state: }; state=${state%% *}
1605
+ if [ "$state" = paused ]; then printf 'paused'; return; fi
1606
+ if [ "$state" = working ]; then
1607
+ src=${line#*source: }; src=${src%% *}
1608
+ case "$src" in run-step|pane) printf 'working'; return ;; esac
1609
+ fi
1610
+ printf 'none'
1611
+ }
1612
+
1613
+ # 0 if crew <id> shows POSITIVE evidence it is still working (crew_absorb_class
1614
+ # reports `working`). This is the "provably working" predicate at the heart of
1615
+ # absorb-only-on-positive-evidence. This is the sole proof for stale wakes and the
1616
+ # shared authoritative proof for no-verb signals. Where a home opts in, brokk-watch.sh
1617
+ # may additionally absorb a bare turn-end on bounded pane churn, while every other
1618
+ # failed verdict surfaces
1619
+ # because the crew may be done, waiting on a decision, or wedged. For stale panes
1620
+ # it is checked before trusting the status log so a pre-validation Allfather-relevant
1621
+ # line does not override an active run. See crew_absorb_class for the exact
1622
+ # working/paused/none decision.
1623
+ crew_is_provably_working() { # <id>
1624
+ [ "$(crew_absorb_class "$1")" = working ]
1625
+ }
1626
+
1627
+ # 0 if crew <id>'s authoritative current state is a declared external-wait pause.
1628
+ # The stale path absorbs such a crew (on a long re-surface cadence) instead of
1629
+ # escalating a possible wedge.
1630
+ crew_is_paused() { # <id>
1631
+ [ "$(crew_absorb_class "$1")" = paused ]
1632
+ }
1633
+
1634
+ # Directories excluded from the worktree write probe below, and the depth it walks.
1635
+ # The excluded set is everything a supervisor read or a package manager can write
1636
+ # without the crew doing any work - .git first, so brokk's own read-only git
1637
+ # commands against the worktree can never make the probe self-fulfilling - plus the
1638
+ # large generated trees that would make the walk expensive. Both are overridable so
1639
+ # a home with an unusual layout can widen or narrow the probe. The list is a skip
1640
+ # list, so clearing it skips nothing and widens the walk to the whole depth-bounded
1641
+ # tree; it never disables the probe, which would quietly cost the wedge detector a
1642
+ # liveness input on a home that meant to widen it. Defaulted with the plain form so
1643
+ # an explicitly empty value stays empty: clearing the knob in the environment is the
1644
+ # documented way to ask for that wider walk, and treating empty as unset would hand
1645
+ # the default skip list back to exactly the home that asked for more coverage.
1646
+ BROKK_WORKTREE_WRITE_PRUNE=${BROKK_WORKTREE_WRITE_PRUNE-'.git node_modules .venv venv __pycache__ .mypy_cache .pytest_cache .ruff_cache .tox target dist build .next .cache vendor'}
1647
+ BROKK_WORKTREE_WRITE_MAXDEPTH=${BROKK_WORKTREE_WRITE_MAXDEPTH:-6}
1648
+
1649
+ # Wall-clock seconds the probe's single walk may take. The walk runs synchronously
1650
+ # inside the caller's poll loop at the exact moment an escalation would otherwise
1651
+ # fire, and -xdev keeps it out of a nested mount but cannot help when the worktree
1652
+ # root ITSELF sits on a hung network or container mount; unbounded, such a walk
1653
+ # would wedge the very supervisor that exists to notice a wedge, stalling its
1654
+ # heartbeat instead of escalating. Hitting the bound is a negative outcome like
1655
+ # every other: it reads as no evidence, so the caller's escalation schedule is
1656
+ # untouched and a stall that writes nothing still escalates on the existing
1657
+ # schedule. A value that is not a positive integer is not a bound at all (`timeout
1658
+ # 0` and the perl fallback's `alarm 0` both disable the deadline), so the default
1659
+ # applies instead; the check lives at the point of use so an in-process override
1660
+ # gets it too.
1661
+ BROKK_WORKTREE_WRITE_TIMEOUT=${BROKK_WORKTREE_WRITE_TIMEOUT:-10}
1662
+
1663
+ # 0 when some regular file under <id>'s recorded worktree is newer than
1664
+ # <anchor-file>: positive evidence the crew is still producing work even though its
1665
+ # rendered pane has gone quiet. This is the third liveness input the wedge detector
1666
+ # has, after pane quietness and the run step, and it exists because neither of
1667
+ # those can see a crew that is writing source, then tests, then documentation
1668
+ # behind a static pane - the 2026-08-14 case of eight consecutive possible-wedge
1669
+ # escalations against a crew that was demonstrably working the whole time.
1670
+ #
1671
+ # 1 for every other outcome, including an id with no recorded worktree, a worktree
1672
+ # that is gone, a missing anchor, and a walk that fails or finds nothing. Absence of
1673
+ # evidence therefore always leaves the caller's existing escalation schedule
1674
+ # untouched, so a crew that writes nothing still escalates exactly as before.
1675
+ #
1676
+ # A kind=eindri-home task records a provisioned brokk home, not a code tree, and
1677
+ # such a home runs its OWN supervision inside it: its state/ directory churns a
1678
+ # watcher beacon, pane hashes, and heartbeats whether or not the mate is producing
1679
+ # anything, so a walk there would report liveness for a mate that has done nothing.
1680
+ # Those homes are excluded outright rather than by pruning "state", which would also
1681
+ # hide a legitimate source directory of that name in an ordinary worktree. The
1682
+ # exclusion is a negative outcome like any other, so an unproductive mate keeps
1683
+ # escalating on the caller's unchanged schedule.
1684
+ #
1685
+ # The anchor is the caller's own idle-window timer file, whose mtime already marks
1686
+ # when the quiet window opened, so `-newer` needs no clock arithmetic, no temp
1687
+ # file, and no portable mtime-setting. Not a pure status-file read (see the header):
1688
+ # one pruned, depth-bounded, wall-clock-bounded walk per call, which callers must
1689
+ # reach only when they are otherwise about to escalate, never on every poll. A walk
1690
+ # that outlives BROKK_WORKTREE_WRITE_TIMEOUT is killed and reported as no evidence, so
1691
+ # a hung mount costs the escalation nothing but the bound. -xdev holds that walk to the
1692
+ # worktree's own filesystem rather than descending into a nested network or container
1693
+ # mount, so a write that lands only under such a mount is one more negative outcome.
1694
+ crew_worktree_written_since() { # <id> <state> <anchor-file>
1695
+ local id=$1 state=$2 anchor=$3 wt kind name hit bound
1696
+ local -a names=() prune=()
1697
+ [ -n "$id" ] || return 1
1698
+ [ -f "$anchor" ] || return 1
1699
+ wt=$(grep '^worktree=' "$state/$id.meta" 2>/dev/null | tail -1 | cut -d= -f2- || true)
1700
+ [ -n "$wt" ] && [ -d "$wt" ] || return 1
1701
+ kind=$(grep '^kind=' "$state/$id.meta" 2>/dev/null | tail -1 | cut -d= -f2- || true)
1702
+ [ "$kind" != eindri-home ] || return 1
1703
+ if [ -e "$wt/.brokk-eindri-home-home" ] || [ -L "$wt/.brokk-eindri-home-home" ]; then
1704
+ return 1
1705
+ fi
1706
+ read -r -a names <<< "$BROKK_WORKTREE_WRITE_PRUNE"
1707
+ for name in ${names[@]+"${names[@]}"}; do
1708
+ [ "${#prune[@]}" -eq 0 ] || prune+=( -o )
1709
+ prune+=( -name "$name" )
1710
+ done
1711
+ bound=$BROKK_WORKTREE_WRITE_TIMEOUT
1712
+ case "$bound" in ''|*[!0-9]*|0) bound=10 ;; esac
1713
+ if [ "${#prune[@]}" -gt 0 ]; then
1714
+ hit=$(fm_run_timed "$bound" find "$wt" -xdev -maxdepth "$BROKK_WORKTREE_WRITE_MAXDEPTH" \
1715
+ \( "${prune[@]}" \) -prune -o -type f -newer "$anchor" -print -quit 2>/dev/null || true)
1716
+ else
1717
+ hit=$(fm_run_timed "$bound" find "$wt" -xdev -maxdepth "$BROKK_WORKTREE_WRITE_MAXDEPTH" \
1718
+ -type f -newer "$anchor" -print -quit 2>/dev/null || true)
1719
+ fi
1720
+ [ -n "$hit" ]
1721
+ }
1722
+
1723
+ # 0 (benign/absorb) if EVERY task referenced by a no-verb "signal:" wake is provably
1724
+ # working; 1 (actionable/surface) if any is not, or no task can be resolved. Pass the
1725
+ # same space-separated file list the caller classified with the span read above.
1726
+ # Files are mapped to task ids by stripping the .status / .turn-ended suffix;
1727
+ # a no-verb wake with nothing
1728
+ # provably working must surface, so an empty/unresolvable list returns 1.
1729
+ # A kind=eindri-home task's .status signal is never absorbable here regardless of
1730
+ # busy evidence: that stream is the mate's routed-reply channel, so every append
1731
+ # is parent-directed content the supervisor must read (a routed reply, a newly
1732
+ # raised decision, a mirrored remote line), and a busy mate agent makes its note
1733
+ # more current, not less deliverable. Scoped to .status files - a mate's bare
1734
+ # turn-ended ping still uses the ordinary provably-working absorb.
1735
+ signal_crew_provably_working() { # <file> ...
1736
+ local f base dir task seen=""
1737
+ for f in "$@"; do
1738
+ base=${f##*/}
1739
+ dir=${f%/*}
1740
+ [ "$dir" != "$f" ] || dir=.
1741
+ case "$base" in
1742
+ *.status) task=${base%.status} ;;
1743
+ *.turn-ended) task=${base%.turn-ended} ;;
1744
+ *) continue ;;
1745
+ esac
1746
+ [ -n "$task" ] || continue
1747
+ case "$base" in
1748
+ *.status)
1749
+ if [ "$(grep '^kind=' "$dir/$task.meta" 2>/dev/null | tail -1 | cut -d= -f2-)" = eindri-home ]; then
1750
+ return 1
1751
+ fi
1752
+ ;;
1753
+ esac
1754
+ case " $seen " in *" $task "*) continue ;; esac
1755
+ seen="$seen $task"
1756
+ crew_is_provably_working "$task" || return 1
1757
+ done
1758
+ [ -n "$seen" ] || return 1
1759
+ return 0
1760
+ }
1761
+
1762
+ # 0 (terminal/actionable) if a stale window's last status line is
1763
+ # Allfather-relevant; 1 otherwise, including the no-status case. A 1 only means
1764
+ # "non-terminal"; the always-on watcher then applies crew_is_provably_working,
1765
+ # while the away-mode daemon applies its persistence recheck.
1766
+ stale_is_terminal() { # <window> <state>
1767
+ local win=$1 state=$2 last
1768
+ last=$(last_status_line "$state/$(window_to_task "$win" "$state").status")
1769
+ [ -n "$last" ] && status_is_Allfather_relevant "$last"
1770
+ }