@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,2191 @@
1
+ #!/usr/bin/env bash
2
+ # fm-test-run.sh - single owner of Firstmate's behavior-test runner, lane
3
+ # composition for portable CI shards, local --jobs for proven-concurrent work,
4
+ # timing markers, and the complete-regression coverage guard.
5
+ #
6
+ # Selection modes (exactly one of: --all, --family, --changed, --lane,
7
+ # --proven-isolated, or script paths):
8
+ # fm-test-run.sh --all
9
+ # fm-test-run.sh --family <name>
10
+ # fm-test-run.sh --changed [--base <git-ref>]
11
+ # fm-test-run.sh --lane portable-parallel-1|portable-parallel-2|portable-serial
12
+ # fm-test-run.sh --lane portable-serial-<k>of<n> (one CI serial shard)
13
+ # fm-test-run.sh --proven-isolated
14
+ # fm-test-run.sh tests/<name>.test.sh [more scripts...]
15
+ #
16
+ # Inspection (no execution):
17
+ # fm-test-run.sh --list --all
18
+ # fm-test-run.sh --list --family <name>
19
+ # fm-test-run.sh --list --lane portable-parallel-1
20
+ # fm-test-run.sh --list-scheduled --family <name>
21
+ # fm-test-run.sh --list-families
22
+ # fm-test-run.sh --list-concurrent-safe-families
23
+ # fm-test-run.sh --concurrent-safe-family-jobs-max <name>
24
+ # fm-test-run.sh --list-lanes
25
+ # fm-test-run.sh --check-coverage
26
+ #
27
+ # Aggregation (no suite execution):
28
+ # fm-test-run.sh --aggregate-json <out.json> <lane.json> [more lane.json...]
29
+ #
30
+ # Options:
31
+ # --json <path> write a deterministic timing artifact after the run
32
+ # --list print selected script paths (one per line) and exit 0
33
+ # --list-scheduled
34
+ # print selected paths longest-hint-first and exit 0
35
+ # --base <ref> with --changed, compare against this ref (default: origin/main)
36
+ # --exclude-family <name>
37
+ # drop scripts whose primary family matches <name> after selection
38
+ # (repeatable; portable CI lanes exclude real-herdr-gated so the
39
+ # dedicated required Herdr lane owns that coverage)
40
+ # --fail-on-gate-skip <token>
41
+ # after each script, fail the run if any output line contains
42
+ # "skip: <token>" (e.g. --fail-on-gate-skip 'herdr not found').
43
+ # The required Herdr CI lane uses this so a missing pin cannot
44
+ # silently pass as a gate skip.
45
+ # --jobs N run the selected scripts with up to N concurrent workers.
46
+ # Plain --changed uses min(4, cpus) workers when multiple
47
+ # selected scripts are admissible.
48
+ # N>1 is allowed only when every selected script is proven
49
+ # safe to run concurrently: individually in the proven-isolated
50
+ # set (bin/fm-test-isolation-proof.sh --list), or in a family
51
+ # carrying a recorded concurrent proof
52
+ # (list_concurrent_safe_families below). Overall cap is 8;
53
+ # family proofs may impose a lower cap. Unproven stateful
54
+ # scripts stay serial. Concurrent runs are ordered
55
+ # longest-hint-first so the slowest script is not stranded
56
+ # alone at the tail. Default is 1 (serial) except for plain
57
+ # --changed, which uses the bounded automatic scheduler. Any
58
+ # unproven remainder runs serially after that group.
59
+ # --per-script-timeout-secs N
60
+ # terminate a script that runs longer than N seconds and
61
+ # record it as exit 124 (0 disables, the default). The
62
+ # --changed applies 900s automatically: no real script
63
+ # approaches it, so it only converts a HUNG
64
+ # script into a bounded failure. --max-wall-ms is checked
65
+ # after the run and so cannot catch a hang on its own.
66
+ # External interruption cleanup is outside this runner's
67
+ # guarantee; configured per-script bounds remain authoritative.
68
+ # --max-wall-ms N fail the run when its measured invocation wall clock exceeds
69
+ # N milliseconds, including an empty selection. It is
70
+ # evaluated after selection and suite execution and cannot
71
+ # interrupt a running script; per-script hangs are
72
+ # bounded by --per-script-timeout-secs. Pathological output
73
+ # sinks that block finalization are explicitly out of scope.
74
+ # -h, --help print this header
75
+ #
76
+ # Per-script machine-parseable markers (stdout):
77
+ # FM_TEST_BEGIN <iso8601> <script> family=<family> expected_gate_skip=<class>
78
+ # FM_TEST_END <iso8601> <script> exit=<code> duration_ms=<n> gate_skip=<true|false>
79
+ #
80
+ # After all scripts (stdout):
81
+ # FM_TEST_SUMMARY total=<n> failed=<n> skipped_gate=<n> duration_ms=<n>
82
+ # FM_TEST_SUMMARY_FAMILY family=<name> count=<n> duration_ms=<n> failed=<n>
83
+ # FM_TEST_SLOWEST rank=<k> script=<path> duration_ms=<n>
84
+ # FM_TEST_BUDGET max_wall_ms=<n> duration_ms=<n> (only with --max-wall-ms)
85
+ #
86
+ # Exit status is non-zero if any selected script exits non-zero, a configured
87
+ # --fail-on-gate-skip token appears, the measured duration exceeds
88
+ # --max-wall-ms, timing-artifact finalization fails, or a concurrent worker
89
+ # violates its isolation check. Other gate skips (first meaningful line
90
+ # matching ^skip:) remain successful and are counted as skipped_gate.
91
+ #
92
+ # Family labels, the changed-file map, and production portable-shard composition
93
+ # live in this script only (one owner). The proven-isolated candidate set remains
94
+ # owned by bin/fm-test-isolation-proof.sh; portable parallel shards are a
95
+ # duration-balanced partition of that exact set (see docs/fm-test-portable-shards.md).
96
+ #
97
+ # portable-serial stays strictly serial. Its CI shards (portable-serial-<k>of<n>)
98
+ # split it across separate runners, so two of its stateful scripts still never
99
+ # share a machine. This script owns <n>: a lane whose <n> disagrees with the
100
+ # configured shard count is refused, so a CI matrix cannot silently drop a shard.
101
+ # --changed is conservative: it over-selects related families rather than
102
+ # under-selecting, and never expands to the complete suite unless --all. The one
103
+ # place it is deliberately narrow is a bin/ path with no curated family: a test
104
+ # that names it is selected as that SCRIPT, because the reference is per-script
105
+ # evidence. Consumer bin/ scripts still resolve through the curated map, so
106
+ # recorded family-level coupling still expands to the whole family.
107
+ set -eu
108
+
109
+ now_ms() {
110
+ if command -v python3 >/dev/null 2>&1; then
111
+ python3 -c 'import time; print(int(time.time() * 1000))'
112
+ else
113
+ echo $(($(date +%s) * 1000))
114
+ fi
115
+ }
116
+
117
+ RUN_STARTED_ISO=$(date -u +%Y-%m-%dT%H:%M:%SZ)
118
+ RUN_STARTED_MS=$(now_ms)
119
+
120
+ ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
121
+ cd "$ROOT" || exit 1
122
+
123
+ MODE=
124
+ LIST_ONLY=0
125
+ LIST_SCHEDULED=0
126
+ LIST_FAMILIES=0
127
+ LIST_CONCURRENT_SAFE_FAMILIES=0
128
+ LIST_LANES=0
129
+ CHECK_COVERAGE=0
130
+ AGGREGATE_OUT=
131
+ FAMILY=
132
+ LANE=
133
+ BASE_REF=origin/main
134
+ JSON_PATH=
135
+ SCRIPTS=()
136
+ EXCLUDE_FAMILIES=()
137
+ FAIL_ON_GATE_SKIP=
138
+ JOBS=1
139
+ JOBS_EXPLICIT=0
140
+ JOBS_MAX=8
141
+ MAX_WALL_MS=
142
+ PER_SCRIPT_TIMEOUT_SECS=0
143
+ # Bound applied automatically on the automatic --changed path, derived from
144
+ # measured healthy runtimes with margin rather than picked: the slowest measured
145
+ # behavior test is the 341s Herdr presentation E2E, and the slowest script in a
146
+ # runner-file changed selection is tests/fm-calm-pi-extension.test.sh at 77s
147
+ # once its Chrome reap terminates. 900s leaves roughly 2.6x headroom over the
148
+ # slowest real script, so this can only ever fire on a script that is genuinely
149
+ # stuck. It is a guard, not a speed control: a HUNG script becomes a bounded
150
+ # failure instead of an unbounded suite, which is the shape that silently
151
+ # outruns a caller's invocation budget.
152
+ CHANGED_DEFAULT_TIMEOUT_SECS=900
153
+
154
+ # How many separate-runner shards the portable serial remainder splits into.
155
+ # One owner: CI lane names carry this count and are refused when they disagree.
156
+ PORTABLE_SERIAL_SHARDS=4
157
+
158
+ # Balance hint for a portable-serial script with no measured duration, close to
159
+ # the measured per-script mean so a newly added test neither starves nor
160
+ # overloads the shard it lands in.
161
+ PORTABLE_SERIAL_DEFAULT_WEIGHT_MS=20000
162
+
163
+ usage() {
164
+ awk '
165
+ NR == 1 { next }
166
+ /^#/ { sub(/^# ?/, ""); print; next }
167
+ { exit }
168
+ ' "$0" >&2
169
+ }
170
+
171
+ die() {
172
+ printf 'fm-test-run: %s\n' "$*" >&2
173
+ exit 2
174
+ }
175
+
176
+ log() {
177
+ printf 'fm-test-run: %s\n' "$*" >&2
178
+ }
179
+
180
+ now_iso() {
181
+ date -u +%Y-%m-%dT%H:%M:%SZ
182
+ }
183
+
184
+ cpu_count() {
185
+ local n
186
+ n=$(getconf _NPROCESSORS_ONLN 2>/dev/null || sysctl -n hw.ncpu 2>/dev/null || echo 1)
187
+ case "$n" in
188
+ ''|*[!0-9]*) n=1 ;;
189
+ esac
190
+ [ "$n" -ge 1 ] || n=1
191
+ printf '%s\n' "$n"
192
+ }
193
+
194
+ # Primary family for one tests/*.test.sh basename. Unmapped scripts are
195
+ # unclassified so new tests are still runnable and visible in summaries.
196
+ family_for_basename() {
197
+ case "$1" in
198
+ fm-arm-pretool-check.test.sh|fm-ask-user-authority.test.sh|\
199
+ fm-bearings-board.test.sh|\
200
+ fm-brief.test.sh|fm-vendor-auth-probe.test.sh|\
201
+ fm-calm-pi-extension.test.sh|fm-cd-pretool-check.test.sh|\
202
+ fm-classify-decision-key.test.sh|\
203
+ fm-composer-ghost.test.sh|fm-composer-lib.test.sh|\
204
+ fm-crew-state.test.sh|fm-captain-hold-lifecycle.test.sh|\
205
+ fm-documentation-audiences.test.sh|fm-ensure-agents-md.test.sh|fm-grok-harness.test.sh|\
206
+ fm-kimi-harness.test.sh|fm-muse-harness.test.sh|fm-herdr-lab.test.sh|fm-lint.test.sh|\
207
+ fm-lint-workflows.test.sh|\
208
+ fm-operational-input.test.sh|fm-pi-primary-types.test.sh|\
209
+ fm-harness-adapter-references.test.sh|\
210
+ fm-send-popup-settle.test.sh|fm-send-settle.test.sh|\
211
+ fm-subagent-pretool-check.test.sh|\
212
+ fm-supervision-instructions.test.sh|fm-task-delivery.test.sh|\
213
+ fm-tmux-submit-busy.test.sh|fm-trace-context-lib.test.sh|\
214
+ fm-transition-lib.test.sh|\
215
+ fm-test-run.test.sh|fm-test-isolation-proof.test.sh)
216
+ printf '%s\n' pure-contract-unit
217
+ ;;
218
+ fm-daemon.test.sh|fm-guard-stale-banner.test.sh|fm-pi-watch-extension.test.sh|\
219
+ fm-session-lock-ancestry.test.sh|fm-cursor-primary.test.sh|\
220
+ fm-supervision-events.test.sh|fm-turnend-guard.test.sh|fm-wake-daemon-lifecycle-e2e.test.sh|\
221
+ fm-wake-drain-unread-status.test.sh|\
222
+ fm-tool-update-check.test.sh|\
223
+ fm-wake-queue.test.sh|fm-watch-arm.test.sh|fm-watch-checkpoint.test.sh|fm-watch-recovery-loop.test.sh|\
224
+ fm-watch-triage.test.sh|fm-task-inbox.test.sh|\
225
+ fm-watcher-lock.test.sh|fm-inactive-reconcile.test.sh)
226
+ printf '%s\n' watcher-wake-lock
227
+ ;;
228
+ fm-afk-inject-herdr-e2e.test.sh|fm-afk-launch.test.sh|fm-backend-autodetect-smoke.test.sh|\
229
+ fm-backend-herdr-eventwait-smoke.test.sh|fm-backend-herdr-presentation-e2e.test.sh|\
230
+ fm-backend-herdr-launcher-workspace-e2e.test.sh|\
231
+ fm-backend-herdr-prune-safety-e2e.test.sh|fm-backend-herdr-respawn-idem-e2e.test.sh|\
232
+ fm-herdr-session-cleanup-e2e.test.sh|\
233
+ fm-backend-herdr-smoke.test.sh|fm-backend-herdr-workspace-per-home-e2e.test.sh|\
234
+ fm-control-herdr-smoke.test.sh)
235
+ printf '%s\n' real-herdr-gated
236
+ ;;
237
+ fm-backlog-handoff.test.sh|fm-on.test.sh|fm-remote-backlog-handoff.test.sh|\
238
+ fm-remote-doctor.test.sh|fm-remote-job.test.sh|fm-remote-job-orphan-reap.test.sh|\
239
+ fm-remote-transport-lanes.test.sh|\
240
+ fm-remote-reply.test.sh|fm-remote-secondmate-lifecycle-e2e.test.sh|\
241
+ fm-remote-secondmate-trace-context.test.sh|\
242
+ fm-secondmate-harness.test.sh|fm-secondmate-lifecycle-e2e.test.sh|\
243
+ fm-secondmate-liveness.test.sh|fm-secondmate-reconcile.test.sh|\
244
+ fm-secondmate-safety.test.sh|fm-secondmate-sync.test.sh|\
245
+ fm-startup-memory-budget.test.sh|fm-stow-cascade.test.sh|\
246
+ fm-send-secondmate-marker.test.sh|fm-shared-captain-inheritance.test.sh)
247
+ printf '%s\n' secondmate
248
+ ;;
249
+ fm-backlog-atomicity.test.sh|\
250
+ fm-bootstrap.test.sh|fm-bootstrap-network-parallel.test.sh|fm-fleet-sync.test.sh|fm-gate-refuse.test.sh|fm-gotmp.test.sh|\
251
+ fm-session-start.test.sh|fm-sessionstart-nudge.test.sh|fm-startup-network.test.sh|\
252
+ fm-tangle-guard.test.sh|fm-update.test.sh)
253
+ printf '%s\n' session-bootstrap
254
+ ;;
255
+ fm-afk-pi-herdr-return-e2e.test.sh|\
256
+ fm-cmux-claude-composer-live-e2e.test.sh|\
257
+ fm-composer-matrix-live-e2e.test.sh|\
258
+ fm-codex-continuity-live-e2e.test.sh|fm-grok-continuity-live-e2e.test.sh|\
259
+ fm-cursor-primary-live-e2e.test.sh|\
260
+ fm-grok-stop-live-e2e.test.sh|fm-harness-adapter-instructions-live-e2e.test.sh|\
261
+ fm-harness-liveness-drift-live-e2e.test.sh|\
262
+ fm-muse-signals-live-e2e.test.sh|\
263
+ fm-herdr-version-floor-live-e2e.test.sh|\
264
+ fm-opencode-primary-live-e2e.test.sh|fm-pi-branch-live-e2e.test.sh|\
265
+ fm-pi-primary-live-e2e.test.sh|\
266
+ fm-sessionstart-hook-live-e2e.test.sh|fm-sessionstart-instruction-refresh-live-e2e.test.sh|\
267
+ fm-quota-array-dispatch-live-e2e.test.sh|fm-send-secondmate-marker-herdr-e2e.test.sh|\
268
+ fm-send-inbox-doorbell-live-e2e.test.sh|\
269
+ fm-herdr-submit-confirm-live-e2e.test.sh)
270
+ printf '%s\n' live-harness-optin
271
+ ;;
272
+ fm-backend-herdr.test.sh|fm-backend-tmux-smoke.test.sh|fm-backend.test.sh|\
273
+ fm-tmux-agent-liveness.test.sh|\
274
+ fm-control.test.sh|fm-control-relaunch.test.sh|\
275
+ fm-herdr-session-cleanup.test.sh|fm-send-resolve-key.test.sh|fm-send-strict.test.sh|\
276
+ fm-send-inbox.test.sh|fm-spawn-batch.test.sh|\
277
+ fm-spawn-dispatch-profile.test.sh|\
278
+ fm-trace-context-spawn.test.sh|fm-spawn-worktree-settle.test.sh|\
279
+ fm-teardown-endpoint-safety.test.sh)
280
+ printf '%s\n' backend-dispatch
281
+ ;;
282
+ fm-check-unregister.test.sh|fm-pr-check-security.test.sh|fm-pr-merge.test.sh|\
283
+ fm-review-diff.test.sh|fm-teardown.test.sh|fm-x-mode.test.sh)
284
+ printf '%s\n' pr-forge
285
+ ;;
286
+ fm-afk-inject-e2e.test.sh|fm-afk-return.test.sh)
287
+ printf '%s\n' afk
288
+ ;;
289
+ fm-bearings-board-render.test.sh|fm-bearings-snapshot.test.sh|\
290
+ fm-fleet-snapshot-view.test.sh|fm-home-summary-refresh.test.sh)
291
+ printf '%s\n' snapshot-bearings
292
+ ;;
293
+ fm-backend-cmux.test.sh|fm-backend-cmux-smoke.test.sh)
294
+ printf '%s\n' cmux
295
+ ;;
296
+ fm-backend-zellij.test.sh|fm-backend-zellij-smoke.test.sh)
297
+ printf '%s\n' zellij
298
+ ;;
299
+ fm-backend-orca.test.sh)
300
+ printf '%s\n' orca
301
+ ;;
302
+ *)
303
+ printf '%s\n' unclassified
304
+ ;;
305
+ esac
306
+ }
307
+
308
+ expected_gate_skip_for_family() {
309
+ case "$1" in
310
+ real-herdr-gated) printf '%s\n' herdr ;;
311
+ live-harness-optin) printf '%s\n' optin-env ;;
312
+ cmux|zellij|orca) printf '%s\n' optional-binary ;;
313
+ snapshot-bearings) printf '%s\n' optional-binary ;;
314
+ *) printf '%s\n' none ;;
315
+ esac
316
+ }
317
+
318
+ list_known_families() {
319
+ cat <<'EOF'
320
+ pure-contract-unit
321
+ watcher-wake-lock
322
+ real-herdr-gated
323
+ secondmate
324
+ session-bootstrap
325
+ live-harness-optin
326
+ backend-dispatch
327
+ pr-forge
328
+ afk
329
+ snapshot-bearings
330
+ cmux
331
+ zellij
332
+ orca
333
+ unclassified
334
+ EOF
335
+ }
336
+
337
+ list_known_lanes() {
338
+ local i
339
+ printf '%s\n' portable-parallel-1
340
+ printf '%s\n' portable-parallel-2
341
+ printf '%s\n' portable-serial
342
+ i=1
343
+ while [ "$i" -le "$PORTABLE_SERIAL_SHARDS" ]; do
344
+ printf 'portable-serial-%sof%s\n' "$i" "$PORTABLE_SERIAL_SHARDS"
345
+ i=$((i + 1))
346
+ done
347
+ printf '%s\n' real-herdr-gated
348
+ }
349
+
350
+ # Exact proven-isolated candidate set (same paths as
351
+ # bin/fm-test-isolation-proof.sh --list). Do not expand without a new concurrent
352
+ # isolation proof archive.
353
+ list_proven_isolated() {
354
+ cat <<'EOF'
355
+ tests/fm-arm-pretool-check.test.sh
356
+ tests/fm-backend-herdr.test.sh
357
+ tests/fm-brief.test.sh
358
+ tests/fm-captain-hold-lifecycle.test.sh
359
+ tests/fm-cd-pretool-check.test.sh
360
+ tests/fm-composer-ghost.test.sh
361
+ tests/fm-composer-lib.test.sh
362
+ tests/fm-crew-state.test.sh
363
+ tests/fm-ensure-agents-md.test.sh
364
+ tests/fm-grok-harness.test.sh
365
+ tests/fm-herdr-lab.test.sh
366
+ tests/fm-lint.test.sh
367
+ tests/fm-pi-primary-types.test.sh
368
+ tests/fm-pr-merge.test.sh
369
+ tests/fm-review-diff.test.sh
370
+ tests/fm-send-popup-settle.test.sh
371
+ tests/fm-send-settle.test.sh
372
+ tests/fm-send-strict.test.sh
373
+ tests/fm-spawn-batch.test.sh
374
+ tests/fm-supervision-instructions.test.sh
375
+ tests/fm-test-run.test.sh
376
+ tests/fm-tmux-submit-busy.test.sh
377
+ tests/fm-transition-lib.test.sh
378
+ tests/fm-x-mode.test.sh
379
+ EOF
380
+ }
381
+
382
+ # Portable parallel shard 1: LPT balance of the proven-isolated set using the
383
+ # current concurrent-proof durations in docs/fm-test-isolation-proof.json.
384
+ # Execution order is longest first so wall-clock stays near the balanced sum.
385
+ list_portable_parallel_1() {
386
+ cat <<'EOF'
387
+ tests/fm-x-mode.test.sh
388
+ tests/fm-cd-pretool-check.test.sh
389
+ tests/fm-captain-hold-lifecycle.test.sh
390
+ tests/fm-test-run.test.sh
391
+ tests/fm-composer-ghost.test.sh
392
+ tests/fm-grok-harness.test.sh
393
+ tests/fm-lint.test.sh
394
+ tests/fm-pi-primary-types.test.sh
395
+ tests/fm-review-diff.test.sh
396
+ tests/fm-brief.test.sh
397
+ tests/fm-transition-lib.test.sh
398
+ EOF
399
+ }
400
+
401
+ # Portable parallel shard 2: the complementary LPT half of the proven set.
402
+ list_portable_parallel_2() {
403
+ cat <<'EOF'
404
+ tests/fm-backend-herdr.test.sh
405
+ tests/fm-arm-pretool-check.test.sh
406
+ tests/fm-crew-state.test.sh
407
+ tests/fm-herdr-lab.test.sh
408
+ tests/fm-pr-merge.test.sh
409
+ tests/fm-send-popup-settle.test.sh
410
+ tests/fm-tmux-submit-busy.test.sh
411
+ tests/fm-send-settle.test.sh
412
+ tests/fm-send-strict.test.sh
413
+ tests/fm-spawn-batch.test.sh
414
+ tests/fm-supervision-instructions.test.sh
415
+ tests/fm-ensure-agents-md.test.sh
416
+ tests/fm-composer-lib.test.sh
417
+ EOF
418
+ }
419
+
420
+ # Families whose scripts are proven safe to run concurrently WITH EACH OTHER
421
+ # under the bounded local scheduler. Deliberately separate from the
422
+ # proven-isolated set, which must stay exactly equal to the portable CI shard
423
+ # union (see the coverage guard); these families keep their serial CI lane and
424
+ # only gain concurrency for a local run.
425
+ #
426
+ # Membership is empirical, never assumed:
427
+ # `bin/fm-test-isolation-proof.sh --pool <family> --jobs 4` is the owner of the
428
+ # proof, and docs/fm-test-isolation-proof.md records the dated result.
429
+ list_concurrent_safe_families() {
430
+ cat <<'EOF'
431
+ watcher-wake-lock
432
+ pure-contract-unit
433
+ EOF
434
+ }
435
+
436
+ family_is_concurrent_safe() {
437
+ local want=$1 line
438
+ while IFS= read -r line; do
439
+ [ "$line" = "$want" ] && return 0
440
+ done < <(list_concurrent_safe_families)
441
+ return 1
442
+ }
443
+
444
+ concurrent_safe_family_jobs_max() {
445
+ case "$1" in
446
+ watcher-wake-lock|pure-contract-unit) printf '4\n' ;;
447
+ *) printf '1\n' ;;
448
+ esac
449
+ }
450
+
451
+ # A script may run under --jobs when it is individually proven isolated or is
452
+ # an exact repository member of a family carrying a recorded concurrent proof.
453
+ script_allows_concurrency() {
454
+ local s=$1 family repo_script
455
+ is_proven_isolated_script "$s" && return 0
456
+ family=$(family_for_basename "$(basename "$s")")
457
+ family_is_concurrent_safe "$family" || return 1
458
+ while IFS= read -r repo_script; do
459
+ [ "$repo_script" = "$s" ] && return 0
460
+ done < <(all_repo_tests)
461
+ return 1
462
+ }
463
+
464
+ is_proven_isolated_script() {
465
+ local want=$1 line
466
+ while IFS= read -r line; do
467
+ [ "$line" = "$want" ] && return 0
468
+ done < <(list_proven_isolated)
469
+ return 1
470
+ }
471
+
472
+ # The portable serial remainder: every tests/*.test.sh that is neither
473
+ # proven-isolated nor real-herdr-gated. Watcher, lock, AFK, real tmux, daemon,
474
+ # secondmate lifecycle, bootstrap, live-harness opt-in, GUI-backend, and other
475
+ # unproven work stays here. Derived rather than enumerated so a newly added test
476
+ # lands here by default instead of falling out of every lane.
477
+ list_portable_serial() {
478
+ local s base fam
479
+ while IFS= read -r s; do
480
+ [ -n "$s" ] || continue
481
+ base=$(basename "$s")
482
+ fam=$(family_for_basename "$base")
483
+ if [ "$fam" = "real-herdr-gated" ]; then
484
+ continue
485
+ fi
486
+ if is_proven_isolated_script "$s"; then
487
+ continue
488
+ fi
489
+ printf '%s\n' "$s"
490
+ done < <(all_repo_tests)
491
+ }
492
+
493
+ # Measured portable-serial script durations in milliseconds, from the CI timing
494
+ # artifact recorded in docs/fm-test-portable-shards.md. These are balance hints
495
+ # only: the shard partition stays complete and disjoint whatever they say, so a
496
+ # stale hint costs balance rather than coverage. That doc owns the refresh
497
+ # procedure.
498
+ portable_serial_weight_hints() {
499
+ cat <<'EOF'
500
+ tests/fm-afk-inject-e2e.test.sh 35900
501
+ tests/fm-afk-pi-herdr-return-e2e.test.sh 66
502
+ tests/fm-afk-return.test.sh 3974
503
+ tests/fm-ask-user-authority.test.sh 83
504
+ tests/fm-backend-cmux-smoke.test.sh 30
505
+ tests/fm-backend-cmux.test.sh 3351
506
+ tests/fm-backend-herdr-focus-flash-e2e.test.sh 21
507
+ tests/fm-backend-orca.test.sh 14681
508
+ tests/fm-backend-tmux-smoke.test.sh 361
509
+ tests/fm-backend-zellij-smoke.test.sh 22
510
+ tests/fm-backend-zellij.test.sh 8297
511
+ tests/fm-backend.test.sh 17169
512
+ tests/fm-backlog-handoff.test.sh 4157
513
+ tests/fm-bearings-board.test.sh 3385
514
+ tests/fm-bearings-snapshot.test.sh 68659
515
+ tests/fm-bootstrap-network-parallel.test.sh 8000
516
+ tests/fm-bootstrap.test.sh 38417
517
+ tests/fm-busy-adapter-wiring.test.sh 14880
518
+ tests/fm-busy-state.test.sh 714
519
+ tests/fm-calm-pi-extension.test.sh 464
520
+ tests/fm-classify-decision-key.test.sh 928
521
+ tests/fm-claude-stop-autoarm-live-e2e.test.sh 30
522
+ tests/fm-claude-stop-autoarm.test.sh 60633
523
+ tests/fm-cmux-claude-composer-live-e2e.test.sh 20
524
+ tests/fm-codex-continuity-live-e2e.test.sh 19
525
+ tests/fm-composer-matrix-live-e2e.test.sh 21
526
+ tests/fm-control-relaunch.test.sh 31881
527
+ tests/fm-control.test.sh 36712
528
+ tests/fm-cursor-harness.test.sh 30071
529
+ tests/fm-cursor-primary-live-e2e.test.sh 20
530
+ tests/fm-cursor-primary.test.sh 52324
531
+ tests/fm-daemon.test.sh 25834
532
+ tests/fm-documentation-audiences.test.sh 642
533
+ tests/fm-fleet-snapshot-view.test.sh 6995
534
+ tests/fm-fleet-sync.test.sh 20194
535
+ tests/fm-extension-binding.test.sh 35000
536
+ tests/fm-gate-refuse.test.sh 4071
537
+ tests/fm-gitignore-config.test.sh 63
538
+ tests/fm-gotmp.test.sh 762
539
+ tests/fm-grok-continuity-live-e2e.test.sh 19
540
+ tests/fm-grok-stop-live-e2e.test.sh 21
541
+ tests/fm-harness-adapter-instructions-live-e2e.test.sh 20
542
+ tests/fm-harness-adapter-references.test.sh 2
543
+ tests/fm-guard-stale-banner.test.sh 11280
544
+ tests/fm-harness-liveness-drift-live-e2e.test.sh 19
545
+ tests/fm-herdr-session-cleanup.test.sh 14120
546
+ tests/fm-herdr-submit-confirm-live-e2e.test.sh 20
547
+ tests/fm-herdr-version-floor-live-e2e.test.sh 20
548
+ tests/fm-inactive-reconcile.test.sh 41671
549
+ tests/fm-kimi-harness.test.sh 15092
550
+ tests/fm-lint-workflows.test.sh 744
551
+ tests/fm-muse-harness.test.sh 27414
552
+ tests/fm-muse-signals-live-e2e.test.sh 21
553
+ tests/fm-on.test.sh 8602
554
+ tests/fm-opencode-primary-live-e2e.test.sh 22
555
+ tests/fm-operational-input.test.sh 246
556
+ tests/fm-peek-remote.test.sh 848
557
+ tests/fm-pending-reply.test.sh 19488
558
+ tests/fm-pi-primary-live-e2e.test.sh 41
559
+ tests/fm-pi-watch-extension.test.sh 17979
560
+ tests/fm-pr-check-security.test.sh 250417
561
+ tests/fm-procevent-when.test.sh 15249
562
+ tests/fm-procevent.test.sh 53142
563
+ tests/fm-project-origin.test.sh 105
564
+ tests/fm-public-followup.test.sh 36301
565
+ tests/fm-quota-array-dispatch-live-e2e.test.sh 18
566
+ tests/fm-remote-backlog-handoff.test.sh 20389
567
+ tests/fm-remote-doctor.test.sh 4705
568
+ tests/fm-remote-entrypoint.test.sh 98
569
+ tests/fm-remote-job-orphan-reap.test.sh 2903
570
+ tests/fm-remote-job.test.sh 48068
571
+ tests/fm-remote-reply.test.sh 40906
572
+ tests/fm-remote-secondmate-lifecycle-e2e.test.sh 170240
573
+ tests/fm-remote-secondmate-parent-binding.test.sh 13064
574
+ tests/fm-remote-secondmate-trace-context.test.sh 39927
575
+ tests/fm-secondmate-harness.test.sh 123471
576
+ tests/fm-secondmate-lifecycle-e2e.test.sh 6539
577
+ tests/fm-secondmate-liveness.test.sh 16365
578
+ tests/fm-secondmate-safety.test.sh 49011
579
+ tests/fm-secondmate-sync.test.sh 29236
580
+ tests/fm-send-remote-delivery.test.sh 4892
581
+ tests/fm-send-resolve-key.test.sh 13450
582
+ tests/fm-send-secondmate-marker-herdr-e2e.test.sh 45
583
+ tests/fm-send-secondmate-marker.test.sh 4439
584
+ tests/fm-session-lock-ancestry.test.sh 1205
585
+ tests/fm-session-start.test.sh 144836
586
+ tests/fm-sessionstart-hook-live-e2e.test.sh 21
587
+ tests/fm-sessionstart-instruction-refresh-live-e2e.test.sh 21
588
+ tests/fm-sessionstart-nudge.test.sh 26684
589
+ tests/fm-shared-captain-inheritance.test.sh 10672
590
+ tests/fm-spawn-dispatch-profile.test.sh 57765
591
+ tests/fm-spawn-pool-base-freshen.test.sh 13257
592
+ tests/fm-spawn-worktree-settle.test.sh 4828
593
+ tests/fm-startup-memory-budget.test.sh 6550
594
+ tests/fm-startup-network.test.sh 48888
595
+ tests/fm-stow-cascade.test.sh 2986
596
+ tests/fm-subagent-pretool-check.test.sh 1066
597
+ tests/fm-supervision-events.test.sh 1431
598
+ tests/fm-tangle-guard.test.sh 8364
599
+ tests/fm-task-delivery.test.sh 2414
600
+ tests/fm-teardown-endpoint-safety.test.sh 7295
601
+ tests/fm-teardown.test.sh 87400
602
+ tests/fm-test-fixture-cleanup.test.sh 532
603
+ tests/fm-test-fixtures.test.sh 1045
604
+ tests/fm-test-isolation-proof.test.sh 451
605
+ tests/fm-tmux-agent-liveness.test.sh 4065
606
+ tests/fm-tool-update-check.test.sh 12846
607
+ tests/fm-trace-context-lib.test.sh 194
608
+ tests/fm-trace-context-spawn.test.sh 35325
609
+ tests/fm-turnend-guard.test.sh 34915
610
+ tests/fm-update.test.sh 5280
611
+ tests/fm-vendor-auth-probe.test.sh 43243
612
+ tests/fm-wake-daemon-lifecycle-e2e.test.sh 6219
613
+ tests/fm-wake-drain-open-decisions-cursor.test.sh 17357
614
+ tests/fm-wake-drain-open-decisions.test.sh 11300
615
+ tests/fm-wake-drain-unread-status.test.sh 25214
616
+ tests/fm-wake-queue.test.sh 30887
617
+ tests/fm-watch-arm.test.sh 53598
618
+ tests/fm-watch-checkpoint.test.sh 5293
619
+ tests/fm-watch-recovery-loop.test.sh 58721
620
+ tests/fm-watch-triage.test.sh 142409
621
+ tests/fm-watcher-lock.test.sh 54364
622
+ EOF
623
+ }
624
+
625
+ portable_serial_weight_for() {
626
+ local want=$1 path ms
627
+ while read -r path ms; do
628
+ if [ "$path" = "$want" ]; then
629
+ printf '%s\n' "$ms"
630
+ return 0
631
+ fi
632
+ done < <(portable_serial_weight_hints)
633
+ printf '%s\n' "$PORTABLE_SERIAL_DEFAULT_WEIGHT_MS"
634
+ }
635
+
636
+ # Longest-processing-time assignment of the serial remainder to
637
+ # PORTABLE_SERIAL_SHARDS bins, printing "<shard>\t<script>" for every script.
638
+ # Deterministic: candidates are ordered by hint descending then path, and ties
639
+ # between equally loaded bins always take the lowest bin index.
640
+ portable_serial_assignments() {
641
+ local ms script i best best_load
642
+ local -a loads=()
643
+ i=1
644
+ while [ "$i" -le "$PORTABLE_SERIAL_SHARDS" ]; do
645
+ loads[i]=0
646
+ i=$((i + 1))
647
+ done
648
+ while IFS=$'\t' read -r ms script; do
649
+ [ -n "$script" ] || continue
650
+ best=1
651
+ best_load=${loads[1]}
652
+ i=2
653
+ while [ "$i" -le "$PORTABLE_SERIAL_SHARDS" ]; do
654
+ if [ "${loads[i]}" -lt "$best_load" ]; then
655
+ best_load=${loads[i]}
656
+ best=$i
657
+ fi
658
+ i=$((i + 1))
659
+ done
660
+ loads[best]=$((best_load + ms))
661
+ printf '%s\t%s\n' "$best" "$script"
662
+ done < <(
663
+ while IFS= read -r script; do
664
+ [ -n "$script" ] || continue
665
+ printf '%s\t%s\n' "$(portable_serial_weight_for "$script")" "$script"
666
+ done < <(list_portable_serial) | LC_ALL=C sort -t$'\t' -k1,1nr -k2,2
667
+ )
668
+ }
669
+
670
+ # Parse "<k>of<n>" from a portable-serial shard lane and echo <k>, refusing when
671
+ # <n> disagrees with this script's configured count so a CI matrix built for a
672
+ # different shard count fails loudly instead of dropping tests.
673
+ portable_serial_shard_index() {
674
+ local lane=$1 spec index count
675
+ spec=${lane#portable-serial-}
676
+ index=${spec%%of*}
677
+ count=${spec#*of}
678
+ case "$spec" in
679
+ *of*) ;;
680
+ *) die "unknown lane '$lane' (see --list-lanes)" ;;
681
+ esac
682
+ case "$index" in
683
+ ''|*[!0-9]*) die "unknown lane '$lane' (see --list-lanes)" ;;
684
+ esac
685
+ case "$count" in
686
+ ''|*[!0-9]*) die "unknown lane '$lane' (see --list-lanes)" ;;
687
+ esac
688
+ if [ "$count" -ne "$PORTABLE_SERIAL_SHARDS" ]; then
689
+ die "lane '$lane' asks for $count portable serial shards but this runner is configured for $PORTABLE_SERIAL_SHARDS (see --list-lanes)"
690
+ fi
691
+ if [ "$index" -lt 1 ] || [ "$index" -gt "$PORTABLE_SERIAL_SHARDS" ]; then
692
+ die "lane '$lane' shard index is outside 1..$PORTABLE_SERIAL_SHARDS (see --list-lanes)"
693
+ fi
694
+ printf '%s\n' "$index"
695
+ }
696
+
697
+ select_proven_isolated() {
698
+ local s
699
+ while IFS= read -r s; do
700
+ [ -n "$s" ] || continue
701
+ add_script "$s"
702
+ done < <(list_proven_isolated)
703
+ }
704
+
705
+ select_lane() {
706
+ local want=$1 s shard idx found=0
707
+ case "$want" in
708
+ portable-parallel-1)
709
+ while IFS= read -r s; do
710
+ [ -n "$s" ] || continue
711
+ add_script "$s"
712
+ found=1
713
+ done < <(list_portable_parallel_1)
714
+ ;;
715
+ portable-parallel-2)
716
+ while IFS= read -r s; do
717
+ [ -n "$s" ] || continue
718
+ add_script "$s"
719
+ found=1
720
+ done < <(list_portable_parallel_2)
721
+ ;;
722
+ portable-serial)
723
+ while IFS= read -r s; do
724
+ [ -n "$s" ] || continue
725
+ add_script "$s"
726
+ found=1
727
+ done < <(list_portable_serial)
728
+ ;;
729
+ portable-serial-*)
730
+ # One separate-runner shard of the same remainder, still serial in itself.
731
+ shard=$(portable_serial_shard_index "$want")
732
+ while IFS=$'\t' read -r idx s; do
733
+ [ -n "$s" ] || continue
734
+ if [ "$idx" = "$shard" ]; then
735
+ add_script "$s"
736
+ found=1
737
+ fi
738
+ done < <(portable_serial_assignments)
739
+ ;;
740
+ real-herdr-gated)
741
+ select_family real-herdr-gated
742
+ found=1
743
+ ;;
744
+ *)
745
+ die "unknown lane '$want' (see --list-lanes)"
746
+ ;;
747
+ esac
748
+ [ "$found" -eq 1 ] || die "lane '$want' selected no tests"
749
+ }
750
+
751
+ run_coverage_guard() {
752
+ local tmp missing extra a b shard
753
+ local -a saved_scripts=()
754
+ tmp=$(mktemp -d "${TMPDIR:-/tmp}/fm-test-coverage.XXXXXX")
755
+
756
+ all_repo_tests | LC_ALL=C sort -u >"$tmp/all"
757
+ list_proven_isolated | LC_ALL=C sort -u >"$tmp/proven"
758
+ list_portable_parallel_1 | LC_ALL=C sort -u >"$tmp/s1"
759
+ list_portable_parallel_2 | LC_ALL=C sort -u >"$tmp/s2"
760
+
761
+ cat "$tmp/s1" "$tmp/s2" | LC_ALL=C sort | uniq -d >"$tmp/shard_dups"
762
+ if [ -s "$tmp/shard_dups" ]; then
763
+ log "coverage guard: portable parallel shards share scripts:"
764
+ cat "$tmp/shard_dups" >&2
765
+ rm -rf "$tmp"
766
+ return 1
767
+ fi
768
+ cat "$tmp/s1" "$tmp/s2" | LC_ALL=C sort -u >"$tmp/shards_union"
769
+ missing=$(comm -23 "$tmp/proven" "$tmp/shards_union" || true)
770
+ extra=$(comm -13 "$tmp/proven" "$tmp/shards_union" || true)
771
+ if [ -n "$missing" ] || [ -n "$extra" ]; then
772
+ log "coverage guard: portable shards must equal the proven-isolated set"
773
+ [ -z "$missing" ] || { log "missing from shards:"; printf '%s\n' "$missing" >&2; }
774
+ [ -z "$extra" ] || { log "extra beyond proven:"; printf '%s\n' "$extra" >&2; }
775
+ rm -rf "$tmp"
776
+ return 1
777
+ fi
778
+
779
+ # Serial (whole lane and each CI shard) + Herdr lane listings without
780
+ # disturbing a caller's selection.
781
+ saved_scripts=("${SCRIPTS[@]+"${SCRIPTS[@]}"}")
782
+ SCRIPTS=()
783
+ select_lane portable-serial
784
+ printf '%s\n' "${SCRIPTS[@]+"${SCRIPTS[@]}"}" | LC_ALL=C sort -u >"$tmp/serial"
785
+ : >"$tmp/serial_shards_raw"
786
+ shard=1
787
+ while [ "$shard" -le "$PORTABLE_SERIAL_SHARDS" ]; do
788
+ SCRIPTS=()
789
+ select_lane "portable-serial-${shard}of${PORTABLE_SERIAL_SHARDS}"
790
+ if [ "${#SCRIPTS[@]}" -eq 0 ]; then
791
+ log "coverage guard: portable serial shard $shard of $PORTABLE_SERIAL_SHARDS is empty"
792
+ SCRIPTS=("${saved_scripts[@]+"${saved_scripts[@]}"}")
793
+ rm -rf "$tmp"
794
+ return 1
795
+ fi
796
+ printf '%s\n' "${SCRIPTS[@]+"${SCRIPTS[@]}"}" >>"$tmp/serial_shards_raw"
797
+ shard=$((shard + 1))
798
+ done
799
+ SCRIPTS=()
800
+ select_family real-herdr-gated
801
+ printf '%s\n' "${SCRIPTS[@]+"${SCRIPTS[@]}"}" | LC_ALL=C sort -u >"$tmp/herdr"
802
+ SCRIPTS=("${saved_scripts[@]+"${saved_scripts[@]}"}")
803
+
804
+ # Every serial script runs in exactly one CI shard: no duplicate work across
805
+ # runners, and no script silently left out of the required lane.
806
+ LC_ALL=C sort "$tmp/serial_shards_raw" | uniq -d >"$tmp/serial_shard_dups"
807
+ if [ -s "$tmp/serial_shard_dups" ]; then
808
+ log "coverage guard: portable serial shards share scripts:"
809
+ cat "$tmp/serial_shard_dups" >&2
810
+ rm -rf "$tmp"
811
+ return 1
812
+ fi
813
+ LC_ALL=C sort -u "$tmp/serial_shards_raw" >"$tmp/serial_shards"
814
+ missing=$(comm -23 "$tmp/serial" "$tmp/serial_shards" || true)
815
+ extra=$(comm -13 "$tmp/serial" "$tmp/serial_shards" || true)
816
+ if [ -n "$missing" ] || [ -n "$extra" ]; then
817
+ log "coverage guard: portable serial shards must equal the portable serial lane"
818
+ [ -z "$missing" ] || { log "missing from serial shards:"; printf '%s\n' "$missing" >&2; }
819
+ [ -z "$extra" ] || { log "extra beyond serial lane:"; printf '%s\n' "$extra" >&2; }
820
+ rm -rf "$tmp"
821
+ return 1
822
+ fi
823
+
824
+ for pair in "shards_union:serial" "shards_union:herdr" "serial:herdr"; do
825
+ a=${pair%%:*}
826
+ b=${pair#*:}
827
+ comm -12 "$tmp/$a" "$tmp/$b" >"$tmp/overlap"
828
+ if [ -s "$tmp/overlap" ]; then
829
+ log "coverage guard: overlap between $a and $b:"
830
+ cat "$tmp/overlap" >&2
831
+ rm -rf "$tmp"
832
+ return 1
833
+ fi
834
+ done
835
+
836
+ cat "$tmp/shards_union" "$tmp/serial" "$tmp/herdr" | LC_ALL=C sort >"$tmp/union_raw"
837
+ uniq -d "$tmp/union_raw" >"$tmp/union_dups"
838
+ if [ -s "$tmp/union_dups" ]; then
839
+ log "coverage guard: duplicate scripts across lanes:"
840
+ cat "$tmp/union_dups" >&2
841
+ rm -rf "$tmp"
842
+ return 1
843
+ fi
844
+ LC_ALL=C sort -u "$tmp/union_raw" >"$tmp/union"
845
+ missing=$(comm -23 "$tmp/all" "$tmp/union" || true)
846
+ extra=$(comm -13 "$tmp/all" "$tmp/union" || true)
847
+ if [ -n "$missing" ] || [ -n "$extra" ]; then
848
+ log "coverage guard: union of portable shards + portable serial + Herdr must equal tests/*.test.sh"
849
+ [ -z "$missing" ] || { log "missing from union:"; printf '%s\n' "$missing" >&2; }
850
+ [ -z "$extra" ] || { log "extra beyond inventory:"; printf '%s\n' "$extra" >&2; }
851
+ rm -rf "$tmp"
852
+ return 1
853
+ fi
854
+
855
+ if [ -x "$ROOT/bin/fm-test-isolation-proof.sh" ]; then
856
+ "$ROOT/bin/fm-test-isolation-proof.sh" --list | LC_ALL=C sort -u >"$tmp/proof_list"
857
+ if ! cmp -s "$tmp/proven" "$tmp/proof_list"; then
858
+ log "coverage guard: embedded proven-isolated set diverges from bin/fm-test-isolation-proof.sh --list"
859
+ comm -3 "$tmp/proven" "$tmp/proof_list" >&2 || true
860
+ rm -rf "$tmp"
861
+ return 1
862
+ fi
863
+ fi
864
+
865
+ printf 'FM_TEST_COVERAGE ok total=%s parallel=%s serial=%s serial_shards=%s herdr=%s\n' \
866
+ "$(wc -l <"$tmp/all" | tr -d ' ')" \
867
+ "$(wc -l <"$tmp/shards_union" | tr -d ' ')" \
868
+ "$(wc -l <"$tmp/serial" | tr -d ' ')" \
869
+ "$PORTABLE_SERIAL_SHARDS" \
870
+ "$(wc -l <"$tmp/herdr" | tr -d ' ')"
871
+ rm -rf "$tmp"
872
+ return 0
873
+ }
874
+
875
+ aggregate_timing_json() {
876
+ local out=$1
877
+ shift
878
+ [ "$#" -gt 0 ] || die "--aggregate-json requires at least one input timing JSON"
879
+ command -v python3 >/dev/null 2>&1 || die "--aggregate-json requires python3"
880
+ python3 - "$out" "$@" <<'PY'
881
+ import json, sys
882
+ from pathlib import Path
883
+
884
+ out = Path(sys.argv[1])
885
+ inputs = [Path(p) for p in sys.argv[2:]]
886
+ lanes = []
887
+ all_scripts = []
888
+ failed = 0
889
+ skipped = 0
890
+ total = 0
891
+ wall_ms = 0
892
+ for path in inputs:
893
+ doc = json.loads(path.read_text(encoding="utf-8"))
894
+ summary = doc.get("summary") or {}
895
+ lane = {
896
+ "path": str(path),
897
+ "run_id": doc.get("run_id"),
898
+ "selection": doc.get("selection"),
899
+ "started_at": doc.get("started_at"),
900
+ "finished_at": doc.get("finished_at"),
901
+ "summary": summary,
902
+ }
903
+ lanes.append(lane)
904
+ total += int(summary.get("total") or 0)
905
+ failed += int(summary.get("failed") or 0)
906
+ skipped += int(summary.get("skipped_gate") or 0)
907
+ wall_ms = max(wall_ms, int(summary.get("duration_ms") or 0))
908
+ for s in doc.get("scripts") or []:
909
+ row = dict(s)
910
+ row["lane_selection"] = doc.get("selection")
911
+ row["lane_run_id"] = doc.get("run_id")
912
+ all_scripts.append(row)
913
+
914
+ all_scripts.sort(key=lambda s: (-int(s.get("duration_ms") or 0), s.get("path") or ""))
915
+ agg = {
916
+ "kind": "aggregate",
917
+ "lanes": lanes,
918
+ "summary": {
919
+ "lanes": len(lanes),
920
+ "total": total,
921
+ "failed": failed,
922
+ "skipped_gate": skipped,
923
+ "critical_path_duration_ms": wall_ms,
924
+ },
925
+ "scripts": all_scripts,
926
+ "slowest": all_scripts[:15],
927
+ }
928
+ out.parent.mkdir(parents=True, exist_ok=True)
929
+ out.write_text(json.dumps(agg, indent=2, sort_keys=True) + "\n", encoding="utf-8")
930
+ print(f"FM_TEST_AGGREGATE lanes={len(lanes)} total={total} failed={failed} skipped_gate={skipped} critical_path_duration_ms={wall_ms}")
931
+ PY
932
+ }
933
+
934
+ all_repo_tests() {
935
+ # Deterministic lexical order (same as bash glob expansion under LC_ALL=C).
936
+ local f
937
+ # shellcheck disable=SC2035
938
+ for f in tests/*.test.sh; do
939
+ [ -f "$f" ] || continue
940
+ printf '%s\n' "$f"
941
+ done | LC_ALL=C sort
942
+ }
943
+
944
+ normalize_script_path() {
945
+ local p=$1
946
+ case "$p" in
947
+ /*) printf '%s\n' "$p" ;;
948
+ tests/*|./tests/*)
949
+ p=${p#./}
950
+ printf '%s\n' "$p"
951
+ ;;
952
+ *.test.sh)
953
+ if [ -f "tests/$p" ]; then
954
+ printf 'tests/%s\n' "$p"
955
+ else
956
+ printf '%s\n' "$p"
957
+ fi
958
+ ;;
959
+ *)
960
+ printf '%s\n' "$p"
961
+ ;;
962
+ esac
963
+ }
964
+
965
+ # Append unique relative-or-absolute script paths to SCRIPTS.
966
+ add_script() {
967
+ local p existing
968
+ p=$(normalize_script_path "$1")
969
+ for existing in "${SCRIPTS[@]+"${SCRIPTS[@]}"}"; do
970
+ [ "$existing" = "$p" ] && return 0
971
+ done
972
+ SCRIPTS+=("$p")
973
+ }
974
+
975
+ select_all() {
976
+ local s
977
+ while IFS= read -r s; do
978
+ [ -n "$s" ] || continue
979
+ add_script "$s"
980
+ done < <(all_repo_tests)
981
+ }
982
+
983
+ select_family() {
984
+ local want=$1 s base fam found=0
985
+ [ -n "$want" ] || die "--family requires a name"
986
+ while IFS= read -r s; do
987
+ [ -n "$s" ] || continue
988
+ base=$(basename "$s")
989
+ fam=$(family_for_basename "$base")
990
+ if [ "$fam" = "$want" ]; then
991
+ add_script "$s"
992
+ found=1
993
+ fi
994
+ done < <(all_repo_tests)
995
+ [ "$found" -eq 1 ] || die "no tests mapped to family '$want'"
996
+ }
997
+
998
+ families_for_test_reference() {
999
+ local needle=$1 s
1000
+ local found=0
1001
+ while IFS= read -r s; do
1002
+ [ -n "$s" ] || continue
1003
+ if grep -Fq "$needle" "$s"; then
1004
+ family_for_basename "$(basename "$s")"
1005
+ found=1
1006
+ fi
1007
+ done < <(all_repo_tests)
1008
+ [ "$found" -eq 1 ]
1009
+ }
1010
+
1011
+ # Tests that name <needle>, selected as individual scripts rather than widened
1012
+ # to each referencing test's whole family. A direct reference is per-script
1013
+ # evidence, so it selects per script: one real-Herdr E2E sourcing a shared
1014
+ # helper must not drag in every other script of that expensive family.
1015
+ scripts_for_test_reference() {
1016
+ local needle=$1 s
1017
+ local found=0
1018
+ while IFS= read -r s; do
1019
+ [ -n "$s" ] || continue
1020
+ if grep -Fq "$needle" "$s"; then
1021
+ printf '__script__:%s\n' "$(basename "$s")"
1022
+ found=1
1023
+ fi
1024
+ done < <(all_repo_tests)
1025
+ [ "$found" -eq 1 ]
1026
+ }
1027
+
1028
+ # bin/ scripts other than <needle> itself that name <needle>.
1029
+ bin_consumers_of() {
1030
+ local needle=$1 b
1031
+ for b in bin/*.sh bin/backends/*.sh; do
1032
+ [ -f "$b" ] || continue
1033
+ [ "$(basename "$b")" = "$needle" ] || ! grep -Fq "$needle" "$b" || printf '%s\n' "$b"
1034
+ done
1035
+ }
1036
+
1037
+ # An unmapped bin/ path has no curated family of its own. Its blast radius is
1038
+ # the tests that name it, plus the curated families of the bin/ scripts that
1039
+ # consume it. Direct test references resolve per script (above) while consumer
1040
+ # scripts resolve back through the curated map, so genuine family-level
1041
+ # coupling a maintainer recorded is preserved while an incidental single-script
1042
+ # reference no longer selects that script's whole family.
1043
+ BIN_FALLBACK_DEPTH=0
1044
+ families_for_unmapped_bin() {
1045
+ local path=$1 needle consumer out found=0
1046
+ needle=$(basename "$path")
1047
+ if out=$(scripts_for_test_reference "$needle"); then
1048
+ printf '%s\n' "$out"
1049
+ found=1
1050
+ fi
1051
+ if [ "$BIN_FALLBACK_DEPTH" -lt 2 ]; then
1052
+ BIN_FALLBACK_DEPTH=$((BIN_FALLBACK_DEPTH + 1))
1053
+ while IFS= read -r consumer; do
1054
+ [ -n "$consumer" ] || continue
1055
+ out=$(families_for_changed_path "$consumer" | grep -v '^__unmapped__:' || true)
1056
+ if [ -n "$out" ]; then
1057
+ printf '%s\n' "$out"
1058
+ found=1
1059
+ fi
1060
+ done < <(bin_consumers_of "$needle")
1061
+ BIN_FALLBACK_DEPTH=$((BIN_FALLBACK_DEPTH - 1))
1062
+ fi
1063
+ [ "$found" -eq 1 ]
1064
+ }
1065
+
1066
+ # Conservative path → family map. Over-selects rather than under-selects.
1067
+ # Never expands to the complete suite.
1068
+ families_for_changed_path() {
1069
+ local path=$1 fixture_ref
1070
+ case "$path" in
1071
+ tests/fm-backend-herdr-eventwait.test.py)
1072
+ printf '%s\n' real-herdr-gated
1073
+ printf '%s\n' backend-dispatch
1074
+ ;;
1075
+ tests/*.test.sh)
1076
+ # A single test file change selects only that script via basename family
1077
+ # resolution in the caller; emit a marker family of __script__
1078
+ printf '%s\n' "__script__:$(basename "$path")"
1079
+ ;;
1080
+ bin/fm-test-run.sh|bin/fm-test-isolation-proof.sh)
1081
+ # Deliberately the WHOLE family, not just the two contract tests. This
1082
+ # runner executes every pure-contract-unit script, so a change to it is
1083
+ # only proven by running them: its own contract test passing says the
1084
+ # runner's logic is right, not that the suite it drives still runs.
1085
+ printf '%s\n' pure-contract-unit
1086
+ ;;
1087
+ bin/backends/herdr*|bin/fm-herdr-lab.sh|tests/herdr-test-safety.sh)
1088
+ printf '%s\n' real-herdr-gated
1089
+ printf '%s\n' backend-dispatch
1090
+ printf '%s\n' pure-contract-unit
1091
+ ;;
1092
+ bin/fm-herdr-session-cleanup.sh)
1093
+ printf '%s\n' session-bootstrap
1094
+ printf '%s\n' real-herdr-gated
1095
+ printf '%s\n' backend-dispatch
1096
+ ;;
1097
+ bin/backends/zellij*|tests/zellij-test-safety.sh)
1098
+ printf '%s\n' zellij
1099
+ printf '%s\n' backend-dispatch
1100
+ ;;
1101
+ bin/backends/cmux*|tests/cmux-test-safety.sh)
1102
+ printf '%s\n' cmux
1103
+ printf '%s\n' backend-dispatch
1104
+ ;;
1105
+ bin/backends/orca*|bin/backends/tmux.sh)
1106
+ printf '%s\n' backend-dispatch
1107
+ printf '%s\n' orca
1108
+ ;;
1109
+ bin/fm-backend.sh|bin/fm-backend-hometag-lib.sh)
1110
+ printf '%s\n' backend-dispatch
1111
+ printf '%s\n' real-herdr-gated
1112
+ ;;
1113
+ bin/fm-watch*|bin/fm-wake*|bin/fm-inactive-reconcile.sh|\
1114
+ bin/fm-classify-lib.sh|bin/fm-daemon*|bin/fm-turnend-guard*|bin/fm-guard.sh)
1115
+ printf '%s\n' watcher-wake-lock
1116
+ ;;
1117
+ bin/fm-afk*)
1118
+ printf '%s\n' afk
1119
+ printf '%s\n' real-herdr-gated
1120
+ ;;
1121
+ bin/fm-supervisor-target-lib.sh)
1122
+ printf '%s\n' watcher-wake-lock
1123
+ printf '%s\n' real-herdr-gated
1124
+ printf '%s\n' live-harness-optin
1125
+ printf '%s\n' afk
1126
+ ;;
1127
+ bin/fm-startup-memory-budget.sh|bin/fm-startup-memory-budget-lib.sh)
1128
+ printf '%s\n' secondmate
1129
+ printf '%s\n' session-bootstrap
1130
+ ;;
1131
+ bin/fm-secondmate*|bin/fm-remote*|bin/fm-on.sh|bin/fm-home-seed.sh|\
1132
+ bin/fm-backlog-handoff.sh|bin/fm-backlog-receive.sh|bin/fm-procevent-remote-reply.sh|\
1133
+ bin/fm-config-inherit-lib.sh|bin/fm-config-push.sh|bin/fm-shared*|\
1134
+ bin/fm-stow-cascade.sh)
1135
+ printf '%s\n' secondmate
1136
+ ;;
1137
+ bin/fm-session-start.sh|bin/fm-bootstrap.sh|bin/fm-fleet-sync.sh|\
1138
+ bin/fm-sessionstart-nudge.sh|bin/fm-startup-network.sh|bin/fm-tangle*|bin/fm-update.sh|\
1139
+ bin/fm-gate-refuse*|bin/fm-lock*)
1140
+ printf '%s\n' session-bootstrap
1141
+ ;;
1142
+ bin/fm-quota-axi-lib.sh)
1143
+ printf '%s\n' session-bootstrap
1144
+ printf '%s\n' "__script__:fm-procevent-quota.test.sh"
1145
+ printf '%s\n' "__script__:fm-quota-choose.test.sh"
1146
+ ;;
1147
+ bin/fm-procevent-quota.sh)
1148
+ printf '%s\n' "__script__:fm-procevent-quota.test.sh"
1149
+ ;;
1150
+ bin/fm-quota-choose.sh)
1151
+ printf '%s\n' "__script__:fm-quota-choose.test.sh"
1152
+ ;;
1153
+ bin/fm-sessionstart-run.sh|.claude/settings.json|.codex/hooks.json|\
1154
+ .pi/extensions/fm-primary-turnend-guard.ts)
1155
+ # The run tier's two harness-supplied facts (source vocabulary and
1156
+ # context-reset stdout injection) only show up against a real harness.
1157
+ printf '%s\n' session-bootstrap
1158
+ printf '%s\n' live-harness-optin
1159
+ ;;
1160
+ bin/fm-extension.mjs|bin/fm-extension.sh|docs/examples/process-event-extension/*)
1161
+ printf '%s\n' __script__:fm-extension-binding.test.sh
1162
+ ;;
1163
+ bin/fm-procevent.sh|bin/fm-procevent-lib.sh|bin/fm-procevent-extension-capture.pl)
1164
+ printf '%s\n' __script__:fm-extension-binding.test.sh
1165
+ printf '%s\n' __script__:fm-procevent.test.sh
1166
+ printf '%s\n' __script__:fm-procevent-when.test.sh
1167
+ printf '%s\n' __script__:fm-remote-reply.test.sh
1168
+ ;;
1169
+ bin/fm-timeout-lib.sh)
1170
+ # The shared hard bound: session start's runtime bound, the fleet/bearings
1171
+ # snapshots, the vendor auth probe, the stow cascade's per-home step, and
1172
+ # the wedge detector's worktree write probe all depend on it.
1173
+ printf '%s\n' session-bootstrap
1174
+ printf '%s\n' snapshot-bearings
1175
+ printf '%s\n' pure-contract-unit
1176
+ printf '%s\n' secondmate
1177
+ printf '%s\n' watcher-wake-lock
1178
+ printf '%s\n' "__script__:fm-procevent-quota.test.sh"
1179
+ ;;
1180
+ bin/fm-pr-*|bin/fm-merge-local.sh|bin/fm-teardown.sh|bin/fm-review-diff.sh|\
1181
+ bin/fm-x-*|bin/fm-check*)
1182
+ printf '%s\n' pr-forge
1183
+ ;;
1184
+ bin/fm-nm-run-lib.sh)
1185
+ # Shared no-mistakes run-attribution primitives, sourced by both
1186
+ # bin/fm-crew-state.sh (pure-contract-unit) and bin/fm-teardown.sh's
1187
+ # pre-teardown run abort (pr-forge).
1188
+ printf '%s\n' pure-contract-unit
1189
+ printf '%s\n' pr-forge
1190
+ ;;
1191
+ bin/fm-control-lib.sh)
1192
+ printf '%s\n' backend-dispatch
1193
+ printf '%s\n' session-bootstrap
1194
+ printf '%s\n' "__script__:fm-quota-choose.test.sh"
1195
+ ;;
1196
+ bin/fm-composer-lib.sh)
1197
+ # The shared shape catalogue is vendor-rendered signal; a change to it
1198
+ # re-selects the live guard (fm-composer-matrix-live-e2e) alongside the
1199
+ # portable families.
1200
+ printf '%s\n' backend-dispatch
1201
+ printf '%s\n' pure-contract-unit
1202
+ printf '%s\n' live-harness-optin
1203
+ ;;
1204
+ bin/fm-spawn.sh|bin/fm-send.sh|bin/fm-harness.sh|\
1205
+ bin/fm-peek.sh|bin/fm-composer*)
1206
+ printf '%s\n' backend-dispatch
1207
+ printf '%s\n' pure-contract-unit
1208
+ ;;
1209
+ bin/fm-task-inbox-lib.sh)
1210
+ # The steering-inbox record/doorbell/ladder owner: fm-send's data plane
1211
+ # (backend-dispatch), the watcher's re-ring check (watcher-wake-lock),
1212
+ # and the live doorbell guard against real harnesses.
1213
+ printf '%s\n' backend-dispatch
1214
+ printf '%s\n' watcher-wake-lock
1215
+ printf '%s\n' live-harness-optin
1216
+ ;;
1217
+ bin/fm-bearings-snapshot.sh|bin/fm-fleet-snapshot.sh|bin/fm-fleet-view.sh|\
1218
+ bin/fm-home-summary-refresh.sh)
1219
+ printf '%s\n' snapshot-bearings
1220
+ ;;
1221
+ bin/fm-install-herdr.sh|bin/fm-install-treehouse.sh|bin/fm-herdr-ci-cleanup.sh)
1222
+ printf '%s\n' pure-contract-unit
1223
+ # Pin or cleanup changes also select the real-Herdr family so the required
1224
+ # lane's contract coverage re-runs.
1225
+ printf '%s\n' real-herdr-gated
1226
+ ;;
1227
+ bin/fm-lint.sh|bin/fm-lint-workflows.sh|bin/fm-install-shellcheck.sh|\
1228
+ bin/fm-install-actionlint.sh|\
1229
+ bin/fm-brief.sh|bin/fm-ensure-agents-md.sh|bin/fm-crew-state.sh|\
1230
+ bin/fm-captain-hold.sh|bin/fm-decision-hold.sh|bin/fm-supervision*|bin/fm-transition-lib.sh|\
1231
+ bin/fm-tmux-lib.sh|bin/fm-marker-lib.sh|bin/fm-operational-input.sh|bin/fm-tasks-axi-lib.sh|\
1232
+ bin/fm-vendor-auth-probe.sh|\
1233
+ bin/fm-primary-scope-lib.sh|bin/fm-project-mode.sh|bin/fm-promote.sh|\
1234
+ bin/fm-ff-lib.sh|bin/fm-gotmp*|bin/*pretool*)
1235
+ printf '%s\n' pure-contract-unit
1236
+ ;;
1237
+ .agents/skills/quota-array-dispatch/SKILL.md)
1238
+ printf '%s\n' pure-contract-unit
1239
+ printf '%s\n' live-harness-optin
1240
+ ;;
1241
+ .agents/skills/harness-adapters/SKILL.md|.agents/skills/harness-adapters/references/*)
1242
+ printf '%s\n' pure-contract-unit
1243
+ printf '%s\n' live-harness-optin
1244
+ ;;
1245
+ .agents/skills/*/SKILL.md)
1246
+ printf '%s\n' pure-contract-unit
1247
+ ;;
1248
+ .github/workflows/ci.yml|.no-mistakes.yaml)
1249
+ printf '%s\n' pure-contract-unit
1250
+ printf '%s\n' real-herdr-gated
1251
+ ;;
1252
+ docs/fm-test-portable-shards.md|docs/fm-test-isolation-proof.md|\
1253
+ docs/fm-test-isolation-proof.json)
1254
+ printf '%s\n' pure-contract-unit
1255
+ ;;
1256
+ .github/*|.tasks.toml|AGENTS.md|CLAUDE.md|CONTRIBUTING.md|\
1257
+ docs/configuration.md|docs/supervision-protocols/*)
1258
+ printf '%s\n' pure-contract-unit
1259
+ ;;
1260
+ tests/lib.sh|tests/*-helpers.sh|tests/fixtures.sh)
1261
+ families_for_test_reference "$(basename "$path")" \
1262
+ || printf '%s\n' "__unmapped__:$path"
1263
+ ;;
1264
+ tests/fixtures/*/*)
1265
+ # A fixture belongs to whichever suite reads its directory, found by the
1266
+ # same reference scan used for shared helpers. Keyed on the directory
1267
+ # rather than the file so adding a fixture selects the same suite.
1268
+ # A removed fixture directory has no consuming suite left to select.
1269
+ fixture_ref=${path#tests/fixtures/}
1270
+ fixture_ref=${fixture_ref%%/*}
1271
+ if [ -d "tests/fixtures/$fixture_ref" ]; then
1272
+ families_for_test_reference "fixtures/$fixture_ref" \
1273
+ || printf '%s\n' "__unmapped__:$path"
1274
+ fi
1275
+ ;;
1276
+ bin/*)
1277
+ # A deleted script has no consuming suite left to select, the same rule
1278
+ # the fixture case above applies. Refusing on its absent mapping would
1279
+ # make every retirement branch unable to select its changed tests.
1280
+ if [ -e "$path" ]; then
1281
+ families_for_unmapped_bin "$path" \
1282
+ || printf '%s\n' "__unmapped__:$path"
1283
+ fi
1284
+ ;;
1285
+ tests/*)
1286
+ printf '%s\n' "__unmapped__:$path"
1287
+ ;;
1288
+ README.md|LICENSE|assets/*|docs/*|.gitignore)
1289
+ ;;
1290
+ *)
1291
+ families_for_test_reference "$path" \
1292
+ || printf '%s\n' "__unmapped__:$path"
1293
+ ;;
1294
+ esac
1295
+ }
1296
+
1297
+ select_changed() {
1298
+ local base=$1 path entry fam script_name s
1299
+ local -a wanted_families=()
1300
+ local -a wanted_scripts=()
1301
+
1302
+ if ! git -C "$ROOT" rev-parse --verify "$base" >/dev/null 2>&1; then
1303
+ die "changed-file base ref not found: $base (pass --base <ref>)"
1304
+ fi
1305
+
1306
+ while IFS= read -r path; do
1307
+ [ -n "$path" ] || continue
1308
+ while IFS= read -r entry; do
1309
+ [ -n "$entry" ] || continue
1310
+ case "$entry" in
1311
+ __script__:*)
1312
+ script_name=${entry#__script__:}
1313
+ wanted_scripts+=("$script_name")
1314
+ ;;
1315
+ __unmapped__:*)
1316
+ die "no changed-test mapping for source path: ${entry#__unmapped__:}"
1317
+ ;;
1318
+ *)
1319
+ wanted_families+=("$entry")
1320
+ ;;
1321
+ esac
1322
+ done < <(families_for_changed_path "$path")
1323
+ done < <(git -C "$ROOT" diff --name-only "${base}...HEAD" 2>/dev/null; \
1324
+ git -C "$ROOT" diff --name-only HEAD 2>/dev/null; \
1325
+ git -C "$ROOT" ls-files --others --exclude-standard 2>/dev/null)
1326
+
1327
+ # Dedup families
1328
+ local f seen_f
1329
+ local -a unique_families=()
1330
+ for f in "${wanted_families[@]+"${wanted_families[@]}"}"; do
1331
+ seen_f=0
1332
+ for u in "${unique_families[@]+"${unique_families[@]}"}"; do
1333
+ [ "$u" = "$f" ] && { seen_f=1; break; }
1334
+ done
1335
+ [ "$seen_f" -eq 0 ] && unique_families+=("$f")
1336
+ done
1337
+
1338
+ for f in "${unique_families[@]+"${unique_families[@]}"}"; do
1339
+ while IFS= read -r s; do
1340
+ [ -n "$s" ] || continue
1341
+ if [ "$(family_for_basename "$(basename "$s")")" = "$f" ]; then
1342
+ add_script "$s"
1343
+ fi
1344
+ done < <(all_repo_tests)
1345
+ done
1346
+
1347
+ for script_name in "${wanted_scripts[@]+"${wanted_scripts[@]}"}"; do
1348
+ if [ -f "tests/$script_name" ]; then
1349
+ add_script "tests/$script_name"
1350
+ fi
1351
+ done
1352
+
1353
+ if [ "${#SCRIPTS[@]}" -eq 0 ]; then
1354
+ log "no tests selected for changes vs $base (map is conservative; use --all for the complete suite)"
1355
+ fi
1356
+ }
1357
+
1358
+ detect_gate_skip() {
1359
+ # True when the first non-empty output line is a skip: gate message.
1360
+ local file=$1 first
1361
+ first=$(awk 'NF { print; exit }' "$file" 2>/dev/null || true)
1362
+ case "$first" in
1363
+ skip:*) return 0 ;;
1364
+ *) return 1 ;;
1365
+ esac
1366
+ }
1367
+
1368
+ # True when any output line contains "skip: <token>" (token may contain spaces).
1369
+ detect_gate_skip_token() {
1370
+ local file=$1 token=$2
1371
+ [ -n "$token" ] || return 1
1372
+ grep -F -q "skip: $token" "$file" 2>/dev/null
1373
+ }
1374
+
1375
+ apply_exclude_families() {
1376
+ local s fam keep ex
1377
+ local -a kept=()
1378
+ [ "${#EXCLUDE_FAMILIES[@]}" -gt 0 ] || return 0
1379
+ for s in "${SCRIPTS[@]+"${SCRIPTS[@]}"}"; do
1380
+ fam=$(family_for_basename "$(basename "$s")")
1381
+ keep=1
1382
+ for ex in "${EXCLUDE_FAMILIES[@]+"${EXCLUDE_FAMILIES[@]}"}"; do
1383
+ if [ "$fam" = "$ex" ]; then
1384
+ keep=0
1385
+ break
1386
+ fi
1387
+ done
1388
+ [ "$keep" -eq 1 ] && kept+=("$s")
1389
+ done
1390
+ SCRIPTS=("${kept[@]+"${kept[@]}"}")
1391
+ }
1392
+
1393
+ write_json_artifact() {
1394
+ local out=$1
1395
+ local started=$2
1396
+ local finished=$3
1397
+ local run_id=$4
1398
+ local total=$5
1399
+ local failed=$6
1400
+ local skipped=$7
1401
+ local duration=$8
1402
+ local selection=$9
1403
+ local records_file=${10}
1404
+ local families_file=${11}
1405
+
1406
+ if ! command -v python3 >/dev/null 2>&1; then
1407
+ die "--json requires python3 to emit a valid timing artifact"
1408
+ fi
1409
+
1410
+ python3 - "$out" "$started" "$finished" "$run_id" "$total" "$failed" "$skipped" "$duration" "$selection" "$records_file" "$families_file" <<'PY'
1411
+ import json, sys
1412
+
1413
+ out, started, finished, run_id, total, failed, skipped, duration, selection, records_file, families_file = sys.argv[1:]
1414
+
1415
+ scripts = []
1416
+ with open(records_file, encoding="utf-8") as fh:
1417
+ for line in fh:
1418
+ line = line.rstrip("\n")
1419
+ if not line:
1420
+ continue
1421
+ path, family, expected, exit_s, dur_s, gate = line.split("\t")
1422
+ scripts.append({
1423
+ "path": path,
1424
+ "family": family,
1425
+ "expected_gate_skip": expected,
1426
+ "duration_ms": int(dur_s),
1427
+ "exit": int(exit_s),
1428
+ "gate_skip": gate == "true",
1429
+ })
1430
+
1431
+ families = []
1432
+ with open(families_file, encoding="utf-8") as fh:
1433
+ for line in fh:
1434
+ line = line.rstrip("\n")
1435
+ if not line:
1436
+ continue
1437
+ name, count_s, dur_s, failed_s = line.split("\t")
1438
+ families.append({
1439
+ "name": name,
1440
+ "count": int(count_s),
1441
+ "duration_ms": int(dur_s),
1442
+ "failed": int(failed_s),
1443
+ })
1444
+
1445
+ doc = {
1446
+ "run_id": run_id,
1447
+ "started_at": started,
1448
+ "finished_at": finished,
1449
+ "selection": selection,
1450
+ "summary": {
1451
+ "total": int(total),
1452
+ "failed": int(failed),
1453
+ "skipped_gate": int(skipped),
1454
+ "duration_ms": int(duration),
1455
+ },
1456
+ "scripts": scripts,
1457
+ "families": families,
1458
+ }
1459
+ with open(out, "w", encoding="utf-8") as fh:
1460
+ json.dump(doc, fh, indent=2, sort_keys=True)
1461
+ fh.write("\n")
1462
+ PY
1463
+ }
1464
+
1465
+ while [ "$#" -gt 0 ]; do
1466
+ case "$1" in
1467
+ --all)
1468
+ [ -z "$MODE" ] || die "only one selection mode is allowed"
1469
+ MODE=all
1470
+ shift
1471
+ ;;
1472
+ --family)
1473
+ [ -z "$MODE" ] || die "only one selection mode is allowed"
1474
+ [ "$#" -gt 1 ] || die "--family requires a name"
1475
+ MODE=family
1476
+ FAMILY=$2
1477
+ shift 2
1478
+ ;;
1479
+ --family=*)
1480
+ [ -z "$MODE" ] || die "only one selection mode is allowed"
1481
+ MODE=family
1482
+ FAMILY=${1#--family=}
1483
+ shift
1484
+ ;;
1485
+ --lane)
1486
+ [ -z "$MODE" ] || die "only one selection mode is allowed"
1487
+ [ "$#" -gt 1 ] || die "--lane requires a name (see --list-lanes)"
1488
+ MODE=lane
1489
+ LANE=$2
1490
+ shift 2
1491
+ ;;
1492
+ --lane=*)
1493
+ [ -z "$MODE" ] || die "only one selection mode is allowed"
1494
+ MODE=lane
1495
+ LANE=${1#--lane=}
1496
+ shift
1497
+ ;;
1498
+ --proven-isolated)
1499
+ [ -z "$MODE" ] || die "only one selection mode is allowed"
1500
+ MODE=proven-isolated
1501
+ shift
1502
+ ;;
1503
+ --changed)
1504
+ [ -z "$MODE" ] || die "only one selection mode is allowed"
1505
+ MODE=changed
1506
+ shift
1507
+ ;;
1508
+ --base)
1509
+ [ "$#" -gt 1 ] || die "--base requires a git ref"
1510
+ BASE_REF=$2
1511
+ shift 2
1512
+ ;;
1513
+ --base=*)
1514
+ BASE_REF=${1#--base=}
1515
+ shift
1516
+ ;;
1517
+ --json)
1518
+ [ "$#" -gt 1 ] || die "--json requires a path"
1519
+ JSON_PATH=$2
1520
+ shift 2
1521
+ ;;
1522
+ --json=*)
1523
+ JSON_PATH=${1#--json=}
1524
+ shift
1525
+ ;;
1526
+ --jobs)
1527
+ [ "$#" -gt 1 ] || die "--jobs requires a positive integer"
1528
+ JOBS=$2
1529
+ JOBS_EXPLICIT=1
1530
+ shift 2
1531
+ ;;
1532
+ --jobs=*)
1533
+ JOBS=${1#--jobs=}
1534
+ JOBS_EXPLICIT=1
1535
+ shift
1536
+ ;;
1537
+ --max-wall-ms)
1538
+ [ "$#" -gt 1 ] || die "--max-wall-ms requires a positive integer"
1539
+ MAX_WALL_MS=$2
1540
+ shift 2
1541
+ ;;
1542
+ --max-wall-ms=*)
1543
+ MAX_WALL_MS=${1#--max-wall-ms=}
1544
+ shift
1545
+ ;;
1546
+ --per-script-timeout-secs)
1547
+ [ "$#" -gt 1 ] || die "--per-script-timeout-secs requires a whole number of seconds"
1548
+ PER_SCRIPT_TIMEOUT_SECS=$2
1549
+ shift 2
1550
+ ;;
1551
+ --per-script-timeout-secs=*)
1552
+ PER_SCRIPT_TIMEOUT_SECS=${1#--per-script-timeout-secs=}
1553
+ shift
1554
+ ;;
1555
+ --list)
1556
+ LIST_ONLY=1
1557
+ shift
1558
+ ;;
1559
+ --list-scheduled)
1560
+ LIST_SCHEDULED=1
1561
+ shift
1562
+ ;;
1563
+ --list-families)
1564
+ LIST_FAMILIES=1
1565
+ shift
1566
+ ;;
1567
+ --list-concurrent-safe-families)
1568
+ LIST_CONCURRENT_SAFE_FAMILIES=1
1569
+ shift
1570
+ ;;
1571
+ --concurrent-safe-family-jobs-max)
1572
+ [ "$#" -gt 1 ] || die "--concurrent-safe-family-jobs-max requires a family name"
1573
+ concurrent_safe_family_jobs_max "$2"
1574
+ exit 0
1575
+ ;;
1576
+ --concurrent-safe-family-jobs-max=*)
1577
+ concurrent_safe_family_jobs_max "${1#--concurrent-safe-family-jobs-max=}"
1578
+ exit 0
1579
+ ;;
1580
+ --list-lanes)
1581
+ LIST_LANES=1
1582
+ shift
1583
+ ;;
1584
+ --check-coverage)
1585
+ CHECK_COVERAGE=1
1586
+ shift
1587
+ ;;
1588
+ --aggregate-json)
1589
+ [ "$#" -gt 1 ] || die "--aggregate-json requires an output path"
1590
+ AGGREGATE_OUT=$2
1591
+ shift 2
1592
+ # Remaining args after options will be collected as inputs below via MODE.
1593
+ # For aggregation we accept only input JSON paths as free args after this.
1594
+ MODE=aggregate
1595
+ ;;
1596
+ --exclude-family)
1597
+ [ "$#" -gt 1 ] || die "--exclude-family requires a name"
1598
+ EXCLUDE_FAMILIES+=("$2")
1599
+ shift 2
1600
+ ;;
1601
+ --exclude-family=*)
1602
+ EXCLUDE_FAMILIES+=("${1#--exclude-family=}")
1603
+ shift
1604
+ ;;
1605
+ --fail-on-gate-skip)
1606
+ [ "$#" -gt 1 ] || die "--fail-on-gate-skip requires a token (e.g. 'herdr not found')"
1607
+ FAIL_ON_GATE_SKIP=$2
1608
+ shift 2
1609
+ ;;
1610
+ --fail-on-gate-skip=*)
1611
+ FAIL_ON_GATE_SKIP=${1#--fail-on-gate-skip=}
1612
+ shift
1613
+ ;;
1614
+ -h|--help)
1615
+ usage
1616
+ exit 0
1617
+ ;;
1618
+ --)
1619
+ shift
1620
+ while [ "$#" -gt 0 ]; do
1621
+ SCRIPTS+=("$1")
1622
+ shift
1623
+ done
1624
+ ;;
1625
+ -*)
1626
+ die "unknown option: $1"
1627
+ ;;
1628
+ *)
1629
+ if [ "${MODE:-}" = "aggregate" ]; then
1630
+ SCRIPTS+=("$1")
1631
+ elif [ -z "$MODE" ] || [ "$MODE" = scripts ]; then
1632
+ MODE=scripts
1633
+ SCRIPTS+=("$1")
1634
+ else
1635
+ die "script paths cannot be combined with --$MODE"
1636
+ fi
1637
+ shift
1638
+ ;;
1639
+ esac
1640
+ done
1641
+
1642
+ if [ "$LIST_FAMILIES" -eq 1 ]; then
1643
+ list_known_families
1644
+ exit 0
1645
+ fi
1646
+
1647
+ if [ "$LIST_CONCURRENT_SAFE_FAMILIES" -eq 1 ]; then
1648
+ list_concurrent_safe_families
1649
+ exit 0
1650
+ fi
1651
+
1652
+ if [ "$LIST_LANES" -eq 1 ]; then
1653
+ list_known_lanes
1654
+ exit 0
1655
+ fi
1656
+
1657
+ if [ "$CHECK_COVERAGE" -eq 1 ]; then
1658
+ run_coverage_guard
1659
+ exit $?
1660
+ fi
1661
+
1662
+ if [ "${MODE:-}" = "aggregate" ]; then
1663
+ [ -n "$AGGREGATE_OUT" ] || die "--aggregate-json requires an output path"
1664
+ [ "${#SCRIPTS[@]}" -gt 0 ] || die "--aggregate-json requires at least one input timing JSON"
1665
+ for s in "${SCRIPTS[@]}"; do
1666
+ [ -f "$s" ] || die "aggregate input not found: $s"
1667
+ done
1668
+ aggregate_timing_json "$AGGREGATE_OUT" "${SCRIPTS[@]}"
1669
+ exit 0
1670
+ fi
1671
+
1672
+ case "$JOBS" in
1673
+ ''|*[!0-9]*) die "--jobs must be a positive integer" ;;
1674
+ esac
1675
+ [ "$JOBS" -ge 1 ] || die "--jobs must be >= 1"
1676
+ [ "$JOBS" -le "$JOBS_MAX" ] || die "--jobs is capped at $JOBS_MAX (got $JOBS)"
1677
+
1678
+ if [ -n "$MAX_WALL_MS" ]; then
1679
+ case "$MAX_WALL_MS" in
1680
+ ''|*[!0-9]*) die "--max-wall-ms requires a positive integer" ;;
1681
+ esac
1682
+ [ "$MAX_WALL_MS" -gt 0 ] || die "--max-wall-ms requires a positive integer"
1683
+ fi
1684
+
1685
+ case "$PER_SCRIPT_TIMEOUT_SECS" in
1686
+ ''|*[!0-9]*) die "--per-script-timeout-secs requires a whole number of seconds (0 disables)" ;;
1687
+ esac
1688
+
1689
+ case "${MODE:-}" in
1690
+ all)
1691
+ select_all
1692
+ SELECTION_DESC="all"
1693
+ ;;
1694
+ family)
1695
+ select_family "$FAMILY"
1696
+ SELECTION_DESC="family=$FAMILY"
1697
+ ;;
1698
+ lane)
1699
+ select_lane "$LANE"
1700
+ SELECTION_DESC="lane=$LANE"
1701
+ ;;
1702
+ proven-isolated)
1703
+ select_proven_isolated
1704
+ SELECTION_DESC="proven-isolated"
1705
+ ;;
1706
+ changed)
1707
+ select_changed "$BASE_REF"
1708
+ SELECTION_DESC="changed:base=$BASE_REF"
1709
+ ;;
1710
+ scripts)
1711
+ # Normalize and re-add through add_script for consistent paths.
1712
+ raw=("${SCRIPTS[@]+"${SCRIPTS[@]}"}")
1713
+ SCRIPTS=()
1714
+ for s in "${raw[@]}"; do
1715
+ add_script "$s"
1716
+ done
1717
+ SELECTION_DESC="scripts"
1718
+ ;;
1719
+ *)
1720
+ die "select with --all, --family <name>, --lane <name>, --proven-isolated, --changed, or one or more script paths (see --help)"
1721
+ ;;
1722
+ esac
1723
+
1724
+ apply_exclude_families
1725
+ if [ "${#EXCLUDE_FAMILIES[@]}" -gt 0 ]; then
1726
+ SELECTION_DESC="${SELECTION_DESC};exclude-family=$(IFS=,; printf '%s' "${EXCLUDE_FAMILIES[*]}")"
1727
+ fi
1728
+ if [ -n "$FAIL_ON_GATE_SKIP" ]; then
1729
+ SELECTION_DESC="${SELECTION_DESC};fail-on-gate-skip=$FAIL_ON_GATE_SKIP"
1730
+ fi
1731
+ if [ "$LIST_ONLY" -eq 1 ] || [ "$LIST_SCHEDULED" -eq 1 ]; then
1732
+ if [ "$LIST_SCHEDULED" -eq 1 ]; then
1733
+ for s in "${SCRIPTS[@]+"${SCRIPTS[@]}"}"; do
1734
+ printf '%s\t%s\n' "$(portable_serial_weight_for "$s")" "$s"
1735
+ done | LC_ALL=C sort -t"$(printf '\t')" -k1,1nr -k2,2 | cut -f2-
1736
+ else
1737
+ for s in "${SCRIPTS[@]+"${SCRIPTS[@]}"}"; do
1738
+ printf '%s\n' "$s"
1739
+ done
1740
+ fi
1741
+ exit 0
1742
+ fi
1743
+
1744
+ # An empty selection is a clean result, not a no-op that falls through. Exiting
1745
+ # here also keeps every array expansion below off the empty-array path: under
1746
+ # `set -u`, bash 3.2 (the stock macOS shell) treats "${arr[@]}" on an empty
1747
+ # array as an unbound-variable error, while bash 4.4+ makes it a harmless no-op.
1748
+ # A contributor on stock macOS who changes only documentation must still get
1749
+ # total=0 and exit 0 rather than a crash.
1750
+ if [ "${#SCRIPTS[@]}" -eq 0 ]; then
1751
+ log "nothing to run"
1752
+ empty_finished_ms=$(now_ms)
1753
+ empty_duration=$((empty_finished_ms - RUN_STARTED_MS))
1754
+ [ "$empty_duration" -ge 0 ] || empty_duration=0
1755
+ empty_rc=0
1756
+ printf 'FM_TEST_SUMMARY total=0 failed=0 skipped_gate=0 duration_ms=%s\n' "$empty_duration"
1757
+ # The budget covers the whole invocation, so a selection phase that outran it
1758
+ # still fails - reporting zero work is not the same as reporting no time.
1759
+ if [ -n "$MAX_WALL_MS" ]; then
1760
+ printf 'FM_TEST_BUDGET max_wall_ms=%s duration_ms=%s\n' "$MAX_WALL_MS" "$empty_duration"
1761
+ if [ "$empty_duration" -gt "$MAX_WALL_MS" ]; then
1762
+ log "wall-clock budget exceeded: ${empty_duration}ms > ${MAX_WALL_MS}ms for $SELECTION_DESC"
1763
+ empty_rc=1
1764
+ fi
1765
+ fi
1766
+ if [ -n "$JSON_PATH" ]; then
1767
+ empty_rec=$(mktemp)
1768
+ empty_fam=$(mktemp)
1769
+ : >"$empty_rec"
1770
+ : >"$empty_fam"
1771
+ empty_finished_iso=$(now_iso)
1772
+ mkdir -p "$(dirname "$JSON_PATH")"
1773
+ write_json_artifact "$JSON_PATH" "$RUN_STARTED_ISO" "$empty_finished_iso" \
1774
+ "fm-test-run-${RUN_STARTED_MS}-$$" 0 0 0 "$empty_duration" \
1775
+ "$SELECTION_DESC" "$empty_rec" "$empty_fam"
1776
+ rm -f "$empty_rec" "$empty_fam"
1777
+ fi
1778
+ exit "$empty_rc"
1779
+ fi
1780
+
1781
+ # Verify selected scripts exist before starting.
1782
+ for s in "${SCRIPTS[@]}"; do
1783
+ [ -f "$s" ] || die "test script not found: $s"
1784
+ [ -x "$s" ] || [ -r "$s" ] || die "test script not readable: $s"
1785
+ done
1786
+
1787
+ # Plain --changed uses the bounded representative-suite scheduler; numeric
1788
+ # --jobs retains the strict all-script admission rule below.
1789
+ AUTO_CONCURRENCY=0
1790
+ if [ "$MODE" = changed ] && [ "$JOBS_EXPLICIT" -eq 0 ]; then
1791
+ if [ "${#SCRIPTS[@]}" -gt 0 ] && [ "$PER_SCRIPT_TIMEOUT_SECS" -eq 0 ]; then
1792
+ PER_SCRIPT_TIMEOUT_SECS=$CHANGED_DEFAULT_TIMEOUT_SECS
1793
+ fi
1794
+ auto_admissible=0
1795
+ for s in "${SCRIPTS[@]}"; do
1796
+ script_allows_concurrency "$s" && auto_admissible=$((auto_admissible + 1))
1797
+ done
1798
+ if [ "$auto_admissible" -gt 1 ]; then
1799
+ JOBS=$(cpu_count)
1800
+ [ "$JOBS" -le 4 ] || JOBS=4
1801
+ [ "$JOBS" -ge 1 ] || JOBS=1
1802
+ [ "$JOBS" -eq 1 ] || AUTO_CONCURRENCY=1
1803
+ fi
1804
+ fi
1805
+ if [ "$JOBS" -gt 1 ] || [ "$MODE" = changed ]; then
1806
+ SELECTION_DESC="${SELECTION_DESC};jobs=$JOBS"
1807
+ fi
1808
+
1809
+ # An explicit --jobs names a concurrency for exactly the selection given, so an
1810
+ # unproven script in it is a refusal rather than something to schedule around.
1811
+ if [ "$JOBS" -gt 1 ] && [ "$AUTO_CONCURRENCY" -eq 0 ]; then
1812
+ for s in "${SCRIPTS[@]}"; do
1813
+ if ! script_allows_concurrency "$s"; then
1814
+ die "--jobs $JOBS refused: $s is not in the proven-isolated set (see bin/fm-test-isolation-proof.sh --list) and its family has no recorded concurrent proof. Unproven stateful scripts stay serial."
1815
+ fi
1816
+ if ! is_proven_isolated_script "$s"; then
1817
+ family=$(family_for_basename "$(basename "$s")")
1818
+ family_jobs_max=$(concurrent_safe_family_jobs_max "$family")
1819
+ [ "$JOBS" -le "$family_jobs_max" ] \
1820
+ || die "--jobs $JOBS refused: family $family is proven only up to $family_jobs_max concurrent workers"
1821
+ fi
1822
+ done
1823
+ fi
1824
+
1825
+ # Split the run into the proven-concurrent scripts and an unproven remainder.
1826
+ # The remainder runs serially AFTER the concurrent group, never beside it, so an
1827
+ # unproven script still never shares a machine with another test. An explicit
1828
+ # --jobs refused above, so its remainder is always empty.
1829
+ CONCURRENT_SCRIPTS=()
1830
+ SERIAL_TAIL_SCRIPTS=()
1831
+ if [ "$JOBS" -gt 1 ]; then
1832
+ SCHEDULE_TMP=$(mktemp "${TMPDIR:-/tmp}/fm-test-sched.XXXXXX")
1833
+ : >"$SCHEDULE_TMP"
1834
+ # Two passes: the tail array must be built in this shell, so the weighted
1835
+ # listing is written to a file rather than piped into sort from a loop whose
1836
+ # appends would be lost in a subshell.
1837
+ for s in "${SCRIPTS[@]}"; do
1838
+ if script_allows_concurrency "$s"; then
1839
+ # Longest first: workers are handed scripts in order, so starting the
1840
+ # longest last strands it running alone at the tail. Measured over the
1841
+ # watcher family, alphabetical order finished in 395s where the balanced
1842
+ # four-worker sum was 205s.
1843
+ printf '%s\t%s\n' "$(portable_serial_weight_for "$s")" "$s" >>"$SCHEDULE_TMP"
1844
+ else
1845
+ SERIAL_TAIL_SCRIPTS+=("$s")
1846
+ fi
1847
+ done
1848
+ while IFS=$'\t' read -r _weight s; do
1849
+ [ -n "$s" ] || continue
1850
+ CONCURRENT_SCRIPTS+=("$s")
1851
+ done < <(LC_ALL=C sort -t"$(printf '\t')" -k1,1nr -k2,2 "$SCHEDULE_TMP")
1852
+ rm -f "$SCHEDULE_TMP"
1853
+ fi
1854
+
1855
+ if [ "$PER_SCRIPT_TIMEOUT_SECS" -gt 0 ]; then
1856
+ [ -r "$ROOT/bin/fm-timeout-lib.sh" ] || die "per-script timeout helper not found: bin/fm-timeout-lib.sh"
1857
+ # shellcheck source=bin/fm-timeout-lib.sh
1858
+ . "$ROOT/bin/fm-timeout-lib.sh"
1859
+ fi
1860
+
1861
+ RUN_TMP=$(mktemp -d "${TMPDIR:-/tmp}/fm-test-run.XXXXXX")
1862
+ RECORDS="$RUN_TMP/records.tsv"
1863
+ FAMILIES_TSV="$RUN_TMP/families.tsv"
1864
+ : >"$RECORDS"
1865
+ declare -a WORKER_PIDS=()
1866
+ declare -a WORKER_IDX=()
1867
+ declare -a WORKER_SCRIPTS=()
1868
+
1869
+ # Invoked indirectly by the EXIT trap below.
1870
+ # shellcheck disable=SC2329
1871
+ cleanup_run() {
1872
+ rm -rf "$RUN_TMP"
1873
+ }
1874
+
1875
+ trap cleanup_run EXIT
1876
+
1877
+ RUN_ID="fm-test-run-${RUN_STARTED_MS}-$$"
1878
+ TOTAL=0
1879
+ FAILED=0
1880
+ SKIPPED_GATE=0
1881
+ AGG_RC=0
1882
+
1883
+ # Family accumulators as TSV lines updated in-memory via temp files.
1884
+ # family -> count, duration_ms, failed
1885
+ family_bump() {
1886
+ local fam=$1 dur=$2 failed_delta=$3
1887
+ local line name count duration failed_count rest
1888
+ local found=0
1889
+ local tmp="$RUN_TMP/families.new"
1890
+ : >"$tmp"
1891
+ if [ -s "$FAMILIES_TSV" ]; then
1892
+ while IFS= read -r line; do
1893
+ name=${line%%$'\t'*}
1894
+ rest=${line#*$'\t'}
1895
+ count=${rest%%$'\t'*}
1896
+ rest=${rest#*$'\t'}
1897
+ duration=${rest%%$'\t'*}
1898
+ failed_count=${rest#*$'\t'}
1899
+ if [ "$name" = "$fam" ]; then
1900
+ count=$((count + 1))
1901
+ duration=$((duration + dur))
1902
+ failed_count=$((failed_count + failed_delta))
1903
+ found=1
1904
+ fi
1905
+ printf '%s\t%s\t%s\t%s\n' "$name" "$count" "$duration" "$failed_count" >>"$tmp"
1906
+ done <"$FAMILIES_TSV"
1907
+ fi
1908
+ if [ "$found" -eq 0 ]; then
1909
+ printf '%s\t%s\t%s\t%s\n' "$fam" 1 "$dur" "$failed_delta" >>"$tmp"
1910
+ fi
1911
+ mv "$tmp" "$FAMILIES_TSV"
1912
+ }
1913
+
1914
+ record_script_result() {
1915
+ local script=$1 rc=$2 duration=$3 out=$4 end_iso=$5
1916
+ local base family expected gate_skip fail_delta
1917
+ base=$(basename "$script")
1918
+ family=$(family_for_basename "$base")
1919
+ expected=$(expected_gate_skip_for_family "$family")
1920
+
1921
+ if [ -n "$FAIL_ON_GATE_SKIP" ] && detect_gate_skip_token "$out" "$FAIL_ON_GATE_SKIP"; then
1922
+ log "required gate skip token seen in $script: skip: $FAIL_ON_GATE_SKIP"
1923
+ rc=1
1924
+ fi
1925
+
1926
+ gate_skip=false
1927
+ if [ "$rc" -eq 0 ] && detect_gate_skip "$out"; then
1928
+ gate_skip=true
1929
+ SKIPPED_GATE=$((SKIPPED_GATE + 1))
1930
+ fi
1931
+
1932
+ printf 'FM_TEST_END %s %s exit=%s duration_ms=%s gate_skip=%s\n' \
1933
+ "$end_iso" "$script" "$rc" "$duration" "$gate_skip"
1934
+
1935
+ fail_delta=0
1936
+ if [ "$rc" -ne 0 ]; then
1937
+ FAILED=$((FAILED + 1))
1938
+ fail_delta=1
1939
+ AGG_RC=1
1940
+ fi
1941
+
1942
+ printf '%s\t%s\t%s\t%s\t%s\t%s\n' \
1943
+ "$script" "$family" "$expected" "$rc" "$duration" "$gate_skip" >>"$RECORDS"
1944
+ family_bump "$family" "$duration" "$fail_delta"
1945
+ TOTAL=$((TOTAL + 1))
1946
+ }
1947
+
1948
+ # Run <script>, capturing output to <out>. <stream> 1 also echoes it live.
1949
+ # <id> only has to be unique within this run. When PER_SCRIPT_TIMEOUT_SECS is
1950
+ # positive, a script that outruns it is terminated and reported as exit 124: a
1951
+ # hung script must become a bounded failure rather than an unbounded suite,
1952
+ # because an unbounded suite is what silently outruns its caller's budget.
1953
+ run_script_bounded() { # <script> <out> <stream> <id>
1954
+ local script=$1 out=$2 stream=$3 id=$4
1955
+ local rc
1956
+ : "$id"
1957
+ set +e
1958
+ if [ "$stream" -eq 1 ]; then
1959
+ if [ "$PER_SCRIPT_TIMEOUT_SECS" -gt 0 ]; then
1960
+ # Expansion is intentionally deferred to the child bash passed to -c.
1961
+ # shellcheck disable=SC2016
1962
+ fm_run_timed "$PER_SCRIPT_TIMEOUT_SECS" bash -c \
1963
+ 'bash "$1" 2>&1 | tee "$2"; exit "${PIPESTATUS[0]}"' _ "$script" "$out"
1964
+ rc=$?
1965
+ else
1966
+ bash "$script" 2>&1 | tee "$out"
1967
+ rc=${PIPESTATUS[0]}
1968
+ fi
1969
+ elif [ "$PER_SCRIPT_TIMEOUT_SECS" -gt 0 ]; then
1970
+ fm_run_timed "$PER_SCRIPT_TIMEOUT_SECS" bash "$script" >"$out" 2>&1
1971
+ rc=$?
1972
+ else
1973
+ bash "$script" >"$out" 2>&1
1974
+ rc=$?
1975
+ fi
1976
+ if [ "$PER_SCRIPT_TIMEOUT_SECS" -gt 0 ] && [ "$rc" -eq 124 ]; then
1977
+ printf 'not ok - %s exceeded the per-script bound of %ss and was terminated\n' \
1978
+ "$script" "$PER_SCRIPT_TIMEOUT_SECS" >>"$out"
1979
+ [ "$stream" -eq 1 ] && tail -1 "$out"
1980
+ fi
1981
+ return "$rc"
1982
+ }
1983
+
1984
+ run_one_serial() {
1985
+ local script=$1
1986
+ local base family expected out begin_iso begin_ms end_ms end_iso duration rc
1987
+ base=$(basename "$script")
1988
+ family=$(family_for_basename "$base")
1989
+ expected=$(expected_gate_skip_for_family "$family")
1990
+ out="$RUN_TMP/out.$TOTAL"
1991
+ begin_iso=$(now_iso)
1992
+ begin_ms=$(now_ms)
1993
+
1994
+ printf 'FM_TEST_BEGIN %s %s family=%s expected_gate_skip=%s\n' \
1995
+ "$begin_iso" "$script" "$family" "$expected"
1996
+
1997
+ set +e
1998
+ # Stream live output while retaining a copy for gate-skip detection.
1999
+ run_script_bounded "$script" "$out" 1 "s$TOTAL"
2000
+ rc=$?
2001
+ set -e
2002
+ : "${rc:=1}"
2003
+
2004
+ end_ms=$(now_ms)
2005
+ end_iso=$(now_iso)
2006
+ duration=$((end_ms - begin_ms))
2007
+ if [ "$duration" -lt 0 ]; then
2008
+ duration=0
2009
+ fi
2010
+ record_script_result "$script" "$rc" "$duration" "$out" "$end_iso"
2011
+ }
2012
+
2013
+ if [ "$JOBS" -eq 1 ]; then
2014
+ for script in "${SCRIPTS[@]}"; do
2015
+ run_one_serial "$script"
2016
+ done
2017
+ else
2018
+ # Bounded concurrent execution for admitted scripts. Each worker gets a
2019
+ # private mode-0700 TMPDIR so mktemp roots cannot collide. Retries are never
2020
+ # used as a green strategy.
2021
+ worker_n=0
2022
+ active_workers=0
2023
+
2024
+ wait_one_job_worker() {
2025
+ local slot=$1 pid idx work script rc duration mode out end_iso
2026
+ pid=${WORKER_PIDS[$slot]}
2027
+ idx=${WORKER_IDX[$slot]}
2028
+ script=${WORKER_SCRIPTS[$slot]}
2029
+ set +e
2030
+ wait "$pid"
2031
+ set -e
2032
+ unset 'WORKER_PIDS[slot]'
2033
+ unset 'WORKER_IDX[slot]'
2034
+ unset 'WORKER_SCRIPTS[slot]'
2035
+ active_workers=$((active_workers - 1))
2036
+ work="$RUN_TMP/w$idx"
2037
+ rc=$(cat "$work/exit" 2>/dev/null || echo 1)
2038
+ duration=$(cat "$work/duration_ms" 2>/dev/null || echo 0)
2039
+ out="$work/output"
2040
+ end_iso=$(now_iso)
2041
+ # Replay captured output after the worker finishes so markers stay ordered.
2042
+ if [ -s "$out" ]; then
2043
+ cat "$out"
2044
+ fi
2045
+ mode=$(stat -c %a "$work" 2>/dev/null || stat -f %Lp "$work" 2>/dev/null || echo unknown)
2046
+ case "$mode" in
2047
+ 700|0700) ;;
2048
+ *)
2049
+ log "isolation failure: worker root mode is $mode, expected 0700 ($work)"
2050
+ rc=1
2051
+ ;;
2052
+ esac
2053
+ record_script_result "$script" "$rc" "$duration" "$out" "$end_iso"
2054
+ }
2055
+
2056
+ worker_pid_is_running() {
2057
+ local want=$1 running inventory="$RUN_TMP/running-pids"
2058
+ # Keep `jobs` in this shell. A process substitution runs it in a subshell
2059
+ # without this shell's job table on Bash 3.2/5.x, falsely reporting every
2060
+ # worker complete and making the scheduler wait for the oldest PID.
2061
+ jobs -r -p >"$inventory"
2062
+ while IFS= read -r running; do
2063
+ [ "$running" = "$want" ] && return 0
2064
+ done <"$inventory"
2065
+ return 1
2066
+ }
2067
+
2068
+ wait_one_completed_job_worker() {
2069
+ local slot work
2070
+ while :; do
2071
+ for slot in "${!WORKER_PIDS[@]}"; do
2072
+ work="$RUN_TMP/w${WORKER_IDX[$slot]}"
2073
+ if [ -f "$work/exit" ] || ! worker_pid_is_running "${WORKER_PIDS[$slot]}"; then
2074
+ wait_one_job_worker "$slot"
2075
+ return
2076
+ fi
2077
+ done
2078
+ sleep 0.01
2079
+ done
2080
+ }
2081
+
2082
+ for script in "${CONCURRENT_SCRIPTS[@]+"${CONCURRENT_SCRIPTS[@]}"}"; do
2083
+ while [ "$active_workers" -ge "$JOBS" ]; do
2084
+ wait_one_completed_job_worker
2085
+ done
2086
+ worker_n=$((worker_n + 1))
2087
+ work="$RUN_TMP/w$worker_n"
2088
+ mkdir -p "$work/tmp"
2089
+ chmod 0700 "$work" "$work/tmp" || die "could not chmod 0700 worker root $work"
2090
+ base=$(basename "$script")
2091
+ family=$(family_for_basename "$base")
2092
+ expected=$(expected_gate_skip_for_family "$family")
2093
+ printf 'FM_TEST_BEGIN %s %s family=%s expected_gate_skip=%s\n' \
2094
+ "$(now_iso)" "$script" "$family" "$expected"
2095
+ (
2096
+ trap - EXIT HUP INT TERM
2097
+ set +e
2098
+ export TMPDIR="$work/tmp"
2099
+ export TMP="$work/tmp"
2100
+ unset FM_HOME FM_STATE_OVERRIDE FM_DATA_OVERRIDE FM_ROOT_OVERRIDE \
2101
+ FM_PROJECTS_OVERRIDE FM_CONFIG_OVERRIDE FM_BACKEND 2>/dev/null || true
2102
+ cd "$ROOT" || exit 1
2103
+ begin_ms=$(now_ms)
2104
+ set +e
2105
+ run_script_bounded "$script" "$work/output" 0 "w$worker_n"
2106
+ rc=$?
2107
+ set -e
2108
+ end_ms=$(now_ms)
2109
+ duration=$((end_ms - begin_ms))
2110
+ if [ "$duration" -lt 0 ]; then
2111
+ duration=0
2112
+ fi
2113
+ printf '%s\n' "$duration" >"$work/duration_ms"
2114
+ printf '%s\n' "$rc" >"$work/exit"
2115
+ exit 0
2116
+ ) &
2117
+ worker_pid=$!
2118
+ WORKER_PIDS[worker_n]=$worker_pid
2119
+ WORKER_IDX[worker_n]=$worker_n
2120
+ WORKER_SCRIPTS[worker_n]=$script
2121
+ active_workers=$((active_workers + 1))
2122
+ done
2123
+ while [ "$active_workers" -gt 0 ]; do
2124
+ wait_one_completed_job_worker
2125
+ done
2126
+ # Unproven remainder, after every concurrent worker has finished.
2127
+ for script in "${SERIAL_TAIL_SCRIPTS[@]+"${SERIAL_TAIL_SCRIPTS[@]}"}"; do
2128
+ run_one_serial "$script"
2129
+ done
2130
+ fi
2131
+
2132
+ RUN_FINISHED_ISO=$(now_iso)
2133
+ RUN_FINISHED_MS=$(now_ms)
2134
+ RUN_DURATION=$((RUN_FINISHED_MS - RUN_STARTED_MS))
2135
+ if [ "$RUN_DURATION" -lt 0 ]; then
2136
+ RUN_DURATION=0
2137
+ fi
2138
+
2139
+ printf 'FM_TEST_SUMMARY total=%s failed=%s skipped_gate=%s duration_ms=%s\n' \
2140
+ "$TOTAL" "$FAILED" "$SKIPPED_GATE" "$RUN_DURATION"
2141
+
2142
+ if [ -s "$FAMILIES_TSV" ]; then
2143
+ # Stable family summary order by name.
2144
+ sort -t$'\t' -k1,1 "$FAMILIES_TSV" | while IFS=$'\t' read -r name count duration failed_count; do
2145
+ printf 'FM_TEST_SUMMARY_FAMILY family=%s count=%s duration_ms=%s failed=%s\n' \
2146
+ "$name" "$count" "$duration" "$failed_count"
2147
+ done
2148
+ fi
2149
+
2150
+ # Slowest scripts (top 15) from records.
2151
+ if [ -s "$RECORDS" ]; then
2152
+ rank=1
2153
+ sort -t$'\t' -k5,5nr "$RECORDS" | head -n 15 | while IFS=$'\t' read -r path _family _expected _rc duration _gate; do
2154
+ printf 'FM_TEST_SLOWEST rank=%s script=%s duration_ms=%s\n' \
2155
+ "$rank" "$path" "$duration"
2156
+ rank=$((rank + 1))
2157
+ done
2158
+ fi
2159
+
2160
+ if [ -n "$JSON_PATH" ]; then
2161
+ mkdir -p "$(dirname "$JSON_PATH")"
2162
+ # Families file may be unsorted; write_json reads as-is (deterministic sort in python).
2163
+ if [ -s "$FAMILIES_TSV" ]; then
2164
+ sort -t$'\t' -k1,1 "$FAMILIES_TSV" -o "$FAMILIES_TSV"
2165
+ else
2166
+ : >"$FAMILIES_TSV"
2167
+ fi
2168
+ set +e
2169
+ write_json_artifact "$JSON_PATH" \
2170
+ "$RUN_STARTED_ISO" "$RUN_FINISHED_ISO" "$RUN_ID" \
2171
+ "$TOTAL" "$FAILED" "$SKIPPED_GATE" "$RUN_DURATION" \
2172
+ "$SELECTION_DESC" "$RECORDS" "$FAMILIES_TSV"
2173
+ json_rc=$?
2174
+ set -e
2175
+ if [ "$json_rc" -eq 0 ]; then
2176
+ log "wrote timing artifact: $JSON_PATH"
2177
+ else
2178
+ log "timing artifact finalization failed: $JSON_PATH"
2179
+ AGG_RC=1
2180
+ fi
2181
+ fi
2182
+
2183
+ if [ -n "$MAX_WALL_MS" ]; then
2184
+ printf 'FM_TEST_BUDGET max_wall_ms=%s duration_ms=%s\n' "$MAX_WALL_MS" "$RUN_DURATION"
2185
+ if [ "$RUN_DURATION" -gt "$MAX_WALL_MS" ]; then
2186
+ log "wall-clock budget exceeded: ${RUN_DURATION}ms > ${MAX_WALL_MS}ms for $SELECTION_DESC"
2187
+ AGG_RC=1
2188
+ fi
2189
+ fi
2190
+
2191
+ exit "$AGG_RC"