@zerwiz/ymir 0.1.4 → 0.1.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (850) hide show
  1. package/.agents/README.md +291 -0
  2. package/.agents/agents/bragi-marketer.md +111 -0
  3. package/.agents/agents/brokk.md +352 -0
  4. package/.agents/agents/forseti-reviewer.md +93 -0
  5. package/.agents/agents/frigg-consent.md +77 -0
  6. package/.agents/agents/groa-update.md +77 -0
  7. package/.agents/agents/hnoss-designer.md +69 -0
  8. package/.agents/agents/huginn-researcher.md +99 -0
  9. package/.agents/agents/jord-projects.md +78 -0
  10. package/.agents/agents/kvasir-scout.md +91 -0
  11. package/.agents/agents/mimir-planner.md +92 -0
  12. package/.agents/agents/muninn-stow.md +77 -0
  13. package/.agents/agents/saga-bearings.md +78 -0
  14. package/.agents/agents/sindri-developer.md +114 -0
  15. package/.agents/agents/snotra-documenter.md +105 -0
  16. package/.agents/agents/syn-recovery.md +78 -0
  17. package/.agents/agents/tyr-check.md +78 -0
  18. package/.agents/agents/urdh-hold.md +77 -0
  19. package/.agents/agents/volundr-smith.md +80 -0
  20. package/.agents/agents/vor-diagnostics.md +77 -0
  21. package/.agents/assets/agents/naming.md +50 -0
  22. package/.agents/assets/agents/registry.md +93 -0
  23. package/.agents/assets/agents/runtime.md +36 -0
  24. package/.agents/assets/agents/toon-tasks-cli.md +42 -0
  25. package/.agents/assets/templates/company_entity.template.md +37 -0
  26. package/.agents/backend/backends/cmux.sh +649 -0
  27. package/.agents/backend/backends/herdr-eventwait.py +157 -0
  28. package/.agents/backend/backends/herdr-workspace-move.py +114 -0
  29. package/.agents/backend/backends/herdr.sh +3386 -0
  30. package/.agents/backend/backends/orca.sh +290 -0
  31. package/.agents/backend/backends/tmux.sh +357 -0
  32. package/.agents/backend/backends/zellij.sh +700 -0
  33. package/.agents/backend/fm-afk-launch.sh +654 -0
  34. package/.agents/backend/fm-afk-return.sh +241 -0
  35. package/.agents/backend/fm-afk-start.sh +172 -0
  36. package/.agents/backend/fm-arm-command-policy.mjs +960 -0
  37. package/.agents/backend/fm-arm-pretool-check.sh +200 -0
  38. package/.agents/backend/fm-backend-hometag-lib.sh +52 -0
  39. package/.agents/backend/fm-backend.sh +981 -0
  40. package/.agents/backend/fm-backlog-handoff.sh +909 -0
  41. package/.agents/backend/fm-backlog-receive.sh +187 -0
  42. package/.agents/backend/fm-backlog-transition-lib.sh +779 -0
  43. package/.agents/backend/fm-bearings-board.sh +204 -0
  44. package/.agents/backend/fm-bearings-snapshot.sh +546 -0
  45. package/.agents/backend/fm-bootstrap.sh +1568 -0
  46. package/.agents/backend/fm-branch-outcome.sh +203 -0
  47. package/.agents/backend/fm-branch-prompt.sh +101 -0
  48. package/.agents/backend/fm-brief.sh +452 -0
  49. package/.agents/backend/fm-busy-event.sh +234 -0
  50. package/.agents/backend/fm-busy-lib.sh +986 -0
  51. package/.agents/backend/fm-captain-hold.sh +1001 -0
  52. package/.agents/backend/fm-cd-command-policy.mjs +153 -0
  53. package/.agents/backend/fm-cd-pretool-check.sh +190 -0
  54. package/.agents/backend/fm-check-lib.sh +74 -0
  55. package/.agents/backend/fm-check-register.sh +42 -0
  56. package/.agents/backend/fm-check-unregister.sh +52 -0
  57. package/.agents/backend/fm-classify-lib.sh +1770 -0
  58. package/.agents/backend/fm-claude-stop-autoarm.sh +334 -0
  59. package/.agents/backend/fm-composer-lib.sh +1416 -0
  60. package/.agents/backend/fm-config-inherit-lib.sh +1202 -0
  61. package/.agents/backend/fm-config-push.sh +254 -0
  62. package/.agents/backend/fm-control-lib.sh +251 -0
  63. package/.agents/backend/fm-control.sh +878 -0
  64. package/.agents/backend/fm-crew-state.sh +625 -0
  65. package/.agents/backend/fm-cursor-lib.sh +243 -0
  66. package/.agents/backend/fm-decision-hold.sh +232 -0
  67. package/.agents/backend/fm-doc-audience-check.sh +269 -0
  68. package/.agents/backend/fm-dod-lib.sh +67 -0
  69. package/.agents/backend/fm-ensure-agents-md.sh +253 -0
  70. package/.agents/backend/fm-extension-launch-barrier.mjs +129 -0
  71. package/.agents/backend/fm-extension.mjs +2577 -0
  72. package/.agents/backend/fm-extension.sh +16 -0
  73. package/.agents/backend/fm-ff-lib.sh +420 -0
  74. package/.agents/backend/fm-fleet-snapshot.sh +1463 -0
  75. package/.agents/backend/fm-fleet-sync.sh +458 -0
  76. package/.agents/backend/fm-fleet-view.sh +96 -0
  77. package/.agents/backend/fm-gate-refuse-lib.sh +102 -0
  78. package/.agents/backend/fm-guard.sh +242 -0
  79. package/.agents/backend/fm-harness.sh +195 -0
  80. package/.agents/backend/fm-herdr-ci-cleanup.sh +115 -0
  81. package/.agents/backend/fm-herdr-lab.sh +346 -0
  82. package/.agents/backend/fm-herdr-session-cleanup.sh +337 -0
  83. package/.agents/backend/fm-home-seed.sh +984 -0
  84. package/.agents/backend/fm-home-summary-refresh.sh +255 -0
  85. package/.agents/backend/fm-hook-host-lib.sh +36 -0
  86. package/.agents/backend/fm-inactive-reconcile.sh +531 -0
  87. package/.agents/backend/fm-inbox.sh +399 -0
  88. package/.agents/backend/fm-install-actionlint.sh +84 -0
  89. package/.agents/backend/fm-install-herdr.sh +96 -0
  90. package/.agents/backend/fm-install-shellcheck.sh +84 -0
  91. package/.agents/backend/fm-install-treehouse.sh +95 -0
  92. package/.agents/backend/fm-kimi-turnend-hook.sh +276 -0
  93. package/.agents/backend/fm-lease-lib.sh +218 -0
  94. package/.agents/backend/fm-lease.sh +189 -0
  95. package/.agents/backend/fm-line-cap-lib.sh +51 -0
  96. package/.agents/backend/fm-lint-workflows.sh +137 -0
  97. package/.agents/backend/fm-lint.sh +591 -0
  98. package/.agents/backend/fm-lock-lib.sh +104 -0
  99. package/.agents/backend/fm-lock.sh +107 -0
  100. package/.agents/backend/fm-marker-lib.sh +12 -0
  101. package/.agents/backend/fm-merge-local.sh +74 -0
  102. package/.agents/backend/fm-merge-outcome-lib.sh +138 -0
  103. package/.agents/backend/fm-nm-run-lib.sh +124 -0
  104. package/.agents/backend/fm-on.sh +125 -0
  105. package/.agents/backend/fm-operational-input.sh +253 -0
  106. package/.agents/backend/fm-peek.sh +46 -0
  107. package/.agents/backend/fm-pending-reply-lib.sh +1409 -0
  108. package/.agents/backend/fm-pr-check.sh +135 -0
  109. package/.agents/backend/fm-pr-lib.sh +1016 -0
  110. package/.agents/backend/fm-pr-merge.sh +707 -0
  111. package/.agents/backend/fm-pr-poll.sh +110 -0
  112. package/.agents/backend/fm-primary-scope-lib.sh +33 -0
  113. package/.agents/backend/fm-procevent-extension-capture.pl +259 -0
  114. package/.agents/backend/fm-procevent-lavish.sh +628 -0
  115. package/.agents/backend/fm-procevent-lib.sh +892 -0
  116. package/.agents/backend/fm-procevent-quota.sh +290 -0
  117. package/.agents/backend/fm-procevent-remote-reply.sh +577 -0
  118. package/.agents/backend/fm-procevent-when.sh +504 -0
  119. package/.agents/backend/fm-procevent.sh +1466 -0
  120. package/.agents/backend/fm-project-mode.sh +92 -0
  121. package/.agents/backend/fm-project-origin-lib.sh +180 -0
  122. package/.agents/backend/fm-promote.sh +282 -0
  123. package/.agents/backend/fm-public-followup-emit.sh +260 -0
  124. package/.agents/backend/fm-public-followup-lib.sh +398 -0
  125. package/.agents/backend/fm-public-followup.sh +1330 -0
  126. package/.agents/backend/fm-push-transition-lib.sh +168 -0
  127. package/.agents/backend/fm-quota-axi-lib.sh +93 -0
  128. package/.agents/backend/fm-quota-choose.sh +384 -0
  129. package/.agents/backend/fm-remote-delta-read.sh +189 -0
  130. package/.agents/backend/fm-remote-doctor.sh +800 -0
  131. package/.agents/backend/fm-remote-entrypoint.sh +193 -0
  132. package/.agents/backend/fm-remote-file.sh +234 -0
  133. package/.agents/backend/fm-remote-home-provision.sh +259 -0
  134. package/.agents/backend/fm-remote-home-seed.sh +259 -0
  135. package/.agents/backend/fm-remote-inherit-push.sh +90 -0
  136. package/.agents/backend/fm-remote-inherit.sh +179 -0
  137. package/.agents/backend/fm-remote-job-lib.sh +1241 -0
  138. package/.agents/backend/fm-remote-job-reap-orphans.sh +137 -0
  139. package/.agents/backend/fm-remote-job-worker.sh +1124 -0
  140. package/.agents/backend/fm-remote-readiness-lib.sh +44 -0
  141. package/.agents/backend/fm-remote-secondmate-control.sh +340 -0
  142. package/.agents/backend/fm-review-diff.sh +158 -0
  143. package/.agents/backend/fm-secondmate-charter-lib.sh +44 -0
  144. package/.agents/backend/fm-secondmate-nudge-lib.sh +68 -0
  145. package/.agents/backend/fm-secondmate-parent-lib.sh +70 -0
  146. package/.agents/backend/fm-secondmate-reconcile.sh +371 -0
  147. package/.agents/backend/fm-secondmate-registry-lib.sh +311 -0
  148. package/.agents/backend/fm-secondmate-report.sh +88 -0
  149. package/.agents/backend/fm-send.sh +1059 -0
  150. package/.agents/backend/fm-session-lock-lib.sh +176 -0
  151. package/.agents/backend/fm-session-start.sh +970 -0
  152. package/.agents/backend/fm-sessionstart-cursor.sh +40 -0
  153. package/.agents/backend/fm-sessionstart-nudge.sh +45 -0
  154. package/.agents/backend/fm-sessionstart-run.sh +145 -0
  155. package/.agents/backend/fm-spawn.sh +3153 -0
  156. package/.agents/backend/fm-startup-memory-budget-lib.sh +224 -0
  157. package/.agents/backend/fm-startup-memory-budget.sh +94 -0
  158. package/.agents/backend/fm-startup-network.sh +642 -0
  159. package/.agents/backend/fm-stow-cascade.sh +251 -0
  160. package/.agents/backend/fm-subagent-pretool-check.sh +207 -0
  161. package/.agents/backend/fm-supervise-daemon.sh +1732 -0
  162. package/.agents/backend/fm-supervision-instructions.sh +215 -0
  163. package/.agents/backend/fm-supervision-lib.sh +90 -0
  164. package/.agents/backend/fm-supervisor-target-lib.sh +78 -0
  165. package/.agents/backend/fm-tangle-lib.sh +53 -0
  166. package/.agents/backend/fm-task-inbox-lib.sh +402 -0
  167. package/.agents/backend/fm-tasks-axi-lib.sh +123 -0
  168. package/.agents/backend/fm-teardown.sh +2922 -0
  169. package/.agents/backend/fm-test-isolation-proof.sh +588 -0
  170. package/.agents/backend/fm-test-run.sh +2191 -0
  171. package/.agents/backend/fm-timeout-lib.sh +141 -0
  172. package/.agents/backend/fm-timing-lib.sh +179 -0
  173. package/.agents/backend/fm-tmux-lib.sh +291 -0
  174. package/.agents/backend/fm-tool-update-check.sh +898 -0
  175. package/.agents/backend/fm-trace-context-lib.sh +227 -0
  176. package/.agents/backend/fm-transition-lib.sh +103 -0
  177. package/.agents/backend/fm-turnend-guard-cursor.sh +391 -0
  178. package/.agents/backend/fm-turnend-guard-grok.sh +90 -0
  179. package/.agents/backend/fm-turnend-guard.sh +432 -0
  180. package/.agents/backend/fm-update.sh +110 -0
  181. package/.agents/backend/fm-vendor-auth-probe.sh +190 -0
  182. package/.agents/backend/fm-voice-client.py +1373 -0
  183. package/.agents/backend/fm-voice-relay.py +1256 -0
  184. package/.agents/backend/fm-wake-drain.sh +592 -0
  185. package/.agents/backend/fm-wake-grant.sh +127 -0
  186. package/.agents/backend/fm-wake-lib.sh +1849 -0
  187. package/.agents/backend/fm-watch-arm.sh +602 -0
  188. package/.agents/backend/fm-watch-checkpoint.sh +109 -0
  189. package/.agents/backend/fm-watch.sh +1962 -0
  190. package/.agents/backend/fm-x-dismiss.sh +116 -0
  191. package/.agents/backend/fm-x-followup.sh +286 -0
  192. package/.agents/backend/fm-x-lib.sh +1000 -0
  193. package/.agents/backend/fm-x-link.sh +234 -0
  194. package/.agents/backend/fm-x-poll.sh +185 -0
  195. package/.agents/backend/fm-x-reply.sh +385 -0
  196. package/.agents/backend/fm_voice_frame.py +166 -0
  197. package/.agents/backend/fm_voice_records.py +574 -0
  198. package/.agents/backend/model-bridge.py +290 -0
  199. package/.agents/backend/opencode-go-bridge.py +30 -0
  200. package/.agents/bus/README.md +12 -0
  201. package/.agents/bus/messages.json +1 -0
  202. package/.agents/config/agents.machine.example.yaml +16 -0
  203. package/.agents/config/agents.yaml +50 -0
  204. package/.agents/config/agents.yaml.example +72 -0
  205. package/.agents/config/crew-dispatch.json +22 -0
  206. package/.agents/config/cron.yaml +9 -0
  207. package/.agents/config/eindri-dispatch.json +67 -0
  208. package/.agents/config/eindri-harness +1 -0
  209. package/.agents/config/startup-memory-budget +1 -0
  210. package/.agents/config/tailscale-sync.yaml +27 -0
  211. package/.agents/filebrowser/.filebrowser.json +6 -0
  212. package/.agents/filebrowser/README.md +11 -0
  213. package/.agents/gateway/README.md +12 -0
  214. package/.agents/github/README.md +15 -0
  215. package/.agents/github/webhooks/.gitkeep +0 -0
  216. package/.agents/github/workflows/.gitkeep +0 -0
  217. package/.agents/harness/opencode/plugins/lib/rodd-operational-input.js +37 -0
  218. package/.agents/harness/opencode/plugins/package.json +4 -0
  219. package/.agents/harness/opencode/plugins/saga-sessionstart.js +77 -0
  220. package/.agents/harness/opencode/plugins/syn-cd-check.js +61 -0
  221. package/.agents/harness/opencode/plugins/syn-pretool-check.js +68 -0
  222. package/.agents/harness/opencode/plugins/syn-turnend-guard.js +105 -0
  223. package/.agents/harness/opencode/plugins/syn-watch-arm.js +521 -0
  224. package/.agents/memory/README.md +46 -0
  225. package/.agents/memory/kaia.engram +0 -0
  226. package/.agents/memory/kaia.engram-shm +0 -0
  227. package/.agents/memory/kaia.engram-wal +0 -0
  228. package/.agents/memory/runes_audit.md +5 -0
  229. package/.agents/memory/well/episodes.jsonl +367 -0
  230. package/.agents/memory/well/workspace.jsonl +27 -0
  231. package/.agents/migrations/0001-hodd-layout.sh +38 -0
  232. package/.agents/migrations/0002-a2a-mcp.sh +27 -0
  233. package/.agents/migrations/0003-private-data-separation.sh +111 -0
  234. package/.agents/migrations/0004-hoard-and-realms.sh +104 -0
  235. package/.agents/sandbox/Dockerfile.utgard +19 -0
  236. package/.agents/sandbox/README.md +14 -0
  237. package/.agents/sandbox/utgard.config.json +14 -0
  238. package/.agents/skills/README.md +59 -0
  239. package/.agents/skills/bragi-marketing/README.md +3 -0
  240. package/.agents/skills/bragi-marketing/SKILL.md +82 -0
  241. package/.agents/skills/eindri-homes/README.md +3 -0
  242. package/.agents/skills/eindri-homes/SKILL.md +291 -0
  243. package/.agents/skills/eindri-homes/assets/control-plane.md +122 -0
  244. package/.agents/skills/eindri-homes/assets/dispatch.md +80 -0
  245. package/.agents/skills/frigg-consent/README.md +3 -0
  246. package/.agents/skills/frigg-consent/SKILL.md +56 -0
  247. package/.agents/skills/galdr-ymirsystem/README.md +3 -0
  248. package/.agents/skills/galdr-ymirsystem/SKILL.md +158 -0
  249. package/.agents/skills/galdr-ymirsystem/assets/README.md +74 -0
  250. package/.agents/skills/galdr-ymirsystem/assets/brokk-distro-runtime.md +390 -0
  251. package/.agents/skills/galdr-ymirsystem/assets/build-method.md +113 -0
  252. package/.agents/skills/galdr-ymirsystem/assets/build-tool-categories.md +178 -0
  253. package/.agents/skills/galdr-ymirsystem/assets/eindri-orchestration.md +518 -0
  254. package/.agents/skills/galdr-ymirsystem/assets/eindri-profiles.md +136 -0
  255. package/.agents/skills/galdr-ymirsystem/assets/harness-integration/README.md +673 -0
  256. package/.agents/skills/galdr-ymirsystem/assets/harness-integration/claude-code.md +197 -0
  257. package/.agents/skills/galdr-ymirsystem/assets/harness-integration/codex.md +191 -0
  258. package/.agents/skills/galdr-ymirsystem/assets/harness-integration/cursor.md +193 -0
  259. package/.agents/skills/galdr-ymirsystem/assets/harness-integration/opencode.md +262 -0
  260. package/.agents/skills/galdr-ymirsystem/assets/harness-integration/pi.md +295 -0
  261. package/.agents/skills/galdr-ymirsystem/assets/hlidskjalf-ui.md +496 -0
  262. package/.agents/skills/galdr-ymirsystem/assets/installation.md +571 -0
  263. package/.agents/skills/galdr-ymirsystem/assets/local-models/HOST-DATA.example.md +72 -0
  264. package/.agents/skills/galdr-ymirsystem/assets/local-models.md +274 -0
  265. package/.agents/skills/galdr-ymirsystem/assets/memory-well.md +161 -0
  266. package/.agents/skills/galdr-ymirsystem/assets/nornir-jobs.md +377 -0
  267. package/.agents/skills/galdr-ymirsystem/assets/norse-naming.md +325 -0
  268. package/.agents/skills/galdr-ymirsystem/assets/odrerir-hall.md +129 -0
  269. package/.agents/skills/galdr-ymirsystem/assets/pi-boot/einherjar-spawn.schema.json +81 -0
  270. package/.agents/skills/galdr-ymirsystem/assets/pi-boot/herdr-profile.toml +75 -0
  271. package/.agents/skills/galdr-ymirsystem/assets/pi-boot/pi-profile.yml +96 -0
  272. package/.agents/skills/galdr-ymirsystem/assets/pi-boot/supervision-tree.yml +163 -0
  273. package/.agents/skills/galdr-ymirsystem/assets/pi-boot-guide.md +58 -0
  274. package/.agents/skills/galdr-ymirsystem/assets/porting-upstream-to-norse.md +301 -0
  275. package/.agents/skills/galdr-ymirsystem/assets/principles.md +385 -0
  276. package/.agents/skills/galdr-ymirsystem/assets/reference-adoption.md +99 -0
  277. package/.agents/skills/galdr-ymirsystem/assets/registry.md +379 -0
  278. package/.agents/skills/galdr-ymirsystem/assets/runtime-compliance.md +463 -0
  279. package/.agents/skills/galdr-ymirsystem/assets/runtime-components.md +553 -0
  280. package/.agents/skills/galdr-ymirsystem/assets/smidja.md +175 -0
  281. package/.agents/skills/galdr-ymirsystem/schemas/toon-schemas.md +190 -0
  282. package/.agents/skills/galdr-ymirsystem/scripts/README.md +45 -0
  283. package/.agents/skills/galdr-ymirsystem/scripts/bench-one.sh +248 -0
  284. package/.agents/skills/galdr-ymirsystem/scripts/compliance-check.sh +360 -0
  285. package/.agents/skills/galdr-ymirsystem/scripts/gpu-sample.sh +43 -0
  286. package/.agents/skills/galdr-ymirsystem/scripts/stop-all.sh +34 -0
  287. package/.agents/skills/galdr-ymirsystem/scripts/toon-check.py +188 -0
  288. package/.agents/skills/gjallarhorn-relay/README.md +3 -0
  289. package/.agents/skills/gjallarhorn-relay/SKILL.md +283 -0
  290. package/.agents/skills/groa-update/SKILL.md +70 -0
  291. package/.agents/skills/hamr-adapters/README.md +3 -0
  292. package/.agents/skills/hamr-adapters/SKILL.md +123 -0
  293. package/.agents/skills/hamr-adapters/references/common/control-and-recovery.md +37 -0
  294. package/.agents/skills/hamr-adapters/references/common/dispatch.md +32 -0
  295. package/.agents/skills/hamr-adapters/references/common/model-and-effort.md +42 -0
  296. package/.agents/skills/hamr-adapters/references/common/primary-hooks.md +40 -0
  297. package/.agents/skills/hamr-adapters/references/harness/claude.md +55 -0
  298. package/.agents/skills/hamr-adapters/references/harness/codex.md +43 -0
  299. package/.agents/skills/hamr-adapters/references/harness/cursor.md +75 -0
  300. package/.agents/skills/hamr-adapters/references/harness/grok.md +69 -0
  301. package/.agents/skills/hamr-adapters/references/harness/kimi.md +51 -0
  302. package/.agents/skills/hamr-adapters/references/harness/muse.md +70 -0
  303. package/.agents/skills/hamr-adapters/references/harness/opencode.md +42 -0
  304. package/.agents/skills/hamr-adapters/references/harness/pi.md +56 -0
  305. package/.agents/skills/herdr-panes/README.md +3 -0
  306. package/.agents/skills/herdr-panes/SKILL.md +92 -0
  307. package/.agents/skills/herdr-panes/assets/herdr-backend.md +346 -0
  308. package/.agents/skills/herdr-panes/assets/tmux-backend.md +115 -0
  309. package/.agents/skills/hnoss-design/DESIGN.md +19 -0
  310. package/.agents/skills/hnoss-design/README.md +3 -0
  311. package/.agents/skills/hnoss-design/SKILL.md +83 -0
  312. package/.agents/skills/hvild-afk/README.md +3 -0
  313. package/.agents/skills/hvild-afk/SKILL.md +241 -0
  314. package/.agents/skills/jord-projects/README.md +3 -0
  315. package/.agents/skills/jord-projects/SKILL.md +92 -0
  316. package/.agents/skills/lifecycle/SKILL.md +56 -0
  317. package/.agents/skills/lifecycle/smoke_test.sh +52 -0
  318. package/.agents/skills/lifecycle/start.sh +21 -0
  319. package/.agents/skills/lifecycle/status.sh +35 -0
  320. package/.agents/skills/lifecycle/stop.sh +16 -0
  321. package/.agents/skills/muninn-stow/README.md +3 -0
  322. package/.agents/skills/muninn-stow/SKILL.md +309 -0
  323. package/.agents/skills/no-mistakes/SKILL.md +379 -0
  324. package/.agents/skills/nornir-schedule/README.md +3 -0
  325. package/.agents/skills/nornir-schedule/SKILL.md +25 -0
  326. package/.agents/skills/nornir-schedule/assets/events.md +137 -0
  327. package/.agents/skills/nornir-schedule/assets/quota.md +131 -0
  328. package/.agents/skills/nsr-compliance/README.md +3 -0
  329. package/.agents/skills/nsr-compliance/SKILL.md +29 -0
  330. package/.agents/skills/nsr-compliance/assets/nsr/README.md +40 -0
  331. package/.agents/skills/nsr-compliance/assets/nsr/assets/RULES/README.md +15 -0
  332. package/.agents/skills/nsr-compliance/assets/nsr/assets/RULES/git-workflow.md +18 -0
  333. package/.agents/skills/nsr-compliance/assets/nsr/assets/RULES/security.md +22 -0
  334. package/.agents/skills/nsr-compliance/assets/nsr/assets/RULES/tech-stack.md +8 -0
  335. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/SKILL.md +71 -0
  336. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/config/core_four.yaml +16 -0
  337. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/gates/check_danger.sh +102 -0
  338. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/gates/check_wiring.sh +69 -0
  339. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/gates/validate_code.sh +29 -0
  340. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/gates/verify_docs.py +66 -0
  341. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/generate.sh +109 -0
  342. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/harness/envelopes/result_envelope.json +14 -0
  343. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/harness/envelopes/task_envelope.json +20 -0
  344. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/harness/runner.py +303 -0
  345. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/telemetry/logger.py +112 -0
  346. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/templates/compliance/README.md +18 -0
  347. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/templates/compliance/config/README.md +10 -0
  348. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/templates/compliance/gates/README.md +12 -0
  349. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/templates/compliance/gates/check_env.sh +23 -0
  350. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/templates/compliance/gates/check_paths.sh +20 -0
  351. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/templates/compliance/gates/check_platform.sh +23 -0
  352. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/templates/compliance/harness/README.md +11 -0
  353. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/templates/compliance/harness/envelopes/README.md +10 -0
  354. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/templates/compliance/installer/README.md +10 -0
  355. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/templates/compliance/installer/install.sh +125 -0
  356. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/templates/compliance/telemetry/README.md +11 -0
  357. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/README.md +20 -0
  358. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/features/README.md +36 -0
  359. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/features/SKILL-template.md +14 -0
  360. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/git_ops/README.md +25 -0
  361. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/git_ops/SKILL.md +13 -0
  362. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/git_ops/create_branch.sh +6 -0
  363. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/git_ops/safe_commit.sh +9 -0
  364. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/git_ops/sync_upstream.sh +6 -0
  365. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/lifecycle/README.md +27 -0
  366. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/lifecycle/SKILL.md +15 -0
  367. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/lifecycle/smoke_test.sh +12 -0
  368. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/lifecycle/start.sh +19 -0
  369. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/lifecycle/status.sh +12 -0
  370. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/lifecycle/stop.sh +15 -0
  371. package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/README.md +18 -0
  372. package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/config/README.md +10 -0
  373. package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/config/core_four.yaml +13 -0
  374. package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/gates/README.md +12 -0
  375. package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/gates/check_danger.sh +102 -0
  376. package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/gates/check_env.sh +23 -0
  377. package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/gates/check_paths.sh +20 -0
  378. package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/gates/check_platform.sh +23 -0
  379. package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/gates/check_wiring.sh +69 -0
  380. package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/harness/README.md +11 -0
  381. package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/harness/envelopes/README.md +10 -0
  382. package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/harness/envelopes/result_envelope.json +14 -0
  383. package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/harness/envelopes/task_envelope.json +20 -0
  384. package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/harness/runner.py +303 -0
  385. package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/installer/README.md +10 -0
  386. package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/installer/install.sh +125 -0
  387. package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/telemetry/README.md +11 -0
  388. package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/telemetry/logger.py +112 -0
  389. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/BEST_PRACTICES/README.md +14 -0
  390. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/BEST_PRACTICES/api.md +17 -0
  391. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/BEST_PRACTICES/compliance-wiring.md +155 -0
  392. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/BEST_PRACTICES/frontend.md +15 -0
  393. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/CI_CD/README.md +11 -0
  394. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/CI_CD/deployment/README.md +23 -0
  395. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/CI_CD/deployment/clients/README.md +16 -0
  396. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/CI_CD/deployment/clients/template-README.md +11 -0
  397. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/CI_CD/deployment/deploy.sh +49 -0
  398. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/CI_CD/deployment/envs/README.md +12 -0
  399. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/CI_CD/deployment/envs/development.env.example +17 -0
  400. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/CI_CD/deployment/envs/production.env.example +17 -0
  401. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/CI_CD/deployment/envs/staging.env.example +17 -0
  402. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/CI_CD/deployment/tenants/README.md +11 -0
  403. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/CI_CD/deployment/tenants/template.env.example +8 -0
  404. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/DEVELOPER_SETUP/README.md +26 -0
  405. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/DEVELOPER_SETUP/agent.md +7 -0
  406. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/DEVELOPER_SETUP/developers/dev-1.md +20 -0
  407. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/DEVELOPER_SETUP/linux.md +9 -0
  408. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/DEVELOPER_SETUP/macos.md +10 -0
  409. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/DEVELOPER_SETUP/shared.md +19 -0
  410. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/DEVELOPER_SETUP/windows.md +14 -0
  411. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/HOSTING/README.md +25 -0
  412. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/HOSTING/_template.md +41 -0
  413. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/HOSTING/hosting-1.md +41 -0
  414. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/README.md +21 -0
  415. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/RUNBOOK/README.md +15 -0
  416. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/RUNBOOK/deployment.md +19 -0
  417. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/RUNBOOK/disaster-recovery.md +15 -0
  418. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/RUNBOOK/ops-tasks.md +14 -0
  419. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/features/README.md +16 -0
  420. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/project-skills.md +146 -0
  421. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/research/README.md +11 -0
  422. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/research/software-compliance-research.md +269 -0
  423. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/working-with-agents.md +260 -0
  424. package/.agents/skills/nsr-compliance/assets/nsr/assets/root/AGENTS.md +38 -0
  425. package/.agents/skills/nsr-compliance/assets/nsr/assets/root/ARCHITECTURE.md +36 -0
  426. package/.agents/skills/nsr-compliance/assets/nsr/assets/root/BEST_PRACTICES.md +27 -0
  427. package/.agents/skills/nsr-compliance/assets/nsr/assets/root/CI_CD.md +23 -0
  428. package/.agents/skills/nsr-compliance/assets/nsr/assets/root/FEATURES.md +15 -0
  429. package/.agents/skills/nsr-compliance/assets/nsr/assets/root/README.md +22 -0
  430. package/.agents/skills/nsr-compliance/assets/nsr/assets/root/RULES.md +37 -0
  431. package/.agents/skills/nsr-compliance/assets/nsr/assets/root/STRUCTURE.md +45 -0
  432. package/.agents/skills/nsr-compliance/assets/nsr/assets/root/TECH_STACK.md +28 -0
  433. package/.agents/skills/nsr-compliance/assets/nsr/scaffold-spec.md +63 -0
  434. package/.agents/skills/nsr-compliance/assets/nsr/scripts/scaffold.sh +103 -0
  435. package/.agents/skills/nsr-compliance/assets/nsr/scripts/validate.sh +52 -0
  436. package/.agents/skills/nsr-compliance/assets/nsrcompliance/SKILL.md +71 -0
  437. package/.agents/skills/nsr-compliance/assets/nsrcompliance/config/core_four.yaml +16 -0
  438. package/.agents/skills/nsr-compliance/assets/nsrcompliance/gates/check_danger.sh +102 -0
  439. package/.agents/skills/nsr-compliance/assets/nsrcompliance/gates/check_wiring.sh +69 -0
  440. package/.agents/skills/nsr-compliance/assets/nsrcompliance/gates/validate_code.sh +29 -0
  441. package/.agents/skills/nsr-compliance/assets/nsrcompliance/gates/verify_docs.py +66 -0
  442. package/.agents/skills/nsr-compliance/assets/nsrcompliance/generate.sh +109 -0
  443. package/.agents/skills/nsr-compliance/assets/nsrcompliance/harness/envelopes/result_envelope.json +14 -0
  444. package/.agents/skills/nsr-compliance/assets/nsrcompliance/harness/envelopes/task_envelope.json +20 -0
  445. package/.agents/skills/nsr-compliance/assets/nsrcompliance/harness/runner.py +303 -0
  446. package/.agents/skills/nsr-compliance/assets/nsrcompliance/telemetry/logger.py +112 -0
  447. package/.agents/skills/nsr-compliance/assets/nsrcompliance/templates/compliance/README.md +18 -0
  448. package/.agents/skills/nsr-compliance/assets/nsrcompliance/templates/compliance/config/README.md +10 -0
  449. package/.agents/skills/nsr-compliance/assets/nsrcompliance/templates/compliance/gates/README.md +12 -0
  450. package/.agents/skills/nsr-compliance/assets/nsrcompliance/templates/compliance/gates/check_env.sh +23 -0
  451. package/.agents/skills/nsr-compliance/assets/nsrcompliance/templates/compliance/gates/check_paths.sh +20 -0
  452. package/.agents/skills/nsr-compliance/assets/nsrcompliance/templates/compliance/gates/check_platform.sh +23 -0
  453. package/.agents/skills/nsr-compliance/assets/nsrcompliance/templates/compliance/harness/README.md +11 -0
  454. package/.agents/skills/nsr-compliance/assets/nsrcompliance/templates/compliance/harness/envelopes/README.md +10 -0
  455. package/.agents/skills/nsr-compliance/assets/nsrcompliance/templates/compliance/installer/README.md +10 -0
  456. package/.agents/skills/nsr-compliance/assets/nsrcompliance/templates/compliance/installer/install.sh +125 -0
  457. package/.agents/skills/nsr-compliance/assets/nsrcompliance/templates/compliance/telemetry/README.md +11 -0
  458. package/.agents/skills/pr-ops/README.md +3 -0
  459. package/.agents/skills/pr-ops/SKILL.md +109 -0
  460. package/.agents/skills/ratatoskr-a2a/README.md +3 -0
  461. package/.agents/skills/ratatoskr-a2a/SKILL.md +148 -0
  462. package/.agents/skills/rules-check-drift/SKILL.md +81 -0
  463. package/.agents/skills/saga-bearings/README.md +3 -0
  464. package/.agents/skills/saga-bearings/SKILL.md +26 -0
  465. package/.agents/skills/saga-bearings/assets/bearings.md +161 -0
  466. package/.agents/skills/saga-bearings/assets/board-template.html +731 -0
  467. package/.agents/skills/saga-bearings/assets/recap.md +56 -0
  468. package/.agents/skills/syn-recovery/README.md +3 -0
  469. package/.agents/skills/syn-recovery/SKILL.md +54 -0
  470. package/.agents/skills/tyr-check/README.md +3 -0
  471. package/.agents/skills/tyr-check/SKILL.md +218 -0
  472. package/.agents/skills/urdh-hold/README.md +3 -0
  473. package/.agents/skills/urdh-hold/SKILL.md +25 -0
  474. package/.agents/skills/urdh-hold/assets/decisions.md +15 -0
  475. package/.agents/skills/urdh-hold/assets/hold.md +54 -0
  476. package/.agents/skills/vor-diagnostics/README.md +3 -0
  477. package/.agents/skills/vor-diagnostics/SKILL.md +68 -0
  478. package/.agents/skills/ymir-host/README.md +3 -0
  479. package/.agents/skills/ymir-host/SKILL.md +34 -0
  480. package/.agents/skills/ymir-host/assets/desktop.md +36 -0
  481. package/.agents/skills/ymir-host/assets/omarchy.md +297 -0
  482. package/.agents/skills/ymir-host/assets/runbooks.md +15 -0
  483. package/.agents/skills/ymir-host/assets/thjazi.md +220 -0
  484. package/.agents/skills/ymir-host/assets/update.md +64 -0
  485. package/.agents/tests/assets/board-render-harness.mjs +117 -0
  486. package/.agents/tests/cmux-test-safety.sh +51 -0
  487. package/.agents/tests/fixtures.sh +291 -0
  488. package/.agents/tests/fm-afk-inject-e2e.test.sh +428 -0
  489. package/.agents/tests/fm-afk-inject-herdr-e2e.test.sh +536 -0
  490. package/.agents/tests/fm-afk-launch.test.sh +961 -0
  491. package/.agents/tests/fm-afk-pi-herdr-return-e2e.test.sh +297 -0
  492. package/.agents/tests/fm-afk-return.test.sh +283 -0
  493. package/.agents/tests/fm-arm-pretool-check.test.sh +482 -0
  494. package/.agents/tests/fm-ask-user-authority.test.sh +42 -0
  495. package/.agents/tests/fm-backend-autodetect-smoke.test.sh +176 -0
  496. package/.agents/tests/fm-backend-cmux-smoke.test.sh +188 -0
  497. package/.agents/tests/fm-backend-cmux.test.sh +1166 -0
  498. package/.agents/tests/fm-backend-herdr-eventwait-smoke.test.sh +136 -0
  499. package/.agents/tests/fm-backend-herdr-eventwait.test.py +106 -0
  500. package/.agents/tests/fm-backend-herdr-focus-flash-e2e.test.sh +409 -0
  501. package/.agents/tests/fm-backend-herdr-launcher-workspace-e2e.test.sh +437 -0
  502. package/.agents/tests/fm-backend-herdr-presentation-e2e.test.sh +1424 -0
  503. package/.agents/tests/fm-backend-herdr-prune-safety-e2e.test.sh +180 -0
  504. package/.agents/tests/fm-backend-herdr-respawn-idem-e2e.test.sh +182 -0
  505. package/.agents/tests/fm-backend-herdr-smoke.test.sh +355 -0
  506. package/.agents/tests/fm-backend-herdr-workspace-per-home-e2e.test.sh +251 -0
  507. package/.agents/tests/fm-backend-herdr.test.sh +4606 -0
  508. package/.agents/tests/fm-backend-orca.test.sh +1357 -0
  509. package/.agents/tests/fm-backend-tmux-smoke.test.sh +173 -0
  510. package/.agents/tests/fm-backend-zellij-smoke.test.sh +205 -0
  511. package/.agents/tests/fm-backend-zellij.test.sh +1360 -0
  512. package/.agents/tests/fm-backend.test.sh +1142 -0
  513. package/.agents/tests/fm-backlog-atomicity.test.sh +2302 -0
  514. package/.agents/tests/fm-backlog-handoff.test.sh +1360 -0
  515. package/.agents/tests/fm-bearings-board-render.test.sh +135 -0
  516. package/.agents/tests/fm-bearings-board.test.sh +414 -0
  517. package/.agents/tests/fm-bearings-snapshot.test.sh +1990 -0
  518. package/.agents/tests/fm-bootstrap-network-parallel.test.sh +316 -0
  519. package/.agents/tests/fm-bootstrap.test.sh +1178 -0
  520. package/.agents/tests/fm-branch-supervision.test.sh +552 -0
  521. package/.agents/tests/fm-brief.test.sh +785 -0
  522. package/.agents/tests/fm-busy-adapter-wiring.test.sh +324 -0
  523. package/.agents/tests/fm-busy-state.test.sh +465 -0
  524. package/.agents/tests/fm-calm-pi-extension.test.sh +3979 -0
  525. package/.agents/tests/fm-captain-hold-lifecycle.test.sh +1187 -0
  526. package/.agents/tests/fm-cd-pretool-check.test.sh +400 -0
  527. package/.agents/tests/fm-check-unregister.test.sh +198 -0
  528. package/.agents/tests/fm-classify-corr-token.test.sh +543 -0
  529. package/.agents/tests/fm-classify-decision-key.test.sh +340 -0
  530. package/.agents/tests/fm-claude-stop-autoarm-live-e2e.test.sh +164 -0
  531. package/.agents/tests/fm-claude-stop-autoarm.test.sh +1190 -0
  532. package/.agents/tests/fm-cmux-claude-composer-live-e2e.test.sh +110 -0
  533. package/.agents/tests/fm-codex-continuity-live-e2e.test.sh +55 -0
  534. package/.agents/tests/fm-composer-ghost.test.sh +716 -0
  535. package/.agents/tests/fm-composer-lib.test.sh +666 -0
  536. package/.agents/tests/fm-composer-matrix-live-e2e.test.sh +220 -0
  537. package/.agents/tests/fm-control-herdr-smoke.test.sh +149 -0
  538. package/.agents/tests/fm-control-relaunch.test.sh +1530 -0
  539. package/.agents/tests/fm-control.test.sh +910 -0
  540. package/.agents/tests/fm-crew-state.test.sh +1610 -0
  541. package/.agents/tests/fm-cursor-harness.test.sh +404 -0
  542. package/.agents/tests/fm-cursor-primary-live-e2e.test.sh +217 -0
  543. package/.agents/tests/fm-cursor-primary.test.sh +708 -0
  544. package/.agents/tests/fm-daemon.test.sh +2740 -0
  545. package/.agents/tests/fm-documentation-audiences.test.sh +141 -0
  546. package/.agents/tests/fm-ensure-agents-md.test.sh +371 -0
  547. package/.agents/tests/fm-extension-binding.test.sh +2187 -0
  548. package/.agents/tests/fm-fleet-snapshot-view.test.sh +816 -0
  549. package/.agents/tests/fm-fleet-sync.test.sh +721 -0
  550. package/.agents/tests/fm-gate-refuse.test.sh +356 -0
  551. package/.agents/tests/fm-gitignore-config.test.sh +89 -0
  552. package/.agents/tests/fm-gotmp.test.sh +230 -0
  553. package/.agents/tests/fm-grok-continuity-live-e2e.test.sh +112 -0
  554. package/.agents/tests/fm-grok-harness.test.sh +117 -0
  555. package/.agents/tests/fm-grok-stop-live-e2e.test.sh +219 -0
  556. package/.agents/tests/fm-guard-stale-banner.test.sh +709 -0
  557. package/.agents/tests/fm-harness-adapter-instructions-live-e2e.test.sh +134 -0
  558. package/.agents/tests/fm-harness-adapter-references.test.sh +30 -0
  559. package/.agents/tests/fm-harness-liveness-drift-live-e2e.test.sh +151 -0
  560. package/.agents/tests/fm-herdr-lab.test.sh +243 -0
  561. package/.agents/tests/fm-herdr-session-cleanup-e2e.test.sh +146 -0
  562. package/.agents/tests/fm-herdr-session-cleanup.test.sh +329 -0
  563. package/.agents/tests/fm-herdr-submit-confirm-live-e2e.test.sh +125 -0
  564. package/.agents/tests/fm-herdr-version-floor-live-e2e.test.sh +144 -0
  565. package/.agents/tests/fm-home-summary-refresh.test.sh +971 -0
  566. package/.agents/tests/fm-inactive-reconcile.test.sh +485 -0
  567. package/.agents/tests/fm-kimi-harness.test.sh +687 -0
  568. package/.agents/tests/fm-lint-workflows.test.sh +532 -0
  569. package/.agents/tests/fm-lint.test.sh +1023 -0
  570. package/.agents/tests/fm-muse-harness.test.sh +932 -0
  571. package/.agents/tests/fm-muse-signals-live-e2e.test.sh +205 -0
  572. package/.agents/tests/fm-no-mistakes-required.test.sh +72 -0
  573. package/.agents/tests/fm-on.test.sh +507 -0
  574. package/.agents/tests/fm-opencode-primary-live-e2e.test.sh +357 -0
  575. package/.agents/tests/fm-operational-input.test.sh +160 -0
  576. package/.agents/tests/fm-peek-remote.test.sh +110 -0
  577. package/.agents/tests/fm-pending-reply.test.sh +1288 -0
  578. package/.agents/tests/fm-pi-branch-extension.test.sh +3182 -0
  579. package/.agents/tests/fm-pi-branch-live-e2e.test.sh +527 -0
  580. package/.agents/tests/fm-pi-primary-live-e2e.test.sh +345 -0
  581. package/.agents/tests/fm-pi-primary-types.test.sh +68 -0
  582. package/.agents/tests/fm-pi-watch-extension.test.sh +2843 -0
  583. package/.agents/tests/fm-pr-check-security.test.sh +2140 -0
  584. package/.agents/tests/fm-pr-merge.test.sh +2135 -0
  585. package/.agents/tests/fm-procevent-quota.test.sh +225 -0
  586. package/.agents/tests/fm-procevent-when.test.sh +406 -0
  587. package/.agents/tests/fm-procevent.test.sh +1745 -0
  588. package/.agents/tests/fm-project-origin.test.sh +110 -0
  589. package/.agents/tests/fm-public-followup.test.sh +2272 -0
  590. package/.agents/tests/fm-quota-array-dispatch-live-e2e.test.sh +526 -0
  591. package/.agents/tests/fm-quota-choose.test.sh +626 -0
  592. package/.agents/tests/fm-remote-backlog-handoff.test.sh +453 -0
  593. package/.agents/tests/fm-remote-doctor.test.sh +625 -0
  594. package/.agents/tests/fm-remote-entrypoint.test.sh +59 -0
  595. package/.agents/tests/fm-remote-job-orphan-reap.test.sh +205 -0
  596. package/.agents/tests/fm-remote-job.test.sh +768 -0
  597. package/.agents/tests/fm-remote-reply.test.sh +520 -0
  598. package/.agents/tests/fm-remote-secondmate-lifecycle-e2e.test.sh +1256 -0
  599. package/.agents/tests/fm-remote-secondmate-parent-binding.test.sh +326 -0
  600. package/.agents/tests/fm-remote-secondmate-trace-context.test.sh +310 -0
  601. package/.agents/tests/fm-remote-transport-lanes.test.sh +431 -0
  602. package/.agents/tests/fm-review-diff.test.sh +176 -0
  603. package/.agents/tests/fm-secondmate-harness.test.sh +2600 -0
  604. package/.agents/tests/fm-secondmate-lifecycle-e2e.test.sh +259 -0
  605. package/.agents/tests/fm-secondmate-liveness.test.sh +559 -0
  606. package/.agents/tests/fm-secondmate-reconcile.test.sh +732 -0
  607. package/.agents/tests/fm-secondmate-safety.test.sh +2972 -0
  608. package/.agents/tests/fm-secondmate-sync.test.sh +913 -0
  609. package/.agents/tests/fm-send-inbox-doorbell-live-e2e.test.sh +206 -0
  610. package/.agents/tests/fm-send-inbox.test.sh +352 -0
  611. package/.agents/tests/fm-send-popup-settle.test.sh +167 -0
  612. package/.agents/tests/fm-send-remote-delivery.test.sh +796 -0
  613. package/.agents/tests/fm-send-resolve-key.test.sh +551 -0
  614. package/.agents/tests/fm-send-secondmate-marker-herdr-e2e.test.sh +187 -0
  615. package/.agents/tests/fm-send-secondmate-marker.test.sh +278 -0
  616. package/.agents/tests/fm-send-settle.test.sh +147 -0
  617. package/.agents/tests/fm-send-strict.test.sh +241 -0
  618. package/.agents/tests/fm-session-lock-ancestry.test.sh +365 -0
  619. package/.agents/tests/fm-session-start.test.sh +2515 -0
  620. package/.agents/tests/fm-sessionstart-hook-live-e2e.test.sh +631 -0
  621. package/.agents/tests/fm-sessionstart-instruction-refresh-live-e2e.test.sh +230 -0
  622. package/.agents/tests/fm-sessionstart-nudge.test.sh +1039 -0
  623. package/.agents/tests/fm-shared-captain-inheritance.test.sh +404 -0
  624. package/.agents/tests/fm-spawn-batch.test.sh +148 -0
  625. package/.agents/tests/fm-spawn-dispatch-profile.test.sh +827 -0
  626. package/.agents/tests/fm-spawn-pool-base-freshen.test.sh +440 -0
  627. package/.agents/tests/fm-spawn-worktree-settle.test.sh +147 -0
  628. package/.agents/tests/fm-startup-memory-budget.test.sh +333 -0
  629. package/.agents/tests/fm-startup-network.test.sh +712 -0
  630. package/.agents/tests/fm-stow-cascade.test.sh +370 -0
  631. package/.agents/tests/fm-subagent-pretool-check.test.sh +291 -0
  632. package/.agents/tests/fm-supervision-events.test.sh +156 -0
  633. package/.agents/tests/fm-supervision-instructions.test.sh +189 -0
  634. package/.agents/tests/fm-tangle-guard.test.sh +277 -0
  635. package/.agents/tests/fm-task-delivery.test.sh +420 -0
  636. package/.agents/tests/fm-task-inbox.test.sh +513 -0
  637. package/.agents/tests/fm-teardown-endpoint-safety.test.sh +374 -0
  638. package/.agents/tests/fm-teardown.test.sh +2665 -0
  639. package/.agents/tests/fm-test-fixture-cleanup.test.sh +172 -0
  640. package/.agents/tests/fm-test-fixtures.test.sh +132 -0
  641. package/.agents/tests/fm-test-isolation-proof.test.sh +278 -0
  642. package/.agents/tests/fm-test-run.test.sh +1196 -0
  643. package/.agents/tests/fm-tmux-agent-liveness.test.sh +354 -0
  644. package/.agents/tests/fm-tmux-submit-busy.test.sh +340 -0
  645. package/.agents/tests/fm-tool-update-check.test.sh +1044 -0
  646. package/.agents/tests/fm-trace-context-lib.test.sh +253 -0
  647. package/.agents/tests/fm-trace-context-spawn.test.sh +581 -0
  648. package/.agents/tests/fm-transition-lib.test.sh +54 -0
  649. package/.agents/tests/fm-turnend-guard.test.sh +1822 -0
  650. package/.agents/tests/fm-update.test.sh +304 -0
  651. package/.agents/tests/fm-vendor-auth-probe.test.sh +395 -0
  652. package/.agents/tests/fm-voice-relay.test.sh +4741 -0
  653. package/.agents/tests/fm-wake-daemon-lifecycle-e2e.test.sh +171 -0
  654. package/.agents/tests/fm-wake-drain-open-decisions-cursor.test.sh +356 -0
  655. package/.agents/tests/fm-wake-drain-open-decisions.test.sh +226 -0
  656. package/.agents/tests/fm-wake-drain-unread-status.test.sh +379 -0
  657. package/.agents/tests/fm-wake-queue.test.sh +1224 -0
  658. package/.agents/tests/fm-watch-arm.test.sh +821 -0
  659. package/.agents/tests/fm-watch-checkpoint.test.sh +87 -0
  660. package/.agents/tests/fm-watch-recovery-loop.test.sh +223 -0
  661. package/.agents/tests/fm-watch-triage.test.sh +3889 -0
  662. package/.agents/tests/fm-watcher-lock.test.sh +1133 -0
  663. package/.agents/tests/fm-x-mode.test.sh +3042 -0
  664. package/.agents/tests/gleipnir-guard-pretool-check.test.sh +51 -0
  665. package/.agents/tests/gleipnir-machine-lock.test.sh +130 -0
  666. package/.agents/tests/herdr-test-safety.sh +42 -0
  667. package/.agents/tests/lib.sh +315 -0
  668. package/.agents/tests/remote-herdr-fixture.sh +125 -0
  669. package/.agents/tests/secondmate-helpers.sh +193 -0
  670. package/.agents/tests/smoke.test.sh +93 -0
  671. package/.agents/tests/wake-helpers.sh +338 -0
  672. package/.agents/tests/zellij-test-safety.sh +60 -0
  673. package/.agents/tools/README.md +10 -0
  674. package/.agents/tools/bin/tasks-cli +267 -0
  675. package/.agents/tools/tasks-cli.ts +216 -0
  676. package/AGENTS.md +454 -0
  677. package/CHANGELOG.md +881 -0
  678. package/LICENSE +1 -1
  679. package/NOTICE +63 -0
  680. package/README.md +113 -59
  681. package/RULES/01-domains.md +51 -0
  682. package/RULES/02-agents.md +78 -0
  683. package/RULES/03-houses.md +61 -0
  684. package/RULES/04-hoard.md +65 -0
  685. package/RULES/05-platforms.md +67 -0
  686. package/RULES/06-append-only.md +58 -0
  687. package/RULES/07-config.md +45 -0
  688. package/RULES/08-delivery-gate.md +75 -0
  689. package/RULES/README.md +20 -0
  690. package/bin/README.md +107 -0
  691. package/bin/a2a-mcp.sh +98 -0
  692. package/bin/a2a-serve.py +103 -0
  693. package/bin/a2a-serve.sh +8 -0
  694. package/bin/a2a-talk.sh +50 -0
  695. package/bin/a2abridge-ensure.sh +108 -0
  696. package/bin/agent-run.sh +43 -0
  697. package/bin/agents-config.sh +233 -0
  698. package/bin/apodex-smoke-test.sh +92 -0
  699. package/bin/bifrost-bridge.sh +112 -0
  700. package/bin/bifrost-ingress.sh +77 -0
  701. package/bin/bootstrap-macos.sh +107 -0
  702. package/bin/bootstrap-windows.ps1 +112 -0
  703. package/bin/branch-guard.sh +64 -0
  704. package/bin/brokk +73 -0
  705. package/bin/brokk-classify-lib.sh +1770 -0
  706. package/bin/brokk-lease-lib.sh +222 -0
  707. package/bin/brokk-lease.sh +193 -0
  708. package/bin/brokk-lint.sh +65 -0
  709. package/bin/brokk-send.sh +38 -0
  710. package/bin/brokk-update.sh +9 -0
  711. package/bin/brokk-wake-grant.sh +127 -0
  712. package/bin/brokk-wake-lib.sh +1849 -0
  713. package/bin/changelog-guard.sh +165 -0
  714. package/bin/crash-sense.sh +160 -0
  715. package/bin/desktop-place.sh +224 -0
  716. package/bin/docs-guard.sh +39 -0
  717. package/bin/editor-place.sh +207 -0
  718. package/bin/eindri-acclaim.sh +34 -0
  719. package/bin/eindri-control.sh +28 -0
  720. package/bin/eindri-role.sh +83 -0
  721. package/bin/eindri-seen.sh +41 -0
  722. package/bin/eindri-send.sh +15 -0
  723. package/bin/eindri-start.sh +85 -0
  724. package/bin/eindri-watch.sh +57 -0
  725. package/bin/einherjar-spawn.sh +576 -0
  726. package/bin/eir-doctor.sh +112 -0
  727. package/bin/erindi-brief.sh +271 -0
  728. package/bin/fleet-apply.sh +68 -0
  729. package/bin/github-deploy.sh +81 -0
  730. package/bin/gjallarhorn-expose.sh +102 -0
  731. package/bin/gjallarhorn-notify.sh +58 -0
  732. package/bin/gjallarhorn-purge.sh +54 -0
  733. package/bin/gjallarhorn-tunnel.sh +72 -0
  734. package/bin/gleipnir-lock-lib.sh +284 -0
  735. package/bin/groa-update.sh +122 -0
  736. package/bin/hall-snapshot.sh +92 -0
  737. package/bin/hamr-harness.sh +246 -0
  738. package/bin/heimdall-ssh-keys.sh +374 -0
  739. package/bin/herdr-agents.py +44 -0
  740. package/bin/herdr-ensure.sh +104 -0
  741. package/bin/herdr-run.sh +431 -0
  742. package/bin/hermes-ensure.sh +88 -0
  743. package/bin/hoard-lib.sh +25 -0
  744. package/bin/hodd.sh +78 -0
  745. package/bin/host-sense.sh +199 -0
  746. package/bin/huginn-research-worker.sh +108 -0
  747. package/bin/local-model-lock.sh +51 -0
  748. package/bin/mimir-bridge.py +226 -0
  749. package/bin/mimir-bridge.sh +87 -0
  750. package/bin/mimir-ingest.sh +131 -0
  751. package/bin/mimir.sh +113 -0
  752. package/bin/mjollnir-webhook.sh +59 -0
  753. package/bin/mjollnir.sh +85 -0
  754. package/bin/model-hardware.sh +82 -0
  755. package/bin/model-resolve.sh +147 -0
  756. package/bin/models-detect.sh +178 -0
  757. package/bin/nornir-cron-start.sh +162 -0
  758. package/bin/nornir-job-daily-briefing.sh +191 -0
  759. package/bin/nornir-job-git-sync.sh +152 -0
  760. package/bin/nornir-job-memory-housekeeping.sh +114 -0
  761. package/bin/nornir-job-observer.sh +166 -0
  762. package/bin/nornir-job-skillopt-sleep.sh +76 -0
  763. package/bin/omarchy-hook-install.sh +60 -0
  764. package/bin/omarchy-install.sh +148 -0
  765. package/bin/omarchy-plugins.sh +174 -0
  766. package/bin/omarchy-sense.sh +149 -0
  767. package/bin/perm-guard.sh +30 -0
  768. package/bin/pi-agent.sh +68 -0
  769. package/bin/pi-ensure.sh +77 -0
  770. package/bin/pi-local.sh +37 -0
  771. package/bin/pi-seat.sh +63 -0
  772. package/bin/prereq-ensure.sh +166 -0
  773. package/bin/private-guard.sh +38 -0
  774. package/bin/project-git.sh +55 -0
  775. package/bin/public-guard.sh +70 -0
  776. package/bin/ratatoskr.sh +49 -0
  777. package/bin/realm-lib.sh +32 -0
  778. package/bin/repo-scrub.sh +62 -0
  779. package/bin/rodd-operational-input.sh +250 -0
  780. package/bin/runes-append.sh +198 -0
  781. package/bin/saga-session-start.sh +172 -0
  782. package/bin/saga-sessionstart-run.sh +79 -0
  783. package/bin/saga-wake-drain.sh +41 -0
  784. package/bin/secret-guard.sh +63 -0
  785. package/bin/sessrumnir-ensure.sh +100 -0
  786. package/bin/sessrumnir-sync.sh +237 -0
  787. package/bin/sessrumnir.sh +91 -0
  788. package/bin/skillopt-setup.sh +76 -0
  789. package/bin/skuld-branch-outcome.sh +203 -0
  790. package/bin/skuld-branch-prompt.sh +101 -0
  791. package/bin/smidja-bootstrap.sh +90 -0
  792. package/bin/smidja-observe.sh +86 -0
  793. package/bin/syn-arm-pretool-check.sh +57 -0
  794. package/bin/syn-asset-pretool-check.sh +76 -0
  795. package/bin/syn-cd-pretool-check.sh +28 -0
  796. package/bin/syn-guard-pretool-check.sh +83 -0
  797. package/bin/syn-turnend-guard.sh +36 -0
  798. package/bin/syn-watch-arm.sh +99 -0
  799. package/bin/tailscale-sync.sh +190 -0
  800. package/bin/telegram-bot.sh +69 -0
  801. package/bin/toolchain.sh +74 -0
  802. package/bin/utgard.sh +111 -0
  803. package/bin/valhalla.sh +115 -0
  804. package/bin/valknut-load.sh +260 -0
  805. package/bin/vor-crew-state.sh +124 -0
  806. package/bin/wedge-notify.sh +47 -0
  807. package/bin/workspace-provision.sh +80 -0
  808. package/bin/workspace-rag.sh +150 -0
  809. package/bin/wyrd-db.sh +47 -0
  810. package/bin/yggdrasil.sh +152 -0
  811. package/bin/ymir-edit.sh +145 -0
  812. package/bin/ymir-install.sh +641 -0
  813. package/bin/ymir-invite.sh +93 -0
  814. package/bin/ymir-isolation.sh +88 -0
  815. package/bin/ymir-migrate.sh +61 -0
  816. package/bin/ymir-platform.sh +348 -0
  817. package/bin/ymir-say.sh +97 -0
  818. package/bin/ymir-setup-auth.sh +108 -0
  819. package/bin/ymir-validate.sh +173 -0
  820. package/bin/ymir.js +74 -16
  821. package/docs/Architecture.md +339 -0
  822. package/docs/design.md +351 -0
  823. package/docs/installations/README.md +63 -0
  824. package/docs/installations/ymir-home.md +93 -0
  825. package/docs/integration.md +105 -0
  826. package/docs/lore.md +1056 -0
  827. package/docs/research/agent-harnesses.md +290 -0
  828. package/docs/runbooks/README.md +19 -0
  829. package/docs/runbooks/a2a.md +91 -0
  830. package/docs/runbooks/agent-permissions.md +99 -0
  831. package/docs/runbooks/agents.md +57 -0
  832. package/docs/runbooks/models.md +69 -0
  833. package/docs/runbooks/realm-onboarding.md +60 -0
  834. package/docs/runbooks/secrets-and-hoard.md +60 -0
  835. package/docs/runbooks/updates-and-migrations.md +110 -0
  836. package/docs/runbooks/worktrees-isolation.md +72 -0
  837. package/docs/session-start.md +105 -0
  838. package/docs/skillopt-integration.md +321 -0
  839. package/docs/sync-system.md +66 -0
  840. package/docs/workspaces.md +104 -0
  841. package/docs/ymir-rut.md +260 -0
  842. package/package.json +44 -14
  843. package/packaging/build.sh +74 -0
  844. package/packaging/macos/Ymir Installer.command +43 -0
  845. package/packaging/windows/ymir-setup.nsi +74 -0
  846. package/scripts/README.md +22 -0
  847. package/scripts/electron.sh +210 -0
  848. package/scripts/start.sh +167 -0
  849. package/scripts/stop.sh +113 -0
  850. package/install.sh +0 -35
@@ -0,0 +1,1732 @@
1
+ #!/usr/bin/env bash
2
+ # fm-supervise-daemon.sh — presence-gated sub-supervisor (closes #27's P2).
3
+ #
4
+ # Wraps bin/fm-watch.sh: runs it as a child, presents and classifies every
5
+ # durable wake after an actionable close, acknowledges only after routing, and
6
+ # either SELF-HANDLES the routine majority in bash (no firstmate turn) or
7
+ # ESCALATES a batched, distilled digest to the supervisor pane on
8
+ # captain-relevant events plus bounded declared-wait rechecks. This is the
9
+ # token-efficient replacement for the prior always-inject daemon: routine
10
+ # signal/stale/heartbeat wakes cost zero firstmate context; only done/
11
+ # needs-decision/blocked/failed/persistent-wedge/check-output events and a
12
+ # declared-wait recheck reach the LLM, and even then as one pre-read digest per
13
+ # batch window.
14
+ #
15
+ # PRESENCE-GATING (the /afk contract). The daemon is the away-mode engine: it
16
+ # injects ONLY when the durable away-mode flag state/.afk is present. Invoking
17
+ # the /afk skill sets that flag and starts this daemon; any real (unmarked)
18
+ # user message clears it and firstmate resumes full responsiveness.
19
+ # When afk is off, normal fm-watch.sh always-on triage is the active mechanism.
20
+ # Any buffered daemon escalations that remain while afk is off survive in
21
+ # state/.subsuper-escalations and are flushed on the next "while you were out"
22
+ # catch-up or when afk is re-entered.
23
+ #
24
+ # IN-BAND OPERATIONAL INPUT. bin/fm-operational-input.sh constructs every
25
+ # current daemon injection as the typed away-supervisor kind after the stable
26
+ # FM_OPERATIONAL_PREFIX. A human cannot type its leading U+2063 from a normal
27
+ # keyboard at the start of a message, and Herdr transports it as text.
28
+ # Firstmate's contract: a message that starts with the current prefix, or a
29
+ # legacy bare-marker daemon escalation, is internal (stay afk); an unmarked
30
+ # message means the captain is back (exit afk, flush catch-up, resume per-wake
31
+ # responsiveness). The prefix and busy-guard solve the same problem - the
32
+ # daemon and the human share one input channel - so they live together under
33
+ # /afk.
34
+ #
35
+ # Reliability model (see the /afk skill):
36
+ # - Nothing is lost in away mode: while state/.afk exists, the watcher reverts
37
+ # to daemon-owned one-shot behavior and enqueues every wake to
38
+ # state/.wake-queue BEFORE advancing its suppression markers, so a
39
+ # crash/restart/missed injection is recovered on the next fm-wake-drain.sh.
40
+ # After a watcher cycle, the daemon handles every durable row through that
41
+ # drain and acknowledges it only after routing completes.
42
+ # - Fail-safe-to-escalate: any wake the classifier cannot confidently mark
43
+ # routine is escalated.
44
+ # - Bounded wedge latency: a stale pane without a declared wait is escalated
45
+ # only after it has been idle for STALE_ESCALATE_SECS
46
+ # (configurable), rechecked once. A wedged crewmate is therefore detected
47
+ # within STALE_ESCALATE_SECS + a tick, never lost. A declared wait - either a
48
+ # paused: external wait or a verified captain-held transfer, per
49
+ # fm-classify-lib.sh's combined predicate - instead gets its own longer
50
+ # PAUSE_RESURFACE_SECS recheck, never a wedge escalation, whether its pane
51
+ # reads idle or busy; only a status append that stops declaring the wait
52
+ # ends that routing.
53
+ # Crewmates are autonomous, so a delayed stale response does not stall a
54
+ # healthy crewmate's own progress.
55
+ # Buffered escalation delivery also has a max-defer alarm: if a digest stays
56
+ # undelivered past FM_MAX_DEFER_SECS, the daemon retries a normal flush and
57
+ # writes state/.subsuper-inject-wedged and attempts a configurable active
58
+ # alert if submit still cannot be confirmed.
59
+ # - Cheap heartbeat catch-all: every HEARTBEAT_SCAN_SECS the daemon greps all
60
+ # state/*.status for a captain-relevant line the per-wake classifier might
61
+ # have missed (e.g. a status verb outside CAPTAIN_RE) and escalates it.
62
+ #
63
+ # The robustness shell from the prior always-inject version is preserved:
64
+ # single-instance lock (portable helper, no flock dependency), crash-loop
65
+ # backoff, pane-gone guard, and a signal-trapped shutdown that flushes buffered
66
+ # escalations before exit.
67
+ #
68
+ # Usage: fm-supervise-daemon.sh
69
+ # Long-lived background loop. Normally started by the /afk skill, which
70
+ # sets state/.afk first. Env knobs:
71
+ # FM_SUPERVISOR_TARGET supervisor pane target (override; otherwise
72
+ # auto-discovered per backend - $TMUX_PANE
73
+ # under tmux, "<session>:<pane-id>" from
74
+ # $HERDR_PANE_ID under herdr - then
75
+ # firstmate:0 fallback). Accepts either a
76
+ # tmux target or a herdr "<session>:<pane-id>"
77
+ # target; which one it's read as is decided by
78
+ # FM_SUPERVISOR_BACKEND (below), independently.
79
+ # FM_SUPERVISOR_BACKEND supervisor pane BACKEND (tmux|herdr;
80
+ # override; otherwise auto-discovered the same
81
+ # way bin/fm-backend.sh's fm_backend_detect
82
+ # resolves the runtime firstmate itself is
83
+ # executing inside - $TMUX_PANE selects tmux,
84
+ # $HERDR_ENV=1 selects herdr - falling back to
85
+ # tmux). zellij, orca, and cmux are not yet
86
+ # supported as supervisor backends; the daemon
87
+ # refuses loudly at startup rather than trying
88
+ # tmux primitives against a non-tmux pane.
89
+ # FM_INJECT_SKIP |-prefixes force-self-handle bypassing
90
+ # classification (default "heartbeat"); empty
91
+ # disables. Use sparingly: it overrides the
92
+ # captain-relevant escalation for matching
93
+ # kinds.
94
+ # FM_STALE_ESCALATE_SECS idle seconds before a stale pane escalates
95
+ # as a possible wedge (default 240)
96
+ # FM_PAUSE_RESURFACE_SECS seconds a declared wait (external or
97
+ # captain-held) stays declared, idle or busy,
98
+ # before it re-surfaces as a recheck
99
+ # (default 3600)
100
+ # FM_ESCALATE_BATCH_SECS buffer window for batched escalation
101
+ # digests; 0 = flush immediately (default 90)
102
+ # FM_HEARTBEAT_SCAN_SECS cadence for the catch-all status scan
103
+ # (default 300)
104
+ # FM_HOUSEKEEPING_TICK seconds between housekeeping passes while
105
+ # the watcher is mid-cycle (default 15)
106
+ # FM_BUSY_REGEX optional rendered busy-signature override
107
+ # for delivery guards and Grok's fallback
108
+ # FM_COMPOSER_IDLE_RE optional shared classifier override; see
109
+ # docs/configuration.md for its safety gates
110
+ # FM_MAX_DEFER_SECS max seconds a buffered escalation may sit
111
+ # undelivered before one normal flush attempt;
112
+ # if that cannot confirm a submit, a wedge
113
+ # alarm fires (default 300; 0 disables)
114
+ # FM_WEDGE_ALARM_CHANNEL override config/wedge-alarm with a single
115
+ # active-alert directive for that wedge alarm
116
+ # (off|auto|osascript|herdr|command:<cmd>). An
117
+ # absent file/var means auto: on macOS that is
118
+ # an OS-level notification, so the alarm is
119
+ # never silent. See wedge_alarm_notify below
120
+ # and docs/configuration.md.
121
+ # FM_WEDGE_ALARM_EXEC notifier seam: when set, every notifier
122
+ # channel routes through this command as
123
+ # `<cmd> <channel> <summary>` instead of
124
+ # invoking its real notifier; "discard" fires
125
+ # nothing. Unset in production. When SOURCED the
126
+ # daemon defaults this to "discard" so no test
127
+ # can post a real notification (wedge_alarm_emit
128
+ # and the library-mode guard at the foot).
129
+ # FM_WEDGE_ALARM_TIMEOUT_SECS seconds allowed for each notifier before
130
+ # its watchdog terminates it and continues to the
131
+ # next channel (default 10; invalid/zero uses the
132
+ # default).
133
+ # FM_INJECT_CONFIRM_RETRIES Enter-retry attempts on a swallowed Enter
134
+ # (default 3); the digest is typed once, only
135
+ # Enter is retried. Composer-empty detection is
136
+ # structural and style-aware (bin/fm-tmux-lib.sh):
137
+ # it drops dim/faint ghost text and strips the
138
+ # harness's box borders before deciding, so a
139
+ # ghost-only or bordered-but-empty composer is
140
+ # not misread as pending input.
141
+ # FM_INJECT_CONFIRM_SLEEP seconds between daemon submit checks
142
+ # (default 0.5)
143
+ # FM_LOG_MAX_BYTES / FM_LOG_KEEP_LINES / FM_CRASH_* log + crash guards
144
+ # FM_STATE_OVERRIDE alternate state dir (testing)
145
+ # Logs each wake to state/.supervise-daemon.log (size-capped). Single
146
+ # instance via portable lock on state/.supervise-daemon.lock. Trapped
147
+ # SIGTERM/SIGINT shut down within ~1s, flush escalations, release the
148
+ # lock. A crashing fm-watch.sh is logged and restarted, never killing
149
+ # the daemon; a tight crash-restart spin is detected and backed off.
150
+ set -u
151
+
152
+ FM_DAEMON_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
153
+ FM_ROOT="${FM_ROOT_OVERRIDE:-$(cd "$FM_DAEMON_DIR/.." && pwd)}"
154
+ FM_HOME="${FM_HOME:-${FM_ROOT_OVERRIDE:-$FM_ROOT}}"
155
+
156
+ # Shared tmux pane primitives for supervisor injection (busy/composer detection
157
+ # + verify-retry submit). Sourced at top level so BOTH the executed daemon and
158
+ # the unit tests (which source this file for its pure functions) get the
159
+ # corrected composer detection. Stale task rechecks use fm-backend.sh below.
160
+ # shellcheck source=bin/fm-tmux-lib.sh
161
+ . "$FM_DAEMON_DIR/fm-tmux-lib.sh"
162
+
163
+ # shellcheck source=bin/fm-backend.sh
164
+ . "$FM_DAEMON_DIR/fm-backend.sh"
165
+
166
+ # Canonical construction and parsing for every Firstmate operational input.
167
+ # shellcheck source=bin/fm-operational-input.sh
168
+ . "$FM_DAEMON_DIR/fm-operational-input.sh"
169
+
170
+ # Shared wake classifier (last_status_line, status_is_captain_relevant,
171
+ # window_to_task, and the status-span reader). The SAME library backs the
172
+ # always-on watcher's triage, so the captain-relevant verb set and the
173
+ # classification predicates have exactly one definition.
174
+ # shellcheck source=bin/fm-classify-lib.sh
175
+ . "$FM_DAEMON_DIR/fm-classify-lib.sh"
176
+
177
+ # Supervisor-pane discovery (FM_SUPERVISOR_TARGET_DEFAULT,
178
+ # FM_SUPERVISOR_BACKEND_DEFAULT, discover_supervisor_target,
179
+ # discover_supervisor_backend). Shared with the script-owned away launcher
180
+ # (bin/fm-afk-launch.sh) so the captain-pane resolution has exactly one owner.
181
+ # shellcheck source=bin/fm-supervisor-target-lib.sh
182
+ . "$FM_DAEMON_DIR/fm-supervisor-target-lib.sh"
183
+
184
+ # The single owner of semantic busy state for recorded tasks
185
+ # (fm_busy_classify).
186
+ # shellcheck source=bin/fm-busy-lib.sh
187
+ . "$FM_DAEMON_DIR/fm-busy-lib.sh"
188
+
189
+ # --- tunables ---------------------------------------------------------------
190
+ # Supervisor backends this daemon knows how to inject into today. zellij, orca,
191
+ # and cmux are real backends elsewhere in firstmate (bin/fm-backend.sh) but this
192
+ # daemon has no verified composer/busy primitives wired up for them yet - see
193
+ # docs/herdr-backend.md and AGENTS.md section 4's
194
+ # harness-verification discipline. Selecting one refuses loudly at startup
195
+ # instead of silently running tmux primitives against a pane that is not a tmux
196
+ # pane.
197
+ FM_SUPERVISOR_SUPPORTED_BACKENDS="tmux herdr"
198
+ INJECT_SKIP_DEFAULT="heartbeat"
199
+ STALE_ESCALATE_SECS_DEFAULT=240
200
+ ESCALATE_BATCH_SECS_DEFAULT=90
201
+ HEARTBEAT_SCAN_SECS_DEFAULT=300
202
+ HOUSEKEEPING_TICK_DEFAULT=15
203
+ # Max time a buffered escalation may sit undelivered before the daemon retries
204
+ # the normal flush path and, if that cannot confirm a submit, raises a loud wedge
205
+ # alarm. The escape hatch makes a guard false-positive visible instead of silent.
206
+ MAX_DEFER_SECS_DEFAULT=300
207
+ WEDGE_ALARM_TIMEOUT_SECS_DEFAULT=10
208
+ WEDGE_ALARM_LAST_EPOCH=0
209
+ WEDGE_ALARM_NOTIFIER_PID=
210
+ # The captain-relevant verb set and the status classifiers (last_status_line,
211
+ # status_is_captain_relevant, window_to_task, and the status-span reader) now
212
+ # live in bin/fm-classify-lib.sh, shared with the always-on watcher.
213
+ # Composer-empty detection, submit acknowledgement, and the harness-scoped
214
+ # supervisor-pane busy guard live in bin/fm-tmux-lib.sh.
215
+ # FM_BUSY_REGEX also overrides Grok's isolated task-state fallback.
216
+ INJECT_FAIL_SLEEP_DEFAULT=30
217
+ INJECT_CONFIRM_RETRIES_DEFAULT=3
218
+ INJECT_CONFIRM_SLEEP_DEFAULT=0.5
219
+ CRASH_THRESHOLD_DEFAULT=10
220
+ CRASH_WINDOW_DEFAULT=60
221
+ CRASH_BACKOFF_DEFAULT=60
222
+ CRASH_NORMAL_SLEEP_DEFAULT=5
223
+ LOG_MAX_BYTES_DEFAULT=1048576
224
+ LOG_KEEP_LINES_DEFAULT=2000
225
+
226
+ # --- presence-gating --------------------------------------------------------
227
+ # bin/fm-operational-input.sh owns the U+2063 FIRSTMATE_OP bytes and typed
228
+ # away-supervisor construction. The away-exit predicate intentionally retains
229
+ # its landed leading-U+2063 compatibility behavior.
230
+ AFK_FLAG_NAME=".afk"
231
+
232
+ # Resolve the effective state dir. FM_STATE_OVERRIDE wins (testing); otherwise
233
+ # $FM_HOME/state. Kept as a function so the pure
234
+ # classifiers can take an explicit state arg without depending on globals.
235
+ _state_root() { printf '%s' "${FM_STATE_OVERRIDE:-$FM_HOME/state}"; }
236
+
237
+ # --- portable stat (same trap as fm-watch.sh: no `stat -f || stat -c`) -------
238
+ if [ "$(uname)" = Darwin ]; then
239
+ _stat_file_mtime() { stat -f %m "$1" 2>/dev/null; }
240
+ else
241
+ _stat_file_mtime() { stat -c %Y "$1" 2>/dev/null; }
242
+ fi
243
+ _now() { date +%s; }
244
+ _file_age() { # seconds since mtime; very large if missing
245
+ local f=$1 m
246
+ m=$(_stat_file_mtime "$f") || { echo 999999; return; }
247
+ echo $(( $(_now) - m ))
248
+ }
249
+
250
+ _hash_text() {
251
+ if command -v md5 >/dev/null 2>&1; then printf '%s' "$1" | md5 -q
252
+ else printf '%s' "$1" | md5sum | cut -d ' ' -f1; fi
253
+ }
254
+
255
+ # --- presence-gating helpers (PURE-ish: side-effect-free reads of state) -----
256
+ # afk_active: 0 if the durable away-mode flag exists, 1 otherwise.
257
+ afk_active() { # <state>
258
+ [ -e "$1/$AFK_FLAG_NAME" ]
259
+ }
260
+
261
+ # afk_enter / afk_exit: write/clear the away-mode flag. Called by the /afk
262
+ # skill (enter) and by firstmate on user return (exit). Durable: a plain file,
263
+ # so recovery (§5) re-enters afk if it is present after a restart.
264
+ afk_enter() { # <state>
265
+ mkdir -p "$1"
266
+ date '+%s' > "$1/$AFK_FLAG_NAME"
267
+ }
268
+
269
+ afk_exit() { # <state>
270
+ rm -f "$1/$AFK_FLAG_NAME"
271
+ }
272
+
273
+ # should_exit_afk: encodes firstmate's afk-exit contract as a testable function.
274
+ # afk inactive -> 1 (nothing to exit)
275
+ # message has marker -> 1 (internal escalation; stay afk)
276
+ # message is /afk command -> 1 (re-entering/extending afk; stay afk)
277
+ # anything else -> 0 (captain is back; exit afk)
278
+ # Bias toward exit: only the marker and an explicit /afk invocation keep afk
279
+ # alive. A false exit is self-correcting (the captain re-runs /afk).
280
+ should_exit_afk() { # <state> <message-text>
281
+ local state=$1 msg=$2
282
+ afk_active "$state" || return 1
283
+ message_is_injection "$msg" && return 1
284
+ case "$msg" in
285
+ /afk*) return 1 ;;
286
+ esac
287
+ return 0
288
+ }
289
+
290
+ # message_is_injection: 0 if the given message text starts with the sentinel
291
+ # marker (a daemon escalation), 1 otherwise (a real user message). Firstmate's
292
+ # afk-exit contract uses this: marker present -> stay afk; absent -> captain is
293
+ # back. Bias ambiguous cases toward exit (a false exit is self-correcting).
294
+ message_is_injection() { # <message-text>
295
+ local msg=$1
296
+ [ -n "$msg" ] || return 1
297
+ case "$msg" in
298
+ "$FM_INJECT_MARK"*) return 0 ;;
299
+ esac
300
+ return 1
301
+ }
302
+
303
+ # strip_injection_marker: remove a current typed away envelope, the landed
304
+ # untyped FIRSTMATE_OP prefix, or the legacy bare sentinel. Current grammar is
305
+ # delegated to its owner rather than reimplemented here.
306
+ strip_injection_marker() { # <message-text>
307
+ local msg=$1 body
308
+ if fm_operational_input_body "$msg" body; then
309
+ printf '%s' "$body"
310
+ return
311
+ fi
312
+ case "$msg" in
313
+ "$FM_OPERATIONAL_PREFIX"*) msg=${msg#"$FM_OPERATIONAL_PREFIX"} ;;
314
+ "$FM_INJECT_MARK"*) msg=${msg#"$FM_INJECT_MARK"} ;;
315
+ esac
316
+ printf '%s' "$msg"
317
+ }
318
+
319
+ # Collapse all newlines to a literal " - " separator so the injected digest is
320
+ # a single line. Submission via send-keys + Enter is then unambiguous regardless
321
+ # of how the target TUI handles embedded newlines in its composer.
322
+ _collapse_newlines() { # <text>
323
+ local s=$1
324
+ s=${s//$'\n'/ - }
325
+ printf '%s' "$s"
326
+ }
327
+
328
+ # discover_supervisor_target / discover_supervisor_backend are owned by
329
+ # bin/fm-supervisor-target-lib.sh (sourced above). fm_super_main below calls
330
+ # them exactly as before; the away launcher reuses the identical resolution to
331
+ # pass the captain pane in as FM_SUPERVISOR_TARGET.
332
+
333
+ # --- classification helpers (PURE: no side effects, testable) ---------------
334
+ # last_status_line, status_is_captain_relevant, window_to_task, and the
335
+ # status-span reader come from bin/fm-classify-lib.sh (sourced above),
336
+ # the single classifier shared with bin/fm-watch.sh. The decision-string wrappers
337
+ # and dedup state below layer the daemon's escalation-digest concerns on top.
338
+ #
339
+ # Decision protocol: every classifier prints exactly one line on stdout of the
340
+ # form "<action>|<distilled>" where action is "self" or "escalate". The distilled
341
+ # field for "self" is informational (logged); for "escalate" it is the pre-read
342
+ # summary firstmate would otherwise have to re-read.
343
+
344
+ classify_signal() { # <reason-after-colon> <state>
345
+ local reason=$1 state=$2 f last event record rest endpoint ident rc distilled="" rel="" seen_rel="" task sig marker
346
+ for f in $reason; do
347
+ case "$f" in *.status) ;; *) continue ;; esac
348
+ [ -e "$f" ] || [ -L "$f" ] || continue
349
+ task=$(basename "$f"); task="${task%.status}"
350
+ record=$(status_span_first_actionable_record "$f" \
351
+ "$(status_seen_offset "$state" "$task")")
352
+ rc=$?
353
+ [ "$rc" -eq 1 ] && [ -z "$record" ] && continue
354
+ if [ "$rc" -eq 2 ]; then
355
+ sig=$(status_observed_signature "$f")
356
+ marker=$(_seen_status_path "$state" "$task")
357
+ status_presentation_marker_reported_matches "$marker" "$sig" && continue
358
+ distilled="${distilled}$(basename "$f"): unreadable status span | "
359
+ [ -n "${FM_STATUS_SPAN_ENDPOINT_FILE:-}" ] \
360
+ && printf 'ERROR\t%s\t%s\n' "$task" "$sig" >> "$FM_STATUS_SPAN_ENDPOINT_FILE"
361
+ rel=1
362
+ continue
363
+ fi
364
+ endpoint=${record%%$'\t'*}
365
+ rest=${record#*$'\t'}; ident=${rest%%$'\t'*}
366
+ [ -n "${FM_STATUS_SPAN_ENDPOINT_FILE:-}" ] \
367
+ && printf '%s\t%s\t%s\n' "$task" "$endpoint" "$ident" >> "$FM_STATUS_SPAN_ENDPOINT_FILE"
368
+ if [ "$rc" -eq 0 ]; then
369
+ event=${rest#*$'\t'}
370
+ distilled="${distilled}$(basename "$f"): ${event} | "
371
+ rel=1
372
+ continue
373
+ fi
374
+ last=$(last_status_line "$f")
375
+ [ -n "$last" ] || continue
376
+ distilled="${distilled}$(basename "$f"): ${last} | "
377
+ # Nothing captain-relevant is left ahead of the recorded offset. When the log
378
+ # nonetheless ends on a captain-relevant line, this signal is a re-notification
379
+ # of something already escalated, not a routine one; position is the whole
380
+ # dedupe, so no separate seen-marker comparison is needed.
381
+ status_is_captain_relevant "$last" && seen_rel=1
382
+ done
383
+ # strip a trailing " | " separator so the distilled line is clean
384
+ distilled="${distilled% | }"
385
+ if [ -n "$rel" ]; then
386
+ printf 'escalate|%s' "$distilled"
387
+ elif [ -n "$seen_rel" ]; then
388
+ # Already escalated by the per-wake path or the catch-all scan; self-handle
389
+ # to avoid a duplicate entry in the digest.
390
+ printf 'self|signal already escalated (catch-all scan): %s' "$distilled"
391
+ else
392
+ printf 'self|routine signal: %s' "$distilled"
393
+ fi
394
+ }
395
+
396
+ # classify_stale decides the WAKE itself (one-shot per distinct hash). On a
397
+ # first sight of a non-terminal stale it returns "self" and the caller records a
398
+ # timestamp marker; persistence is escalated by housekeeping's recheck, not here.
399
+ classify_stale() { # <window> <state> [<span-record> <span-status>]
400
+ local win=$1 state=$2 record=${3-} rc=${4-} task last event rest
401
+ task=$(window_to_task "$win" "$state")
402
+ if [ -z "$rc" ]; then
403
+ record=$(status_span_first_actionable_record "$state/$task.status" \
404
+ "$(status_seen_offset "$state" "$task")")
405
+ rc=$?
406
+ fi
407
+ last=$(last_status_line "$state/$task.status")
408
+ if [ "$rc" -eq 2 ]; then
409
+ printf 'escalate|unreadable status span for %s' "$task"
410
+ return
411
+ fi
412
+ if [ "$rc" -eq 0 ]; then
413
+ rest=${record#*$'\t'}
414
+ event=${rest#*$'\t'}
415
+ printf 'escalate|stale + actionable status: %s' "$event"
416
+ return
417
+ fi
418
+ if [ -n "$last" ] && status_is_paused_or_captain_held "$last"; then
419
+ # A DECLARED external-wait pause or a verified captain-held transfer
420
+ # (fm-classify-lib.sh owns which declarations qualify): an idle pane is
421
+ # EXPECTED, so this is not a wedge. The caller records a pause marker (long
422
+ # re-surface cadence in housekeeping) rather than a wedge stale marker. Cheap:
423
+ # reuses the status line already read, no fm-crew-state.sh call, mirroring the
424
+ # daemon's existing status-log classification.
425
+ printf 'pause|paused (awaiting external), rechecked on a long cadence: %s' "$last"
426
+ return
427
+ fi
428
+ if [ -n "$last" ] && status_is_captain_relevant "$last"; then
429
+ # Independent of free-text captain-relevant matching: a nonterminal progress
430
+ # verb (working:) must never take the terminal stale path. Seen-status dedupe
431
+ # must not permanently suppress or clear possible-wedge aging merely because
432
+ # prose once looked captain-relevant. Real terminal verbs and legacy free-text
433
+ # captain lines without those verbs keep the terminal escalate/dedupe path.
434
+ if ! status_is_terminal_verb "$last"; then
435
+ case "$(status_line_verb "$last")" in
436
+ working|resolved|captain-held)
437
+ printf 'self|transient stale (%s): %s' "$win" "$last"
438
+ return
439
+ ;;
440
+ esac
441
+ fi
442
+ printf 'self|stale + terminal (already escalated by signal): %s' "$last"
443
+ return
444
+ fi
445
+ # Non-terminal (or no status): defer to the persistence recheck. The caller
446
+ # records/refreshes the stale marker so housekeeping can age it.
447
+ printf 'self|transient stale (%s): %s' "$win" "${last:-no status}"
448
+ }
449
+
450
+ classify_check() { # <full reason> — check scripts print only when firstmate should wake
451
+ printf 'escalate|%s' "$1"
452
+ }
453
+
454
+ classify_heartbeat() {
455
+ # The wake itself is routine; the catch-all scan runs separately in
456
+ # housekeeping on the HEARTBEAT_SCAN_SECS cadence.
457
+ printf 'self|heartbeat (catch-all scan runs in housekeeping)'
458
+ }
459
+
460
+ # Anything unrecognized is escalated (fail-safe).
461
+ classify_unknown() { # <reason>
462
+ printf 'escalate|unknown wake: %s' "$1"
463
+ }
464
+
465
+ # --- stale marker + escalation buffer (stateful, but via explicit state dir) -
466
+ # Marker: state/.subsuper-stale-<key> contains the epoch first seen idle.
467
+ # Buffer: state/.subsuper-escalations one distilled line per escalation.
468
+ # Seen: state/.subsuper-seen-status-<task> last reported file signature and
469
+ # classified byte offset, so failures and events do not re-fire while
470
+ # unread bytes remain recoverable.
471
+
472
+ _stale_key() { printf '%s' "$1" | tr ':/.' '___'; }
473
+
474
+ stale_marker_record() { # <window> <state> — create if absent
475
+ local win=$1 state=$2 key marker
476
+ key=$(_stale_key "$(window_to_task "$win" "$state")")
477
+ marker="$state/.subsuper-stale-$key"
478
+ [ -e "$marker" ] || _now > "$marker"
479
+ }
480
+
481
+ stale_marker_remove() { # <window> <state>
482
+ local win=$1 state=$2 key
483
+ key=$(_stale_key "$(window_to_task "$win" "$state")")
484
+ rm -f "$state/.subsuper-stale-$key"
485
+ }
486
+
487
+ # Pause marker: state/.subsuper-paused-<key> holds the epoch a declared wait (a
488
+ # paused: external wait or a verified captain-held transfer) was first observed
489
+ # declared, whether its pane read idle or busy. Housekeeping ages it against
490
+ # PAUSE_RESURFACE_SECS (much longer than a wedge) and re-surfaces the wait once
491
+ # per window. Recording is create-if-absent so the timestamp is stable across a
492
+ # churny pane (many distinct stale hashes map to one marker), keeping the cadence
493
+ # hash-immune.
494
+ pause_marker_record() { # <window> <state> - create if absent
495
+ local win=$1 state=$2 key marker
496
+ key=$(_stale_key "$(window_to_task "$win" "$state")")
497
+ marker="$state/.subsuper-paused-$key"
498
+ [ -e "$marker" ] || _now > "$marker"
499
+ }
500
+
501
+ pause_marker_remove() { # <window> <state>
502
+ local win=$1 state=$2 key
503
+ key=$(_stale_key "$(window_to_task "$win" "$state")")
504
+ rm -f "$state/.subsuper-paused-$key"
505
+ }
506
+
507
+ clear_pause_tracking() { # <window> <state>
508
+ local win=$1 state=$2 task key watcher_key
509
+ task=$(window_to_task "$win" "$state")
510
+ key=$(_stale_key "$task")
511
+ watcher_key=$(_stale_key "$win")
512
+ rm -f "$state/.subsuper-paused-$key" "$state/.subsuper-stale-$key" \
513
+ "$state/.paused-$watcher_key" "$state/.paused-rechecked-$watcher_key" "$state/.paused-resurfaced-$watcher_key" \
514
+ "$state/.stale-$watcher_key" "$state/.stale-since-$watcher_key" "$state/.wedge-escalations-$watcher_key" \
515
+ "$state/.writing-since-$watcher_key" "$state/.writing-resurfaced-$watcher_key"
516
+ }
517
+
518
+ reconcile_pause_tracking() { # <window> <state> <last-status-line>
519
+ local win=$1 state=$2 last=$3 task key marker watcher_key
520
+ task=$(window_to_task "$win" "$state")
521
+ key=$(_stale_key "$task")
522
+ marker="$state/.subsuper-paused-$key"
523
+ watcher_key=$(_stale_key "$win")
524
+ if status_is_paused_or_captain_held "$last"; then
525
+ stale_marker_remove "$win" "$state"
526
+ pause_marker_record "$win" "$state"
527
+ elif [ -e "$marker" ] || [ -e "$state/.paused-$watcher_key" ]; then
528
+ clear_pause_tracking "$win" "$state"
529
+ fi
530
+ }
531
+
532
+ migrate_watcher_pause_markers() { # <state>
533
+ local state=$1 meta win task key last watcher_key
534
+ for meta in "$state"/*.meta; do
535
+ [ -e "$meta" ] || continue
536
+ win=$(fm_backend_target_of_meta "$meta")
537
+ [ -n "$win" ] || continue
538
+ task=$(basename "$meta"); task=${task%.meta}
539
+ key=$(_stale_key "$task")
540
+ watcher_key=$(_stale_key "$win")
541
+ last=$(last_status_line "$state/$task.status")
542
+ if status_is_paused_or_captain_held "$last" || [ -e "$state/.subsuper-paused-$key" ] || [ -e "$state/.paused-$watcher_key" ]; then
543
+ reconcile_pause_tracking "$win" "$state" "$last"
544
+ fi
545
+ done
546
+ }
547
+
548
+ sync_pause_markers_from_signal() { # <state> <signal files>
549
+ local state=$1 paths=$2 f last task win
550
+ local -a files
551
+ read -r -a files <<<"$paths"
552
+ for f in "${files[@]}"; do
553
+ case "$f" in *.status) ;; *) continue ;; esac
554
+ [ -e "$f" ] || continue
555
+ last=$(last_status_line "$f")
556
+ task=$(basename "$f"); task=${task%.status}
557
+ win=$(window_for_task "$task" "$state" 2>/dev/null || true)
558
+ [ -n "$win" ] || continue
559
+ reconcile_pause_tracking "$win" "$state" "$last"
560
+ done
561
+ }
562
+
563
+ _seen_status_path() { # <state> <task>
564
+ status_daemon_seen_marker_path "$1" "$2"
565
+ }
566
+
567
+ # The byte offset in <task>'s status log through which this daemon has
568
+ # successfully classified content, or 0 when it has no usable position.
569
+ # A position rather than an event line prevents both a later routine append from
570
+ # hiding earlier events and repeated event text from suppressing a new occurrence.
571
+ # An absent, malformed, identity-mismatched, or legacy marker reads 0, so the
572
+ # whole log is classified and uncertainty prefers a duplicate over event loss.
573
+ status_seen_offset() { # <state> <task>
574
+ status_presentation_marker_offset "$(_seen_status_path "$1" "$2")" "$1/$2.status"
575
+ }
576
+
577
+ # Commit <task>'s successfully classified endpoint, so the heartbeat catch-all
578
+ # scan does not re-read events already handled by the per-wake or scan path.
579
+ mark_status_seen() { # <state> <task> <captured-end-offset> <captured-identity>
580
+ status_presentation_marker_commit "$(_seen_status_path "$1" "$2")" \
581
+ "$1/$2.status" "$3" "$4"
582
+ }
583
+
584
+ # Advance the offset for every task a per-wake classification escalated, so the
585
+ # catch-all scan does not re-escalate the same events within HEARTBEAT_SCAN_SECS.
586
+ # An ERROR row names a task whose log could not be classified and carries the
587
+ # observed file signature.
588
+ # Recording that signature bounds the report while leaving its classification
589
+ # position unchanged, so readable recovery resumes from the last proven byte.
590
+ mark_escalated_seen() { # <state> <captured-endpoint-file>
591
+ local state=$1 capture=$2 task endpoint ident rc=0
592
+ [ -f "$capture" ] || return 1
593
+ while IFS=$(printf '\t') read -r task endpoint ident; do
594
+ [ -n "$task" ] || continue
595
+ if [ "$task" = ERROR ]; then
596
+ status_presentation_marker_report "$(_seen_status_path "$state" "$endpoint")" "$ident" || rc=1
597
+ continue
598
+ fi
599
+ mark_status_seen "$state" "$task" "$endpoint" "$ident" || rc=1
600
+ done < "$capture"
601
+ return "$rc"
602
+ }
603
+
604
+ # Busy and composer-empty detection form the injection boundary.
605
+ # These thin wrappers keep the daemon's call sites and unit tests stable.
606
+ #
607
+ # pane_input_pending returns 0 unless the composer is positively proven empty.
608
+ # This includes real unsubmitted text, ambiguous structure, unreadable state,
609
+ # blank or otherwise unidentified rows (the strict container-proof rule owned
610
+ # by bin/fm-composer-lib.sh), and future verdicts. The detector drops
611
+ # dim/faint ghost text and strips the harness's composer box borders, so an
612
+ # aligned ghost-only or idle bordered claude composer ("│ > … │") is correctly
613
+ # proven empty while a modal dialog or dead shell never is.
614
+ # pane_is_busy / pane_input_pending: BACKEND-AWARE (dispatch goes through
615
+ # bin/fm-backend.sh's generic per-backend primitives rather than a hand-rolled
616
+ # case statement here). <backend> defaults to tmux when omitted, so every
617
+ # existing caller/test that passes only <target> is unaffected.
618
+ #
619
+ # This rendered reader applies only to the supervisor pane during away-mode
620
+ # injection. It never classifies a recorded worker task. The detected primary
621
+ # harness selects exactly one signature, so output from another harness cannot
622
+ # make the primary read busy.
623
+ #
624
+ # Resolved lazily and memoized: harness detection walks process ancestry, which
625
+ # is too heavy to pay on every source of this library (the unit tests and the
626
+ # launcher source it purely for its pure functions).
627
+ fm_daemon_primary_harness() {
628
+ if [ -z "${FM_DAEMON_PRIMARY_HARNESS:-}" ]; then
629
+ FM_DAEMON_PRIMARY_HARNESS=$("$FM_DAEMON_DIR/fm-harness.sh" 2>/dev/null || printf 'unknown')
630
+ [ -n "$FM_DAEMON_PRIMARY_HARNESS" ] || FM_DAEMON_PRIMARY_HARNESS=unknown
631
+ fi
632
+ printf '%s' "$FM_DAEMON_PRIMARY_HARNESS"
633
+ }
634
+
635
+ pane_is_busy() { # <target> [backend]
636
+ local target=$1 backend=${2:-tmux} native tail40 harness
637
+ harness=$(fm_daemon_primary_harness)
638
+ native=$(fm_backend_busy_state "$backend" "$target" 2>/dev/null)
639
+ case "$native" in
640
+ busy) return 0 ;;
641
+ esac
642
+ tail40=$(fm_backend_capture "$backend" "$target" 40 2>/dev/null) || return 1
643
+ printf '%s' "$tail40" | grep -v '^[[:space:]]*$' | tail -12 \
644
+ | fm_busy_lines_match "$harness"
645
+ }
646
+
647
+ # pane_input_pending dispatches through fm_backend_composer_state and treats
648
+ # every verdict except exact empty as unsafe. inject_msg reads the full verdict
649
+ # directly and applies the same positive-proof boundary.
650
+ pane_input_pending() { # <target> [backend]
651
+ local target=$1 backend=${2:-tmux}
652
+ [ "$(fm_backend_composer_state "$backend" "$target" 2>/dev/null)" != empty ]
653
+ }
654
+
655
+ task_window_backend() { # <window> <state>
656
+ local win=$1 state=$2 task meta
657
+ task=$(window_to_task "$win" "$state")
658
+ meta="$state/$task.meta"
659
+ fm_backend_of_meta "$meta"
660
+ }
661
+
662
+ task_window_harness() { # <window> <state>
663
+ local win=$1 state=$2 task meta
664
+ task=$(window_to_task "$win" "$state")
665
+ meta="$state/$task.meta"
666
+ grep '^harness=' "$meta" 2>/dev/null | cut -d= -f2- || true
667
+ }
668
+
669
+ # stale_window_is_busy: 0 when the task is PROVABLY working through the
670
+ # semantic busy-state contract (bin/fm-busy-lib.sh), 1 when it is not, and 2
671
+ # when the endpoint could not be read at all. Only an exact busy verdict is
672
+ # working: unknown semantic state never becomes busy and never becomes a
673
+ # silent idle, so a stale pane whose state cannot be proven surfaces.
674
+ stale_window_is_busy() { # <window> <state>
675
+ local win=$1 state=$2 backend harness label task tail40 verdict
676
+ backend=$(task_window_backend "$win" "$state")
677
+ harness=$(task_window_harness "$win" "$state")
678
+ task=$(window_to_task "$win" "$state")
679
+ label="fm-$task"
680
+ tail40=$(fm_backend_capture "$backend" "$win" 40 "$label" 2>/dev/null) || return 2
681
+ verdict=$(fm_busy_classify "$backend" "$win" "$harness" "$task" "$state" "$tail40")
682
+ [ "${verdict%% *}" = busy ]
683
+ }
684
+
685
+ escalate_add() { # <state> <distilled-item>
686
+ local state=$1 item=$2 buf
687
+ buf="$state/.subsuper-escalations"
688
+ [ -s "$buf" ] || _now > "${buf}.since"
689
+ printf '%s\n' "$item" >> "$buf"
690
+ }
691
+
692
+ # Flush the escalation buffer as ONE batched, single-line digest to the
693
+ # supervisor pane. Returns 0 on successful inject (or empty buffer), non-zero on
694
+ # inject failure (buffer preserved for retry / catch-up).
695
+ escalate_flush() { # <state>
696
+ local state=$1 buf item n msg
697
+ buf="$state/.subsuper-escalations"
698
+ [ -s "$buf" ] || return 0
699
+ n=$(wc -l < "$buf" 2>/dev/null || echo 0)
700
+ # Join buffered items with the literal " | " separator into one digest line.
701
+ msg=$(awk 'NR>1{printf " | "} {printf "%s",$0} END{print ""}' "$buf" 2>/dev/null)
702
+ # Single-line wrapper: no embedded newlines (inject_msg also collapses as a
703
+ # safety net, but keeping the source single-line makes the intent explicit).
704
+ msg=$(printf 'Supervisor escalate (%s event(s)): %s (pre-read; re-arm not needed — watcher daemon-managed)' "$n" "$msg")
705
+ if inject_msg "$msg" "$state"; then : > "$buf"; rm -f "${buf}.since" "$state/.subsuper-inject-wedged"; return 0; fi
706
+ return 1
707
+ }
708
+
709
+ # --- backend-independent active wedge alert ---------------------------------
710
+ # The tmux status-line flash in inject_wedge_alarm below is a cosmetic,
711
+ # client-side OSD with no cross-backend equivalent, so a wedged non-tmux primary
712
+ # (the 2026-07-10 overnight incident: a claude-on-herdr primary) got NO active
713
+ # signal - only the passive state/.subsuper-inject-wedged marker, which nothing
714
+ # surfaces until the next fleet action (that night, 20 escalations sat buffered
715
+ # for 8.5h). These helpers add a configurable active alert that does not depend
716
+ # on any pane or its backend status-line: an OS-level macOS notification, a
717
+ # herdr notification, or a captain-supplied command (push to a phone, etc.).
718
+ # Every channel is best-effort - a missing or failing channel logs and is
719
+ # skipped, never crashing the daemon loop - and the durable marker plus the tmux
720
+ # flash stay exactly as before.
721
+ #
722
+ # Config: config/wedge-alarm (local, gitignored), one channel directive per
723
+ # non-empty, non-comment line. FM_WEDGE_ALARM_CHANNEL overrides the file with a
724
+ # single directive. Directives:
725
+ # off disable the active alert entirely, regardless of position
726
+ # (marker + flash remain)
727
+ # auto | default platform default: macOS -> osascript; otherwise none
728
+ # osascript macOS Notification Center banner (backend-independent)
729
+ # herdr herdr UI notification (herdr notification show)
730
+ # command:<cmd> run <cmd> via `sh -c`, summary on $1 and on stdin
731
+ # An absent config means auto, i.e. default-ON on macOS: the alarm's whole
732
+ # purpose is to never be silent, so the reachable OS channel fires unless the
733
+ # captain explicitly disables it.
734
+
735
+ # Print the configured channel directives, one per line. FM_WEDGE_ALARM_CHANNEL
736
+ # wins (a single directive); else each non-empty, non-comment line of
737
+ # config/wedge-alarm; else "auto".
738
+ wedge_alarm_configured_channels() {
739
+ local cfg line found=
740
+ if [ -n "${FM_WEDGE_ALARM_CHANNEL:-}" ]; then
741
+ printf '%s\n' "$FM_WEDGE_ALARM_CHANNEL"
742
+ return 0
743
+ fi
744
+ cfg="${FM_CONFIG_OVERRIDE:-$FM_HOME/config}/wedge-alarm"
745
+ if [ -f "$cfg" ]; then
746
+ while IFS= read -r line || [ -n "$line" ]; do
747
+ line="${line#"${line%%[![:space:]]*}"}"
748
+ line="${line%"${line##*[![:space:]]}"}"
749
+ [ -n "$line" ] || continue
750
+ case "$line" in '#'*) continue ;; esac
751
+ printf '%s\n' "$line"
752
+ found=1
753
+ done < "$cfg"
754
+ fi
755
+ [ -n "$found" ] || printf 'auto\n'
756
+ }
757
+
758
+ # Resolve the platform's default OS-level channel for `auto`. macOS reaches the
759
+ # captain via an osascript Notification Center banner; other platforms have no
760
+ # built-in OS channel (the captain wires a command: directive), so this prints
761
+ # nothing and wedge_alarm_notify logs that the marker is the only signal.
762
+ wedge_alarm_platform_default() {
763
+ case "$(uname)" in
764
+ Darwin) command -v osascript >/dev/null 2>&1 && printf 'osascript' ;;
765
+ *) : ;;
766
+ esac
767
+ }
768
+
769
+ wedge_alarm_run_bounded() {
770
+ local channel=$1 timeout monitor_was_on=0 pid start elapsed rc
771
+ shift
772
+ timeout=${FM_WEDGE_ALARM_TIMEOUT_SECS:-$WEDGE_ALARM_TIMEOUT_SECS_DEFAULT}
773
+ case "$timeout" in
774
+ ''|*[!0-9]*) timeout=$WEDGE_ALARM_TIMEOUT_SECS_DEFAULT ;;
775
+ *) [ "$timeout" -gt 0 ] 2>/dev/null || timeout=$WEDGE_ALARM_TIMEOUT_SECS_DEFAULT ;;
776
+ esac
777
+ case $- in *m*) monitor_was_on=1 ;; esac
778
+ set -m 2>/dev/null || true
779
+ case $- in
780
+ *m*) ;;
781
+ *) log "wedge alarm: ${channel} notifier skipped because its watchdog could not start"; return 125 ;;
782
+ esac
783
+ "$@" &
784
+ pid=$!
785
+ WEDGE_ALARM_NOTIFIER_PID=$pid
786
+ start=$SECONDS
787
+ while kill -0 "-$pid" 2>/dev/null; do
788
+ elapsed=$((SECONDS - start))
789
+ if [ "$elapsed" -ge "$timeout" ]; then
790
+ wedge_alarm_stop_active_notifier
791
+ [ "$monitor_was_on" -eq 1 ] || set +m 2>/dev/null || true
792
+ log "wedge alarm: ${channel} notifier timed out after ${elapsed}s (limit ${timeout}s)"
793
+ return 124
794
+ fi
795
+ sleep 0.1
796
+ done
797
+ if wait "$pid"; then rc=0; else rc=$?; fi
798
+ WEDGE_ALARM_NOTIFIER_PID=
799
+ [ "$monitor_was_on" -eq 1 ] || set +m 2>/dev/null || true
800
+ return "$rc"
801
+ }
802
+
803
+ wedge_alarm_stop_active_notifier() {
804
+ local pid=${WEDGE_ALARM_NOTIFIER_PID:-}
805
+ [ -n "$pid" ] || return 0
806
+ WEDGE_ALARM_NOTIFIER_PID=
807
+ kill -TERM "-$pid" 2>/dev/null || kill -TERM "$pid" 2>/dev/null || true
808
+ sleep 0.2
809
+ kill -KILL "-$pid" 2>/dev/null || kill -KILL "$pid" 2>/dev/null || true
810
+ wait "$pid" 2>/dev/null || true
811
+ }
812
+
813
+ # The single execution seam for every configured notifier channel.
814
+ # FM_WEDGE_ALARM_EXEC, when set, REPLACES the real notifier: the resolved channel
815
+ # name and summary are handed to that command instead of ever invoking osascript
816
+ # or herdr or a captain-supplied command. This is the one injection point the test harness forces to a recorder
817
+ # so no test can post a real desktop notification - the library-mode guard at the
818
+ # foot of this file defaults it to "discard" whenever the daemon is SOURCED
819
+ # rather than executed, which is the only way a test reaches these functions. The
820
+ # special value "discard" fires nothing; unset means production (the executed
821
+ # daemon), so the real channels fire.
822
+ wedge_alarm_os_notifier_override() { # <channel> <summary>
823
+ local channel=$1 summary=$2 rc exec_override=${FM_WEDGE_ALARM_EXEC:-}
824
+ case "$exec_override" in
825
+ '') return 2 ;;
826
+ discard) return 0 ;;
827
+ *)
828
+ wedge_alarm_run_bounded "$channel" "$exec_override" "$channel" "$summary" >/dev/null 2>&1
829
+ rc=$?
830
+ [ "$rc" -eq 0 ] && return 0
831
+ log "wedge alarm: notifier override exited $rc for channel '$channel'"
832
+ return 1 ;;
833
+ esac
834
+ }
835
+
836
+ # Post a macOS Notification Center banner. `display notification` is OS-level,
837
+ # independent of any terminal pane or multiplexer status-line. The summary is
838
+ # passed as an argv item (never interpolated into the AppleScript source) so its
839
+ # text can never break the script. Best-effort: logs and returns 1 on failure.
840
+ wedge_alarm_via_osascript() { # <summary>
841
+ local summary=$1 rc
842
+ wedge_alarm_os_notifier_override osascript "$summary"
843
+ rc=$?
844
+ case "$rc" in
845
+ 0) return 0 ;;
846
+ 1) return 1 ;;
847
+ esac
848
+ command -v osascript >/dev/null 2>&1 || {
849
+ log "wedge alarm: osascript not found; cannot post a macOS notification"; return 1; }
850
+ wedge_alarm_run_bounded osascript osascript -e 'on run argv' \
851
+ -e 'display notification (item 1 of argv) with title "firstmate: away-mode escalations WEDGED" sound name "Basso"' \
852
+ -e 'end run' "$summary" >/dev/null 2>&1 && return 0
853
+ log "wedge alarm: osascript notification failed"
854
+ return 1
855
+ }
856
+
857
+ # Post a herdr UI notification - herdr's own surface, separate from the pane and
858
+ # its status-line. Best-effort: logs and returns 1 on failure.
859
+ wedge_alarm_via_herdr() { # <summary>
860
+ local summary=$1 rc
861
+ wedge_alarm_os_notifier_override herdr "$summary"
862
+ rc=$?
863
+ case "$rc" in
864
+ 0) return 0 ;;
865
+ 1) return 1 ;;
866
+ esac
867
+ command -v herdr >/dev/null 2>&1 || {
868
+ log "wedge alarm: herdr not found; cannot post a herdr notification"; return 1; }
869
+ wedge_alarm_run_bounded herdr herdr notification show "firstmate: away-mode escalations WEDGED" \
870
+ --body "$summary" --sound request >/dev/null 2>&1 && return 0
871
+ log "wedge alarm: herdr notification failed"
872
+ return 1
873
+ }
874
+
875
+ # Run a captain-supplied command with the summary on $1 and on stdin, so an
876
+ # alert can reach a phone/pager (ntfy, Slack, SMS) even when the captain is away
877
+ # from the machine entirely. Best-effort: logs and returns 1 on failure.
878
+ wedge_alarm_via_command() { # <cmd> <summary>
879
+ local cmd=$1 summary=$2 rc
880
+ if [ "${WEDGE_ALARM_EMIT_ACTIVE:-}" != 1 ]; then
881
+ wedge_alarm_emit command "$summary" "$cmd"
882
+ return $?
883
+ fi
884
+ [ -n "$cmd" ] || { log "wedge alarm: empty command: channel; nothing to run"; return 1; }
885
+ wedge_alarm_run_bounded command sh -c "$cmd" fm-wedge-alarm "$summary" \
886
+ <<< "$summary" >/dev/null 2>&1
887
+ rc=$?
888
+ [ "$rc" -eq 0 ] && return 0
889
+ log "wedge alarm: command channel exited $rc (command redacted)"
890
+ return 1
891
+ }
892
+
893
+ wedge_alarm_emit() { # <channel> <summary>
894
+ local channel=$1 summary=$2 cmd=${3:-} rc exec_override=${FM_WEDGE_ALARM_EXEC:-} WEDGE_ALARM_EMIT_ACTIVE=1
895
+ case "$exec_override" in
896
+ '') ;;
897
+ discard) return 0 ;;
898
+ *)
899
+ wedge_alarm_run_bounded "$channel" "$exec_override" "$channel" "$summary" >/dev/null 2>&1
900
+ rc=$?
901
+ [ "$rc" -eq 0 ] && return 0
902
+ log "wedge alarm: notifier override exited $rc for channel '$channel'"
903
+ return 1 ;;
904
+ esac
905
+ case "$channel" in
906
+ osascript) wedge_alarm_via_osascript "$summary" ;;
907
+ herdr) wedge_alarm_via_herdr "$summary" ;;
908
+ command) wedge_alarm_via_command "$cmd" "$summary" ;;
909
+ esac
910
+ }
911
+
912
+ # Fire every configured active-alert channel, best-effort. Always returns 0: a
913
+ # channel failure can never abort inject_wedge_alarm or the daemon loop. Any
914
+ # `off` directive disables the alert, regardless of position; an unresolvable
915
+ # `auto` (no OS channel on this platform) logs that the durable marker is the
916
+ # only signal. Every notifier routes through the test-forced recorder seam.
917
+ wedge_alarm_notify() { # <summary> <marker>
918
+ local summary=$1 marker=$2 ch
919
+ local -a channels=()
920
+ while IFS= read -r ch; do
921
+ [ -n "$ch" ] || continue
922
+ channels+=("$ch")
923
+ done < <(wedge_alarm_configured_channels)
924
+ for ch in "${channels[@]}"; do
925
+ [ "$ch" = off ] && return 0
926
+ done
927
+ for ch in "${channels[@]}"; do
928
+ case "$ch" in auto|default) ch=$(wedge_alarm_platform_default) ;; esac
929
+ case "$ch" in
930
+ '') log "wedge alarm: no OS-level alert channel on $(uname); durable marker $marker is the only signal - set config/wedge-alarm (e.g. a command: directive)" ;;
931
+ osascript|herdr) wedge_alarm_emit "$ch" "$summary" || true ;;
932
+ command:*) wedge_alarm_emit command "$summary" "${ch#command:}" || true ;;
933
+ *) log "wedge alarm: unrecognized active-alert channel directive (redacted); marker still written" ;;
934
+ esac
935
+ done
936
+ return 0
937
+ }
938
+
939
+ # Raise a loud, rate-limited alarm when escalations cannot be delivered after
940
+ # max-defer (the supervisor pane is genuinely busy/wedged, or the submit's Enter
941
+ # is swallowed). The daemon must NEVER silently wedge: this logs
942
+ # an ERROR, drops a durable marker firstmate/recovery can surface, flashes
943
+ # the tmux supervisor client's status line when applicable, and attempts a
944
+ # configurable backend-independent active alert (wedge_alarm_notify). Nothing
945
+ # is lost - the buffer and the
946
+ # wake-queue both survive - but the stall stops being invisible.
947
+ inject_wedge_alarm() { # <state> <age-seconds>
948
+ local state=$1 age=$2 marker target backend max_defer now notify=1
949
+ marker="$state/.subsuper-inject-wedged"
950
+ max_defer="${FM_MAX_DEFER_SECS:-$MAX_DEFER_SECS_DEFAULT}"
951
+ # Re-alarm at most once per max-defer window so a long wedge does not spam.
952
+ if [ "$(_file_age "$marker")" -lt "$max_defer" ]; then
953
+ return 0
954
+ fi
955
+ now=$(_now)
956
+ if [ "$WEDGE_ALARM_LAST_EPOCH" -gt 0 ] && [ $((now - WEDGE_ALARM_LAST_EPOCH)) -lt "$max_defer" ]; then
957
+ notify=0
958
+ else
959
+ WEDGE_ALARM_LAST_EPOCH=$now
960
+ log "ERROR: away-mode escalation undelivered ${age}s; inject could not confirm a submit (supervisor pane busy or wedged). Buffer + wake-queue preserved; alarm marker written."
961
+ fi
962
+ {
963
+ printf 'fm away-mode inject WEDGED: %ss undelivered as of %s\n' "$age" "$(date '+%Y-%m-%dT%H:%M:%S%z')"
964
+ printf 'The supervisor pane could not accept an escalation. Buffered items:\n'
965
+ cat "$state/.subsuper-escalations" 2>/dev/null
966
+ } 2>/dev/null > "$marker" || true
967
+ target="${FM_SUPERVISOR_TARGET:-$FM_SUPERVISOR_TARGET_DEFAULT}"
968
+ backend="${FM_SUPERVISOR_BACKEND:-$FM_SUPERVISOR_BACKEND_DEFAULT}"
969
+ # Best-effort status-line flash. tmux's display-message is a client-side OSD
970
+ # with no herdr equivalent; the log line + durable marker above are already
971
+ # the primary, backend-independent signal, so a non-tmux backend just skips
972
+ # this cosmetic extra rather than attempting an unsupported call.
973
+ if [ "$backend" = tmux ]; then
974
+ tmux display-message -t "$target" "fm: away-mode escalations WEDGED ${age}s — see $marker" 2>/dev/null || true
975
+ fi
976
+ # Backend-independent active alert. Unlike the tmux flash above (skipped on
977
+ # every non-tmux backend), this can reach the captain even when every pane and
978
+ # its backend status-line is unreadable - the gap the 2026-07-10 overnight
979
+ # incident fell through. Configurable and best-effort; the marker above stays
980
+ # the durable record whether or not any channel fires.
981
+ if [ "$notify" -eq 1 ]; then
982
+ wedge_alarm_notify "away-mode escalations WEDGED ${age}s undelivered - see $marker" "$marker"
983
+ fi
984
+ }
985
+
986
+ _oldest_line_age() { # <buf> -> seconds since the oldest buffered item first arrived (sidecar epoch)
987
+ local f=$1 since
988
+ [ -s "$f" ] || { echo 999999; return; }
989
+ since="${f}.since"
990
+ if [ -r "$since" ]; then
991
+ echo $(( $(_now) - $(cat "$since" 2>/dev/null || echo 0) ))
992
+ else
993
+ echo 999999
994
+ fi
995
+ }
996
+
997
+ # --- housekeeping (runs every tick while the watcher is mid-cycle) ----------
998
+ # Four cheap jobs, each guarded so an empty/quiet fleet costs near zero:
999
+ # 1) batch flush: if the escalation buffer's oldest content is older than
1000
+ # ESCALATE_BATCH_SECS (or batching is disabled), inject one digest.
1001
+ # 1b) max-defer escape: if the buffer is STILL undelivered past MAX_DEFER_SECS,
1002
+ # attempt one normal delivery; if it cannot confirm, raise the wedge alarm.
1003
+ # Never silently defer forever.
1004
+ # 2) stale recheck: for each pending stale marker past STALE_ESCALATE_SECS,
1005
+ # re-peek the pane; still idle -> escalate (wedge); resumed -> clear marker.
1006
+ # 2b) pause re-surface: for each declared-wait marker past PAUSE_RESURFACE_SECS,
1007
+ # re-peek; gone -> clear; still declaring the wait, on an idle OR a busy pane
1008
+ # -> escalate a recheck digest naming which human the wait is on, and reset
1009
+ # the window (repeating bounded re-surface, never a wedge).
1010
+ # 3) heartbeat scan: every HEARTBEAT_SCAN_SECS, grep state/*.status for a
1011
+ # captain-relevant line the per-wake classifier missed and escalate it.
1012
+ housekeeping() { # <state>
1013
+ local state=$1 now due f key task win marker age last max_defer oldest pause_secs
1014
+ now=$(_now)
1015
+ migrate_watcher_pause_markers "$state"
1016
+
1017
+ # (1) batch flush
1018
+ if [ "${FM_ESCALATE_BATCH_SECS:-$ESCALATE_BATCH_SECS_DEFAULT}" -le 0 ]; then
1019
+ escalate_flush "$state" || true
1020
+ else
1021
+ due=$(_oldest_line_age "$state/.subsuper-escalations")
1022
+ if [ "$due" -ge "${FM_ESCALATE_BATCH_SECS:-$ESCALATE_BATCH_SECS_DEFAULT}" ]; then
1023
+ escalate_flush "$state" || true
1024
+ fi
1025
+ fi
1026
+
1027
+ # (1b) max-defer escape. If anything is still buffered past MAX_DEFER_SECS,
1028
+ # retry the normal delivery path. If that still cannot confirm, raise a loud
1029
+ # wedge alarm while preserving the buffer.
1030
+ max_defer=${FM_MAX_DEFER_SECS:-$MAX_DEFER_SECS_DEFAULT}
1031
+ if afk_active "$state" && [ "$max_defer" -gt 0 ] && [ -s "$state/.subsuper-escalations" ]; then
1032
+ oldest=$(_oldest_line_age "$state/.subsuper-escalations")
1033
+ # Throttle the alarm to once per max-defer window (the wedge marker doubles
1034
+ # as the throttle). A successful flush clears the buffer; a failed one alarms
1035
+ # and waits.
1036
+ if [ "$oldest" -ge "$max_defer" ] \
1037
+ && [ "$(_file_age "$state/.subsuper-inject-wedged")" -ge "$max_defer" ]; then
1038
+ if escalate_flush "$state"; then
1039
+ log "inject recovered: max-defer flush succeeded after ${oldest}s undelivered"
1040
+ rm -f "$state/.subsuper-inject-wedged"
1041
+ else
1042
+ inject_wedge_alarm "$state" "$oldest"
1043
+ fi
1044
+ fi
1045
+ fi
1046
+
1047
+ # (2) stale persistence recheck
1048
+ for marker in "$state"/.subsuper-stale-*; do
1049
+ [ -e "$marker" ] || continue
1050
+ key="${marker##*.subsuper-stale-}"
1051
+ # Reconstruct the backend target from metadata, with the live tmux list as the
1052
+ # legacy fallback for old markers that predate meta lookup.
1053
+ win=$(window_for_task "$key" "$state" 2>/dev/null || true)
1054
+ if [ -z "$win" ]; then
1055
+ # Window gone (task torn down): drop the marker, nothing to escalate.
1056
+ rm -f "$marker"; continue
1057
+ fi
1058
+ task=$(window_to_task "$win" "$state")
1059
+ last=$(last_status_line "$state/$task.status")
1060
+ if [ -n "$last" ] && status_is_paused_or_captain_held "$last"; then
1061
+ reconcile_pause_tracking "$win" "$state" "$last"
1062
+ continue
1063
+ fi
1064
+ age=$(( now - $(cat "$marker" 2>/dev/null || echo "$now") ))
1065
+ [ "$age" -ge "${FM_STALE_ESCALATE_SECS:-$STALE_ESCALATE_SECS_DEFAULT}" ] || continue
1066
+ stale_window_is_busy "$win" "$state"
1067
+ case "$?" in
1068
+ 0) rm -f "$marker" ;;
1069
+ 2) rm -f "$marker" ;;
1070
+ *) if escalate_add "$state" "stale persisted ${age}s (possible wedge): $win"; then
1071
+ stale_marker_remove "$win" "$state"
1072
+ fi ;;
1073
+ esac
1074
+ done
1075
+
1076
+ # (2b) pause re-surface recheck. A declared wait is waiting, not wedged (fm-classify-lib.sh's
1077
+ # status_is_paused_or_captain_held owns which declarations qualify), so it is
1078
+ # rechecked on a much longer cadence than a wedge (PAUSE_RESURFACE_SECS) and never
1079
+ # escalated as one - but it MUST re-surface, so neither a forgotten pause nor a
1080
+ # forgotten captain hold can rot invisibly. Past the window: gone -> drop; still
1081
+ # declaring the wait -> escalate a recheck digest and reset the marker so the window
1082
+ # repeats. The digest names WHICH human the wait is on, because the captain is the
1083
+ # one reading it: an external dependency for a paused: declaration, and the captain
1084
+ # themself for a verified hold transfer.
1085
+ # Pane busy state does NOT end the wait. A declared wait can legitimately hold a
1086
+ # pane busy - a worker parked on a long foreground call it keeps live for as long
1087
+ # as the wait lasts - so reading busy as "the crew resumed" retires the window of
1088
+ # exactly the declaration that needs it. The crew's own latest status line is the
1089
+ # authority, and the loop head above already drops the marker the moment that line
1090
+ # stops declaring the wait.
1091
+ pause_secs=${FM_PAUSE_RESURFACE_SECS:-$FM_PAUSE_RESURFACE_SECS_DEFAULT}
1092
+ for marker in "$state"/.subsuper-paused-*; do
1093
+ [ -e "$marker" ] || continue
1094
+ key="${marker##*.subsuper-paused-}"
1095
+ win=$(window_for_task "$key" "$state" 2>/dev/null || true)
1096
+ if [ -z "$win" ]; then
1097
+ rm -f "$marker"; continue
1098
+ fi
1099
+ task=$(window_to_task "$win" "$state")
1100
+ last=$(last_status_line "$state/$task.status")
1101
+ if [ -z "$last" ] || ! status_is_paused_or_captain_held "$last"; then
1102
+ reconcile_pause_tracking "$win" "$state" "$last"
1103
+ continue
1104
+ fi
1105
+ age=$(( now - $(cat "$marker" 2>/dev/null || echo "$now") ))
1106
+ [ "$age" -ge "$pause_secs" ] || continue
1107
+ # Endpoint-readability probe only: exit code 2 means the capture failed, so the
1108
+ # endpoint is gone and there is nothing left to re-surface. The busy/idle verdict
1109
+ # is deliberately discarded here. Do NOT reinstate a `0)` arm dropping the marker
1110
+ # on busy: migrate_watcher_pause_markers recreates it with a fresh timestamp on
1111
+ # the very next tick while the declaration still stands, so the window would
1112
+ # restart forever and the wait would never mature into its one recheck.
1113
+ stale_window_is_busy "$win" "$state"
1114
+ case "$?" in
1115
+ 2) rm -f "$marker" ;;
1116
+ *)
1117
+ last=$(last_status_line "$state/$task.status")
1118
+ if [ -n "$last" ] && status_is_captain_held "$last"; then
1119
+ if escalate_add "$state" "captain-held ${age}s (awaiting the captain, answer the held decision or release the hold): $win"; then
1120
+ _now > "$marker"
1121
+ fi
1122
+ elif [ -n "$last" ] && status_is_paused "$last"; then
1123
+ if escalate_add "$state" "paused ${age}s (awaiting external, recheck whether the wait still holds): $win"; then
1124
+ _now > "$marker"
1125
+ fi
1126
+ else
1127
+ rm -f "$marker"
1128
+ fi
1129
+ ;;
1130
+ esac
1131
+ done
1132
+
1133
+ # (3) heartbeat scan (catch-all for a captain-relevant status the per-wake
1134
+ # classifier may have missed). Cheap: status files only, no tmux. It walks
1135
+ # every log rather than only those whose LAST line looks captain-relevant,
1136
+ # because the event this backstop most needs to catch is precisely one a
1137
+ # later routine append has already moved past; fm-classify-lib.sh's span
1138
+ # read decides relevance, and the classified-through offset is the dedup.
1139
+ if [ "$(_file_age "$state/.subsuper-last-scan")" -ge "${FM_HEARTBEAT_SCAN_SECS:-$HEARTBEAT_SCAN_SECS_DEFAULT}" ]; then
1140
+ _now > "$state/.subsuper-last-scan"
1141
+ local event record rest endpoint ident rc
1142
+ for f in "$state"/*.status; do
1143
+ [ -e "$f" ] || [ -L "$f" ] || continue
1144
+ task=$(basename "$f"); task="${task%.status}"
1145
+ record=$(status_span_first_actionable_record "$f" \
1146
+ "$(status_seen_offset "$state" "$task")")
1147
+ rc=$?
1148
+ if [ "$rc" -eq 2 ]; then
1149
+ ident=$(status_observed_signature "$f")
1150
+ status_presentation_marker_reported_matches "$(_seen_status_path "$state" "$task")" "$ident" \
1151
+ && continue
1152
+ if escalate_add "$state" "$(basename "$f"): unreadable status span (catch-all scan)"; then
1153
+ status_presentation_marker_report "$(_seen_status_path "$state" "$task")" "$ident" || true
1154
+ fi
1155
+ continue
1156
+ fi
1157
+ [ "$rc" -eq 1 ] && [ -z "$record" ] && continue
1158
+ endpoint=${record%%$'\t'*}
1159
+ rest=${record#*$'\t'}; ident=${rest%%$'\t'*}
1160
+ if [ "$rc" -eq 0 ]; then
1161
+ event=${rest#*$'\t'}
1162
+ if escalate_add "$state" "$(basename "$f"): $event (catch-all scan)"; then
1163
+ mark_status_seen "$state" "$task" "$endpoint" "$ident" || true
1164
+ fi
1165
+ elif ! mark_status_seen "$state" "$task" "$endpoint" "$ident"; then
1166
+ escalate_add "$state" "$(basename "$f"): status position commit failed (catch-all scan)"
1167
+ fi
1168
+ done
1169
+ fi
1170
+ }
1171
+
1172
+ # Find a recorded or live window target whose task id matches the marker key.
1173
+ window_for_task() { # <task-key> [state]
1174
+ local key=$1 state=${2:-$(_state_root)} meta task w t
1175
+ for meta in "$state"/*.meta; do
1176
+ [ -e "$meta" ] || continue
1177
+ task=$(basename "$meta"); task=${task%.meta}
1178
+ [ "$(_stale_key "$task")" = "$key" ] || continue
1179
+ w=$(fm_backend_target_of_meta "$meta")
1180
+ [ -n "$w" ] && { printf '%s' "$w"; return 0; }
1181
+ done
1182
+ for w in $(tmux list-windows -a -F '#{session_name}:#{window_name}' 2>/dev/null | grep ':fm-' || true); do
1183
+ t=$(window_to_task "$w" "$state")
1184
+ [ "$(_stale_key "$t")" = "$key" ] && { printf '%s' "$w"; return 0; }
1185
+ done
1186
+ return 1
1187
+ }
1188
+
1189
+ # --- injection --------------------------------------------------------------
1190
+ # inject_msg: send one escalation digest to the supervisor pane.
1191
+ # Returns 0 on successful inject (or empty buffer), non-zero if the pane is
1192
+ # gone, the supervisor is busy, afk is inactive, or the verified submit cannot
1193
+ # be confirmed after bounded retries. On non-zero the caller preserves
1194
+ # the buffer so the escalation survives for the next cycle or the catch-up flush.
1195
+ #
1196
+ # Submit model:
1197
+ # - TYPE ONCE, then submit with Enter. Never retype the digest: a swallowed
1198
+ # Enter leaves our text in the composer, and retyping would concatenate two
1199
+ # sentinel-prefixed digests into one corrupted turn.
1200
+ # - SUBMIT ACK = the backend submit primitive reports `empty` after Enter.
1201
+ # For tmux that means a cleared composer; for herdr's normal idle-baseline
1202
+ # path it means native agent-state observed a real turn start.
1203
+ # Pending means Enter was swallowed; unknown is treated as undelivered by
1204
+ # this strict daemon path.
1205
+ # - COMPOSER GUARD before typing: if the cursor line already has real content
1206
+ # after dim/faint ghost text and borders are ignored (a human's half-typed
1207
+ # line, or a previous injection's unsent text), defer entirely - injecting
1208
+ # would merge with the human's text.
1209
+ inject_msg() { # <message> [state]
1210
+ local msg=$1 state target backend retries sleep_s verdict composer encoded
1211
+ state="${2:-$(_state_root)}"
1212
+ # (1) Presence-gate: inject ONLY when afk is active. When afk is off, the
1213
+ # daemon self-handles and stays quiet; firstmate drives the normal always-on
1214
+ # watcher triage. Escalations buffer and survive for the next catch-up flush.
1215
+ afk_active "$state" || { log "inject deferred: afk inactive"; return 1; }
1216
+ # (2) Single-line digest: collapse any embedded newlines so submission via
1217
+ # send-keys + Enter is unambiguous regardless of how the TUI composer treats
1218
+ # them. Then use the canonical typed envelope so downstream consumers retain
1219
+ # the exact away-supervisor kind without interpreting this payload's prose.
1220
+ msg=$(_collapse_newlines "$msg")
1221
+ fm_operational_input_encode away-supervisor "$msg" encoded || return 1
1222
+ msg=$encoded
1223
+ target="${FM_SUPERVISOR_TARGET:-$FM_SUPERVISOR_TARGET_DEFAULT}"
1224
+ # BACKEND-AWARE (previously a raw `tmux display-message` pane-exists probe):
1225
+ # dispatches through bin/fm-backend.sh so a herdr supervisor pane is checked
1226
+ # via the herdr adapter instead of always assuming tmux. Falls back to tmux
1227
+ # when unset (sourced/test contexts that never ran fm_super_main's startup
1228
+ # discovery), matching this function's pre-existing default assumption.
1229
+ backend="${FM_SUPERVISOR_BACKEND:-tmux}"
1230
+ fm_backend_target_exists "$backend" "$target" || return 1
1231
+ # (3) Busy-guard: never inject into an in-use supervisor pane.
1232
+ if pane_is_busy "$target" "$backend"; then
1233
+ log "inject deferred: supervisor pane busy (agent mid-turn)"
1234
+ return 1
1235
+ fi
1236
+ # b) Composer-guard: inject ONLY into a confirmed-empty GENUINE agent
1237
+ # composer. The shared classifier (fm_backend_composer_state ->
1238
+ # fm_composer_classify_content, bin/fm-composer-lib.sh) reports 'pending'
1239
+ # for real unsubmitted text (a human's half-typed line, or a swallowed
1240
+ # prior injection) and 'unknown' for a bare dead-shell prompt (the agent
1241
+ # exited to its login shell) or an unreadable pane. Neither is a safe
1242
+ # target - typing the escalation into a shell could execute it - so defer
1243
+ # on anything that is not affirmatively 'empty'. A deferred escalation
1244
+ # stays buffered for the next cycle or the catch-up flush.
1245
+ composer=$(fm_backend_composer_state "$backend" "$target" 2>/dev/null)
1246
+ if [ "$composer" != empty ]; then
1247
+ log "inject deferred: supervisor composer not confirmed-empty (state=${composer:-unknown}: pending input, dead-shell prompt, or unreadable pane)"
1248
+ return 1
1249
+ fi
1250
+ # (4) Type the digest ONCE, then submit with Enter (retry Enter only, never
1251
+ # retype) via the shared submit primitive. Success = the backend confirms
1252
+ # submit. An unconfirmed/unknown pane does NOT count as delivered, so the
1253
+ # buffer is preserved (strict) rather than cleared.
1254
+ # Dispatches through fm_backend_send_text_submit (bin/fm-backend.sh): for
1255
+ # backend=tmux this calls fm_backend_tmux_send_text_submit, a verbatim
1256
+ # re-export of fm_tmux_submit_core - byte-identical to calling it directly.
1257
+ retries=${FM_INJECT_CONFIRM_RETRIES:-$INJECT_CONFIRM_RETRIES_DEFAULT}
1258
+ sleep_s=${FM_INJECT_CONFIRM_SLEEP:-$INJECT_CONFIRM_SLEEP_DEFAULT}
1259
+ verdict=$(fm_backend_send_text_submit "$backend" "$target" "$msg" "$retries" "$sleep_s" "$sleep_s")
1260
+ if [ "$verdict" = empty ]; then
1261
+ return 0 # Backend confirmed the submit.
1262
+ fi
1263
+ log "inject failed: submit unconfirmed after $retries retries (verdict=$verdict, text may be in composer)"
1264
+ return 1
1265
+ }
1266
+
1267
+ # --- INJECT_SKIP prefix match (literal prefixes, no regex) ------------------
1268
+ should_force_self() { # <reason>
1269
+ local reason=$1 skip="${FM_INJECT_SKIP:-$INJECT_SKIP_DEFAULT}" prefix
1270
+ [ -n "$skip" ] || return 1
1271
+ local -a prefixes
1272
+ IFS='|' read -ra prefixes <<<"$skip"
1273
+ for prefix in "${prefixes[@]}"; do
1274
+ [ -n "$prefix" ] || continue
1275
+ [ "$reason" != "${reason#"$prefix"}" ] && return 0
1276
+ done
1277
+ return 1
1278
+ }
1279
+
1280
+ # A real watcher WAKE reason starts with one of these prefixes. Anything else on
1281
+ # the watcher child's stdout (e.g. "watcher: already running" on a singleton-lock
1282
+ # collision, reachable if the daemon was SIGKILL'd and its orphaned watcher child
1283
+ # still holds the #29 singleton lock) is a STATUS line, not a wake: handling it
1284
+ # as an unknown wake would flood the escalation buffer and restart the child with
1285
+ # no crash backoff. The main loop treats a non-wake line as idle (log + sleep +
1286
+ # continue), so a singleton collision cannot hot-loop escalations.
1287
+ is_wake_reason() { # <reason>
1288
+ local reason=$1
1289
+ case "$reason" in
1290
+ signal:*|stale:*|check:*|heartbeat|heartbeat:*) return 0 ;;
1291
+ esac
1292
+ return 1
1293
+ }
1294
+
1295
+ # --- dispatch one wake reason to self-handle or escalate --------------------
1296
+ # Side effects: logging, marker records, escalation buffer appends.
1297
+ handle_wake() { # <reason> <state>
1298
+ local reason=$1 state=$2 decision action distilled task last stale_detail
1299
+ local capture="$state/.subsuper-classified-end.$$" span_record='' span_rc='' endpoint ident rest sig marker
1300
+ local kind="" arg="" classification_failed=0 span_failure_repeat=0
1301
+ : > "$capture" || return 1
1302
+ if should_force_self "$reason"; then
1303
+ log "wake force-self (FM_INJECT_SKIP): $reason"
1304
+ rm -f "$capture"
1305
+ return
1306
+ fi
1307
+ case "$reason" in
1308
+ signal:*) kind=signal; arg="${reason#signal: }"
1309
+ decision=$(FM_STATUS_SPAN_ENDPOINT_FILE="$capture" classify_signal "$arg" "$state") ;;
1310
+ stale:*) kind=stale; arg="${reason#stale: }"; stale_detail="${arg#"$arg"}"
1311
+ case "$arg" in *" ("*) stale_detail="${arg#*" ("}"; arg="${arg%% \(*}" ;; esac
1312
+ task=$(window_to_task "$arg" "$state")
1313
+ if [ -n "$task" ]; then
1314
+ span_record=$(status_span_first_actionable_record "$state/$task.status" \
1315
+ "$(status_seen_offset "$state" "$task")")
1316
+ span_rc=$?
1317
+ case "$span_rc" in
1318
+ 0|1)
1319
+ if [ -n "$span_record" ]; then endpoint=${span_record%%$'\t'*}; rest=${span_record#*$'\t'}; ident=${rest%%$'\t'*}; printf '%s\t%s\t%s\n' "$task" "$endpoint" "$ident" > "$capture"; fi
1320
+ ;;
1321
+ *)
1322
+ sig=$(status_observed_signature "$state/$task.status")
1323
+ marker=$(_seen_status_path "$state" "$task")
1324
+ if status_presentation_marker_reported_matches "$marker" "$sig"; then
1325
+ span_failure_repeat=1
1326
+ else
1327
+ printf 'ERROR\t%s\t%s\n' "$task" "$sig" > "$capture"
1328
+ fi
1329
+ ;;
1330
+ esac
1331
+ else
1332
+ span_rc=2
1333
+ printf 'ERROR\t%s\n' "$arg" > "$capture"
1334
+ fi
1335
+ if [ "$span_failure_repeat" -eq 1 ]; then
1336
+ decision="self|unreadable status span already reported for $task"
1337
+ else
1338
+ decision=$(classify_stale "$arg" "$state" "$span_record" "$span_rc")
1339
+ fi
1340
+ # An enriched wedge reason carries the watcher's own escalation count
1341
+ # and its "do not re-absorb on the run-step/pane state alone" demand,
1342
+ # so it outranks this daemon's cheaper status-log absorption - EXCEPT
1343
+ # under a current declared wait. A `pause` verdict is not run-step or
1344
+ # pane state at all: it is the crew's own declaration that this pane
1345
+ # waits by design, which is the one question the wedge timer cannot
1346
+ # answer for itself. Overriding it escalated healthy declared waits
1347
+ # once per STALE_ESCALATE_SECS for as long as the wait lasted.
1348
+ # Housekeeping (2b) then owns the re-surface, so the wait is still
1349
+ # bounded - by one recheck per PAUSE_RESURFACE_SECS instead.
1350
+ case "${decision%%|*}" in
1351
+ pause) : ;;
1352
+ *) case "$stale_detail" in
1353
+ idle\ *s,\ possible\ wedge,\ escalation\ *)
1354
+ last=$(last_status_line "$state/$task.status")
1355
+ status_is_paused_or_captain_held "$last" \
1356
+ || decision="escalate|${reason#stale: }"
1357
+ ;;
1358
+ esac ;;
1359
+ esac ;;
1360
+ check:*) decision=$(classify_check "$reason") ;;
1361
+ heartbeat|heartbeat:*) decision=$(classify_heartbeat) ;;
1362
+ *) decision=$(classify_unknown "$reason") ;;
1363
+ esac
1364
+ action=${decision%%|*}
1365
+ distilled=${decision#*|}
1366
+ [ "$kind" = signal ] && sync_pause_markers_from_signal "$state" "$arg"
1367
+ if [ "$kind" = stale ] && [ "$action" = escalate ]; then
1368
+ task=$(window_to_task "$arg" "$state")
1369
+ last=$(last_status_line "$state/$task.status")
1370
+ reconcile_pause_tracking "$arg" "$state" "$last"
1371
+ fi
1372
+ case "$action" in
1373
+ escalate)
1374
+ log "escalate: $reason -> $distilled"
1375
+ if escalate_add "$state" "$distilled"; then
1376
+ # A terminal-stale escalate must not leave a persistence marker behind, or
1377
+ # housekeeping re-escalates the same pane as a false wedge later.
1378
+ [ "$kind" = "stale" ] && stale_marker_remove "$arg" "$state"
1379
+ mark_escalated_seen "$state" "$capture" || classification_failed=1
1380
+ [ "${FM_ESCALATE_BATCH_SECS:-$ESCALATE_BATCH_SECS_DEFAULT}" -le 0 ] && { escalate_flush "$state" || true; }
1381
+ else
1382
+ classification_failed=1
1383
+ fi
1384
+ ;;
1385
+ pause)
1386
+ # Declared wait, an external-wait pause or a verified captain-held transfer:
1387
+ # record a pause marker (long re-surface cadence in housekeeping) and drop any
1388
+ # wedge stale marker, so a pane that transitioned working->declared-wait is not
1389
+ # still wedge-aged. Only stale produces this action.
1390
+ if [ "$kind" = "stale" ]; then
1391
+ stale_marker_remove "$arg" "$state"
1392
+ pause_marker_record "$arg" "$state"
1393
+ fi
1394
+ log "self-handle (paused): $reason -> $distilled"
1395
+ ;;
1396
+ *)
1397
+ # Transient (non-terminal) stale: record/refresh the wedge marker so
1398
+ # housekeeping can age it, and drop any pause marker (a crew that left its
1399
+ # pause reverts to normal wedge aging). The persistence recheck, not this
1400
+ # wake, escalates a wedge.
1401
+ if [ "$kind" = "stale" ]; then
1402
+ task=$(window_to_task "$arg" "$state")
1403
+ last=$(last_status_line "$state/$task.status")
1404
+ # Clear wedge aging only for terminal (or legacy free-text) captain lines.
1405
+ # Nonterminal progress verbs keep possible-wedge markers even if free text
1406
+ # once looked captain-relevant or was written into a seen marker.
1407
+ _clear_wedge=0
1408
+ if [ -n "$last" ] && status_is_captain_relevant "$last"; then
1409
+ if status_is_terminal_verb "$last"; then
1410
+ _clear_wedge=1
1411
+ else
1412
+ case "$(status_line_verb "$last")" in
1413
+ working|resolved|captain-held) _clear_wedge=0 ;;
1414
+ *) _clear_wedge=1 ;;
1415
+ esac
1416
+ fi
1417
+ fi
1418
+ if [ "$_clear_wedge" = 1 ]; then
1419
+ stale_marker_remove "$arg" "$state"
1420
+ else
1421
+ pause_marker_remove "$arg" "$state"
1422
+ stale_marker_record "$arg" "$state"
1423
+ fi
1424
+ fi
1425
+ log "self-handle: $reason -> $distilled"
1426
+ ;;
1427
+ esac
1428
+ if [ "$action" = self ] && { [ "$kind" = signal ] || [ "$kind" = stale ]; }; then
1429
+ mark_escalated_seen "$state" "$capture" || classification_failed=1
1430
+ fi
1431
+ rm -f "$capture"
1432
+ [ "$classification_failed" -eq 0 ]
1433
+ }
1434
+
1435
+ handle_durable_wakes() { # <watcher-reason> <state>
1436
+ local fallback_reason=$1 state=$2 out err tab epoch sequence kind key payload rest
1437
+ local handled=0 failed=0 ack_through ack_generation
1438
+ out=$(mktemp "$state/.subsuper-wake-drain.XXXXXX") || return 1
1439
+ err=$(mktemp "$state/.subsuper-wake-drain.XXXXXX") || { rm -f "$out"; return 1; }
1440
+ if ! "$FM_DAEMON_DIR/fm-wake-drain.sh" > "$out" 2> "$err"; then
1441
+ cat "$err" >&2
1442
+ rm -f "$out" "$err"
1443
+ return 1
1444
+ fi
1445
+
1446
+ tab=$(printf '\t')
1447
+ while IFS="$tab" read -r epoch sequence kind key payload rest; do
1448
+ case "$epoch" in ''|*[!0-9]*) continue ;; esac
1449
+ case "$sequence" in ''|*[!0-9]*) continue ;; esac
1450
+ case "$kind" in signal|stale|check|heartbeat) ;; *) continue ;; esac
1451
+ handle_wake "$payload" "$state" || failed=1
1452
+ handled=$((handled + 1))
1453
+ done < "$out"
1454
+ if [ "$handled" -eq 0 ]; then handle_wake "$fallback_reason" "$state" || failed=1; fi
1455
+
1456
+ ack_through=$(sed -n 's/^WAKE_ACK_REQUIRED:.*--ack-through \([0-9][0-9]*\) --recovery-generation [A-Za-z0-9._-][A-Za-z0-9._-]*$/\1/p' "$err" | tail -1)
1457
+ ack_generation=$(sed -n 's/^WAKE_ACK_REQUIRED:.*--ack-through [0-9][0-9]* --recovery-generation \([A-Za-z0-9._-][A-Za-z0-9._-]*\)$/\1/p' "$err" | tail -1)
1458
+ grep -v '^WAKE_ACK_REQUIRED:' "$err" >&2 || true
1459
+ rm -f "$out" "$err"
1460
+ if [ "$failed" -ne 0 ]; then
1461
+ log "wake classification failed; retaining durable wakes"
1462
+ return 1
1463
+ fi
1464
+ if [ -z "$ack_through" ] || [ -z "$ack_generation" ]; then
1465
+ log "wake drain omitted its generation-bound acknowledgement; retaining durable wakes"
1466
+ return 1
1467
+ fi
1468
+ "$FM_DAEMON_DIR/fm-wake-drain.sh" --ack-through "$ack_through" \
1469
+ --recovery-generation "$ack_generation"
1470
+ }
1471
+
1472
+ # --- log --------------------------------------------------------------------
1473
+ # Uses LOG set by fm_super_main; harmless no-op-ish if unset (tests source fns
1474
+ # directly and pass state explicitly, so they do not call log).
1475
+ log() { [ -n "${LOG:-}" ] && printf '[%s] %s\n' "$(date '+%Y-%m-%dT%H:%M:%S%z')" "$*" >> "$LOG"; }
1476
+
1477
+ trim_log() {
1478
+ local sz tmp
1479
+ [ -n "${LOG:-}" ] || return 0
1480
+ sz=$(wc -c < "$LOG" 2>/dev/null) || return 0
1481
+ [ "$sz" -ge "${FM_LOG_MAX_BYTES:-$LOG_MAX_BYTES_DEFAULT}" ] || return 0
1482
+ tmp=$(mktemp "${TMPDIR:-/tmp}/fm-daemon-log.XXXXXX") || return 0
1483
+ tail -n "${FM_LOG_KEEP_LINES:-$LOG_KEEP_LINES_DEFAULT}" "$LOG" >"$tmp" 2>/dev/null && mv -f "$tmp" "$LOG"
1484
+ }
1485
+
1486
+ # ============================================================================
1487
+ # Everything below runs only when the script is EXECUTED, not sourced. The pure
1488
+ # classifiers above are sourceable for unit tests (tests/fm-daemon.test.sh).
1489
+ # ============================================================================
1490
+
1491
+ fm_super_main() {
1492
+ local STATE
1493
+ STATE="$(_state_root)"
1494
+ mkdir -p "$STATE"
1495
+
1496
+ # Source the portable lock helpers (works on macOS where flock is absent).
1497
+ # Export FM_STATE_OVERRIDE so the lib resolves the same state dir.
1498
+ # shellcheck source=bin/fm-wake-lib.sh
1499
+ FM_STATE_OVERRIDE="$STATE" . "$FM_DAEMON_DIR/fm-wake-lib.sh"
1500
+
1501
+ local WATCH="$FM_DAEMON_DIR/fm-watch.sh"
1502
+ local LOG="$STATE/.supervise-daemon.log"
1503
+ local WATCH_ERR="$STATE/.supervise-daemon.watcher.err"
1504
+ local LOCK="$STATE/.supervise-daemon.lock"
1505
+ local PIDFILE="$STATE/.supervise-daemon.pid"
1506
+ local INJECT_FAIL_SLEEP=${FM_INJECT_FAIL_SLEEP:-$INJECT_FAIL_SLEEP_DEFAULT}
1507
+ local CRASH_THRESHOLD=${FM_CRASH_THRESHOLD:-$CRASH_THRESHOLD_DEFAULT}
1508
+ local CRASH_WINDOW=${FM_CRASH_WINDOW:-$CRASH_WINDOW_DEFAULT}
1509
+ local CRASH_BACKOFF=${FM_CRASH_BACKOFF:-$CRASH_BACKOFF_DEFAULT}
1510
+ local CRASH_NORMAL_SLEEP=${FM_CRASH_NORMAL_SLEEP:-$CRASH_NORMAL_SLEEP_DEFAULT}
1511
+
1512
+ [ -x "$WATCH" ] || { echo "error: watcher not found or not executable: $WATCH" >&2; exit 1; }
1513
+
1514
+ # --- single instance (portable lock, no flock dependency) ------------------
1515
+ if ! fm_lock_try_acquire "$LOCK"; then
1516
+ if [ -n "${FM_LOCK_HELD_PID:-}" ]; then
1517
+ echo "error: another fm-supervise-daemon is already running (pid $FM_LOCK_HELD_PID, lock $LOCK held)" >&2
1518
+ else
1519
+ echo "error: another fm-supervise-daemon is already running (lock $LOCK held)" >&2
1520
+ fi
1521
+ exit 1
1522
+ fi
1523
+ echo "$$" > "$PIDFILE"
1524
+ fm_pid_identity "${BASHPID:-$$}" > "$LOCK/pid-identity" 2>/dev/null || true
1525
+
1526
+ # --- auto-discover the supervisor BACKEND (tmux vs herdr) first -----------
1527
+ # Priority: FM_SUPERVISOR_BACKEND override > $TMUX_PANE (tmux) > $HERDR_ENV=1
1528
+ # (herdr) > tmux fallback. Resolved before the target below, since target
1529
+ # discovery composes a herdr "<session>:<pane-id>" string using the same
1530
+ # $HERDR_PANE_ID/$HERDR_SESSION markers this checks. Exporting the result
1531
+ # into FM_SUPERVISOR_BACKEND makes inject_msg/pane_is_busy/pane_input_pending
1532
+ # (which read that env var) dispatch through the right backend without an
1533
+ # extra global thread-through.
1534
+ local discovered_backend backend_source
1535
+ backend_source="FM_SUPERVISOR_BACKEND"
1536
+ if [ -z "${FM_SUPERVISOR_BACKEND:-}" ]; then
1537
+ if [ -n "${TMUX_PANE:-}" ]; then
1538
+ backend_source="TMUX_PANE"
1539
+ elif [ "${HERDR_ENV:-}" = "1" ] && [ -n "${HERDR_PANE_ID:-}" ]; then
1540
+ backend_source="HERDR_ENV"
1541
+ else
1542
+ backend_source="FALLBACK($FM_SUPERVISOR_BACKEND_DEFAULT)"
1543
+ fi
1544
+ fi
1545
+ discovered_backend=$(discover_supervisor_backend) || true
1546
+ FM_SUPERVISOR_BACKEND="$discovered_backend"
1547
+ local BACKEND="$FM_SUPERVISOR_BACKEND"
1548
+
1549
+ # --- refuse an unsupported supervisor backend loudly, before ever trying a
1550
+ # tmux/herdr-specific call against it (zellij, orca, and cmux have no verified
1551
+ # composer/busy primitives wired up for this daemon yet - AGENTS.md section 4
1552
+ # harness-verification discipline). This is the clear refusal the task calls
1553
+ # for, instead of a confusing "does not resolve to a tmux pane" error.
1554
+ if ! fm_backend_list_contains "$FM_SUPERVISOR_SUPPORTED_BACKENDS" "$BACKEND"; then
1555
+ echo "error: away-mode daemon does not support supervisor backend '$BACKEND' yet (supported: $FM_SUPERVISOR_SUPPORTED_BACKENDS); set FM_SUPERVISOR_BACKEND=tmux|herdr and FM_SUPERVISOR_TARGET to run firstmate's own pane under a supported backend" >&2
1556
+ log "startup failed: unsupported supervisor backend '$BACKEND' (source=$backend_source)"
1557
+ fm_lock_release "$LOCK" 2>/dev/null || true
1558
+ rm -f "$PIDFILE" 2>/dev/null || true
1559
+ exit 1
1560
+ fi
1561
+
1562
+ # --- auto-discover the supervisor target (the pane running firstmate) -----
1563
+ # Priority: FM_SUPERVISOR_TARGET override > $TMUX_PANE (tmux; inherited from
1564
+ # the pane that launched the daemon, normally firstmate's own) >
1565
+ # $HERDR_PANE_ID (herdr, composed into "<session>:<pane-id>") > firstmate:0
1566
+ # fallback. Exporting the result into FM_SUPERVISOR_TARGET makes inject_msg
1567
+ # (which reads that env var) use the discovered pane without an extra global.
1568
+ local discovered target_source
1569
+ target_source="FM_SUPERVISOR_TARGET"
1570
+ if [ -z "${FM_SUPERVISOR_TARGET:-}" ]; then
1571
+ if [ -n "${TMUX_PANE:-}" ]; then
1572
+ target_source="TMUX_PANE"
1573
+ elif [ "${HERDR_ENV:-}" = "1" ] && [ -n "${HERDR_PANE_ID:-}" ]; then
1574
+ target_source="HERDR_ENV(HERDR_PANE_ID)"
1575
+ else
1576
+ target_source="FALLBACK(firstmate:0)"
1577
+ fi
1578
+ fi
1579
+ if discovered=$(discover_supervisor_target); then
1580
+ : # resolved cleanly
1581
+ else
1582
+ echo "warn: could not auto-discover supervisor pane (no FM_SUPERVISOR_TARGET, TMUX_PANE, or HERDR_ENV/HERDR_PANE_ID); falling back to '$discovered' — verify this is firstmate's pane" >&2
1583
+ fi
1584
+ FM_SUPERVISOR_TARGET="$discovered"
1585
+ local TARGET="$FM_SUPERVISOR_TARGET"
1586
+
1587
+ # --- validate supervisor target at startup (a missing target is a typo) ---
1588
+ # Dispatches through bin/fm-backend.sh instead of a raw `tmux display-message`
1589
+ # probe, so a herdr supervisor pane is checked via the herdr adapter; for
1590
+ # backend=tmux this runs the exact same `tmux display-message -p -t "$TARGET"
1591
+ # '#{pane_id}'` call as before.
1592
+ if ! fm_backend_target_exists "$BACKEND" "$TARGET"; then
1593
+ echo "error: supervisor target '$TARGET' does not resolve to a $BACKEND pane; set FM_SUPERVISOR_TARGET" >&2
1594
+ log "startup failed: target '$TARGET' not found (backend=$BACKEND)"
1595
+ fm_lock_release "$LOCK" 2>/dev/null || true
1596
+ rm -f "$PIDFILE" 2>/dev/null || true
1597
+ exit 1
1598
+ fi
1599
+
1600
+ local afk_status="off"
1601
+ afk_active "$STATE" && afk_status="on"
1602
+ log "daemon starting (pid $$); target=$TARGET; target_source=$target_source; backend=$BACKEND; backend_source=$backend_source; afk=$afk_status; inject_skip='${FM_INJECT_SKIP:-$INJECT_SKIP_DEFAULT}'; stale_escalate=${FM_STALE_ESCALATE_SECS:-$STALE_ESCALATE_SECS_DEFAULT}s; batch=${FM_ESCALATE_BATCH_SECS:-$ESCALATE_BATCH_SECS_DEFAULT}s"
1603
+ migrate_watcher_pause_markers "$STATE"
1604
+
1605
+ # --- shutdown: flush buffered escalations, reap child, release lock -------
1606
+ local WATCHER_PID="" CUR_TMP=""
1607
+ cleanup() {
1608
+ trap - TERM INT
1609
+ wedge_alarm_stop_active_notifier
1610
+ escalate_flush "$STATE" 2>/dev/null || true
1611
+ if [ -n "${WATCHER_PID:-}" ]; then
1612
+ kill "$WATCHER_PID" 2>/dev/null || true
1613
+ wait "$WATCHER_PID" 2>/dev/null || true
1614
+ fi
1615
+ if [ -n "${CUR_TMP:-}" ]; then
1616
+ rm -f "$CUR_TMP" 2>/dev/null || true
1617
+ fi
1618
+ fm_lock_release "$LOCK" 2>/dev/null || true
1619
+ rm -f "$PIDFILE" 2>/dev/null || true
1620
+ log "daemon shutting down"
1621
+ exit 0
1622
+ }
1623
+ trap cleanup TERM INT
1624
+
1625
+ # --- crash-loop guard -----------------------------------------------------
1626
+ local crash_times=() backoff_secs=$CRASH_NORMAL_SLEEP
1627
+ record_crash() {
1628
+ local now t
1629
+ now=$(_now)
1630
+ local -a keep=()
1631
+ for t in "${crash_times[@]:-}"; do
1632
+ [ -n "$t" ] && [ $((now - t)) -lt "$CRASH_WINDOW" ] && keep+=("$t")
1633
+ done
1634
+ keep+=("$now")
1635
+ crash_times=("${keep[@]}")
1636
+ if [ "${#crash_times[@]}" -gt "$CRASH_THRESHOLD" ]; then
1637
+ log "ERROR: watcher crashed ${#crash_times[@]} times within ${CRASH_WINDOW}s; backing off ${CRASH_BACKOFF}s"
1638
+ crash_times=()
1639
+ backoff_secs=$CRASH_BACKOFF
1640
+ else
1641
+ backoff_secs=$CRASH_NORMAL_SLEEP
1642
+ fi
1643
+ }
1644
+
1645
+ start_watcher() {
1646
+ CUR_TMP=$(mktemp "${TMPDIR:-/tmp}/fm-watch.XXXXXX") || { log "error: mktemp failed; retrying in 5s"; sleep 5; return 1; }
1647
+ "$WATCH" >"$CUR_TMP" 2>>"$WATCH_ERR" &
1648
+ WATCHER_PID=$!
1649
+ }
1650
+
1651
+ local rc reason
1652
+ while true; do
1653
+ # --- pane-gone guard (preserved) ---------------------------------------
1654
+ # With the #29 watcher's enqueue-before-suppress, a wake is no longer
1655
+ # swallowed by running the watcher with no injection target. We still back
1656
+ # off while the pane is gone: self-handling needs no pane, but escalation
1657
+ # has nowhere to go, and firstmate itself is the consumer of escalations.
1658
+ # Catch-up signals persist in state/*.status and flow on the next run, so
1659
+ # this delays rather than loses work.
1660
+ if ! fm_backend_target_exists "$BACKEND" "$TARGET"; then
1661
+ log "warn: supervisor target '$TARGET' gone; backing off ${INJECT_FAIL_SLEEP}s, will retry"
1662
+ # Flush is pointless with no pane; preserve any buffered escalations.
1663
+ sleep "$INJECT_FAIL_SLEEP"
1664
+ continue
1665
+ fi
1666
+
1667
+ # --- (re)start watcher if it has exited --------------------------------
1668
+ if [ -z "${WATCHER_PID:-}" ] || ! kill -0 "${WATCHER_PID:-}" 2>/dev/null; then
1669
+ if [ -n "${WATCHER_PID:-}" ]; then
1670
+ # child exited: reap + classify its wake reason
1671
+ if wait "${WATCHER_PID}"; then rc=0; else rc=$?; fi
1672
+ reason=""
1673
+ if [ -n "${CUR_TMP:-}" ] && [ -e "${CUR_TMP:-}" ]; then
1674
+ reason=$(<"${CUR_TMP}")
1675
+ fi
1676
+ if [ -n "${CUR_TMP:-}" ]; then
1677
+ rm -f "${CUR_TMP}" 2>/dev/null || true
1678
+ fi
1679
+ CUR_TMP=""
1680
+ if [ "$rc" -ne 0 ] || [ -z "$reason" ]; then
1681
+ record_crash
1682
+ log "watcher exited rc=$rc reason='$reason'; restarting after ${backoff_secs}s"
1683
+ WATCHER_PID=""
1684
+ sleep "$backoff_secs"
1685
+ continue
1686
+ fi
1687
+ # Non-wake stdout (e.g. a watcher singleton-collision "already running"
1688
+ # status line) is NOT a wake: idling here prevents an escalation flood
1689
+ # and a backoff-less child restart. record_crash is intentionally
1690
+ # skipped (rc=0, this is normal idle, not a crash).
1691
+ if ! is_wake_reason "$reason"; then
1692
+ log "watcher non-wake stdout, idling: $reason"
1693
+ WATCHER_PID=""
1694
+ sleep "${HOUSEKEEPING_TICK:-$HOUSEKEEPING_TICK_DEFAULT}"
1695
+ continue
1696
+ fi
1697
+ log "wake: $reason"
1698
+ if ! handle_durable_wakes "$reason" "$STATE"; then
1699
+ log "durable wake handling was not acknowledged; restarting for recovery"
1700
+ fi
1701
+ trim_log
1702
+ fi
1703
+ start_watcher || continue
1704
+ fi
1705
+
1706
+ # --- one housekeeping tick (gated to HOUSEKEEPING_TICK), then poll -------
1707
+ # The watcher child runs on its own FM_POLL cadence internally; we only need
1708
+ # to detect its exit (the kill -0 above) promptly and run housekeeping often
1709
+ # enough that batch flushes, stale rechecks, and the catch-all scan fire on
1710
+ # cadence. Gating keeps a large fleet cheap between ticks.
1711
+ sleep 1
1712
+ if [ "$(_file_age "$STATE/.subsuper-last-housekeep")" -ge "${FM_HOUSEKEEPING_TICK:-$HOUSEKEEPING_TICK_DEFAULT}" ]; then
1713
+ _now > "$STATE/.subsuper-last-housekeep"
1714
+ housekeeping "$STATE"
1715
+ fi
1716
+ done
1717
+ }
1718
+
1719
+ # Run only when executed, not when sourced (tests source the classifiers).
1720
+ if [ "${BASH_SOURCE[0]}" = "${0}" ]; then
1721
+ fm_super_main "$@"
1722
+ else
1723
+ # Library mode: these functions were SOURCED (only tests do this - production
1724
+ # execs the daemon, see bin/fm-afk-start.sh). Make it structurally impossible
1725
+ # for a sourced context to fire a real desktop notification from the wedge
1726
+ # alarm: default the FM_WEDGE_ALARM_EXEC notifier seam to "discard" unless the
1727
+ # embedder already wired one (e.g. a recorder in tests/wake-helpers.sh). It is
1728
+ # exported so a real daemon a test later spawns inherits the safe default too.
1729
+ # The executed branch above never runs this, so production is untouched.
1730
+ : "${FM_WEDGE_ALARM_EXEC:=discard}"
1731
+ export FM_WEDGE_ALARM_EXEC
1732
+ fi