@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,1196 @@
1
+ #!/usr/bin/env bash
2
+ # Contract tests for bin/fm-test-run.sh - the single owner of behavior suite
3
+ # selection, portable lane composition, bounded concurrency, budgets, timing
4
+ # markers, JSON artifacts, coverage guard, and aggregate exit status.
5
+ #
6
+ # These tests intentionally exercise the runner with fixtures, --list, and
7
+ # focused scheduler checks, not the complete Firstmate suite.
8
+ set -u
9
+
10
+ # shellcheck source=tests/lib.sh
11
+ . "$(dirname "${BASH_SOURCE[0]}")/lib.sh"
12
+
13
+ RUNNER="$ROOT/bin/fm-test-run.sh"
14
+
15
+ assert_present "$RUNNER" "bin/fm-test-run.sh is missing"
16
+ [ -x "$RUNNER" ] || fail "bin/fm-test-run.sh must be executable"
17
+
18
+ test_list_all_exact_suite_coverage() {
19
+ local listed expected missing extra f
20
+ listed=$("$RUNNER" --list --all | LC_ALL=C sort)
21
+ expected=$(
22
+ for f in "$ROOT"/tests/*.test.sh; do
23
+ [ -f "$f" ] || continue
24
+ printf 'tests/%s\n' "$(basename "$f")"
25
+ done | LC_ALL=C sort
26
+ )
27
+ [ -n "$listed" ] || fail "--list --all printed nothing"
28
+ missing=$(comm -23 <(printf '%s\n' "$expected") <(printf '%s\n' "$listed") || true)
29
+ extra=$(comm -13 <(printf '%s\n' "$expected") <(printf '%s\n' "$listed") || true)
30
+ [ -z "$missing" ] || fail "--list --all missing scripts: $missing"
31
+ [ -z "$extra" ] || fail "--list --all unexpected scripts: $extra"
32
+ # No duplicates.
33
+ [ "$(printf '%s\n' "$listed" | uniq | wc -l | tr -d ' ')" = \
34
+ "$(printf '%s\n' "$listed" | wc -l | tr -d ' ')" ] \
35
+ || fail "--list --all must not duplicate scripts"
36
+ pass "exact suite coverage: --all lists every tests/*.test.sh once"
37
+ }
38
+
39
+ test_family_selection() {
40
+ local listed line
41
+ listed=$("$RUNNER" --list --family pure-contract-unit)
42
+ [ -n "$listed" ] || fail "--family pure-contract-unit selected nothing"
43
+ printf '%s\n' "$listed" | grep -Fq 'tests/fm-test-run.test.sh' \
44
+ || fail "pure-contract-unit must include fm-test-run.test.sh"
45
+ while IFS= read -r line; do
46
+ [ -n "$line" ] || continue
47
+ case "$line" in
48
+ tests/*.test.sh) ;;
49
+ *) fail "family selection produced non-test path: $line" ;;
50
+ esac
51
+ done <<<"$listed"
52
+ # Family mode must not equal the complete suite for a narrow family.
53
+ local all_count fam_count
54
+ all_count=$("$RUNNER" --list --all | wc -l | tr -d ' ')
55
+ fam_count=$(printf '%s\n' "$listed" | wc -l | tr -d ' ')
56
+ [ "$fam_count" -lt "$all_count" ] \
57
+ || fail "pure-contract-unit must be a proper subset of --all"
58
+ pass "family selection returns a proper subset of the suite"
59
+ }
60
+
61
+ test_single_script_selection() {
62
+ local listed
63
+ listed=$("$RUNNER" --list tests/fm-lint.test.sh)
64
+ [ "$listed" = "tests/fm-lint.test.sh" ] \
65
+ || fail "single-script list expected tests/fm-lint.test.sh, got: $listed"
66
+ pass "single-script selection lists exactly that path"
67
+ }
68
+
69
+ test_changed_file_selection_is_conservative() {
70
+ local listed all_count fam_count listed_count
71
+ # A path-mapped pure unit should not expand to --all.
72
+ listed=$("$RUNNER" --list --family pure-contract-unit)
73
+ all_count=$("$RUNNER" --list --all | wc -l | tr -d ' ')
74
+ fam_count=$(printf '%s\n' "$listed" | wc -l | tr -d ' ')
75
+ [ "$fam_count" -lt "$all_count" ] || fail "changed-informed pure family still full suite"
76
+ # Directly exercise --changed: empty or partial selection is ok; must not
77
+ # exceed the suite and must never silently become --all by accident.
78
+ listed=$("$RUNNER" --list --changed --base HEAD 2>/dev/null || true)
79
+ if [ -n "$listed" ]; then
80
+ listed_count=$(printf '%s\n' "$listed" | wc -l | tr -d ' ')
81
+ [ "$listed_count" -le "$all_count" ] || fail "changed selection larger than suite"
82
+ fi
83
+ # A single test path selects only that script (same contract as a
84
+ # tests/*.test.sh change entry in the map).
85
+ listed=$("$RUNNER" --list tests/fm-brief.test.sh)
86
+ [ "$listed" = "tests/fm-brief.test.sh" ] \
87
+ || fail "test-file-only change contract should select one script"
88
+ pass "changed-file selection stays conservative (never silent full suite)"
89
+ }
90
+
91
+ init_changed_fixture_repo() {
92
+ local repo=$1 script
93
+ mkdir -p "$repo/bin" "$repo/tests"
94
+ cp "$RUNNER" "$repo/bin/fm-test-run.sh"
95
+ chmod +x "$repo/bin/fm-test-run.sh"
96
+ for script in \
97
+ fm-brief.test.sh \
98
+ fm-ask-user-authority.test.sh \
99
+ fm-documentation-audiences.test.sh \
100
+ fm-test-isolation-proof.test.sh \
101
+ fm-test-run.test.sh \
102
+ fm-cd-pretool-check.test.sh \
103
+ fm-daemon.test.sh \
104
+ fm-harness-adapter-instructions-live-e2e.test.sh \
105
+ fm-harness-adapter-references.test.sh \
106
+ fm-backend-herdr-smoke.test.sh \
107
+ fm-secondmate-safety.test.sh \
108
+ fm-session-start.test.sh \
109
+ fm-afk-pi-herdr-return-e2e.test.sh \
110
+ fm-backend.test.sh \
111
+ fm-pr-merge.test.sh \
112
+ fm-procevent-quota.test.sh \
113
+ fm-quota-choose.test.sh \
114
+ fm-pi-watch-extension.test.sh \
115
+ fm-afk-return.test.sh \
116
+ fm-bearings-snapshot.test.sh \
117
+ fm-backend-cmux.test.sh \
118
+ fm-backend-zellij.test.sh \
119
+ fm-control-herdr-smoke.test.sh \
120
+ fm-backend-orca.test.sh; do
121
+ printf '#!/usr/bin/env bash\n# tests/lib.sh\n' >"$repo/tests/$script"
122
+ chmod +x "$repo/tests/$script"
123
+ done
124
+ : >"$repo/tests/lib.sh"
125
+ : >"$repo/tests/fm-backend-herdr-eventwait.test.py"
126
+ : >"$repo/bin/fm-supervisor-target-lib.sh"
127
+ : >"$repo/bin/fm-control-lib.sh"
128
+ : >"$repo/bin/fm-timeout-lib.sh"
129
+ : >"$repo/bin/fm-procevent-quota.sh"
130
+ : >"$repo/bin/fm-quota-axi-lib.sh"
131
+ : >"$repo/bin/fm-quota-choose.sh"
132
+ : >"$repo/bin/unmapped-source.sh"
133
+ # A shared helper with no curated family of its own, named by exactly ONE
134
+ # script of the expensive real-Herdr family and consumed by one curated
135
+ # watcher script. This is the shape that made a one-line helper change select
136
+ # every real-Herdr E2E.
137
+ : >"$repo/bin/shared-probe-lib.sh"
138
+ printf '# shared-probe-lib.sh\n' >>"$repo/tests/fm-backend-herdr-smoke.test.sh"
139
+ # shellcheck disable=SC2016 # literal fixture text: the reference must reach
140
+ # the file verbatim so the changed-file scan can find it, not expand here.
141
+ printf '. "$ROOT/bin/shared-probe-lib.sh"\n' >"$repo/bin/fm-watch-probe.sh"
142
+ printf '# .claude/settings.json\n# .pi/extensions/fm-primary-turnend-guard.ts\n' \
143
+ >>"$repo/tests/fm-cd-pretool-check.test.sh"
144
+ printf '# .pi/extensions/fm-primary-pi-watch.ts\n' >>"$repo/tests/fm-pi-watch-extension.test.sh"
145
+ mkdir -p \
146
+ "$repo/.agents/skills/example" \
147
+ "$repo/.agents/skills/harness-adapters/references/common" \
148
+ "$repo/.claude" "$repo/.pi/extensions" "$repo/docs" "$repo/src"
149
+ : >"$repo/.agents/skills/example/SKILL.md"
150
+ : >"$repo/.agents/skills/harness-adapters/SKILL.md"
151
+ : >"$repo/.agents/skills/harness-adapters/references/common/dispatch.md"
152
+ : >"$repo/.claude/settings.json"
153
+ : >"$repo/.pi/extensions/fm-primary-pi-watch.ts"
154
+ : >"$repo/.pi/extensions/fm-primary-turnend-guard.ts"
155
+ : >"$repo/docs/fm-test-isolation-proof.md"
156
+ : >"$repo/CONTRIBUTING.md"
157
+ : >"$repo/src/unmapped.ts"
158
+ git -C "$repo" init -q
159
+ git -C "$repo" add .
160
+ git -C "$repo" -c user.name=test -c user.email=test@example.invalid commit -qm baseline
161
+ }
162
+
163
+ test_changed_runner_surfaces_select_their_family() {
164
+ local tmp repo listed
165
+ tmp=$(mktemp -d "${TMPDIR:-/tmp}/fm-test-run-owner-scope.XXXXXX")
166
+ repo="$tmp/repo"
167
+ init_changed_fixture_repo "$repo"
168
+
169
+ # A change to the runner selects the WHOLE pure-contract-unit family, not
170
+ # just its own contract test. The runner executes every script in that
171
+ # family, so its own test passing proves its logic is right, not that the
172
+ # suite it drives still runs. Narrowing this to the contract owners would
173
+ # also make any wall-clock claim about the changed suite trivially true by
174
+ # not running the work.
175
+ printf '\n' >>"$repo/bin/fm-test-run.sh"
176
+ listed=$(cd "$repo" && bin/fm-test-run.sh --list --changed --base HEAD | LC_ALL=C sort)
177
+ case "$listed" in
178
+ *tests/fm-test-run.test.sh*) ;;
179
+ *) fail "runner change did not select its own contract test: $listed" ;;
180
+ esac
181
+ case "$listed" in
182
+ *tests/fm-brief.test.sh*) ;;
183
+ *) fail "runner change did not select its pure-contract-unit family: $listed" ;;
184
+ esac
185
+ case "$listed" in
186
+ *tests/fm-ask-user-authority.test.sh*) ;;
187
+ *) fail "runner change did not select its pure-contract-unit family: $listed" ;;
188
+ esac
189
+ git -C "$repo" add bin/fm-test-run.sh
190
+ git -C "$repo" -c user.name=test -c user.email=test@example.invalid commit -qm runner-change
191
+
192
+ # The same holds for the surfaces that document that contract.
193
+ printf '\n' >>"$repo/docs/fm-test-isolation-proof.md"
194
+ printf '\n' >>"$repo/CONTRIBUTING.md"
195
+ listed=$(cd "$repo" && bin/fm-test-run.sh --list --changed --base HEAD | LC_ALL=C sort)
196
+ case "$listed" in
197
+ *tests/fm-documentation-audiences.test.sh*) ;;
198
+ *) fail "documentation surface change did not select audience coverage: $listed" ;;
199
+ esac
200
+ case "$listed" in
201
+ *tests/fm-brief.test.sh*) ;;
202
+ *) fail "documentation surface change did not select its curated family: $listed" ;;
203
+ esac
204
+
205
+ rm -rf "$tmp"
206
+ pass "runner and its documentation surfaces select their curated family, not just their contract owners"
207
+ }
208
+
209
+ test_changed_dependency_selection_and_unmapped_failure() {
210
+ local tmp repo listed rc
211
+ tmp=$(mktemp -d "${TMPDIR:-/tmp}/fm-test-run-changed.XXXXXX")
212
+ repo="$tmp/repo"
213
+ init_changed_fixture_repo "$repo"
214
+
215
+ printf '\n' >>"$repo/tests/lib.sh"
216
+ listed=$(cd "$repo" && bin/fm-test-run.sh --list --changed --base HEAD)
217
+ assert_contains "$listed" "tests/fm-pr-merge.test.sh" "shared helper selects pr-forge dependents"
218
+ assert_contains "$listed" "tests/fm-secondmate-safety.test.sh" "shared helper selects secondmate dependents"
219
+ assert_contains "$listed" "tests/fm-bearings-snapshot.test.sh" "shared helper selects snapshot dependents"
220
+ git -C "$repo" add tests/lib.sh
221
+ git -C "$repo" -c user.name=test -c user.email=test@example.invalid commit -qm helper-change
222
+
223
+ printf '\n' >>"$repo/tests/fm-backend-herdr-eventwait.test.py"
224
+ listed=$(cd "$repo" && bin/fm-test-run.sh --list --changed --base HEAD)
225
+ assert_contains "$listed" "tests/fm-backend-herdr-smoke.test.sh" "eventwait test selects Herdr coverage"
226
+ assert_contains "$listed" "tests/fm-backend.test.sh" "eventwait test selects backend coverage"
227
+ git -C "$repo" add tests/fm-backend-herdr-eventwait.test.py
228
+ git -C "$repo" -c user.name=test -c user.email=test@example.invalid commit -qm eventwait-change
229
+
230
+ printf '\n' >>"$repo/bin/fm-supervisor-target-lib.sh"
231
+ listed=$(cd "$repo" && bin/fm-test-run.sh --list --changed --base HEAD)
232
+ assert_contains "$listed" "tests/fm-daemon.test.sh" "supervisor target selects daemon coverage"
233
+ assert_contains "$listed" "tests/fm-afk-return.test.sh" "supervisor target selects afk coverage"
234
+ git -C "$repo" add bin/fm-supervisor-target-lib.sh
235
+ git -C "$repo" -c user.name=test -c user.email=test@example.invalid commit -qm supervisor-change
236
+
237
+ printf '\n' >>"$repo/.agents/skills/example/SKILL.md"
238
+ printf '\n' >>"$repo/.claude/settings.json"
239
+ printf '\n' >>"$repo/.pi/extensions/fm-primary-pi-watch.ts"
240
+ printf '\n' >>"$repo/.pi/extensions/fm-primary-turnend-guard.ts"
241
+ listed=$(cd "$repo" && bin/fm-test-run.sh --list --changed --base HEAD)
242
+ assert_contains "$listed" "tests/fm-ask-user-authority.test.sh" "skill source selects pure contract coverage"
243
+ assert_contains "$listed" "tests/fm-cd-pretool-check.test.sh" "Claude and Pi source selects hook coverage"
244
+ assert_contains "$listed" "tests/fm-pi-watch-extension.test.sh" "Pi source selects watcher coverage"
245
+ git -C "$repo" add .agents .claude .pi
246
+ git -C "$repo" -c user.name=test -c user.email=test@example.invalid commit -qm non-bin-source-change
247
+
248
+ printf '\n' >>"$repo/.agents/skills/harness-adapters/references/common/dispatch.md"
249
+ listed=$(cd "$repo" && bin/fm-test-run.sh --list --changed --base HEAD)
250
+ assert_contains "$listed" "tests/fm-harness-adapter-references.test.sh" "harness adapter reference selects portable structural coverage"
251
+ assert_contains "$listed" "tests/fm-harness-adapter-instructions-live-e2e.test.sh" "harness adapter reference selects opt-in instruction coverage"
252
+ git -C "$repo" add .agents/skills/harness-adapters
253
+ git -C "$repo" -c user.name=test -c user.email=test@example.invalid commit -qm harness-adapter-reference-change
254
+
255
+ printf '\n' >>"$repo/.agents/skills/harness-adapters/SKILL.md"
256
+ listed=$(cd "$repo" && bin/fm-test-run.sh --list --changed --base HEAD)
257
+ assert_contains "$listed" "tests/fm-harness-adapter-references.test.sh" "harness adapter router selects portable structural coverage"
258
+ assert_contains "$listed" "tests/fm-harness-adapter-instructions-live-e2e.test.sh" "harness adapter router selects opt-in instruction coverage"
259
+ git -C "$repo" add .agents/skills/harness-adapters/SKILL.md
260
+ git -C "$repo" -c user.name=test -c user.email=test@example.invalid commit -qm harness-adapter-router-change
261
+
262
+ printf '\n' >>"$repo/bin/fm-procevent-quota.sh"
263
+ printf '\n' >>"$repo/bin/fm-quota-choose.sh"
264
+ listed=$(cd "$repo" && bin/fm-test-run.sh --list --changed --base HEAD)
265
+ assert_contains "$listed" "tests/fm-procevent-quota.test.sh" \
266
+ "quota process-event source selects its focused test"
267
+ assert_contains "$listed" "tests/fm-quota-choose.test.sh" \
268
+ "quota chooser source selects its focused test"
269
+ git -C "$repo" add bin/fm-procevent-quota.sh bin/fm-quota-choose.sh
270
+ git -C "$repo" -c user.name=test -c user.email=test@example.invalid commit -qm quota-source-change
271
+
272
+ printf '\n' >>"$repo/bin/fm-quota-axi-lib.sh"
273
+ listed=$(cd "$repo" && bin/fm-test-run.sh --list --changed --base HEAD)
274
+ assert_contains "$listed" "tests/fm-procevent-quota.test.sh" \
275
+ "shared quota validator selects process-event coverage"
276
+ assert_contains "$listed" "tests/fm-quota-choose.test.sh" \
277
+ "shared quota validator selects chooser coverage"
278
+ git -C "$repo" add bin/fm-quota-axi-lib.sh
279
+ git -C "$repo" -c user.name=test -c user.email=test@example.invalid commit -qm quota-validator-change
280
+
281
+ printf '\n' >>"$repo/bin/fm-control-lib.sh"
282
+ listed=$(cd "$repo" && bin/fm-test-run.sh --list --changed --base HEAD)
283
+ assert_contains "$listed" "tests/fm-backend.test.sh" \
284
+ "control library keeps backend coverage"
285
+ assert_contains "$listed" "tests/fm-session-start.test.sh" \
286
+ "control library keeps session coverage"
287
+ assert_contains "$listed" "tests/fm-quota-choose.test.sh" \
288
+ "control library selects chooser coverage"
289
+ git -C "$repo" add bin/fm-control-lib.sh
290
+ git -C "$repo" -c user.name=test -c user.email=test@example.invalid commit -qm control-lib-change
291
+
292
+ printf '\n' >>"$repo/bin/fm-timeout-lib.sh"
293
+ listed=$(cd "$repo" && bin/fm-test-run.sh --list --changed --base HEAD)
294
+ assert_contains "$listed" "tests/fm-procevent-quota.test.sh" \
295
+ "timeout library selects quota polling coverage"
296
+ git -C "$repo" add bin/fm-timeout-lib.sh
297
+ git -C "$repo" -c user.name=test -c user.email=test@example.invalid commit -qm timeout-lib-change
298
+
299
+ printf '\n' >>"$repo/src/unmapped.ts"
300
+ set +e
301
+ (cd "$repo" && bin/fm-test-run.sh --list --changed --base HEAD) >"$tmp/out" 2>"$tmp/err"
302
+ rc=$?
303
+ set -e
304
+ [ "$rc" -eq 2 ] || fail "unmapped changed source must fail with exit 2, got $rc"
305
+ grep -Fq 'no changed-test mapping for source path: src/unmapped.ts' "$tmp/err" \
306
+ || fail "unmapped changed source failure is not actionable: $(cat "$tmp/err")"
307
+ rm -rf "$tmp"
308
+ pass "changed selection covers dependents and fails closed for unmapped source"
309
+ }
310
+
311
+ # A direct test reference is per-script evidence. Widening it to the referencing
312
+ # test's whole family is what turned a one-line change to a shared helper into
313
+ # every real-Herdr E2E, including scripts with no dependency on it at all.
314
+ # Consumer bin/ scripts must still resolve through the curated map, so recorded
315
+ # family-level coupling is not lost along the way.
316
+ test_changed_bin_reference_selects_per_script_not_per_family() {
317
+ local tmp repo listed
318
+ tmp=$(mktemp -d "${TMPDIR:-/tmp}/fm-test-run-changed-scope.XXXXXX")
319
+ repo="$tmp/repo"
320
+ init_changed_fixture_repo "$repo"
321
+
322
+ printf '\n' >>"$repo/bin/shared-probe-lib.sh"
323
+ listed=$(cd "$repo" && bin/fm-test-run.sh --list --changed --base HEAD)
324
+
325
+ assert_contains "$listed" "tests/fm-backend-herdr-smoke.test.sh" \
326
+ "the one gated script that names the helper must still be selected"
327
+ case "$listed" in
328
+ *tests/fm-control-herdr-smoke.test.sh*)
329
+ fail "a single gated script's reference dragged in its whole family: $listed"
330
+ ;;
331
+ esac
332
+ # The curated consumer keeps its family-level coupling.
333
+ assert_contains "$listed" "tests/fm-daemon.test.sh" \
334
+ "a curated consumer of the helper must still select its whole family"
335
+ assert_contains "$listed" "tests/fm-pi-watch-extension.test.sh" \
336
+ "a curated consumer of the helper must still select its whole family"
337
+
338
+ rm -rf "$tmp"
339
+ pass "a bin reference selects the referencing scripts, and consumers still select their curated families"
340
+ }
341
+
342
+ # Exercise begin/end markers from real fixture processes to prove the automatic
343
+ # changed-suite default and its explicit serial override.
344
+ test_changed_uses_bounded_automatic_concurrency() {
345
+ local tmp repo script serial_shape parallel_shape timeout_repo timeout_script expected_jobs rc
346
+ tmp=$(mktemp -d "${TMPDIR:-/tmp}/fm-test-run-changed-consent.XXXXXX")
347
+ repo="$tmp/repo"
348
+ init_changed_fixture_repo "$repo"
349
+ cp "$ROOT/bin/fm-timeout-lib.sh" "$repo/bin/fm-timeout-lib.sh"
350
+ for script in fm-backend-herdr-smoke.test.sh fm-daemon.test.sh fm-pi-watch-extension.test.sh; do
351
+ cat >"$repo/tests/$script" <<'SH'
352
+ #!/usr/bin/env bash
353
+ sleep 1
354
+ echo "ok - concurrency consent fixture"
355
+ SH
356
+ chmod +x "$repo/tests/$script"
357
+ done
358
+ git -C "$repo" add .
359
+ git -C "$repo" -c user.name=test -c user.email=test@example.invalid commit -qm fixtures
360
+ printf '\n' >>"$repo/bin/shared-probe-lib.sh"
361
+
362
+ (cd "$repo" && bin/fm-test-run.sh --changed --base HEAD --json "$tmp/parallel.json") \
363
+ >"$tmp/parallel.out" 2>"$tmp/parallel.err" \
364
+ || fail "default changed fixture run failed: $(cat "$tmp/parallel.err")"
365
+ parallel_shape=$(grep -E '^FM_TEST_(BEGIN|END)' "$tmp/parallel.out" | head -n 2 | awk '{print $1}' | paste -sd, -)
366
+ [ "$parallel_shape" = FM_TEST_BEGIN,FM_TEST_BEGIN ] \
367
+ || fail "plain --changed did not use bounded concurrent scheduling: $parallel_shape"
368
+
369
+ (cd "$repo" && bin/fm-test-run.sh --changed --base HEAD --jobs 1 --json "$tmp/serial.json") \
370
+ >"$tmp/serial.out" 2>"$tmp/serial.err" \
371
+ || fail "explicit serial changed fixture run failed: $(cat "$tmp/serial.err")"
372
+ serial_shape=$(grep -E '^FM_TEST_(BEGIN|END)' "$tmp/serial.out" | head -n 2 | awk '{print $1}' | paste -sd, -)
373
+ [ "$serial_shape" = FM_TEST_BEGIN,FM_TEST_END ] \
374
+ || fail "explicit --jobs 1 did not force serial execution: $serial_shape"
375
+ expected_jobs=$(getconf _NPROCESSORS_ONLN 2>/dev/null || sysctl -n hw.ncpu 2>/dev/null || echo 1)
376
+ case "$expected_jobs" in
377
+ ''|*[!0-9]*) expected_jobs=1 ;;
378
+ esac
379
+ [ "$expected_jobs" -le 4 ] || expected_jobs=4
380
+ [ "$expected_jobs" -ge 1 ] || expected_jobs=1
381
+ python3 - "$tmp/parallel.json" "$tmp/serial.json" "$expected_jobs" <<'PY' \
382
+ || fail "changed timing artifacts did not record their resolved worker counts"
383
+ import json, sys
384
+ automatic = json.load(open(sys.argv[1], encoding="utf-8"))
385
+ serial = json.load(open(sys.argv[2], encoding="utf-8"))
386
+ expected = int(sys.argv[3])
387
+ assert automatic["selection"].split(";")[-1] == f"jobs={expected}"
388
+ assert serial["selection"].split(";")[-1] == "jobs=1"
389
+ PY
390
+
391
+ timeout_repo="$tmp/timeout-repo"
392
+ timeout_script=tests/fm-calm-pi-extension.test.sh
393
+ mkdir -p "$timeout_repo/bin" "$timeout_repo/tests"
394
+ cp "$RUNNER" "$timeout_repo/bin/fm-test-run.sh"
395
+ cat >"$timeout_repo/bin/fm-timeout-lib.sh" <<'SH'
396
+ fm_run_timed() {
397
+ [ "$1" -eq 900 ] || return 99
398
+ return 124
399
+ }
400
+ SH
401
+ cat >"$timeout_repo/$timeout_script" <<'SH'
402
+ #!/usr/bin/env bash
403
+ touch should-not-run
404
+ echo "not ok - automatic timeout helper was bypassed"
405
+ SH
406
+ chmod +x "$timeout_repo/bin/fm-test-run.sh" "$timeout_repo/$timeout_script"
407
+ git -C "$timeout_repo" init -q
408
+ git -C "$timeout_repo" add .
409
+ git -C "$timeout_repo" -c user.name=test -c user.email=test@example.invalid commit -qm baseline
410
+ printf '\n' >>"$timeout_repo/$timeout_script"
411
+ set +e
412
+ (cd "$timeout_repo" && bin/fm-test-run.sh --changed --base HEAD) \
413
+ >"$tmp/timeout.out" 2>"$tmp/timeout.err"
414
+ rc=$?
415
+ set -e
416
+ [ "$rc" -eq 1 ] || fail "single-script automatic timeout must fail the run, got $rc"
417
+ grep -Eq '^FM_TEST_END .+ tests/fm-calm-pi-extension\.test\.sh exit=124 ' "$tmp/timeout.out" \
418
+ || fail "single unproven changed script did not receive the automatic timeout: $(cat "$tmp/timeout.out")"
419
+ [ ! -e "$timeout_repo/should-not-run" ] || fail "automatic timeout helper did not own the single changed script"
420
+
421
+ rm -rf "$tmp"
422
+ pass "changed defaults to bounded automatic scheduling with serial override"
423
+ }
424
+
425
+ test_empty_selection_emits_summary() {
426
+ local tmp repo out json rc fake_bin real_git
427
+ tmp=$(mktemp -d "${TMPDIR:-/tmp}/fm-test-run-empty.XXXXXX")
428
+ repo="$tmp/repo"
429
+ init_changed_fixture_repo "$repo"
430
+ printf 'documentation only\n' >"$repo/README.md"
431
+ out=$(cd "$repo" && bin/fm-test-run.sh --changed --base HEAD --json "$tmp/artifacts/timing.json" 2>"$tmp/err") \
432
+ || fail "empty valid changed selection must pass"
433
+ printf '%s\n' "$out" | grep -Eq \
434
+ '^FM_TEST_SUMMARY total=0 failed=0 skipped_gate=0 duration_ms=[0-9]+$' \
435
+ || fail "empty selection summary is missing or malformed: $out"
436
+ json="$tmp/artifacts/timing.json"
437
+ python3 -c '
438
+ import json, sys
439
+ doc = json.load(open(sys.argv[1]))
440
+ assert doc["summary"]["total"] == 0
441
+ assert doc["summary"]["failed"] == 0
442
+ assert doc["summary"]["skipped_gate"] == 0
443
+ assert doc["summary"]["duration_ms"] >= 0
444
+ assert doc["scripts"] == []
445
+ assert doc["families"] == []
446
+ ' "$json" || { rm -rf "$tmp"; fail "empty selection JSON summary is wrong"; }
447
+ fake_bin="$tmp/fake-bin"
448
+ real_git=$(command -v git)
449
+ mkdir -p "$fake_bin"
450
+ cat >"$fake_bin/git" <<'SH'
451
+ #!/usr/bin/env bash
452
+ if [ ! -e "$SLOW_GIT_MARKER" ]; then
453
+ : >"$SLOW_GIT_MARKER"
454
+ sleep 1
455
+ fi
456
+ exec "$REAL_GIT" "$@"
457
+ SH
458
+ chmod +x "$fake_bin/git"
459
+ set +e
460
+ (cd "$repo" && PATH="$fake_bin:$PATH" REAL_GIT="$real_git" SLOW_GIT_MARKER="$tmp/slow-git" \
461
+ bin/fm-test-run.sh --changed --base HEAD --max-wall-ms 100) \
462
+ >"$tmp/slow-selection.out" 2>"$tmp/slow-selection.err"
463
+ rc=$?
464
+ set -e
465
+ [ "$rc" -eq 1 ] || fail "an empty run past its budget must fail normally, got $rc"
466
+ grep -Eq '^FM_TEST_SUMMARY total=0 failed=0 skipped_gate=0 duration_ms=[0-9]+$' "$tmp/slow-selection.out" \
467
+ || fail "over-budget empty selection omitted its summary"
468
+ grep -Eq '^FM_TEST_BUDGET max_wall_ms=100 duration_ms=[0-9]+$' "$tmp/slow-selection.out" \
469
+ || fail "over-budget empty selection omitted its budget result"
470
+ [ -e "$tmp/slow-git" ] || fail "the slow selection fixture did not run"
471
+ set +e
472
+ (cd "$repo" && bin/fm-test-run.sh --changed --base HEAD --max-wall-ms nope) \
473
+ >"$tmp/bad-budget.out" 2>"$tmp/bad-budget.err"
474
+ rc=$?
475
+ set -e
476
+ [ "$rc" -eq 2 ] || fail "malformed budget on an empty selection must be refused, got $rc"
477
+ set +e
478
+ (cd "$repo" && bin/fm-test-run.sh --changed --base HEAD --per-script-timeout-secs nope) \
479
+ >"$tmp/bad-timeout.out" 2>"$tmp/bad-timeout.err"
480
+ rc=$?
481
+ set -e
482
+ [ "$rc" -eq 2 ] || fail "malformed timeout on an empty selection must be refused, got $rc"
483
+ rm -rf "$tmp"
484
+ pass "empty changed selection emits deterministic text and JSON summaries"
485
+ }
486
+
487
+ test_timing_markers_and_json() {
488
+ local tmp fixture out json begin_n end_n summary
489
+ tmp=$(mktemp -d "${TMPDIR:-/tmp}/fm-test-run-timing.XXXXXX")
490
+ fixture="$tmp/ok.test.sh"
491
+ out="$tmp/out.txt"
492
+ json="$tmp/timing.json"
493
+ cat >"$fixture" <<'SH'
494
+ #!/usr/bin/env bash
495
+ echo "ok - fixture"
496
+ exit 0
497
+ SH
498
+ chmod +x "$fixture"
499
+ "$RUNNER" --json "$json" "$fixture" >"$out" 2>"$tmp/err.txt" \
500
+ || { rm -rf "$tmp"; fail "runner should pass on a green fixture"; }
501
+ begin_n=$(grep -c '^FM_TEST_BEGIN ' "$out" || true)
502
+ end_n=$(grep -c '^FM_TEST_END ' "$out" || true)
503
+ [ "$begin_n" -eq 1 ] || fail "expected one FM_TEST_BEGIN, got $begin_n"
504
+ [ "$end_n" -eq 1 ] || fail "expected one FM_TEST_END, got $end_n"
505
+ grep -Eq '^FM_TEST_BEGIN .+ family=unclassified expected_gate_skip=none$' "$out" \
506
+ || fail "BEGIN line missing family/expected_gate_skip: $(grep '^FM_TEST_BEGIN' "$out")"
507
+ grep -Eq '^FM_TEST_END .+ exit=0 duration_ms=[0-9]+ gate_skip=false$' "$out" \
508
+ || fail "END line missing exit/duration/gate_skip: $(grep '^FM_TEST_END' "$out")"
509
+ summary=$(grep '^FM_TEST_SUMMARY ' "$out" || true)
510
+ assert_contains "$summary" "total=1" "summary total"
511
+ assert_contains "$summary" "failed=0" "summary failed"
512
+ assert_contains "$summary" "skipped_gate=0" "summary skipped_gate"
513
+ grep -q '^FM_TEST_SLOWEST rank=1 ' "$out" \
514
+ || fail "expected FM_TEST_SLOWEST rank=1"
515
+ [ -f "$json" ] || fail "JSON timing artifact was not written"
516
+ python3 -c 'import json,sys; json.load(open(sys.argv[1]))' "$json" \
517
+ || fail "JSON timing artifact is not valid JSON"
518
+ python3 -c '
519
+ import json, sys
520
+ doc = json.load(open(sys.argv[1]))
521
+ assert "scripts" in doc and len(doc["scripts"]) == 1, doc
522
+ assert doc["scripts"][0]["exit"] == 0
523
+ assert doc["scripts"][0]["gate_skip"] is False
524
+ assert doc["summary"]["total"] == 1
525
+ assert doc["summary"]["failed"] == 0
526
+ assert "duration_ms" in doc["scripts"][0]
527
+ assert "family" in doc["scripts"][0]
528
+ ' "$json" || { rm -rf "$tmp"; fail "JSON timing artifact missing required fields"; }
529
+ rm -rf "$tmp"
530
+ pass "timing markers and JSON artifact are valid"
531
+ }
532
+
533
+ test_aggregate_exit_behavior() {
534
+ local tmp pass_f fail_f rc
535
+ tmp=$(mktemp -d "${TMPDIR:-/tmp}/fm-test-run-agg.XXXXXX")
536
+ pass_f="$tmp/pass.test.sh"
537
+ fail_f="$tmp/fail.test.sh"
538
+ cat >"$pass_f" <<'SH'
539
+ #!/usr/bin/env bash
540
+ echo "ok - pass"
541
+ exit 0
542
+ SH
543
+ cat >"$fail_f" <<'SH'
544
+ #!/usr/bin/env bash
545
+ echo "not ok - fail"
546
+ exit 1
547
+ SH
548
+ chmod +x "$pass_f" "$fail_f"
549
+ set +e
550
+ "$RUNNER" "$pass_f" "$fail_f" >"$tmp/out.txt" 2>"$tmp/err.txt"
551
+ rc=$?
552
+ set -e
553
+ [ "$rc" -ne 0 ] || fail "aggregate exit must be non-zero when any script fails"
554
+ grep -q 'FM_TEST_SUMMARY total=2 failed=1' "$tmp/out.txt" \
555
+ || fail "summary should report total=2 failed=1: $(grep FM_TEST_SUMMARY "$tmp/out.txt")"
556
+ # All-green stays 0.
557
+ set +e
558
+ "$RUNNER" "$pass_f" >"$tmp/out2.txt" 2>"$tmp/err2.txt"
559
+ rc=$?
560
+ set -e
561
+ [ "$rc" -eq 0 ] || { rm -rf "$tmp"; fail "aggregate exit must be 0 when every script passes"; }
562
+ rm -rf "$tmp"
563
+ pass "aggregate exit reflects any script failure"
564
+ }
565
+
566
+ test_gate_skip_accounting() {
567
+ local tmp skip_f out json
568
+ tmp=$(mktemp -d "${TMPDIR:-/tmp}/fm-test-run-skip.XXXXXX")
569
+ skip_f="$tmp/skip.test.sh"
570
+ out="$tmp/out.txt"
571
+ json="$tmp/timing.json"
572
+ cat >"$skip_f" <<'SH'
573
+ #!/usr/bin/env bash
574
+ echo "skip: herdr not found"
575
+ exit 0
576
+ SH
577
+ chmod +x "$skip_f"
578
+ "$RUNNER" --json "$json" "$skip_f" >"$out" 2>"$tmp/err.txt" \
579
+ || fail "gate-skip fixture must exit 0 from the runner"
580
+ grep -Eq '^FM_TEST_END .+ exit=0 duration_ms=[0-9]+ gate_skip=true$' "$out" \
581
+ || fail "END must mark gate_skip=true: $(grep '^FM_TEST_END' "$out")"
582
+ grep -q 'FM_TEST_SUMMARY total=1 failed=0 skipped_gate=1' "$out" \
583
+ || fail "summary must count skipped_gate=1: $(grep FM_TEST_SUMMARY "$out")"
584
+ python3 -c '
585
+ import json, sys
586
+ doc = json.load(open(sys.argv[1]))
587
+ assert doc["scripts"][0]["gate_skip"] is True
588
+ assert doc["summary"]["skipped_gate"] == 1
589
+ assert doc["summary"]["failed"] == 0
590
+ ' "$json" || { rm -rf "$tmp"; fail "JSON gate_skip accounting is wrong"; }
591
+ rm -rf "$tmp"
592
+ pass "gate-skip accounting is honest and non-failing"
593
+ }
594
+
595
+ test_fail_on_gate_skip_token() {
596
+ local tmp skip_f out rc
597
+ tmp=$(mktemp -d "${TMPDIR:-/tmp}/fm-test-run-fail-skip.XXXXXX")
598
+ skip_f="$tmp/skip.test.sh"
599
+ out="$tmp/out.txt"
600
+ cat >"$skip_f" <<'SH'
601
+ #!/usr/bin/env bash
602
+ echo "skip: herdr not found"
603
+ exit 0
604
+ SH
605
+ chmod +x "$skip_f"
606
+ set +e
607
+ "$RUNNER" --fail-on-gate-skip 'herdr not found' "$skip_f" >"$out" 2>"$tmp/err.txt"
608
+ rc=$?
609
+ set -e
610
+ [ "$rc" -ne 0 ] || fail "fail-on-gate-skip must make herdr-not-found a hard failure"
611
+ grep -q 'FM_TEST_SUMMARY total=1 failed=1' "$out" \
612
+ || fail "summary must report failed=1 under fail-on-gate-skip: $(grep FM_TEST_SUMMARY "$out")"
613
+ grep -q 'required gate skip token' "$tmp/err.txt" \
614
+ || fail "runner must log the required gate skip token"
615
+ rm -rf "$tmp"
616
+ pass "fail-on-gate-skip converts herdr-not-found into a hard failure"
617
+ }
618
+
619
+ test_exclude_family() {
620
+ local listed
621
+ listed=$("$RUNNER" --list --all --exclude-family real-herdr-gated)
622
+ printf '%s\n' "$listed" | grep -Fq 'tests/fm-backend-herdr-smoke.test.sh' \
623
+ && fail "exclude-family real-herdr-gated left a real-herdr script"
624
+ printf '%s\n' "$listed" | grep -Fq 'tests/fm-lint.test.sh' \
625
+ || fail "exclude-family must retain pure-contract-unit scripts"
626
+ # Explicit family mode still works; exclude of a different family is a no-op.
627
+ listed=$("$RUNNER" --list --family real-herdr-gated)
628
+ printf '%s\n' "$listed" | grep -Fq 'tests/fm-backend-herdr-smoke.test.sh' \
629
+ || fail "family real-herdr-gated must list smoke test"
630
+ pass "exclude-family drops the named primary family after selection"
631
+ }
632
+
633
+ test_portable_shard_union_and_coverage_guard() {
634
+ local s1 s2 proven serial herdr all_count union_count overlap out first
635
+ s1=$("$RUNNER" --list --lane portable-parallel-1)
636
+ s2=$("$RUNNER" --list --lane portable-parallel-2)
637
+ proven=$("$RUNNER" --list --proven-isolated)
638
+ serial=$("$RUNNER" --list --lane portable-serial)
639
+ herdr=$("$RUNNER" --list --family real-herdr-gated)
640
+ [ -n "$s1" ] && [ -n "$s2" ] || fail "portable parallel shards must be non-empty"
641
+ # Shards disjoint.
642
+ overlap=$(comm -12 <(printf '%s\n' "$s1" | LC_ALL=C sort) <(printf '%s\n' "$s2" | LC_ALL=C sort) || true)
643
+ [ -z "$overlap" ] || fail "portable parallel shards overlap: $overlap"
644
+ # Union of shards equals proven-isolated.
645
+ [ "$(printf '%s\n' "$s1" "$s2" | LC_ALL=C sort -u)" = \
646
+ "$(printf '%s\n' "$proven" | LC_ALL=C sort -u)" ] \
647
+ || fail "shard union must equal proven-isolated set"
648
+ # No herdr in portable lanes.
649
+ printf '%s\n' "$s1" "$s2" "$serial" | grep -Fq 'tests/fm-backend-herdr-smoke.test.sh' \
650
+ && fail "portable lanes must not include real-herdr-gated smoke"
651
+ printf '%s\n' "$herdr" | grep -Fq 'tests/fm-backend-herdr-smoke.test.sh' \
652
+ || fail "herdr family must include smoke"
653
+ out=$("$RUNNER" --check-coverage)
654
+ assert_contains "$out" "FM_TEST_COVERAGE ok" "coverage guard success marker"
655
+ all_count=$("$RUNNER" --list --all | wc -l | tr -d ' ')
656
+ union_count=$(printf '%s\n' "$s1" "$s2" "$serial" "$herdr" | LC_ALL=C sort -u | wc -l | tr -d ' ')
657
+ [ "$union_count" = "$all_count" ] \
658
+ || fail "union of lanes ($union_count) must equal --all ($all_count)"
659
+ # No duplicates across the four partitions.
660
+ [ "$(printf '%s\n' "$s1" "$s2" "$serial" "$herdr" | LC_ALL=C sort | uniq -d | wc -l | tr -d ' ')" = "0" ] \
661
+ || fail "lanes must not duplicate scripts"
662
+ # LPT order: first script of shard 1 is the longest proven script.
663
+ first=$(printf '%s\n' "$s1" | head -n 1)
664
+ [ "$first" = "tests/fm-x-mode.test.sh" ] \
665
+ || fail "shard 1 must start with the longest proven script, got $first"
666
+ pass "portable shard union, disjointness, and coverage guard hold"
667
+ }
668
+
669
+ test_portable_serial_shards_partition_the_serial_lane() {
670
+ local lanes count serial shard listed union dups shard_lane total cap
671
+ lanes=$("$RUNNER" --list-lanes)
672
+ count=$(printf '%s\n' "$lanes" | grep -c '^portable-serial-[0-9]*of[0-9]*$')
673
+ [ "$count" -ge 2 ] || fail "expected at least two portable serial shard lanes, got $count"
674
+ printf '%s\n' "$lanes" | grep -q "^portable-serial-1of${count}\$" \
675
+ || fail "shard lane names must carry the shard count ${count}: $lanes"
676
+
677
+ serial=$("$RUNNER" --list --lane portable-serial | LC_ALL=C sort)
678
+ union=""
679
+ shard=1
680
+ while [ "$shard" -le "$count" ]; do
681
+ shard_lane="portable-serial-${shard}of${count}"
682
+ listed=$("$RUNNER" --list --lane "$shard_lane")
683
+ [ -n "$listed" ] || fail "$shard_lane selected no tests"
684
+ union=$(printf '%s\n%s' "$union" "$listed")
685
+ shard=$((shard + 1))
686
+ done
687
+ union=$(printf '%s\n' "$union" | grep -v '^$' || true)
688
+
689
+ dups=$(printf '%s\n' "$union" | LC_ALL=C sort | uniq -d || true)
690
+ [ -z "$dups" ] || fail "portable serial shards run the same script twice: $dups"
691
+ [ "$(printf '%s\n' "$union" | LC_ALL=C sort)" = "$serial" ] \
692
+ || fail "portable serial shards must exactly cover the portable serial lane"
693
+
694
+ # Every shard carries a real share of the lane, so no degenerate partition
695
+ # leaves one runner doing nearly all of the work the split exists to spread.
696
+ total=$(printf '%s\n' "$serial" | wc -l | tr -d ' ')
697
+ cap=$((total * 6 / 10))
698
+ shard=1
699
+ while [ "$shard" -le "$count" ]; do
700
+ listed=$("$RUNNER" --list --lane "portable-serial-${shard}of${count}" | wc -l | tr -d ' ')
701
+ [ "$listed" -ge 2 ] \
702
+ || fail "portable-serial-${shard}of${count} holds only $listed script(s)"
703
+ [ "$listed" -le "$cap" ] \
704
+ || fail "portable-serial-${shard}of${count} holds $listed of $total scripts"
705
+ shard=$((shard + 1))
706
+ done
707
+
708
+ # Assignment is deterministic across invocations.
709
+ [ "$("$RUNNER" --list --lane "portable-serial-1of${count}")" = \
710
+ "$("$RUNNER" --list --lane "portable-serial-1of${count}")" ] \
711
+ || fail "portable serial shard membership must be deterministic"
712
+ pass "portable serial shards are a deterministic disjoint cover of the serial lane"
713
+ }
714
+
715
+ test_portable_serial_shard_lane_refusals() {
716
+ local tmp count rc other
717
+ tmp=$(mktemp -d "${TMPDIR:-/tmp}/fm-test-run-shard-lane.XXXXXX")
718
+ count=$("$RUNNER" --list-lanes | grep -c '^portable-serial-[0-9]*of[0-9]*$')
719
+ other=$((count + 1))
720
+
721
+ # A lane built for a different shard count must refuse rather than run a
722
+ # partial suite: this is what keeps a CI matrix from silently dropping tests.
723
+ set +e
724
+ "$RUNNER" --list --lane "portable-serial-1of${other}" >"$tmp/out" 2>"$tmp/err"
725
+ rc=$?
726
+ set -e
727
+ [ "$rc" -eq 2 ] || fail "mismatched shard count must refuse (exit 2), got $rc"
728
+ [ ! -s "$tmp/out" ] || fail "mismatched shard count must not list tests"
729
+ grep -Fq "configured for $count" "$tmp/err" \
730
+ || fail "mismatch refusal must name the configured count: $(cat "$tmp/err")"
731
+
732
+ set +e
733
+ "$RUNNER" --list --lane "portable-serial-$((count + 1))of${count}" >"$tmp/out2" 2>"$tmp/err2"
734
+ rc=$?
735
+ set -e
736
+ [ "$rc" -eq 2 ] || fail "out-of-range shard index must refuse (exit 2), got $rc"
737
+ grep -Fq "outside 1..$count" "$tmp/err2" \
738
+ || fail "range refusal message missing: $(cat "$tmp/err2")"
739
+
740
+ set +e
741
+ "$RUNNER" --list --lane portable-serial-1 >"$tmp/out3" 2>"$tmp/err3"
742
+ rc=$?
743
+ set -e
744
+ [ "$rc" -eq 2 ] || fail "shard lane without a count must refuse (exit 2), got $rc"
745
+ rm -rf "$tmp"
746
+ pass "portable serial shard lanes refuse mismatched, out-of-range, and countless names"
747
+ }
748
+
749
+ test_jobs_requires_proven_isolated() {
750
+ local tmp rc shard_lane
751
+ tmp=$(mktemp -d "${TMPDIR:-/tmp}/fm-test-run-jobs.XXXXXX")
752
+ set +e
753
+ "$RUNNER" --jobs 2 --lane portable-serial >"$tmp/out" 2>"$tmp/err"
754
+ rc=$?
755
+ set -e
756
+ [ "$rc" -eq 2 ] || fail "--jobs with portable-serial must refuse (exit 2), got $rc"
757
+ grep -Fq 'not in the proven-isolated set' "$tmp/err" \
758
+ || fail "--jobs refusal message missing: $(cat "$tmp/err")"
759
+ set +e
760
+ "$RUNNER" --jobs 2 tests/fm-afk-inject-e2e.test.sh >"$tmp/out2" 2>"$tmp/err2"
761
+ rc=$?
762
+ set -e
763
+ [ "$rc" -eq 2 ] || fail "--jobs on a family with no recorded proof must refuse, got $rc"
764
+ # Sharding across runners never relaxes the serial rule inside one shard.
765
+ shard_lane=$("$RUNNER" --list-lanes | grep -m1 '^portable-serial-[0-9]*of[0-9]*$')
766
+ set +e
767
+ "$RUNNER" --jobs 2 --lane "$shard_lane" >"$tmp/out3" 2>"$tmp/err3"
768
+ rc=$?
769
+ set -e
770
+ [ "$rc" -eq 2 ] || fail "--jobs with a portable serial shard must refuse, got $rc"
771
+ grep -Fq 'not in the proven-isolated set' "$tmp/err3" \
772
+ || fail "shard --jobs refusal message missing: $(cat "$tmp/err3")"
773
+ rm -rf "$tmp"
774
+ pass "--jobs refuses non-proven / stateful selections"
775
+ }
776
+
777
+ # The complement of the refusal above: a family carrying a recorded concurrent
778
+ # proof is admitted and actually scheduled, so the admission rule is two-sided
779
+ # rather than a blanket refusal that happens to pass its negative cases.
780
+ test_jobs_admits_a_concurrent_safe_family() {
781
+ local tmp rc external
782
+ tmp=$(mktemp -d "${TMPDIR:-/tmp}/fm-test-run-jobs-admit.XXXXXX")
783
+ # --list exits before the admission guard, so this has to be a real run for
784
+ # the assertion to mean anything. Two cheap watcher-wake-lock scripts exercise
785
+ # admission and the concurrent scheduler for real.
786
+ set +e
787
+ "$RUNNER" --jobs 2 \
788
+ tests/fm-supervision-events.test.sh tests/fm-session-lock-ancestry.test.sh \
789
+ >"$tmp/out" 2>"$tmp/err"
790
+ rc=$?
791
+ set -e
792
+ [ "$rc" -eq 0 ] || fail "--jobs on a proven family must be admitted, got $rc: $(cat "$tmp/err") $(cat "$tmp/out")"
793
+ grep -Fq 'FM_TEST_SUMMARY total=2 failed=0' "$tmp/out" \
794
+ || fail "the admitted concurrent run did not report both scripts green: $(cat "$tmp/out")"
795
+
796
+ set +e
797
+ "$RUNNER" --jobs 5 tests/fm-session-lock-ancestry.test.sh \
798
+ >"$tmp/over-cap.out" 2>"$tmp/over-cap.err"
799
+ rc=$?
800
+ set -e
801
+ [ "$rc" -eq 2 ] || fail "a family run above its proven four-worker cap must be refused, got $rc"
802
+
803
+ external="$tmp/fm-session-lock-ancestry.test.sh"
804
+ printf '#!/usr/bin/env bash\necho "ok - colliding external fixture"\n' >"$external"
805
+ chmod +x "$external"
806
+ set +e
807
+ "$RUNNER" --jobs 2 "$external" >"$tmp/external.out" 2>"$tmp/external.err"
808
+ rc=$?
809
+ set -e
810
+ [ "$rc" -eq 2 ] \
811
+ || fail "an external script colliding with a proven family member must be refused, got $rc"
812
+ rm -rf "$tmp"
813
+ pass "--jobs admits and schedules a family with a recorded concurrent proof"
814
+ }
815
+
816
+ # Workers are handed scripts in order, so the slowest script must start first or
817
+ # it runs alone at the tail and throws away most of the concurrency.
818
+ test_concurrent_runs_are_ordered_longest_first() {
819
+ local tmp listed first
820
+ tmp=$(mktemp -d "${TMPDIR:-/tmp}/fm-test-run-order.XXXXXX")
821
+ set +e
822
+ "$RUNNER" --jobs 2 --family watcher-wake-lock --list >"$tmp/serial" 2>&1
823
+ set -e
824
+ # The scheduler reorders the real run, so assert on the begin-marker order of
825
+ # a real concurrent run over scripts whose hints differ by a wide margin.
826
+ set +e
827
+ "$RUNNER" --jobs 2 \
828
+ tests/fm-session-lock-ancestry.test.sh tests/fm-task-inbox.test.sh \
829
+ >"$tmp/out" 2>"$tmp/err"
830
+ set -e
831
+ first=$(grep -m1 '^FM_TEST_BEGIN' "$tmp/out" | awk '{print $3}')
832
+ [ "$first" = tests/fm-task-inbox.test.sh ] \
833
+ || fail "concurrent run did not start the longest script first, started: $first"
834
+ rm -rf "$tmp"
835
+ pass "a concurrent run starts the longest-hint script first"
836
+ }
837
+
838
+ # --max-wall-ms is checked after the run, so it cannot end a run that never
839
+ # finishes. A hung script has to become a bounded failure, because an unbounded
840
+ # suite is exactly what silently outruns its caller's invocation budget.
841
+ test_per_script_timeout_bounds_a_hang() {
842
+ local tmp repo runner hang rc began ended grandchild_pid grandchild waited
843
+ tmp=$(mktemp -d "${TMPDIR:-/tmp}/fm-test-run-hang.XXXXXX")
844
+ repo="$tmp/repo"
845
+ runner="$repo/bin/fm-test-run.sh"
846
+ hang=tests/fm-hang-fixture.test.sh
847
+ mkdir -p "$repo/bin" "$repo/tests"
848
+ cp "$RUNNER" "$runner"
849
+ cp "$ROOT/bin/fm-timeout-lib.sh" "$repo/bin/fm-timeout-lib.sh"
850
+ grandchild_pid="$tmp/grandchild.pid"
851
+ cat >"$repo/$hang" <<'SH'
852
+ #!/usr/bin/env bash
853
+ echo "ok - fixture is about to hang"
854
+ sh -c 'trap "" TERM; echo $$ >"$1"; sleep 600' _ "$GRANDCHILD_PID" &
855
+ sleep 600
856
+ SH
857
+ chmod +x "$runner" "$repo/$hang"
858
+
859
+ began=$(date +%s)
860
+ set +e
861
+ GRANDCHILD_PID="$grandchild_pid" \
862
+ "$runner" --per-script-timeout-secs 3 "$hang" >"$tmp/out" 2>"$tmp/err"
863
+ rc=$?
864
+ set -e
865
+ ended=$(date +%s)
866
+
867
+ [ "$rc" -ne 0 ] || fail "a terminated script must fail the run: $(cat "$tmp/out")"
868
+ [ "$((ended - began))" -lt 120 ] \
869
+ || fail "the per-script bound did not stop a 600s hang (took $((ended - began))s)"
870
+ grep -Fq 'exceeded the per-script bound' "$tmp/out" \
871
+ || fail "the terminated script was not named: $(cat "$tmp/out")"
872
+ grep -Eq 'FM_TEST_END .* exit=124 ' "$tmp/out" \
873
+ || fail "a terminated script must be recorded as exit 124: $(cat "$tmp/out")"
874
+ # The run still completes and accounts for the script, rather than dying.
875
+ grep -Fq 'FM_TEST_SUMMARY total=1 failed=1' "$tmp/out" \
876
+ || fail "the bounded run did not report a complete summary: $(cat "$tmp/out")"
877
+ [ -s "$grandchild_pid" ] || fail "the hanging fixture did not record its grandchild"
878
+ grandchild=$(cat "$grandchild_pid")
879
+ waited=0
880
+ while kill -0 "$grandchild" 2>/dev/null && [ "$waited" -lt 50 ]; do
881
+ sleep 0.1
882
+ waited=$((waited + 1))
883
+ done
884
+ if kill -0 "$grandchild" 2>/dev/null; then
885
+ kill -KILL "$grandchild" 2>/dev/null || true
886
+ fail "the timed-out script left grandchild $grandchild running"
887
+ fi
888
+
889
+ # 0 keeps the historical unbounded behavior, so no existing caller changes.
890
+ set +e
891
+ "$runner" --per-script-timeout-secs nope "$hang" >"$tmp/o2" 2>"$tmp/e2"
892
+ rc=$?
893
+ set -e
894
+ [ "$rc" -eq 2 ] || fail "--per-script-timeout-secs with a non-number must be refused, got $rc"
895
+
896
+ rm -rf "$tmp"
897
+ pass "--per-script-timeout-secs turns a hung script into a bounded failure"
898
+ }
899
+
900
+ # The duration regression this guard exists for: a suite whose scripts are all
901
+ # green but whose wall clock outgrew its caller's invocation budget. The caller
902
+ # gets killed mid-run and retries invisibly, so an over-budget run has to be a
903
+ # failure, not a note in the log.
904
+ test_max_wall_ms_is_a_result_not_advice() {
905
+ local tmp repo runner fast rc summary_duration budget_duration
906
+ tmp=$(mktemp -d "${TMPDIR:-/tmp}/fm-test-run-budget.XXXXXX")
907
+ repo="$tmp/repo"
908
+ runner="$repo/bin/fm-test-run.sh"
909
+ fast=tests/fm-budget-fixture.test.sh
910
+ mkdir -p "$repo/bin" "$repo/tests"
911
+ cp "$RUNNER" "$runner"
912
+ cat >"$repo/$fast" <<'SH'
913
+ #!/usr/bin/env bash
914
+ sleep 1
915
+ echo "ok - budget fixture"
916
+ SH
917
+ chmod +x "$runner" "$repo/$fast"
918
+
919
+ # Comfortably inside budget: the run passes and states the budget it met.
920
+ set +e
921
+ "$runner" --max-wall-ms 60000 "$fast" >"$tmp/under" 2>"$tmp/under.err"
922
+ rc=$?
923
+ set -e
924
+ [ "$rc" -eq 0 ] || fail "a run inside its budget must pass, got $rc: $(cat "$tmp/under.err")"
925
+ grep -Eq '^FM_TEST_BUDGET max_wall_ms=60000 duration_ms=[0-9]+$' "$tmp/under" \
926
+ || fail "an inside-budget run did not report the budget: $(cat "$tmp/under")"
927
+
928
+ # Same green script, budget it cannot meet: the run must FAIL.
929
+ set +e
930
+ "$runner" --max-wall-ms 500 "$fast" >"$tmp/over" 2>"$tmp/over.err"
931
+ rc=$?
932
+ set -e
933
+ [ "$rc" -eq 1 ] || fail "an over-budget run must fail through the result path, got $rc"
934
+ grep -Eq '^FM_TEST_SUMMARY total=1 failed=0 skipped_gate=0 duration_ms=[0-9]+$' "$tmp/over" \
935
+ || fail "an over-budget run omitted its summary: $(cat "$tmp/over")"
936
+ grep -Eq '^FM_TEST_SUMMARY_FAMILY .+$' "$tmp/over" \
937
+ || fail "an over-budget run omitted its family summary: $(cat "$tmp/over")"
938
+ grep -Eq '^FM_TEST_SLOWEST rank=1 .+$' "$tmp/over" \
939
+ || fail "an over-budget run omitted its slowest result: $(cat "$tmp/over")"
940
+ grep -Eq '^FM_TEST_BUDGET max_wall_ms=500 duration_ms=[0-9]+$' "$tmp/over" \
941
+ || fail "an over-budget run omitted its budget result: $(cat "$tmp/over")"
942
+ summary_duration=$(awk '/^FM_TEST_SUMMARY / { for (i=1;i<=NF;i++) if ($i ~ /^duration_ms=/) { sub(/^duration_ms=/, "", $i); print $i } }' "$tmp/over")
943
+ budget_duration=$(awk '/^FM_TEST_BUDGET / { for (i=1;i<=NF;i++) if ($i ~ /^duration_ms=/) { sub(/^duration_ms=/, "", $i); print $i } }' "$tmp/over")
944
+ [ "$budget_duration" = "$summary_duration" ] \
945
+ || fail "budget verdict used a different duration than the summary: $(cat "$tmp/over")"
946
+
947
+ # A malformed budget is refused rather than silently ignored.
948
+ set +e
949
+ "$runner" --max-wall-ms 0 "$fast" >"$tmp/bad" 2>"$tmp/bad.err"
950
+ rc=$?
951
+ set -e
952
+ [ "$rc" -eq 2 ] || fail "--max-wall-ms 0 must be refused (exit 2), got $rc"
953
+ set +e
954
+ "$runner" --max-wall-ms nope "$fast" >"$tmp/bad2" 2>"$tmp/bad2.err"
955
+ rc=$?
956
+ set -e
957
+ [ "$rc" -eq 2 ] || fail "--max-wall-ms with a non-number must be refused (exit 2), got $rc"
958
+
959
+ rm -rf "$tmp"
960
+ pass "--max-wall-ms fails an over-budget run and refuses a malformed budget"
961
+ }
962
+
963
+ test_jobs_parallel_scheduler_and_failure_propagation() {
964
+ local tmp repo runner evidence fake_bin a b c d rc begin_n end_n
965
+ tmp=$(mktemp -d "${TMPDIR:-/tmp}/fm-test-run-jobs-sched.XXXXXX")
966
+ repo="$tmp/repo"
967
+ runner="$repo/bin/fm-test-run.sh"
968
+ evidence="$tmp/evidence"
969
+ fake_bin="$tmp/fake-bin"
970
+ a=tests/fm-brief.test.sh
971
+ b=tests/fm-composer-lib.test.sh
972
+ c=tests/fm-lint.test.sh
973
+ d=tests/fm-supervision-instructions.test.sh
974
+ mkdir -p "$repo/bin" "$repo/tests" "$evidence" "$fake_bin"
975
+ cp "$RUNNER" "$runner"
976
+ cat >"$fake_bin/stat" <<'SH'
977
+ #!/usr/bin/env bash
978
+ if [ "$1" = "-c" ] && [ "$2" = "%a" ]; then
979
+ printf '700\n'
980
+ exit 0
981
+ fi
982
+ if [ "$1" = "-f" ] && [ "$2" = "%Lp" ]; then
983
+ printf ' File: "%s"\n ID: fake Namelen: 255 Type: ext2/ext3\n700\n' "$3"
984
+ exit 0
985
+ fi
986
+ exit 1
987
+ SH
988
+ # The slow fixture blocks on the replacement fixture's own signal rather than
989
+ # a wall-clock sleep, so a loaded machine cannot let it finish first and turn
990
+ # a correct scheduler into a failure. The bounded deadline is only there so a
991
+ # scheduler that really does wait for the oldest worker still reports instead
992
+ # of hanging.
993
+ cat >"$repo/$a" <<'SH'
994
+ #!/usr/bin/env bash
995
+ if [ -n "${SCHED_WAIT_FOR_REPLACEMENT:-}" ]; then
996
+ waited=0
997
+ while [ ! -e "$SCHED_EVIDENCE/replacement-started" ] && [ "$waited" -lt 600 ]; do
998
+ sleep 0.05
999
+ waited=$((waited + 1))
1000
+ done
1001
+ fi
1002
+ touch "$SCHED_EVIDENCE/slow-done"
1003
+ echo "ok - slow fixture"
1004
+ SH
1005
+ cat >"$repo/$b" <<'SH'
1006
+ #!/usr/bin/env bash
1007
+ echo "ok - fast fixture"
1008
+ SH
1009
+ cat >"$repo/$c" <<'SH'
1010
+ #!/usr/bin/env bash
1011
+ # Read the evidence before releasing the slow fixture, so the release can never
1012
+ # race ahead of the check it is being used to make.
1013
+ if [ -e "$SCHED_EVIDENCE/slow-done" ]; then
1014
+ touch "$SCHED_EVIDENCE/replacement-started"
1015
+ echo "not ok - scheduler waited for oldest worker"
1016
+ exit 1
1017
+ fi
1018
+ touch "$SCHED_EVIDENCE/replacement-started"
1019
+ echo "ok - replacement fixture started before slow fixture finished"
1020
+ SH
1021
+ chmod +x "$runner" "$repo/$a" "$repo/$b" "$repo/$c" "$fake_bin/stat"
1022
+ set +e
1023
+ PATH="$fake_bin:$PATH" SCHED_EVIDENCE="$evidence" SCHED_WAIT_FOR_REPLACEMENT=1 \
1024
+ "$runner" --jobs 2 --json "$tmp/timing.json" \
1025
+ "$a" "$b" "$c" >"$tmp/out" 2>"$tmp/err"
1026
+ rc=$?
1027
+ set -e
1028
+ [ "$rc" -eq 0 ] || { cat "$tmp/out" "$tmp/err"; rm -rf "$tmp"; fail "jobs=2 must refill the first completed slot"; }
1029
+ begin_n=$(grep -c '^FM_TEST_BEGIN ' "$tmp/out" || true)
1030
+ end_n=$(grep -c '^FM_TEST_END ' "$tmp/out" || true)
1031
+ [ "$begin_n" -eq 3 ] || fail "expected 3 BEGIN markers, got $begin_n"
1032
+ [ "$end_n" -eq 3 ] || fail "expected 3 END markers, got $end_n"
1033
+ grep -q 'FM_TEST_SUMMARY total=3 failed=0' "$tmp/out" \
1034
+ || fail "summary missing for jobs run: $(grep FM_TEST_SUMMARY "$tmp/out")"
1035
+ python3 -c '
1036
+ import json,sys
1037
+ doc=json.load(open(sys.argv[1]))
1038
+ assert doc["summary"]["total"]==3
1039
+ assert doc["summary"]["failed"]==0
1040
+ assert "jobs=2" in doc["selection"]
1041
+ ' "$tmp/timing.json" || { rm -rf "$tmp"; fail "jobs JSON artifact wrong"; }
1042
+
1043
+ # Non-proven path is refused before any worker starts (no race masking).
1044
+ cat >"$tmp/fail.test.sh" <<'SH'
1045
+ #!/usr/bin/env bash
1046
+ echo "not ok - deliberate fail"
1047
+ exit 1
1048
+ SH
1049
+ chmod +x "$tmp/fail.test.sh"
1050
+ set +e
1051
+ "$runner" --jobs 2 "$a" "$tmp/fail.test.sh" >"$tmp/out3" 2>"$tmp/err3"
1052
+ rc=$?
1053
+ set -e
1054
+ [ "$rc" -eq 2 ] || fail "jobs with non-proven fail fixture must refuse before run, got $rc"
1055
+
1056
+ # Parallel failure propagation stays inside the private runner fixture.
1057
+ cat >"$repo/$b" <<'SH'
1058
+ #!/usr/bin/env bash
1059
+ echo "not ok - deliberate proven-set fail"
1060
+ exit 1
1061
+ SH
1062
+ chmod +x "$repo/$b"
1063
+ set +e
1064
+ SCHED_EVIDENCE="$evidence" "$runner" --jobs 2 "$a" "$b" >"$tmp/out4" 2>"$tmp/err4"
1065
+ rc=$?
1066
+ set -e
1067
+ [ "$rc" -ne 0 ] || { rm -rf "$tmp"; fail "jobs aggregate must be non-zero when a proven worker fails"; }
1068
+ grep -q 'FM_TEST_SUMMARY total=2 failed=1' "$tmp/out4" \
1069
+ || { rm -rf "$tmp"; fail "jobs failure summary wrong: $(grep FM_TEST_SUMMARY "$tmp/out4")"; }
1070
+
1071
+ cat >"$repo/$d" <<'SH'
1072
+ #!/usr/bin/env bash
1073
+ echo "skip: herdr not found" >&2
1074
+ exit 0
1075
+ SH
1076
+ chmod +x "$repo/$d"
1077
+ set +e
1078
+ "$runner" --jobs 2 --fail-on-gate-skip 'herdr not found' "$d" >"$tmp/out5" 2>"$tmp/err5"
1079
+ rc=$?
1080
+ set -e
1081
+ [ "$rc" -ne 0 ] || { rm -rf "$tmp"; fail "parallel stderr gate skip must hard-fail"; }
1082
+ grep -q 'FM_TEST_SUMMARY total=1 failed=1' "$tmp/out5" \
1083
+ || { rm -rf "$tmp"; fail "parallel stderr hard-fail summary wrong: $(grep FM_TEST_SUMMARY "$tmp/out5")"; }
1084
+
1085
+ "$runner" --jobs 2 "$d" >"$tmp/out6" 2>"$tmp/err6" \
1086
+ || { rm -rf "$tmp"; fail "ordinary parallel stderr gate skip must remain successful"; }
1087
+ grep -Eq '^FM_TEST_END .+ exit=0 duration_ms=[0-9]+ gate_skip=true$' "$tmp/out6" \
1088
+ || { rm -rf "$tmp"; fail "parallel stderr gate skip was not recorded"; }
1089
+ grep -q 'FM_TEST_SUMMARY total=1 failed=0 skipped_gate=1' "$tmp/out6" \
1090
+ || { rm -rf "$tmp"; fail "parallel stderr skip summary wrong: $(grep FM_TEST_SUMMARY "$tmp/out6")"; }
1091
+
1092
+ rm -rf "$tmp"
1093
+ pass "jobs scheduler runs proven scripts; failure propagates; non-proven refused"
1094
+ }
1095
+
1096
+ test_herdr_ci_family_run_has_a_step_timeout() {
1097
+ # The required Herdr lane's hang tripwire is the family-run *step* bound, not
1098
+ # the 75-minute job cap. Parse the workflow as YAML so nested `with.name`
1099
+ # artifact keys cannot masquerade as the step contract.
1100
+ command -v ruby >/dev/null 2>&1 \
1101
+ || fail "ruby is required to parse .github/workflows/ci.yml as YAML"
1102
+ local json job_timeout step_timeout
1103
+ json=$(ruby -ryaml -rjson -e '
1104
+ doc = YAML.load_file(ARGV[0])
1105
+ job = doc.fetch("jobs").fetch("tests-herdr")
1106
+ step = job.fetch("steps").find { |s|
1107
+ s.is_a?(Hash) && s["name"] == "Run real-Herdr family (serial, required)"
1108
+ }
1109
+ raise "missing family-run step" if step.nil?
1110
+ raise "family-run step has no timeout-minutes" unless step.key?("timeout-minutes")
1111
+ puts JSON.generate(
1112
+ "job_timeout" => job.fetch("timeout-minutes"),
1113
+ "step_timeout" => step.fetch("timeout-minutes")
1114
+ )
1115
+ ' "$ROOT/.github/workflows/ci.yml") \
1116
+ || fail "could not parse tests-herdr timeouts from ci.yml"
1117
+ job_timeout=$(python3 -c 'import json,sys; print(json.load(sys.stdin)["job_timeout"])' <<<"$json") \
1118
+ || fail "could not read job timeout from parsed workflow"
1119
+ step_timeout=$(python3 -c 'import json,sys; print(json.load(sys.stdin)["step_timeout"])' <<<"$json") \
1120
+ || fail "could not read step timeout from parsed workflow"
1121
+ [ "$job_timeout" = 75 ] \
1122
+ || fail "tests-herdr job backstop must stay 75 minutes, got $job_timeout"
1123
+ [ "$step_timeout" = 20 ] \
1124
+ || fail "family-run step timeout must be 20 minutes, got $step_timeout"
1125
+ [ "$step_timeout" -lt "$job_timeout" ] \
1126
+ || fail "family-run step timeout must be below the job backstop"
1127
+ pass "Herdr CI family-run step times out at 20 min under a 75 min job backstop"
1128
+ }
1129
+
1130
+ test_aggregate_json() {
1131
+ local tmp a b
1132
+ tmp=$(mktemp -d "${TMPDIR:-/tmp}/fm-test-run-aggjson.XXXXXX")
1133
+ cat >"$tmp/a.json" <<'JSON'
1134
+ {
1135
+ "run_id": "a",
1136
+ "selection": "lane=portable-parallel-1",
1137
+ "started_at": "2026-07-22T00:00:00Z",
1138
+ "finished_at": "2026-07-22T00:01:00Z",
1139
+ "summary": {"total": 1, "failed": 0, "skipped_gate": 0, "duration_ms": 1000},
1140
+ "scripts": [{"path": "tests/a.test.sh", "family": "pure-contract-unit", "duration_ms": 1000, "exit": 0, "gate_skip": false}]
1141
+ }
1142
+ JSON
1143
+ cat >"$tmp/b.json" <<'JSON'
1144
+ {
1145
+ "run_id": "b",
1146
+ "selection": "lane=portable-serial",
1147
+ "started_at": "2026-07-22T00:00:00Z",
1148
+ "finished_at": "2026-07-22T00:02:00Z",
1149
+ "summary": {"total": 2, "failed": 1, "skipped_gate": 0, "duration_ms": 2000},
1150
+ "scripts": [
1151
+ {"path": "tests/b.test.sh", "family": "afk", "duration_ms": 1500, "exit": 1, "gate_skip": false},
1152
+ {"path": "tests/c.test.sh", "family": "afk", "duration_ms": 500, "exit": 0, "gate_skip": false}
1153
+ ]
1154
+ }
1155
+ JSON
1156
+ out=$("$RUNNER" --aggregate-json "$tmp/out.json" "$tmp/a.json" "$tmp/b.json")
1157
+ assert_contains "$out" "FM_TEST_AGGREGATE lanes=2 total=3 failed=1" "aggregate summary line"
1158
+ python3 -c '
1159
+ import json,sys
1160
+ doc=json.load(open(sys.argv[1]))
1161
+ assert doc["kind"]=="aggregate"
1162
+ assert doc["summary"]["lanes"]==2
1163
+ assert doc["summary"]["total"]==3
1164
+ assert doc["summary"]["failed"]==1
1165
+ assert doc["summary"]["critical_path_duration_ms"]==2000
1166
+ assert len(doc["scripts"])==3
1167
+ ' "$tmp/out.json" || { rm -rf "$tmp"; fail "aggregate JSON shape wrong"; }
1168
+ rm -rf "$tmp"
1169
+ pass "aggregate-json merges lane timing artifacts"
1170
+ }
1171
+
1172
+ test_list_all_exact_suite_coverage
1173
+ test_family_selection
1174
+ test_single_script_selection
1175
+ test_changed_file_selection_is_conservative
1176
+ test_changed_runner_surfaces_select_their_family
1177
+ test_changed_dependency_selection_and_unmapped_failure
1178
+ test_changed_bin_reference_selects_per_script_not_per_family
1179
+ test_changed_uses_bounded_automatic_concurrency
1180
+ test_empty_selection_emits_summary
1181
+ test_timing_markers_and_json
1182
+ test_aggregate_exit_behavior
1183
+ test_gate_skip_accounting
1184
+ test_fail_on_gate_skip_token
1185
+ test_exclude_family
1186
+ test_portable_shard_union_and_coverage_guard
1187
+ test_portable_serial_shards_partition_the_serial_lane
1188
+ test_portable_serial_shard_lane_refusals
1189
+ test_jobs_requires_proven_isolated
1190
+ test_jobs_admits_a_concurrent_safe_family
1191
+ test_concurrent_runs_are_ordered_longest_first
1192
+ test_per_script_timeout_bounds_a_hang
1193
+ test_max_wall_ms_is_a_result_not_advice
1194
+ test_jobs_parallel_scheduler_and_failure_propagation
1195
+ test_herdr_ci_family_run_has_a_step_timeout
1196
+ test_aggregate_json