@zerwiz/ymir 0.1.3 → 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 -39
  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,898 @@
1
+ #!/usr/bin/env bash
2
+ # fm-tool-update-check.sh - report watched tooling that has an update available,
3
+ # and tooling whose update is installed but not in effect.
4
+ #
5
+ # Usage:
6
+ # fm-tool-update-check.sh [check]
7
+ # fm-tool-update-check.sh arm
8
+ # fm-tool-update-check.sh disarm
9
+ # fm-tool-update-check.sh --help
10
+ #
11
+ # `check` prints one line when something needs attention and prints nothing at
12
+ # all otherwise, so it composes with the existing watcher state-check contract
13
+ # instead of needing a schedule of its own. `arm` writes
14
+ # state/tool-updates.check.sh and binds its bytes with fm-check-register.sh, so
15
+ # the watcher dispatches it on its normal FM_CHECK_INTERVAL cadence and turns
16
+ # its one line into a `check:` wake. `disarm` removes the shim, its trust
17
+ # binding, and the report record.
18
+ #
19
+ # Two conditions are reported, and they are deliberately distinct:
20
+ #
21
+ # "<tool> update available" a newer version exists at the update source.
22
+ # "<tool> update not in effect" a newer copy is installed on this host, but
23
+ # PATH still resolves an older one.
24
+ #
25
+ # The second condition is the reason this script exists. A tool that
26
+ # self-installs into ~/.local/bin while a version manager keeps its own older
27
+ # copy earlier on PATH looks fully up to date to anything that asks only "is a
28
+ # newer version published". So PATH skew is measured, never inferred: every
29
+ # executable copy on PATH is asked for its own version, and those answers are
30
+ # compared. A directory name is never read as a version, because a version
31
+ # manager's "latest" directory can hold an older build. A copy that will not
32
+ # report a version is reported as a check failure rather than assumed current.
33
+ #
34
+ # What this script never does: it reports, and it repairs nothing. It does not
35
+ # install, update, uninstall, reorder PATH, or touch any version manager's
36
+ # configuration, and it never fetches into a watched git repository. Every git
37
+ # probe is read-only (rev-parse, symbolic-ref, ls-remote, cat-file, merge-base,
38
+ # rev-list), so a watched project is never mutated.
39
+ #
40
+ # The watched tools live in config/watched-tools.json, which is local and
41
+ # gitignored, and is never propagated to another home. Adding a tool is a config
42
+ # edit, never a code change. docs/configuration.md owns that schema.
43
+ #
44
+ # Probing costs real time, so `check` runs its probes at most once per
45
+ # FM_TOOL_UPDATE_INTERVAL (default 900, 0 disables the gate, otherwise 60..86400)
46
+ # and stays silent in between. Each probe is bounded by
47
+ # FM_TOOL_UPDATE_PROBE_SECS (default 5, valid 1..30) and a whole sweep by
48
+ # FM_TOOL_UPDATE_BUDGET_SECS (default 20, valid 1..120).
49
+ #
50
+ # The sweep has to finish inside the watcher's own per check bound, because a run
51
+ # the watcher kills prints nothing and writes no record, so it would repeat that
52
+ # silence on every poll. That coupling is enforced rather than assumed: a budget
53
+ # larger than FM_CHECK_TIMEOUT (default 30, read from this check's own
54
+ # environment because the watcher runs it as a direct child) allows is cut down
55
+ # to what fits, and the cut is reported in the report line so the operator sees
56
+ # it. A budget that cannot be read as a whole number from 1 to 120 is still
57
+ # refused outright.
58
+ #
59
+ # The report record state/.tool-updates is written only when a sweep runs to its
60
+ # end, and it carries the whole finding set the last report was made from,
61
+ # uncut, so the same pending update is reported once rather than on every poll
62
+ # while a new finding that lands past the one-line cut is still news. A sweep
63
+ # killed part way through leaves no record and is retried, instead of
64
+ # suppressing its finding.
65
+ set -u
66
+ export LC_ALL=C
67
+ # A watched git remote must never stop to ask for credentials; an unauthenticated
68
+ # probe has to fail inside its bound instead of waiting for an answer.
69
+ export GIT_TERMINAL_PROMPT=0
70
+
71
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
72
+ FM_HOME="${FM_HOME:-${FM_ROOT_OVERRIDE:-$(cd "$SCRIPT_DIR/.." && pwd)}}"
73
+ STATE="${FM_STATE_OVERRIDE:-$FM_HOME/state}"
74
+ CONFIG="${FM_CONFIG_OVERRIDE:-$FM_HOME/config}/watched-tools.json"
75
+ RECORD="$STATE/.tool-updates"
76
+ CHECK_ID=tool-updates
77
+ CHECK_SHIM="$STATE/$CHECK_ID.check.sh"
78
+ CHECK_TRUST="$STATE/$CHECK_ID.check-trust"
79
+ REGISTER_BIN="$SCRIPT_DIR/fm-check-register.sh"
80
+ RECORD_SCHEMA=fm-tool-updates-v1
81
+ # Wider than the digest default because one finding names two absolute paths and
82
+ # their two versions, and several tools can report in the same sweep.
83
+ MAX_LINE=1000
84
+
85
+ # shellcheck source=bin/fm-timeout-lib.sh
86
+ . "$SCRIPT_DIR/fm-timeout-lib.sh"
87
+ # shellcheck source=bin/fm-pr-lib.sh
88
+ . "$SCRIPT_DIR/fm-pr-lib.sh"
89
+ # shellcheck source=bin/fm-line-cap-lib.sh
90
+ . "$SCRIPT_DIR/fm-line-cap-lib.sh"
91
+ # shellcheck source=bin/fm-check-lib.sh
92
+ . "$SCRIPT_DIR/fm-check-lib.sh"
93
+
94
+ usage() {
95
+ cat <<'EOF'
96
+ Usage:
97
+ fm-tool-update-check.sh [check] report watched tools needing attention (silent when current)
98
+ fm-tool-update-check.sh arm write and register state/tool-updates.check.sh
99
+ fm-tool-update-check.sh disarm remove the check shim, its trust binding, and the record
100
+ fm-tool-update-check.sh --help print this help
101
+
102
+ Watched tools are read from config/watched-tools.json (local, gitignored).
103
+ See docs/configuration.md for the schema and docs/examples/watched-tools.json for a starting point.
104
+ EOF
105
+ }
106
+
107
+ die_usage() {
108
+ printf 'fm-tool-update-check: %s\n' "$1" >&2
109
+ usage >&2
110
+ exit 2
111
+ }
112
+
113
+ INTERVAL=${FM_TOOL_UPDATE_INTERVAL:-900}
114
+ case "$INTERVAL" in
115
+ ''|*[!0-9]*)
116
+ printf 'fm-tool-update-check: FM_TOOL_UPDATE_INTERVAL must be 0 or a whole number from 60 to 86400\n' >&2
117
+ exit 2
118
+ ;;
119
+ esac
120
+ if [ "$INTERVAL" -ne 0 ] && { [ "$INTERVAL" -lt 60 ] || [ "$INTERVAL" -gt 86400 ]; }; then
121
+ printf 'fm-tool-update-check: FM_TOOL_UPDATE_INTERVAL must be 0 or a whole number from 60 to 86400\n' >&2
122
+ exit 2
123
+ fi
124
+
125
+ PROBE_SECS=${FM_TOOL_UPDATE_PROBE_SECS:-5}
126
+ case "$PROBE_SECS" in
127
+ ''|*[!0-9]*|0)
128
+ printf 'fm-tool-update-check: FM_TOOL_UPDATE_PROBE_SECS must be a whole number from 1 to 30\n' >&2
129
+ exit 2
130
+ ;;
131
+ esac
132
+ if [ "$PROBE_SECS" -gt 30 ]; then
133
+ printf 'fm-tool-update-check: FM_TOOL_UPDATE_PROBE_SECS must be a whole number from 1 to 30\n' >&2
134
+ exit 2
135
+ fi
136
+
137
+ BUDGET_SECS=${FM_TOOL_UPDATE_BUDGET_SECS:-20}
138
+ case "$BUDGET_SECS" in
139
+ ''|*[!0-9]*|0)
140
+ printf 'fm-tool-update-check: FM_TOOL_UPDATE_BUDGET_SECS must be a whole number from 1 to 120\n' >&2
141
+ exit 2
142
+ ;;
143
+ esac
144
+ if [ "$BUDGET_SECS" -gt 120 ]; then
145
+ printf 'fm-tool-update-check: FM_TOOL_UPDATE_BUDGET_SECS must be a whole number from 1 to 120\n' >&2
146
+ exit 2
147
+ fi
148
+
149
+ # The smallest bound a probe can be given, because fm_run_timed treats a
150
+ # non-positive bound as no bound.
151
+ PROBE_MIN_SECS=1
152
+ # Both clocks here count whole seconds, so a probe can start when the arithmetic
153
+ # says a second is left while almost none of it really is, and it still gets a
154
+ # full bound.
155
+ CLOCK_ROUNDING_SECS=1
156
+ # fm_run_timed asks its runner for -k 1, so a probe that does not stop on TERM is
157
+ # only killed a second after its bound.
158
+ KILL_GRACE_SECS=1
159
+
160
+ # The watcher's per check bound, read from this check's own environment. The
161
+ # watcher runs the check as a direct child, so an operator who raised it is seen
162
+ # here too, and when it is unset both sides resolve the same default.
163
+ CHECK_TIMEOUT=${FM_CHECK_TIMEOUT:-30}
164
+ case "$CHECK_TIMEOUT" in
165
+ ''|*[!0-9]*|0) CHECK_TIMEOUT=30 ;;
166
+ esac
167
+ # The last probe of a sweep can end this far past the deadline, so that is what
168
+ # the budget has to leave the watcher's own bound.
169
+ BUDGET_MAX=$((CHECK_TIMEOUT - PROBE_MIN_SECS - CLOCK_ROUNDING_SECS - KILL_GRACE_SECS))
170
+ [ "$BUDGET_MAX" -ge 1 ] || BUDGET_MAX=1
171
+ # Cut rather than refuse. A refusal is reported once and then suppressed by the
172
+ # no-nag gate, which leaves the detector dead and quiet, and a check that goes
173
+ # silent is worse than a check that reports something awkward.
174
+ BUDGET_CUT_FROM=
175
+ if [ "$BUDGET_SECS" -gt "$BUDGET_MAX" ]; then
176
+ BUDGET_CUT_FROM=$BUDGET_SECS
177
+ BUDGET_SECS=$BUDGET_MAX
178
+ fi
179
+
180
+ # --- small helpers ----------------------------------------------------------
181
+
182
+ # The record epoch is overridable so a test can drive the cadence gate; the
183
+ # sweep budget always uses real time so a frozen epoch cannot disable it.
184
+ record_epoch_now() {
185
+ case "${FM_TOOL_UPDATE_NOW:-}" in
186
+ ''|*[!0-9]*) date +%s ;;
187
+ *) printf '%s\n' "$FM_TOOL_UPDATE_NOW" ;;
188
+ esac
189
+ }
190
+
191
+ real_epoch() { date +%s; }
192
+
193
+ FINDINGS=
194
+ DEADLINE=0
195
+ INCOMPLETE_REPORTED=0
196
+
197
+ # Each finding is flattened to a single line here, because the whole report must
198
+ # stay one line for the wake record.
199
+ emit() {
200
+ local text
201
+ text=$(printf '%s' "$1" | tr '\t\r\n' ' ')
202
+ if [ -z "$FINDINGS" ]; then
203
+ FINDINGS=$text
204
+ else
205
+ FINDINGS="$FINDINGS; $text"
206
+ fi
207
+ }
208
+
209
+ budget_exhausted() {
210
+ [ "$(real_epoch)" -ge "$DEADLINE" ]
211
+ }
212
+
213
+ # True while the sweep budget still has room for another probe. When it does not,
214
+ # it records once which tool the sweep did not finish, so a sweep that cannot
215
+ # finish says so rather than being killed by the watcher with nothing printed.
216
+ budget_allows() {
217
+ local name=$1
218
+ budget_exhausted || return 0
219
+ if [ "$INCOMPLETE_REPORTED" -eq 0 ]; then
220
+ INCOMPLETE_REPORTED=1
221
+ emit "check incomplete: the time budget ran out before $name"
222
+ fi
223
+ return 1
224
+ }
225
+
226
+ # The bound for one probe: the probe bound, cut down to whatever the sweep
227
+ # budget has left, so no probe can run past the end of the sweep. Never below
228
+ # PROBE_MIN_SECS, because fm_run_timed treats a non-positive bound as no bound.
229
+ probe_bound() {
230
+ local left
231
+ left=$((DEADLINE - $(real_epoch)))
232
+ if [ "$left" -lt "$PROBE_MIN_SECS" ]; then
233
+ printf '%s\n' "$PROBE_MIN_SECS"
234
+ elif [ "$left" -lt "$PROBE_SECS" ]; then
235
+ printf '%s\n' "$left"
236
+ else
237
+ printf '%s\n' "$PROBE_SECS"
238
+ fi
239
+ }
240
+
241
+ # First dotted number in the text, so "herdr 0.8.2" and "v1.46.0" both work.
242
+ parse_version() {
243
+ printf '%s' "$1" | grep -oE '[0-9]+(\.[0-9]+)+' | head -n 1
244
+ }
245
+
246
+ # version_newer <a> <b>: true when version a is numerically newer than b.
247
+ version_newer() {
248
+ local a=$1 b=$2 i left right
249
+ local -a ap bp
250
+ IFS=. read -r -a ap <<< "$a"
251
+ IFS=. read -r -a bp <<< "$b"
252
+ i=0
253
+ while [ "$i" -lt "${#ap[@]}" ] || [ "$i" -lt "${#bp[@]}" ]; do
254
+ left=$((10#${ap[i]:-0}))
255
+ right=$((10#${bp[i]:-0}))
256
+ if [ "$left" -gt "$right" ]; then
257
+ return 0
258
+ elif [ "$left" -lt "$right" ]; then
259
+ return 1
260
+ fi
261
+ i=$((i + 1))
262
+ done
263
+ return 1
264
+ }
265
+
266
+ commit_phrase() {
267
+ if [ "$1" = 1 ]; then
268
+ printf '1 commit\n'
269
+ else
270
+ printf '%s commits\n' "$1"
271
+ fi
272
+ }
273
+
274
+ # --- watched tool registry --------------------------------------------------
275
+
276
+ CONFIG_PROBLEM=
277
+
278
+ # jq can check that an announce_pattern is a non-empty single-line string, but
279
+ # only grep can say whether it compiles as an extended regular expression. A
280
+ # pattern grep refuses would silently disable that tool's update source, which is
281
+ # the exact failure this script exists to prevent.
282
+ announce_pattern_usable() {
283
+ local pattern=$1 status
284
+ printf '%s' '' | grep -qE -- "$pattern" 2>/dev/null
285
+ status=$?
286
+ [ "$status" -le 1 ]
287
+ }
288
+
289
+ # Deliberately separate from config_validate, and asked only by arm. Arming is a
290
+ # deliberate operator action that should fail loudly, but a sweep must not treat
291
+ # one tool's unusable pattern as a reason to stop watching every other tool: that
292
+ # would let a one character typo turn the PATH skew detector off. So `check`
293
+ # reports this per tool instead, in command_findings.
294
+ config_announce_patterns_usable() {
295
+ local name announce
296
+ while IFS=$FIELD_SEP read -r name _ _ announce _; do
297
+ [ -n "$announce" ] || continue
298
+ if ! announce_pattern_usable "$announce"; then
299
+ CONFIG_PROBLEM="tool $name announce_pattern is not a usable extended regular expression"
300
+ return 1
301
+ fi
302
+ done < <(config_records)
303
+ return 0
304
+ }
305
+
306
+ config_validate() {
307
+ local problem status
308
+ if ! command -v jq >/dev/null 2>&1; then
309
+ CONFIG_PROBLEM='jq is required to read the watched tool registry'
310
+ return 1
311
+ fi
312
+ problem=$(jq -r '
313
+ def tool_problem($t):
314
+ if ($t | type) != "object" then "every entry in tools must be an object"
315
+ elif ($t.name | type) != "string" or ($t.name | length) == 0 then "every tool needs a non-empty name"
316
+ elif ($t.name | test("^[A-Za-z0-9._+-]+$") | not) then "tool name \($t.name) may use only letters, digits, dot, underscore, plus, and dash"
317
+ elif ($t | has("command") | not) and ($t | has("git") | not) then "tool \($t.name) needs command, git, or both"
318
+ elif ($t | has("command")) and (($t.command | type) != "string" or ($t.command | test("^[A-Za-z0-9._+-]+$") | not)) then "tool \($t.name) command must be a bare executable name"
319
+ elif ($t | has("version_args")) and (($t.version_args | type) != "array" or ($t.version_args | length) == 0) then "tool \($t.name) version_args must be a non-empty array"
320
+ elif ($t | has("version_args")) and ([$t.version_args[] | select((type != "string") or (test("^[A-Za-z0-9._=+/:-]+$") | not))] | length) > 0 then "tool \($t.name) version_args must be simple flag strings without spaces"
321
+ elif ($t | has("announce_pattern")) and (($t.announce_pattern | type) != "string" or ($t.announce_pattern | length) == 0 or ($t.announce_pattern | test("[[:cntrl:]]"))) then "tool \($t.name) announce_pattern must be a non-empty single-line string"
322
+ elif ($t | has("announce_pattern")) and (($t | has("command")) | not) then "tool \($t.name) announce_pattern needs command"
323
+ elif ($t | has("announce_args")) and (($t.announce_args | type) != "array" or ($t.announce_args | length) == 0) then "tool \($t.name) announce_args must be a non-empty array"
324
+ elif ($t | has("announce_args")) and ([$t.announce_args[] | select((type != "string") or (test("^[A-Za-z0-9._=+/:-]+$") | not))] | length) > 0 then "tool \($t.name) announce_args must be simple flag strings without spaces"
325
+ elif ($t | has("announce_args")) and (($t | has("announce_pattern")) | not) then "tool \($t.name) announce_args needs announce_pattern"
326
+ elif ($t | has("git")) and (($t.git | type) != "object") then "tool \($t.name) git must be an object"
327
+ elif ($t | has("git")) and (($t.git.repo | type) != "string" or ($t.git.repo | startswith("/") | not) or ($t.git.repo | test("[[:cntrl:]]"))) then "tool \($t.name) git.repo must be an absolute path on one line"
328
+ elif ($t | has("git")) and ($t.git | has("remote")) and (($t.git.remote | type) != "string" or ($t.git.remote | test("^[A-Za-z0-9._-]+$") | not)) then "tool \($t.name) git.remote must be a simple remote name"
329
+ elif ($t | has("git")) and ($t.git | has("branch")) and (($t.git.branch | type) != "string" or ($t.git.branch | test("^[A-Za-z0-9._/-]+$") | not)) then "tool \($t.name) git.branch must be a simple branch name"
330
+ else empty
331
+ end;
332
+ def problems:
333
+ if type != "object" then ["the top level must be an object"]
334
+ elif (.tools | type) != "array" then ["tools must be an array"]
335
+ elif (.tools | length) == 0 then ["tools must list at least one tool"]
336
+ else
337
+ [.tools[] | tool_problem(.)]
338
+ + (if ([.tools[].name] | unique | length) != (.tools | length) then ["tool names must be unique"] else [] end)
339
+ end;
340
+ problems | .[0] // "ok"
341
+ ' "$CONFIG" 2>/dev/null)
342
+ status=$?
343
+ if [ "$status" -ne 0 ] || [ -z "$problem" ]; then
344
+ CONFIG_PROBLEM='the watched tool registry is not valid JSON'
345
+ return 1
346
+ fi
347
+ if [ "$problem" != ok ]; then
348
+ CONFIG_PROBLEM=$problem
349
+ return 1
350
+ fi
351
+ CONFIG_PROBLEM=
352
+ return 0
353
+ }
354
+
355
+ # One record per tool, in config order. Fields are joined with the unit
356
+ # separator rather than a tab, because tab is IFS whitespace and `read` would
357
+ # collapse the empty fields that an optional key leaves behind.
358
+ FIELD_SEP=$(printf '\037')
359
+
360
+ config_records() {
361
+ jq -r '
362
+ .tools[] | [
363
+ .name,
364
+ (.command // ""),
365
+ ((.version_args // ["--version"]) | join(" ")),
366
+ (.announce_pattern // ""),
367
+ ((.announce_args // .version_args // ["--version"]) | join(" ")),
368
+ (.git.repo // ""),
369
+ (.git.remote // "origin"),
370
+ (.git.branch // "")
371
+ ] | join("\u001f")
372
+ ' "$CONFIG" 2>/dev/null
373
+ }
374
+
375
+ # --- PATH probes ------------------------------------------------------------
376
+
377
+ # Every executable copy of <command> on PATH, in PATH order, deduplicated by
378
+ # device and inode so one copy reached through two PATH entries is not read as
379
+ # two installs.
380
+ path_hits() {
381
+ local command_name=$1 dir candidate identity seen=''
382
+ while IFS= read -r dir; do
383
+ [ -n "$dir" ] || continue
384
+ candidate="$dir/$command_name"
385
+ [ -f "$candidate" ] && [ -x "$candidate" ] || continue
386
+ identity=$(fm_pr_file_identity "$candidate" 2>/dev/null) || identity=
387
+ [ -n "$identity" ] || identity=$candidate
388
+ case " $seen " in
389
+ *" $identity "*) continue ;;
390
+ esac
391
+ seen="$seen $identity"
392
+ printf '%s\n' "$candidate"
393
+ done < <(printf '%s\n' "$PATH" | tr ':' '\n')
394
+ }
395
+
396
+ # Ask one copy for its own version. Combined output, because tools answer on
397
+ # either stream, and no-mistakes announces its update on stderr.
398
+ probe_output() {
399
+ local path=$1
400
+ shift
401
+ fm_run_timed "$(probe_bound)" "$path" "$@" 2>&1
402
+ }
403
+
404
+ command_findings() {
405
+ local name=$1 command_name=$2 args_joined=$3 announce=$4 announce_args=$5
406
+ local hit out version matched announce_out status
407
+ local resolved_path='' resolved_version='' resolved_out=''
408
+ local best_path='' best_version='' unreadable='' hits=''
409
+
410
+ # This tool's announcement source is dead if its pattern cannot be used, which
411
+ # is reported here, for this tool alone, so the rest of the sweep still runs.
412
+ if [ -n "$announce" ] && ! announce_pattern_usable "$announce"; then
413
+ emit "$name check failed: announce_pattern is not a usable extended regular expression"
414
+ announce=
415
+ fi
416
+
417
+ hits=$(path_hits "$command_name")
418
+ if [ -z "$hits" ]; then
419
+ emit "$name check failed: $command_name is not on PATH"
420
+ return 0
421
+ fi
422
+
423
+ while IFS= read -r hit; do
424
+ [ -n "$hit" ] || continue
425
+ if budget_exhausted; then
426
+ emit "$name check failed: the time budget ran out before every copy answered"
427
+ break
428
+ fi
429
+ # shellcheck disable=SC2086 # deliberate split on validated space-free tokens
430
+ out=$(probe_output "$hit" $args_joined)
431
+ version=$(parse_version "$out")
432
+ if [ -z "$resolved_path" ]; then
433
+ resolved_path=$hit
434
+ resolved_version=$version
435
+ resolved_out=$out
436
+ fi
437
+ if [ -z "$version" ]; then
438
+ [ -n "$unreadable" ] || unreadable=$hit
439
+ continue
440
+ fi
441
+ if [ -z "$best_version" ] || version_newer "$version" "$best_version"; then
442
+ best_version=$version
443
+ best_path=$hit
444
+ fi
445
+ done <<EOF
446
+ $hits
447
+ EOF
448
+
449
+ if [ -n "$announce" ] && [ -n "$resolved_path" ]; then
450
+ # A tool does not have to announce its update on the command that reports its
451
+ # version: no-mistakes prints its version for --version but announces a new
452
+ # release on its other commands. So announce_args may name a second command,
453
+ # and it is asked of the copy PATH actually resolves.
454
+ announce_out=$resolved_out
455
+ if [ "$announce_args" != "$args_joined" ]; then
456
+ if budget_exhausted; then
457
+ # The version probe's output cannot carry the announcement, so searching
458
+ # it would present a source that was never asked as a clean result.
459
+ emit "$name check failed: the time budget ran out before the update announcement was checked"
460
+ announce_out=
461
+ else
462
+ # shellcheck disable=SC2086 # deliberate split on validated space-free tokens
463
+ announce_out=$(probe_output "$resolved_path" $announce_args)
464
+ status=$?
465
+ if [ "$status" -eq 124 ]; then
466
+ # A source that was asked and never answered is not a source that had
467
+ # nothing to say. The one that answers with nothing stays silent below.
468
+ emit "$name check failed: $resolved_path did not answer when asked for its update announcement"
469
+ announce_out=
470
+ fi
471
+ fi
472
+ fi
473
+ if [ -n "$announce_out" ]; then
474
+ # Not a pipeline, so grep's own status is still readable here: a pattern
475
+ # grep cannot use is a check failure, never read as nothing to announce.
476
+ matched=$(grep -oE -- "$announce" <<< "$announce_out" 2>/dev/null)
477
+ status=$?
478
+ if [ "$status" -gt 1 ]; then
479
+ emit "$name check failed: announce_pattern is not a usable extended regular expression"
480
+ elif [ -n "$matched" ]; then
481
+ emit "$name update available: $(printf '%s\n' "$matched" | head -n 1)"
482
+ fi
483
+ fi
484
+ fi
485
+
486
+ if [ -z "$resolved_version" ]; then
487
+ # No copy was probed at all when the path is empty, and the budget report
488
+ # already covers that, so do not blame a copy that was never asked.
489
+ [ -z "$resolved_path" ] || emit "$name check failed: $resolved_path did not report a version"
490
+ return 0
491
+ fi
492
+
493
+ if [ -n "$best_version" ] && [ "$best_path" != "$resolved_path" ] \
494
+ && version_newer "$best_version" "$resolved_version"; then
495
+ emit "$name update not in effect: PATH resolves $resolved_version at $resolved_path but $best_version is installed at $best_path"
496
+ fi
497
+
498
+ if [ -n "$unreadable" ]; then
499
+ emit "$name check failed: $unreadable did not report a version"
500
+ fi
501
+ return 0
502
+ }
503
+
504
+ # --- git probes -------------------------------------------------------------
505
+
506
+ # A probe the sweep budget can no longer afford is never issued, and says so with
507
+ # a status of its own rather than a git status, so no caller can read it as an
508
+ # answer. Neither git nor the bounded runner uses this value.
509
+ GIT_PROBE_NOT_ISSUED=3
510
+
511
+ # One bounded read-only git probe. The budget check lives here rather than in the
512
+ # callers, so no probe can be issued past the sweep deadline whatever a caller
513
+ # does, and the budget only has to leave room for the one probe that was already
514
+ # running when the deadline passed.
515
+ git_probe() {
516
+ local repo=$1
517
+ shift
518
+ budget_exhausted && return "$GIT_PROBE_NOT_ISSUED"
519
+ fm_run_timed "$(probe_bound)" git -C "$repo" "$@"
520
+ }
521
+
522
+ # The single place that reads a probe status as no answer at all, so every probe
523
+ # reports an unanswered read the same way instead of taking it for the answer no.
524
+ git_probe_answered() {
525
+ local status=$1 name=$2 subject=$3 question=$4
526
+ case "$status" in
527
+ "$GIT_PROBE_NOT_ISSUED")
528
+ emit "$name check failed: the time budget ran out before $subject was asked $question"
529
+ return 1
530
+ ;;
531
+ 124)
532
+ emit "$name check failed: $subject did not answer $question"
533
+ return 1
534
+ ;;
535
+ esac
536
+ return 0
537
+ }
538
+
539
+ # Read-only throughout: nothing here writes to the watched repository. This is the
540
+ # one tool kind that issues several probes in a row, two of them over the network,
541
+ # and each of them goes through git_probe, which owns both the bound and the
542
+ # budget check, so the sweep cannot outrun its deadline here.
543
+ git_findings() {
544
+ local name=$1 repo=$2 remote=$3 branch=$4
545
+ local status remote_sha local_sha local_label count short symref
546
+
547
+ if ! command -v git >/dev/null 2>&1; then
548
+ emit "$name check failed: git is not installed"
549
+ return 0
550
+ fi
551
+ if [ ! -d "$repo" ]; then
552
+ emit "$name check failed: $repo is not a directory"
553
+ return 0
554
+ fi
555
+ budget_allows "$name" || return 0
556
+ git_probe "$repo" rev-parse --git-dir >/dev/null 2>&1
557
+ status=$?
558
+ git_probe_answered "$status" "$name" "$repo" "whether it is a git repository" || return 0
559
+ if [ "$status" -ne 0 ]; then
560
+ emit "$name check failed: $repo is not a git repository"
561
+ return 0
562
+ fi
563
+
564
+ if [ -z "$branch" ]; then
565
+ branch=$(git_probe "$repo" symbolic-ref --short "refs/remotes/$remote/HEAD" 2>/dev/null)
566
+ git_probe_answered "$?" "$name" "$repo" "which branch it records for $remote" || return 0
567
+ branch=${branch#"$remote/"}
568
+ fi
569
+ if [ -z "$branch" ]; then
570
+ # A clone made with --single-branch, or one that never ran remote set-head,
571
+ # has no local record of the remote's default branch. Ask the remote itself
572
+ # rather than reporting a check failure the operator cannot act on.
573
+ symref=$(git_probe "$repo" ls-remote --symref "$remote" HEAD 2>/dev/null)
574
+ git_probe_answered "$?" "$name" "$remote" "which branch it uses by default" || return 0
575
+ branch=$(printf '%s\n' "$symref" \
576
+ | awk '$1 == "ref:" { sub(/^refs\/heads\//, "", $2); print $2; exit }')
577
+ fi
578
+ if [ -z "$branch" ]; then
579
+ emit "$name check failed: cannot resolve the default branch of $remote in $repo"
580
+ return 0
581
+ fi
582
+
583
+ remote_sha=$(git_probe "$repo" ls-remote "$remote" "refs/heads/$branch" 2>/dev/null)
584
+ status=$?
585
+ git_probe_answered "$status" "$name" "$remote" "where $branch points" || return 0
586
+ if [ "$status" -ne 0 ]; then
587
+ # The probe itself failed, so nothing at all is known about the branch. An
588
+ # offline host and a deleted branch are different problems, and reporting a
589
+ # missing branch here would name a cause that was never established.
590
+ emit "$name check failed: $remote could not be reached or read from $repo"
591
+ return 0
592
+ fi
593
+ remote_sha=$(printf '%s\n' "$remote_sha" | awk 'NR == 1 { print $1 }')
594
+ if [ -z "$remote_sha" ]; then
595
+ emit "$name check failed: $remote has no branch $branch"
596
+ return 0
597
+ fi
598
+
599
+ # Each probe below is bounded, so a non-zero status means either the answer no
600
+ # or no answer at all. They are kept apart: reading a bound that was hit as an
601
+ # answer would report an update this check never established.
602
+ local_sha=$(git_probe "$repo" rev-parse --verify --quiet "refs/heads/$branch" 2>/dev/null)
603
+ git_probe_answered "$?" "$name" "$repo" "where $branch points" || return 0
604
+ if [ -n "$local_sha" ]; then
605
+ local_label="local $branch"
606
+ else
607
+ local_sha=$(git_probe "$repo" rev-parse --verify --quiet HEAD 2>/dev/null)
608
+ git_probe_answered "$?" "$name" "$repo" "where HEAD points" || return 0
609
+ if [ -z "$local_sha" ]; then
610
+ emit "$name check failed: $repo has no commit to compare"
611
+ return 0
612
+ fi
613
+ local_label='local HEAD'
614
+ fi
615
+
616
+ [ "$local_sha" != "$remote_sha" ] || return 0
617
+
618
+ short=$(printf '%s' "$remote_sha" | cut -c1-12)
619
+
620
+ git_probe "$repo" cat-file -e "$remote_sha^{commit}" 2>/dev/null
621
+ status=$?
622
+ git_probe_answered "$status" "$name" "$repo" "whether it already has $short" || return 0
623
+ if [ "$status" -eq 0 ]; then
624
+ # The local copy may be ahead of, or diverged from, the remote branch; only
625
+ # commits it does not have yet are an available update.
626
+ git_probe "$repo" merge-base --is-ancestor "$remote_sha" "$local_sha" 2>/dev/null
627
+ status=$?
628
+ git_probe_answered "$status" "$name" "$repo" "how its history compares with $remote/$branch" || return 0
629
+ [ "$status" -ne 0 ] || return 0
630
+ count=$(git_probe "$repo" rev-list --count "$local_sha..$remote_sha" 2>/dev/null)
631
+ git_probe_answered "$?" "$name" "$repo" "how many commits it is behind $remote/$branch" || return 0
632
+ case "$count" in
633
+ ''|*[!0-9]*|0) count= ;;
634
+ esac
635
+ if [ -n "$count" ]; then
636
+ emit "$name update available: $local_label is $(commit_phrase "$count") behind $remote/$branch"
637
+ return 0
638
+ fi
639
+ fi
640
+
641
+ emit "$name update available: $remote/$branch is at $short which this copy does not have"
642
+ return 0
643
+ }
644
+
645
+ # --- report record ----------------------------------------------------------
646
+
647
+ RECORD_EPOCH=0
648
+ RECORD_REPORTED=
649
+
650
+ record_read() {
651
+ local line first=1
652
+ RECORD_EPOCH=0
653
+ RECORD_REPORTED=
654
+ [ -f "$RECORD" ] || return 0
655
+ while IFS= read -r line; do
656
+ if [ "$first" = 1 ]; then
657
+ first=0
658
+ [ "$line" = "$RECORD_SCHEMA" ] || return 0
659
+ continue
660
+ fi
661
+ case "$line" in
662
+ epoch=*)
663
+ line=${line#epoch=}
664
+ case "$line" in
665
+ ''|*[!0-9]*) RECORD_EPOCH=0 ;;
666
+ *) RECORD_EPOCH=$line ;;
667
+ esac
668
+ ;;
669
+ reported=*) RECORD_REPORTED=${line#reported=} ;;
670
+ esac
671
+ done < "$RECORD"
672
+ return 0
673
+ }
674
+
675
+ record_write() {
676
+ local reported=$1 tmp
677
+ tmp=$(mktemp "$RECORD.XXXXXX" 2>/dev/null) || return 1
678
+ chmod 0600 "$tmp" 2>/dev/null || { rm -f -- "$tmp"; return 1; }
679
+ {
680
+ printf '%s\n' "$RECORD_SCHEMA"
681
+ printf 'epoch=%s\n' "$(record_epoch_now)"
682
+ printf 'reported=%s\n' "$reported"
683
+ } > "$tmp" || { rm -f -- "$tmp"; return 1; }
684
+ mv -f -- "$tmp" "$RECORD" || { rm -f -- "$tmp"; return 1; }
685
+ return 0
686
+ }
687
+
688
+ # --- actions ----------------------------------------------------------------
689
+
690
+ action_check() {
691
+ local name command_name args_joined announce announce_args repo remote branch
692
+ local line now
693
+
694
+ [ -f "$CONFIG" ] || return 0
695
+
696
+ record_read
697
+ now=$(record_epoch_now)
698
+ if [ "$INTERVAL" -ne 0 ] && [ "$RECORD_EPOCH" -gt 0 ] \
699
+ && [ "$now" -ge "$RECORD_EPOCH" ] && [ $((now - RECORD_EPOCH)) -lt "$INTERVAL" ]; then
700
+ return 0
701
+ fi
702
+
703
+ DEADLINE=$(($(real_epoch) + BUDGET_SECS))
704
+
705
+ if [ -n "$BUDGET_CUT_FROM" ]; then
706
+ emit "sweep budget ${BUDGET_CUT_FROM}s cut to ${BUDGET_SECS}s to stay inside the watcher check timeout of ${CHECK_TIMEOUT}s"
707
+ fi
708
+
709
+ if ! config_validate; then
710
+ emit "watched tool registry: $CONFIG_PROBLEM"
711
+ else
712
+ while IFS=$FIELD_SEP read -r name command_name args_joined announce announce_args repo remote branch; do
713
+ [ -n "$name" ] || continue
714
+ budget_allows "$name" || break
715
+ [ -z "$command_name" ] || command_findings "$name" "$command_name" "$args_joined" "$announce" "$announce_args"
716
+ [ -z "$repo" ] || git_findings "$name" "$repo" "$remote" "$branch"
717
+ done < <(config_records)
718
+ fi
719
+
720
+ line=
721
+ if [ -n "$FINDINGS" ]; then
722
+ # Capped through the shared cut so an over-long report carries the same
723
+ # visible truncation marker the digests use, instead of ending mid-finding
724
+ # as if that were all of it.
725
+ fm_cap_line_var "tool updates: $FINDINGS" "$MAX_LINE"
726
+ line=$FM_LINE_CAP_LINE
727
+ fi
728
+
729
+ # The cut line is what gets printed, but the whole finding set is what decides
730
+ # whether this is news, because a finding that lands past the cut leaves the
731
+ # printed line unchanged and would otherwise be suppressed for good.
732
+ #
733
+ # Report before recording, so a record that cannot be written costs a repeated
734
+ # report rather than a lost one.
735
+ if [ -n "$line" ] && [ "$FINDINGS" != "$RECORD_REPORTED" ]; then
736
+ printf '%s\n' "$line"
737
+ fi
738
+ record_write "$FINDINGS" || true
739
+ return 0
740
+ }
741
+
742
+ # The home is embedded already resolved, because the watcher runs the shim from
743
+ # its own working directory and a relative spelling would send the check to a
744
+ # different home, or to none at all.
745
+ shim_content() {
746
+ local home=$1
747
+ printf '%s\n' \
748
+ '#!/usr/bin/env bash' \
749
+ '# Auto-generated by fm-tool-update-check.sh - watched tool update poll shim.' \
750
+ '# The watcher validates these bytes, then dispatches the trusted check script.' \
751
+ "export FM_HOME=$(printf '%q' "$home")" \
752
+ "exec $(printf '%q' "$SCRIPT_DIR/fm-tool-update-check.sh") check"
753
+ }
754
+
755
+ # Write the shim the way this repo writes its other trusted check shim: the
756
+ # guards run before anything is written, so a symlink at the shim path is
757
+ # refused instead of followed, and the bytes arrive by rename so the watcher
758
+ # never reads a half-written shim and rejects it as unauthenticated.
759
+ SHIM_WRITE_TMP=
760
+
761
+ shim_write() {
762
+ local want=$1 device tmp
763
+ [ -d "$STATE" ] && [ ! -L "$STATE" ] || return 1
764
+ device=$(fm_pr_file_device "$STATE") || return 1
765
+ [ -n "$device" ] || return 1
766
+ fm_pr_regular_destination_on_device_or_absent "$CHECK_SHIM" "$device" || return 1
767
+ if [ -e "$CHECK_SHIM" ] && [ "$(fm_pr_file_mode "$CHECK_SHIM")" = 700 ] \
768
+ && [ "$(cat "$CHECK_SHIM" 2>/dev/null)" = "$want" ]; then
769
+ return 0
770
+ fi
771
+ tmp=$(umask 077; mktemp "$STATE/.fm-tool-updates-check.XXXXXX" 2>/dev/null) || return 1
772
+ SHIM_WRITE_TMP=$tmp
773
+ if ! printf '%s\n' "$want" > "$tmp" \
774
+ || ! chmod 0700 "$tmp" \
775
+ || ! fm_pr_private_file_valid "$tmp" 700 "$device"; then
776
+ rm -f -- "$tmp"
777
+ SHIM_WRITE_TMP=
778
+ return 1
779
+ fi
780
+ if ! fm_pr_regular_destination_on_device_or_absent "$CHECK_SHIM" "$device" \
781
+ || ! mv -f -- "$tmp" "$CHECK_SHIM"; then
782
+ rm -f -- "$tmp"
783
+ SHIM_WRITE_TMP=
784
+ return 1
785
+ fi
786
+ SHIM_WRITE_TMP=
787
+ fm_pr_private_file_valid "$CHECK_SHIM" 700 "$device"
788
+ }
789
+
790
+ # Keep a byte copy of a shim that is already in place, so a failed arm can put
791
+ # back the shim a working home was already using rather than an equivalent
792
+ # rewrite. The trust binding is over the bytes, so a rewrite would satisfy it
793
+ # too, but a home that was armed stays armed with what it had.
794
+ shim_backup() {
795
+ local device tmp
796
+ device=$(fm_pr_file_device "$STATE") || return 1
797
+ [ -n "$device" ] || return 1
798
+ tmp=$(umask 077; mktemp "$STATE/.fm-tool-updates-check.XXXXXX" 2>/dev/null) || return 1
799
+ if ! cat "$CHECK_SHIM" > "$tmp" 2>/dev/null \
800
+ || ! chmod 0700 "$tmp" \
801
+ || ! fm_pr_private_file_valid "$tmp" 700 "$device"; then
802
+ rm -f -- "$tmp"
803
+ return 1
804
+ fi
805
+ printf '%s\n' "$tmp"
806
+ }
807
+
808
+ ARM_BACKUP=
809
+
810
+ # An unregistered shim is not inert: the watcher rejects it on every cycle and
811
+ # wakes firstmate about unauthenticated state checks. So the one rule after a
812
+ # failed or interrupted arm is that the home never holds a shim without a
813
+ # matching trust binding. The shim a working home had is put back and kept only
814
+ # when it is still bound; otherwise the shim goes, so the home is plainly not
815
+ # armed and the failure is the only thing the operator has to act on.
816
+ arm_rollback() {
817
+ [ -z "$SHIM_WRITE_TMP" ] || rm -f -- "$SHIM_WRITE_TMP"
818
+ SHIM_WRITE_TMP=
819
+ if [ -n "$ARM_BACKUP" ]; then
820
+ mv -f -- "$ARM_BACKUP" "$CHECK_SHIM" 2>/dev/null || rm -f -- "$ARM_BACKUP"
821
+ ARM_BACKUP=
822
+ if fm_custom_check_registered "$STATE" "$CHECK_ID"; then
823
+ return 0
824
+ fi
825
+ fi
826
+ rm -f -- "$CHECK_SHIM"
827
+ }
828
+
829
+ # shellcheck disable=SC2329 # Registered by action_arm's signal trap.
830
+ arm_interrupted() {
831
+ arm_rollback
832
+ printf 'fm-tool-update-check: arming was interrupted, so state/%s.check.sh is not armed\n' "$CHECK_ID" >&2
833
+ exit 1
834
+ }
835
+
836
+ action_arm() {
837
+ local want home
838
+ if [ ! -f "$CONFIG" ]; then
839
+ printf 'fm-tool-update-check: no watched tool registry at %s\n' "$CONFIG" >&2
840
+ return 1
841
+ fi
842
+ if ! config_validate || ! config_announce_patterns_usable; then
843
+ printf 'fm-tool-update-check: %s (%s)\n' "$CONFIG_PROBLEM" "$CONFIG" >&2
844
+ return 1
845
+ fi
846
+ mkdir -p "$STATE" || return 1
847
+ case "$FM_HOME" in
848
+ /*) home=$FM_HOME ;;
849
+ *)
850
+ home=$(CDPATH='' cd -- "$FM_HOME" 2>/dev/null && pwd -P) || {
851
+ printf 'fm-tool-update-check: cannot resolve FM_HOME %s\n' "$FM_HOME" >&2
852
+ return 1
853
+ }
854
+ ;;
855
+ esac
856
+ want=$(shim_content "$home")
857
+ ARM_BACKUP=
858
+ if [ -f "$CHECK_SHIM" ] && [ ! -L "$CHECK_SHIM" ]; then
859
+ ARM_BACKUP=$(shim_backup) || {
860
+ printf 'fm-tool-update-check: could not save the existing %s\n' "$CHECK_SHIM" >&2
861
+ return 1
862
+ }
863
+ fi
864
+ # The shim exists unbound from the rename until the register returns, so a
865
+ # signal in that window rolls back the same way a failure does.
866
+ trap arm_interrupted HUP INT TERM
867
+ if ! shim_write "$want"; then
868
+ trap - HUP INT TERM
869
+ arm_rollback
870
+ printf 'fm-tool-update-check: could not write %s\n' "$CHECK_SHIM" >&2
871
+ return 1
872
+ fi
873
+ if ! FM_HOME="$home" "$REGISTER_BIN" "$CHECK_ID" >/dev/null; then
874
+ trap - HUP INT TERM
875
+ arm_rollback
876
+ printf 'fm-tool-update-check: could not register %s\n' "$CHECK_SHIM" >&2
877
+ return 1
878
+ fi
879
+ trap - HUP INT TERM
880
+ [ -z "$ARM_BACKUP" ] || rm -f -- "$ARM_BACKUP"
881
+ ARM_BACKUP=
882
+ printf 'armed: state/%s.check.sh\n' "$CHECK_ID"
883
+ return 0
884
+ }
885
+
886
+ action_disarm() {
887
+ rm -f -- "$CHECK_SHIM" "$CHECK_TRUST" "$RECORD"
888
+ printf 'disarmed: state/%s.check.sh\n' "$CHECK_ID"
889
+ return 0
890
+ }
891
+
892
+ case "${1:-check}" in
893
+ check) action_check ;;
894
+ arm) action_arm ;;
895
+ disarm) action_disarm ;;
896
+ -h|--help) usage ;;
897
+ *) die_usage "unknown action: $1" ;;
898
+ esac