@zerwiz/ymir 0.1.3 → 0.1.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (850) hide show
  1. package/.agents/README.md +291 -0
  2. package/.agents/agents/bragi-marketer.md +111 -0
  3. package/.agents/agents/brokk.md +352 -0
  4. package/.agents/agents/forseti-reviewer.md +93 -0
  5. package/.agents/agents/frigg-consent.md +77 -0
  6. package/.agents/agents/groa-update.md +77 -0
  7. package/.agents/agents/hnoss-designer.md +69 -0
  8. package/.agents/agents/huginn-researcher.md +99 -0
  9. package/.agents/agents/jord-projects.md +78 -0
  10. package/.agents/agents/kvasir-scout.md +91 -0
  11. package/.agents/agents/mimir-planner.md +92 -0
  12. package/.agents/agents/muninn-stow.md +77 -0
  13. package/.agents/agents/saga-bearings.md +78 -0
  14. package/.agents/agents/sindri-developer.md +114 -0
  15. package/.agents/agents/snotra-documenter.md +105 -0
  16. package/.agents/agents/syn-recovery.md +78 -0
  17. package/.agents/agents/tyr-check.md +78 -0
  18. package/.agents/agents/urdh-hold.md +77 -0
  19. package/.agents/agents/volundr-smith.md +80 -0
  20. package/.agents/agents/vor-diagnostics.md +77 -0
  21. package/.agents/assets/agents/naming.md +50 -0
  22. package/.agents/assets/agents/registry.md +93 -0
  23. package/.agents/assets/agents/runtime.md +36 -0
  24. package/.agents/assets/agents/toon-tasks-cli.md +42 -0
  25. package/.agents/assets/templates/company_entity.template.md +37 -0
  26. package/.agents/backend/backends/cmux.sh +649 -0
  27. package/.agents/backend/backends/herdr-eventwait.py +157 -0
  28. package/.agents/backend/backends/herdr-workspace-move.py +114 -0
  29. package/.agents/backend/backends/herdr.sh +3386 -0
  30. package/.agents/backend/backends/orca.sh +290 -0
  31. package/.agents/backend/backends/tmux.sh +357 -0
  32. package/.agents/backend/backends/zellij.sh +700 -0
  33. package/.agents/backend/fm-afk-launch.sh +654 -0
  34. package/.agents/backend/fm-afk-return.sh +241 -0
  35. package/.agents/backend/fm-afk-start.sh +172 -0
  36. package/.agents/backend/fm-arm-command-policy.mjs +960 -0
  37. package/.agents/backend/fm-arm-pretool-check.sh +200 -0
  38. package/.agents/backend/fm-backend-hometag-lib.sh +52 -0
  39. package/.agents/backend/fm-backend.sh +981 -0
  40. package/.agents/backend/fm-backlog-handoff.sh +909 -0
  41. package/.agents/backend/fm-backlog-receive.sh +187 -0
  42. package/.agents/backend/fm-backlog-transition-lib.sh +779 -0
  43. package/.agents/backend/fm-bearings-board.sh +204 -0
  44. package/.agents/backend/fm-bearings-snapshot.sh +546 -0
  45. package/.agents/backend/fm-bootstrap.sh +1568 -0
  46. package/.agents/backend/fm-branch-outcome.sh +203 -0
  47. package/.agents/backend/fm-branch-prompt.sh +101 -0
  48. package/.agents/backend/fm-brief.sh +452 -0
  49. package/.agents/backend/fm-busy-event.sh +234 -0
  50. package/.agents/backend/fm-busy-lib.sh +986 -0
  51. package/.agents/backend/fm-captain-hold.sh +1001 -0
  52. package/.agents/backend/fm-cd-command-policy.mjs +153 -0
  53. package/.agents/backend/fm-cd-pretool-check.sh +190 -0
  54. package/.agents/backend/fm-check-lib.sh +74 -0
  55. package/.agents/backend/fm-check-register.sh +42 -0
  56. package/.agents/backend/fm-check-unregister.sh +52 -0
  57. package/.agents/backend/fm-classify-lib.sh +1770 -0
  58. package/.agents/backend/fm-claude-stop-autoarm.sh +334 -0
  59. package/.agents/backend/fm-composer-lib.sh +1416 -0
  60. package/.agents/backend/fm-config-inherit-lib.sh +1202 -0
  61. package/.agents/backend/fm-config-push.sh +254 -0
  62. package/.agents/backend/fm-control-lib.sh +251 -0
  63. package/.agents/backend/fm-control.sh +878 -0
  64. package/.agents/backend/fm-crew-state.sh +625 -0
  65. package/.agents/backend/fm-cursor-lib.sh +243 -0
  66. package/.agents/backend/fm-decision-hold.sh +232 -0
  67. package/.agents/backend/fm-doc-audience-check.sh +269 -0
  68. package/.agents/backend/fm-dod-lib.sh +67 -0
  69. package/.agents/backend/fm-ensure-agents-md.sh +253 -0
  70. package/.agents/backend/fm-extension-launch-barrier.mjs +129 -0
  71. package/.agents/backend/fm-extension.mjs +2577 -0
  72. package/.agents/backend/fm-extension.sh +16 -0
  73. package/.agents/backend/fm-ff-lib.sh +420 -0
  74. package/.agents/backend/fm-fleet-snapshot.sh +1463 -0
  75. package/.agents/backend/fm-fleet-sync.sh +458 -0
  76. package/.agents/backend/fm-fleet-view.sh +96 -0
  77. package/.agents/backend/fm-gate-refuse-lib.sh +102 -0
  78. package/.agents/backend/fm-guard.sh +242 -0
  79. package/.agents/backend/fm-harness.sh +195 -0
  80. package/.agents/backend/fm-herdr-ci-cleanup.sh +115 -0
  81. package/.agents/backend/fm-herdr-lab.sh +346 -0
  82. package/.agents/backend/fm-herdr-session-cleanup.sh +337 -0
  83. package/.agents/backend/fm-home-seed.sh +984 -0
  84. package/.agents/backend/fm-home-summary-refresh.sh +255 -0
  85. package/.agents/backend/fm-hook-host-lib.sh +36 -0
  86. package/.agents/backend/fm-inactive-reconcile.sh +531 -0
  87. package/.agents/backend/fm-inbox.sh +399 -0
  88. package/.agents/backend/fm-install-actionlint.sh +84 -0
  89. package/.agents/backend/fm-install-herdr.sh +96 -0
  90. package/.agents/backend/fm-install-shellcheck.sh +84 -0
  91. package/.agents/backend/fm-install-treehouse.sh +95 -0
  92. package/.agents/backend/fm-kimi-turnend-hook.sh +276 -0
  93. package/.agents/backend/fm-lease-lib.sh +218 -0
  94. package/.agents/backend/fm-lease.sh +189 -0
  95. package/.agents/backend/fm-line-cap-lib.sh +51 -0
  96. package/.agents/backend/fm-lint-workflows.sh +137 -0
  97. package/.agents/backend/fm-lint.sh +591 -0
  98. package/.agents/backend/fm-lock-lib.sh +104 -0
  99. package/.agents/backend/fm-lock.sh +107 -0
  100. package/.agents/backend/fm-marker-lib.sh +12 -0
  101. package/.agents/backend/fm-merge-local.sh +74 -0
  102. package/.agents/backend/fm-merge-outcome-lib.sh +138 -0
  103. package/.agents/backend/fm-nm-run-lib.sh +124 -0
  104. package/.agents/backend/fm-on.sh +125 -0
  105. package/.agents/backend/fm-operational-input.sh +253 -0
  106. package/.agents/backend/fm-peek.sh +46 -0
  107. package/.agents/backend/fm-pending-reply-lib.sh +1409 -0
  108. package/.agents/backend/fm-pr-check.sh +135 -0
  109. package/.agents/backend/fm-pr-lib.sh +1016 -0
  110. package/.agents/backend/fm-pr-merge.sh +707 -0
  111. package/.agents/backend/fm-pr-poll.sh +110 -0
  112. package/.agents/backend/fm-primary-scope-lib.sh +33 -0
  113. package/.agents/backend/fm-procevent-extension-capture.pl +259 -0
  114. package/.agents/backend/fm-procevent-lavish.sh +628 -0
  115. package/.agents/backend/fm-procevent-lib.sh +892 -0
  116. package/.agents/backend/fm-procevent-quota.sh +290 -0
  117. package/.agents/backend/fm-procevent-remote-reply.sh +577 -0
  118. package/.agents/backend/fm-procevent-when.sh +504 -0
  119. package/.agents/backend/fm-procevent.sh +1466 -0
  120. package/.agents/backend/fm-project-mode.sh +92 -0
  121. package/.agents/backend/fm-project-origin-lib.sh +180 -0
  122. package/.agents/backend/fm-promote.sh +282 -0
  123. package/.agents/backend/fm-public-followup-emit.sh +260 -0
  124. package/.agents/backend/fm-public-followup-lib.sh +398 -0
  125. package/.agents/backend/fm-public-followup.sh +1330 -0
  126. package/.agents/backend/fm-push-transition-lib.sh +168 -0
  127. package/.agents/backend/fm-quota-axi-lib.sh +93 -0
  128. package/.agents/backend/fm-quota-choose.sh +384 -0
  129. package/.agents/backend/fm-remote-delta-read.sh +189 -0
  130. package/.agents/backend/fm-remote-doctor.sh +800 -0
  131. package/.agents/backend/fm-remote-entrypoint.sh +193 -0
  132. package/.agents/backend/fm-remote-file.sh +234 -0
  133. package/.agents/backend/fm-remote-home-provision.sh +259 -0
  134. package/.agents/backend/fm-remote-home-seed.sh +259 -0
  135. package/.agents/backend/fm-remote-inherit-push.sh +90 -0
  136. package/.agents/backend/fm-remote-inherit.sh +179 -0
  137. package/.agents/backend/fm-remote-job-lib.sh +1241 -0
  138. package/.agents/backend/fm-remote-job-reap-orphans.sh +137 -0
  139. package/.agents/backend/fm-remote-job-worker.sh +1124 -0
  140. package/.agents/backend/fm-remote-readiness-lib.sh +44 -0
  141. package/.agents/backend/fm-remote-secondmate-control.sh +340 -0
  142. package/.agents/backend/fm-review-diff.sh +158 -0
  143. package/.agents/backend/fm-secondmate-charter-lib.sh +44 -0
  144. package/.agents/backend/fm-secondmate-nudge-lib.sh +68 -0
  145. package/.agents/backend/fm-secondmate-parent-lib.sh +70 -0
  146. package/.agents/backend/fm-secondmate-reconcile.sh +371 -0
  147. package/.agents/backend/fm-secondmate-registry-lib.sh +311 -0
  148. package/.agents/backend/fm-secondmate-report.sh +88 -0
  149. package/.agents/backend/fm-send.sh +1059 -0
  150. package/.agents/backend/fm-session-lock-lib.sh +176 -0
  151. package/.agents/backend/fm-session-start.sh +970 -0
  152. package/.agents/backend/fm-sessionstart-cursor.sh +40 -0
  153. package/.agents/backend/fm-sessionstart-nudge.sh +45 -0
  154. package/.agents/backend/fm-sessionstart-run.sh +145 -0
  155. package/.agents/backend/fm-spawn.sh +3153 -0
  156. package/.agents/backend/fm-startup-memory-budget-lib.sh +224 -0
  157. package/.agents/backend/fm-startup-memory-budget.sh +94 -0
  158. package/.agents/backend/fm-startup-network.sh +642 -0
  159. package/.agents/backend/fm-stow-cascade.sh +251 -0
  160. package/.agents/backend/fm-subagent-pretool-check.sh +207 -0
  161. package/.agents/backend/fm-supervise-daemon.sh +1732 -0
  162. package/.agents/backend/fm-supervision-instructions.sh +215 -0
  163. package/.agents/backend/fm-supervision-lib.sh +90 -0
  164. package/.agents/backend/fm-supervisor-target-lib.sh +78 -0
  165. package/.agents/backend/fm-tangle-lib.sh +53 -0
  166. package/.agents/backend/fm-task-inbox-lib.sh +402 -0
  167. package/.agents/backend/fm-tasks-axi-lib.sh +123 -0
  168. package/.agents/backend/fm-teardown.sh +2922 -0
  169. package/.agents/backend/fm-test-isolation-proof.sh +588 -0
  170. package/.agents/backend/fm-test-run.sh +2191 -0
  171. package/.agents/backend/fm-timeout-lib.sh +141 -0
  172. package/.agents/backend/fm-timing-lib.sh +179 -0
  173. package/.agents/backend/fm-tmux-lib.sh +291 -0
  174. package/.agents/backend/fm-tool-update-check.sh +898 -0
  175. package/.agents/backend/fm-trace-context-lib.sh +227 -0
  176. package/.agents/backend/fm-transition-lib.sh +103 -0
  177. package/.agents/backend/fm-turnend-guard-cursor.sh +391 -0
  178. package/.agents/backend/fm-turnend-guard-grok.sh +90 -0
  179. package/.agents/backend/fm-turnend-guard.sh +432 -0
  180. package/.agents/backend/fm-update.sh +110 -0
  181. package/.agents/backend/fm-vendor-auth-probe.sh +190 -0
  182. package/.agents/backend/fm-voice-client.py +1373 -0
  183. package/.agents/backend/fm-voice-relay.py +1256 -0
  184. package/.agents/backend/fm-wake-drain.sh +592 -0
  185. package/.agents/backend/fm-wake-grant.sh +127 -0
  186. package/.agents/backend/fm-wake-lib.sh +1849 -0
  187. package/.agents/backend/fm-watch-arm.sh +602 -0
  188. package/.agents/backend/fm-watch-checkpoint.sh +109 -0
  189. package/.agents/backend/fm-watch.sh +1962 -0
  190. package/.agents/backend/fm-x-dismiss.sh +116 -0
  191. package/.agents/backend/fm-x-followup.sh +286 -0
  192. package/.agents/backend/fm-x-lib.sh +1000 -0
  193. package/.agents/backend/fm-x-link.sh +234 -0
  194. package/.agents/backend/fm-x-poll.sh +185 -0
  195. package/.agents/backend/fm-x-reply.sh +385 -0
  196. package/.agents/backend/fm_voice_frame.py +166 -0
  197. package/.agents/backend/fm_voice_records.py +574 -0
  198. package/.agents/backend/model-bridge.py +290 -0
  199. package/.agents/backend/opencode-go-bridge.py +30 -0
  200. package/.agents/bus/README.md +12 -0
  201. package/.agents/bus/messages.json +1 -0
  202. package/.agents/config/agents.machine.example.yaml +16 -0
  203. package/.agents/config/agents.yaml +50 -0
  204. package/.agents/config/agents.yaml.example +72 -0
  205. package/.agents/config/crew-dispatch.json +22 -0
  206. package/.agents/config/cron.yaml +9 -0
  207. package/.agents/config/eindri-dispatch.json +67 -0
  208. package/.agents/config/eindri-harness +1 -0
  209. package/.agents/config/startup-memory-budget +1 -0
  210. package/.agents/config/tailscale-sync.yaml +27 -0
  211. package/.agents/filebrowser/.filebrowser.json +6 -0
  212. package/.agents/filebrowser/README.md +11 -0
  213. package/.agents/gateway/README.md +12 -0
  214. package/.agents/github/README.md +15 -0
  215. package/.agents/github/webhooks/.gitkeep +0 -0
  216. package/.agents/github/workflows/.gitkeep +0 -0
  217. package/.agents/harness/opencode/plugins/lib/rodd-operational-input.js +37 -0
  218. package/.agents/harness/opencode/plugins/package.json +4 -0
  219. package/.agents/harness/opencode/plugins/saga-sessionstart.js +77 -0
  220. package/.agents/harness/opencode/plugins/syn-cd-check.js +61 -0
  221. package/.agents/harness/opencode/plugins/syn-pretool-check.js +68 -0
  222. package/.agents/harness/opencode/plugins/syn-turnend-guard.js +105 -0
  223. package/.agents/harness/opencode/plugins/syn-watch-arm.js +521 -0
  224. package/.agents/memory/README.md +46 -0
  225. package/.agents/memory/kaia.engram +0 -0
  226. package/.agents/memory/kaia.engram-shm +0 -0
  227. package/.agents/memory/kaia.engram-wal +0 -0
  228. package/.agents/memory/runes_audit.md +5 -0
  229. package/.agents/memory/well/episodes.jsonl +367 -0
  230. package/.agents/memory/well/workspace.jsonl +27 -0
  231. package/.agents/migrations/0001-hodd-layout.sh +38 -0
  232. package/.agents/migrations/0002-a2a-mcp.sh +27 -0
  233. package/.agents/migrations/0003-private-data-separation.sh +111 -0
  234. package/.agents/migrations/0004-hoard-and-realms.sh +104 -0
  235. package/.agents/sandbox/Dockerfile.utgard +19 -0
  236. package/.agents/sandbox/README.md +14 -0
  237. package/.agents/sandbox/utgard.config.json +14 -0
  238. package/.agents/skills/README.md +59 -0
  239. package/.agents/skills/bragi-marketing/README.md +3 -0
  240. package/.agents/skills/bragi-marketing/SKILL.md +82 -0
  241. package/.agents/skills/eindri-homes/README.md +3 -0
  242. package/.agents/skills/eindri-homes/SKILL.md +291 -0
  243. package/.agents/skills/eindri-homes/assets/control-plane.md +122 -0
  244. package/.agents/skills/eindri-homes/assets/dispatch.md +80 -0
  245. package/.agents/skills/frigg-consent/README.md +3 -0
  246. package/.agents/skills/frigg-consent/SKILL.md +56 -0
  247. package/.agents/skills/galdr-ymirsystem/README.md +3 -0
  248. package/.agents/skills/galdr-ymirsystem/SKILL.md +158 -0
  249. package/.agents/skills/galdr-ymirsystem/assets/README.md +74 -0
  250. package/.agents/skills/galdr-ymirsystem/assets/brokk-distro-runtime.md +390 -0
  251. package/.agents/skills/galdr-ymirsystem/assets/build-method.md +113 -0
  252. package/.agents/skills/galdr-ymirsystem/assets/build-tool-categories.md +178 -0
  253. package/.agents/skills/galdr-ymirsystem/assets/eindri-orchestration.md +518 -0
  254. package/.agents/skills/galdr-ymirsystem/assets/eindri-profiles.md +136 -0
  255. package/.agents/skills/galdr-ymirsystem/assets/harness-integration/README.md +673 -0
  256. package/.agents/skills/galdr-ymirsystem/assets/harness-integration/claude-code.md +197 -0
  257. package/.agents/skills/galdr-ymirsystem/assets/harness-integration/codex.md +191 -0
  258. package/.agents/skills/galdr-ymirsystem/assets/harness-integration/cursor.md +193 -0
  259. package/.agents/skills/galdr-ymirsystem/assets/harness-integration/opencode.md +262 -0
  260. package/.agents/skills/galdr-ymirsystem/assets/harness-integration/pi.md +295 -0
  261. package/.agents/skills/galdr-ymirsystem/assets/hlidskjalf-ui.md +496 -0
  262. package/.agents/skills/galdr-ymirsystem/assets/installation.md +571 -0
  263. package/.agents/skills/galdr-ymirsystem/assets/local-models/HOST-DATA.example.md +72 -0
  264. package/.agents/skills/galdr-ymirsystem/assets/local-models.md +274 -0
  265. package/.agents/skills/galdr-ymirsystem/assets/memory-well.md +161 -0
  266. package/.agents/skills/galdr-ymirsystem/assets/nornir-jobs.md +377 -0
  267. package/.agents/skills/galdr-ymirsystem/assets/norse-naming.md +325 -0
  268. package/.agents/skills/galdr-ymirsystem/assets/odrerir-hall.md +129 -0
  269. package/.agents/skills/galdr-ymirsystem/assets/pi-boot/einherjar-spawn.schema.json +81 -0
  270. package/.agents/skills/galdr-ymirsystem/assets/pi-boot/herdr-profile.toml +75 -0
  271. package/.agents/skills/galdr-ymirsystem/assets/pi-boot/pi-profile.yml +96 -0
  272. package/.agents/skills/galdr-ymirsystem/assets/pi-boot/supervision-tree.yml +163 -0
  273. package/.agents/skills/galdr-ymirsystem/assets/pi-boot-guide.md +58 -0
  274. package/.agents/skills/galdr-ymirsystem/assets/porting-upstream-to-norse.md +301 -0
  275. package/.agents/skills/galdr-ymirsystem/assets/principles.md +385 -0
  276. package/.agents/skills/galdr-ymirsystem/assets/reference-adoption.md +99 -0
  277. package/.agents/skills/galdr-ymirsystem/assets/registry.md +379 -0
  278. package/.agents/skills/galdr-ymirsystem/assets/runtime-compliance.md +463 -0
  279. package/.agents/skills/galdr-ymirsystem/assets/runtime-components.md +553 -0
  280. package/.agents/skills/galdr-ymirsystem/assets/smidja.md +175 -0
  281. package/.agents/skills/galdr-ymirsystem/schemas/toon-schemas.md +190 -0
  282. package/.agents/skills/galdr-ymirsystem/scripts/README.md +45 -0
  283. package/.agents/skills/galdr-ymirsystem/scripts/bench-one.sh +248 -0
  284. package/.agents/skills/galdr-ymirsystem/scripts/compliance-check.sh +360 -0
  285. package/.agents/skills/galdr-ymirsystem/scripts/gpu-sample.sh +43 -0
  286. package/.agents/skills/galdr-ymirsystem/scripts/stop-all.sh +34 -0
  287. package/.agents/skills/galdr-ymirsystem/scripts/toon-check.py +188 -0
  288. package/.agents/skills/gjallarhorn-relay/README.md +3 -0
  289. package/.agents/skills/gjallarhorn-relay/SKILL.md +283 -0
  290. package/.agents/skills/groa-update/SKILL.md +70 -0
  291. package/.agents/skills/hamr-adapters/README.md +3 -0
  292. package/.agents/skills/hamr-adapters/SKILL.md +123 -0
  293. package/.agents/skills/hamr-adapters/references/common/control-and-recovery.md +37 -0
  294. package/.agents/skills/hamr-adapters/references/common/dispatch.md +32 -0
  295. package/.agents/skills/hamr-adapters/references/common/model-and-effort.md +42 -0
  296. package/.agents/skills/hamr-adapters/references/common/primary-hooks.md +40 -0
  297. package/.agents/skills/hamr-adapters/references/harness/claude.md +55 -0
  298. package/.agents/skills/hamr-adapters/references/harness/codex.md +43 -0
  299. package/.agents/skills/hamr-adapters/references/harness/cursor.md +75 -0
  300. package/.agents/skills/hamr-adapters/references/harness/grok.md +69 -0
  301. package/.agents/skills/hamr-adapters/references/harness/kimi.md +51 -0
  302. package/.agents/skills/hamr-adapters/references/harness/muse.md +70 -0
  303. package/.agents/skills/hamr-adapters/references/harness/opencode.md +42 -0
  304. package/.agents/skills/hamr-adapters/references/harness/pi.md +56 -0
  305. package/.agents/skills/herdr-panes/README.md +3 -0
  306. package/.agents/skills/herdr-panes/SKILL.md +92 -0
  307. package/.agents/skills/herdr-panes/assets/herdr-backend.md +346 -0
  308. package/.agents/skills/herdr-panes/assets/tmux-backend.md +115 -0
  309. package/.agents/skills/hnoss-design/DESIGN.md +19 -0
  310. package/.agents/skills/hnoss-design/README.md +3 -0
  311. package/.agents/skills/hnoss-design/SKILL.md +83 -0
  312. package/.agents/skills/hvild-afk/README.md +3 -0
  313. package/.agents/skills/hvild-afk/SKILL.md +241 -0
  314. package/.agents/skills/jord-projects/README.md +3 -0
  315. package/.agents/skills/jord-projects/SKILL.md +92 -0
  316. package/.agents/skills/lifecycle/SKILL.md +56 -0
  317. package/.agents/skills/lifecycle/smoke_test.sh +52 -0
  318. package/.agents/skills/lifecycle/start.sh +21 -0
  319. package/.agents/skills/lifecycle/status.sh +35 -0
  320. package/.agents/skills/lifecycle/stop.sh +16 -0
  321. package/.agents/skills/muninn-stow/README.md +3 -0
  322. package/.agents/skills/muninn-stow/SKILL.md +309 -0
  323. package/.agents/skills/no-mistakes/SKILL.md +379 -0
  324. package/.agents/skills/nornir-schedule/README.md +3 -0
  325. package/.agents/skills/nornir-schedule/SKILL.md +25 -0
  326. package/.agents/skills/nornir-schedule/assets/events.md +137 -0
  327. package/.agents/skills/nornir-schedule/assets/quota.md +131 -0
  328. package/.agents/skills/nsr-compliance/README.md +3 -0
  329. package/.agents/skills/nsr-compliance/SKILL.md +29 -0
  330. package/.agents/skills/nsr-compliance/assets/nsr/README.md +40 -0
  331. package/.agents/skills/nsr-compliance/assets/nsr/assets/RULES/README.md +15 -0
  332. package/.agents/skills/nsr-compliance/assets/nsr/assets/RULES/git-workflow.md +18 -0
  333. package/.agents/skills/nsr-compliance/assets/nsr/assets/RULES/security.md +22 -0
  334. package/.agents/skills/nsr-compliance/assets/nsr/assets/RULES/tech-stack.md +8 -0
  335. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/SKILL.md +71 -0
  336. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/config/core_four.yaml +16 -0
  337. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/gates/check_danger.sh +102 -0
  338. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/gates/check_wiring.sh +69 -0
  339. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/gates/validate_code.sh +29 -0
  340. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/gates/verify_docs.py +66 -0
  341. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/generate.sh +109 -0
  342. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/harness/envelopes/result_envelope.json +14 -0
  343. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/harness/envelopes/task_envelope.json +20 -0
  344. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/harness/runner.py +303 -0
  345. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/telemetry/logger.py +112 -0
  346. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/templates/compliance/README.md +18 -0
  347. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/templates/compliance/config/README.md +10 -0
  348. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/templates/compliance/gates/README.md +12 -0
  349. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/templates/compliance/gates/check_env.sh +23 -0
  350. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/templates/compliance/gates/check_paths.sh +20 -0
  351. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/templates/compliance/gates/check_platform.sh +23 -0
  352. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/templates/compliance/harness/README.md +11 -0
  353. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/templates/compliance/harness/envelopes/README.md +10 -0
  354. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/templates/compliance/installer/README.md +10 -0
  355. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/templates/compliance/installer/install.sh +125 -0
  356. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/templates/compliance/telemetry/README.md +11 -0
  357. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/README.md +20 -0
  358. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/features/README.md +36 -0
  359. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/features/SKILL-template.md +14 -0
  360. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/git_ops/README.md +25 -0
  361. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/git_ops/SKILL.md +13 -0
  362. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/git_ops/create_branch.sh +6 -0
  363. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/git_ops/safe_commit.sh +9 -0
  364. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/git_ops/sync_upstream.sh +6 -0
  365. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/lifecycle/README.md +27 -0
  366. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/lifecycle/SKILL.md +15 -0
  367. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/lifecycle/smoke_test.sh +12 -0
  368. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/lifecycle/start.sh +19 -0
  369. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/lifecycle/status.sh +12 -0
  370. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/lifecycle/stop.sh +15 -0
  371. package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/README.md +18 -0
  372. package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/config/README.md +10 -0
  373. package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/config/core_four.yaml +13 -0
  374. package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/gates/README.md +12 -0
  375. package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/gates/check_danger.sh +102 -0
  376. package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/gates/check_env.sh +23 -0
  377. package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/gates/check_paths.sh +20 -0
  378. package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/gates/check_platform.sh +23 -0
  379. package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/gates/check_wiring.sh +69 -0
  380. package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/harness/README.md +11 -0
  381. package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/harness/envelopes/README.md +10 -0
  382. package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/harness/envelopes/result_envelope.json +14 -0
  383. package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/harness/envelopes/task_envelope.json +20 -0
  384. package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/harness/runner.py +303 -0
  385. package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/installer/README.md +10 -0
  386. package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/installer/install.sh +125 -0
  387. package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/telemetry/README.md +11 -0
  388. package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/telemetry/logger.py +112 -0
  389. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/BEST_PRACTICES/README.md +14 -0
  390. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/BEST_PRACTICES/api.md +17 -0
  391. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/BEST_PRACTICES/compliance-wiring.md +155 -0
  392. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/BEST_PRACTICES/frontend.md +15 -0
  393. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/CI_CD/README.md +11 -0
  394. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/CI_CD/deployment/README.md +23 -0
  395. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/CI_CD/deployment/clients/README.md +16 -0
  396. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/CI_CD/deployment/clients/template-README.md +11 -0
  397. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/CI_CD/deployment/deploy.sh +49 -0
  398. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/CI_CD/deployment/envs/README.md +12 -0
  399. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/CI_CD/deployment/envs/development.env.example +17 -0
  400. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/CI_CD/deployment/envs/production.env.example +17 -0
  401. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/CI_CD/deployment/envs/staging.env.example +17 -0
  402. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/CI_CD/deployment/tenants/README.md +11 -0
  403. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/CI_CD/deployment/tenants/template.env.example +8 -0
  404. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/DEVELOPER_SETUP/README.md +26 -0
  405. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/DEVELOPER_SETUP/agent.md +7 -0
  406. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/DEVELOPER_SETUP/developers/dev-1.md +20 -0
  407. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/DEVELOPER_SETUP/linux.md +9 -0
  408. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/DEVELOPER_SETUP/macos.md +10 -0
  409. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/DEVELOPER_SETUP/shared.md +19 -0
  410. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/DEVELOPER_SETUP/windows.md +14 -0
  411. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/HOSTING/README.md +25 -0
  412. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/HOSTING/_template.md +41 -0
  413. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/HOSTING/hosting-1.md +41 -0
  414. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/README.md +21 -0
  415. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/RUNBOOK/README.md +15 -0
  416. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/RUNBOOK/deployment.md +19 -0
  417. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/RUNBOOK/disaster-recovery.md +15 -0
  418. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/RUNBOOK/ops-tasks.md +14 -0
  419. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/features/README.md +16 -0
  420. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/project-skills.md +146 -0
  421. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/research/README.md +11 -0
  422. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/research/software-compliance-research.md +269 -0
  423. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/working-with-agents.md +260 -0
  424. package/.agents/skills/nsr-compliance/assets/nsr/assets/root/AGENTS.md +38 -0
  425. package/.agents/skills/nsr-compliance/assets/nsr/assets/root/ARCHITECTURE.md +36 -0
  426. package/.agents/skills/nsr-compliance/assets/nsr/assets/root/BEST_PRACTICES.md +27 -0
  427. package/.agents/skills/nsr-compliance/assets/nsr/assets/root/CI_CD.md +23 -0
  428. package/.agents/skills/nsr-compliance/assets/nsr/assets/root/FEATURES.md +15 -0
  429. package/.agents/skills/nsr-compliance/assets/nsr/assets/root/README.md +22 -0
  430. package/.agents/skills/nsr-compliance/assets/nsr/assets/root/RULES.md +37 -0
  431. package/.agents/skills/nsr-compliance/assets/nsr/assets/root/STRUCTURE.md +45 -0
  432. package/.agents/skills/nsr-compliance/assets/nsr/assets/root/TECH_STACK.md +28 -0
  433. package/.agents/skills/nsr-compliance/assets/nsr/scaffold-spec.md +63 -0
  434. package/.agents/skills/nsr-compliance/assets/nsr/scripts/scaffold.sh +103 -0
  435. package/.agents/skills/nsr-compliance/assets/nsr/scripts/validate.sh +52 -0
  436. package/.agents/skills/nsr-compliance/assets/nsrcompliance/SKILL.md +71 -0
  437. package/.agents/skills/nsr-compliance/assets/nsrcompliance/config/core_four.yaml +16 -0
  438. package/.agents/skills/nsr-compliance/assets/nsrcompliance/gates/check_danger.sh +102 -0
  439. package/.agents/skills/nsr-compliance/assets/nsrcompliance/gates/check_wiring.sh +69 -0
  440. package/.agents/skills/nsr-compliance/assets/nsrcompliance/gates/validate_code.sh +29 -0
  441. package/.agents/skills/nsr-compliance/assets/nsrcompliance/gates/verify_docs.py +66 -0
  442. package/.agents/skills/nsr-compliance/assets/nsrcompliance/generate.sh +109 -0
  443. package/.agents/skills/nsr-compliance/assets/nsrcompliance/harness/envelopes/result_envelope.json +14 -0
  444. package/.agents/skills/nsr-compliance/assets/nsrcompliance/harness/envelopes/task_envelope.json +20 -0
  445. package/.agents/skills/nsr-compliance/assets/nsrcompliance/harness/runner.py +303 -0
  446. package/.agents/skills/nsr-compliance/assets/nsrcompliance/telemetry/logger.py +112 -0
  447. package/.agents/skills/nsr-compliance/assets/nsrcompliance/templates/compliance/README.md +18 -0
  448. package/.agents/skills/nsr-compliance/assets/nsrcompliance/templates/compliance/config/README.md +10 -0
  449. package/.agents/skills/nsr-compliance/assets/nsrcompliance/templates/compliance/gates/README.md +12 -0
  450. package/.agents/skills/nsr-compliance/assets/nsrcompliance/templates/compliance/gates/check_env.sh +23 -0
  451. package/.agents/skills/nsr-compliance/assets/nsrcompliance/templates/compliance/gates/check_paths.sh +20 -0
  452. package/.agents/skills/nsr-compliance/assets/nsrcompliance/templates/compliance/gates/check_platform.sh +23 -0
  453. package/.agents/skills/nsr-compliance/assets/nsrcompliance/templates/compliance/harness/README.md +11 -0
  454. package/.agents/skills/nsr-compliance/assets/nsrcompliance/templates/compliance/harness/envelopes/README.md +10 -0
  455. package/.agents/skills/nsr-compliance/assets/nsrcompliance/templates/compliance/installer/README.md +10 -0
  456. package/.agents/skills/nsr-compliance/assets/nsrcompliance/templates/compliance/installer/install.sh +125 -0
  457. package/.agents/skills/nsr-compliance/assets/nsrcompliance/templates/compliance/telemetry/README.md +11 -0
  458. package/.agents/skills/pr-ops/README.md +3 -0
  459. package/.agents/skills/pr-ops/SKILL.md +109 -0
  460. package/.agents/skills/ratatoskr-a2a/README.md +3 -0
  461. package/.agents/skills/ratatoskr-a2a/SKILL.md +148 -0
  462. package/.agents/skills/rules-check-drift/SKILL.md +81 -0
  463. package/.agents/skills/saga-bearings/README.md +3 -0
  464. package/.agents/skills/saga-bearings/SKILL.md +26 -0
  465. package/.agents/skills/saga-bearings/assets/bearings.md +161 -0
  466. package/.agents/skills/saga-bearings/assets/board-template.html +731 -0
  467. package/.agents/skills/saga-bearings/assets/recap.md +56 -0
  468. package/.agents/skills/syn-recovery/README.md +3 -0
  469. package/.agents/skills/syn-recovery/SKILL.md +54 -0
  470. package/.agents/skills/tyr-check/README.md +3 -0
  471. package/.agents/skills/tyr-check/SKILL.md +218 -0
  472. package/.agents/skills/urdh-hold/README.md +3 -0
  473. package/.agents/skills/urdh-hold/SKILL.md +25 -0
  474. package/.agents/skills/urdh-hold/assets/decisions.md +15 -0
  475. package/.agents/skills/urdh-hold/assets/hold.md +54 -0
  476. package/.agents/skills/vor-diagnostics/README.md +3 -0
  477. package/.agents/skills/vor-diagnostics/SKILL.md +68 -0
  478. package/.agents/skills/ymir-host/README.md +3 -0
  479. package/.agents/skills/ymir-host/SKILL.md +34 -0
  480. package/.agents/skills/ymir-host/assets/desktop.md +36 -0
  481. package/.agents/skills/ymir-host/assets/omarchy.md +297 -0
  482. package/.agents/skills/ymir-host/assets/runbooks.md +15 -0
  483. package/.agents/skills/ymir-host/assets/thjazi.md +220 -0
  484. package/.agents/skills/ymir-host/assets/update.md +64 -0
  485. package/.agents/tests/assets/board-render-harness.mjs +117 -0
  486. package/.agents/tests/cmux-test-safety.sh +51 -0
  487. package/.agents/tests/fixtures.sh +291 -0
  488. package/.agents/tests/fm-afk-inject-e2e.test.sh +428 -0
  489. package/.agents/tests/fm-afk-inject-herdr-e2e.test.sh +536 -0
  490. package/.agents/tests/fm-afk-launch.test.sh +961 -0
  491. package/.agents/tests/fm-afk-pi-herdr-return-e2e.test.sh +297 -0
  492. package/.agents/tests/fm-afk-return.test.sh +283 -0
  493. package/.agents/tests/fm-arm-pretool-check.test.sh +482 -0
  494. package/.agents/tests/fm-ask-user-authority.test.sh +42 -0
  495. package/.agents/tests/fm-backend-autodetect-smoke.test.sh +176 -0
  496. package/.agents/tests/fm-backend-cmux-smoke.test.sh +188 -0
  497. package/.agents/tests/fm-backend-cmux.test.sh +1166 -0
  498. package/.agents/tests/fm-backend-herdr-eventwait-smoke.test.sh +136 -0
  499. package/.agents/tests/fm-backend-herdr-eventwait.test.py +106 -0
  500. package/.agents/tests/fm-backend-herdr-focus-flash-e2e.test.sh +409 -0
  501. package/.agents/tests/fm-backend-herdr-launcher-workspace-e2e.test.sh +437 -0
  502. package/.agents/tests/fm-backend-herdr-presentation-e2e.test.sh +1424 -0
  503. package/.agents/tests/fm-backend-herdr-prune-safety-e2e.test.sh +180 -0
  504. package/.agents/tests/fm-backend-herdr-respawn-idem-e2e.test.sh +182 -0
  505. package/.agents/tests/fm-backend-herdr-smoke.test.sh +355 -0
  506. package/.agents/tests/fm-backend-herdr-workspace-per-home-e2e.test.sh +251 -0
  507. package/.agents/tests/fm-backend-herdr.test.sh +4606 -0
  508. package/.agents/tests/fm-backend-orca.test.sh +1357 -0
  509. package/.agents/tests/fm-backend-tmux-smoke.test.sh +173 -0
  510. package/.agents/tests/fm-backend-zellij-smoke.test.sh +205 -0
  511. package/.agents/tests/fm-backend-zellij.test.sh +1360 -0
  512. package/.agents/tests/fm-backend.test.sh +1142 -0
  513. package/.agents/tests/fm-backlog-atomicity.test.sh +2302 -0
  514. package/.agents/tests/fm-backlog-handoff.test.sh +1360 -0
  515. package/.agents/tests/fm-bearings-board-render.test.sh +135 -0
  516. package/.agents/tests/fm-bearings-board.test.sh +414 -0
  517. package/.agents/tests/fm-bearings-snapshot.test.sh +1990 -0
  518. package/.agents/tests/fm-bootstrap-network-parallel.test.sh +316 -0
  519. package/.agents/tests/fm-bootstrap.test.sh +1178 -0
  520. package/.agents/tests/fm-branch-supervision.test.sh +552 -0
  521. package/.agents/tests/fm-brief.test.sh +785 -0
  522. package/.agents/tests/fm-busy-adapter-wiring.test.sh +324 -0
  523. package/.agents/tests/fm-busy-state.test.sh +465 -0
  524. package/.agents/tests/fm-calm-pi-extension.test.sh +3979 -0
  525. package/.agents/tests/fm-captain-hold-lifecycle.test.sh +1187 -0
  526. package/.agents/tests/fm-cd-pretool-check.test.sh +400 -0
  527. package/.agents/tests/fm-check-unregister.test.sh +198 -0
  528. package/.agents/tests/fm-classify-corr-token.test.sh +543 -0
  529. package/.agents/tests/fm-classify-decision-key.test.sh +340 -0
  530. package/.agents/tests/fm-claude-stop-autoarm-live-e2e.test.sh +164 -0
  531. package/.agents/tests/fm-claude-stop-autoarm.test.sh +1190 -0
  532. package/.agents/tests/fm-cmux-claude-composer-live-e2e.test.sh +110 -0
  533. package/.agents/tests/fm-codex-continuity-live-e2e.test.sh +55 -0
  534. package/.agents/tests/fm-composer-ghost.test.sh +716 -0
  535. package/.agents/tests/fm-composer-lib.test.sh +666 -0
  536. package/.agents/tests/fm-composer-matrix-live-e2e.test.sh +220 -0
  537. package/.agents/tests/fm-control-herdr-smoke.test.sh +149 -0
  538. package/.agents/tests/fm-control-relaunch.test.sh +1530 -0
  539. package/.agents/tests/fm-control.test.sh +910 -0
  540. package/.agents/tests/fm-crew-state.test.sh +1610 -0
  541. package/.agents/tests/fm-cursor-harness.test.sh +404 -0
  542. package/.agents/tests/fm-cursor-primary-live-e2e.test.sh +217 -0
  543. package/.agents/tests/fm-cursor-primary.test.sh +708 -0
  544. package/.agents/tests/fm-daemon.test.sh +2740 -0
  545. package/.agents/tests/fm-documentation-audiences.test.sh +141 -0
  546. package/.agents/tests/fm-ensure-agents-md.test.sh +371 -0
  547. package/.agents/tests/fm-extension-binding.test.sh +2187 -0
  548. package/.agents/tests/fm-fleet-snapshot-view.test.sh +816 -0
  549. package/.agents/tests/fm-fleet-sync.test.sh +721 -0
  550. package/.agents/tests/fm-gate-refuse.test.sh +356 -0
  551. package/.agents/tests/fm-gitignore-config.test.sh +89 -0
  552. package/.agents/tests/fm-gotmp.test.sh +230 -0
  553. package/.agents/tests/fm-grok-continuity-live-e2e.test.sh +112 -0
  554. package/.agents/tests/fm-grok-harness.test.sh +117 -0
  555. package/.agents/tests/fm-grok-stop-live-e2e.test.sh +219 -0
  556. package/.agents/tests/fm-guard-stale-banner.test.sh +709 -0
  557. package/.agents/tests/fm-harness-adapter-instructions-live-e2e.test.sh +134 -0
  558. package/.agents/tests/fm-harness-adapter-references.test.sh +30 -0
  559. package/.agents/tests/fm-harness-liveness-drift-live-e2e.test.sh +151 -0
  560. package/.agents/tests/fm-herdr-lab.test.sh +243 -0
  561. package/.agents/tests/fm-herdr-session-cleanup-e2e.test.sh +146 -0
  562. package/.agents/tests/fm-herdr-session-cleanup.test.sh +329 -0
  563. package/.agents/tests/fm-herdr-submit-confirm-live-e2e.test.sh +125 -0
  564. package/.agents/tests/fm-herdr-version-floor-live-e2e.test.sh +144 -0
  565. package/.agents/tests/fm-home-summary-refresh.test.sh +971 -0
  566. package/.agents/tests/fm-inactive-reconcile.test.sh +485 -0
  567. package/.agents/tests/fm-kimi-harness.test.sh +687 -0
  568. package/.agents/tests/fm-lint-workflows.test.sh +532 -0
  569. package/.agents/tests/fm-lint.test.sh +1023 -0
  570. package/.agents/tests/fm-muse-harness.test.sh +932 -0
  571. package/.agents/tests/fm-muse-signals-live-e2e.test.sh +205 -0
  572. package/.agents/tests/fm-no-mistakes-required.test.sh +72 -0
  573. package/.agents/tests/fm-on.test.sh +507 -0
  574. package/.agents/tests/fm-opencode-primary-live-e2e.test.sh +357 -0
  575. package/.agents/tests/fm-operational-input.test.sh +160 -0
  576. package/.agents/tests/fm-peek-remote.test.sh +110 -0
  577. package/.agents/tests/fm-pending-reply.test.sh +1288 -0
  578. package/.agents/tests/fm-pi-branch-extension.test.sh +3182 -0
  579. package/.agents/tests/fm-pi-branch-live-e2e.test.sh +527 -0
  580. package/.agents/tests/fm-pi-primary-live-e2e.test.sh +345 -0
  581. package/.agents/tests/fm-pi-primary-types.test.sh +68 -0
  582. package/.agents/tests/fm-pi-watch-extension.test.sh +2843 -0
  583. package/.agents/tests/fm-pr-check-security.test.sh +2140 -0
  584. package/.agents/tests/fm-pr-merge.test.sh +2135 -0
  585. package/.agents/tests/fm-procevent-quota.test.sh +225 -0
  586. package/.agents/tests/fm-procevent-when.test.sh +406 -0
  587. package/.agents/tests/fm-procevent.test.sh +1745 -0
  588. package/.agents/tests/fm-project-origin.test.sh +110 -0
  589. package/.agents/tests/fm-public-followup.test.sh +2272 -0
  590. package/.agents/tests/fm-quota-array-dispatch-live-e2e.test.sh +526 -0
  591. package/.agents/tests/fm-quota-choose.test.sh +626 -0
  592. package/.agents/tests/fm-remote-backlog-handoff.test.sh +453 -0
  593. package/.agents/tests/fm-remote-doctor.test.sh +625 -0
  594. package/.agents/tests/fm-remote-entrypoint.test.sh +59 -0
  595. package/.agents/tests/fm-remote-job-orphan-reap.test.sh +205 -0
  596. package/.agents/tests/fm-remote-job.test.sh +768 -0
  597. package/.agents/tests/fm-remote-reply.test.sh +520 -0
  598. package/.agents/tests/fm-remote-secondmate-lifecycle-e2e.test.sh +1256 -0
  599. package/.agents/tests/fm-remote-secondmate-parent-binding.test.sh +326 -0
  600. package/.agents/tests/fm-remote-secondmate-trace-context.test.sh +310 -0
  601. package/.agents/tests/fm-remote-transport-lanes.test.sh +431 -0
  602. package/.agents/tests/fm-review-diff.test.sh +176 -0
  603. package/.agents/tests/fm-secondmate-harness.test.sh +2600 -0
  604. package/.agents/tests/fm-secondmate-lifecycle-e2e.test.sh +259 -0
  605. package/.agents/tests/fm-secondmate-liveness.test.sh +559 -0
  606. package/.agents/tests/fm-secondmate-reconcile.test.sh +732 -0
  607. package/.agents/tests/fm-secondmate-safety.test.sh +2972 -0
  608. package/.agents/tests/fm-secondmate-sync.test.sh +913 -0
  609. package/.agents/tests/fm-send-inbox-doorbell-live-e2e.test.sh +206 -0
  610. package/.agents/tests/fm-send-inbox.test.sh +352 -0
  611. package/.agents/tests/fm-send-popup-settle.test.sh +167 -0
  612. package/.agents/tests/fm-send-remote-delivery.test.sh +796 -0
  613. package/.agents/tests/fm-send-resolve-key.test.sh +551 -0
  614. package/.agents/tests/fm-send-secondmate-marker-herdr-e2e.test.sh +187 -0
  615. package/.agents/tests/fm-send-secondmate-marker.test.sh +278 -0
  616. package/.agents/tests/fm-send-settle.test.sh +147 -0
  617. package/.agents/tests/fm-send-strict.test.sh +241 -0
  618. package/.agents/tests/fm-session-lock-ancestry.test.sh +365 -0
  619. package/.agents/tests/fm-session-start.test.sh +2515 -0
  620. package/.agents/tests/fm-sessionstart-hook-live-e2e.test.sh +631 -0
  621. package/.agents/tests/fm-sessionstart-instruction-refresh-live-e2e.test.sh +230 -0
  622. package/.agents/tests/fm-sessionstart-nudge.test.sh +1039 -0
  623. package/.agents/tests/fm-shared-captain-inheritance.test.sh +404 -0
  624. package/.agents/tests/fm-spawn-batch.test.sh +148 -0
  625. package/.agents/tests/fm-spawn-dispatch-profile.test.sh +827 -0
  626. package/.agents/tests/fm-spawn-pool-base-freshen.test.sh +440 -0
  627. package/.agents/tests/fm-spawn-worktree-settle.test.sh +147 -0
  628. package/.agents/tests/fm-startup-memory-budget.test.sh +333 -0
  629. package/.agents/tests/fm-startup-network.test.sh +712 -0
  630. package/.agents/tests/fm-stow-cascade.test.sh +370 -0
  631. package/.agents/tests/fm-subagent-pretool-check.test.sh +291 -0
  632. package/.agents/tests/fm-supervision-events.test.sh +156 -0
  633. package/.agents/tests/fm-supervision-instructions.test.sh +189 -0
  634. package/.agents/tests/fm-tangle-guard.test.sh +277 -0
  635. package/.agents/tests/fm-task-delivery.test.sh +420 -0
  636. package/.agents/tests/fm-task-inbox.test.sh +513 -0
  637. package/.agents/tests/fm-teardown-endpoint-safety.test.sh +374 -0
  638. package/.agents/tests/fm-teardown.test.sh +2665 -0
  639. package/.agents/tests/fm-test-fixture-cleanup.test.sh +172 -0
  640. package/.agents/tests/fm-test-fixtures.test.sh +132 -0
  641. package/.agents/tests/fm-test-isolation-proof.test.sh +278 -0
  642. package/.agents/tests/fm-test-run.test.sh +1196 -0
  643. package/.agents/tests/fm-tmux-agent-liveness.test.sh +354 -0
  644. package/.agents/tests/fm-tmux-submit-busy.test.sh +340 -0
  645. package/.agents/tests/fm-tool-update-check.test.sh +1044 -0
  646. package/.agents/tests/fm-trace-context-lib.test.sh +253 -0
  647. package/.agents/tests/fm-trace-context-spawn.test.sh +581 -0
  648. package/.agents/tests/fm-transition-lib.test.sh +54 -0
  649. package/.agents/tests/fm-turnend-guard.test.sh +1822 -0
  650. package/.agents/tests/fm-update.test.sh +304 -0
  651. package/.agents/tests/fm-vendor-auth-probe.test.sh +395 -0
  652. package/.agents/tests/fm-voice-relay.test.sh +4741 -0
  653. package/.agents/tests/fm-wake-daemon-lifecycle-e2e.test.sh +171 -0
  654. package/.agents/tests/fm-wake-drain-open-decisions-cursor.test.sh +356 -0
  655. package/.agents/tests/fm-wake-drain-open-decisions.test.sh +226 -0
  656. package/.agents/tests/fm-wake-drain-unread-status.test.sh +379 -0
  657. package/.agents/tests/fm-wake-queue.test.sh +1224 -0
  658. package/.agents/tests/fm-watch-arm.test.sh +821 -0
  659. package/.agents/tests/fm-watch-checkpoint.test.sh +87 -0
  660. package/.agents/tests/fm-watch-recovery-loop.test.sh +223 -0
  661. package/.agents/tests/fm-watch-triage.test.sh +3889 -0
  662. package/.agents/tests/fm-watcher-lock.test.sh +1133 -0
  663. package/.agents/tests/fm-x-mode.test.sh +3042 -0
  664. package/.agents/tests/gleipnir-guard-pretool-check.test.sh +51 -0
  665. package/.agents/tests/gleipnir-machine-lock.test.sh +130 -0
  666. package/.agents/tests/herdr-test-safety.sh +42 -0
  667. package/.agents/tests/lib.sh +315 -0
  668. package/.agents/tests/remote-herdr-fixture.sh +125 -0
  669. package/.agents/tests/secondmate-helpers.sh +193 -0
  670. package/.agents/tests/smoke.test.sh +93 -0
  671. package/.agents/tests/wake-helpers.sh +338 -0
  672. package/.agents/tests/zellij-test-safety.sh +60 -0
  673. package/.agents/tools/README.md +10 -0
  674. package/.agents/tools/bin/tasks-cli +267 -0
  675. package/.agents/tools/tasks-cli.ts +216 -0
  676. package/AGENTS.md +454 -0
  677. package/CHANGELOG.md +881 -0
  678. package/LICENSE +1 -1
  679. package/NOTICE +63 -0
  680. package/README.md +113 -39
  681. package/RULES/01-domains.md +51 -0
  682. package/RULES/02-agents.md +78 -0
  683. package/RULES/03-houses.md +61 -0
  684. package/RULES/04-hoard.md +65 -0
  685. package/RULES/05-platforms.md +67 -0
  686. package/RULES/06-append-only.md +58 -0
  687. package/RULES/07-config.md +45 -0
  688. package/RULES/08-delivery-gate.md +75 -0
  689. package/RULES/README.md +20 -0
  690. package/bin/README.md +107 -0
  691. package/bin/a2a-mcp.sh +98 -0
  692. package/bin/a2a-serve.py +103 -0
  693. package/bin/a2a-serve.sh +8 -0
  694. package/bin/a2a-talk.sh +50 -0
  695. package/bin/a2abridge-ensure.sh +108 -0
  696. package/bin/agent-run.sh +43 -0
  697. package/bin/agents-config.sh +233 -0
  698. package/bin/apodex-smoke-test.sh +92 -0
  699. package/bin/bifrost-bridge.sh +112 -0
  700. package/bin/bifrost-ingress.sh +77 -0
  701. package/bin/bootstrap-macos.sh +107 -0
  702. package/bin/bootstrap-windows.ps1 +112 -0
  703. package/bin/branch-guard.sh +64 -0
  704. package/bin/brokk +73 -0
  705. package/bin/brokk-classify-lib.sh +1770 -0
  706. package/bin/brokk-lease-lib.sh +222 -0
  707. package/bin/brokk-lease.sh +193 -0
  708. package/bin/brokk-lint.sh +65 -0
  709. package/bin/brokk-send.sh +38 -0
  710. package/bin/brokk-update.sh +9 -0
  711. package/bin/brokk-wake-grant.sh +127 -0
  712. package/bin/brokk-wake-lib.sh +1849 -0
  713. package/bin/changelog-guard.sh +165 -0
  714. package/bin/crash-sense.sh +160 -0
  715. package/bin/desktop-place.sh +224 -0
  716. package/bin/docs-guard.sh +39 -0
  717. package/bin/editor-place.sh +207 -0
  718. package/bin/eindri-acclaim.sh +34 -0
  719. package/bin/eindri-control.sh +28 -0
  720. package/bin/eindri-role.sh +83 -0
  721. package/bin/eindri-seen.sh +41 -0
  722. package/bin/eindri-send.sh +15 -0
  723. package/bin/eindri-start.sh +85 -0
  724. package/bin/eindri-watch.sh +57 -0
  725. package/bin/einherjar-spawn.sh +576 -0
  726. package/bin/eir-doctor.sh +112 -0
  727. package/bin/erindi-brief.sh +271 -0
  728. package/bin/fleet-apply.sh +68 -0
  729. package/bin/github-deploy.sh +81 -0
  730. package/bin/gjallarhorn-expose.sh +102 -0
  731. package/bin/gjallarhorn-notify.sh +58 -0
  732. package/bin/gjallarhorn-purge.sh +54 -0
  733. package/bin/gjallarhorn-tunnel.sh +72 -0
  734. package/bin/gleipnir-lock-lib.sh +284 -0
  735. package/bin/groa-update.sh +122 -0
  736. package/bin/hall-snapshot.sh +92 -0
  737. package/bin/hamr-harness.sh +246 -0
  738. package/bin/heimdall-ssh-keys.sh +374 -0
  739. package/bin/herdr-agents.py +44 -0
  740. package/bin/herdr-ensure.sh +104 -0
  741. package/bin/herdr-run.sh +431 -0
  742. package/bin/hermes-ensure.sh +88 -0
  743. package/bin/hoard-lib.sh +25 -0
  744. package/bin/hodd.sh +78 -0
  745. package/bin/host-sense.sh +199 -0
  746. package/bin/huginn-research-worker.sh +108 -0
  747. package/bin/local-model-lock.sh +51 -0
  748. package/bin/mimir-bridge.py +226 -0
  749. package/bin/mimir-bridge.sh +87 -0
  750. package/bin/mimir-ingest.sh +131 -0
  751. package/bin/mimir.sh +113 -0
  752. package/bin/mjollnir-webhook.sh +59 -0
  753. package/bin/mjollnir.sh +85 -0
  754. package/bin/model-hardware.sh +82 -0
  755. package/bin/model-resolve.sh +147 -0
  756. package/bin/models-detect.sh +178 -0
  757. package/bin/nornir-cron-start.sh +162 -0
  758. package/bin/nornir-job-daily-briefing.sh +191 -0
  759. package/bin/nornir-job-git-sync.sh +152 -0
  760. package/bin/nornir-job-memory-housekeeping.sh +114 -0
  761. package/bin/nornir-job-observer.sh +166 -0
  762. package/bin/nornir-job-skillopt-sleep.sh +76 -0
  763. package/bin/omarchy-hook-install.sh +60 -0
  764. package/bin/omarchy-install.sh +148 -0
  765. package/bin/omarchy-plugins.sh +174 -0
  766. package/bin/omarchy-sense.sh +149 -0
  767. package/bin/perm-guard.sh +30 -0
  768. package/bin/pi-agent.sh +68 -0
  769. package/bin/pi-ensure.sh +77 -0
  770. package/bin/pi-local.sh +37 -0
  771. package/bin/pi-seat.sh +63 -0
  772. package/bin/prereq-ensure.sh +166 -0
  773. package/bin/private-guard.sh +38 -0
  774. package/bin/project-git.sh +55 -0
  775. package/bin/public-guard.sh +70 -0
  776. package/bin/ratatoskr.sh +49 -0
  777. package/bin/realm-lib.sh +32 -0
  778. package/bin/repo-scrub.sh +62 -0
  779. package/bin/rodd-operational-input.sh +250 -0
  780. package/bin/runes-append.sh +198 -0
  781. package/bin/saga-session-start.sh +172 -0
  782. package/bin/saga-sessionstart-run.sh +79 -0
  783. package/bin/saga-wake-drain.sh +41 -0
  784. package/bin/secret-guard.sh +63 -0
  785. package/bin/sessrumnir-ensure.sh +100 -0
  786. package/bin/sessrumnir-sync.sh +237 -0
  787. package/bin/sessrumnir.sh +91 -0
  788. package/bin/skillopt-setup.sh +76 -0
  789. package/bin/skuld-branch-outcome.sh +203 -0
  790. package/bin/skuld-branch-prompt.sh +101 -0
  791. package/bin/smidja-bootstrap.sh +90 -0
  792. package/bin/smidja-observe.sh +86 -0
  793. package/bin/syn-arm-pretool-check.sh +57 -0
  794. package/bin/syn-asset-pretool-check.sh +76 -0
  795. package/bin/syn-cd-pretool-check.sh +28 -0
  796. package/bin/syn-guard-pretool-check.sh +83 -0
  797. package/bin/syn-turnend-guard.sh +36 -0
  798. package/bin/syn-watch-arm.sh +99 -0
  799. package/bin/tailscale-sync.sh +190 -0
  800. package/bin/telegram-bot.sh +69 -0
  801. package/bin/toolchain.sh +74 -0
  802. package/bin/utgard.sh +111 -0
  803. package/bin/valhalla.sh +115 -0
  804. package/bin/valknut-load.sh +260 -0
  805. package/bin/vor-crew-state.sh +124 -0
  806. package/bin/wedge-notify.sh +47 -0
  807. package/bin/workspace-provision.sh +80 -0
  808. package/bin/workspace-rag.sh +150 -0
  809. package/bin/wyrd-db.sh +47 -0
  810. package/bin/yggdrasil.sh +152 -0
  811. package/bin/ymir-edit.sh +145 -0
  812. package/bin/ymir-install.sh +641 -0
  813. package/bin/ymir-invite.sh +93 -0
  814. package/bin/ymir-isolation.sh +88 -0
  815. package/bin/ymir-migrate.sh +61 -0
  816. package/bin/ymir-platform.sh +348 -0
  817. package/bin/ymir-say.sh +97 -0
  818. package/bin/ymir-setup-auth.sh +108 -0
  819. package/bin/ymir-validate.sh +173 -0
  820. package/bin/ymir.js +74 -16
  821. package/docs/Architecture.md +339 -0
  822. package/docs/design.md +351 -0
  823. package/docs/installations/README.md +63 -0
  824. package/docs/installations/ymir-home.md +93 -0
  825. package/docs/integration.md +105 -0
  826. package/docs/lore.md +1056 -0
  827. package/docs/research/agent-harnesses.md +290 -0
  828. package/docs/runbooks/README.md +19 -0
  829. package/docs/runbooks/a2a.md +91 -0
  830. package/docs/runbooks/agent-permissions.md +99 -0
  831. package/docs/runbooks/agents.md +57 -0
  832. package/docs/runbooks/models.md +69 -0
  833. package/docs/runbooks/realm-onboarding.md +60 -0
  834. package/docs/runbooks/secrets-and-hoard.md +60 -0
  835. package/docs/runbooks/updates-and-migrations.md +110 -0
  836. package/docs/runbooks/worktrees-isolation.md +72 -0
  837. package/docs/session-start.md +105 -0
  838. package/docs/skillopt-integration.md +321 -0
  839. package/docs/sync-system.md +66 -0
  840. package/docs/workspaces.md +104 -0
  841. package/docs/ymir-rut.md +260 -0
  842. package/package.json +44 -14
  843. package/packaging/build.sh +74 -0
  844. package/packaging/macos/Ymir Installer.command +43 -0
  845. package/packaging/windows/ymir-setup.nsi +74 -0
  846. package/scripts/README.md +22 -0
  847. package/scripts/electron.sh +210 -0
  848. package/scripts/start.sh +167 -0
  849. package/scripts/stop.sh +113 -0
  850. package/install.sh +0 -35
@@ -0,0 +1,1610 @@
1
+ #!/usr/bin/env bash
2
+ # Behavior tests for bin/fm-crew-state.sh - the deterministic crew-current-state
3
+ # helper.
4
+ #
5
+ # The status file (state/<id>.status) is a best-effort append-only EVENT LOG, so
6
+ # `tail -1` of it reports the last event, not the current state. fm-crew-state
7
+ # reads the AUTHORITATIVE source (a matching no-mistakes run-step, else the
8
+ # semantic busy-state contract) and reconciles the possibly-stale log against it. These
9
+ # cases pin every branch of that logic, hermetically, over real throwaway git
10
+ # repos with a fake `no-mistakes` (run-step source) and a fake `tmux` (pane
11
+ # source):
12
+ # (a) active run-step is authoritative -> run-step
13
+ # (b) needs-decision/blocked log + resumed run = SUPERSEDED -> run-step
14
+ # (c) genuine parked run + needs-decision log = NOT superseded -> run-step
15
+ # (d) terminal run-step (passed/failed) is authoritative -> run-step
16
+ # (e) cross-branch attribution: this branch's own run found via list lookup
17
+ # (f) no run + semantic busy -> pane
18
+ # (g) no run + semantic idle falls to the status-log verb -> status-log
19
+ # (h) dead pane: no run -> unknown/none; with a run -> run-step (not the shell)
20
+ # (i) kind=scout skips the run lookup -> pane/status-log
21
+ # (j) torn-down worktree / missing meta -> unknown/none
22
+ # (k) crew_is_provably_working end-to-end over the REAL helper (not a canned
23
+ # fake fm-crew-state.sh verdict): cross-branch attribution via the runs
24
+ # list -> absorbed; genuinely no run anywhere + idle pane -> surfaced.
25
+ # This is the direct regression pair for the 2026-07-02 herdr incident,
26
+ # proving the watcher's own absorb-only-when-provably-working predicate
27
+ # benefits from the fix in both directions.
28
+ set -u
29
+
30
+ # shellcheck source=tests/lib.sh
31
+ . "$(dirname "${BASH_SOURCE[0]}")/lib.sh"
32
+ # shellcheck source=/dev/null
33
+ . "$ROOT/bin/fm-classify-lib.sh"
34
+
35
+ CREW_STATE="$ROOT/bin/fm-crew-state.sh"
36
+ TMP_ROOT=$(fm_test_tmproot fm-crew-state)
37
+ fm_git_identity fmtest fmtest@example.invalid
38
+
39
+ # A real git repo checked out on <branch>, so the helper's branch attribution
40
+ # (git symbolic-ref) resolves like it would for a live crew worktree.
41
+ make_repo_on_branch() { # <dir> <branch>
42
+ local dir=$1 branch=$2
43
+ mkdir -p "$dir"
44
+ git -C "$dir" init -q
45
+ git -C "$dir" commit -q --allow-empty -m init
46
+ git -C "$dir" checkout -q -b "$branch"
47
+ # Real worktree HEAD for run head-binding (fixtures read FM_FAKE_RUN_HEAD).
48
+ FM_FAKE_RUN_HEAD=$(git -C "$dir" rev-parse HEAD)
49
+ export FM_FAKE_RUN_HEAD
50
+ }
51
+
52
+ # A fakebin with a fake `no-mistakes` (serves the env-driven run output) and a
53
+ # fake `tmux` (serves a busy or idle pane). The fake no-mistakes mirrors the real
54
+ # command surface the helper uses: `axi status`, `axi status --run <id>` (the
55
+ # `axi` surface - no runs-listing subcommand exists under it, verified against
56
+ # the real CLI), and the actual top-level run-listing command, `no-mistakes
57
+ # runs --limit N`, which is plain text - no run id, no quoting - serving
58
+ # FM_FAKE_RUNS_LIST verbatim.
59
+ make_fakebin() { # <dir> -> echoes fakebin path
60
+ local dir=$1 fb="$1/fakebin"
61
+ mkdir -p "$fb"
62
+ cat > "$fb/no-mistakes" <<'SH'
63
+ #!/usr/bin/env bash
64
+ set -u
65
+ case "${1:-}" in
66
+ axi)
67
+ shift
68
+ case "${1:-}" in
69
+ status)
70
+ shift
71
+ if [ "${1:-}" = --run ]; then printf '%s\n' "${FM_FAKE_AXI_STATUS_RUN:-}"
72
+ else printf '%s\n' "${FM_FAKE_AXI_STATUS:-}"; fi ;;
73
+ logs)
74
+ printf '%s\n' "${FM_FAKE_CI_LOGS:-}" ;;
75
+ esac
76
+ ;;
77
+ runs)
78
+ printf '%s\n' "${FM_FAKE_RUNS_LIST:-}" ;;
79
+ esac
80
+ exit 0
81
+ SH
82
+ cat > "$fb/tmux" <<'SH'
83
+ #!/usr/bin/env bash
84
+ set -u
85
+ case "${1:-}" in
86
+ display-message)
87
+ [ "${FM_FAKE_TMUX_MISSING:-0}" = 1 ] && exit 1
88
+ printf '%%1\n' ;;
89
+ capture-pane)
90
+ [ "${FM_FAKE_TMUX_MISSING:-0}" = 1 ] && exit 1
91
+ if [ "${FM_FAKE_BUSY:-0}" = 1 ]; then printf 'work in progress\n%s\n' "${FM_FAKE_BUSY_TEXT:-esc to interrupt}"
92
+ else printf 'all quiet\n> \n'; fi ;;
93
+ esac
94
+ exit 0
95
+ SH
96
+ cat > "$fb/herdr" <<'SH'
97
+ #!/usr/bin/env bash
98
+ set -u
99
+ case "${1:-}" in
100
+ status)
101
+ [ "${2:-}" = --json ] && {
102
+ printf '{"client":{"version":"0.7.1","protocol":14},"server":{"running":true}}\n'
103
+ exit 0
104
+ } ;;
105
+ server)
106
+ exit 0 ;;
107
+ pane)
108
+ case "${2:-}" in
109
+ read)
110
+ [ "${FM_FAKE_HERDR_MISSING:-0}" = 1 ] && exit 1
111
+ if [ "${FM_FAKE_HERDR_BUSY:-0}" = 1 ]; then printf 'work in progress\nesc to interrupt\n'
112
+ else printf 'all quiet\n> \n'; fi
113
+ exit 0 ;;
114
+ esac ;;
115
+ agent)
116
+ case "${2:-}" in
117
+ get)
118
+ [ -n "${FM_FAKE_HERDR_AGENT_STATUS:-}" ] || exit 1
119
+ printf '{"result":{"agent":{"agent_status":"%s"}}}\n' "$FM_FAKE_HERDR_AGENT_STATUS"
120
+ exit 0 ;;
121
+ esac ;;
122
+ esac
123
+ exit 0
124
+ SH
125
+ chmod +x "$fb/no-mistakes" "$fb/tmux" "$fb/herdr"
126
+ printf '%s\n' "$fb"
127
+ }
128
+
129
+ make_no_timeout_toolbin() { # <dir> -> echoes toolbin path
130
+ local dir=$1 tb="$1/notimeoutbin" tool real
131
+ mkdir -p "$tb"
132
+ for tool in bash git grep sed head cut tail dirname perl; do
133
+ real=$(command -v "$tool" || true)
134
+ [ -n "$real" ] || fail "missing tool for no-timeout path: $tool"
135
+ ln -s "$real" "$tb/$tool"
136
+ done
137
+ printf '%s\n' "$tb"
138
+ }
139
+
140
+ # Run the helper for one case dir. FM_FAKE_* env (run output, busy flag) are read
141
+ # from the caller's environment by the fakes above.
142
+ run_crew_state() { # <case-dir> <id>
143
+ PATH="$1/fakebin:$PATH" FM_STATE_OVERRIDE="$1/state" "$CREW_STATE" "$2"
144
+ }
145
+
146
+ new_case() { # <name> -> echoes case dir with an empty state/
147
+ local d="$TMP_ROOT/$1"
148
+ mkdir -p "$d/state"
149
+ printf '%s\n' "$d"
150
+ }
151
+
152
+ arm_idle_record() { # <state-dir> <id>
153
+ local state=$1 id=$2 gen
154
+ gen=$("$ROOT/bin/fm-busy-event.sh" arm "$state" "$id")
155
+ "$ROOT/bin/fm-busy-event.sh" apply "$state" "$id" idle --gen "$gen" \
156
+ --source claude-hook --event stop
157
+ }
158
+
159
+ # Clear the fake-driver vars and (re-)mark them exported, so the per-test plain
160
+ # assignments below stay exported into the fakes without an `export VAR=$(...)`
161
+ # command-substitution assignment (SC2155).
162
+ reset_fakes() {
163
+ FM_FAKE_AXI_STATUS=""
164
+ FM_FAKE_AXI_STATUS_RUN=""
165
+ FM_FAKE_RUNS_LIST=""
166
+ FM_FAKE_BUSY=0
167
+ FM_FAKE_BUSY_TEXT=
168
+ FM_FAKE_TMUX_MISSING=0
169
+ FM_FAKE_HERDR_BUSY=0
170
+ FM_FAKE_HERDR_MISSING=0
171
+ FM_FAKE_HERDR_AGENT_STATUS=""
172
+ FM_FAKE_CI_LOGS=""
173
+ export FM_FAKE_AXI_STATUS FM_FAKE_AXI_STATUS_RUN FM_FAKE_RUNS_LIST FM_FAKE_BUSY FM_FAKE_BUSY_TEXT FM_FAKE_TMUX_MISSING
174
+ export FM_FAKE_HERDR_BUSY FM_FAKE_HERDR_MISSING FM_FAKE_HERDR_AGENT_STATUS FM_FAKE_CI_LOGS
175
+ }
176
+
177
+ # --- run-object fixtures (TOON, as `no-mistakes axi status` emits) -----------
178
+
179
+ run_running() { # <branch>
180
+ cat <<EOF
181
+ run:
182
+ id: "01RUN"
183
+ branch: $1
184
+ status: running
185
+ head: "${FM_FAKE_RUN_HEAD:-abc1234}"
186
+ pr: ""
187
+ findings: none
188
+ steps[2]{step,status,findings,duration_ms}:
189
+ intent,completed,0,0
190
+ review,running,0,0
191
+ EOF
192
+ }
193
+
194
+ run_fixing() { # <branch>
195
+ cat <<EOF
196
+ run:
197
+ id: "01RUN"
198
+ branch: $1
199
+ status: fixing
200
+ head: "${FM_FAKE_RUN_HEAD:-abc1234}"
201
+ pr: ""
202
+ findings: none
203
+ EOF
204
+ }
205
+
206
+ run_top_level_ci() { # <branch>
207
+ cat <<EOF
208
+ run:
209
+ id: "01RUN"
210
+ branch: $1
211
+ status: ci
212
+ head: "${FM_FAKE_RUN_HEAD:-abc1234}"
213
+ pr: "https://github.com/o/r/pull/2"
214
+ findings: none
215
+ EOF
216
+ }
217
+
218
+ run_parked() { # <branch>
219
+ cat <<EOF
220
+ run:
221
+ id: "01RUN"
222
+ branch: $1
223
+ status: awaiting_approval
224
+ awaiting_agent: parked 2m10s
225
+ head: "${FM_FAKE_RUN_HEAD:-abc1234}"
226
+ pr: ""
227
+ findings[2]{id,severity,file,line,action,description}:
228
+ r1,warning,a.go,,auto-fix,ignored error
229
+ r2,error,b.go,,ask-user,changes product behavior
230
+ gate: review
231
+ EOF
232
+ }
233
+
234
+ run_parked_scalar_gate_running() { # <branch>
235
+ cat <<EOF
236
+ run:
237
+ id: "01RUN"
238
+ branch: $1
239
+ status: running
240
+ head: "${FM_FAKE_RUN_HEAD:-abc1234}"
241
+ pr: ""
242
+ findings[1]{id,severity,file,line,action,description}:
243
+ r1,error,b.go,,ask-user,changes product behavior
244
+ gate: review
245
+ EOF
246
+ }
247
+
248
+ run_parked_in_gate_block() { # <branch>
249
+ cat <<EOF
250
+ run:
251
+ id: "01RUN"
252
+ branch: $1
253
+ status: running
254
+ head: "${FM_FAKE_RUN_HEAD:-abc1234}"
255
+ pr: ""
256
+ findings[1]{id,severity,file,line,action,description}:
257
+ r1,error,b.go,,ask-user,changes product behavior
258
+ gate:
259
+ step: review
260
+ status: fix_review
261
+ steps[3]{step,status,findings,duration_ms}:
262
+ intent,completed,0,0
263
+ review,fix_review,1,0
264
+ test,pending,0,0
265
+ EOF
266
+ }
267
+
268
+ run_passed() { # <branch>
269
+ cat <<EOF
270
+ run:
271
+ id: "01RUN"
272
+ branch: $1
273
+ status: completed
274
+ head: "${FM_FAKE_RUN_HEAD:-abc1234}"
275
+ pr: "https://github.com/o/r/pull/1"
276
+ findings: none
277
+ outcome: passed
278
+ EOF
279
+ }
280
+
281
+ run_failed() { # <branch>
282
+ cat <<EOF
283
+ run:
284
+ id: "01RUN"
285
+ branch: $1
286
+ status: completed
287
+ head: "${FM_FAKE_RUN_HEAD:-abc1234}"
288
+ pr: ""
289
+ findings: none
290
+ outcome: failed
291
+ EOF
292
+ }
293
+
294
+ run_ci_monitoring() { # <branch>
295
+ cat <<EOF
296
+ run:
297
+ id: "01RUN"
298
+ branch: $1
299
+ status: running
300
+ head: "${FM_FAKE_RUN_HEAD:-abc1234}"
301
+ pr: "https://github.com/o/r/pull/2"
302
+ findings: none
303
+ steps[4]{step,status,findings,duration_ms}:
304
+ intent,completed,0,0
305
+ review,completed,0,0
306
+ push,completed,0,0
307
+ ci,running,0,0
308
+ EOF
309
+ }
310
+
311
+ run_fixing_ci_running() { # <branch>
312
+ cat <<EOF
313
+ run:
314
+ id: "01RUN"
315
+ branch: $1
316
+ status: fixing
317
+ head: "${FM_FAKE_RUN_HEAD:-abc1234}"
318
+ pr: "https://github.com/o/r/pull/2"
319
+ findings: none
320
+ steps[4]{step,status,findings,duration_ms}:
321
+ intent,completed,0,0
322
+ review,completed,0,0
323
+ push,completed,0,0
324
+ ci,running,0,0
325
+ EOF
326
+ }
327
+
328
+ run_ci_fixing() { # <branch>
329
+ cat <<EOF
330
+ run:
331
+ id: "01RUN"
332
+ branch: $1
333
+ status: fixing
334
+ head: "${FM_FAKE_RUN_HEAD:-abc1234}"
335
+ pr: "https://github.com/o/r/pull/2"
336
+ findings: none
337
+ steps[4]{step,status,findings,duration_ms}:
338
+ intent,completed,0,0
339
+ review,completed,0,0
340
+ push,completed,0,0
341
+ ci,fixing,0,0
342
+ EOF
343
+ }
344
+
345
+ # ---------------------------------------------------------------------------
346
+ # (a) active run-step is authoritative
347
+ test_active_run_is_authoritative() {
348
+ reset_fakes
349
+ local d; d=$(new_case active)
350
+ make_repo_on_branch "$d/wt" fm/feat-a
351
+ make_fakebin "$d" >/dev/null
352
+ fm_write_meta "$d/state/feat-a.meta" "window=fm:fm-feat-a" "worktree=$d/wt" "kind=ship"
353
+ FM_FAKE_AXI_STATUS="$(run_running fm/feat-a)"
354
+ local out; out=$(run_crew_state "$d" feat-a)
355
+ assert_contains "$out" "state: working" "active run -> working"
356
+ assert_contains "$out" "source: run-step" "active run -> run-step source"
357
+ assert_contains "$out" "validating (running)" "active run reports the step"
358
+ pass "active run-step is authoritative"
359
+ }
360
+
361
+ # (b) needs-decision log + a resumed (running/fixing) run = SUPERSEDED
362
+ test_stale_needs_decision_superseded() {
363
+ reset_fakes
364
+ local d; d=$(new_case superseded)
365
+ make_repo_on_branch "$d/wt" fm/feat-b
366
+ make_fakebin "$d" >/dev/null
367
+ fm_write_meta "$d/state/feat-b.meta" "window=fm:fm-feat-b" "worktree=$d/wt" "kind=ship"
368
+ printf 'working: started\nneeds-decision: pick A or B\n' > "$d/state/feat-b.status"
369
+ FM_FAKE_AXI_STATUS="$(run_fixing fm/feat-b)"
370
+ local out; out=$(run_crew_state "$d" feat-b)
371
+ assert_contains "$out" "state: working" "resumed run -> working despite needs-decision log"
372
+ assert_contains "$out" "source: run-step" "resumed run -> run-step source"
373
+ assert_contains "$out" "superseded" "stale needs-decision log flagged superseded"
374
+ pass "stale needs-decision over active run is superseded"
375
+ }
376
+
377
+ # blocked log + a resumed run is also superseded
378
+ test_stale_blocked_superseded() {
379
+ reset_fakes
380
+ local d; d=$(new_case superseded-blocked)
381
+ make_repo_on_branch "$d/wt" fm/feat-bb
382
+ make_fakebin "$d" >/dev/null
383
+ fm_write_meta "$d/state/feat-bb.meta" "window=fm:fm-feat-bb" "worktree=$d/wt" "kind=ship"
384
+ printf 'blocked: waiting on review answer\n' > "$d/state/feat-bb.status"
385
+ FM_FAKE_AXI_STATUS="$(run_running fm/feat-bb)"
386
+ local out; out=$(run_crew_state "$d" feat-bb)
387
+ assert_contains "$out" "state: working" "resumed run -> working despite blocked log"
388
+ assert_contains "$out" "superseded" "stale blocked log flagged superseded"
389
+ pass "stale blocked over active run is superseded"
390
+ }
391
+
392
+ # (c) genuine parked run + needs-decision log AGREE -> parked, NOT superseded
393
+ test_genuine_parked_not_superseded() {
394
+ reset_fakes
395
+ local d; d=$(new_case parked)
396
+ make_repo_on_branch "$d/wt" fm/feat-c
397
+ make_fakebin "$d" >/dev/null
398
+ fm_write_meta "$d/state/feat-c.meta" "window=fm:fm-feat-c" "worktree=$d/wt" "kind=ship"
399
+ printf 'needs-decision: review gate\n' > "$d/state/feat-c.status"
400
+ FM_FAKE_AXI_STATUS="$(run_parked fm/feat-c)"
401
+ local out; out=$(run_crew_state "$d" feat-c)
402
+ assert_contains "$out" "state: parked" "genuine parked run -> parked"
403
+ assert_contains "$out" "source: run-step" "parked -> run-step source"
404
+ assert_contains "$out" "2 finding(s)" "parked includes gate finding count"
405
+ assert_contains "$out" "ask-user" "parked surfaces ask-user finding"
406
+ assert_not_contains "$out" "superseded" "agreeing parked+needs-decision not flagged stale"
407
+ pass "genuine parked run is not flagged superseded"
408
+ }
409
+
410
+ test_scalar_gate_parked_not_superseded() {
411
+ reset_fakes
412
+ local d; d=$(new_case parked-scalar-gate)
413
+ make_repo_on_branch "$d/wt" fm/feat-cs
414
+ make_fakebin "$d" >/dev/null
415
+ fm_write_meta "$d/state/feat-cs.meta" "window=fm:fm-feat-cs" "worktree=$d/wt" "kind=ship"
416
+ printf 'needs-decision: review gate\n' > "$d/state/feat-cs.status"
417
+ FM_FAKE_AXI_STATUS="$(run_parked_scalar_gate_running fm/feat-cs)"
418
+ local out; out=$(run_crew_state "$d" feat-cs)
419
+ assert_contains "$out" "state: parked" "scalar gate wait -> parked"
420
+ assert_contains "$out" "source: run-step" "scalar gate wait -> run-step source"
421
+ assert_contains "$out" "parked at review" "scalar gate wait names the gate"
422
+ assert_contains "$out" "1 finding(s)" "scalar gate wait includes finding count"
423
+ assert_not_contains "$out" "superseded" "scalar gate wait not flagged stale"
424
+ pass "scalar gate parked run is not flagged superseded"
425
+ }
426
+
427
+ test_gate_block_parked_not_superseded() {
428
+ reset_fakes
429
+ local d; d=$(new_case parked-gate-block)
430
+ make_repo_on_branch "$d/wt" fm/feat-cb
431
+ make_fakebin "$d" >/dev/null
432
+ fm_write_meta "$d/state/feat-cb.meta" "window=fm:fm-feat-cb" "worktree=$d/wt" "kind=ship"
433
+ printf 'needs-decision: review gate\n' > "$d/state/feat-cb.status"
434
+ FM_FAKE_AXI_STATUS="$(run_parked_in_gate_block fm/feat-cb)"
435
+ local out; out=$(run_crew_state "$d" feat-cb)
436
+ assert_contains "$out" "state: parked" "gate block wait -> parked"
437
+ assert_contains "$out" "source: run-step" "gate block wait -> run-step source"
438
+ assert_contains "$out" "parked at review" "gate block wait names the gate"
439
+ assert_contains "$out" "1 finding(s)" "gate block wait includes finding count"
440
+ assert_not_contains "$out" "superseded" "gate block wait not flagged stale"
441
+ pass "gate block parked run is not flagged superseded"
442
+ }
443
+
444
+ test_ci_ready_done_log_beats_monitoring_run() {
445
+ reset_fakes
446
+ local d; d=$(new_case ci-ready)
447
+ make_repo_on_branch "$d/wt" fm/feat-ci
448
+ make_fakebin "$d" >/dev/null
449
+ fm_write_meta "$d/state/feat-ci.meta" "window=fm:fm-feat-ci" "worktree=$d/wt" "kind=ship"
450
+ printf 'done: PR https://github.com/o/r/pull/2 checks green\n' > "$d/state/feat-ci.status"
451
+ FM_FAKE_AXI_STATUS="$(run_ci_monitoring fm/feat-ci)"
452
+ local out; out=$(run_crew_state "$d" feat-ci)
453
+ assert_contains "$out" "state: done" "ci-ready status log -> done"
454
+ assert_contains "$out" "source: status-log" "ci-ready state comes from the status log"
455
+ assert_contains "$out" "checks green" "ci-ready detail preserves the report"
456
+ assert_not_contains "$out" "state: working" "ci-ready is not hidden by monitoring run"
457
+ pass "ci-ready status log beats monitoring run"
458
+ }
459
+
460
+ # Regression for the PR #252 incident: the crew's own status log never got a
461
+ # "done: ... checks green" line (log_reports_ci_ready above does not apply),
462
+ # but the ci step's log tail shows CI is actually green and only waiting on
463
+ # merge/close. fm-crew-state must surface this as done, not "validating
464
+ # (running)", so a green PR is never silently absorbed as still-in-progress.
465
+ test_ci_monitoring_checks_green_surfaces_done() {
466
+ reset_fakes
467
+ local d; d=$(new_case ci-green)
468
+ make_repo_on_branch "$d/wt" fm/feat-cigreen
469
+ make_fakebin "$d" >/dev/null
470
+ fm_write_meta "$d/state/feat-cigreen.meta" "window=fm:fm-feat-cigreen" "worktree=$d/wt" "kind=ship"
471
+ # No status-log line at all: the crew never reported its own checks-green line.
472
+ FM_FAKE_AXI_STATUS="$(run_ci_monitoring fm/feat-cigreen)"
473
+ FM_FAKE_CI_LOGS=$(cat <<'EOF'
474
+ CI checks running, waiting for results...
475
+ all CI checks passed - still monitoring until merged or closed
476
+ EOF
477
+ )
478
+ local out; out=$(run_crew_state "$d" feat-cigreen)
479
+ assert_contains "$out" "state: done" "green ci-monitor run -> done"
480
+ assert_contains "$out" "source: run-step" "green ci-monitor -> run-step source"
481
+ assert_contains "$out" "checks green" "green ci-monitor detail mentions checks green"
482
+ assert_not_contains "$out" "state: working" "green ci-monitor must not read as still validating"
483
+ pass "ci-monitoring run with checks already green surfaces done"
484
+ }
485
+
486
+ test_top_level_ci_checks_green_surfaces_done() {
487
+ reset_fakes
488
+ local d; d=$(new_case top-level-ci-green)
489
+ make_repo_on_branch "$d/wt" fm/feat-topcigreen
490
+ make_fakebin "$d" >/dev/null
491
+ fm_write_meta "$d/state/feat-topcigreen.meta" "window=fm:fm-feat-topcigreen" "worktree=$d/wt" "kind=ship"
492
+ FM_FAKE_AXI_STATUS="$(run_top_level_ci fm/feat-topcigreen)"
493
+ FM_FAKE_CI_LOGS="all CI checks passed - still monitoring until merged or closed"
494
+ local out; out=$(run_crew_state "$d" feat-topcigreen)
495
+ assert_contains "$out" "state: done" "top-level ci with green log -> done"
496
+ assert_contains "$out" "source: run-step" "top-level ci green -> run-step source"
497
+ assert_contains "$out" "checks green" "top-level ci green detail mentions checks green"
498
+ assert_not_contains "$out" "state: working" "top-level ci green must not stay working"
499
+ pass "top-level ci status uses ci log green marker"
500
+ }
501
+
502
+ test_ci_monitoring_no_checks_terminal_surfaces_done() {
503
+ reset_fakes
504
+ local d; d=$(new_case ci-nochecks)
505
+ make_repo_on_branch "$d/wt" fm/feat-cinochecks
506
+ make_fakebin "$d" >/dev/null
507
+ fm_write_meta "$d/state/feat-cinochecks.meta" "window=fm:fm-feat-cinochecks" "worktree=$d/wt" "kind=ship"
508
+ FM_FAKE_AXI_STATUS="$(run_ci_monitoring fm/feat-cinochecks)"
509
+ FM_FAKE_CI_LOGS="no CI checks reported - still monitoring until merged or closed"
510
+ local out; out=$(run_crew_state "$d" feat-cinochecks)
511
+ assert_contains "$out" "state: done" "terminal no-checks ci-monitor run -> done"
512
+ assert_contains "$out" "checks green" "terminal no-checks ci-monitor detail mentions checks green"
513
+ pass "terminal no-checks ci-monitor marker surfaces done"
514
+ }
515
+
516
+ test_ci_monitoring_green_then_rearm_stays_working() {
517
+ reset_fakes
518
+ local d; d=$(new_case ci-green-then-rearm)
519
+ make_repo_on_branch "$d/wt" fm/feat-cirearm
520
+ make_fakebin "$d" >/dev/null
521
+ fm_write_meta "$d/state/feat-cirearm.meta" "window=fm:fm-feat-cirearm" "worktree=$d/wt" "kind=ship"
522
+ FM_FAKE_AXI_STATUS="$(run_ci_monitoring fm/feat-cirearm)"
523
+ FM_FAKE_CI_LOGS=$(cat <<'EOF'
524
+ all CI checks passed - still monitoring until merged or closed
525
+ base branch advanced (aaaaaaa..bbbbbbb), re-arming CI monitor timeout
526
+ EOF
527
+ )
528
+ local out; out=$(run_crew_state "$d" feat-cirearm)
529
+ assert_contains "$out" "state: working" "base-advance rearm marker -> working"
530
+ assert_not_contains "$out" "state: done" "base-advance rearm marker must not read as done"
531
+ assert_not_contains "$out" "checks green" "base-advance rearm marker must not read as checks green"
532
+ pass "base-advance rearm after green stays working"
533
+ }
534
+
535
+ test_ci_monitoring_no_checks_yet_stays_working() {
536
+ reset_fakes
537
+ local d; d=$(new_case ci-nochecks-yet)
538
+ make_repo_on_branch "$d/wt" fm/feat-cinochecksyet
539
+ make_fakebin "$d" >/dev/null
540
+ fm_write_meta "$d/state/feat-cinochecksyet.meta" "window=fm:fm-feat-cinochecksyet" "worktree=$d/wt" "kind=ship"
541
+ FM_FAKE_AXI_STATUS="$(run_ci_monitoring fm/feat-cinochecksyet)"
542
+ FM_FAKE_CI_LOGS=$(cat <<'EOF'
543
+ no CI checks reported - still monitoring until merged or closed
544
+ base branch advanced (aaaaaaa..bbbbbbb), re-arming CI monitor timeout
545
+ no CI checks reported yet, waiting for checks to register...
546
+ EOF
547
+ )
548
+ local out; out=$(run_crew_state "$d" feat-cinochecksyet)
549
+ assert_contains "$out" "state: working" "pending no-checks marker -> working"
550
+ assert_not_contains "$out" "state: done" "pending no-checks marker must not read as done"
551
+ assert_not_contains "$out" "checks green" "pending no-checks marker must not read as checks green"
552
+ pass "pending no-checks ci-monitor marker stays working"
553
+ }
554
+
555
+ test_ci_monitoring_still_waiting_stays_working() {
556
+ reset_fakes
557
+ local d; d=$(new_case ci-waiting)
558
+ make_repo_on_branch "$d/wt" fm/feat-ciwait
559
+ make_fakebin "$d" >/dev/null
560
+ fm_write_meta "$d/state/feat-ciwait.meta" "window=fm:fm-feat-ciwait" "worktree=$d/wt" "kind=ship"
561
+ FM_FAKE_AXI_STATUS="$(run_ci_monitoring fm/feat-ciwait)"
562
+ FM_FAKE_CI_LOGS="CI checks running, waiting for results..."
563
+ local out; out=$(run_crew_state "$d" feat-ciwait)
564
+ assert_contains "$out" "state: working" "ci step still red -> working"
565
+ assert_not_contains "$out" "checks green" "no green marker present -> no checks-green detail"
566
+ pass "ci-monitoring run with checks not yet green stays working"
567
+ }
568
+
569
+ # A later merge-conflict auto-fix round after an earlier green reading must
570
+ # not be masked: the MOST RECENT marker in the log tail wins.
571
+ test_ci_monitoring_green_then_new_issue_stays_working() {
572
+ reset_fakes
573
+ local d; d=$(new_case ci-green-then-issue)
574
+ make_repo_on_branch "$d/wt" fm/feat-cirelapse
575
+ make_fakebin "$d" >/dev/null
576
+ fm_write_meta "$d/state/feat-cirelapse.meta" "window=fm:fm-feat-cirelapse" "worktree=$d/wt" "kind=ship"
577
+ FM_FAKE_AXI_STATUS="$(run_ci_monitoring fm/feat-cirelapse)"
578
+ FM_FAKE_CI_LOGS=$(cat <<'EOF'
579
+ all CI checks passed - still monitoring until merged or closed
580
+ base branch advanced (aaaaaaa..bbbbbbb), re-arming CI monitor timeout
581
+ issues detected: merge conflict - auto-fixing (attempt 2/10)...
582
+ EOF
583
+ )
584
+ local out; out=$(run_crew_state "$d" feat-cirelapse)
585
+ assert_contains "$out" "state: working" "a later relapse marker must win over an earlier green one"
586
+ assert_not_contains "$out" "state: done" "relapsed ci run must not read as done"
587
+ pass "a fresh issue after an earlier green reading is not masked"
588
+ }
589
+
590
+ test_ci_ready_done_log_relapse_stays_working() {
591
+ reset_fakes
592
+ local d; d=$(new_case ci-ready-then-relapse)
593
+ make_repo_on_branch "$d/wt" fm/feat-cireadyrelapse
594
+ make_fakebin "$d" >/dev/null
595
+ fm_write_meta "$d/state/feat-cireadyrelapse.meta" "window=fm:fm-feat-cireadyrelapse" "worktree=$d/wt" "kind=ship"
596
+ printf 'done: PR https://github.com/o/r/pull/2 checks green\n' > "$d/state/feat-cireadyrelapse.status"
597
+ FM_FAKE_AXI_STATUS="$(run_ci_monitoring fm/feat-cireadyrelapse)"
598
+ FM_FAKE_CI_LOGS=$(cat <<'EOF'
599
+ all CI checks passed - still monitoring until merged or closed
600
+ base branch advanced (aaaaaaa..bbbbbbb), re-arming CI monitor timeout
601
+ CI checks running, waiting for results...
602
+ EOF
603
+ )
604
+ local out; out=$(run_crew_state "$d" feat-cireadyrelapse)
605
+ assert_contains "$out" "state: working" "a stale ready status must not mask a later CI relapse"
606
+ assert_contains "$out" "source: run-step" "relapsed ci run remains run-step sourced"
607
+ assert_not_contains "$out" "state: done" "relapsed ci run with stale done log must not read as done"
608
+ pass "stale checks-green status log does not mask CI relapse"
609
+ }
610
+
611
+ test_ci_fixing_after_green_stays_working() {
612
+ reset_fakes
613
+ local d; d=$(new_case ci-fixing-after-green)
614
+ make_repo_on_branch "$d/wt" fm/feat-cifixing
615
+ make_fakebin "$d" >/dev/null
616
+ fm_write_meta "$d/state/feat-cifixing.meta" "window=fm:fm-feat-cifixing" "worktree=$d/wt" "kind=ship"
617
+ printf 'done: PR https://github.com/o/r/pull/2 checks green\n' > "$d/state/feat-cifixing.status"
618
+ FM_FAKE_AXI_STATUS="$(run_ci_fixing fm/feat-cifixing)"
619
+ FM_FAKE_CI_LOGS="all CI checks passed - still monitoring until merged or closed"
620
+ local out; out=$(run_crew_state "$d" feat-cifixing)
621
+ assert_contains "$out" "state: working" "ci fixing step must stay working"
622
+ assert_contains "$out" "source: run-step" "ci fixing remains run-step sourced"
623
+ assert_not_contains "$out" "state: done" "ci fixing must not read as checks-green done"
624
+ pass "ci fixing is not overridden by an earlier green marker"
625
+ }
626
+
627
+ test_top_level_fixing_ci_running_after_green_stays_working() {
628
+ reset_fakes
629
+ local d; d=$(new_case top-level-fixing-ci-running)
630
+ make_repo_on_branch "$d/wt" fm/feat-topfixingci
631
+ make_fakebin "$d" >/dev/null
632
+ fm_write_meta "$d/state/feat-topfixingci.meta" "window=fm:fm-feat-topfixingci" "worktree=$d/wt" "kind=ship"
633
+ FM_FAKE_AXI_STATUS="$(run_fixing_ci_running fm/feat-topfixingci)"
634
+ FM_FAKE_CI_LOGS="all CI checks passed - still monitoring until merged or closed"
635
+ local out; out=$(run_crew_state "$d" feat-topfixingci)
636
+ assert_contains "$out" "state: working" "top-level fixing with ci running must stay working"
637
+ assert_contains "$out" "source: run-step" "top-level fixing with ci running remains run-step sourced"
638
+ assert_contains "$out" "validating (fixing)" "top-level fixing keeps fixing detail"
639
+ assert_not_contains "$out" "state: done" "top-level fixing must not use stale green marker"
640
+ pass "top-level fixing is not overridden by a stale ci running row"
641
+ }
642
+
643
+ test_top_level_fixing_done_log_stays_working() {
644
+ reset_fakes
645
+ local d; d=$(new_case top-level-fixing-done-log)
646
+ make_repo_on_branch "$d/wt" fm/feat-topfixing
647
+ make_fakebin "$d" >/dev/null
648
+ fm_write_meta "$d/state/feat-topfixing.meta" "window=fm:fm-feat-topfixing" "worktree=$d/wt" "kind=ship"
649
+ printf 'done: PR https://github.com/o/r/pull/2 checks green\n' > "$d/state/feat-topfixing.status"
650
+ FM_FAKE_AXI_STATUS="$(run_fixing fm/feat-topfixing)"
651
+ FM_FAKE_CI_LOGS="all CI checks passed - still monitoring until merged or closed"
652
+ local out; out=$(run_crew_state "$d" feat-topfixing)
653
+ assert_contains "$out" "state: working" "top-level fixing must stay working"
654
+ assert_contains "$out" "source: run-step" "top-level fixing remains run-step sourced"
655
+ assert_contains "$out" "validating (fixing)" "top-level fixing keeps fixing detail"
656
+ assert_not_contains "$out" "state: done" "top-level fixing must not read as stale checks-green done"
657
+ pass "top-level fixing is not overridden by a stale done log"
658
+ }
659
+
660
+ # (d) terminal run-step is authoritative
661
+ test_terminal_passed() {
662
+ reset_fakes
663
+ local d; d=$(new_case passed)
664
+ make_repo_on_branch "$d/wt" fm/feat-d
665
+ make_fakebin "$d" >/dev/null
666
+ fm_write_meta "$d/state/feat-d.meta" "window=fm:fm-feat-d" "worktree=$d/wt" "kind=ship"
667
+ FM_FAKE_AXI_STATUS="$(run_passed fm/feat-d)"
668
+ local out; out=$(run_crew_state "$d" feat-d)
669
+ assert_contains "$out" "state: done" "passed run -> done"
670
+ assert_contains "$out" "source: run-step" "passed -> run-step source"
671
+ pass "terminal passed run is authoritative"
672
+ }
673
+
674
+ test_terminal_failed() {
675
+ reset_fakes
676
+ local d; d=$(new_case failed)
677
+ make_repo_on_branch "$d/wt" fm/feat-e
678
+ make_fakebin "$d" >/dev/null
679
+ fm_write_meta "$d/state/feat-e.meta" "window=fm:fm-feat-e" "worktree=$d/wt" "kind=ship"
680
+ FM_FAKE_AXI_STATUS="$(run_failed fm/feat-e)"
681
+ local out; out=$(run_crew_state "$d" feat-e)
682
+ assert_contains "$out" "state: failed" "failed run -> failed"
683
+ assert_contains "$out" "source: run-step" "failed -> run-step source"
684
+ pass "terminal failed run is authoritative"
685
+ }
686
+
687
+ # (e) cross-branch attribution: `axi status` returns ANOTHER branch's run (the
688
+ # routine case once more than one crew validates the same underlying repo
689
+ # concurrently - they share ONE no-mistakes repo registration), so the helper
690
+ # falls back to the real top-level `no-mistakes runs` listing to learn whether
691
+ # THIS branch has an active run of its own. Regression coverage for the
692
+ # 2026-07-02 herdr incident: the old fallback shelled out to `no-mistakes axi`
693
+ # (bare) expecting a `runs[N]{...}:` TOON table that the real CLI never emits
694
+ # (verified against the installed v1.32.2 - the `axi` surface has no
695
+ # runs-listing subcommand at all), so attribution silently failed every time
696
+ # the repo-wide answer was not this crew's own branch.
697
+ test_cross_branch_attribution_via_runs_list() {
698
+ reset_fakes
699
+ local d short; d=$(new_case crossbranch)
700
+ make_repo_on_branch "$d/wt" fm/feat-f
701
+ short=$(git -C "$d/wt" rev-parse --short=7 HEAD)
702
+ make_fakebin "$d" >/dev/null
703
+ fm_write_meta "$d/state/feat-f.meta" "window=fm:fm-feat-f" "worktree=$d/wt" "kind=ship"
704
+ # The repo-wide active/most-recent run belongs to a different crew's branch.
705
+ FM_FAKE_AXI_STATUS="$(run_running fm/other-crew)"
706
+ # Real `no-mistakes runs` shape: plain text, newest-first, no run id, no
707
+ # quoting - "<status> <branch> <short-sha> <date> [<pr-url>]".
708
+ FM_FAKE_RUNS_LIST="$(cat <<EOF
709
+ running fm/other-crew aaaaaaa 2026-07-02 22:10
710
+ running fm/feat-f ${short} 2026-07-02 22:05
711
+ EOF
712
+ )"
713
+ local out; out=$(run_crew_state "$d" feat-f)
714
+ assert_contains "$out" "state: working" "this branch's own run attributed via the runs list"
715
+ assert_contains "$out" "source: run-step" "runs-list-resolved run -> run-step source"
716
+ pass "cross-branch run is attributed via the real runs list"
717
+ }
718
+
719
+ # The runs list is newest-first; a branch with an OLDER completed run must not
720
+ # shadow its own newer active one - the first (topmost) matching row wins.
721
+ test_cross_branch_attribution_picks_most_recent_row() {
722
+ reset_fakes
723
+ local d short; d=$(new_case crossbranch-mostrecent)
724
+ make_repo_on_branch "$d/wt" fm/feat-fq
725
+ short=$(git -C "$d/wt" rev-parse --short=7 HEAD)
726
+ make_fakebin "$d" >/dev/null
727
+ fm_write_meta "$d/state/feat-fq.meta" "window=fm:fm-feat-fq" "worktree=$d/wt" "kind=ship"
728
+ FM_FAKE_AXI_STATUS="$(run_running fm/other-crew)"
729
+ FM_FAKE_RUNS_LIST="$(cat <<EOF
730
+ running fm/other-crew aaaaaaa 2026-07-02 22:10
731
+ running fm/feat-fq ${short} 2026-07-02 21:50
732
+ completed fm/feat-fq bbbbbbb 2026-07-02 20:00 https://github.com/o/r/pull/1
733
+ EOF
734
+ )"
735
+ local out; out=$(run_crew_state "$d" feat-fq)
736
+ assert_contains "$out" "state: working" "most recent (running) row wins over an older completed row"
737
+ assert_contains "$out" "source: run-step" "most-recent-row resolution -> run-step source"
738
+ pass "cross-branch attribution picks the branch's most recent row"
739
+ }
740
+
741
+ test_coarse_run_does_not_probe_other_branch_ci_log_for_ready_status() {
742
+ reset_fakes
743
+ local d short; d=$(new_case coarse-ready-other-log)
744
+ make_repo_on_branch "$d/wt" fm/feat-coarseready
745
+ short=$(git -C "$d/wt" rev-parse --short=7 HEAD)
746
+ make_fakebin "$d" >/dev/null
747
+ fm_write_meta "$d/state/feat-coarseready.meta" "window=fm:fm-feat-coarseready" "worktree=$d/wt" "kind=ship"
748
+ printf 'done: PR https://github.com/o/r/pull/4 checks green\n' > "$d/state/feat-coarseready.status"
749
+ FM_FAKE_AXI_STATUS="$(run_ci_monitoring fm/other-crew)"
750
+ FM_FAKE_RUNS_LIST="$(cat <<EOF
751
+ running fm/other-crew aaaaaaa 2026-07-02 22:10
752
+ running fm/feat-coarseready ${short} 2026-07-02 22:05
753
+ EOF
754
+ )"
755
+ FM_FAKE_CI_LOGS="CI checks running, waiting for results..."
756
+ local out; out=$(run_crew_state "$d" feat-coarseready)
757
+ assert_contains "$out" "state: done" "coarse ready status -> done"
758
+ assert_contains "$out" "source: status-log" "coarse ready status remains status-log sourced"
759
+ assert_not_contains "$out" "state: working" "coarse ready status must not be suppressed by another branch log"
760
+ pass "coarse run does not probe another branch's ci log"
761
+ }
762
+
763
+ # A different-branch run with NO matching runs-list row must NOT be
764
+ # misattributed, and must not be treated as a false "working" verdict either.
765
+ test_other_branch_run_ignored() {
766
+ reset_fakes
767
+ local d; d=$(new_case otherbranch)
768
+ make_repo_on_branch "$d/wt" fm/feat-g
769
+ make_fakebin "$d" >/dev/null
770
+ fm_write_meta "$d/state/feat-g.meta" "window=fm:fm-feat-g" "worktree=$d/wt" "kind=ship" "harness=claude"
771
+ printf 'done: implemented, ready to validate\n' > "$d/state/feat-g.status"
772
+ FM_FAKE_AXI_STATUS="$(run_running fm/some-other)"
773
+ FM_FAKE_RUNS_LIST="$(cat <<'EOF'
774
+ running fm/some-other aaaaaaa 2026-07-02 22:10
775
+ EOF
776
+ )"
777
+ FM_FAKE_BUSY=0
778
+ arm_idle_record "$d/state" feat-g
779
+ local out; out=$(run_crew_state "$d" feat-g)
780
+ assert_not_contains "$out" "source: run-step" "another branch's run not misattributed"
781
+ assert_contains "$out" "source: status-log" "no own run -> falls back to status-log"
782
+ assert_contains "$out" "state: done" "falls back to the log verb"
783
+ pass "another branch's run is ignored, falls back"
784
+ }
785
+
786
+ # (f) no run for this crew + a busy pane -> working via pane
787
+ test_no_run_busy_pane() {
788
+ reset_fakes
789
+ local d; d=$(new_case busy)
790
+ make_repo_on_branch "$d/wt" fm/feat-h
791
+ make_fakebin "$d" >/dev/null
792
+ fm_write_meta "$d/state/feat-h.meta" "window=fm:fm-feat-h" "worktree=$d/wt" "kind=ship" "harness=claude"
793
+ # No matching run anywhere. The busy verdict comes from the crew's own
794
+ # semantic lifecycle record (bin/fm-busy-lib.sh), not from rendered text.
795
+ FM_FAKE_AXI_STATUS=""
796
+ FM_FAKE_RUNS_LIST=""
797
+ FM_FAKE_BUSY=1
798
+ local gen; gen=$("$ROOT/bin/fm-busy-event.sh" arm "$d/state" feat-h)
799
+ "$ROOT/bin/fm-busy-event.sh" apply "$d/state" feat-h busy --gen "$gen" \
800
+ --source claude-hook --event user-prompt-submit
801
+ local out; out=$(run_crew_state "$d" feat-h)
802
+ assert_contains "$out" "state: working" "busy record -> working"
803
+ assert_contains "$out" "source: pane" "busy record -> pane source"
804
+ assert_contains "$out" "claude-hook" "the working verdict names its semantic source"
805
+ pass "no run + a busy semantic record reads working, attributed to its source"
806
+ }
807
+
808
+ # A converted adapter must NOT read working from rendered footer text: the
809
+ # redesign removed that dependency, so a pane painting "esc to interrupt" with
810
+ # no semantic record is unknown, never working and never silently idle.
811
+ test_no_run_footer_text_alone_is_not_working() {
812
+ reset_fakes
813
+ local d; d=$(new_case busy-footer-only)
814
+ make_repo_on_branch "$d/wt" fm/feat-h2
815
+ make_fakebin "$d" >/dev/null
816
+ fm_write_meta "$d/state/feat-h2.meta" "window=fm:fm-feat-h2" "worktree=$d/wt" "kind=ship" "harness=claude"
817
+ FM_FAKE_AXI_STATUS=""
818
+ FM_FAKE_RUNS_LIST=""
819
+ FM_FAKE_BUSY=1
820
+ printf 'done: stale completion event\n' > "$d/state/feat-h2.status"
821
+ local out; out=$(run_crew_state "$d" feat-h2)
822
+ assert_not_contains "$out" "state: working" "a footer alone must not read working for a converted adapter"
823
+ assert_contains "$out" "state: unknown" "no semantic record -> unknown"
824
+ assert_not_contains "$out" "source: status-log" "unknown semantic state must not fall through to a stale log"
825
+ pass "a converted adapter never reads working from rendered footer text"
826
+ }
827
+
828
+ # Grok keeps its isolated temporary rendered-tail fallback until its structured
829
+ # lifecycle is live-verified, so a grok crew still reads working from its own
830
+ # verified signature.
831
+ test_no_run_grok_uses_isolated_fallback() {
832
+ reset_fakes
833
+ local d; d=$(new_case busy-grok)
834
+ make_repo_on_branch "$d/wt" fm/feat-h3
835
+ make_fakebin "$d" >/dev/null
836
+ fm_write_meta "$d/state/feat-h3.meta" "window=fm:fm-feat-h3" "worktree=$d/wt" "kind=ship" "harness=grok"
837
+ FM_FAKE_AXI_STATUS=""
838
+ FM_FAKE_RUNS_LIST=""
839
+ FM_FAKE_BUSY=1
840
+ FM_FAKE_BUSY_TEXT='Ctrl+c:cancel'
841
+ export FM_FAKE_BUSY_TEXT
842
+ local out; out=$(run_crew_state "$d" feat-h3)
843
+ assert_contains "$out" "state: working" "grok busy tail -> working"
844
+ assert_contains "$out" "grok-regex" "the grok verdict names its isolated fallback source"
845
+ pass "grok still reads working through its isolated rendered-tail fallback"
846
+ }
847
+
848
+ test_no_run_herdr_unknown_uses_backend_capture() {
849
+ command -v jq >/dev/null 2>&1 || { pass "herdr pane fallback skipped without jq"; return; }
850
+ reset_fakes
851
+ local d; d=$(new_case herdr-busy)
852
+ make_repo_on_branch "$d/wt" fm/feat-herdr
853
+ make_fakebin "$d" >/dev/null
854
+ fm_write_meta "$d/state/feat-herdr.meta" "window=default:w1:p2" "worktree=$d/wt" "kind=ship" \
855
+ "backend=herdr" "harness=claude"
856
+ FM_FAKE_AXI_STATUS=""
857
+ FM_FAKE_RUNS_LIST=""
858
+ FM_FAKE_TMUX_MISSING=1
859
+ FM_FAKE_HERDR_BUSY=1
860
+ FM_FAKE_HERDR_AGENT_STATUS=working
861
+ local out; out=$(run_crew_state "$d" feat-herdr)
862
+ assert_contains "$out" "state: working" "herdr native busy -> working"
863
+ assert_contains "$out" "source: pane" "herdr native busy -> pane source"
864
+ assert_contains "$out" "herdr-native" "the herdr verdict names its native source"
865
+ pass "herdr's native busy verdict reads working with no record present"
866
+ }
867
+
868
+ # Regression (2026-07 herdr false-surface incident, now solved semantically):
869
+ # herdr's agent.get reports generation state ("working" only while the model is
870
+ # actively streaming - docs/herdr-backend.md "Busy state"), not "this crew's
871
+ # turn is still in progress". A crew blocked on its own long-running foreground
872
+ # `no-mistakes axi run` (no --yes; blocks until a gate or outcome) is not
873
+ # generating for that whole span, so agent.get reads idle. The crew's own
874
+ # semantic lifecycle record still says busy for the whole turn, and it outranks
875
+ # the narrower native verdict - so the crew is no longer misread as not-working.
876
+ test_no_run_herdr_idle_agent_status_outranked_by_record() {
877
+ command -v jq >/dev/null 2>&1 || { pass "herdr idle corroboration skipped without jq"; return; }
878
+ reset_fakes
879
+ local d; d=$(new_case herdr-idle-busy-record)
880
+ make_repo_on_branch "$d/wt" fm/feat-herdr-idle
881
+ make_fakebin "$d" >/dev/null
882
+ fm_write_meta "$d/state/feat-herdr-idle.meta" "window=default:w1:p3" "worktree=$d/wt" "kind=ship" \
883
+ "backend=herdr" "harness=claude"
884
+ # No run attributable (mirrors a no-mistakes run-step lookup that found no
885
+ # matching row within the configured runs-list window): the crew's semantic
886
+ # busy state is the only remaining signal.
887
+ FM_FAKE_AXI_STATUS=""
888
+ FM_FAKE_RUNS_LIST=""
889
+ FM_FAKE_TMUX_MISSING=1
890
+ FM_FAKE_HERDR_AGENT_STATUS=idle
891
+ FM_FAKE_HERDR_BUSY=0
892
+ local gen; gen=$("$ROOT/bin/fm-busy-event.sh" arm "$d/state" feat-herdr-idle)
893
+ "$ROOT/bin/fm-busy-event.sh" apply "$d/state" feat-herdr-idle busy --gen "$gen" \
894
+ --source claude-hook --event user-prompt-submit
895
+ local out; out=$(run_crew_state "$d" feat-herdr-idle)
896
+ assert_contains "$out" "state: working" "a busy record with herdr idle agent_status -> working"
897
+ assert_contains "$out" "claude-hook" "the record's source outranks herdr's narrower native verdict"
898
+ pass "a mid-tool-call crew stays working because its record outranks herdr's generation state"
899
+ }
900
+
901
+ # The record must not mask a genuinely idle or human-blocked agent: an idle
902
+ # record with idle agent_status still reads not-busy.
903
+ test_no_run_herdr_idle_agent_status_and_idle_record_stays_idle() {
904
+ command -v jq >/dev/null 2>&1 || { pass "herdr idle+idle-record skipped without jq"; return; }
905
+ reset_fakes
906
+ local d; d=$(new_case herdr-idle-idle-record)
907
+ make_repo_on_branch "$d/wt" fm/feat-herdr-stopped
908
+ make_fakebin "$d" >/dev/null
909
+ fm_write_meta "$d/state/feat-herdr-stopped.meta" "window=default:w1:p4" "worktree=$d/wt" "kind=ship" \
910
+ "backend=herdr" "harness=claude"
911
+ printf 'working: implementing\n' > "$d/state/feat-herdr-stopped.status"
912
+ FM_FAKE_AXI_STATUS=""
913
+ FM_FAKE_RUNS_LIST=""
914
+ FM_FAKE_TMUX_MISSING=1
915
+ FM_FAKE_HERDR_AGENT_STATUS=idle
916
+ FM_FAKE_HERDR_BUSY=0
917
+ local gen; gen=$("$ROOT/bin/fm-busy-event.sh" arm "$d/state" feat-herdr-stopped)
918
+ "$ROOT/bin/fm-busy-event.sh" apply "$d/state" feat-herdr-stopped idle --gen "$gen" \
919
+ --source claude-hook --event stop
920
+ local out; out=$(run_crew_state "$d" feat-herdr-stopped)
921
+ assert_not_contains "$out" "source: pane" "an idle record must not read as busy"
922
+ assert_contains "$out" "source: status-log" "an idle record falls to the status log"
923
+ pass "an idle record with idle agent_status stays not-busy (no regression for a human-blocked agent)"
924
+ }
925
+
926
+ # (g) no run + idle pane -> the status-log verb, as-is
927
+ test_no_run_idle_pane_uses_log() {
928
+ reset_fakes
929
+ local d; d=$(new_case idle)
930
+ make_repo_on_branch "$d/wt" fm/feat-i
931
+ make_fakebin "$d" >/dev/null
932
+ fm_write_meta "$d/state/feat-i.meta" "window=fm:fm-feat-i" "worktree=$d/wt" "kind=ship" "harness=claude"
933
+ printf 'needs-decision: which database?\n' > "$d/state/feat-i.status"
934
+ FM_FAKE_AXI_STATUS=""
935
+ FM_FAKE_BUSY=0
936
+ arm_idle_record "$d/state" feat-i
937
+ local out; out=$(run_crew_state "$d" feat-i)
938
+ assert_contains "$out" "state: parked" "needs-decision log -> parked"
939
+ assert_contains "$out" "source: status-log" "idle pane -> status-log source"
940
+ pass "no run + idle pane uses the status-log verb"
941
+ }
942
+
943
+ test_no_run_idle_pane_uses_keyed_log() {
944
+ reset_fakes
945
+ local d; d=$(new_case keyed-idle)
946
+ make_repo_on_branch "$d/wt" fm/feat-keyed
947
+ make_fakebin "$d" >/dev/null
948
+ fm_write_meta "$d/state/feat-keyed.meta" "window=fm:fm-feat-keyed" "worktree=$d/wt" "kind=ship" "harness=claude"
949
+ printf 'needs-decision [key=q1]: which database?\n' > "$d/state/feat-keyed.status"
950
+ FM_FAKE_AXI_STATUS=""
951
+ FM_FAKE_BUSY=0
952
+ arm_idle_record "$d/state" feat-keyed
953
+ local out; out=$(run_crew_state "$d" feat-keyed)
954
+ assert_contains "$out" "state: parked" "keyed needs-decision log -> parked"
955
+ assert_contains "$out" "which database?" "key token is excluded from status detail"
956
+ pass "no run + idle pane parses keyed status syntax"
957
+ }
958
+
959
+ # (g') no run + idle pane on a DECLARED external-wait pause -> state: paused, so a
960
+ # supervisor reading the crew sees a distinct pause (and its reason) rather than a
961
+ # wedge-suspect idle. This is the reader half the watcher/daemon build on.
962
+ test_no_run_idle_pane_paused() {
963
+ reset_fakes
964
+ local d; d=$(new_case paused)
965
+ make_repo_on_branch "$d/wt" fm/feat-pause
966
+ make_fakebin "$d" >/dev/null
967
+ fm_write_meta "$d/state/feat-pause.meta" "window=fm:fm-feat-pause" "worktree=$d/wt" "kind=ship" "harness=claude"
968
+ printf 'paused: holding for the upstream tool release\n' > "$d/state/feat-pause.status"
969
+ FM_FAKE_AXI_STATUS=""
970
+ FM_FAKE_BUSY=0
971
+ arm_idle_record "$d/state" feat-pause
972
+ local out; out=$(run_crew_state "$d" feat-pause)
973
+ assert_contains "$out" "state: paused" "paused log -> paused"
974
+ assert_contains "$out" "source: status-log" "idle pause -> status-log source"
975
+ assert_contains "$out" "holding for the upstream tool release" "the pause reason is carried in the detail"
976
+ pass "no run + idle pane on a paused: status reports state: paused with its reason"
977
+ }
978
+
979
+ test_no_run_idle_pane_custom_paused_verb() {
980
+ reset_fakes
981
+ local d; d=$(new_case custom-paused)
982
+ make_repo_on_branch "$d/wt" fm/feat-custom-pause
983
+ make_fakebin "$d" >/dev/null
984
+ fm_write_meta "$d/state/feat-custom-pause.meta" "window=fm:fm-feat-custom-pause" "worktree=$d/wt" "kind=ship" "harness=claude"
985
+ printf 'awaiting: vendor maintenance window\n' > "$d/state/feat-custom-pause.status"
986
+ FM_FAKE_AXI_STATUS=""
987
+ FM_FAKE_BUSY=0
988
+ arm_idle_record "$d/state" feat-custom-pause
989
+ local out; out=$(FM_CLASSIFY_PAUSED_VERB=awaiting run_crew_state "$d" feat-custom-pause)
990
+ assert_contains "$out" "state: paused" "custom paused verb -> paused"
991
+ assert_contains "$out" "source: status-log" "custom paused verb -> status-log source"
992
+ assert_contains "$out" "vendor maintenance window" "custom pause preserves its reason"
993
+ printf 'paused: default verb no longer selected\n' > "$d/state/feat-custom-pause.status"
994
+ out=$(FM_CLASSIFY_PAUSED_VERB=awaiting run_crew_state "$d" feat-custom-pause)
995
+ assert_contains "$out" "state: unknown" "custom paused verb replaces the default"
996
+ pass "no run + idle pane honors the configured paused verb"
997
+ }
998
+
999
+ # A trailing keyed resolved: event is a decision-CLOSING event, not a run-state
1000
+ # verb. It must never become the current state or leak its resolution prose as the
1001
+ # detail: a healthy idle secondmate that just closed a keyed decision falls through
1002
+ # to the idle default (unknown/none), not `unknown` with the resolution note as its
1003
+ # `doing`. Regression for the bearings render bug where such a secondmate showed
1004
+ # state=unknown with resolution prose. The one-owner keyed fold in fm-classify-lib.sh
1005
+ # is untouched; this only stops the deriver from reading a non-state event as state.
1006
+ test_no_run_idle_secondmate_resolved_event_not_state() {
1007
+ reset_fakes
1008
+ local d; d=$(new_case resolved-idle)
1009
+ mkdir -p "$d/wt"
1010
+ make_fakebin "$d" >/dev/null
1011
+ fm_write_meta "$d/state/mate.meta" "window=fm:fm-mate" "worktree=$d/wt" "kind=secondmate" "home=$d/wt"
1012
+ printf 'needs-decision [key=race]: pick subscribe order\n' > "$d/state/mate.status"
1013
+ printf 'resolved [key=race]: went with subscribe-before-write\n' >> "$d/state/mate.status"
1014
+ FM_FAKE_AXI_STATUS=""
1015
+ FM_FAKE_BUSY=0
1016
+ local out; out=$(run_crew_state "$d" mate)
1017
+ assert_contains "$out" "state: unknown" "resolved-then-idle secondmate is not a spurious run-state"
1018
+ assert_contains "$out" "source: none" "a resolved event is not treated as a status-log state source"
1019
+ assert_not_contains "$out" "subscribe-before-write" "resolution prose must not leak into the detail"
1020
+ # A bare (non-keyed) resolved: closes the default key and behaves the same.
1021
+ printf 'blocked: waiting on infra\nresolved: infra access granted\n' > "$d/state/mate.status"
1022
+ out=$(run_crew_state "$d" mate)
1023
+ assert_contains "$out" "source: none" "a bare resolved: is not a state source either"
1024
+ assert_not_contains "$out" "infra access granted" "bare resolution prose must not leak into the detail"
1025
+ # Control: a genuine trailing state verb still renders from the log.
1026
+ printf 'working: reconciling routed items\n' > "$d/state/mate.status"
1027
+ out=$(run_crew_state "$d" mate)
1028
+ assert_contains "$out" "state: working" "a real trailing state verb still renders"
1029
+ assert_contains "$out" "reconciling routed items" "a real state line still carries its detail"
1030
+ pass "a trailing resolved: event does not corrupt state render (idle stays idle)"
1031
+ }
1032
+
1033
+ test_dead_window_ignores_stale_status_log() {
1034
+ reset_fakes
1035
+ local d; d=$(new_case dead-window)
1036
+ make_repo_on_branch "$d/wt" fm/feat-dead
1037
+ make_fakebin "$d" >/dev/null
1038
+ fm_write_meta "$d/state/feat-dead.meta" "window=fm:fm-feat-dead" "worktree=$d/wt" "kind=ship"
1039
+ printf 'done: old completion event\n' > "$d/state/feat-dead.status"
1040
+ FM_FAKE_AXI_STATUS=""
1041
+ FM_FAKE_RUNS_LIST=""
1042
+ FM_FAKE_TMUX_MISSING=1
1043
+ local out; out=$(run_crew_state "$d" feat-dead)
1044
+ assert_contains "$out" "state: unknown" "dead window -> unknown"
1045
+ assert_contains "$out" "source: none" "dead window -> none source"
1046
+ assert_not_contains "$out" "source: status-log" "dead window does not reuse stale log"
1047
+ pass "dead window ignores stale status log"
1048
+ }
1049
+
1050
+ # A closed/unreadable pane must NOT mask an authoritative run-step: judge by the
1051
+ # run-step, not the shell. The common case is a finished crew whose agent has
1052
+ # exited and closed its window (the normal gap between completion and teardown) -
1053
+ # it must still report its terminal run-step state (e.g. done), never unknown.
1054
+ test_dead_window_still_reports_terminal_run_step() {
1055
+ reset_fakes
1056
+ local d; d=$(new_case dead-window-done)
1057
+ make_repo_on_branch "$d/wt" fm/feat-dead-done
1058
+ make_fakebin "$d" >/dev/null
1059
+ fm_write_meta "$d/state/feat-dead-done.meta" "window=fm:fm-feat-dead-done" "worktree=$d/wt" "kind=ship"
1060
+ printf 'done: PR https://github.com/o/r/pull/3 checks green\n' > "$d/state/feat-dead-done.status"
1061
+ FM_FAKE_AXI_STATUS="$(run_passed fm/feat-dead-done)"
1062
+ FM_FAKE_TMUX_MISSING=1 # the crew's window has closed
1063
+ local out; out=$(run_crew_state "$d" feat-dead-done)
1064
+ assert_contains "$out" "state: done" "closed pane still reports terminal run-step done"
1065
+ assert_contains "$out" "source: run-step" "closed pane does not mask the run-step"
1066
+ assert_not_contains "$out" "state: unknown" "closed pane with a run must never be unknown"
1067
+ pass "closed pane still reports a terminal run-step"
1068
+ }
1069
+
1070
+ # The same for an active run: an agent pane that crashed mid-validation while the
1071
+ # daemon-backed run continues must report the live run-step, not unknown.
1072
+ test_dead_window_still_reports_active_run_step() {
1073
+ reset_fakes
1074
+ local d; d=$(new_case dead-window-active)
1075
+ make_repo_on_branch "$d/wt" fm/feat-dead-act
1076
+ make_fakebin "$d" >/dev/null
1077
+ fm_write_meta "$d/state/feat-dead-act.meta" "window=fm:fm-feat-dead-act" "worktree=$d/wt" "kind=ship"
1078
+ FM_FAKE_AXI_STATUS="$(run_running fm/feat-dead-act)"
1079
+ FM_FAKE_TMUX_MISSING=1
1080
+ local out; out=$(run_crew_state "$d" feat-dead-act)
1081
+ assert_contains "$out" "state: working" "closed pane still reports active run-step"
1082
+ assert_contains "$out" "source: run-step" "closed pane does not mask the active run-step"
1083
+ assert_not_contains "$out" "state: unknown" "closed pane with an active run must never be unknown"
1084
+ pass "closed pane still reports an active run-step"
1085
+ }
1086
+
1087
+ test_no_timeout_uses_perl_bound() {
1088
+ reset_fakes
1089
+ local d toolbin out start elapsed calls_file calls
1090
+ d=$(new_case no-timeout)
1091
+ make_repo_on_branch "$d/wt" fm/feat-timeout
1092
+ make_fakebin "$d" >/dev/null
1093
+ calls_file="$d/no-mistakes.calls"
1094
+ : > "$calls_file"
1095
+ cat > "$d/fakebin/no-mistakes" <<'SH'
1096
+ #!/usr/bin/env bash
1097
+ printf '%s\n' "$*" >> "${FM_FAKE_NM_CALLS:-/dev/null}"
1098
+ while :; do :; done
1099
+ SH
1100
+ chmod +x "$d/fakebin/no-mistakes"
1101
+ toolbin=$(make_no_timeout_toolbin "$d")
1102
+ fm_write_meta "$d/state/feat-timeout.meta" "window=fm:fm-feat-timeout" "worktree=$d/wt" "kind=ship" \
1103
+ "harness=claude"
1104
+ FM_FAKE_BUSY=1
1105
+ local gen; gen=$("$ROOT/bin/fm-busy-event.sh" arm "$d/state" feat-timeout)
1106
+ "$ROOT/bin/fm-busy-event.sh" apply "$d/state" feat-timeout busy --gen "$gen" \
1107
+ --source claude-hook --event user-prompt-submit
1108
+ start=$SECONDS
1109
+ out=$(FM_FAKE_NM_CALLS="$calls_file" PATH="$d/fakebin:$toolbin" FM_STATE_OVERRIDE="$d/state" FM_CREW_STATE_NM_TIMEOUT=1 "$CREW_STATE" feat-timeout)
1110
+ elapsed=$((SECONDS - start))
1111
+ assert_contains "$out" "state: working" "timed-out no-mistakes falls back to pane"
1112
+ assert_contains "$out" "source: pane" "timed-out no-mistakes -> pane source"
1113
+ [ "$elapsed" -lt 5 ] || fail "perl timeout did not bound no-mistakes calls (elapsed ${elapsed}s)"
1114
+ calls=$(awk 'END { print NR + 0 }' "$calls_file" 2>/dev/null || echo 0)
1115
+ [ "$calls" -eq 1 ] || fail "empty no-mistakes status triggered extra lookups ($calls calls)"
1116
+ pass "no timeout command uses perl bound"
1117
+ }
1118
+
1119
+ # (i) kind=scout skips the run lookup entirely (its deliverable is a report).
1120
+ test_scout_skips_run_lookup() {
1121
+ reset_fakes
1122
+ local d; d=$(new_case scout)
1123
+ make_repo_on_branch "$d/wt" fm/scout-j
1124
+ make_fakebin "$d" >/dev/null
1125
+ fm_write_meta "$d/state/scout-j.meta" "window=fm:fm-scout-j" "worktree=$d/wt" "kind=scout" \
1126
+ "harness=claude"
1127
+ # Even if a run existed on this branch, a scout must not read it.
1128
+ FM_FAKE_AXI_STATUS="$(run_running fm/scout-j)"
1129
+ FM_FAKE_BUSY=1
1130
+ local gen; gen=$("$ROOT/bin/fm-busy-event.sh" arm "$d/state" scout-j)
1131
+ "$ROOT/bin/fm-busy-event.sh" apply "$d/state" scout-j busy --gen "$gen" \
1132
+ --source claude-hook --event user-prompt-submit
1133
+ local out; out=$(run_crew_state "$d" scout-j)
1134
+ assert_not_contains "$out" "source: run-step" "scout ignores no-mistakes run-step"
1135
+ assert_contains "$out" "source: pane" "scout reads its semantic busy state"
1136
+ pass "scout skips the run lookup"
1137
+ }
1138
+
1139
+ # (j) torn-down worktree and missing meta are graceful (unknown/none, exit 0)
1140
+ test_torn_down_worktree() {
1141
+ reset_fakes
1142
+ local d; d=$(new_case torndown)
1143
+ make_fakebin "$d" >/dev/null
1144
+ fm_write_meta "$d/state/gone-k.meta" "window=fm:fm-gone-k" "worktree=$d/no-such-worktree" "kind=ship"
1145
+ local out rc
1146
+ out=$(run_crew_state "$d" gone-k); rc=$?
1147
+ expect_code 0 "$rc" "torn-down worktree exits 0"
1148
+ assert_contains "$out" "state: unknown" "torn-down -> unknown"
1149
+ assert_contains "$out" "source: none" "torn-down -> none source"
1150
+ pass "torn-down worktree is handled gracefully"
1151
+ }
1152
+
1153
+ # --- remote secondmate arm ---------------------------------------------------
1154
+ # A meta recording remote_host= must never be read through the local worktree
1155
+ # probe or a local backend adapter: the recorded worktree and pane live on the
1156
+ # remote host, and the old local reads misreported a healthy remote mate as
1157
+ # "worktree gone". These cases drive the real helper over the real fm-on.sh
1158
+ # route with a stubbed ssh transport (FM_SSH_BIN seam): the stub prints
1159
+ # FM_FAKE_REMOTE_STATE_OUT as the remote endpoint's recovery-grade state and
1160
+ # exits FM_FAKE_SSH_RC.
1161
+
1162
+ setup_remote_case() { # <name> -> echoes case dir with remote meta + registry
1163
+ local d
1164
+ d=$(new_case "$1")
1165
+ mkdir -p "$d/data" "$d/fakebin"
1166
+ fm_write_meta "$d/state/rsm.meta" \
1167
+ "window=remote:rsm" \
1168
+ "endpoint_task_id=rsm" \
1169
+ "worktree=/remote/home/never-locally-present" \
1170
+ "harness=claude" \
1171
+ "kind=secondmate" \
1172
+ "mode=secondmate" \
1173
+ "remote_host=remote-mac" \
1174
+ "remote_root=/remote/root" \
1175
+ "remote_backend=herdr" \
1176
+ "remote_herdr_session=fm-remote" \
1177
+ "remote_target=fm-remote:w1:p1"
1178
+ cat > "$d/data/secondmates.md" <<EOF
1179
+ - rsm - remote test domain (host: remote-mac; root: /remote/root; home: /remote/home; scope: remote testing; projects: alpha; added 2026-08-02)
1180
+ EOF
1181
+ cat > "$d/fakebin/fake-ssh" <<'SH'
1182
+ #!/usr/bin/env bash
1183
+ cat > /dev/null
1184
+ [ -z "${FM_FAKE_REMOTE_STATE_OUT:-}" ] || printf '%s\n' "$FM_FAKE_REMOTE_STATE_OUT"
1185
+ exit "${FM_FAKE_SSH_RC:-0}"
1186
+ SH
1187
+ chmod +x "$d/fakebin/fake-ssh"
1188
+ printf '%s\n' "$d"
1189
+ }
1190
+
1191
+ run_remote_crew_state() { # <case-dir> <id>
1192
+ PATH="$1/fakebin:$PATH" FM_HOME="$1" FM_STATE_OVERRIDE="$1/state" \
1193
+ FM_SSH_BIN="$1/fakebin/fake-ssh" "$CREW_STATE" "$2"
1194
+ }
1195
+
1196
+ test_remote_alive_with_log_uses_status_log() {
1197
+ reset_fakes
1198
+ local d out rc
1199
+ d=$(setup_remote_case remote-alive-log)
1200
+ make_fakebin "$d" >/dev/null
1201
+ printf 'working: refactoring the quota adapter\n' > "$d/state/rsm.status"
1202
+ out=$(FM_FAKE_REMOTE_STATE_OUT=alive FM_FAKE_SSH_RC=0 run_remote_crew_state "$d" rsm); rc=$?
1203
+ expect_code 0 "$rc" "remote alive exits 0"
1204
+ assert_contains "$out" "state: working" "alive remote mate with a working log reads working"
1205
+ assert_contains "$out" "source: status-log" "alive remote mate reads current activity from the routed log"
1206
+ assert_contains "$out" "remote endpoint alive on remote-mac" "the remote liveness read should be visible"
1207
+ assert_not_contains "$out" "worktree gone" "a healthy remote mate must never read as torn down"
1208
+ pass "fm-crew-state remote: alive endpoint falls through to the routed status log"
1209
+ }
1210
+
1211
+ test_remote_alive_idle_is_healthy_not_gone() {
1212
+ reset_fakes
1213
+ local d out rc
1214
+ d=$(setup_remote_case remote-alive-idle)
1215
+ make_fakebin "$d" >/dev/null
1216
+ out=$(FM_FAKE_REMOTE_STATE_OUT=alive FM_FAKE_SSH_RC=0 run_remote_crew_state "$d" rsm); rc=$?
1217
+ expect_code 0 "$rc" "remote alive-idle exits 0"
1218
+ assert_contains "$out" "source: remote-endpoint" "the remote endpoint is the reported source"
1219
+ assert_contains "$out" "alive on remote-mac" "an idle remote mate reads alive"
1220
+ assert_not_contains "$out" "worktree gone" "a healthy remote mate must never read as torn down"
1221
+ assert_not_contains "$out" "backend target gone" "a healthy remote mate must never read as a dead target"
1222
+ pass "fm-crew-state remote: an idle alive endpoint reads alive, never gone or dead"
1223
+ }
1224
+
1225
+ test_remote_unreachable_is_unknown_remote_not_dead() {
1226
+ reset_fakes
1227
+ local d out rc
1228
+ d=$(setup_remote_case remote-unreachable)
1229
+ make_fakebin "$d" >/dev/null
1230
+ printf 'working: refactoring the quota adapter\n' > "$d/state/rsm.status"
1231
+ out=$(FM_FAKE_SSH_RC=255 run_remote_crew_state "$d" rsm); rc=$?
1232
+ expect_code 0 "$rc" "unreachable remote exits 0"
1233
+ assert_contains "$out" "unknown-remote" "an unreachable remote must be labeled unknown-remote"
1234
+ assert_contains "$out" "not proof of death" "an unreachable remote must not read as dead"
1235
+ assert_not_contains "$out" "worktree gone" "an unreachable remote must never read as torn down"
1236
+ assert_not_contains "$out" "backend target gone" "an unreachable remote must never read as a dead target"
1237
+ pass "fm-crew-state remote: an unreachable host reads unknown-remote, never gone or dead"
1238
+ }
1239
+
1240
+ test_remote_dead_reports_remote_verdict() {
1241
+ reset_fakes
1242
+ local d out rc
1243
+ d=$(setup_remote_case remote-dead)
1244
+ make_fakebin "$d" >/dev/null
1245
+ out=$(FM_FAKE_REMOTE_STATE_OUT=dead FM_FAKE_SSH_RC=0 run_remote_crew_state "$d" rsm); rc=$?
1246
+ expect_code 0 "$rc" "remote dead exits 0"
1247
+ assert_contains "$out" "remote endpoint dead on remote-mac" \
1248
+ "a genuinely dead remote endpoint reports the remote host's own verdict"
1249
+ pass "fm-crew-state remote: the remote host's own dead verdict is reported truthfully"
1250
+ }
1251
+
1252
+ test_missing_meta() {
1253
+ reset_fakes
1254
+ local d; d=$(new_case nometa)
1255
+ make_fakebin "$d" >/dev/null
1256
+ local out rc
1257
+ out=$(run_crew_state "$d" ghost-z); rc=$?
1258
+ expect_code 0 "$rc" "missing meta exits 0"
1259
+ assert_contains "$out" "state: unknown" "missing meta -> unknown"
1260
+ assert_contains "$out" "source: none" "missing meta -> none source"
1261
+ pass "missing meta is handled gracefully"
1262
+ }
1263
+
1264
+ # (k) crew_is_provably_working end-to-end over the REAL fm-crew-state.sh (not a
1265
+ # canned fake verdict, unlike tests/fm-watch-triage.test.sh's classifier
1266
+ # coverage). This is the direct regression pair for the 2026-07-02 herdr
1267
+ # incident: a validating crew whose bare `axi status` answer belongs to
1268
+ # another branch must still be absorbed by the watcher via the runs-list
1269
+ # fallback (working), while a crew with genuinely no run anywhere and an idle
1270
+ # pane must still surface (the safety property the fix must never widen away).
1271
+ test_provably_working_via_runs_list_fallback() {
1272
+ reset_fakes
1273
+ local d short; d=$(new_case provably-working-crossbranch)
1274
+ make_repo_on_branch "$d/wt" fm/feat-provable
1275
+ short=$(git -C "$d/wt" rev-parse --short=7 HEAD)
1276
+ make_fakebin "$d" >/dev/null
1277
+ fm_write_meta "$d/state/feat-provable.meta" "window=fm:fm-feat-provable" "worktree=$d/wt" "kind=ship"
1278
+ FM_FAKE_AXI_STATUS="$(run_running fm/other-crew)"
1279
+ FM_FAKE_RUNS_LIST="$(cat <<EOF
1280
+ running fm/other-crew aaaaaaa 2026-07-02 22:10
1281
+ running fm/feat-provable ${short} 2026-07-02 22:05
1282
+ EOF
1283
+ )"
1284
+ PATH="$d/fakebin:$PATH" FM_STATE_OVERRIDE="$d/state" crew_is_provably_working feat-provable \
1285
+ || fail "cross-branch attribution via the runs list was not treated as provably working"
1286
+ pass "crew_is_provably_working absorbs a validating crew found only via the runs-list fallback"
1287
+ }
1288
+
1289
+ test_not_provably_working_when_stopped() {
1290
+ reset_fakes
1291
+ local d; d=$(new_case provably-working-stopped)
1292
+ make_repo_on_branch "$d/wt" fm/feat-stopped
1293
+ make_fakebin "$d" >/dev/null
1294
+ fm_write_meta "$d/state/feat-stopped.meta" "window=fm:fm-feat-stopped" "worktree=$d/wt" "kind=ship"
1295
+ # Repo-wide run belongs to someone else, and this branch has no row in the
1296
+ # runs list either (it never validated, or genuinely finished/stopped) - the
1297
+ # only remaining signal is the pane, which is idle.
1298
+ FM_FAKE_AXI_STATUS="$(run_running fm/other-crew)"
1299
+ FM_FAKE_RUNS_LIST="$(cat <<'EOF'
1300
+ running fm/other-crew aaaaaaa 2026-07-02 22:10
1301
+ EOF
1302
+ )"
1303
+ FM_FAKE_BUSY=0
1304
+ PATH="$d/fakebin:$PATH" FM_STATE_OVERRIDE="$d/state" crew_is_provably_working feat-stopped \
1305
+ && fail "a stopped crew with no run anywhere and an idle pane was treated as provably working"
1306
+ pass "crew_is_provably_working still surfaces a genuinely stopped crew (safety property preserved)"
1307
+ }
1308
+
1309
+ # Usage error (no id) is the one non-zero exit.
1310
+ test_usage_error() {
1311
+ reset_fakes
1312
+ local rc
1313
+ "$CREW_STATE" >/dev/null 2>&1; rc=$?
1314
+ expect_code 2 "$rc" "no-arg usage error exits 2"
1315
+ pass "usage error exits 2"
1316
+ }
1317
+
1318
+ # Head-binding: same branch name with a rewritten/diverged worktree tip must not
1319
+ # attribute a historical no-mistakes run (multi-stage branch reuse incident).
1320
+ test_historical_same_branch_rewritten_head_not_current() {
1321
+ reset_fakes
1322
+ local d old_head new_head out
1323
+ d=$(new_case rewritten-head)
1324
+ make_repo_on_branch "$d/wt" fm/todo-flag
1325
+ old_head=$(git -C "$d/wt" rev-parse HEAD)
1326
+ # Simulate a rebase rewrite: orphan new history on the same branch name.
1327
+ git -C "$d/wt" checkout -q --orphan tmp-rewrite
1328
+ git -C "$d/wt" commit -q --allow-empty -m 'rewritten tip'
1329
+ git -C "$d/wt" branch -q -M fm/todo-flag
1330
+ new_head=$(git -C "$d/wt" rev-parse HEAD)
1331
+ [ "$old_head" != "$new_head" ] || fail "rewrite did not produce a new head"
1332
+ make_fakebin "$d" >/dev/null
1333
+ fm_write_meta "$d/state/wishlist.meta" "window=fm:fm-wishlist" "worktree=$d/wt" "kind=ship" "harness=claude"
1334
+ printf 'working: stage 2 setup complete rebased onto merged #76\n' > "$d/state/wishlist.status"
1335
+ # Historical run still reports the pre-rewrite head on the reused branch.
1336
+ FM_FAKE_RUN_HEAD="$old_head"
1337
+ FM_FAKE_AXI_STATUS="$(run_parked fm/todo-flag)"
1338
+ FM_FAKE_BUSY=0
1339
+ arm_idle_record "$d/state" wishlist
1340
+ out=$(run_crew_state "$d" wishlist)
1341
+ assert_not_contains "$out" "source: run-step" "historical rewritten head must not use run-step"
1342
+ assert_not_contains "$out" "parked at" "historical parked run must not mask current state"
1343
+ assert_contains "$out" "source: status-log" "falls back to status-log after head mismatch"
1344
+ assert_contains "$out" "state: working" "status-log working: remains current"
1345
+ pass "historical same-branch rewritten head is not attributed as current"
1346
+ }
1347
+
1348
+ # Head-binding: an active pipeline whose run head is a descendant of the local
1349
+ # tip (fix commits on the same history) remains current.
1350
+ test_active_run_descendant_fix_head_remains_current() {
1351
+ reset_fakes
1352
+ local d base_head fix_head out
1353
+ d=$(new_case pipeline-descendant)
1354
+ make_repo_on_branch "$d/wt" fm/feat-pipeline
1355
+ base_head=$(git -C "$d/wt" rev-parse HEAD)
1356
+ git -C "$d/wt" commit -q --allow-empty -m 'pipeline fix commit'
1357
+ fix_head=$(git -C "$d/wt" rev-parse HEAD)
1358
+ # Worktree still at the pre-fix tip; run reports the pipeline fix head.
1359
+ git -C "$d/wt" reset -q --hard "$base_head"
1360
+ make_fakebin "$d" >/dev/null
1361
+ fm_write_meta "$d/state/pipe.meta" "window=fm:fm-pipe" "worktree=$d/wt" "kind=ship"
1362
+ FM_FAKE_RUN_HEAD="$fix_head"
1363
+ FM_FAKE_AXI_STATUS="$(run_fixing fm/feat-pipeline)"
1364
+ out=$(run_crew_state "$d" pipe)
1365
+ assert_contains "$out" "source: run-step" "descendant pipeline fix head remains run-step"
1366
+ assert_contains "$out" "state: working" "active fixing run remains working"
1367
+ pass "active run with valid descendant fix head remains current"
1368
+ }
1369
+
1370
+ # Head-binding: local work that advanced past the run head invalidates the run.
1371
+ test_local_advanced_past_run_head_invalidates() {
1372
+ reset_fakes
1373
+ local d run_head out
1374
+ d=$(new_case local-advanced)
1375
+ make_repo_on_branch "$d/wt" fm/feat-adv
1376
+ run_head=$(git -C "$d/wt" rev-parse HEAD)
1377
+ git -C "$d/wt" commit -q --allow-empty -m 'local stage-2 work after prior run'
1378
+ make_fakebin "$d" >/dev/null
1379
+ fm_write_meta "$d/state/adv.meta" "window=fm:fm-adv" "worktree=$d/wt" "kind=ship" "harness=claude"
1380
+ printf 'working: stage 2 implementation in progress\n' > "$d/state/adv.status"
1381
+ FM_FAKE_RUN_HEAD="$run_head"
1382
+ FM_FAKE_AXI_STATUS="$(run_parked fm/feat-adv)"
1383
+ FM_FAKE_BUSY=0
1384
+ arm_idle_record "$d/state" adv
1385
+ out=$(run_crew_state "$d" adv)
1386
+ assert_not_contains "$out" "source: run-step" "local-advanced tip must not use historical run"
1387
+ assert_contains "$out" "source: status-log" "falls back after local advanced past run"
1388
+ assert_contains "$out" "state: working" "status-log working: is current"
1389
+ pass "local work advanced past run head invalidates attribution"
1390
+ }
1391
+
1392
+ # --- Run-attribution precedence for pipeline-owned lane heads ----------------
1393
+ # A live run whose pipeline OWNS the branch (branch_sync.state=pipeline_owned)
1394
+ # can report a lane head that is not a git object in the task worktree.
1395
+ # Every fixture head is deliberately unresolvable so only the top-level
1396
+ # branch_sync exemption - never an accidental nested-field match - attributes
1397
+ # the run.
1398
+ run_running_pipeline_owned() { # <branch> <head> [<sync-state>]
1399
+ cat <<EOF
1400
+ run:
1401
+ id: "01RUNLIVE"
1402
+ branch: $1
1403
+ status: running
1404
+ head: "$2"
1405
+ pr: ""
1406
+ findings: none
1407
+ steps[2]{step,status,findings,duration_ms}:
1408
+ intent,completed,0,0
1409
+ review,running,0,0
1410
+ branch_sync:
1411
+ state: ${3:-pipeline_owned}
1412
+ changed: false
1413
+ local:
1414
+ branch: $1
1415
+ head: "e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5"
1416
+ clean: true
1417
+ next_action:
1418
+ code: continue_active_run
1419
+ command: no-mistakes axi status
1420
+ EOF
1421
+ }
1422
+
1423
+ # T1 direction 1: the daemon-attributed ACTIVE pipeline-owned run binds without
1424
+ # head equality and wins over the older superseded failed row.
1425
+ test_pipeline_owned_active_run_beats_superseded_failed_row() {
1426
+ reset_fakes
1427
+ local d short; d=$(new_case f10-pipeline-owned)
1428
+ make_repo_on_branch "$d/wt" fm/feat-f10
1429
+ short=$(git -C "$d/wt" rev-parse --short=8 HEAD)
1430
+ make_fakebin "$d" >/dev/null
1431
+ fm_write_meta "$d/state/feat-f10.meta" "window=fm:fm-feat-f10" "worktree=$d/wt" "kind=ship"
1432
+ FM_FAKE_AXI_STATUS="$(run_running_pipeline_owned fm/feat-f10 f0f0f0f0)"
1433
+ FM_FAKE_RUNS_LIST="$(cat <<EOF
1434
+ running fm/feat-f10 f0f0f0f0 2026-08-27 13:53
1435
+ failed fm/feat-f10 ${short} 2026-08-27 12:09
1436
+ EOF
1437
+ )"
1438
+ local out; out=$(run_crew_state "$d" feat-f10)
1439
+ assert_contains "$out" "state: working" "pipeline-owned live run -> working"
1440
+ assert_contains "$out" "source: run-step" "pipeline-owned live run -> run-step source"
1441
+ assert_not_contains "$out" "state: failed" "superseded failed row must not surface over the live run"
1442
+ pass "pipeline-owned active run binds without head equality and beats the failed row"
1443
+ }
1444
+
1445
+ # T1 direction 2: a genuinely-failed run with NO later run on the branch still
1446
+ # surfaces as failed - hiding real failures is equally wrong.
1447
+ test_failed_run_with_no_later_run_still_surfaces() {
1448
+ reset_fakes
1449
+ local d short; d=$(new_case f10-genuine-failure)
1450
+ make_repo_on_branch "$d/wt" fm/feat-f10b
1451
+ short=$(git -C "$d/wt" rev-parse --short=8 HEAD)
1452
+ make_fakebin "$d" >/dev/null
1453
+ fm_write_meta "$d/state/feat-f10b.meta" "window=fm:fm-feat-f10b" "worktree=$d/wt" "kind=ship"
1454
+ FM_FAKE_AXI_STATUS="$(run_failed fm/feat-f10b)"
1455
+ FM_FAKE_RUNS_LIST=" failed fm/feat-f10b ${short} 2026-08-27 12:09"
1456
+ local out; out=$(run_crew_state "$d" feat-f10b)
1457
+ assert_contains "$out" "state: failed" "a genuinely failed run with no later run still reports failed"
1458
+ assert_contains "$out" "source: run-step" "the genuine failure is run-step sourced"
1459
+ pass "a genuinely failed run with no later run is not hidden"
1460
+ }
1461
+
1462
+ # The coarse runs-list scan: an ACTIVE row for this branch at an unresolvable
1463
+ # head is unknown attribution and must STOP the scan, never fall through onto
1464
+ # the older failed row (axi status answers another branch here, so attribution
1465
+ # can only go through the coarse list).
1466
+ test_coarse_unresolvable_active_row_never_falls_to_older_row() {
1467
+ reset_fakes
1468
+ local d short; d=$(new_case f10-coarse-guard)
1469
+ make_repo_on_branch "$d/wt" fm/feat-f10c
1470
+ short=$(git -C "$d/wt" rev-parse --short=8 HEAD)
1471
+ make_fakebin "$d" >/dev/null
1472
+ fm_write_meta "$d/state/feat-f10c.meta" "window=fm:fm-feat-f10c" "worktree=$d/wt" "kind=ship" "harness=claude"
1473
+ FM_FAKE_AXI_STATUS="$(run_running fm/other-crew)"
1474
+ FM_FAKE_RUNS_LIST="$(cat <<EOF
1475
+ running fm/other-crew aaaaaaa 2026-08-27 14:00
1476
+ running fm/feat-f10c f0f0f0f0 2026-08-27 13:53
1477
+ failed fm/feat-f10c ${short} 2026-08-27 12:09
1478
+ EOF
1479
+ )"
1480
+ FM_FAKE_BUSY=1
1481
+ local gen; gen=$("$ROOT/bin/fm-busy-event.sh" arm "$d/state" feat-f10c)
1482
+ "$ROOT/bin/fm-busy-event.sh" apply "$d/state" feat-f10c busy --gen "$gen" \
1483
+ --source claude-hook --event user-prompt-submit
1484
+ local out; out=$(run_crew_state "$d" feat-f10c)
1485
+ assert_not_contains "$out" "state: failed" "an unresolvable active row must not fall to the older failed row"
1486
+ assert_not_contains "$out" "source: run-step" "unknown attribution must not bind a run"
1487
+ assert_contains "$out" "state: working" "the busy crew still reads working through the pane fallback"
1488
+ assert_contains "$out" "source: pane" "unknown attribution falls to the pane, not an older row"
1489
+ pass "coarse scan stops on an unresolvable active row instead of binding an older one"
1490
+ }
1491
+
1492
+ # Negative control: the exemption is gated on pipeline_owned specifically - any
1493
+ # other branch_sync state keeps the strict head rule.
1494
+ test_non_pipeline_owned_unresolvable_head_not_attributed() {
1495
+ reset_fakes
1496
+ local d; d=$(new_case f10-not-owned)
1497
+ make_repo_on_branch "$d/wt" fm/feat-f10d
1498
+ make_fakebin "$d" >/dev/null
1499
+ fm_write_meta "$d/state/feat-f10d.meta" "window=fm:fm-feat-f10d" "worktree=$d/wt" "kind=ship" "harness=claude"
1500
+ printf 'working: implementing\n' > "$d/state/feat-f10d.status"
1501
+ FM_FAKE_AXI_STATUS="$(run_running_pipeline_owned fm/feat-f10d f0f0f0f0 synced)"
1502
+ FM_FAKE_RUNS_LIST=""
1503
+ FM_FAKE_BUSY=0
1504
+ arm_idle_record "$d/state" feat-f10d
1505
+ local out; out=$(run_crew_state "$d" feat-f10d)
1506
+ assert_not_contains "$out" "source: run-step" "a non-pipeline-owned unresolvable head must not bind"
1507
+ assert_contains "$out" "source: status-log" "falls back to the status log without the exemption"
1508
+ pass "the exemption requires branch_sync.state=pipeline_owned"
1509
+ }
1510
+
1511
+ # Negative control: the exemption also requires an ACTIVE run - a terminal run
1512
+ # released the branch, so an inconsistent pipeline_owned label must not bind a
1513
+ # terminal run by branch name alone.
1514
+ test_pipeline_owned_terminal_run_not_exempt() {
1515
+ reset_fakes
1516
+ local d; d=$(new_case f10-terminal-not-exempt)
1517
+ make_repo_on_branch "$d/wt" fm/feat-f10e
1518
+ make_fakebin "$d" >/dev/null
1519
+ fm_write_meta "$d/state/feat-f10e.meta" "window=fm:fm-feat-f10e" "worktree=$d/wt" "kind=ship" "harness=claude"
1520
+ printf 'working: stage 2 in progress\n' > "$d/state/feat-f10e.status"
1521
+ FM_FAKE_AXI_STATUS="$(run_running_pipeline_owned fm/feat-f10e f0f0f0f0)
1522
+ outcome: failed"
1523
+ FM_FAKE_RUNS_LIST=""
1524
+ FM_FAKE_BUSY=0
1525
+ arm_idle_record "$d/state" feat-f10e
1526
+ local out; out=$(run_crew_state "$d" feat-f10e)
1527
+ assert_not_contains "$out" "source: run-step" "a terminal run must not bind through the exemption"
1528
+ assert_contains "$out" "source: status-log" "falls back to the status log for a terminal unresolvable head"
1529
+ pass "the exemption never applies to a terminal run"
1530
+ }
1531
+
1532
+ test_missing_run_head_falls_back_to_current_state() {
1533
+ reset_fakes
1534
+ local d out
1535
+ d=$(new_case missing-run-head)
1536
+ make_repo_on_branch "$d/wt" fm/feat-no-head
1537
+ make_fakebin "$d" >/dev/null
1538
+ fm_write_meta "$d/state/no-head.meta" "window=fm:fm-no-head" "worktree=$d/wt" "kind=ship" "harness=claude"
1539
+ printf 'working: current stage still in progress\n' > "$d/state/no-head.status"
1540
+ FM_FAKE_AXI_STATUS=$(run_parked fm/feat-no-head | grep -v '^ head:')
1541
+ FM_FAKE_RUNS_LIST=""
1542
+ FM_FAKE_BUSY=0
1543
+ arm_idle_record "$d/state" no-head
1544
+ out=$(run_crew_state "$d" no-head)
1545
+ assert_not_contains "$out" "source: run-step" "missing run head must not permit branch-only attribution"
1546
+ assert_contains "$out" "source: status-log" "missing run head falls back to current state sources"
1547
+ assert_contains "$out" "state: working" "status-log remains current after missing run head"
1548
+ pass "missing run head falls back instead of matching by branch"
1549
+ }
1550
+
1551
+ test_active_run_is_authoritative
1552
+ test_stale_needs_decision_superseded
1553
+ test_stale_blocked_superseded
1554
+ test_genuine_parked_not_superseded
1555
+ test_scalar_gate_parked_not_superseded
1556
+ test_gate_block_parked_not_superseded
1557
+ test_ci_ready_done_log_beats_monitoring_run
1558
+ test_ci_monitoring_checks_green_surfaces_done
1559
+ test_top_level_ci_checks_green_surfaces_done
1560
+ test_ci_monitoring_no_checks_terminal_surfaces_done
1561
+ test_ci_monitoring_green_then_rearm_stays_working
1562
+ test_ci_monitoring_no_checks_yet_stays_working
1563
+ test_ci_monitoring_still_waiting_stays_working
1564
+ test_ci_monitoring_green_then_new_issue_stays_working
1565
+ test_ci_ready_done_log_relapse_stays_working
1566
+ test_ci_fixing_after_green_stays_working
1567
+ test_top_level_fixing_ci_running_after_green_stays_working
1568
+ test_top_level_fixing_done_log_stays_working
1569
+ test_terminal_passed
1570
+ test_terminal_failed
1571
+ test_cross_branch_attribution_via_runs_list
1572
+ test_cross_branch_attribution_picks_most_recent_row
1573
+ test_coarse_run_does_not_probe_other_branch_ci_log_for_ready_status
1574
+ test_other_branch_run_ignored
1575
+ test_no_run_busy_pane
1576
+ test_no_run_footer_text_alone_is_not_working
1577
+ test_no_run_grok_uses_isolated_fallback
1578
+ test_no_run_herdr_unknown_uses_backend_capture
1579
+ test_no_run_herdr_idle_agent_status_outranked_by_record
1580
+ test_no_run_herdr_idle_agent_status_and_idle_record_stays_idle
1581
+ test_no_run_idle_pane_uses_log
1582
+ test_no_run_idle_pane_uses_keyed_log
1583
+ test_no_run_idle_pane_paused
1584
+ test_no_run_idle_pane_custom_paused_verb
1585
+ test_no_run_idle_secondmate_resolved_event_not_state
1586
+ test_dead_window_ignores_stale_status_log
1587
+ test_dead_window_still_reports_terminal_run_step
1588
+ test_dead_window_still_reports_active_run_step
1589
+ test_no_timeout_uses_perl_bound
1590
+ test_scout_skips_run_lookup
1591
+ test_torn_down_worktree
1592
+ test_remote_alive_with_log_uses_status_log
1593
+ test_remote_alive_idle_is_healthy_not_gone
1594
+ test_remote_unreachable_is_unknown_remote_not_dead
1595
+ test_remote_dead_reports_remote_verdict
1596
+ test_missing_meta
1597
+ test_provably_working_via_runs_list_fallback
1598
+ test_not_provably_working_when_stopped
1599
+ test_usage_error
1600
+ test_historical_same_branch_rewritten_head_not_current
1601
+ test_active_run_descendant_fix_head_remains_current
1602
+ test_local_advanced_past_run_head_invalidates
1603
+ test_pipeline_owned_active_run_beats_superseded_failed_row
1604
+ test_failed_run_with_no_later_run_still_surfaces
1605
+ test_coarse_unresolvable_active_row_never_falls_to_older_row
1606
+ test_non_pipeline_owned_unresolvable_head_not_attributed
1607
+ test_pipeline_owned_terminal_run_not_exempt
1608
+ test_missing_run_head_falls_back_to_current_state
1609
+
1610
+ echo "all fm-crew-state tests passed"