@zerwiz/ymir 0.1.4 → 0.1.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (850) hide show
  1. package/.agents/README.md +291 -0
  2. package/.agents/agents/bragi-marketer.md +111 -0
  3. package/.agents/agents/brokk.md +352 -0
  4. package/.agents/agents/forseti-reviewer.md +93 -0
  5. package/.agents/agents/frigg-consent.md +77 -0
  6. package/.agents/agents/groa-update.md +77 -0
  7. package/.agents/agents/hnoss-designer.md +69 -0
  8. package/.agents/agents/huginn-researcher.md +99 -0
  9. package/.agents/agents/jord-projects.md +78 -0
  10. package/.agents/agents/kvasir-scout.md +91 -0
  11. package/.agents/agents/mimir-planner.md +92 -0
  12. package/.agents/agents/muninn-stow.md +77 -0
  13. package/.agents/agents/saga-bearings.md +78 -0
  14. package/.agents/agents/sindri-developer.md +114 -0
  15. package/.agents/agents/snotra-documenter.md +105 -0
  16. package/.agents/agents/syn-recovery.md +78 -0
  17. package/.agents/agents/tyr-check.md +78 -0
  18. package/.agents/agents/urdh-hold.md +77 -0
  19. package/.agents/agents/volundr-smith.md +80 -0
  20. package/.agents/agents/vor-diagnostics.md +77 -0
  21. package/.agents/assets/agents/naming.md +50 -0
  22. package/.agents/assets/agents/registry.md +93 -0
  23. package/.agents/assets/agents/runtime.md +36 -0
  24. package/.agents/assets/agents/toon-tasks-cli.md +42 -0
  25. package/.agents/assets/templates/company_entity.template.md +37 -0
  26. package/.agents/backend/backends/cmux.sh +649 -0
  27. package/.agents/backend/backends/herdr-eventwait.py +157 -0
  28. package/.agents/backend/backends/herdr-workspace-move.py +114 -0
  29. package/.agents/backend/backends/herdr.sh +3386 -0
  30. package/.agents/backend/backends/orca.sh +290 -0
  31. package/.agents/backend/backends/tmux.sh +357 -0
  32. package/.agents/backend/backends/zellij.sh +700 -0
  33. package/.agents/backend/fm-afk-launch.sh +654 -0
  34. package/.agents/backend/fm-afk-return.sh +241 -0
  35. package/.agents/backend/fm-afk-start.sh +172 -0
  36. package/.agents/backend/fm-arm-command-policy.mjs +960 -0
  37. package/.agents/backend/fm-arm-pretool-check.sh +200 -0
  38. package/.agents/backend/fm-backend-hometag-lib.sh +52 -0
  39. package/.agents/backend/fm-backend.sh +981 -0
  40. package/.agents/backend/fm-backlog-handoff.sh +909 -0
  41. package/.agents/backend/fm-backlog-receive.sh +187 -0
  42. package/.agents/backend/fm-backlog-transition-lib.sh +779 -0
  43. package/.agents/backend/fm-bearings-board.sh +204 -0
  44. package/.agents/backend/fm-bearings-snapshot.sh +546 -0
  45. package/.agents/backend/fm-bootstrap.sh +1568 -0
  46. package/.agents/backend/fm-branch-outcome.sh +203 -0
  47. package/.agents/backend/fm-branch-prompt.sh +101 -0
  48. package/.agents/backend/fm-brief.sh +452 -0
  49. package/.agents/backend/fm-busy-event.sh +234 -0
  50. package/.agents/backend/fm-busy-lib.sh +986 -0
  51. package/.agents/backend/fm-captain-hold.sh +1001 -0
  52. package/.agents/backend/fm-cd-command-policy.mjs +153 -0
  53. package/.agents/backend/fm-cd-pretool-check.sh +190 -0
  54. package/.agents/backend/fm-check-lib.sh +74 -0
  55. package/.agents/backend/fm-check-register.sh +42 -0
  56. package/.agents/backend/fm-check-unregister.sh +52 -0
  57. package/.agents/backend/fm-classify-lib.sh +1770 -0
  58. package/.agents/backend/fm-claude-stop-autoarm.sh +334 -0
  59. package/.agents/backend/fm-composer-lib.sh +1416 -0
  60. package/.agents/backend/fm-config-inherit-lib.sh +1202 -0
  61. package/.agents/backend/fm-config-push.sh +254 -0
  62. package/.agents/backend/fm-control-lib.sh +251 -0
  63. package/.agents/backend/fm-control.sh +878 -0
  64. package/.agents/backend/fm-crew-state.sh +625 -0
  65. package/.agents/backend/fm-cursor-lib.sh +243 -0
  66. package/.agents/backend/fm-decision-hold.sh +232 -0
  67. package/.agents/backend/fm-doc-audience-check.sh +269 -0
  68. package/.agents/backend/fm-dod-lib.sh +67 -0
  69. package/.agents/backend/fm-ensure-agents-md.sh +253 -0
  70. package/.agents/backend/fm-extension-launch-barrier.mjs +129 -0
  71. package/.agents/backend/fm-extension.mjs +2577 -0
  72. package/.agents/backend/fm-extension.sh +16 -0
  73. package/.agents/backend/fm-ff-lib.sh +420 -0
  74. package/.agents/backend/fm-fleet-snapshot.sh +1463 -0
  75. package/.agents/backend/fm-fleet-sync.sh +458 -0
  76. package/.agents/backend/fm-fleet-view.sh +96 -0
  77. package/.agents/backend/fm-gate-refuse-lib.sh +102 -0
  78. package/.agents/backend/fm-guard.sh +242 -0
  79. package/.agents/backend/fm-harness.sh +195 -0
  80. package/.agents/backend/fm-herdr-ci-cleanup.sh +115 -0
  81. package/.agents/backend/fm-herdr-lab.sh +346 -0
  82. package/.agents/backend/fm-herdr-session-cleanup.sh +337 -0
  83. package/.agents/backend/fm-home-seed.sh +984 -0
  84. package/.agents/backend/fm-home-summary-refresh.sh +255 -0
  85. package/.agents/backend/fm-hook-host-lib.sh +36 -0
  86. package/.agents/backend/fm-inactive-reconcile.sh +531 -0
  87. package/.agents/backend/fm-inbox.sh +399 -0
  88. package/.agents/backend/fm-install-actionlint.sh +84 -0
  89. package/.agents/backend/fm-install-herdr.sh +96 -0
  90. package/.agents/backend/fm-install-shellcheck.sh +84 -0
  91. package/.agents/backend/fm-install-treehouse.sh +95 -0
  92. package/.agents/backend/fm-kimi-turnend-hook.sh +276 -0
  93. package/.agents/backend/fm-lease-lib.sh +218 -0
  94. package/.agents/backend/fm-lease.sh +189 -0
  95. package/.agents/backend/fm-line-cap-lib.sh +51 -0
  96. package/.agents/backend/fm-lint-workflows.sh +137 -0
  97. package/.agents/backend/fm-lint.sh +591 -0
  98. package/.agents/backend/fm-lock-lib.sh +104 -0
  99. package/.agents/backend/fm-lock.sh +107 -0
  100. package/.agents/backend/fm-marker-lib.sh +12 -0
  101. package/.agents/backend/fm-merge-local.sh +74 -0
  102. package/.agents/backend/fm-merge-outcome-lib.sh +138 -0
  103. package/.agents/backend/fm-nm-run-lib.sh +124 -0
  104. package/.agents/backend/fm-on.sh +125 -0
  105. package/.agents/backend/fm-operational-input.sh +253 -0
  106. package/.agents/backend/fm-peek.sh +46 -0
  107. package/.agents/backend/fm-pending-reply-lib.sh +1409 -0
  108. package/.agents/backend/fm-pr-check.sh +135 -0
  109. package/.agents/backend/fm-pr-lib.sh +1016 -0
  110. package/.agents/backend/fm-pr-merge.sh +707 -0
  111. package/.agents/backend/fm-pr-poll.sh +110 -0
  112. package/.agents/backend/fm-primary-scope-lib.sh +33 -0
  113. package/.agents/backend/fm-procevent-extension-capture.pl +259 -0
  114. package/.agents/backend/fm-procevent-lavish.sh +628 -0
  115. package/.agents/backend/fm-procevent-lib.sh +892 -0
  116. package/.agents/backend/fm-procevent-quota.sh +290 -0
  117. package/.agents/backend/fm-procevent-remote-reply.sh +577 -0
  118. package/.agents/backend/fm-procevent-when.sh +504 -0
  119. package/.agents/backend/fm-procevent.sh +1466 -0
  120. package/.agents/backend/fm-project-mode.sh +92 -0
  121. package/.agents/backend/fm-project-origin-lib.sh +180 -0
  122. package/.agents/backend/fm-promote.sh +282 -0
  123. package/.agents/backend/fm-public-followup-emit.sh +260 -0
  124. package/.agents/backend/fm-public-followup-lib.sh +398 -0
  125. package/.agents/backend/fm-public-followup.sh +1330 -0
  126. package/.agents/backend/fm-push-transition-lib.sh +168 -0
  127. package/.agents/backend/fm-quota-axi-lib.sh +93 -0
  128. package/.agents/backend/fm-quota-choose.sh +384 -0
  129. package/.agents/backend/fm-remote-delta-read.sh +189 -0
  130. package/.agents/backend/fm-remote-doctor.sh +800 -0
  131. package/.agents/backend/fm-remote-entrypoint.sh +193 -0
  132. package/.agents/backend/fm-remote-file.sh +234 -0
  133. package/.agents/backend/fm-remote-home-provision.sh +259 -0
  134. package/.agents/backend/fm-remote-home-seed.sh +259 -0
  135. package/.agents/backend/fm-remote-inherit-push.sh +90 -0
  136. package/.agents/backend/fm-remote-inherit.sh +179 -0
  137. package/.agents/backend/fm-remote-job-lib.sh +1241 -0
  138. package/.agents/backend/fm-remote-job-reap-orphans.sh +137 -0
  139. package/.agents/backend/fm-remote-job-worker.sh +1124 -0
  140. package/.agents/backend/fm-remote-readiness-lib.sh +44 -0
  141. package/.agents/backend/fm-remote-secondmate-control.sh +340 -0
  142. package/.agents/backend/fm-review-diff.sh +158 -0
  143. package/.agents/backend/fm-secondmate-charter-lib.sh +44 -0
  144. package/.agents/backend/fm-secondmate-nudge-lib.sh +68 -0
  145. package/.agents/backend/fm-secondmate-parent-lib.sh +70 -0
  146. package/.agents/backend/fm-secondmate-reconcile.sh +371 -0
  147. package/.agents/backend/fm-secondmate-registry-lib.sh +311 -0
  148. package/.agents/backend/fm-secondmate-report.sh +88 -0
  149. package/.agents/backend/fm-send.sh +1059 -0
  150. package/.agents/backend/fm-session-lock-lib.sh +176 -0
  151. package/.agents/backend/fm-session-start.sh +970 -0
  152. package/.agents/backend/fm-sessionstart-cursor.sh +40 -0
  153. package/.agents/backend/fm-sessionstart-nudge.sh +45 -0
  154. package/.agents/backend/fm-sessionstart-run.sh +145 -0
  155. package/.agents/backend/fm-spawn.sh +3153 -0
  156. package/.agents/backend/fm-startup-memory-budget-lib.sh +224 -0
  157. package/.agents/backend/fm-startup-memory-budget.sh +94 -0
  158. package/.agents/backend/fm-startup-network.sh +642 -0
  159. package/.agents/backend/fm-stow-cascade.sh +251 -0
  160. package/.agents/backend/fm-subagent-pretool-check.sh +207 -0
  161. package/.agents/backend/fm-supervise-daemon.sh +1732 -0
  162. package/.agents/backend/fm-supervision-instructions.sh +215 -0
  163. package/.agents/backend/fm-supervision-lib.sh +90 -0
  164. package/.agents/backend/fm-supervisor-target-lib.sh +78 -0
  165. package/.agents/backend/fm-tangle-lib.sh +53 -0
  166. package/.agents/backend/fm-task-inbox-lib.sh +402 -0
  167. package/.agents/backend/fm-tasks-axi-lib.sh +123 -0
  168. package/.agents/backend/fm-teardown.sh +2922 -0
  169. package/.agents/backend/fm-test-isolation-proof.sh +588 -0
  170. package/.agents/backend/fm-test-run.sh +2191 -0
  171. package/.agents/backend/fm-timeout-lib.sh +141 -0
  172. package/.agents/backend/fm-timing-lib.sh +179 -0
  173. package/.agents/backend/fm-tmux-lib.sh +291 -0
  174. package/.agents/backend/fm-tool-update-check.sh +898 -0
  175. package/.agents/backend/fm-trace-context-lib.sh +227 -0
  176. package/.agents/backend/fm-transition-lib.sh +103 -0
  177. package/.agents/backend/fm-turnend-guard-cursor.sh +391 -0
  178. package/.agents/backend/fm-turnend-guard-grok.sh +90 -0
  179. package/.agents/backend/fm-turnend-guard.sh +432 -0
  180. package/.agents/backend/fm-update.sh +110 -0
  181. package/.agents/backend/fm-vendor-auth-probe.sh +190 -0
  182. package/.agents/backend/fm-voice-client.py +1373 -0
  183. package/.agents/backend/fm-voice-relay.py +1256 -0
  184. package/.agents/backend/fm-wake-drain.sh +592 -0
  185. package/.agents/backend/fm-wake-grant.sh +127 -0
  186. package/.agents/backend/fm-wake-lib.sh +1849 -0
  187. package/.agents/backend/fm-watch-arm.sh +602 -0
  188. package/.agents/backend/fm-watch-checkpoint.sh +109 -0
  189. package/.agents/backend/fm-watch.sh +1962 -0
  190. package/.agents/backend/fm-x-dismiss.sh +116 -0
  191. package/.agents/backend/fm-x-followup.sh +286 -0
  192. package/.agents/backend/fm-x-lib.sh +1000 -0
  193. package/.agents/backend/fm-x-link.sh +234 -0
  194. package/.agents/backend/fm-x-poll.sh +185 -0
  195. package/.agents/backend/fm-x-reply.sh +385 -0
  196. package/.agents/backend/fm_voice_frame.py +166 -0
  197. package/.agents/backend/fm_voice_records.py +574 -0
  198. package/.agents/backend/model-bridge.py +290 -0
  199. package/.agents/backend/opencode-go-bridge.py +30 -0
  200. package/.agents/bus/README.md +12 -0
  201. package/.agents/bus/messages.json +1 -0
  202. package/.agents/config/agents.machine.example.yaml +16 -0
  203. package/.agents/config/agents.yaml +50 -0
  204. package/.agents/config/agents.yaml.example +72 -0
  205. package/.agents/config/crew-dispatch.json +22 -0
  206. package/.agents/config/cron.yaml +9 -0
  207. package/.agents/config/eindri-dispatch.json +67 -0
  208. package/.agents/config/eindri-harness +1 -0
  209. package/.agents/config/startup-memory-budget +1 -0
  210. package/.agents/config/tailscale-sync.yaml +27 -0
  211. package/.agents/filebrowser/.filebrowser.json +6 -0
  212. package/.agents/filebrowser/README.md +11 -0
  213. package/.agents/gateway/README.md +12 -0
  214. package/.agents/github/README.md +15 -0
  215. package/.agents/github/webhooks/.gitkeep +0 -0
  216. package/.agents/github/workflows/.gitkeep +0 -0
  217. package/.agents/harness/opencode/plugins/lib/rodd-operational-input.js +37 -0
  218. package/.agents/harness/opencode/plugins/package.json +4 -0
  219. package/.agents/harness/opencode/plugins/saga-sessionstart.js +77 -0
  220. package/.agents/harness/opencode/plugins/syn-cd-check.js +61 -0
  221. package/.agents/harness/opencode/plugins/syn-pretool-check.js +68 -0
  222. package/.agents/harness/opencode/plugins/syn-turnend-guard.js +105 -0
  223. package/.agents/harness/opencode/plugins/syn-watch-arm.js +521 -0
  224. package/.agents/memory/README.md +46 -0
  225. package/.agents/memory/kaia.engram +0 -0
  226. package/.agents/memory/kaia.engram-shm +0 -0
  227. package/.agents/memory/kaia.engram-wal +0 -0
  228. package/.agents/memory/runes_audit.md +5 -0
  229. package/.agents/memory/well/episodes.jsonl +367 -0
  230. package/.agents/memory/well/workspace.jsonl +27 -0
  231. package/.agents/migrations/0001-hodd-layout.sh +38 -0
  232. package/.agents/migrations/0002-a2a-mcp.sh +27 -0
  233. package/.agents/migrations/0003-private-data-separation.sh +111 -0
  234. package/.agents/migrations/0004-hoard-and-realms.sh +104 -0
  235. package/.agents/sandbox/Dockerfile.utgard +19 -0
  236. package/.agents/sandbox/README.md +14 -0
  237. package/.agents/sandbox/utgard.config.json +14 -0
  238. package/.agents/skills/README.md +59 -0
  239. package/.agents/skills/bragi-marketing/README.md +3 -0
  240. package/.agents/skills/bragi-marketing/SKILL.md +82 -0
  241. package/.agents/skills/eindri-homes/README.md +3 -0
  242. package/.agents/skills/eindri-homes/SKILL.md +291 -0
  243. package/.agents/skills/eindri-homes/assets/control-plane.md +122 -0
  244. package/.agents/skills/eindri-homes/assets/dispatch.md +80 -0
  245. package/.agents/skills/frigg-consent/README.md +3 -0
  246. package/.agents/skills/frigg-consent/SKILL.md +56 -0
  247. package/.agents/skills/galdr-ymirsystem/README.md +3 -0
  248. package/.agents/skills/galdr-ymirsystem/SKILL.md +158 -0
  249. package/.agents/skills/galdr-ymirsystem/assets/README.md +74 -0
  250. package/.agents/skills/galdr-ymirsystem/assets/brokk-distro-runtime.md +390 -0
  251. package/.agents/skills/galdr-ymirsystem/assets/build-method.md +113 -0
  252. package/.agents/skills/galdr-ymirsystem/assets/build-tool-categories.md +178 -0
  253. package/.agents/skills/galdr-ymirsystem/assets/eindri-orchestration.md +518 -0
  254. package/.agents/skills/galdr-ymirsystem/assets/eindri-profiles.md +136 -0
  255. package/.agents/skills/galdr-ymirsystem/assets/harness-integration/README.md +673 -0
  256. package/.agents/skills/galdr-ymirsystem/assets/harness-integration/claude-code.md +197 -0
  257. package/.agents/skills/galdr-ymirsystem/assets/harness-integration/codex.md +191 -0
  258. package/.agents/skills/galdr-ymirsystem/assets/harness-integration/cursor.md +193 -0
  259. package/.agents/skills/galdr-ymirsystem/assets/harness-integration/opencode.md +262 -0
  260. package/.agents/skills/galdr-ymirsystem/assets/harness-integration/pi.md +295 -0
  261. package/.agents/skills/galdr-ymirsystem/assets/hlidskjalf-ui.md +496 -0
  262. package/.agents/skills/galdr-ymirsystem/assets/installation.md +571 -0
  263. package/.agents/skills/galdr-ymirsystem/assets/local-models/HOST-DATA.example.md +72 -0
  264. package/.agents/skills/galdr-ymirsystem/assets/local-models.md +274 -0
  265. package/.agents/skills/galdr-ymirsystem/assets/memory-well.md +161 -0
  266. package/.agents/skills/galdr-ymirsystem/assets/nornir-jobs.md +377 -0
  267. package/.agents/skills/galdr-ymirsystem/assets/norse-naming.md +325 -0
  268. package/.agents/skills/galdr-ymirsystem/assets/odrerir-hall.md +129 -0
  269. package/.agents/skills/galdr-ymirsystem/assets/pi-boot/einherjar-spawn.schema.json +81 -0
  270. package/.agents/skills/galdr-ymirsystem/assets/pi-boot/herdr-profile.toml +75 -0
  271. package/.agents/skills/galdr-ymirsystem/assets/pi-boot/pi-profile.yml +96 -0
  272. package/.agents/skills/galdr-ymirsystem/assets/pi-boot/supervision-tree.yml +163 -0
  273. package/.agents/skills/galdr-ymirsystem/assets/pi-boot-guide.md +58 -0
  274. package/.agents/skills/galdr-ymirsystem/assets/porting-upstream-to-norse.md +301 -0
  275. package/.agents/skills/galdr-ymirsystem/assets/principles.md +385 -0
  276. package/.agents/skills/galdr-ymirsystem/assets/reference-adoption.md +99 -0
  277. package/.agents/skills/galdr-ymirsystem/assets/registry.md +379 -0
  278. package/.agents/skills/galdr-ymirsystem/assets/runtime-compliance.md +463 -0
  279. package/.agents/skills/galdr-ymirsystem/assets/runtime-components.md +553 -0
  280. package/.agents/skills/galdr-ymirsystem/assets/smidja.md +175 -0
  281. package/.agents/skills/galdr-ymirsystem/schemas/toon-schemas.md +190 -0
  282. package/.agents/skills/galdr-ymirsystem/scripts/README.md +45 -0
  283. package/.agents/skills/galdr-ymirsystem/scripts/bench-one.sh +248 -0
  284. package/.agents/skills/galdr-ymirsystem/scripts/compliance-check.sh +360 -0
  285. package/.agents/skills/galdr-ymirsystem/scripts/gpu-sample.sh +43 -0
  286. package/.agents/skills/galdr-ymirsystem/scripts/stop-all.sh +34 -0
  287. package/.agents/skills/galdr-ymirsystem/scripts/toon-check.py +188 -0
  288. package/.agents/skills/gjallarhorn-relay/README.md +3 -0
  289. package/.agents/skills/gjallarhorn-relay/SKILL.md +283 -0
  290. package/.agents/skills/groa-update/SKILL.md +70 -0
  291. package/.agents/skills/hamr-adapters/README.md +3 -0
  292. package/.agents/skills/hamr-adapters/SKILL.md +123 -0
  293. package/.agents/skills/hamr-adapters/references/common/control-and-recovery.md +37 -0
  294. package/.agents/skills/hamr-adapters/references/common/dispatch.md +32 -0
  295. package/.agents/skills/hamr-adapters/references/common/model-and-effort.md +42 -0
  296. package/.agents/skills/hamr-adapters/references/common/primary-hooks.md +40 -0
  297. package/.agents/skills/hamr-adapters/references/harness/claude.md +55 -0
  298. package/.agents/skills/hamr-adapters/references/harness/codex.md +43 -0
  299. package/.agents/skills/hamr-adapters/references/harness/cursor.md +75 -0
  300. package/.agents/skills/hamr-adapters/references/harness/grok.md +69 -0
  301. package/.agents/skills/hamr-adapters/references/harness/kimi.md +51 -0
  302. package/.agents/skills/hamr-adapters/references/harness/muse.md +70 -0
  303. package/.agents/skills/hamr-adapters/references/harness/opencode.md +42 -0
  304. package/.agents/skills/hamr-adapters/references/harness/pi.md +56 -0
  305. package/.agents/skills/herdr-panes/README.md +3 -0
  306. package/.agents/skills/herdr-panes/SKILL.md +92 -0
  307. package/.agents/skills/herdr-panes/assets/herdr-backend.md +346 -0
  308. package/.agents/skills/herdr-panes/assets/tmux-backend.md +115 -0
  309. package/.agents/skills/hnoss-design/DESIGN.md +19 -0
  310. package/.agents/skills/hnoss-design/README.md +3 -0
  311. package/.agents/skills/hnoss-design/SKILL.md +83 -0
  312. package/.agents/skills/hvild-afk/README.md +3 -0
  313. package/.agents/skills/hvild-afk/SKILL.md +241 -0
  314. package/.agents/skills/jord-projects/README.md +3 -0
  315. package/.agents/skills/jord-projects/SKILL.md +92 -0
  316. package/.agents/skills/lifecycle/SKILL.md +56 -0
  317. package/.agents/skills/lifecycle/smoke_test.sh +52 -0
  318. package/.agents/skills/lifecycle/start.sh +21 -0
  319. package/.agents/skills/lifecycle/status.sh +35 -0
  320. package/.agents/skills/lifecycle/stop.sh +16 -0
  321. package/.agents/skills/muninn-stow/README.md +3 -0
  322. package/.agents/skills/muninn-stow/SKILL.md +309 -0
  323. package/.agents/skills/no-mistakes/SKILL.md +379 -0
  324. package/.agents/skills/nornir-schedule/README.md +3 -0
  325. package/.agents/skills/nornir-schedule/SKILL.md +25 -0
  326. package/.agents/skills/nornir-schedule/assets/events.md +137 -0
  327. package/.agents/skills/nornir-schedule/assets/quota.md +131 -0
  328. package/.agents/skills/nsr-compliance/README.md +3 -0
  329. package/.agents/skills/nsr-compliance/SKILL.md +29 -0
  330. package/.agents/skills/nsr-compliance/assets/nsr/README.md +40 -0
  331. package/.agents/skills/nsr-compliance/assets/nsr/assets/RULES/README.md +15 -0
  332. package/.agents/skills/nsr-compliance/assets/nsr/assets/RULES/git-workflow.md +18 -0
  333. package/.agents/skills/nsr-compliance/assets/nsr/assets/RULES/security.md +22 -0
  334. package/.agents/skills/nsr-compliance/assets/nsr/assets/RULES/tech-stack.md +8 -0
  335. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/SKILL.md +71 -0
  336. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/config/core_four.yaml +16 -0
  337. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/gates/check_danger.sh +102 -0
  338. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/gates/check_wiring.sh +69 -0
  339. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/gates/validate_code.sh +29 -0
  340. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/gates/verify_docs.py +66 -0
  341. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/generate.sh +109 -0
  342. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/harness/envelopes/result_envelope.json +14 -0
  343. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/harness/envelopes/task_envelope.json +20 -0
  344. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/harness/runner.py +303 -0
  345. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/telemetry/logger.py +112 -0
  346. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/templates/compliance/README.md +18 -0
  347. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/templates/compliance/config/README.md +10 -0
  348. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/templates/compliance/gates/README.md +12 -0
  349. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/templates/compliance/gates/check_env.sh +23 -0
  350. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/templates/compliance/gates/check_paths.sh +20 -0
  351. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/templates/compliance/gates/check_platform.sh +23 -0
  352. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/templates/compliance/harness/README.md +11 -0
  353. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/templates/compliance/harness/envelopes/README.md +10 -0
  354. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/templates/compliance/installer/README.md +10 -0
  355. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/templates/compliance/installer/install.sh +125 -0
  356. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/templates/compliance/telemetry/README.md +11 -0
  357. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/README.md +20 -0
  358. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/features/README.md +36 -0
  359. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/features/SKILL-template.md +14 -0
  360. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/git_ops/README.md +25 -0
  361. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/git_ops/SKILL.md +13 -0
  362. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/git_ops/create_branch.sh +6 -0
  363. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/git_ops/safe_commit.sh +9 -0
  364. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/git_ops/sync_upstream.sh +6 -0
  365. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/lifecycle/README.md +27 -0
  366. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/lifecycle/SKILL.md +15 -0
  367. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/lifecycle/smoke_test.sh +12 -0
  368. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/lifecycle/start.sh +19 -0
  369. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/lifecycle/status.sh +12 -0
  370. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/lifecycle/stop.sh +15 -0
  371. package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/README.md +18 -0
  372. package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/config/README.md +10 -0
  373. package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/config/core_four.yaml +13 -0
  374. package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/gates/README.md +12 -0
  375. package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/gates/check_danger.sh +102 -0
  376. package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/gates/check_env.sh +23 -0
  377. package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/gates/check_paths.sh +20 -0
  378. package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/gates/check_platform.sh +23 -0
  379. package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/gates/check_wiring.sh +69 -0
  380. package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/harness/README.md +11 -0
  381. package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/harness/envelopes/README.md +10 -0
  382. package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/harness/envelopes/result_envelope.json +14 -0
  383. package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/harness/envelopes/task_envelope.json +20 -0
  384. package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/harness/runner.py +303 -0
  385. package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/installer/README.md +10 -0
  386. package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/installer/install.sh +125 -0
  387. package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/telemetry/README.md +11 -0
  388. package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/telemetry/logger.py +112 -0
  389. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/BEST_PRACTICES/README.md +14 -0
  390. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/BEST_PRACTICES/api.md +17 -0
  391. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/BEST_PRACTICES/compliance-wiring.md +155 -0
  392. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/BEST_PRACTICES/frontend.md +15 -0
  393. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/CI_CD/README.md +11 -0
  394. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/CI_CD/deployment/README.md +23 -0
  395. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/CI_CD/deployment/clients/README.md +16 -0
  396. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/CI_CD/deployment/clients/template-README.md +11 -0
  397. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/CI_CD/deployment/deploy.sh +49 -0
  398. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/CI_CD/deployment/envs/README.md +12 -0
  399. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/CI_CD/deployment/envs/development.env.example +17 -0
  400. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/CI_CD/deployment/envs/production.env.example +17 -0
  401. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/CI_CD/deployment/envs/staging.env.example +17 -0
  402. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/CI_CD/deployment/tenants/README.md +11 -0
  403. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/CI_CD/deployment/tenants/template.env.example +8 -0
  404. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/DEVELOPER_SETUP/README.md +26 -0
  405. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/DEVELOPER_SETUP/agent.md +7 -0
  406. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/DEVELOPER_SETUP/developers/dev-1.md +20 -0
  407. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/DEVELOPER_SETUP/linux.md +9 -0
  408. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/DEVELOPER_SETUP/macos.md +10 -0
  409. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/DEVELOPER_SETUP/shared.md +19 -0
  410. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/DEVELOPER_SETUP/windows.md +14 -0
  411. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/HOSTING/README.md +25 -0
  412. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/HOSTING/_template.md +41 -0
  413. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/HOSTING/hosting-1.md +41 -0
  414. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/README.md +21 -0
  415. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/RUNBOOK/README.md +15 -0
  416. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/RUNBOOK/deployment.md +19 -0
  417. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/RUNBOOK/disaster-recovery.md +15 -0
  418. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/RUNBOOK/ops-tasks.md +14 -0
  419. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/features/README.md +16 -0
  420. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/project-skills.md +146 -0
  421. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/research/README.md +11 -0
  422. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/research/software-compliance-research.md +269 -0
  423. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/working-with-agents.md +260 -0
  424. package/.agents/skills/nsr-compliance/assets/nsr/assets/root/AGENTS.md +38 -0
  425. package/.agents/skills/nsr-compliance/assets/nsr/assets/root/ARCHITECTURE.md +36 -0
  426. package/.agents/skills/nsr-compliance/assets/nsr/assets/root/BEST_PRACTICES.md +27 -0
  427. package/.agents/skills/nsr-compliance/assets/nsr/assets/root/CI_CD.md +23 -0
  428. package/.agents/skills/nsr-compliance/assets/nsr/assets/root/FEATURES.md +15 -0
  429. package/.agents/skills/nsr-compliance/assets/nsr/assets/root/README.md +22 -0
  430. package/.agents/skills/nsr-compliance/assets/nsr/assets/root/RULES.md +37 -0
  431. package/.agents/skills/nsr-compliance/assets/nsr/assets/root/STRUCTURE.md +45 -0
  432. package/.agents/skills/nsr-compliance/assets/nsr/assets/root/TECH_STACK.md +28 -0
  433. package/.agents/skills/nsr-compliance/assets/nsr/scaffold-spec.md +63 -0
  434. package/.agents/skills/nsr-compliance/assets/nsr/scripts/scaffold.sh +103 -0
  435. package/.agents/skills/nsr-compliance/assets/nsr/scripts/validate.sh +52 -0
  436. package/.agents/skills/nsr-compliance/assets/nsrcompliance/SKILL.md +71 -0
  437. package/.agents/skills/nsr-compliance/assets/nsrcompliance/config/core_four.yaml +16 -0
  438. package/.agents/skills/nsr-compliance/assets/nsrcompliance/gates/check_danger.sh +102 -0
  439. package/.agents/skills/nsr-compliance/assets/nsrcompliance/gates/check_wiring.sh +69 -0
  440. package/.agents/skills/nsr-compliance/assets/nsrcompliance/gates/validate_code.sh +29 -0
  441. package/.agents/skills/nsr-compliance/assets/nsrcompliance/gates/verify_docs.py +66 -0
  442. package/.agents/skills/nsr-compliance/assets/nsrcompliance/generate.sh +109 -0
  443. package/.agents/skills/nsr-compliance/assets/nsrcompliance/harness/envelopes/result_envelope.json +14 -0
  444. package/.agents/skills/nsr-compliance/assets/nsrcompliance/harness/envelopes/task_envelope.json +20 -0
  445. package/.agents/skills/nsr-compliance/assets/nsrcompliance/harness/runner.py +303 -0
  446. package/.agents/skills/nsr-compliance/assets/nsrcompliance/telemetry/logger.py +112 -0
  447. package/.agents/skills/nsr-compliance/assets/nsrcompliance/templates/compliance/README.md +18 -0
  448. package/.agents/skills/nsr-compliance/assets/nsrcompliance/templates/compliance/config/README.md +10 -0
  449. package/.agents/skills/nsr-compliance/assets/nsrcompliance/templates/compliance/gates/README.md +12 -0
  450. package/.agents/skills/nsr-compliance/assets/nsrcompliance/templates/compliance/gates/check_env.sh +23 -0
  451. package/.agents/skills/nsr-compliance/assets/nsrcompliance/templates/compliance/gates/check_paths.sh +20 -0
  452. package/.agents/skills/nsr-compliance/assets/nsrcompliance/templates/compliance/gates/check_platform.sh +23 -0
  453. package/.agents/skills/nsr-compliance/assets/nsrcompliance/templates/compliance/harness/README.md +11 -0
  454. package/.agents/skills/nsr-compliance/assets/nsrcompliance/templates/compliance/harness/envelopes/README.md +10 -0
  455. package/.agents/skills/nsr-compliance/assets/nsrcompliance/templates/compliance/installer/README.md +10 -0
  456. package/.agents/skills/nsr-compliance/assets/nsrcompliance/templates/compliance/installer/install.sh +125 -0
  457. package/.agents/skills/nsr-compliance/assets/nsrcompliance/templates/compliance/telemetry/README.md +11 -0
  458. package/.agents/skills/pr-ops/README.md +3 -0
  459. package/.agents/skills/pr-ops/SKILL.md +109 -0
  460. package/.agents/skills/ratatoskr-a2a/README.md +3 -0
  461. package/.agents/skills/ratatoskr-a2a/SKILL.md +148 -0
  462. package/.agents/skills/rules-check-drift/SKILL.md +81 -0
  463. package/.agents/skills/saga-bearings/README.md +3 -0
  464. package/.agents/skills/saga-bearings/SKILL.md +26 -0
  465. package/.agents/skills/saga-bearings/assets/bearings.md +161 -0
  466. package/.agents/skills/saga-bearings/assets/board-template.html +731 -0
  467. package/.agents/skills/saga-bearings/assets/recap.md +56 -0
  468. package/.agents/skills/syn-recovery/README.md +3 -0
  469. package/.agents/skills/syn-recovery/SKILL.md +54 -0
  470. package/.agents/skills/tyr-check/README.md +3 -0
  471. package/.agents/skills/tyr-check/SKILL.md +218 -0
  472. package/.agents/skills/urdh-hold/README.md +3 -0
  473. package/.agents/skills/urdh-hold/SKILL.md +25 -0
  474. package/.agents/skills/urdh-hold/assets/decisions.md +15 -0
  475. package/.agents/skills/urdh-hold/assets/hold.md +54 -0
  476. package/.agents/skills/vor-diagnostics/README.md +3 -0
  477. package/.agents/skills/vor-diagnostics/SKILL.md +68 -0
  478. package/.agents/skills/ymir-host/README.md +3 -0
  479. package/.agents/skills/ymir-host/SKILL.md +34 -0
  480. package/.agents/skills/ymir-host/assets/desktop.md +36 -0
  481. package/.agents/skills/ymir-host/assets/omarchy.md +297 -0
  482. package/.agents/skills/ymir-host/assets/runbooks.md +15 -0
  483. package/.agents/skills/ymir-host/assets/thjazi.md +220 -0
  484. package/.agents/skills/ymir-host/assets/update.md +64 -0
  485. package/.agents/tests/assets/board-render-harness.mjs +117 -0
  486. package/.agents/tests/cmux-test-safety.sh +51 -0
  487. package/.agents/tests/fixtures.sh +291 -0
  488. package/.agents/tests/fm-afk-inject-e2e.test.sh +428 -0
  489. package/.agents/tests/fm-afk-inject-herdr-e2e.test.sh +536 -0
  490. package/.agents/tests/fm-afk-launch.test.sh +961 -0
  491. package/.agents/tests/fm-afk-pi-herdr-return-e2e.test.sh +297 -0
  492. package/.agents/tests/fm-afk-return.test.sh +283 -0
  493. package/.agents/tests/fm-arm-pretool-check.test.sh +482 -0
  494. package/.agents/tests/fm-ask-user-authority.test.sh +42 -0
  495. package/.agents/tests/fm-backend-autodetect-smoke.test.sh +176 -0
  496. package/.agents/tests/fm-backend-cmux-smoke.test.sh +188 -0
  497. package/.agents/tests/fm-backend-cmux.test.sh +1166 -0
  498. package/.agents/tests/fm-backend-herdr-eventwait-smoke.test.sh +136 -0
  499. package/.agents/tests/fm-backend-herdr-eventwait.test.py +106 -0
  500. package/.agents/tests/fm-backend-herdr-focus-flash-e2e.test.sh +409 -0
  501. package/.agents/tests/fm-backend-herdr-launcher-workspace-e2e.test.sh +437 -0
  502. package/.agents/tests/fm-backend-herdr-presentation-e2e.test.sh +1424 -0
  503. package/.agents/tests/fm-backend-herdr-prune-safety-e2e.test.sh +180 -0
  504. package/.agents/tests/fm-backend-herdr-respawn-idem-e2e.test.sh +182 -0
  505. package/.agents/tests/fm-backend-herdr-smoke.test.sh +355 -0
  506. package/.agents/tests/fm-backend-herdr-workspace-per-home-e2e.test.sh +251 -0
  507. package/.agents/tests/fm-backend-herdr.test.sh +4606 -0
  508. package/.agents/tests/fm-backend-orca.test.sh +1357 -0
  509. package/.agents/tests/fm-backend-tmux-smoke.test.sh +173 -0
  510. package/.agents/tests/fm-backend-zellij-smoke.test.sh +205 -0
  511. package/.agents/tests/fm-backend-zellij.test.sh +1360 -0
  512. package/.agents/tests/fm-backend.test.sh +1142 -0
  513. package/.agents/tests/fm-backlog-atomicity.test.sh +2302 -0
  514. package/.agents/tests/fm-backlog-handoff.test.sh +1360 -0
  515. package/.agents/tests/fm-bearings-board-render.test.sh +135 -0
  516. package/.agents/tests/fm-bearings-board.test.sh +414 -0
  517. package/.agents/tests/fm-bearings-snapshot.test.sh +1990 -0
  518. package/.agents/tests/fm-bootstrap-network-parallel.test.sh +316 -0
  519. package/.agents/tests/fm-bootstrap.test.sh +1178 -0
  520. package/.agents/tests/fm-branch-supervision.test.sh +552 -0
  521. package/.agents/tests/fm-brief.test.sh +785 -0
  522. package/.agents/tests/fm-busy-adapter-wiring.test.sh +324 -0
  523. package/.agents/tests/fm-busy-state.test.sh +465 -0
  524. package/.agents/tests/fm-calm-pi-extension.test.sh +3979 -0
  525. package/.agents/tests/fm-captain-hold-lifecycle.test.sh +1187 -0
  526. package/.agents/tests/fm-cd-pretool-check.test.sh +400 -0
  527. package/.agents/tests/fm-check-unregister.test.sh +198 -0
  528. package/.agents/tests/fm-classify-corr-token.test.sh +543 -0
  529. package/.agents/tests/fm-classify-decision-key.test.sh +340 -0
  530. package/.agents/tests/fm-claude-stop-autoarm-live-e2e.test.sh +164 -0
  531. package/.agents/tests/fm-claude-stop-autoarm.test.sh +1190 -0
  532. package/.agents/tests/fm-cmux-claude-composer-live-e2e.test.sh +110 -0
  533. package/.agents/tests/fm-codex-continuity-live-e2e.test.sh +55 -0
  534. package/.agents/tests/fm-composer-ghost.test.sh +716 -0
  535. package/.agents/tests/fm-composer-lib.test.sh +666 -0
  536. package/.agents/tests/fm-composer-matrix-live-e2e.test.sh +220 -0
  537. package/.agents/tests/fm-control-herdr-smoke.test.sh +149 -0
  538. package/.agents/tests/fm-control-relaunch.test.sh +1530 -0
  539. package/.agents/tests/fm-control.test.sh +910 -0
  540. package/.agents/tests/fm-crew-state.test.sh +1610 -0
  541. package/.agents/tests/fm-cursor-harness.test.sh +404 -0
  542. package/.agents/tests/fm-cursor-primary-live-e2e.test.sh +217 -0
  543. package/.agents/tests/fm-cursor-primary.test.sh +708 -0
  544. package/.agents/tests/fm-daemon.test.sh +2740 -0
  545. package/.agents/tests/fm-documentation-audiences.test.sh +141 -0
  546. package/.agents/tests/fm-ensure-agents-md.test.sh +371 -0
  547. package/.agents/tests/fm-extension-binding.test.sh +2187 -0
  548. package/.agents/tests/fm-fleet-snapshot-view.test.sh +816 -0
  549. package/.agents/tests/fm-fleet-sync.test.sh +721 -0
  550. package/.agents/tests/fm-gate-refuse.test.sh +356 -0
  551. package/.agents/tests/fm-gitignore-config.test.sh +89 -0
  552. package/.agents/tests/fm-gotmp.test.sh +230 -0
  553. package/.agents/tests/fm-grok-continuity-live-e2e.test.sh +112 -0
  554. package/.agents/tests/fm-grok-harness.test.sh +117 -0
  555. package/.agents/tests/fm-grok-stop-live-e2e.test.sh +219 -0
  556. package/.agents/tests/fm-guard-stale-banner.test.sh +709 -0
  557. package/.agents/tests/fm-harness-adapter-instructions-live-e2e.test.sh +134 -0
  558. package/.agents/tests/fm-harness-adapter-references.test.sh +30 -0
  559. package/.agents/tests/fm-harness-liveness-drift-live-e2e.test.sh +151 -0
  560. package/.agents/tests/fm-herdr-lab.test.sh +243 -0
  561. package/.agents/tests/fm-herdr-session-cleanup-e2e.test.sh +146 -0
  562. package/.agents/tests/fm-herdr-session-cleanup.test.sh +329 -0
  563. package/.agents/tests/fm-herdr-submit-confirm-live-e2e.test.sh +125 -0
  564. package/.agents/tests/fm-herdr-version-floor-live-e2e.test.sh +144 -0
  565. package/.agents/tests/fm-home-summary-refresh.test.sh +971 -0
  566. package/.agents/tests/fm-inactive-reconcile.test.sh +485 -0
  567. package/.agents/tests/fm-kimi-harness.test.sh +687 -0
  568. package/.agents/tests/fm-lint-workflows.test.sh +532 -0
  569. package/.agents/tests/fm-lint.test.sh +1023 -0
  570. package/.agents/tests/fm-muse-harness.test.sh +932 -0
  571. package/.agents/tests/fm-muse-signals-live-e2e.test.sh +205 -0
  572. package/.agents/tests/fm-no-mistakes-required.test.sh +72 -0
  573. package/.agents/tests/fm-on.test.sh +507 -0
  574. package/.agents/tests/fm-opencode-primary-live-e2e.test.sh +357 -0
  575. package/.agents/tests/fm-operational-input.test.sh +160 -0
  576. package/.agents/tests/fm-peek-remote.test.sh +110 -0
  577. package/.agents/tests/fm-pending-reply.test.sh +1288 -0
  578. package/.agents/tests/fm-pi-branch-extension.test.sh +3182 -0
  579. package/.agents/tests/fm-pi-branch-live-e2e.test.sh +527 -0
  580. package/.agents/tests/fm-pi-primary-live-e2e.test.sh +345 -0
  581. package/.agents/tests/fm-pi-primary-types.test.sh +68 -0
  582. package/.agents/tests/fm-pi-watch-extension.test.sh +2843 -0
  583. package/.agents/tests/fm-pr-check-security.test.sh +2140 -0
  584. package/.agents/tests/fm-pr-merge.test.sh +2135 -0
  585. package/.agents/tests/fm-procevent-quota.test.sh +225 -0
  586. package/.agents/tests/fm-procevent-when.test.sh +406 -0
  587. package/.agents/tests/fm-procevent.test.sh +1745 -0
  588. package/.agents/tests/fm-project-origin.test.sh +110 -0
  589. package/.agents/tests/fm-public-followup.test.sh +2272 -0
  590. package/.agents/tests/fm-quota-array-dispatch-live-e2e.test.sh +526 -0
  591. package/.agents/tests/fm-quota-choose.test.sh +626 -0
  592. package/.agents/tests/fm-remote-backlog-handoff.test.sh +453 -0
  593. package/.agents/tests/fm-remote-doctor.test.sh +625 -0
  594. package/.agents/tests/fm-remote-entrypoint.test.sh +59 -0
  595. package/.agents/tests/fm-remote-job-orphan-reap.test.sh +205 -0
  596. package/.agents/tests/fm-remote-job.test.sh +768 -0
  597. package/.agents/tests/fm-remote-reply.test.sh +520 -0
  598. package/.agents/tests/fm-remote-secondmate-lifecycle-e2e.test.sh +1256 -0
  599. package/.agents/tests/fm-remote-secondmate-parent-binding.test.sh +326 -0
  600. package/.agents/tests/fm-remote-secondmate-trace-context.test.sh +310 -0
  601. package/.agents/tests/fm-remote-transport-lanes.test.sh +431 -0
  602. package/.agents/tests/fm-review-diff.test.sh +176 -0
  603. package/.agents/tests/fm-secondmate-harness.test.sh +2600 -0
  604. package/.agents/tests/fm-secondmate-lifecycle-e2e.test.sh +259 -0
  605. package/.agents/tests/fm-secondmate-liveness.test.sh +559 -0
  606. package/.agents/tests/fm-secondmate-reconcile.test.sh +732 -0
  607. package/.agents/tests/fm-secondmate-safety.test.sh +2972 -0
  608. package/.agents/tests/fm-secondmate-sync.test.sh +913 -0
  609. package/.agents/tests/fm-send-inbox-doorbell-live-e2e.test.sh +206 -0
  610. package/.agents/tests/fm-send-inbox.test.sh +352 -0
  611. package/.agents/tests/fm-send-popup-settle.test.sh +167 -0
  612. package/.agents/tests/fm-send-remote-delivery.test.sh +796 -0
  613. package/.agents/tests/fm-send-resolve-key.test.sh +551 -0
  614. package/.agents/tests/fm-send-secondmate-marker-herdr-e2e.test.sh +187 -0
  615. package/.agents/tests/fm-send-secondmate-marker.test.sh +278 -0
  616. package/.agents/tests/fm-send-settle.test.sh +147 -0
  617. package/.agents/tests/fm-send-strict.test.sh +241 -0
  618. package/.agents/tests/fm-session-lock-ancestry.test.sh +365 -0
  619. package/.agents/tests/fm-session-start.test.sh +2515 -0
  620. package/.agents/tests/fm-sessionstart-hook-live-e2e.test.sh +631 -0
  621. package/.agents/tests/fm-sessionstart-instruction-refresh-live-e2e.test.sh +230 -0
  622. package/.agents/tests/fm-sessionstart-nudge.test.sh +1039 -0
  623. package/.agents/tests/fm-shared-captain-inheritance.test.sh +404 -0
  624. package/.agents/tests/fm-spawn-batch.test.sh +148 -0
  625. package/.agents/tests/fm-spawn-dispatch-profile.test.sh +827 -0
  626. package/.agents/tests/fm-spawn-pool-base-freshen.test.sh +440 -0
  627. package/.agents/tests/fm-spawn-worktree-settle.test.sh +147 -0
  628. package/.agents/tests/fm-startup-memory-budget.test.sh +333 -0
  629. package/.agents/tests/fm-startup-network.test.sh +712 -0
  630. package/.agents/tests/fm-stow-cascade.test.sh +370 -0
  631. package/.agents/tests/fm-subagent-pretool-check.test.sh +291 -0
  632. package/.agents/tests/fm-supervision-events.test.sh +156 -0
  633. package/.agents/tests/fm-supervision-instructions.test.sh +189 -0
  634. package/.agents/tests/fm-tangle-guard.test.sh +277 -0
  635. package/.agents/tests/fm-task-delivery.test.sh +420 -0
  636. package/.agents/tests/fm-task-inbox.test.sh +513 -0
  637. package/.agents/tests/fm-teardown-endpoint-safety.test.sh +374 -0
  638. package/.agents/tests/fm-teardown.test.sh +2665 -0
  639. package/.agents/tests/fm-test-fixture-cleanup.test.sh +172 -0
  640. package/.agents/tests/fm-test-fixtures.test.sh +132 -0
  641. package/.agents/tests/fm-test-isolation-proof.test.sh +278 -0
  642. package/.agents/tests/fm-test-run.test.sh +1196 -0
  643. package/.agents/tests/fm-tmux-agent-liveness.test.sh +354 -0
  644. package/.agents/tests/fm-tmux-submit-busy.test.sh +340 -0
  645. package/.agents/tests/fm-tool-update-check.test.sh +1044 -0
  646. package/.agents/tests/fm-trace-context-lib.test.sh +253 -0
  647. package/.agents/tests/fm-trace-context-spawn.test.sh +581 -0
  648. package/.agents/tests/fm-transition-lib.test.sh +54 -0
  649. package/.agents/tests/fm-turnend-guard.test.sh +1822 -0
  650. package/.agents/tests/fm-update.test.sh +304 -0
  651. package/.agents/tests/fm-vendor-auth-probe.test.sh +395 -0
  652. package/.agents/tests/fm-voice-relay.test.sh +4741 -0
  653. package/.agents/tests/fm-wake-daemon-lifecycle-e2e.test.sh +171 -0
  654. package/.agents/tests/fm-wake-drain-open-decisions-cursor.test.sh +356 -0
  655. package/.agents/tests/fm-wake-drain-open-decisions.test.sh +226 -0
  656. package/.agents/tests/fm-wake-drain-unread-status.test.sh +379 -0
  657. package/.agents/tests/fm-wake-queue.test.sh +1224 -0
  658. package/.agents/tests/fm-watch-arm.test.sh +821 -0
  659. package/.agents/tests/fm-watch-checkpoint.test.sh +87 -0
  660. package/.agents/tests/fm-watch-recovery-loop.test.sh +223 -0
  661. package/.agents/tests/fm-watch-triage.test.sh +3889 -0
  662. package/.agents/tests/fm-watcher-lock.test.sh +1133 -0
  663. package/.agents/tests/fm-x-mode.test.sh +3042 -0
  664. package/.agents/tests/gleipnir-guard-pretool-check.test.sh +51 -0
  665. package/.agents/tests/gleipnir-machine-lock.test.sh +130 -0
  666. package/.agents/tests/herdr-test-safety.sh +42 -0
  667. package/.agents/tests/lib.sh +315 -0
  668. package/.agents/tests/remote-herdr-fixture.sh +125 -0
  669. package/.agents/tests/secondmate-helpers.sh +193 -0
  670. package/.agents/tests/smoke.test.sh +93 -0
  671. package/.agents/tests/wake-helpers.sh +338 -0
  672. package/.agents/tests/zellij-test-safety.sh +60 -0
  673. package/.agents/tools/README.md +10 -0
  674. package/.agents/tools/bin/tasks-cli +267 -0
  675. package/.agents/tools/tasks-cli.ts +216 -0
  676. package/AGENTS.md +454 -0
  677. package/CHANGELOG.md +881 -0
  678. package/LICENSE +1 -1
  679. package/NOTICE +63 -0
  680. package/README.md +113 -59
  681. package/RULES/01-domains.md +51 -0
  682. package/RULES/02-agents.md +78 -0
  683. package/RULES/03-houses.md +61 -0
  684. package/RULES/04-hoard.md +65 -0
  685. package/RULES/05-platforms.md +67 -0
  686. package/RULES/06-append-only.md +58 -0
  687. package/RULES/07-config.md +45 -0
  688. package/RULES/08-delivery-gate.md +75 -0
  689. package/RULES/README.md +20 -0
  690. package/bin/README.md +107 -0
  691. package/bin/a2a-mcp.sh +98 -0
  692. package/bin/a2a-serve.py +103 -0
  693. package/bin/a2a-serve.sh +8 -0
  694. package/bin/a2a-talk.sh +50 -0
  695. package/bin/a2abridge-ensure.sh +108 -0
  696. package/bin/agent-run.sh +43 -0
  697. package/bin/agents-config.sh +233 -0
  698. package/bin/apodex-smoke-test.sh +92 -0
  699. package/bin/bifrost-bridge.sh +112 -0
  700. package/bin/bifrost-ingress.sh +77 -0
  701. package/bin/bootstrap-macos.sh +107 -0
  702. package/bin/bootstrap-windows.ps1 +112 -0
  703. package/bin/branch-guard.sh +64 -0
  704. package/bin/brokk +73 -0
  705. package/bin/brokk-classify-lib.sh +1770 -0
  706. package/bin/brokk-lease-lib.sh +222 -0
  707. package/bin/brokk-lease.sh +193 -0
  708. package/bin/brokk-lint.sh +65 -0
  709. package/bin/brokk-send.sh +38 -0
  710. package/bin/brokk-update.sh +9 -0
  711. package/bin/brokk-wake-grant.sh +127 -0
  712. package/bin/brokk-wake-lib.sh +1849 -0
  713. package/bin/changelog-guard.sh +165 -0
  714. package/bin/crash-sense.sh +160 -0
  715. package/bin/desktop-place.sh +224 -0
  716. package/bin/docs-guard.sh +39 -0
  717. package/bin/editor-place.sh +207 -0
  718. package/bin/eindri-acclaim.sh +34 -0
  719. package/bin/eindri-control.sh +28 -0
  720. package/bin/eindri-role.sh +83 -0
  721. package/bin/eindri-seen.sh +41 -0
  722. package/bin/eindri-send.sh +15 -0
  723. package/bin/eindri-start.sh +85 -0
  724. package/bin/eindri-watch.sh +57 -0
  725. package/bin/einherjar-spawn.sh +576 -0
  726. package/bin/eir-doctor.sh +112 -0
  727. package/bin/erindi-brief.sh +271 -0
  728. package/bin/fleet-apply.sh +68 -0
  729. package/bin/github-deploy.sh +81 -0
  730. package/bin/gjallarhorn-expose.sh +102 -0
  731. package/bin/gjallarhorn-notify.sh +58 -0
  732. package/bin/gjallarhorn-purge.sh +54 -0
  733. package/bin/gjallarhorn-tunnel.sh +72 -0
  734. package/bin/gleipnir-lock-lib.sh +284 -0
  735. package/bin/groa-update.sh +122 -0
  736. package/bin/hall-snapshot.sh +92 -0
  737. package/bin/hamr-harness.sh +246 -0
  738. package/bin/heimdall-ssh-keys.sh +374 -0
  739. package/bin/herdr-agents.py +44 -0
  740. package/bin/herdr-ensure.sh +104 -0
  741. package/bin/herdr-run.sh +431 -0
  742. package/bin/hermes-ensure.sh +88 -0
  743. package/bin/hoard-lib.sh +25 -0
  744. package/bin/hodd.sh +78 -0
  745. package/bin/host-sense.sh +199 -0
  746. package/bin/huginn-research-worker.sh +108 -0
  747. package/bin/local-model-lock.sh +51 -0
  748. package/bin/mimir-bridge.py +226 -0
  749. package/bin/mimir-bridge.sh +87 -0
  750. package/bin/mimir-ingest.sh +131 -0
  751. package/bin/mimir.sh +113 -0
  752. package/bin/mjollnir-webhook.sh +59 -0
  753. package/bin/mjollnir.sh +85 -0
  754. package/bin/model-hardware.sh +82 -0
  755. package/bin/model-resolve.sh +147 -0
  756. package/bin/models-detect.sh +178 -0
  757. package/bin/nornir-cron-start.sh +162 -0
  758. package/bin/nornir-job-daily-briefing.sh +191 -0
  759. package/bin/nornir-job-git-sync.sh +152 -0
  760. package/bin/nornir-job-memory-housekeeping.sh +114 -0
  761. package/bin/nornir-job-observer.sh +166 -0
  762. package/bin/nornir-job-skillopt-sleep.sh +76 -0
  763. package/bin/omarchy-hook-install.sh +60 -0
  764. package/bin/omarchy-install.sh +148 -0
  765. package/bin/omarchy-plugins.sh +174 -0
  766. package/bin/omarchy-sense.sh +149 -0
  767. package/bin/perm-guard.sh +30 -0
  768. package/bin/pi-agent.sh +68 -0
  769. package/bin/pi-ensure.sh +77 -0
  770. package/bin/pi-local.sh +37 -0
  771. package/bin/pi-seat.sh +63 -0
  772. package/bin/prereq-ensure.sh +166 -0
  773. package/bin/private-guard.sh +38 -0
  774. package/bin/project-git.sh +55 -0
  775. package/bin/public-guard.sh +70 -0
  776. package/bin/ratatoskr.sh +49 -0
  777. package/bin/realm-lib.sh +32 -0
  778. package/bin/repo-scrub.sh +62 -0
  779. package/bin/rodd-operational-input.sh +250 -0
  780. package/bin/runes-append.sh +198 -0
  781. package/bin/saga-session-start.sh +172 -0
  782. package/bin/saga-sessionstart-run.sh +79 -0
  783. package/bin/saga-wake-drain.sh +41 -0
  784. package/bin/secret-guard.sh +63 -0
  785. package/bin/sessrumnir-ensure.sh +100 -0
  786. package/bin/sessrumnir-sync.sh +237 -0
  787. package/bin/sessrumnir.sh +91 -0
  788. package/bin/skillopt-setup.sh +76 -0
  789. package/bin/skuld-branch-outcome.sh +203 -0
  790. package/bin/skuld-branch-prompt.sh +101 -0
  791. package/bin/smidja-bootstrap.sh +90 -0
  792. package/bin/smidja-observe.sh +86 -0
  793. package/bin/syn-arm-pretool-check.sh +57 -0
  794. package/bin/syn-asset-pretool-check.sh +76 -0
  795. package/bin/syn-cd-pretool-check.sh +28 -0
  796. package/bin/syn-guard-pretool-check.sh +83 -0
  797. package/bin/syn-turnend-guard.sh +36 -0
  798. package/bin/syn-watch-arm.sh +99 -0
  799. package/bin/tailscale-sync.sh +190 -0
  800. package/bin/telegram-bot.sh +69 -0
  801. package/bin/toolchain.sh +74 -0
  802. package/bin/utgard.sh +111 -0
  803. package/bin/valhalla.sh +115 -0
  804. package/bin/valknut-load.sh +260 -0
  805. package/bin/vor-crew-state.sh +124 -0
  806. package/bin/wedge-notify.sh +47 -0
  807. package/bin/workspace-provision.sh +80 -0
  808. package/bin/workspace-rag.sh +150 -0
  809. package/bin/wyrd-db.sh +47 -0
  810. package/bin/yggdrasil.sh +152 -0
  811. package/bin/ymir-edit.sh +145 -0
  812. package/bin/ymir-install.sh +641 -0
  813. package/bin/ymir-invite.sh +93 -0
  814. package/bin/ymir-isolation.sh +88 -0
  815. package/bin/ymir-migrate.sh +61 -0
  816. package/bin/ymir-platform.sh +348 -0
  817. package/bin/ymir-say.sh +97 -0
  818. package/bin/ymir-setup-auth.sh +108 -0
  819. package/bin/ymir-validate.sh +173 -0
  820. package/bin/ymir.js +74 -16
  821. package/docs/Architecture.md +339 -0
  822. package/docs/design.md +351 -0
  823. package/docs/installations/README.md +63 -0
  824. package/docs/installations/ymir-home.md +93 -0
  825. package/docs/integration.md +105 -0
  826. package/docs/lore.md +1056 -0
  827. package/docs/research/agent-harnesses.md +290 -0
  828. package/docs/runbooks/README.md +19 -0
  829. package/docs/runbooks/a2a.md +91 -0
  830. package/docs/runbooks/agent-permissions.md +99 -0
  831. package/docs/runbooks/agents.md +57 -0
  832. package/docs/runbooks/models.md +69 -0
  833. package/docs/runbooks/realm-onboarding.md +60 -0
  834. package/docs/runbooks/secrets-and-hoard.md +60 -0
  835. package/docs/runbooks/updates-and-migrations.md +110 -0
  836. package/docs/runbooks/worktrees-isolation.md +72 -0
  837. package/docs/session-start.md +105 -0
  838. package/docs/skillopt-integration.md +321 -0
  839. package/docs/sync-system.md +66 -0
  840. package/docs/workspaces.md +104 -0
  841. package/docs/ymir-rut.md +260 -0
  842. package/package.json +44 -14
  843. package/packaging/build.sh +74 -0
  844. package/packaging/macos/Ymir Installer.command +43 -0
  845. package/packaging/windows/ymir-setup.nsi +74 -0
  846. package/scripts/README.md +22 -0
  847. package/scripts/electron.sh +210 -0
  848. package/scripts/start.sh +167 -0
  849. package/scripts/stop.sh +113 -0
  850. package/install.sh +0 -35
@@ -0,0 +1,1044 @@
1
+ #!/usr/bin/env bash
2
+ # Tests for fm-tool-update-check.sh, the watched tooling update report.
3
+ #
4
+ # The case that matters most is PATH skew: a tool that has already installed its
5
+ # newer copy, while PATH still resolves an older one. On 2026-08-20 that exact
6
+ # shape broke this fleet. Herdr self-installed 0.8.2 into ~/.local/bin, a version
7
+ # manager kept its own 0.8.0 earlier on PATH inside a directory named "latest",
8
+ # and every Herdr command then failed on a protocol mismatch. A check that only
9
+ # asks "is a newer version published" reports everything up to date and misses
10
+ # it, so test_path_skew_is_reported_from_every_copy reproduces the incident and
11
+ # asserts the report names the older copy PATH resolves AND the newer copy that
12
+ # is already installed. A single `command -v` lookup cannot know the second
13
+ # version, so that assertion fails against any build without real per-copy
14
+ # probing.
15
+ #
16
+ # The fixtures use a synthetic command name and their own temporary PATH
17
+ # directories, so no case ever probes, launches, or otherwise touches a tool
18
+ # actually installed on this host.
19
+ set -u
20
+
21
+ # shellcheck source=tests/lib.sh
22
+ . "$(dirname "${BASH_SOURCE[0]}")/lib.sh"
23
+
24
+ CHECK="$ROOT/bin/fm-tool-update-check.sh"
25
+ CHECKPOINT="$ROOT/bin/fm-watch-checkpoint.sh"
26
+ TMP_ROOT=$(fm_test_tmproot fm-tool-update-check)
27
+
28
+ # Exported here, at the top level, because git_fixture runs inside a command
29
+ # substitution and an export from that subshell never reaches the cases, which
30
+ # make fixture commits of their own. A host with no git identity configured
31
+ # would otherwise fail those commits and leave the fixture in a shape the case
32
+ # did not ask for.
33
+ fm_git_identity fmtest fmtest@example.invalid
34
+
35
+ # The incident's tool, under a name that cannot exist on this host.
36
+ TOOL=herdr-fixture
37
+
38
+ make_home() {
39
+ local name=$1 home
40
+ home="$TMP_ROOT/$name"
41
+ mkdir -p "$home/state" "$home/config"
42
+ printf '%s\n' "$home"
43
+ }
44
+
45
+ # make_copy <dir> <command> <version-output>: an executable copy that answers
46
+ # --version with the given text and nothing else.
47
+ make_copy() {
48
+ local dir=$1 command_name=$2 text=$3
49
+ mkdir -p "$dir"
50
+ cat > "$dir/$command_name" <<SH
51
+ #!/usr/bin/env bash
52
+ printf '%s\n' '$text'
53
+ SH
54
+ chmod 0755 "$dir/$command_name"
55
+ }
56
+
57
+ # make_slow_copy <dir> <command> <seconds>: a copy that answers far too late, so a
58
+ # case can spend the sweep budget the way a hung tool would.
59
+ make_slow_copy() {
60
+ local dir=$1 command_name=$2 seconds=$3
61
+ mkdir -p "$dir"
62
+ cat > "$dir/$command_name" <<SH
63
+ #!/usr/bin/env bash
64
+ sleep $seconds
65
+ printf 'herdr 0.8.2\n'
66
+ SH
67
+ chmod 0755 "$dir/$command_name"
68
+ }
69
+
70
+ # make_counting_copy <dir> <command> <version-output> <log>: the same copy, which
71
+ # also appends one line to <log> every time it runs, so a case can assert how
72
+ # many times the check actually probed it.
73
+ make_counting_copy() {
74
+ local dir=$1 command_name=$2 text=$3 log=$4
75
+ mkdir -p "$dir"
76
+ cat > "$dir/$command_name" <<SH
77
+ #!/usr/bin/env bash
78
+ printf 'probed\n' >> '$log'
79
+ printf '%s\n' '$text'
80
+ SH
81
+ chmod 0755 "$dir/$command_name"
82
+ }
83
+
84
+ write_config() {
85
+ local home=$1
86
+ shift
87
+ printf '%s\n' "$*" > "$home/config/watched-tools.json"
88
+ }
89
+
90
+ # The fixture directories first, then the ambient PATH, which the check needs
91
+ # because it shells out to ordinary tools such as jq, git, date, grep, stat, and
92
+ # timeout. What keeps a tool installed on this host out of a fixture is the
93
+ # synthetic command name, not this PATH, so every case watches a command name
94
+ # that cannot exist here.
95
+ fixture_path() {
96
+ printf '%s:%s\n' "$1" "$PATH"
97
+ }
98
+
99
+ # The watcher check timeout is pinned to its documented default here, because the
100
+ # sweep budget is cut to fit it and an operator's ambient value would otherwise
101
+ # add a report line to cases that mean to be silent. The one case that exercises
102
+ # the cut sets its own value.
103
+ run_check() {
104
+ local home=$1 path=$2 out=$3
105
+ shift 3
106
+ local status=0
107
+ env FM_CHECK_TIMEOUT=30 "$@" FM_HOME="$home" PATH="$path" FM_TOOL_UPDATE_INTERVAL=0 "$CHECK" >"$out" 2>&1 || status=$?
108
+ expect_code 0 "$status" "check exit"
109
+ }
110
+
111
+ # --- the regression this script exists for ----------------------------------
112
+
113
+ test_path_skew_is_reported_from_every_copy() {
114
+ local home stale fresh out report
115
+ home=$(make_home skew)
116
+ # The stale copy sits in a directory named "latest" on purpose: the incident's
117
+ # version manager did exactly that, so a directory name is no evidence at all.
118
+ stale="$TMP_ROOT/skew/mise/installs/herdr/latest/bin"
119
+ fresh="$TMP_ROOT/skew/local/bin"
120
+ make_copy "$stale" "$TOOL" 'herdr 0.8.0'
121
+ make_copy "$fresh" "$TOOL" 'herdr 0.8.2'
122
+ write_config "$home" "{\"tools\":[{\"name\":\"herdr\",\"command\":\"$TOOL\",\"version_args\":[\"--version\"]}]}"
123
+ out="$home/out.txt"
124
+ run_check "$home" "$(fixture_path "$stale:$fresh")" "$out"
125
+ report=$(cat "$out")
126
+
127
+ assert_contains "$report" "herdr update not in effect" "PATH skew was not reported as an update that is not in effect"
128
+ # Both sides of the comparison must be named, and the newer one can only be
129
+ # known by asking a copy other than the one PATH resolves.
130
+ assert_contains "$report" "PATH resolves 0.8.0 at $stale/$TOOL" "the report does not name the older version PATH actually resolves"
131
+ assert_contains "$report" "0.8.2 is installed at $fresh/$TOOL" "the report does not name the newer installed copy, so no other PATH copy was asked for its version"
132
+ assert_not_contains "$report" "update available" "PATH skew must not be reported as a published update"
133
+ assert_contains "$report" "$(printf 'tool updates:')" "the report is missing its one-line prefix"
134
+ [ "$(wc -l < "$out" | tr -d '[:space:]')" = 1 ] || fail "the report must be exactly one line for the wake record"
135
+ pass "PATH skew is reported by asking every copy on PATH for its own version"
136
+ }
137
+
138
+ test_newest_copy_first_on_path_is_silent() {
139
+ local home stale fresh out
140
+ # Control for the case above: the same two copies, the newer one resolved
141
+ # first, must produce no report at all.
142
+ home=$(make_home no-skew)
143
+ stale="$TMP_ROOT/no-skew/mise/installs/herdr/latest/bin"
144
+ fresh="$TMP_ROOT/no-skew/local/bin"
145
+ make_copy "$stale" "$TOOL" 'herdr 0.8.0'
146
+ make_copy "$fresh" "$TOOL" 'herdr 0.8.2'
147
+ write_config "$home" "{\"tools\":[{\"name\":\"herdr\",\"command\":\"$TOOL\"}]}"
148
+ out="$home/out.txt"
149
+ run_check "$home" "$(fixture_path "$fresh:$stale")" "$out"
150
+ [ ! -s "$out" ] || fail "check reported skew when PATH already resolves the newest copy: $(cat "$out")"
151
+ pass "no report when PATH already resolves the newest installed copy"
152
+ }
153
+
154
+ test_identical_versions_are_silent() {
155
+ local home first second out
156
+ home=$(make_home same-version)
157
+ first="$TMP_ROOT/same-version/a/bin"
158
+ second="$TMP_ROOT/same-version/b/bin"
159
+ make_copy "$first" "$TOOL" 'herdr 0.8.2'
160
+ make_copy "$second" "$TOOL" 'herdr 0.8.2'
161
+ write_config "$home" "{\"tools\":[{\"name\":\"herdr\",\"command\":\"$TOOL\"}]}"
162
+ out="$home/out.txt"
163
+ run_check "$home" "$(fixture_path "$first:$second")" "$out"
164
+ [ ! -s "$out" ] || fail "two copies of the same version reported skew: $(cat "$out")"
165
+ pass "two copies of the same version are not skew"
166
+ }
167
+
168
+ test_one_copy_reached_twice_is_probed_once() {
169
+ local home dir link out log probes
170
+ # A single install reachable through two PATH entries must not read as two
171
+ # installs, or a symlinked bin directory would report skew against itself.
172
+ # Silence alone does not prove that, because two answers of the same version
173
+ # are silent too, so count the probes: the one install must be asked once.
174
+ home=$(make_home one-copy)
175
+ dir="$TMP_ROOT/one-copy/real/bin"
176
+ link="$TMP_ROOT/one-copy/linked-bin"
177
+ log="$TMP_ROOT/one-copy/probes.log"
178
+ make_counting_copy "$dir" "$TOOL" 'herdr 0.8.2' "$log"
179
+ ln -s "$dir" "$link"
180
+ write_config "$home" "{\"tools\":[{\"name\":\"herdr\",\"command\":\"$TOOL\"}]}"
181
+ out="$home/out.txt"
182
+ : > "$log"
183
+ run_check "$home" "$(fixture_path "$dir:$link")" "$out"
184
+ [ ! -s "$out" ] || fail "one copy reached through two PATH entries reported a finding: $(cat "$out")"
185
+ probes=$(wc -l < "$log" | tr -d ' ')
186
+ [ "$probes" = 1 ] || fail "one install reached through two PATH entries was probed $probes times, so the two entries were not recognized as one install"
187
+ pass "one copy reached through two PATH entries is probed once as one install"
188
+ }
189
+
190
+ test_unreadable_version_is_a_failure_not_a_pass() {
191
+ local home dir out report
192
+ # A copy that will not say what it is cannot be called current.
193
+ home=$(make_home mute)
194
+ dir="$TMP_ROOT/mute/bin"
195
+ make_copy "$dir" "$TOOL" 'no version here'
196
+ write_config "$home" "{\"tools\":[{\"name\":\"herdr\",\"command\":\"$TOOL\"}]}"
197
+ out="$home/out.txt"
198
+ run_check "$home" "$(fixture_path "$dir")" "$out"
199
+ report=$(cat "$out")
200
+ assert_contains "$report" "herdr check failed" "a copy that reports no version was treated as current"
201
+ assert_contains "$report" "$dir/$TOOL did not report a version" "the failing copy was not named"
202
+ pass "a copy that reports no version is a check failure, not a pass"
203
+ }
204
+
205
+ test_missing_command_is_reported() {
206
+ local home out
207
+ home=$(make_home absent)
208
+ write_config "$home" '{"tools":[{"name":"herdr","command":"herdr-absent-fixture"}]}'
209
+ out="$home/out.txt"
210
+ run_check "$home" "$PATH" "$out"
211
+ assert_contains "$(cat "$out")" "herdr check failed: herdr-absent-fixture is not on PATH" "a watched command missing from PATH was not reported"
212
+ pass "a watched command missing from PATH is reported"
213
+ }
214
+
215
+ # --- published updates ------------------------------------------------------
216
+
217
+ test_announced_update_is_reported_from_the_tool_itself() {
218
+ local home dir out report
219
+ # no-mistakes already announces its own update on stderr; read that rather
220
+ # than reimplementing its version lookup.
221
+ home=$(make_home announce)
222
+ dir="$TMP_ROOT/announce/bin"
223
+ mkdir -p "$dir"
224
+ cat > "$dir/no-mistakes-fixture" <<'SH'
225
+ #!/usr/bin/env bash
226
+ printf '1.46.0\n'
227
+ printf 'A new version of no-mistakes is available: v1.46.0 -> v1.47.0\n' >&2
228
+ SH
229
+ chmod 0755 "$dir/no-mistakes-fixture"
230
+ write_config "$home" '{"tools":[{"name":"no-mistakes","command":"no-mistakes-fixture","announce_pattern":"A new version of no-mistakes is available: [^ ]+ -> [^ ]+"}]}'
231
+ out="$home/out.txt"
232
+ run_check "$home" "$(fixture_path "$dir")" "$out"
233
+ report=$(cat "$out")
234
+ assert_contains "$report" "no-mistakes update available: A new version of no-mistakes is available: v1.46.0 -> v1.47.0" "the tool's own update announcement was not reported"
235
+ assert_not_contains "$report" "not in effect" "a published update must not be reported as PATH skew"
236
+ pass "a tool's own update announcement is read from its output"
237
+ }
238
+
239
+ test_announcement_is_read_from_a_second_command() {
240
+ local home dir out report quiet_home
241
+ # The real no-mistakes prints its version for --version but announces a new
242
+ # release only on its other commands, so the announcement has to be asked of a
243
+ # command of its own while the version probe keeps reporting the version.
244
+ home=$(make_home announce-args)
245
+ dir="$TMP_ROOT/announce-args/bin"
246
+ mkdir -p "$dir"
247
+ cat > "$dir/no-mistakes-fixture" <<'SH'
248
+ #!/usr/bin/env bash
249
+ if [ "${1:-}" = "--version" ]; then
250
+ printf 'no-mistakes version v1.46.0\n'
251
+ exit 0
252
+ fi
253
+ printf 'A new version of no-mistakes is available: v1.46.0 -> v1.53.0\n' >&2
254
+ printf 'Usage: no-mistakes <command>\n'
255
+ SH
256
+ chmod 0755 "$dir/no-mistakes-fixture"
257
+ out="$home/out.txt"
258
+
259
+ write_config "$home" '{"tools":[{"name":"no-mistakes","command":"no-mistakes-fixture","version_args":["--version"],"announce_args":["--help"],"announce_pattern":"A new version of no-mistakes is available: [^ ]+ -> [^ ]+"}]}'
260
+ run_check "$home" "$(fixture_path "$dir")" "$out"
261
+ report=$(cat "$out")
262
+ assert_contains "$report" "no-mistakes update available: A new version of no-mistakes is available: v1.46.0 -> v1.53.0" "the announcement was not read from the command that carries it"
263
+ assert_not_contains "$report" "check failed" "the version probe stopped reporting this copy's version"
264
+
265
+ # Control: the same tool watched without announce_args sees only the version
266
+ # probe, which never carries the announcement, so the update is missed.
267
+ quiet_home=$(make_home announce-args-control)
268
+ write_config "$quiet_home" '{"tools":[{"name":"no-mistakes","command":"no-mistakes-fixture","version_args":["--version"],"announce_pattern":"A new version of no-mistakes is available: [^ ]+ -> [^ ]+"}]}'
269
+ run_check "$quiet_home" "$(fixture_path "$dir")" "$quiet_home/out.txt"
270
+ [ ! -s "$quiet_home/out.txt" ] || fail "the control home reported without a second command, so this test proves nothing: $(cat "$quiet_home/out.txt")"
271
+ pass "an announcement carried by another command is read from that command"
272
+ }
273
+
274
+ test_unusable_announce_pattern_is_reported_not_read_as_silence() {
275
+ local home dir out status
276
+ # A pattern the search cannot use answers exactly like a tool with nothing to
277
+ # announce, which is the silently dead update source this check exists to
278
+ # prevent. It is reported as that tool's own check failure.
279
+ home=$(make_home bad-pattern)
280
+ dir="$TMP_ROOT/bad-pattern/bin"
281
+ make_copy "$dir" no-mistakes-fixture 'no-mistakes version v1.46.0'
282
+ write_config "$home" '{"tools":[{"name":"no-mistakes","command":"no-mistakes-fixture","announce_pattern":"A new version of no-mistakes is available: ([^ ]+ -> [^ ]+"}]}'
283
+ out="$home/out.txt"
284
+ run_check "$home" "$(fixture_path "$dir")" "$out"
285
+ assert_contains "$(cat "$out")" "no-mistakes check failed: announce_pattern is not a usable extended regular expression" "a pattern that cannot be used was read as nothing to announce"
286
+
287
+ # Arming is a deliberate operator action, so the same registry refuses it
288
+ # rather than arming a check with a source that can never fire.
289
+ status=0
290
+ FM_HOME="$home" "$CHECK" arm >/dev/null 2>&1 || status=$?
291
+ expect_code 1 "$status" "arm with an unusable announce_pattern exit"
292
+ assert_absent "$home/state/tool-updates.check.sh" "arm registered a check whose announcement source cannot fire"
293
+ pass "an announce_pattern that cannot be used is reported instead of read as silence"
294
+ }
295
+
296
+ test_one_broken_pattern_does_not_blind_the_rest_of_the_sweep() {
297
+ local home stale fresh dir out report
298
+ # A one character typo in one tool's pattern must not turn off the detector for
299
+ # every other tool. The PATH skew below is the whole reason this check exists,
300
+ # so it has to be reported in the same sweep as the pattern problem.
301
+ home=$(make_home pattern-blind)
302
+ stale="$TMP_ROOT/pattern-blind/mise/installs/herdr/latest/bin"
303
+ fresh="$TMP_ROOT/pattern-blind/local/bin"
304
+ dir="$TMP_ROOT/pattern-blind/announce/bin"
305
+ make_copy "$stale" "$TOOL" 'herdr 0.8.0'
306
+ make_copy "$fresh" "$TOOL" 'herdr 0.8.2'
307
+ make_copy "$dir" no-mistakes-fixture 'no-mistakes version v1.46.0'
308
+ write_config "$home" "{\"tools\":[{\"name\":\"herdr\",\"command\":\"$TOOL\"},{\"name\":\"no-mistakes\",\"command\":\"no-mistakes-fixture\",\"announce_pattern\":\"A new version of no-mistakes is available: ([^ ]+ -> [^ ]+\"}]}"
309
+ out="$home/out.txt"
310
+ run_check "$home" "$(fixture_path "$stale:$fresh:$dir")" "$out"
311
+ report=$(cat "$out")
312
+ assert_contains "$report" "herdr update not in effect: PATH resolves 0.8.0 at $stale/$TOOL" "a broken pattern on another tool suppressed the PATH skew report"
313
+ assert_contains "$report" "no-mistakes check failed: announce_pattern is not a usable extended regular expression" "the tool whose pattern cannot be used was not named"
314
+ [ "$(wc -l < "$out" | tr -d '[:space:]')" = 1 ] || fail "the report must stay exactly one line"
315
+ pass "a broken pattern is reported for its own tool and the rest of the sweep still reports"
316
+ }
317
+
318
+ test_an_unchecked_announcement_source_is_not_read_as_current() {
319
+ local home dir out report
320
+ # When the budget is gone the separate announcement command cannot run, and the
321
+ # version probe's output never carries the announcement. Searching that output
322
+ # anyway would present a source that was never asked as a clean result, which is
323
+ # the same silently dead source announce_args was added to close.
324
+ home=$(make_home announce-budget)
325
+ dir="$TMP_ROOT/announce-budget/bin"
326
+ mkdir -p "$dir"
327
+ cat > "$dir/no-mistakes-fixture" <<'SH'
328
+ #!/usr/bin/env bash
329
+ if [ "${1:-}" = "--version" ]; then
330
+ printf 'no-mistakes version v1.46.0\n'
331
+ sleep 30
332
+ exit 0
333
+ fi
334
+ printf 'A new version of no-mistakes is available: v1.46.0 -> v1.53.0\n' >&2
335
+ SH
336
+ chmod 0755 "$dir/no-mistakes-fixture"
337
+ write_config "$home" '{"tools":[{"name":"no-mistakes","command":"no-mistakes-fixture","version_args":["--version"],"announce_args":["--help"],"announce_pattern":"A new version of no-mistakes is available: [^ ]+ -> [^ ]+"}]}'
338
+ out="$home/out.txt"
339
+ # The deadline is whole-second granular (real_epoch is `date +%s`), so a
340
+ # budget of 1 leaves headroom anywhere in (0, 1] seconds: when the sweep
341
+ # starts near the end of a second the very first budget check already reads
342
+ # as exhausted and the sweep reports "before every copy answered" instead of
343
+ # reaching the announcement step this case is about. A budget of 2 guarantees
344
+ # more than a full second of headroom for the millisecond-scale work before
345
+ # the copy loop, while the version probe below (bounded, then sleeping 30)
346
+ # still exhausts the budget before the announcement check.
347
+ run_check "$home" "$(fixture_path "$dir")" "$out" FM_TOOL_UPDATE_BUDGET_SECS=2
348
+ report=$(cat "$out")
349
+ assert_contains "$report" "no-mistakes check failed: the time budget ran out before the update announcement was checked" "an announcement source that was never asked was not reported"
350
+ pass "an announcement source the budget could not reach is reported, not read as current"
351
+ }
352
+
353
+ test_an_announcement_probe_that_does_not_answer_is_reported() {
354
+ local home dir out report
355
+ # no-mistakes learns about a new release from the network, so the command that
356
+ # carries the announcement is exactly the one that stalls on a flaky link. A
357
+ # source that was asked and never answered must not read as a clean sweep.
358
+ home=$(make_home announce-mute)
359
+ dir="$TMP_ROOT/announce-mute/bin"
360
+ mkdir -p "$dir"
361
+ cat > "$dir/no-mistakes-fixture" <<'SH'
362
+ #!/usr/bin/env bash
363
+ if [ "${1:-}" = "--version" ]; then
364
+ printf 'no-mistakes version v1.46.0\n'
365
+ exit 0
366
+ fi
367
+ sleep 30
368
+ SH
369
+ chmod 0755 "$dir/no-mistakes-fixture"
370
+ write_config "$home" '{"tools":[{"name":"no-mistakes","command":"no-mistakes-fixture","version_args":["--version"],"announce_args":["--help"],"announce_pattern":"A new version of no-mistakes is available: [^ ]+ -> [^ ]+"}]}'
371
+ out="$home/out.txt"
372
+ run_check "$home" "$(fixture_path "$dir")" "$out" FM_TOOL_UPDATE_PROBE_SECS=1
373
+ report=$(cat "$out")
374
+ assert_contains "$report" "no-mistakes check failed: $dir/no-mistakes-fixture did not answer when asked for its update announcement" "an announcement probe that never answered was read as a clean sweep"
375
+ pass "an announcement probe that does not answer is reported, not read as current"
376
+ }
377
+
378
+ test_quiet_tool_with_announce_pattern_is_silent() {
379
+ local home dir out
380
+ home=$(make_home announce-quiet)
381
+ dir="$TMP_ROOT/announce-quiet/bin"
382
+ make_copy "$dir" no-mistakes-fixture '1.46.0'
383
+ write_config "$home" '{"tools":[{"name":"no-mistakes","command":"no-mistakes-fixture","announce_pattern":"A new version of no-mistakes is available: [^ ]+ -> [^ ]+"}]}'
384
+ out="$home/out.txt"
385
+ run_check "$home" "$(fixture_path "$dir")" "$out"
386
+ [ ! -s "$out" ] || fail "a tool announcing nothing produced a report: $(cat "$out")"
387
+ pass "a tool that announces nothing stays silent"
388
+ }
389
+
390
+ # --- git sources ------------------------------------------------------------
391
+
392
+ # git_fixture <name>: a work repo whose origin branch is two commits ahead,
393
+ # with those commits already present locally so the count is exact.
394
+ git_fixture() {
395
+ local name=$1 bare work
396
+ bare="$TMP_ROOT/$name.git"
397
+ work="$TMP_ROOT/$name"
398
+ git init -q --bare --initial-branch=main "$bare"
399
+ git clone -q "$bare" "$work" 2>/dev/null
400
+ printf 'one\n' > "$work/f1"
401
+ git -C "$work" add f1
402
+ git -C "$work" commit -qm one
403
+ printf 'two\n' > "$work/f2"
404
+ git -C "$work" add f2
405
+ git -C "$work" commit -qm two
406
+ printf 'three\n' > "$work/f3"
407
+ git -C "$work" add f3
408
+ git -C "$work" commit -qm three
409
+ git -C "$work" push -q origin main
410
+ git -C "$work" remote set-head origin main >/dev/null 2>&1
411
+ printf '%s\n' "$work"
412
+ }
413
+
414
+ test_commits_behind_origin_are_reported() {
415
+ local home work out head_before
416
+ home=$(make_home git-behind)
417
+ work=$(git_fixture git-behind-repo)
418
+ git -C "$work" reset -q --hard HEAD~2
419
+ head_before=$(git -C "$work" rev-parse HEAD)
420
+ write_config "$home" "{\"tools\":[{\"name\":\"firstmate\",\"git\":{\"repo\":\"$work\",\"remote\":\"origin\",\"branch\":\"main\"}}]}"
421
+ out="$home/out.txt"
422
+ run_check "$home" "$PATH" "$out"
423
+ assert_contains "$(cat "$out")" "firstmate update available: local main is 2 commits behind origin/main" "commits behind the origin branch were not reported"
424
+ # The probe is read-only: the watched repository must be untouched.
425
+ [ "$(git -C "$work" rev-parse HEAD)" = "$head_before" ] || fail "the check moved the watched repository's HEAD"
426
+ git -C "$work" diff --quiet || fail "the check left changes in the watched repository"
427
+ pass "commits behind the origin branch are reported without touching the repository"
428
+ }
429
+
430
+ test_default_branch_is_detected_when_branch_is_omitted() {
431
+ local home work out
432
+ home=$(make_home git-default)
433
+ work=$(git_fixture git-default-repo)
434
+ git -C "$work" reset -q --hard HEAD~1
435
+ write_config "$home" "{\"tools\":[{\"name\":\"firstmate\",\"git\":{\"repo\":\"$work\"}}]}"
436
+ out="$home/out.txt"
437
+ run_check "$home" "$PATH" "$out"
438
+ assert_contains "$(cat "$out")" "firstmate update available: local main is 1 commit behind origin/main" "the default branch was not detected from the remote"
439
+ pass "an omitted branch is detected from the remote's default branch"
440
+ }
441
+
442
+ test_default_branch_is_asked_of_the_remote_when_the_clone_has_no_record() {
443
+ local home work out
444
+ # A --single-branch clone, or one that never ran remote set-head, has no local
445
+ # refs/remotes/origin/HEAD. The remote still knows its default branch, so this
446
+ # must report the update rather than an unactionable check failure.
447
+ home=$(make_home git-symref)
448
+ work=$(git_fixture git-symref-repo)
449
+ git -C "$work" remote set-head origin --delete >/dev/null 2>&1
450
+ git -C "$work" reset -q --hard HEAD~2
451
+ # Ask git what it knows rather than looking for a loose ref file, which never
452
+ # exists under a non-loose ref backend and would make this vacuous there.
453
+ ! git -C "$work" symbolic-ref --quiet refs/remotes/origin/HEAD >/dev/null 2>&1 \
454
+ || fail "the fixture still records the remote's default branch locally"
455
+ write_config "$home" "{\"tools\":[{\"name\":\"firstmate\",\"git\":{\"repo\":\"$work\"}}]}"
456
+ out="$home/out.txt"
457
+ run_check "$home" "$PATH" "$out"
458
+ assert_contains "$(cat "$out")" "firstmate update available: local main is 2 commits behind origin/main" "the default branch was not asked of the remote"
459
+ pass "the default branch is asked of the remote when the clone has no local record"
460
+ }
461
+
462
+ test_current_and_ahead_repositories_are_silent() {
463
+ local home work out
464
+ home=$(make_home git-current)
465
+ work=$(git_fixture git-current-repo)
466
+ write_config "$home" "{\"tools\":[{\"name\":\"firstmate\",\"git\":{\"repo\":\"$work\",\"branch\":\"main\"}}]}"
467
+ out="$home/out.txt"
468
+ run_check "$home" "$PATH" "$out"
469
+ [ ! -s "$out" ] || fail "an up to date repository produced a report: $(cat "$out")"
470
+
471
+ printf 'local only\n' > "$work/f4"
472
+ git -C "$work" add f4
473
+ git -C "$work" commit -qm four
474
+ rm -f "$home/state/.tool-updates"
475
+ run_check "$home" "$PATH" "$out"
476
+ [ ! -s "$out" ] || fail "a repository ahead of its origin branch produced a report: $(cat "$out")"
477
+ pass "a repository that is current or ahead of its origin branch is silent"
478
+ }
479
+
480
+ test_unusable_git_source_is_reported() {
481
+ local home out
482
+ home=$(make_home git-broken)
483
+ mkdir -p "$TMP_ROOT/git-broken/not-a-repo"
484
+ write_config "$home" "{\"tools\":[{\"name\":\"firstmate\",\"git\":{\"repo\":\"$TMP_ROOT/git-broken/not-a-repo\",\"branch\":\"main\"}}]}"
485
+ out="$home/out.txt"
486
+ run_check "$home" "$PATH" "$out"
487
+ assert_contains "$(cat "$out")" "firstmate check failed" "an unusable git source was not reported"
488
+ pass "an unusable git source is reported as a check failure"
489
+ }
490
+
491
+ test_unreadable_remote_is_not_reported_as_a_missing_branch() {
492
+ local home work out report
493
+ # A remote that cannot be reached at all and a branch that was deleted are
494
+ # different problems with different repairs. Reporting the first as the second
495
+ # wakes firstmate with a diagnosis that is simply wrong, so the report must
496
+ # name only what the probe established.
497
+ home=$(make_home git-unreadable)
498
+ work=$(git_fixture git-unreadable-repo)
499
+ rm -rf "$TMP_ROOT/git-unreadable-repo.git"
500
+ write_config "$home" "{\"tools\":[{\"name\":\"firstmate\",\"git\":{\"repo\":\"$work\",\"remote\":\"origin\",\"branch\":\"main\"}}]}"
501
+ out="$home/out.txt"
502
+ run_check "$home" "$PATH" "$out"
503
+ report=$(cat "$out")
504
+ assert_contains "$report" "firstmate check failed" "a remote that could not be read was not reported"
505
+ assert_contains "$report" "origin could not be reached or read" "the report does not name the condition the probe actually found"
506
+ assert_not_contains "$report" "has no branch" "a remote that could not be read was reported as a deleted branch"
507
+ pass "a remote that cannot be read is reported as unreadable, not as a missing branch"
508
+ }
509
+
510
+ test_missing_branch_on_a_readable_remote_is_still_reported() {
511
+ local home work out
512
+ # The other side of the case above: the remote answers, and it really does not
513
+ # have the watched branch, so that must still be reported as a missing branch.
514
+ home=$(make_home git-no-branch)
515
+ work=$(git_fixture git-no-branch-repo)
516
+ write_config "$home" "{\"tools\":[{\"name\":\"firstmate\",\"git\":{\"repo\":\"$work\",\"remote\":\"origin\",\"branch\":\"release\"}}]}"
517
+ out="$home/out.txt"
518
+ run_check "$home" "$PATH" "$out"
519
+ assert_contains "$(cat "$out")" "firstmate check failed: origin has no branch release" "a branch the remote does not have was not reported as missing"
520
+ pass "a branch a readable remote does not have is still reported as missing"
521
+ }
522
+
523
+ test_git_probes_stop_when_the_sweep_budget_is_gone() {
524
+ local home work slow out report
525
+ # The git probes are the several-in-a-row case, two of them over the network,
526
+ # so they are the ones that can push a sweep past the watcher's own timeout and
527
+ # leave it killed with nothing printed at all. Here the tool's command probe
528
+ # spends the whole budget, so its git probes must not start: the sweep says
529
+ # which tool it did not finish instead of quietly running on.
530
+ home=$(make_home git-budget)
531
+ work=$(git_fixture git-budget-repo)
532
+ git -C "$work" reset -q --hard HEAD~2
533
+ slow="$TMP_ROOT/git-budget/bin"
534
+ make_slow_copy "$slow" "$TOOL" 30
535
+ write_config "$home" "{\"tools\":[{\"name\":\"firstmate\",\"command\":\"$TOOL\",\"git\":{\"repo\":\"$work\",\"remote\":\"origin\",\"branch\":\"main\"}}]}"
536
+ out="$home/out.txt"
537
+ run_check "$home" "$(fixture_path "$slow")" "$out" FM_TOOL_UPDATE_BUDGET_SECS=1
538
+ report=$(cat "$out")
539
+ assert_contains "$report" "check incomplete: the time budget ran out before firstmate" "a sweep with no budget left did not say which tool it did not finish"
540
+ assert_not_contains "$report" "commits behind" "the git probes ran after the sweep budget was already gone"
541
+ pass "git probes stop and name their tool once the sweep budget is gone"
542
+ }
543
+
544
+ test_a_git_probe_that_does_not_answer_is_not_an_update() {
545
+ local home work dir out report head_before
546
+ # The local git probes are bounded too, so a bound that is hit must not be read
547
+ # as the answer "this clone does not have that commit". This clone is ahead of
548
+ # its remote branch, which is silent when the probes answer, so any claim of an
549
+ # available update here was never established.
550
+ home=$(make_home git-mute)
551
+ work=$(git_fixture git-mute-repo)
552
+ printf 'local only\n' > "$work/f4"
553
+ git -C "$work" add f4
554
+ git -C "$work" commit -qm four
555
+ head_before=$(git -C "$work" rev-parse HEAD)
556
+
557
+ # A git that answers everything except the object query, which never answers.
558
+ dir="$TMP_ROOT/git-mute/bin"
559
+ mkdir -p "$dir"
560
+ cat > "$dir/git" <<SH
561
+ #!/usr/bin/env bash
562
+ for arg in "\$@"; do
563
+ if [ "\$arg" = cat-file ]; then
564
+ sleep 30
565
+ exit 0
566
+ fi
567
+ done
568
+ exec $(command -v git) "\$@"
569
+ SH
570
+ chmod 0755 "$dir/git"
571
+
572
+ write_config "$home" "{\"tools\":[{\"name\":\"firstmate\",\"git\":{\"repo\":\"$work\",\"remote\":\"origin\",\"branch\":\"main\"}}]}"
573
+ out="$home/out.txt"
574
+ # The bound is wide enough that the earlier probes answer comfortably, so the
575
+ # only probe that can hit it is the object query the fixture stalls. Asserting
576
+ # that specific report keeps an unrelated timeout from passing this case for the
577
+ # wrong reason.
578
+ run_check "$home" "$(fixture_path "$dir")" "$out" FM_TOOL_UPDATE_PROBE_SECS=3
579
+ report=$(cat "$out")
580
+ assert_not_contains "$report" "update available" "a probe that never answered was reported as an available update"
581
+ assert_contains "$report" "firstmate check failed: $work did not answer whether it already has" "the stalled object query was not the reported failure"
582
+ [ "$(git -C "$work" rev-parse HEAD)" = "$head_before" ] || fail "the check moved the watched repository's HEAD"
583
+ pass "a git probe that does not answer is reported as a failure, never as an update"
584
+ }
585
+
586
+ test_a_stalled_repository_probe_is_not_reported_as_not_a_repository() {
587
+ local home work dir out report
588
+ # The very first git probe is bounded too. A clone on a stalled mount that
589
+ # never answers must be reported as not answering, not as not being a git
590
+ # repository, which is a diagnosis the probe never established.
591
+ home=$(make_home git-stall)
592
+ work=$(git_fixture git-stall-repo)
593
+ git -C "$work" reset -q --hard HEAD~2
594
+
595
+ dir="$TMP_ROOT/git-stall/bin"
596
+ mkdir -p "$dir"
597
+ cat > "$dir/git" <<SH
598
+ #!/usr/bin/env bash
599
+ for arg in "\$@"; do
600
+ if [ "\$arg" = rev-parse ]; then
601
+ sleep 30
602
+ exit 0
603
+ fi
604
+ done
605
+ exec $(command -v git) "\$@"
606
+ SH
607
+ chmod 0755 "$dir/git"
608
+
609
+ write_config "$home" "{\"tools\":[{\"name\":\"firstmate\",\"git\":{\"repo\":\"$work\",\"remote\":\"origin\",\"branch\":\"main\"}}]}"
610
+ out="$home/out.txt"
611
+ run_check "$home" "$(fixture_path "$dir")" "$out" FM_TOOL_UPDATE_PROBE_SECS=1
612
+ report=$(cat "$out")
613
+ assert_contains "$report" "firstmate check failed: $work did not answer whether it is a git repository" "a repository probe that never answered was not reported as such"
614
+ assert_not_contains "$report" "is not a git repository" "a repository probe that never answered was reported as not a repository"
615
+ pass "a stalled repository probe is reported as no answer, not as not a repository"
616
+ }
617
+
618
+ # --- registry and reporting contract ----------------------------------------
619
+
620
+ test_absent_registry_is_silent() {
621
+ local home out
622
+ home=$(make_home no-config)
623
+ out="$home/out.txt"
624
+ run_check "$home" "$PATH" "$out"
625
+ [ ! -s "$out" ] || fail "check spoke without a watched tool registry: $(cat "$out")"
626
+ assert_absent "$home/state/.tool-updates" "check wrote a record without a registry"
627
+ pass "no watched tool registry means no output at all"
628
+ }
629
+
630
+ test_malformed_registry_is_reported_not_ignored() {
631
+ local home out
632
+ home=$(make_home bad-config)
633
+ printf 'not json at all\n' > "$home/config/watched-tools.json"
634
+ out="$home/out.txt"
635
+ run_check "$home" "$PATH" "$out"
636
+ assert_contains "$(cat "$out")" "watched tool registry: the watched tool registry is not valid JSON" "a malformed registry was silently ignored"
637
+
638
+ printf '%s\n' '{"tools":[{"name":"herdr"}]}' > "$home/config/watched-tools.json"
639
+ rm -f "$home/state/.tool-updates"
640
+ run_check "$home" "$PATH" "$out"
641
+ assert_contains "$(cat "$out")" "tool herdr needs command, git, or both" "a tool entry with no update source was accepted"
642
+
643
+ printf '%s\n' '{"tools":[{"name":"herdr","command":"herdr; rm -rf /"}]}' > "$home/config/watched-tools.json"
644
+ rm -f "$home/state/.tool-updates"
645
+ run_check "$home" "$PATH" "$out"
646
+ assert_contains "$(cat "$out")" "command must be a bare executable name" "a command name with shell characters was accepted"
647
+
648
+ printf '%s\n' '{"tools":[{"name":"herdr","command":"herdr","announce_args":["--help"]}]}' > "$home/config/watched-tools.json"
649
+ rm -f "$home/state/.tool-updates"
650
+ run_check "$home" "$PATH" "$out"
651
+ assert_contains "$(cat "$out")" "tool herdr announce_args needs announce_pattern" "a command to search with no pattern to search for was accepted"
652
+ pass "a malformed registry is reported instead of quietly skipped"
653
+ }
654
+
655
+ test_findings_are_reported_once_until_they_change() {
656
+ local home stale fresh out path
657
+ home=$(make_home no-nag)
658
+ stale="$TMP_ROOT/no-nag/old/bin"
659
+ fresh="$TMP_ROOT/no-nag/new/bin"
660
+ make_copy "$stale" "$TOOL" 'herdr 0.8.0'
661
+ make_copy "$fresh" "$TOOL" 'herdr 0.8.2'
662
+ write_config "$home" "{\"tools\":[{\"name\":\"herdr\",\"command\":\"$TOOL\"}]}"
663
+ out="$home/out.txt"
664
+ path=$(fixture_path "$stale:$fresh")
665
+
666
+ run_check "$home" "$path" "$out"
667
+ assert_contains "$(cat "$out")" "not in effect" "the first sweep did not report the pending update"
668
+ run_check "$home" "$path" "$out"
669
+ [ ! -s "$out" ] || fail "the same pending update was reported twice: $(cat "$out")"
670
+
671
+ # A changed finding is news again.
672
+ make_copy "$fresh" "$TOOL" 'herdr 0.9.0'
673
+ run_check "$home" "$path" "$out"
674
+ assert_contains "$(cat "$out")" "0.9.0 is installed" "a changed finding was suppressed as a repeat"
675
+
676
+ # Once the condition clears, the report clears with it, and a later return of
677
+ # the same condition is reported again.
678
+ make_copy "$stale" "$TOOL" 'herdr 0.9.0'
679
+ run_check "$home" "$path" "$out"
680
+ [ ! -s "$out" ] || fail "a cleared finding still produced a report: $(cat "$out")"
681
+ make_copy "$stale" "$TOOL" 'herdr 0.8.0'
682
+ run_check "$home" "$path" "$out"
683
+ assert_contains "$(cat "$out")" "PATH resolves 0.8.0" "a returning finding was not reported again"
684
+ pass "the same pending update is reported once, and a change is reported again"
685
+ }
686
+
687
+ test_an_overlong_report_says_it_was_cut() {
688
+ local home out report i tools=
689
+ # Many watched tools can outgrow one line. The report must say it was cut
690
+ # rather than end mid-finding as if that were everything found.
691
+ home=$(make_home long)
692
+ for i in $(seq 1 30); do
693
+ [ -z "$tools" ] || tools="$tools,"
694
+ tools="$tools{\"name\":\"absent-tool-$i\",\"command\":\"fm-absent-fixture-$i\"}"
695
+ done
696
+ write_config "$home" "{\"tools\":[$tools]}"
697
+ out="$home/out.txt"
698
+ run_check "$home" "$PATH" "$out"
699
+ report=$(cat "$out")
700
+ assert_contains "$report" "[truncated]" "an over-long report was cut without saying so"
701
+ [ "$(wc -l < "$out" | tr -d '[:space:]')" = 1 ] || fail "the cut report must still be exactly one line"
702
+ pass "an over-long report is cut with the shared truncation marker"
703
+ }
704
+
705
+ test_a_finding_past_the_cut_is_still_reported() {
706
+ local home stale fresh out report i tools=
707
+ # Once a report is long enough to be cut, a new finding lands past the cut and
708
+ # leaves the printed line unchanged. It still has to count as news, or the PATH
709
+ # skew this check exists for would be suppressed for good on a busy home.
710
+ home=$(make_home past-cut)
711
+ stale="$TMP_ROOT/past-cut/mise/installs/herdr/latest/bin"
712
+ fresh="$TMP_ROOT/past-cut/local/bin"
713
+ make_copy "$stale" "$TOOL" 'herdr 0.8.0'
714
+ make_copy "$fresh" "$TOOL" 'herdr 0.8.2'
715
+ for i in $(seq 1 30); do
716
+ [ -z "$tools" ] || tools="$tools,"
717
+ tools="$tools{\"name\":\"absent-tool-$i\",\"command\":\"fm-absent-fixture-$i\"}"
718
+ done
719
+ out="$home/out.txt"
720
+ write_config "$home" "{\"tools\":[$tools]}"
721
+ run_check "$home" "$(fixture_path "$stale:$fresh")" "$out"
722
+ assert_contains "$(cat "$out")" "[truncated]" "the first report was not long enough to be cut, so this case proves nothing"
723
+
724
+ # The skew tool goes last, so its finding falls past the cut and the printed
725
+ # line is byte identical to the one the first sweep already recorded.
726
+ write_config "$home" "{\"tools\":[$tools,{\"name\":\"herdr\",\"command\":\"$TOOL\"}]}"
727
+ run_check "$home" "$(fixture_path "$stale:$fresh")" "$out"
728
+ report=$(cat "$out")
729
+ [ -n "$report" ] || fail "a finding past the cut produced no report at all, so it can never reach the watcher"
730
+ assert_contains "$report" "[truncated]" "the second report was not cut, so the finding was not past the cut"
731
+ pass "a finding that lands past the cut is still reported as news"
732
+ }
733
+
734
+ test_probes_are_skipped_between_intervals() {
735
+ local home dir out status now
736
+ home=$(make_home cadence)
737
+ dir="$TMP_ROOT/cadence/bin"
738
+ make_copy "$dir" "$TOOL" 'herdr 0.8.2'
739
+ write_config "$home" "{\"tools\":[{\"name\":\"herdr\",\"command\":\"$TOOL\"}]}"
740
+ out="$home/out.txt"
741
+ now=1700000000
742
+
743
+ status=0
744
+ FM_HOME="$home" PATH="$(fixture_path "$dir")" FM_CHECK_TIMEOUT=30 FM_TOOL_UPDATE_INTERVAL=900 FM_TOOL_UPDATE_NOW="$now" \
745
+ "$CHECK" >"$out" 2>&1 || status=$?
746
+ expect_code 0 "$status" "first cadence run exit"
747
+ assert_grep 'fm-tool-updates-v1' "$home/state/.tool-updates" "the first run did not record its sweep"
748
+
749
+ # A finding appears, but the interval has not elapsed, so no probe runs.
750
+ make_copy "$dir" "$TOOL" 'no version here'
751
+ status=0
752
+ FM_HOME="$home" PATH="$(fixture_path "$dir")" FM_CHECK_TIMEOUT=30 FM_TOOL_UPDATE_INTERVAL=900 FM_TOOL_UPDATE_NOW="$((now + 300))" \
753
+ "$CHECK" >"$out" 2>&1 || status=$?
754
+ expect_code 0 "$status" "gated cadence run exit"
755
+ [ ! -s "$out" ] || fail "a run inside the interval probed and spoke: $(cat "$out")"
756
+
757
+ status=0
758
+ FM_HOME="$home" PATH="$(fixture_path "$dir")" FM_CHECK_TIMEOUT=30 FM_TOOL_UPDATE_INTERVAL=900 FM_TOOL_UPDATE_NOW="$((now + 901))" \
759
+ "$CHECK" >"$out" 2>&1 || status=$?
760
+ expect_code 0 "$status" "due cadence run exit"
761
+ assert_contains "$(cat "$out")" "did not report a version" "the run after the interval did not probe"
762
+ pass "probes run once per interval, not on every poll"
763
+ }
764
+
765
+ test_an_oversized_budget_is_cut_to_fit_and_reported() {
766
+ local home stale fresh out report status
767
+ # A sweep budget larger than the watcher's own per check bound lets the watcher
768
+ # kill the run, which prints nothing and records nothing, so the same silence
769
+ # repeats on every poll. Cutting it keeps the detector alive, and the cut is
770
+ # reported so the operator can see the setting was not used as written.
771
+ home=$(make_home budget-cut)
772
+ stale="$TMP_ROOT/budget-cut/mise/installs/herdr/latest/bin"
773
+ fresh="$TMP_ROOT/budget-cut/local/bin"
774
+ make_copy "$stale" "$TOOL" 'herdr 0.8.0'
775
+ make_copy "$fresh" "$TOOL" 'herdr 0.8.2'
776
+ write_config "$home" "{\"tools\":[{\"name\":\"herdr\",\"command\":\"$TOOL\"}]}"
777
+ out="$home/out.txt"
778
+ status=0
779
+ env FM_HOME="$home" PATH="$(fixture_path "$stale:$fresh")" FM_TOOL_UPDATE_INTERVAL=0 \
780
+ FM_TOOL_UPDATE_BUDGET_SECS=60 FM_CHECK_TIMEOUT=30 "$CHECK" >"$out" 2>&1 || status=$?
781
+ expect_code 0 "$status" "oversized budget exit"
782
+ report=$(cat "$out")
783
+ # The cut leaves room for the whole-second rounding and the kill grace as well
784
+ # as one probe bound, so a cut sweep really does end before the watcher bound.
785
+ assert_contains "$report" "sweep budget 60s cut to 27s to stay inside the watcher check timeout of 30s" "a budget that cannot fit the watcher bound was not cut and reported"
786
+ assert_contains "$report" "herdr update not in effect" "the detector went quiet instead of sweeping with the cut budget"
787
+
788
+ # The default budget of 20s fits the default bound, so it is used as written.
789
+ # The record is cleared first because the no-nag gate would otherwise suppress
790
+ # this run, whose bare skew line differs from the cut run's line above.
791
+ rm -f "$home/state/.tool-updates"
792
+ status=0
793
+ env FM_HOME="$home" PATH="$(fixture_path "$stale:$fresh")" FM_TOOL_UPDATE_INTERVAL=0 \
794
+ FM_CHECK_TIMEOUT=30 "$CHECK" >"$out" 2>&1 || status=$?
795
+ expect_code 0 "$status" "default budget exit"
796
+ report=$(cat "$out")
797
+ assert_not_contains "$report" "sweep budget" "the default budget was cut at the default watcher bound"
798
+ assert_contains "$report" "herdr update not in effect" "the sweep stopped reporting with the default budget"
799
+
800
+ # A budget the watcher bound has room for is used as written. The cleared
801
+ # record keeps the no-nag gate from hiding this run's repeat of the same line.
802
+ rm -f "$home/state/.tool-updates"
803
+ status=0
804
+ env FM_HOME="$home" PATH="$(fixture_path "$stale:$fresh")" FM_TOOL_UPDATE_INTERVAL=0 \
805
+ FM_TOOL_UPDATE_BUDGET_SECS=60 FM_CHECK_TIMEOUT=120 "$CHECK" >"$out" 2>&1 || status=$?
806
+ expect_code 0 "$status" "fitting budget exit"
807
+ report=$(cat "$out")
808
+ assert_not_contains "$report" "sweep budget" "a budget that fits the watcher bound was cut anyway"
809
+ assert_contains "$report" "herdr update not in effect" "the sweep stopped reporting with a budget that fits"
810
+ pass "a budget that cannot fit the watcher bound is cut and reported, and the sweep keeps working"
811
+ }
812
+
813
+ test_invalid_environment_and_action_refuse() {
814
+ local home status
815
+ home=$(make_home refuse)
816
+ status=0
817
+ FM_HOME="$home" FM_TOOL_UPDATE_INTERVAL=5 "$CHECK" >/dev/null 2>&1 || status=$?
818
+ expect_code 2 "$status" "too-small interval exit"
819
+ status=0
820
+ FM_HOME="$home" FM_TOOL_UPDATE_PROBE_SECS=0 "$CHECK" >/dev/null 2>&1 || status=$?
821
+ expect_code 2 "$status" "zero probe bound exit"
822
+ status=0
823
+ FM_HOME="$home" FM_TOOL_UPDATE_BUDGET_SECS=999 "$CHECK" >/dev/null 2>&1 || status=$?
824
+ expect_code 2 "$status" "oversized budget exit"
825
+ status=0
826
+ FM_HOME="$home" "$CHECK" sweep >/dev/null 2>&1 || status=$?
827
+ expect_code 2 "$status" "unknown action exit"
828
+ status=0
829
+ FM_HOME="$home" "$CHECK" --help >/dev/null 2>&1 || status=$?
830
+ expect_code 0 "$status" "help exit"
831
+ pass "an out of range bound or unknown action refuses instead of guessing"
832
+ }
833
+
834
+ # --- arming through the existing watcher contract ----------------------------
835
+
836
+ test_arm_registers_the_check_and_disarm_removes_it() {
837
+ local home dir status
838
+ home=$(make_home arm)
839
+ dir="$TMP_ROOT/arm/bin"
840
+ make_copy "$dir" "$TOOL" 'herdr 0.8.2'
841
+ status=0
842
+ FM_HOME="$home" "$CHECK" arm >/dev/null 2>&1 || status=$?
843
+ expect_code 1 "$status" "arm without a registry exit"
844
+ assert_absent "$home/state/tool-updates.check.sh" "arm wrote a check shim without a registry"
845
+
846
+ write_config "$home" "{\"tools\":[{\"name\":\"herdr\",\"command\":\"$TOOL\"}]}"
847
+ status=0
848
+ FM_HOME="$home" "$CHECK" arm >/dev/null || status=$?
849
+ expect_code 0 "$status" "arm exit"
850
+ assert_present "$home/state/tool-updates.check.sh" "arm did not write the check shim"
851
+ assert_present "$home/state/tool-updates.check-trust" "arm did not register the check's bytes"
852
+ [ "$(stat -c %a "$home/state/tool-updates.check.sh" 2>/dev/null || stat -f %Lp "$home/state/tool-updates.check.sh")" = 700 ] \
853
+ || fail "the check shim is not mode 700"
854
+ assert_grep 'fm-custom-check-v1' "$home/state/tool-updates.check-trust" "the trust binding has the wrong schema"
855
+
856
+ # Arming twice must stay valid rather than invalidating its own binding.
857
+ FM_HOME="$home" "$CHECK" arm >/dev/null || fail "arming twice failed"
858
+ assert_grep 'fm-custom-check-v1' "$home/state/tool-updates.check-trust" "re-arming lost the trust binding"
859
+
860
+ FM_HOME="$home" "$CHECK" disarm >/dev/null || fail "disarm failed"
861
+ assert_absent "$home/state/tool-updates.check.sh" "disarm left the check shim behind"
862
+ assert_absent "$home/state/tool-updates.check-trust" "disarm left the trust binding behind"
863
+ assert_absent "$home/state/.tool-updates" "disarm left the report record behind"
864
+ pass "arm registers a trusted check and disarm removes every trace"
865
+ }
866
+
867
+ test_arm_refuses_a_symlink_at_the_shim_path() {
868
+ local home dir target mode status
869
+ # A stale or hostile symlink at the shim path must be refused rather than
870
+ # followed: following it would write the shim body into a file someone else
871
+ # owns and then make that file executable.
872
+ home=$(make_home arm-symlink)
873
+ dir="$TMP_ROOT/arm-symlink/bin"
874
+ make_copy "$dir" "$TOOL" 'herdr 0.8.2'
875
+ write_config "$home" "{\"tools\":[{\"name\":\"herdr\",\"command\":\"$TOOL\"}]}"
876
+ target="$TMP_ROOT/arm-symlink/not-the-shim.txt"
877
+ printf 'a file the shim must not touch\n' > "$target"
878
+ mode=$(stat -c %a "$target" 2>/dev/null || stat -f %Lp "$target")
879
+ ln -s "$target" "$home/state/tool-updates.check.sh"
880
+
881
+ status=0
882
+ FM_HOME="$home" "$CHECK" arm >/dev/null 2>&1 || status=$?
883
+ expect_code 1 "$status" "arm over a symlink exit"
884
+ [ "$(cat "$target")" = 'a file the shim must not touch' ] || fail "arm followed the symlink and overwrote its target"
885
+ [ "$(stat -c %a "$target" 2>/dev/null || stat -f %Lp "$target")" = "$mode" ] || fail "arm changed the mode of the symlink's target"
886
+ assert_absent "$home/state/tool-updates.check-trust" "arm registered a shim it refused to write"
887
+ pass "a symlink at the shim path is refused instead of followed"
888
+ }
889
+
890
+ test_a_failed_registration_leaves_no_unregistered_shim() {
891
+ local home dir target stale_shim status
892
+ # An unregistered shim in state/ is not inert: the watcher rejects it every
893
+ # cycle and wakes firstmate about unauthenticated state checks until someone
894
+ # deletes it by hand. So a home that could not be armed has to come back to the
895
+ # state it was in, and arm still has to say it failed.
896
+ home=$(make_home arm-register-fail)
897
+ dir="$TMP_ROOT/arm-register-fail/bin"
898
+ make_copy "$dir" "$TOOL" 'herdr 0.8.2'
899
+ write_config "$home" "{\"tools\":[{\"name\":\"herdr\",\"command\":\"$TOOL\"}]}"
900
+ # A symlink at the trust path makes registration refuse, which is the shape any
901
+ # register failure has from arm's side.
902
+ target="$TMP_ROOT/arm-register-fail/not-the-trust.txt"
903
+ printf 'a file the trust binding must not touch\n' > "$target"
904
+ ln -s "$target" "$home/state/tool-updates.check-trust"
905
+
906
+ status=0
907
+ FM_HOME="$home" "$CHECK" arm >/dev/null 2>&1 || status=$?
908
+ expect_code 1 "$status" "arm with an unusable trust path exit"
909
+ assert_absent "$home/state/tool-updates.check.sh" "a failed registration left an unregistered check shim behind"
910
+ [ "$(cat "$target")" = 'a file the trust binding must not touch' ] || fail "arm wrote through the trust symlink"
911
+
912
+ # A shim that was already there is only kept when its trust binding is still
913
+ # intact. Here the binding is unusable, so putting the old bytes back would
914
+ # leave exactly the unbound shim the watcher wakes about, and the home has to
915
+ # end plainly not armed instead.
916
+ stale_shim="$TMP_ROOT/arm-register-fail/shim-armed-earlier"
917
+ printf '#!/usr/bin/env bash\n# a shim armed earlier\nexit 0\n' > "$stale_shim"
918
+ cp "$stale_shim" "$home/state/tool-updates.check.sh"
919
+ chmod 0700 "$home/state/tool-updates.check.sh"
920
+ status=0
921
+ FM_HOME="$home" "$CHECK" arm >/dev/null 2>&1 || status=$?
922
+ expect_code 1 "$status" "arm over an existing shim with an unusable trust path exit"
923
+ assert_absent "$home/state/tool-updates.check.sh" "a failed arm left a shim behind that no trust binding covers"
924
+ pass "a failed registration never leaves a shim without a matching trust binding"
925
+ }
926
+
927
+ test_a_failed_rearm_leaves_no_shim_the_trust_binding_lost() {
928
+ local home dir fake status
929
+ # The register removes an existing trust binding when its own post-write check
930
+ # fails, so an arm that fails there would leave a home that WAS armed holding a
931
+ # shim with no binding, which the watcher rejects on every cycle. The home must
932
+ # end plainly not armed instead.
933
+ home=$(make_home arm-rearm-fail)
934
+ dir="$TMP_ROOT/arm-rearm-fail/bin"
935
+ make_copy "$dir" "$TOOL" 'herdr 0.8.2'
936
+ write_config "$home" "{\"tools\":[{\"name\":\"herdr\",\"command\":\"$TOOL\"}]}"
937
+ FM_HOME="$home" "$CHECK" arm >/dev/null || fail "the first arm failed"
938
+ assert_present "$home/state/tool-updates.check-trust" "the first arm did not bind the shim"
939
+
940
+ # A hash tool that answers with nothing makes the register write a binding it
941
+ # then rejects, and it removes the old binding on the way out.
942
+ fake="$TMP_ROOT/arm-rearm-fail/fake-hash"
943
+ mkdir -p "$fake"
944
+ printf '#!/usr/bin/env bash\nexit 1\n' > "$fake/shasum"
945
+ printf '#!/usr/bin/env bash\nexit 1\n' > "$fake/sha256sum"
946
+ chmod 0755 "$fake/shasum" "$fake/sha256sum"
947
+ printf '#!/usr/bin/env bash\n# a shim armed earlier\nexit 0\n' > "$home/state/tool-updates.check.sh"
948
+ chmod 0700 "$home/state/tool-updates.check.sh"
949
+
950
+ status=0
951
+ env FM_HOME="$home" PATH="$(fixture_path "$fake")" "$CHECK" arm >/dev/null 2>&1 || status=$?
952
+ expect_code 1 "$status" "arm whose registration cannot hash exit"
953
+ assert_absent "$home/state/tool-updates.check.sh" "a failed re-arm left a shim behind after the trust binding was removed"
954
+ assert_absent "$home/state/tool-updates.check-trust" "the failed registration left a trust binding behind"
955
+ pass "a re-arm that loses the trust binding leaves no shim behind"
956
+ }
957
+
958
+ test_arm_resolves_a_relative_home_into_the_shim() {
959
+ local home stale fresh out status
960
+ # The watcher runs the shim from its own working directory, so a relative home
961
+ # has to be resolved before it is persisted. Otherwise the shim reads whatever
962
+ # sits under the watcher's directory, finds no registry, and stays silent for
963
+ # good.
964
+ home=$(make_home arm-relative)
965
+ stale="$TMP_ROOT/arm-relative/mise/bin"
966
+ fresh="$TMP_ROOT/arm-relative/local/bin"
967
+ make_copy "$stale" "$TOOL" 'herdr 0.8.0'
968
+ make_copy "$fresh" "$TOOL" 'herdr 0.8.2'
969
+ write_config "$home" "{\"tools\":[{\"name\":\"herdr\",\"command\":\"$TOOL\"}]}"
970
+
971
+ status=0
972
+ (cd "$TMP_ROOT" && FM_HOME=arm-relative "$CHECK" arm >/dev/null 2>&1) || status=$?
973
+ expect_code 0 "$status" "arm with a relative home exit"
974
+
975
+ out="$home/out.txt"
976
+ status=0
977
+ (cd / && env -u FM_HOME PATH="$(fixture_path "$stale:$fresh")" FM_CHECK_TIMEOUT=30 FM_TOOL_UPDATE_INTERVAL=0 \
978
+ "$home/state/tool-updates.check.sh" >"$out" 2>&1) || status=$?
979
+ expect_code 0 "$status" "shim run from another directory exit"
980
+ assert_contains "$(cat "$out")" "herdr update not in effect" "the shim read a different home than the one it was armed for"
981
+ pass "a relative home is resolved before it is persisted into the shim"
982
+ }
983
+
984
+ test_armed_check_wakes_the_watcher_with_the_skew_report() {
985
+ local home stale fresh out err status
986
+ # End to end through the real watcher: the armed check must reach it as a
987
+ # `check:` wake carrying the same PATH skew line, with no new machinery.
988
+ home=$(make_home wake)
989
+ stale="$TMP_ROOT/wake/mise/installs/herdr/latest/bin"
990
+ fresh="$TMP_ROOT/wake/local/bin"
991
+ make_copy "$stale" "$TOOL" 'herdr 0.8.0'
992
+ make_copy "$fresh" "$TOOL" 'herdr 0.8.2'
993
+ write_config "$home" "{\"tools\":[{\"name\":\"herdr\",\"command\":\"$TOOL\"}]}"
994
+ FM_HOME="$home" "$CHECK" arm >/dev/null || fail "could not arm the watched tool check"
995
+
996
+ out="$home/out.txt"
997
+ err="$home/err.txt"
998
+ status=0
999
+ env FM_HOME="$home" PATH="$(fixture_path "$stale:$fresh")" FM_CHECK_TIMEOUT=30 FM_TOOL_UPDATE_INTERVAL=0 \
1000
+ FM_POLL=1 FM_SIGNAL_GRACE=1 FM_CHECK_INTERVAL=1 \
1001
+ "$CHECKPOINT" --seconds 10 >"$out" 2>"$err" || status=$?
1002
+ expect_code 0 "$status" "watcher checkpoint exit"
1003
+ assert_contains "$(cat "$out")" "check:" "the armed check did not reach the watcher as a check wake"
1004
+ assert_contains "$(cat "$out")" "tool updates: herdr update not in effect" "the wake did not carry the PATH skew report"
1005
+ pass "the armed check reaches the watcher as an ordinary check wake"
1006
+ }
1007
+
1008
+ test_path_skew_is_reported_from_every_copy
1009
+ test_newest_copy_first_on_path_is_silent
1010
+ test_identical_versions_are_silent
1011
+ test_one_copy_reached_twice_is_probed_once
1012
+ test_unreadable_version_is_a_failure_not_a_pass
1013
+ test_missing_command_is_reported
1014
+ test_announced_update_is_reported_from_the_tool_itself
1015
+ test_announcement_is_read_from_a_second_command
1016
+ test_unusable_announce_pattern_is_reported_not_read_as_silence
1017
+ test_one_broken_pattern_does_not_blind_the_rest_of_the_sweep
1018
+ test_an_unchecked_announcement_source_is_not_read_as_current
1019
+ test_an_announcement_probe_that_does_not_answer_is_reported
1020
+ test_quiet_tool_with_announce_pattern_is_silent
1021
+ test_commits_behind_origin_are_reported
1022
+ test_default_branch_is_detected_when_branch_is_omitted
1023
+ test_default_branch_is_asked_of_the_remote_when_the_clone_has_no_record
1024
+ test_current_and_ahead_repositories_are_silent
1025
+ test_unusable_git_source_is_reported
1026
+ test_unreadable_remote_is_not_reported_as_a_missing_branch
1027
+ test_missing_branch_on_a_readable_remote_is_still_reported
1028
+ test_git_probes_stop_when_the_sweep_budget_is_gone
1029
+ test_a_git_probe_that_does_not_answer_is_not_an_update
1030
+ test_a_stalled_repository_probe_is_not_reported_as_not_a_repository
1031
+ test_absent_registry_is_silent
1032
+ test_malformed_registry_is_reported_not_ignored
1033
+ test_findings_are_reported_once_until_they_change
1034
+ test_an_overlong_report_says_it_was_cut
1035
+ test_a_finding_past_the_cut_is_still_reported
1036
+ test_probes_are_skipped_between_intervals
1037
+ test_an_oversized_budget_is_cut_to_fit_and_reported
1038
+ test_invalid_environment_and_action_refuse
1039
+ test_arm_registers_the_check_and_disarm_removes_it
1040
+ test_arm_refuses_a_symlink_at_the_shim_path
1041
+ test_a_failed_registration_leaves_no_unregistered_shim
1042
+ test_a_failed_rearm_leaves_no_shim_the_trust_binding_lost
1043
+ test_arm_resolves_a_relative_home_into_the_shim
1044
+ test_armed_check_wakes_the_watcher_with_the_skew_report