@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,1990 @@
1
+ #!/usr/bin/env bash
2
+ # Behavior tests for the bearings projection wrapper over fm-fleet-snapshot.sh.
3
+ # Covers the output/token bound, TOON/JSON parity, the local-only default (zero
4
+ # GitHub/network calls), the --include-prs opt-in path, graceful degradation on a
5
+ # partial PR-fetch failure, end-to-end unresolved-decision durability, and current
6
+ # report pointers.
7
+ set -u
8
+
9
+ # shellcheck source=tests/lib.sh
10
+ # shellcheck disable=SC1091
11
+ . "$(dirname "${BASH_SOURCE[0]}")/lib.sh"
12
+
13
+ BEARINGS="$ROOT/bin/fm-bearings-snapshot.sh"
14
+ TMP_ROOT=$(fm_test_tmproot fm-bearings)
15
+ # Keep disposable homes outside the snapshot's fixture repo boundary even when
16
+ # TMPDIR is inside an isolated source worktree.
17
+ FM_ROOT_OVERRIDE="$TMP_ROOT/fixture-root"
18
+ mkdir -p "$FM_ROOT_OVERRIDE"
19
+ export FM_ROOT_OVERRIDE
20
+
21
+ command -v jq >/dev/null 2>&1 || { echo "skip: jq not found"; exit 0; }
22
+
23
+ # A fakebin that stubs the local tools the canonical snapshot may reach for, plus a
24
+ # gh/gh-axi that RECORDS every call to $NET_LOG so a test can prove the default path
25
+ # makes no network call. gh returns one fixture open PR keyed to the ship task.
26
+ make_fakebin() { # <dir>
27
+ local fb
28
+ fb=$(fm_fakebin "$1")
29
+ cat > "$fb/no-mistakes" <<'SH'
30
+ #!/usr/bin/env bash
31
+ [ "${FAKE_NM_SLEEP:-0}" = 1 ] && sleep 30
32
+ exit 0
33
+ SH
34
+ cat > "$fb/tmux" <<'SH'
35
+ #!/usr/bin/env bash
36
+ case "${1:-}" in
37
+ display-message) case "$*" in *dead-*) exit 1 ;; *) printf '%%1\n' ;; esac ;;
38
+ capture-pane)
39
+ case "$*" in
40
+ *fm-domain-alpha*) printf 'stale terminal summary: Phase 7 started\n> \n' ;;
41
+ *) printf 'all quiet\n> \n' ;;
42
+ esac
43
+ ;;
44
+ esac
45
+ exit 0
46
+ SH
47
+ cat > "$fb/gh" <<'SH'
48
+ #!/usr/bin/env bash
49
+ echo "gh $*" >> "$NET_LOG"
50
+ if [ "${FAKE_GH_FAIL:-0}" = 1 ]; then exit 1; fi
51
+ if [ "${FAKE_GH_SLEEP:-0}" = 1 ]; then sleep 30; fi
52
+ if [ "${FAKE_GH_MANY:-0}" = 1 ]; then
53
+ cat <<'JSON'
54
+ [{"number":1,"title":"One","url":"https://github.com/acme/repo/pull/1","headRefName":"fm/one","reviewDecision":"","mergeable":"MERGEABLE","statusCheckRollup":[]},{"number":2,"title":"Two","url":"https://github.com/acme/repo/pull/2","headRefName":"fm/two","reviewDecision":"","mergeable":"MERGEABLE","statusCheckRollup":[]},{"number":3,"title":"Three","url":"https://github.com/acme/repo/pull/3","headRefName":"fm/three","reviewDecision":"","mergeable":"MERGEABLE","statusCheckRollup":[]}]
55
+ JSON
56
+ exit 0
57
+ fi
58
+ cat <<'JSON'
59
+ [{"number":9,"title":"Ship the thing","url":"https://github.com/kunchenguid/firstmate/pull/9","headRefName":"fm/ship-task","reviewDecision":"APPROVED","mergeable":"MERGEABLE","statusCheckRollup":[{"conclusion":"SUCCESS","status":"COMPLETED"}]}]
60
+ JSON
61
+ SH
62
+ cat > "$fb/gh-axi" <<'SH'
63
+ #!/usr/bin/env bash
64
+ echo "gh-axi $*" >> "$NET_LOG"
65
+ [ "${FAKE_GH_FAIL:-0}" = 1 ] && exit 1
66
+ exit 0
67
+ SH
68
+ cat > "$fb/curl" <<'SH'
69
+ #!/usr/bin/env bash
70
+ echo "curl $*" >> "$NET_LOG"
71
+ exit 1
72
+ SH
73
+ chmod +x "$fb/no-mistakes" "$fb/tmux" "$fb/gh" "$fb/gh-axi" "$fb/curl"
74
+ printf '%s\n' "$fb"
75
+ }
76
+
77
+ make_home() { # <name>
78
+ local home=$TMP_ROOT/$1
79
+ mkdir -p "$home/state" "$home/data" "$home/projects" "$home/config"
80
+ printf '%s\n' "$home"
81
+ }
82
+
83
+ record_claude_state() { # <state-dir> <id> <busy|idle>
84
+ local state=$1 id=$2 semantic_state=$3 gen event
85
+ case "$semantic_state" in
86
+ busy) event=user-prompt-submit ;;
87
+ idle) event=stop ;;
88
+ *) fail "unsupported semantic fixture state: $semantic_state" ;;
89
+ esac
90
+ gen=$("$ROOT/bin/fm-busy-event.sh" arm "$state" "$id")
91
+ "$ROOT/bin/fm-busy-event.sh" apply "$state" "$id" "$semantic_state" --gen "$gen" \
92
+ --source claude-hook --event "$event"
93
+ }
94
+
95
+ fixture_mate_home() { # <parent-home>
96
+ printf '%s/%s-secondmate-home\n' "$TMP_ROOT" "$(basename "$1")"
97
+ }
98
+
99
+ # Standard fixture: a ship task with a recorded PR, a scout task with a report, a
100
+ # secondmate with a MASKED open decision (needs-decision then a later unrelated
101
+ # done), and a backlog with a superseded queued item.
102
+ write_fixture() { # <home>
103
+ local home=$1 mate
104
+ mate=$(fixture_mate_home "$home")
105
+ mkdir -p "$home/projects/ship-wt" "$home/data/scout-x" "$mate/data" "$mate/state" "$mate/config" "$mate/projects" "$mate/bin"
106
+ printf '# Firstmate fixture\n' > "$mate/AGENTS.md"
107
+ printf 'mate\n' > "$mate/.fm-secondmate-home"
108
+ printf -- '- mate - fixture domain (home: %s; scope: fixture work; projects: firstmate; added 2026-07-11)\n' \
109
+ "$mate" > "$home/data/secondmates.md"
110
+ cat > "$home/data/backlog.md" <<EOF
111
+ ## In flight
112
+ - [ ] ship-task - Ship the thing (repo: firstmate) (kind: ship) (since 2026-07-11)
113
+ - [ ] scout-x - Investigate the thing data/scout-x/report.md (repo: firstmate) (kind: scout) (since 2026-07-11)
114
+
115
+ ## Queued
116
+ - [ ] live-gate - Real queued work blocked-by: ship-task (repo: firstmate) (kind: ship)
117
+ - [ ] dead-gate - Old conditional work (repo: firstmate) (kind: scout)
118
+ NOT REQUIRED - superseded 2026-07-11; kept as reference only.
119
+
120
+ ## Done
121
+ - [x] done-a - Landed thing https://github.com/kunchenguid/firstmate/pull/7 (repo: firstmate) (kind: ship) (merged 2026-07-10)
122
+ EOF
123
+ printf '# Scout X\n' > "$home/data/scout-x/report.md"
124
+ fm_write_meta "$home/state/ship-task.meta" \
125
+ "window=firstmate:fm-ship-task" \
126
+ "worktree=$home/projects/ship-wt" \
127
+ "project=firstmate" \
128
+ "harness=claude" \
129
+ "kind=ship" \
130
+ "mode=no-mistakes" \
131
+ "pr=https://github.com/kunchenguid/firstmate/pull/9"
132
+ record_claude_state "$home/state" ship-task busy
133
+ printf 'working: building the thing\n' > "$home/state/ship-task.status"
134
+ fm_write_meta "$home/state/scout-x.meta" \
135
+ "window=firstmate:fm-scout-x" \
136
+ "worktree=$home/projects/ship-wt" \
137
+ "project=firstmate" \
138
+ "harness=claude" \
139
+ "kind=scout" \
140
+ "mode=scout"
141
+ record_claude_state "$home/state" scout-x idle
142
+ printf 'done: report ready\n' > "$home/state/scout-x.status"
143
+ fm_write_meta "$home/state/mate.meta" \
144
+ "window=firstmate:fm-mate" \
145
+ "worktree=$mate" \
146
+ "project=$mate" \
147
+ "harness=codex" \
148
+ "kind=secondmate" \
149
+ "mode=secondmate" \
150
+ "home=$mate" \
151
+ "projects=firstmate"
152
+ printf 'needs-decision [key=race]: pick subscribe order\n' > "$home/state/mate.status"
153
+ printf 'done: an unrelated subtask finished\n' >> "$home/state/mate.status"
154
+ fm_write_meta "$home/state/external-wait.meta" \
155
+ "window=firstmate:fm-external-wait" \
156
+ "worktree=$home/projects/ship-wt" \
157
+ "project=firstmate" \
158
+ "harness=claude" \
159
+ "kind=ship" \
160
+ "mode=no-mistakes"
161
+ record_claude_state "$home/state" external-wait idle
162
+ printf 'paused: declared external-wait for upstream release\n' > "$home/state/external-wait.status"
163
+ # The secondmate's OWN home backlog records a merge it managed. This lands in the
164
+ # secondmate home, never the main backlog, so landed-work views only see it via the
165
+ # bounded cross-home Done roll-up.
166
+ cat > "$mate/data/backlog.md" <<'EOF'
167
+ ## In flight
168
+ - [ ] mate - Decide subscription order (repo: firstmate) (kind: ship) (since 2026-07-11)
169
+
170
+ ## Queued
171
+ - [ ] mate-decision-race - Choose subscription order (repo: firstmate) (kind: captain) (hold: captain choice pending) (hold-kind: captain)
172
+
173
+ ## Done
174
+ - [x] mate-landed - Secondmate-managed fix https://github.com/kunchenguid/firstmate/pull/50 (repo: firstmate) (kind: ship) (merged 2026-07-11)
175
+ EOF
176
+ mkdir -p "$mate/projects/mate"
177
+ fm_write_meta "$mate/state/mate.meta" \
178
+ "window=firstmate:fm-mate" "worktree=$mate/projects/mate" "project=firstmate" \
179
+ "harness=claude" "kind=ship" "mode=no-mistakes"
180
+ record_claude_state "$mate/state" mate idle
181
+ printf 'needs-decision [key=race]: pick subscribe order\n' > "$mate/state/mate.status"
182
+ }
183
+
184
+ run() { # <home> <fakebin> <args...>
185
+ local home=$1 fakebin=$2; shift 2
186
+ PATH="$fakebin:$PATH" FM_HOME="$home" FM_BEARINGS_NOW=2026-07-11T18:00:00Z NET_LOG="$home/net.log" "$BEARINGS" "$@"
187
+ }
188
+
189
+ # End-to-end Domain Alpha regression fixture.
190
+ # The parent event claims Phase 7 started, while the registered home has no child
191
+ # metadata, every sample-rollout item is Done, and only an external legal hold remains.
192
+ write_domain_alpha_fixture() { # <parent-home> <secondmate-home>
193
+ local home=$1 mate=$2 i
194
+ mkdir -p "$mate/state" "$mate/data" "$mate/config" "$mate/projects" "$mate/bin"
195
+ printf '# Firstmate fixture\n' > "$mate/AGENTS.md"
196
+ printf 'domain-alpha\n' > "$mate/.fm-secondmate-home"
197
+ printf -- '- domain-alpha - sample rollout (home: %s; scope: sample rollout and legal release; projects: sample; added 2026-07-13)\n' \
198
+ "$mate" > "$home/data/secondmates.md"
199
+ fm_write_secondmate_meta "$home/state/domain-alpha.meta" "$mate" "firstmate:fm-domain-alpha" sample
200
+ printf 'working [key=phase7]: Phase 7 started\n' > "$home/state/domain-alpha.status"
201
+ cat > "$mate/data/backlog.md" <<'EOF'
202
+ ## In flight
203
+
204
+ ## Queued
205
+ - [ ] legal-release - Release approval blocked-by: external-legal - external legal dependency (repo: sample) (kind: ship)
206
+
207
+ ## Done
208
+ EOF
209
+ i=1
210
+ while [ "$i" -le 7 ]; do
211
+ printf -- '- [x] phase%s - Sample rollout Phase %s (repo: sample) (kind: ship) (done 2026-07-%02d)\n' \
212
+ "$i" "$i" "$i" >> "$mate/data/backlog.md"
213
+ i=$((i + 1))
214
+ done
215
+ }
216
+
217
+ # This is the Domain Alpha failure shape exactly: the structured home says Phase 7 is Done
218
+ # and no child is active, so the stale parent event must never become Underway.
219
+ test_domain_alpha_stale_parent_event_does_not_become_current_work() {
220
+ local home mate fakebin json canonical
221
+ home=$(make_home domain-alpha-parent)
222
+ mate="$TMP_ROOT/domain-alpha-home"
223
+ write_domain_alpha_fixture "$home" "$mate"
224
+ fakebin=$(make_fakebin "$home"); : > "$home/net.log"
225
+ json=$(FAKE_GH_FAIL=1 run "$home" "$fakebin" --json)
226
+ printf '%s' "$json" | jq -e '
227
+ (.in_flight | any(.[]; .id == "domain-alpha") | not)
228
+ and (.secondmates | any(.[];
229
+ .id == "domain-alpha"
230
+ and .state == "externally_held"
231
+ and .provenance == "structured-home"
232
+ and .freshness == "fresh"
233
+ and .contradiction == true))
234
+ and (.gates | any(.[]; .id == "legal-release" and .owner == "domain-alpha"))
235
+ and (.landed | any(.[]; .id == "phase7" and .owner == "domain-alpha"))
236
+ ' >/dev/null || fail "stale parent Phase 7 event overrode authoritative Domain Alpha state: $json"
237
+ canonical=$(PATH="$fakebin:$PATH" FM_HOME="$home" FM_SNAPSHOT_NOW=2026-07-11T18:00:00Z \
238
+ FM_SNAPSHOT_NOW_EPOCH=1783792800 FM_SNAPSHOT_TERMINAL_LINES=2 FM_SNAPSHOT_TERMINAL_BYTES=64 \
239
+ NET_LOG="$home/net.log" FAKE_GH_FAIL=1 "$ROOT/bin/fm-fleet-snapshot.sh" --json)
240
+ printf '%s' "$canonical" | jq -e '
241
+ .secondmate_current.records[] | select(.id == "domain-alpha")
242
+ | .provenance.selected == "structured-home"
243
+ and .freshness.status == "fresh"
244
+ and .terminal_evidence.provenance == "parent-direct-report-terminal"
245
+ and .terminal_evidence.trust == "untrusted-supplement"
246
+ and .terminal_evidence.captured == true
247
+ and .terminal_evidence.lines == 2
248
+ and .terminal_evidence.bytes <= 64
249
+ and (.terminal_evidence | has("content") | not)
250
+ and .terminal_evidence.event_note_seen == true
251
+ and .terminal_evidence.contradiction == true
252
+ and .contradiction == true
253
+ ' >/dev/null || fail "bounded terminal contradiction evidence was not labeled and subordinate: $canonical"
254
+ [ ! -s "$home/net.log" ] || fail "Domain Alpha structured-home read made a network call: $(cat "$home/net.log")"
255
+ pass "Domain Alpha structured state overrides a stale parent Phase 7 event"
256
+ }
257
+
258
+ test_gnu_stat_uses_file_formats_without_bsd_fallback_pollution() {
259
+ local home mate fakebin canonical stat_log
260
+ home=$(make_home gnu-stat-parent)
261
+ mate="$TMP_ROOT/gnu-stat-home"
262
+ write_domain_alpha_fixture "$home" "$mate"
263
+ fakebin=$(make_fakebin "$home")
264
+ stat_log="$home/stat.log"
265
+ cat > "$fakebin/uname" <<'SH'
266
+ #!/usr/bin/env bash
267
+ printf 'Linux\n'
268
+ SH
269
+ cat > "$fakebin/stat" <<'SH'
270
+ #!/usr/bin/env bash
271
+ printf '%s\n' "$*" >> "$STAT_LOG"
272
+ case "$1 $2" in
273
+ '-c %a') printf '600\n' ;;
274
+ '-c %Y') printf '1783792800\n' ;;
275
+ '-c %s') LC_ALL=C wc -c < "$3" | tr -d ' ' ;;
276
+ -f\ *)
277
+ printf ' File: "%s"\nBlocks: Total: 1\n' "$2"
278
+ exit 1
279
+ ;;
280
+ *) exit 2 ;;
281
+ esac
282
+ SH
283
+ chmod +x "$fakebin/uname" "$fakebin/stat"
284
+ canonical=$(PATH="$fakebin:$PATH" STAT_LOG="$stat_log" FM_HOME="$home" \
285
+ FM_SNAPSHOT_NOW=2026-07-11T18:00:00Z FM_SNAPSHOT_NOW_EPOCH=1783792800 \
286
+ "$ROOT/bin/fm-fleet-snapshot.sh" --json)
287
+ printf '%s' "$canonical" | jq -e '
288
+ .secondmate_current.records[] | select(.id == "domain-alpha")
289
+ | .provenance.selected == "structured-home"
290
+ and .parent_event.activity_scan.available == true
291
+ ' >/dev/null || fail "GNU stat fixture corrupted the authoritative secondmate summary: $canonical"
292
+ assert_contains "$(cat "$stat_log")" '-c %a' "GNU registry mode must use stat -c"
293
+ assert_contains "$(cat "$stat_log")" '-c %Y' "GNU parent-event mtime must use stat -c"
294
+ assert_contains "$(cat "$stat_log")" '-c %s' "GNU parent-event size must use stat -c"
295
+ if grep -q '^-f ' "$stat_log"; then
296
+ fail "GNU snapshot invoked BSD stat -f before its GNU file reads: $(cat "$stat_log")"
297
+ fi
298
+ pass "GNU stat file reads select -c without BSD filesystem-report pollution"
299
+ }
300
+
301
+ test_parent_activity_evidence_is_bounded_and_disclosed() {
302
+ local home mate fakebin canonical json i
303
+ home=$(make_home bounded-parent-activity)
304
+ mate="$TMP_ROOT/bounded-parent-activity-home"
305
+ write_domain_alpha_fixture "$home" "$mate"
306
+ : > "$home/state/domain-alpha.status"
307
+ i=1
308
+ while [ "$i" -le 6 ]; do
309
+ printf 'working [key=phase%s]: Phase %s started\n' "$i" "$i" >> "$home/state/domain-alpha.status"
310
+ i=$((i + 1))
311
+ done
312
+ fakebin=$(make_fakebin "$home")
313
+ canonical=$(PATH="$fakebin:$PATH" FM_HOME="$home" FM_SNAPSHOT_NOW=2026-07-11T18:00:00Z \
314
+ FM_SNAPSHOT_PARENT_ACTIVITY_LINES=4 FM_SNAPSHOT_PARENT_ACTIVITY_BYTES=4096 \
315
+ FM_SNAPSHOT_PARENT_ACTIVITIES=2 "$ROOT/bin/fm-fleet-snapshot.sh" --json)
316
+ printf '%s' "$canonical" | jq -e '
317
+ .secondmate_current.records[] | select(.id == "domain-alpha")
318
+ | .parent_event.activity_scan.available == true
319
+ and .parent_event.activity_scan.input_truncated == true
320
+ and .parent_event.activity_scan.retained_truncated == true
321
+ and .parent_event.activity_scan.lines_in_window == 4
322
+ and .parent_event.activity_scan.records_in_window == 4
323
+ and .parent_event.activity_scan.reasons == ["line_limit", "activity_limit"]
324
+ and (.parent_event.open_activities | map(.key)) == ["phase5", "phase6"]
325
+ ' >/dev/null || fail "parent activity evidence was not bounded and disclosed: $canonical"
326
+ json=$(FM_SNAPSHOT_PARENT_ACTIVITY_LINES=4 FM_SNAPSHOT_PARENT_ACTIVITY_BYTES=4096 \
327
+ FM_SNAPSHOT_PARENT_ACTIVITIES=2 run "$home" "$fakebin" --json)
328
+ printf '%s' "$json" | jq -e '
329
+ .omitted | any(.surface == "secondmate parent activity evidence truncated for 1 record(s)")
330
+ ' >/dev/null || fail "bearings did not disclose bounded parent activity evidence: $json"
331
+ pass "parent activity evidence is bounded and disclosed"
332
+ }
333
+
334
+ test_active_child_overrides_old_parent_event() {
335
+ local home mate fakebin json canonical
336
+ home=$(make_home active-child-parent)
337
+ mate="$TMP_ROOT/active-child-home"
338
+ write_domain_alpha_fixture "$home" "$mate"
339
+ mkdir -p "$mate/projects/phase8"
340
+ cat > "$mate/data/backlog.md" <<'EOF'
341
+ ## In flight
342
+ - [ ] phase8 - Sample rollout Phase 8 (repo: sample) (kind: ship) (since 2026-07-13)
343
+
344
+ ## Queued
345
+
346
+ ## Done
347
+ - [x] phase7 - Sample rollout Phase 7 (repo: sample) (kind: ship) (done 2026-07-12)
348
+ EOF
349
+ fm_write_meta "$mate/state/phase8.meta" \
350
+ "window=firstmate:fm-phase8" "worktree=$mate/projects/phase8" "project=sample" \
351
+ "harness=codex" "kind=ship" "mode=no-mistakes"
352
+ printf 'working [key=phase8]: implementing Phase 8 parity\nneeds-decision [key=release]: choose release A or B\n' \
353
+ > "$mate/state/phase8.status"
354
+ fakebin=$(make_fakebin "$home")
355
+ json=$(run "$home" "$fakebin" --json)
356
+ printf '%s' "$json" | jq -e '
357
+ (.secondmates | any(.[]; .id == "domain-alpha" and .state != "captain_decision"
358
+ and (.doing | contains("release A or B") | not)))
359
+ and (.decisions_open | any(.owner == "domain-alpha") | not)
360
+ ' >/dev/null || fail "status-only child decision leaked into Bearings: $json"
361
+ canonical=$(PATH="$fakebin:$PATH" FM_HOME="$home" FM_SNAPSHOT_NOW=2026-07-11T18:00:00Z \
362
+ "$ROOT/bin/fm-fleet-snapshot.sh" --json)
363
+ printf '%s' "$canonical" | jq -e '
364
+ .secondmate_current.records[] | select(.id == "domain-alpha") | .endpoints[] | select(.id == "phase8")
365
+ | .endpoint.status == "unknown"
366
+ and .endpoint.exists == true
367
+ and .endpoint.freshness == "fresh"
368
+ and .endpoint.observed_at == "2026-07-11T18:00:00Z"
369
+ ' >/dev/null || fail "child endpoint observation lacked bounded current freshness: $canonical"
370
+ pass "Bearings excludes a status-only child decision"
371
+ }
372
+
373
+ test_structured_child_decision_reaches_captains_call() {
374
+ local home mate fakebin json
375
+ home=$(make_home child-decision-parent)
376
+ mate="$TMP_ROOT/child-decision-home"
377
+ write_domain_alpha_fixture "$home" "$mate"
378
+ mkdir -p "$mate/projects/phase8"
379
+ cat > "$mate/data/backlog.md" <<'EOF'
380
+ ## In flight
381
+ - [ ] phase8 - Sample rollout Phase 8 (repo: sample) (kind: ship) (since 2026-07-13)
382
+
383
+ ## Queued
384
+ - [ ] phase8-decision-release - Choose sample release (repo: sample) (kind: captain) (hold: captain release choice pending) (hold-kind: captain)
385
+
386
+ ## Done
387
+ - [x] phase7 - Sample rollout Phase 7 (repo: sample) (kind: ship) (done 2026-07-12)
388
+ EOF
389
+ fm_write_meta "$mate/state/phase8.meta" \
390
+ "window=firstmate:fm-phase8" "worktree=$mate/projects/phase8" "project=sample" \
391
+ "harness=claude" "kind=ship" "mode=no-mistakes"
392
+ record_claude_state "$mate/state" phase8 idle
393
+ printf 'needs-decision [key=release]: choose release A or B\n' > "$mate/state/phase8.status"
394
+ fakebin=$(make_fakebin "$home")
395
+ json=$(run "$home" "$fakebin" --json)
396
+ printf '%s' "$json" | jq -e '
397
+ (.secondmates | any(.[]; .id == "domain-alpha" and .state == "captain_decision"))
398
+ and (.decisions_open | any(.[]; .id == "domain-alpha/phase8-decision-release"
399
+ and .key == "phase8-decision-release" and .verb == "captain-hold"))
400
+ and (.in_flight | any(.[]; .id == "domain-alpha") | not)
401
+ ' >/dev/null || fail "structured child captain hold did not reach Captain Call: $json"
402
+ pass "a structured child captain hold reaches Captain's Call"
403
+ }
404
+
405
+ make_valid_secondmate_home() { # <id> <home>
406
+ local id=$1 home=$2
407
+ mkdir -p "$home/state" "$home/data" "$home/config" "$home/projects" "$home/bin"
408
+ printf '# Firstmate fixture\n' > "$home/AGENTS.md"
409
+ printf '%s\n' "$id" > "$home/.fm-secondmate-home"
410
+ cat > "$home/data/backlog.md" <<'EOF'
411
+ ## In flight
412
+
413
+ ## Queued
414
+
415
+ ## Done
416
+ EOF
417
+ }
418
+
419
+ append_secondmate_registry() { # <parent> <id> <home>
420
+ printf -- '- %s - fixture domain (home: %s; scope: fixture; projects: sample; added 2026-07-13)\n' \
421
+ "$2" "$3" >> "$1/data/secondmates.md"
422
+ }
423
+
424
+ append_landed_row() { # <secondmate-home> <id> <title> <date>
425
+ printf -- '- [x] %s - %s (repo: firstmate) (kind: ship) (merged %s)\n' \
426
+ "$2" "$3" "$4" >> "$1/data/backlog.md"
427
+ }
428
+
429
+ make_landed_secondmate() { # <parent> <id>
430
+ local parent=$1 id=$2 mate
431
+ mate="$TMP_ROOT/$(basename "$parent")-$id-home"
432
+ make_valid_secondmate_home "$id" "$mate"
433
+ append_secondmate_registry "$parent" "$id" "$mate"
434
+ printf '%s\n' "$mate"
435
+ }
436
+
437
+ write_parent_secondmate_event() { # <parent> <id> <home> <note>
438
+ fm_write_secondmate_meta "$1/state/$2.meta" "$3" "firstmate:fm-$2" sample
439
+ printf 'working [key=%s]: %s\n' "$2" "$4" > "$1/state/$2.status"
440
+ }
441
+
442
+ test_bad_secondmate_homes_never_revive_parent_work() {
443
+ local home fakebin missing invalid unreadable malformed timedout wt json
444
+ home=$(make_home bad-homes)
445
+ : > "$home/data/secondmates.md"
446
+ missing="$TMP_ROOT/missing-home"
447
+ invalid="$TMP_ROOT/invalid-home"
448
+ unreadable="$TMP_ROOT/unreadable-home"
449
+ malformed="$TMP_ROOT/malformed-home"
450
+ timedout="$TMP_ROOT/timedout-home"
451
+
452
+ append_secondmate_registry "$home" missing "$missing"
453
+
454
+ make_valid_secondmate_home invalid "$invalid"
455
+ printf 'someone-else\n' > "$invalid/.fm-secondmate-home"
456
+ append_secondmate_registry "$home" invalid "$invalid"
457
+ write_parent_secondmate_event "$home" invalid "$invalid" "old invalid work"
458
+
459
+ make_valid_secondmate_home unreadable "$unreadable"
460
+ chmod 000 "$unreadable/data"
461
+ append_secondmate_registry "$home" unreadable "$unreadable"
462
+ write_parent_secondmate_event "$home" unreadable "$unreadable" "old unreadable work"
463
+
464
+ make_valid_secondmate_home malformed "$malformed"
465
+ printf '## In flight\nthis current row is not structured\n' > "$malformed/data/backlog.md"
466
+ append_secondmate_registry "$home" malformed "$malformed"
467
+ write_parent_secondmate_event "$home" malformed "$malformed" "old malformed work"
468
+
469
+ make_valid_secondmate_home timedout "$timedout"
470
+ wt="$timedout/projects/slow"
471
+ fm_git_init_commit "$wt"
472
+ git -C "$wt" checkout -q -b fm/slow
473
+ printf '## In flight\n- [ ] slow - Slow child (repo: sample) (kind: ship) (since 2026-07-13)\n\n## Queued\n\n## Done\n' > "$timedout/data/backlog.md"
474
+ fm_write_meta "$timedout/state/slow.meta" \
475
+ "window=firstmate:fm-slow" "worktree=$wt" "project=sample" \
476
+ "harness=codex" "kind=ship" "mode=no-mistakes"
477
+ append_secondmate_registry "$home" timedout "$timedout"
478
+ write_parent_secondmate_event "$home" timedout "$timedout" "old timed work"
479
+
480
+ fakebin=$(make_fakebin "$home")
481
+ json=$(FAKE_NM_SLEEP=1 FM_SNAPSHOT_SECONDMATE_TIMEOUT=1 run "$home" "$fakebin" --json)
482
+ chmod 700 "$unreadable/data"
483
+ printf '%s' "$json" | jq -e '
484
+ (.secondmates | length) == 5
485
+ and all(.secondmates[]; .state == "unknown")
486
+ and (.in_flight | map(.id) | all(. != "invalid" and . != "unreadable" and . != "malformed" and . != "timedout"))
487
+ and (.secondmates | any(.[]; .id == "missing" and .provenance == "unknown"
488
+ and .freshness == "unknown" and (.reason | contains("invalid home"))))
489
+ and ([.secondmates[] | select(.id != "missing")]
490
+ | all(.provenance == "parent-event-fallback" and .freshness == "historical-event"))
491
+ and (.secondmates | any(.[]; .id == "invalid" and (.reason | contains("marked for"))))
492
+ and (.secondmates | any(.[]; .id == "unreadable" and (.reason | test("invalid home|unreadable"))))
493
+ and (.secondmates | any(.[]; .id == "malformed" and (.reason | contains("unstructured current backlog row"))))
494
+ and (.secondmates | any(.[]; .id == "timedout" and (.reason | contains("timed out"))))
495
+ and ([.secondmate_reconcile[].id] == ["malformed"])
496
+ and (.secondmate_reconcile[0].kind == "unstructured_current")
497
+ ' >/dev/null || fail "bad home outcomes revived stale work or lacked provenance: $json"
498
+ pass "missing, invalid, unreadable, malformed, and timed-out homes stay explicit unknowns"
499
+ }
500
+
501
+ test_oversized_secondmate_summary_stays_strict_unknown() {
502
+ local home mate fakebin json i
503
+ home=$(make_home oversized-home)
504
+ mate="$TMP_ROOT/oversized-secondmate-home"
505
+ make_valid_secondmate_home oversized "$mate"
506
+ append_secondmate_registry "$home" oversized "$mate"
507
+ fm_write_secondmate_meta "$home/state/oversized.meta" "$mate" "firstmate:fm-oversized" sample
508
+ printf 'working [key=old]: stale parent activity\n' > "$home/state/oversized.status"
509
+ cat > "$mate/data/backlog.md" <<'EOF'
510
+ ## In flight
511
+
512
+ ## Queued
513
+
514
+ ## Done
515
+ EOF
516
+ i=1
517
+ while [ "$i" -le 30 ]; do
518
+ printf -- '- [x] landed-%02d - Bounded landed fixture %02d (repo: sample) (kind: ship) (done 2026-07-01)\n' \
519
+ "$i" "$i" >> "$mate/data/backlog.md"
520
+ i=$((i + 1))
521
+ done
522
+ fakebin=$(make_fakebin "$home")
523
+ json=$(FM_SNAPSHOT_SECONDMATE_MAX_BYTES=512 run "$home" "$fakebin" --json)
524
+ printf '%s' "$json" | jq -e '
525
+ (.secondmates | any(.id == "oversized" and .state == "unknown"
526
+ and .provenance == "parent-event-fallback"
527
+ and (.reason | contains("exceeded byte limit"))))
528
+ and (.in_flight | any(.id == "oversized") | not)
529
+ and (.decisions_open | any(.owner == "oversized") | not)
530
+ and (.landed | any(.owner == "oversized") | not)
531
+ ' >/dev/null || fail "oversized summary revived or retained unvalidated surfaces: $json"
532
+ pass "an oversized secondmate summary retains the strict empty unknown fallback"
533
+ }
534
+
535
+ test_secondmate_and_child_bounds_are_disclosed() {
536
+ local home fakebin id mate child json expanded canonical i
537
+ home=$(make_home secondmate-bounds)
538
+ : > "$home/data/secondmates.md"
539
+ for id in a b c; do
540
+ mate="$TMP_ROOT/bounds-$id"
541
+ make_valid_secondmate_home "$id" "$mate"
542
+ append_secondmate_registry "$home" "$id" "$mate"
543
+ done
544
+ mate="$TMP_ROOT/bounds-a"
545
+ : > "$mate/data/backlog.md"
546
+ printf '## In flight\n' >> "$mate/data/backlog.md"
547
+ i=1
548
+ while [ "$i" -le 3 ]; do
549
+ child="child-$i"
550
+ mkdir -p "$mate/projects/$child"
551
+ printf -- '- [ ] %s - Active %s (repo: sample) (kind: ship) (since 2026-07-13)\n' "$child" "$child" >> "$mate/data/backlog.md"
552
+ fm_write_meta "$mate/state/$child.meta" \
553
+ "window=firstmate:fm-$child" "worktree=$mate/projects/$child" "project=sample" \
554
+ "harness=claude" "kind=ship" "mode=no-mistakes"
555
+ record_claude_state "$mate/state" "$child" busy
556
+ printf 'working [key=%s]: active child %s\n' "$child" "$i" > "$mate/state/$child.status"
557
+ i=$((i + 1))
558
+ done
559
+ printf '\n## Queued\n\n## Done\n' >> "$mate/data/backlog.md"
560
+ fakebin=$(make_fakebin "$home")
561
+ canonical=$(PATH="$fakebin:$PATH" FM_HOME="$home" FM_SNAPSHOT_NOW=2026-07-11T18:00:00Z \
562
+ FM_SNAPSHOT_SECONDMATES=2 FM_SNAPSHOT_SECONDMATE_CHILDREN=2 "$ROOT/bin/fm-fleet-snapshot.sh" --json)
563
+ printf '%s' "$canonical" | jq -e '
564
+ .secondmate_current.total_registered == 3
565
+ and .secondmate_current.shown == 2
566
+ and .secondmate_current.truncated == 1
567
+ and (.secondmate_current.records[] | select(.id == "a")
568
+ | .counts.active_children == 3 and (.active_children | length) == 2
569
+ and (.omitted | any(.surface == "active_children" and .count == 1)))
570
+ and (.secondmate_current.records | any(.id == "b" and .current.state == "no_active_work"))
571
+ ' >/dev/null || fail "canonical secondmate or child bounds were not enforced: $canonical"
572
+ json=$(FM_SNAPSHOT_SECONDMATES=2 FM_SNAPSHOT_SECONDMATE_CHILDREN=2 FM_BEARINGS_SECONDMATES=1 \
573
+ run "$home" "$fakebin" --json)
574
+ printf '%s' "$json" | jq -e '
575
+ (.secondmates | length) == 1
576
+ and ([.omitted[].surface] | any(test("secondmates showing 1 of 2")))
577
+ and ([.omitted[].surface] | any(test("registered secondmates omitted by snapshot bound: 1")))
578
+ ' >/dev/null || fail "bearings secondmate bound was not disclosed: $json"
579
+ expanded=$(FM_SNAPSHOT_SECONDMATE_CHILDREN=2 FM_BEARINGS_SECONDMATES=1 \
580
+ run "$home" "$fakebin" --json --all-secondmates)
581
+ printf '%s' "$expanded" | jq -e '
582
+ (.secondmates | length) == 3
583
+ and ([.omitted[].surface] | any(test("secondmates showing|registered secondmates omitted")) | not)
584
+ ' >/dev/null || fail "--all-secondmates did not expand the canonical and bearings bounds: $expanded"
585
+ pass "secondmate and per-home child counts are bounded, disclosed, and explicitly expandable"
586
+ }
587
+
588
+ test_parent_decision_is_untrusted_contradiction_only() {
589
+ local home mate fakebin canonical json
590
+ home=$(make_home parent-decision-only)
591
+ mate="$TMP_ROOT/parent-decision-only-home"
592
+ make_valid_secondmate_home authority "$mate"
593
+ append_secondmate_registry "$home" authority "$mate"
594
+ fm_write_secondmate_meta "$home/state/authority.meta" "$mate" "firstmate:fm-authority" sample
595
+ printf 'needs-decision [key=stale]: old parent question\n' > "$home/state/authority.status"
596
+ fakebin=$(make_fakebin "$home")
597
+ canonical=$(PATH="$fakebin:$PATH" FM_HOME="$home" FM_SNAPSHOT_NOW=2026-07-11T18:00:00Z \
598
+ "$ROOT/bin/fm-fleet-snapshot.sh" --json)
599
+ printf '%s' "$canonical" | jq -e '
600
+ .secondmate_current.records[] | select(.id == "authority")
601
+ | .current.state == "no_active_work"
602
+ and .decisions_open == []
603
+ and .contradiction == true
604
+ and (.parent_event.open_decisions | any(.key == "stale" and .verb == "needs-decision"))
605
+ ' >/dev/null || fail "parent decision crossed structured-home authority boundary: $canonical"
606
+ json=$(run "$home" "$fakebin" --json)
607
+ printf '%s' "$json" | jq -e '
608
+ (.secondmates | any(.[]; .id == "authority" and .state == "no_active_work" and .contradiction == true))
609
+ and (.decisions_open | any(.[]; .id == "authority") | not)
610
+ ' >/dev/null || fail "bearings promoted a stale parent decision: $json"
611
+ pass "parent decisions remain untrusted contradiction evidence"
612
+ }
613
+
614
+ test_parent_evidence_reconciles_by_verb_and_key() {
615
+ local home hold blocked decision fakebin canonical mate child
616
+ home=$(make_home keyed-parent-evidence)
617
+ hold="$TMP_ROOT/keyed-parent-hold-home"
618
+ blocked="$TMP_ROOT/keyed-parent-blocked-home"
619
+ decision="$TMP_ROOT/keyed-parent-decision-home"
620
+ make_valid_secondmate_home hold "$hold"
621
+ make_valid_secondmate_home blocked "$blocked"
622
+ make_valid_secondmate_home decision "$decision"
623
+ append_secondmate_registry "$home" hold "$hold"
624
+ append_secondmate_registry "$home" blocked "$blocked"
625
+ append_secondmate_registry "$home" decision "$decision"
626
+ fm_write_secondmate_meta "$home/state/hold.meta" "$hold" "firstmate:fm-hold" sample
627
+ fm_write_secondmate_meta "$home/state/blocked.meta" "$blocked" "firstmate:fm-blocked" sample
628
+ fm_write_secondmate_meta "$home/state/decision.meta" "$decision" "firstmate:fm-decision" sample
629
+ printf 'working [key=stale-work]: old work still running\n' > "$home/state/hold.status"
630
+ printf 'paused [key=legal-release]: waiting for legal release\n' >> "$home/state/hold.status"
631
+ printf 'paused: legacy pause without an identity\n' >> "$home/state/hold.status"
632
+ printf 'blocked [key=vendor-release]: waiting for vendor release\n' > "$home/state/blocked.status"
633
+ printf 'blocked: legacy block without an identity\n' >> "$home/state/blocked.status"
634
+ printf 'needs-decision [key=stale-route]: choose the old route\n' > "$home/state/decision.status"
635
+ printf 'working: legacy work without an identity\n' >> "$home/state/decision.status"
636
+ cat > "$hold/data/backlog.md" <<'EOF'
637
+ ## In flight
638
+
639
+ ## Queued
640
+ - [ ] legal-release - Legal release blocked-by: external-legal - legal review (repo: sample) (kind: ship)
641
+
642
+ ## Done
643
+ EOF
644
+ cat > "$blocked/data/backlog.md" <<'EOF'
645
+ ## In flight
646
+
647
+ ## Queued
648
+ - [ ] vendor-release - Vendor release blocked-by: external-vendor - vendor review (repo: sample) (kind: ship)
649
+
650
+ ## Done
651
+ EOF
652
+ child='decision-child'
653
+ mkdir -p "$decision/projects/$child"
654
+ cat > "$decision/data/backlog.md" <<EOF
655
+ ## In flight
656
+ - [ ] $child - Decision child (repo: sample) (kind: ship) (since 2026-07-11)
657
+
658
+ ## Queued
659
+
660
+ ## Done
661
+ EOF
662
+ fm_write_meta "$decision/state/$child.meta" \
663
+ "window=firstmate:fm-$child" "worktree=$decision/projects/$child" "project=sample" \
664
+ "harness=claude" "kind=ship" "mode=no-mistakes"
665
+ record_claude_state "$decision/state" "$child" idle
666
+ printf 'needs-decision [key=live-route]: choose the current route\n' > "$decision/state/$child.status"
667
+ fakebin=$(make_fakebin "$home")
668
+ canonical=$(PATH="$fakebin:$PATH" FM_HOME="$home" FM_SNAPSHOT_NOW=2026-07-11T18:00:00Z \
669
+ "$ROOT/bin/fm-fleet-snapshot.sh" --json)
670
+ printf '%s' "$canonical" | jq -e '
671
+ (.secondmate_current.records[] | select(.id == "hold")
672
+ | .current.state == "externally_held"
673
+ and .contradiction == true
674
+ and .terminal_evidence.captured == false
675
+ and (.parent_event.reconciliation.activities
676
+ | any(.verb == "paused" and .key == "legal-release" and .verdict == "corroborates"))
677
+ and (.parent_event.reconciliation.activities
678
+ | any(.verb == "paused" and .key == "default" and .verdict == "inconclusive" and .matched == null))
679
+ and (.parent_event.reconciliation.activities
680
+ | any(.verb == "working" and .key == "stale-work" and .verdict == "contradicts")))
681
+ and (.secondmate_current.records[] | select(.id == "blocked")
682
+ | .current.state == "externally_held"
683
+ and .contradiction == false
684
+ and (.parent_event.reconciliation.decisions
685
+ | any(.verb == "blocked" and .key == "vendor-release" and .verdict == "corroborates"))
686
+ and (.parent_event.reconciliation.decisions
687
+ | any(.verb == "blocked" and .key == "default" and .verdict == "inconclusive" and .matched == null)))
688
+ and (.secondmate_current.records[] | select(.id == "decision")
689
+ | .current.state == "captain_decision"
690
+ and .contradiction == true
691
+ and .terminal_evidence.captured == false
692
+ and (.parent_event.reconciliation.activities
693
+ | any(.verb == "working" and .key == "default" and .verdict == "inconclusive" and .matched == null))
694
+ and (.parent_event.reconciliation.decisions
695
+ | any(.verb == "needs-decision" and .key == "stale-route" and .verdict == "contradicts")))
696
+ ' >/dev/null || fail "parent evidence was not reconciled by verb and key: $canonical"
697
+ pass "parent evidence reconciliation distinguishes matching holds, blocks, and decisions"
698
+ }
699
+
700
+ test_nonprogressing_child_states_are_explicit() {
701
+ local home mate fakebin canonical
702
+ home=$(make_home child-state-classification)
703
+ mate="$TMP_ROOT/child-state-classification-home"
704
+ make_valid_secondmate_home states "$mate"
705
+ append_secondmate_registry "$home" states "$mate"
706
+ mkdir -p "$mate/projects/parked" "$mate/projects/done" "$mate/projects/failed"
707
+ cat > "$mate/data/backlog.md" <<'EOF'
708
+ ## In flight
709
+ - [ ] parked - Parked child (repo: sample) (kind: ship) (since 2026-07-11)
710
+
711
+ ## Queued
712
+
713
+ ## Done
714
+ EOF
715
+ fm_write_meta "$mate/state/parked.meta" \
716
+ "window=firstmate:fm-parked" "worktree=$mate/projects/parked" "project=sample" \
717
+ "harness=claude" "kind=ship" "mode=no-mistakes"
718
+ record_claude_state "$mate/state" parked idle
719
+ printf 'needs-decision [key=parked]: choose a route\n' > "$mate/state/parked.status"
720
+ fakebin=$(make_fakebin "$home")
721
+ canonical=$(PATH="$fakebin:$PATH" FM_HOME="$home" FM_SNAPSHOT_NOW=2026-07-11T18:00:00Z \
722
+ "$ROOT/bin/fm-fleet-snapshot.sh" --json)
723
+ printf '%s' "$canonical" | jq -e '
724
+ .secondmate_current.records[] | select(.id == "states")
725
+ | .current.state == "captain_decision"
726
+ and .active_children == []
727
+ and (.holds | any(.id == "parked" and .source == "child-state"))
728
+ ' >/dev/null || fail "parked child was classified as active work: $canonical"
729
+ cat > "$mate/data/backlog.md" <<'EOF'
730
+ ## In flight
731
+
732
+ ## Queued
733
+
734
+ ## Done
735
+ EOF
736
+ canonical=$(PATH="$fakebin:$PATH" FM_HOME="$home" FM_SNAPSHOT_NOW=2026-07-11T18:00:00Z \
737
+ "$ROOT/bin/fm-fleet-snapshot.sh" --json)
738
+ printf '%s' "$canonical" | jq -e '
739
+ .secondmate_current.records[] | select(.id == "states")
740
+ | .current.state == "captain_decision"
741
+ and (.current.reason | contains("live child state has no in-flight backlog item"))
742
+ and (.current.reason | contains("parked=parked"))
743
+ and .provenance.selected == "structured-home"
744
+ and .provenance.trust == "partial-structured"
745
+ and .invalidity == {kind:"unowned_current",ids:["parked"]}
746
+ and [.decisions_open[].key] == ["parked"]
747
+ ' >/dev/null || fail "unowned held child lost its classification or decisions: $canonical"
748
+ cat > "$mate/data/backlog.md" <<'EOF'
749
+ ## In flight
750
+ - [ ] done - Done child still in flight (repo: sample) (kind: ship) (since 2026-07-11)
751
+ - [ ] failed - Failed child still in flight (repo: sample) (kind: ship) (since 2026-07-11)
752
+
753
+ ## Queued
754
+
755
+ ## Done
756
+ EOF
757
+ fm_write_meta "$mate/state/done.meta" \
758
+ "window=firstmate:fm-done" "worktree=$mate/projects/done" "project=sample" \
759
+ "harness=claude" "kind=ship" "mode=no-mistakes"
760
+ fm_write_meta "$mate/state/failed.meta" \
761
+ "window=firstmate:fm-failed" "worktree=$mate/projects/failed" "project=sample" \
762
+ "harness=claude" "kind=ship" "mode=no-mistakes"
763
+ record_claude_state "$mate/state" "done" idle
764
+ record_claude_state "$mate/state" failed idle
765
+ printf 'done: complete\n' > "$mate/state/done.status"
766
+ printf 'failed: stopped\n' > "$mate/state/failed.status"
767
+ rm "$mate/state/parked.meta" "$mate/state/parked.status"
768
+ canonical=$(PATH="$fakebin:$PATH" FM_HOME="$home" FM_SNAPSHOT_NOW=2026-07-11T18:00:00Z \
769
+ "$ROOT/bin/fm-fleet-snapshot.sh" --json)
770
+ printf '%s' "$canonical" | jq -e '
771
+ .secondmate_current.records[] | select(.id == "states")
772
+ | .current.state == "no_active_work"
773
+ and (.current.reason | contains("terminal child state"))
774
+ and (.current.reason | contains("done=done"))
775
+ and (.current.reason | contains("failed=failed"))
776
+ and .provenance.selected == "structured-home"
777
+ and .provenance.trust == "partial-structured"
778
+ and .invalidity == {kind:"terminal_in_flight",ids:["done","failed"]}
779
+ ' >/dev/null || fail "terminal in-flight rows discarded the readable home: $canonical"
780
+ pass "nonprogressing child states are explicit and inconsistent terminal rows invalidate"
781
+ }
782
+
783
+ test_registry_unavailability_and_bounds_are_explicit() {
784
+ local home fakebin json canonical id mate boundary
785
+ home=$(make_home registry-unavailable)
786
+ mate="$TMP_ROOT/registry-hidden"
787
+ make_valid_secondmate_home hidden "$mate"
788
+ printf -- '- hidden - fixture (home: %s; scope: fixture; projects: sample; added 2026-07-11)\n' "$mate" > "$home/data/secondmates.md"
789
+ fm_write_secondmate_meta "$home/state/hidden.meta" "$mate" "firstmate:fm-hidden" sample
790
+ chmod 000 "$home/data/secondmates.md"
791
+ fakebin=$(make_fakebin "$home")
792
+ canonical=$(PATH="$fakebin:$PATH" FM_HOME="$home" FM_SNAPSHOT_NOW=2026-07-11T18:00:00Z \
793
+ "$ROOT/bin/fm-fleet-snapshot.sh" --json)
794
+ json=$(run "$home" "$fakebin" --json)
795
+ chmod 600 "$home/data/secondmates.md"
796
+ printf '%s' "$canonical" | jq -e '
797
+ .secondmate_current.registry.complete == false
798
+ and (.secondmate_current.records[] | select(.id == "hidden")
799
+ | .registered == null
800
+ and (.current.reason | contains("registration is unknown")))
801
+ ' >/dev/null || fail "unavailable registry produced false unregistered provenance: $canonical"
802
+ printf '%s' "$json" | jq -e '
803
+ (.secondmates | any(.[]; .id == "(registry)" and .state == "unknown"
804
+ and .provenance == "registered-table" and .freshness == "unavailable"))
805
+ and (.omitted | any(.surface | contains("secondmate registry unavailable")))
806
+ ' >/dev/null || fail "unreadable registry disappeared from bearings: $json"
807
+ home=$(make_home registry-bounds)
808
+ : > "$home/data/secondmates.md"
809
+ for id in one two three; do
810
+ mate="$TMP_ROOT/registry-$id"
811
+ make_valid_secondmate_home "$id" "$mate"
812
+ append_secondmate_registry "$home" "$id" "$mate"
813
+ done
814
+ fakebin=$(make_fakebin "$home")
815
+ canonical=$(PATH="$fakebin:$PATH" FM_HOME="$home" FM_SNAPSHOT_NOW=2026-07-11T18:00:00Z \
816
+ FM_SNAPSHOT_REGISTRY_RECORDS=2 "$ROOT/bin/fm-fleet-snapshot.sh" --json)
817
+ printf '%s' "$canonical" | jq -e '
818
+ .secondmate_current.registry
819
+ | .available == true and .provenance == "registered-table"
820
+ and .freshness.status == "fresh" and .records_truncated == true
821
+ and .records_in_window == 3 and (.records | length) == 2
822
+ and (.reasons | index("record_limit") != null)
823
+ ' >/dev/null || fail "registry record bound was not enforced or disclosed: $canonical"
824
+ canonical=$(PATH="$fakebin:$PATH" FM_HOME="$home" FM_SNAPSHOT_NOW=2026-07-11T18:00:00Z \
825
+ FM_SNAPSHOT_REGISTRY_LINES=2 "$ROOT/bin/fm-fleet-snapshot.sh" --json)
826
+ printf '%s' "$canonical" | jq -e '
827
+ .secondmate_current.registry
828
+ | .input_truncated == true and .records_truncated == false
829
+ and .lines_in_window == 2 and (.records | length) == 2
830
+ and .reasons == ["line_limit"]
831
+ ' >/dev/null || fail "registry line bound was not enforced or disclosed: $canonical"
832
+ canonical=$(PATH="$fakebin:$PATH" FM_HOME="$home" FM_SNAPSHOT_NOW=2026-07-11T18:00:00Z \
833
+ FM_SNAPSHOT_REGISTRY_BYTES=100 "$ROOT/bin/fm-fleet-snapshot.sh" --json)
834
+ printf '%s' "$canonical" | jq -e '
835
+ .secondmate_current.registry
836
+ | .input_truncated == true and (.reasons | index("byte_limit") != null)
837
+ and .records_in_window < 3
838
+ ' >/dev/null || fail "registry byte bound was not enforced or disclosed: $canonical"
839
+ boundary=$(LC_ALL=C head -n 1 "$home/data/secondmates.md" | wc -c | tr -d ' ')
840
+ canonical=$(PATH="$fakebin:$PATH" FM_HOME="$home" FM_SNAPSHOT_NOW=2026-07-11T18:00:00Z \
841
+ FM_SNAPSHOT_REGISTRY_BYTES="$((boundary - 1))" "$ROOT/bin/fm-fleet-snapshot.sh" --json)
842
+ printf '%s' "$canonical" | jq -e '
843
+ .secondmate_current.registry
844
+ | .input_truncated == true and .complete == false
845
+ and (.reasons | index("byte_limit") != null)
846
+ ' >/dev/null || fail "registry newline byte boundary hid truncation: $canonical"
847
+ json=$(FM_SNAPSHOT_REGISTRY_RECORDS=2 run "$home" "$fakebin" --json)
848
+ printf '%s' "$json" | jq -e '
849
+ .omitted | any(.surface == "secondmate registry records omitted by bounded read")
850
+ ' >/dev/null || fail "bearings omitted registry truncation disclosure: $json"
851
+ mate="$TMP_ROOT/registry-z-hidden"
852
+ make_valid_secondmate_home z-hidden "$mate"
853
+ append_secondmate_registry "$home" z-hidden "$mate"
854
+ fm_write_secondmate_meta "$home/state/z-hidden.meta" "$mate" "firstmate:fm-z-hidden" sample
855
+ canonical=$(PATH="$fakebin:$PATH" FM_HOME="$home" FM_SNAPSHOT_NOW=2026-07-11T18:00:00Z \
856
+ FM_SNAPSHOT_REGISTRY_RECORDS=3 "$ROOT/bin/fm-fleet-snapshot.sh" --json)
857
+ printf '%s' "$canonical" | jq -e '
858
+ .secondmate_current.registry.complete == false
859
+ and (.secondmate_current.records[] | select(.id == "z-hidden")
860
+ | .registered == null
861
+ and (.current.reason | contains("registration is unknown")))
862
+ ' >/dev/null || fail "truncated registry produced false unregistered provenance: $canonical"
863
+ pass "registry unavailability and bounded truncation remain explicit"
864
+ }
865
+
866
+ test_current_landed_baseline_is_repeatable_and_prior_report_independent() {
867
+ local home fakebin one two
868
+ home=$(make_home standalone-baseline); write_fixture "$home"
869
+ cat > "$home/data/status-report-2026-07-10.md" <<'EOF'
870
+ # Misleading old report
871
+
872
+ ## Recently Landed
873
+ - fake-old-item
874
+
875
+ ## Underway
876
+ - Phase 7 started
877
+ EOF
878
+ fakebin=$(make_fakebin "$home")
879
+ one=$(run "$home" "$fakebin" --json)
880
+ two=$(run "$home" "$fakebin" --json)
881
+ [ "$(printf '%s' "$one" | jq -c '.landed')" = "$(printf '%s' "$two" | jq -c '.landed')" ] \
882
+ || fail "the same structured state produced different recent-completion baselines"
883
+ printf '%s' "$two" | jq -e '
884
+ (.landed | any(.id == "done-a"))
885
+ and (.landed | any(.id == "mate-landed"))
886
+ and (.landed | any(.id == "fake-old-item") | not)
887
+ and (.in_flight | any(.doing == "Phase 7 started") | not)
888
+ ' >/dev/null || fail "prior status report influenced the standalone snapshot: $two"
889
+ pass "repeated snapshots keep the same current landed baseline and ignore prior reports"
890
+ }
891
+
892
+ test_default_is_bounded_and_local_only() {
893
+ local home fakebin toon json
894
+ home=$(make_home bounded); write_fixture "$home"
895
+ fakebin=$(make_fakebin "$home"); : > "$home/net.log"
896
+ toon=$(run "$home" "$fakebin")
897
+ json=$(run "$home" "$fakebin" --json)
898
+ # Bound: well under the ~50 KB tool-display limit.
899
+ [ "${#toon}" -lt 50000 ] || fail "default TOON must stay under the display bound, got ${#toon}"
900
+ # TOON is materially smaller than the canonical snapshot it projects.
901
+ local canon; canon=$(PATH="$fakebin:$PATH" FM_HOME="$home" "$ROOT/bin/fm-fleet-snapshot.sh" --json)
902
+ [ "${#toon}" -lt "${#canon}" ] || fail "projection must be smaller than the canonical snapshot"
903
+ # Local-only: no GitHub/network call on the default path.
904
+ [ ! -s "$home/net.log" ] || fail "default run must make no gh/gh-axi call, got: $(cat "$home/net.log")"
905
+ # Definitive not-requested PR state, never a silent omission.
906
+ assert_contains "$toon" 'prs: "not_requested' "default must state PR checks were not requested"
907
+ assert_contains "$toon" "live PR discovery + checks,\"--include-prs\"" "omitted must mark the dropped live-PR surface"
908
+ # Valid JSON, correct schema.
909
+ printf '%s' "$json" | jq -e '.schema == "fm-bearings.v1"' >/dev/null || fail "json schema wrong"
910
+ pass "default output is bounded, local-only, and marks omitted surfaces"
911
+ }
912
+
913
+ test_toon_json_parity() {
914
+ local home fakebin toon json keys k
915
+ home=$(make_home parity); write_fixture "$home"
916
+ fakebin=$(make_fakebin "$home")
917
+ toon=$(run "$home" "$fakebin")
918
+ json=$(run "$home" "$fakebin" --json)
919
+ # Same top-level keys in both representations.
920
+ keys=$(printf '%s' "$json" | jq -r 'keys_unsorted[]')
921
+ for k in $keys; do
922
+ if printf '%s' "$json" | jq -e --arg k "$k" '.[$k] | type == "array"' >/dev/null; then
923
+ local n hdr
924
+ n=$(printf '%s' "$json" | jq --arg k "$k" '.[$k] | length')
925
+ if [ "$n" = 0 ]; then
926
+ assert_contains "$toon" "$k: []" "empty array $k must render as 'key: []'"
927
+ else
928
+ # Header must declare the same count and the same field set.
929
+ hdr=$(printf '%s' "$toon" | grep -E "^$k\[[0-9]+\]\{" || true)
930
+ [ -n "$hdr" ] || fail "TOON missing tabular header for $k"
931
+ assert_contains "$hdr" "[$n]" "TOON $k row count must equal JSON length $n"
932
+ local jfields tfields
933
+ jfields=$(printf '%s' "$json" | jq -r --arg k "$k" '.[$k][0] | keys_unsorted | join(",")')
934
+ tfields=$(printf '%s' "$hdr" | sed -E 's/^[^{]*\{//; s/\}:.*$//; s/"//g')
935
+ [ "$jfields" = "$tfields" ] || fail "TOON $k fields ($tfields) must equal JSON fields ($jfields)"
936
+ fi
937
+ else
938
+ # Scalar: the key must appear as a "key: value" line.
939
+ assert_contains "$toon" "$k: " "TOON must carry scalar field $k"
940
+ fi
941
+ done
942
+ pass "TOON and JSON are parity representations of the same model"
943
+ }
944
+
945
+ test_open_decision_surfaces_end_to_end() {
946
+ local home fakebin json
947
+ home=$(make_home e2e-decision); write_fixture "$home"
948
+ fakebin=$(make_fakebin "$home")
949
+ json=$(run "$home" "$fakebin" --json)
950
+ printf '%s' "$json" | jq -e '
951
+ .decisions_open | any(.[]; .id == "mate/mate-decision-race"
952
+ and .key == "mate-decision-race" and .verb == "captain-hold")
953
+ ' >/dev/null || fail "an authoritative captain hold must surface in decisions_open: $json"
954
+ pass "an authoritative captain hold surfaces end-to-end"
955
+ }
956
+
957
+ test_report_pointers_surface() {
958
+ local home fakebin json
959
+ home=$(make_home reports); write_fixture "$home"
960
+ fakebin=$(make_fakebin "$home")
961
+ json=$(run "$home" "$fakebin" --json)
962
+ printf '%s' "$json" | jq -e --arg p "$home/data/scout-x/report.md" '
963
+ .reports | any(.[]; .id == "scout-x" and .path == $p)
964
+ ' >/dev/null || fail "current scout report pointer must surface: $json"
965
+ pass "current report pointers surface"
966
+ }
967
+
968
+ test_superseded_queued_item_dropped_by_default() {
969
+ local home fakebin json
970
+ home=$(make_home superseded); write_fixture "$home"
971
+ fakebin=$(make_fakebin "$home")
972
+ json=$(run "$home" "$fakebin" --json)
973
+ printf '%s' "$json" | jq -e '
974
+ (.gates | any(.[]; .id == "live-gate")) and (.gates | any(.[]; .id == "dead-gate") | not)
975
+ ' >/dev/null || fail "default gates must include live and drop superseded: $json"
976
+ json=$(run "$home" "$fakebin" --json --all-queued)
977
+ printf '%s' "$json" | jq -e '.gates | any(.[]; .id == "dead-gate")' >/dev/null \
978
+ || fail "--all-queued must restore the superseded item"
979
+ pass "superseded queued items are dropped by default and restored with --all-queued"
980
+ }
981
+
982
+ # The collapsed captain-call contract: any due, unblocked captain-held task is
983
+ # Captain's Call whatever its kind; a date-deferred hold is a dated gate until
984
+ # due; a prose-deferred hold leaves the default views with a disclosure; and
985
+ # Recently Landed excludes only what closed while still held for the captain.
986
+ test_collapsed_captain_call_deferral_and_landed() {
987
+ local home fakebin json
988
+ home=$(make_home collapsed-call)
989
+ mkdir -p "$home/data"
990
+ cat > "$home/data/backlog.md" <<'EOF'
991
+ ## In flight
992
+
993
+ ## Queued
994
+ - [ ] work-gate - Captain-gated ship work (repo: firstmate) (kind: ship) (hold: captain go needed) (hold-kind: captain)
995
+ - [ ] later-call - Deferred captain call (repo: firstmate) (kind: captain) (hold: revisit with the captain) (hold-kind: captain) (hold-until: 2026-08-01)
996
+ - [ ] due-call - Due captain call (repo: firstmate) (kind: captain) (hold: overdue captain choice) (hold-kind: captain) (hold-until: 2026-07-11)
997
+ - [ ] parked-call - Prose-parked captain call (repo: firstmate) (kind: ship) (hold: DEFERRED by captain revisit later) (hold-kind: captain)
998
+ - [ ] external-gate - Externally held work (repo: firstmate) (kind: ship) (hold: upstream release pending) (hold-kind: external)
999
+
1000
+ ## Done
1001
+ - [x] answered-call - Answered captain question (repo: firstmate) (kind: captain) (done 2026-07-10) (hold: captain choice pending) (hold-kind: captain)
1002
+ - [x] shipped-work - Ordinary landed work (repo: firstmate) (kind: ship) (merged 2026-07-10)
1003
+ EOF
1004
+ fakebin=$(make_fakebin "$home")
1005
+ json=$(run "$home" "$fakebin" --json)
1006
+ printf '%s' "$json" | jq -e '
1007
+ (.decisions_open | any(.[]; .id == "work-gate"))
1008
+ and (.decisions_open | any(.[]; .id == "due-call"))
1009
+ and (.decisions_open | any(.[]; .id == "later-call") | not)
1010
+ and (.decisions_open | any(.[]; .id == "parked-call") | not)
1011
+ and (.decisions_open | any(.[]; .id == "external-gate") | not)
1012
+ and (.gates | any(.[]; .id == "later-call" and (.reason | startswith("until 2026-08-01"))))
1013
+ and (.gates | any(.[]; .id == "work-gate") | not)
1014
+ and (.gates | any(.[]; .id == "parked-call") | not)
1015
+ and (.gates | any(.[]; .id == "external-gate"))
1016
+ and (.landed | any(.[]; .id == "shipped-work"))
1017
+ and (.landed | any(.[]; .id == "answered-call") | not)
1018
+ and (.omitted | any(.[]; .surface | startswith("captain holds marked deferred")))
1019
+ ' >/dev/null || fail "the collapsed captain-call projection is wrong: $json"
1020
+ json=$(run "$home" "$fakebin" --json --all-decisions --all-queued)
1021
+ printf '%s' "$json" | jq -e '
1022
+ (.decisions_open | any(.[]; .id == "parked-call"))
1023
+ and (.gates | any(.[]; .id == "parked-call") | not)
1024
+ ' >/dev/null || fail "--all-decisions must reveal the prose-deferred call: $json"
1025
+ pass "captain-held tasks of any kind reach Captain's Call, deferral is honored, and landed excludes answered calls"
1026
+ }
1027
+
1028
+ test_include_prs_is_the_only_fetch_path() {
1029
+ local home fakebin json
1030
+ home=$(make_home prs); write_fixture "$home"
1031
+ fakebin=$(make_fakebin "$home"); : > "$home/net.log"
1032
+ json=$(run "$home" "$fakebin" --include-prs --json)
1033
+ # Now gh WAS called, exactly for pr list.
1034
+ grep -q '^gh pr list ' "$home/net.log" || fail "--include-prs must call gh pr list"
1035
+ printf '%s' "$json" | jq -e '
1036
+ .prs | startswith("checked")
1037
+ ' >/dev/null || fail "--include-prs must report checked PR state"
1038
+ printf '%s' "$json" | jq -e '
1039
+ .candidate_prs | any(.[]; .num == "9" and .task == "ship-task" and .checks == "passing" and .review == "APPROVED")
1040
+ ' >/dev/null || fail "candidate_prs must carry the fetched PR cross-referenced to its task: $json"
1041
+ pass "--include-prs is the only path that fetches, and it enriches correctly"
1042
+ }
1043
+
1044
+ test_partial_github_failure_degrades() {
1045
+ local home fakebin json rc
1046
+ home=$(make_home partial); write_fixture "$home"
1047
+ fakebin=$(make_fakebin "$home")
1048
+ json=$(FAKE_GH_FAIL=1 run "$home" "$fakebin" --include-prs --json); rc=$?
1049
+ expect_code 0 "$rc" "a PR-fetch failure must not crash the view"
1050
+ printf '%s' "$json" | jq -e '
1051
+ .schema == "fm-bearings.v1"
1052
+ and (.candidate_prs | length) == 0
1053
+ and (.prs | test("unavailable"))
1054
+ and (.in_flight | length) > 0
1055
+ ' >/dev/null || fail "on gh failure the view must still emit, with an unavailable note: $json"
1056
+ pass "a partial GitHub failure degrades gracefully"
1057
+ }
1058
+
1059
+ test_perl_fallback_bounds_github_call() {
1060
+ local home fakebin toolbin cmd json started elapsed
1061
+ home=$(make_home perl-timeout); write_fixture "$home"
1062
+ fakebin=$(make_fakebin "$home")
1063
+ toolbin="$home/toolbin"
1064
+ mkdir -p "$toolbin"
1065
+ for cmd in bash dirname basename jq date sed git grep tail cut tr head sort wc perl sleep cat find; do
1066
+ ln -s "$(command -v "$cmd")" "$toolbin/$cmd"
1067
+ done
1068
+ started=$(date +%s)
1069
+ json=$(PATH="$fakebin:$toolbin" FM_HOME="$home" FM_BEARINGS_NOW=2026-07-11T18:00:00Z \
1070
+ FM_BEARINGS_PR_TIMEOUT=1 NET_LOG="$home/net.log" FAKE_GH_SLEEP=1 "$BEARINGS" --include-prs --json)
1071
+ elapsed=$(( $(date +%s) - started ))
1072
+ [ "$elapsed" -lt 10 ] || fail "Perl fallback did not bound a stalled gh call (${elapsed}s)"
1073
+ printf '%s' "$json" | jq -e '.prs | test("unavailable")' >/dev/null \
1074
+ || fail "timed-out gh call did not fail soft: $json"
1075
+ pass "Perl fallback bounds stalled GitHub calls without coreutils timeout"
1076
+ }
1077
+
1078
+ write_large_fixture() { # <home> <count>
1079
+ local home=$1 count=$2 i id
1080
+ : > "$home/data/backlog.md"
1081
+ printf '## Queued\n' >> "$home/data/backlog.md"
1082
+ i=1
1083
+ while [ "$i" -le "$count" ]; do
1084
+ id="dead-$i"
1085
+ mkdir -p "$home/projects/$id" "$home/data/$id"
1086
+ printf '# Report\n' > "$home/data/$id/report.md"
1087
+ printf -- '- [ ] gate-%s - Gate %s blocked-by: task-%s (repo: repo-%s) (kind: ship)\n' "$i" "$i" "$i" "$i" >> "$home/data/backlog.md"
1088
+ printf -- '- [ ] decision-%s - Decision %s (repo: repo-%s) (kind: captain) (hold: captain choice pending) (hold-kind: captain)\n' "$i" "$i" "$i" >> "$home/data/backlog.md"
1089
+ fm_write_meta "$home/state/$id.meta" \
1090
+ "window=firstmate:fm-$id" \
1091
+ "worktree=$home/projects/$id" \
1092
+ "project=repo-$i" \
1093
+ "harness=codex" \
1094
+ "kind=scout" \
1095
+ "mode=scout" \
1096
+ "pr=https://github.com/acme/repo-$i/pull/$i"
1097
+ printf 'needs-decision [key=q%s]: choose %s\n' "$i" "$i" > "$home/state/$id.status"
1098
+ i=$((i + 1))
1099
+ done
1100
+ }
1101
+
1102
+ test_section_caps_and_expansion_flags() {
1103
+ local home fakebin json expanded
1104
+ home=$(make_home caps); write_large_fixture "$home" 5
1105
+ fakebin=$(make_fakebin "$home")
1106
+ json=$(FM_BEARINGS_IN_FLIGHT=2 FM_BEARINGS_DECISIONS=2 FM_BEARINGS_GATES=2 \
1107
+ FM_BEARINGS_REPORTS=2 FM_BEARINGS_RECORDED_PRS=2 FM_BEARINGS_UNHEALTHY=2 \
1108
+ run "$home" "$fakebin" --json)
1109
+ printf '%s' "$json" | jq -e '
1110
+ (.in_flight|length) == 2 and (.decisions_open|length) == 2 and (.gates|length) == 2
1111
+ and (.reports|length) == 2 and (.recorded_prs|length) == 2 and (.unhealthy_endpoints|length) == 2
1112
+ and ([.omitted[].surface] | index("in_flight showing 2 of 5") != null)
1113
+ and ([.omitted[].surface] | index("decisions_open showing 2 of 5") != null)
1114
+ and ([.omitted[].surface] | index("gates showing 2 of 5") != null)
1115
+ and ([.omitted[].surface] | index("reports showing 2 of 5") != null)
1116
+ and ([.omitted[].surface] | index("recorded_prs showing 2 of 5") != null)
1117
+ and ([.omitted[].surface] | index("unhealthy_endpoints showing 2 of 5") != null)
1118
+ ' >/dev/null || fail "section caps or counted omissions are wrong: $json"
1119
+ expanded=$(FM_BEARINGS_IN_FLIGHT=2 FM_BEARINGS_DECISIONS=2 FM_BEARINGS_GATES=2 \
1120
+ FM_BEARINGS_REPORTS=2 FM_BEARINGS_RECORDED_PRS=2 FM_BEARINGS_UNHEALTHY=2 \
1121
+ run "$home" "$fakebin" --json --all-in-flight --all-decisions --all-queued \
1122
+ --all-reports --all-recorded-prs --all-unhealthy)
1123
+ printf '%s' "$expanded" | jq -e '
1124
+ (.in_flight|length) == 5 and (.decisions_open|length) == 5 and (.gates|length) == 5
1125
+ and (.reports|length) == 5 and (.recorded_prs|length) == 5 and (.unhealthy_endpoints|length) == 5
1126
+ ' >/dev/null || fail "section expansion flags did not reveal full sets: $expanded"
1127
+ pass "all fleet-sized sections are capped with counted opt-in expansion"
1128
+ }
1129
+
1130
+ test_pr_repository_cap_and_expansion() {
1131
+ local home fakebin json expanded
1132
+ home=$(make_home repo-caps); write_large_fixture "$home" 5
1133
+ fakebin=$(make_fakebin "$home"); : > "$home/net.log"
1134
+ json=$(FM_BEARINGS_PR_REPOS=2 run "$home" "$fakebin" --include-prs --json)
1135
+ [ "$(grep -c '^gh pr list ' "$home/net.log")" = 2 ] || fail "default PR repository cap was not enforced"
1136
+ printf '%s' "$json" | jq -e '
1137
+ [.omitted[] | select(.surface == "PR repositories showing 2 of 5" and .reveal == "--all-pr-repos")] | length == 1
1138
+ ' >/dev/null || fail "PR repository truncation was not recorded: $json"
1139
+ : > "$home/net.log"
1140
+ expanded=$(FM_BEARINGS_PR_REPOS=2 run "$home" "$fakebin" --include-prs --all-pr-repos --json)
1141
+ [ "$(grep -c '^gh pr list ' "$home/net.log")" = 5 ] || fail "--all-pr-repos did not reveal every repository"
1142
+ printf '%s' "$expanded" | jq -e '.candidate_prs | length == 5' >/dev/null \
1143
+ || fail "expanded PR repository set did not enrich every repository: $expanded"
1144
+ pass "live PR enrichment caps repositories with counted expansion"
1145
+ }
1146
+
1147
+ test_per_repository_pr_cap_is_disclosed() {
1148
+ local home fakebin json toon
1149
+ home=$(make_home pr-row-cap); write_fixture "$home"
1150
+ fakebin=$(make_fakebin "$home")
1151
+ json=$(FM_BEARINGS_PR_LIMIT=2 FAKE_GH_MANY=1 run "$home" "$fakebin" --include-prs --json)
1152
+ toon=$(FM_BEARINGS_PR_LIMIT=2 FAKE_GH_MANY=1 run "$home" "$fakebin" --include-prs)
1153
+ printf '%s' "$json" | jq -e '
1154
+ (.candidate_prs | length) == 2
1155
+ and (.prs | test("2 shown, at least 3 open; capped in 1 repo"))
1156
+ and ([.omitted[] | select(.surface == "candidate_prs showing 2 of at least 3; capped in 1 repo(s)" and .reveal == "raise FM_BEARINGS_PR_LIMIT")] | length) == 1
1157
+ ' >/dev/null || fail "per-repository PR truncation was not disclosed: $json"
1158
+ assert_contains "$toon" 'candidate_prs showing 2 of at least 3' "TOON did not preserve PR truncation disclosure"
1159
+ pass "per-repository open-PR caps are disclosed with an expansion knob"
1160
+ }
1161
+
1162
+ install_failing_jq() { # <fakebin> <model|toon>
1163
+ local fakebin=$1 phase=$2 real
1164
+ real=$(command -v jq)
1165
+ cat > "$fakebin/jq" <<SH
1166
+ #!/usr/bin/env bash
1167
+ case "\$*" in
1168
+ *'def trunc'*) [ "$phase" = model ] && exit 9 ;;
1169
+ *'def q:'*) [ "$phase" = toon ] && exit 9 ;;
1170
+ esac
1171
+ exec "$real" "\$@"
1172
+ SH
1173
+ chmod +x "$fakebin/jq"
1174
+ }
1175
+
1176
+ test_projection_and_toon_fail_closed() {
1177
+ local home fakebin out err rc
1178
+ home=$(make_home fail-closed); write_fixture "$home"
1179
+ fakebin=$(make_fakebin "$home")
1180
+ install_failing_jq "$fakebin" model
1181
+ err="$home/model.err"
1182
+ out=$(run "$home" "$fakebin" --json 2> "$err"); rc=$?
1183
+ [ "$rc" -ne 0 ] || fail "projection failure exited successfully"
1184
+ [ -z "$out" ] || fail "projection failure emitted output"
1185
+ grep -F 'projection failed' "$err" >/dev/null || fail "projection failure lacked a diagnostic"
1186
+ install_failing_jq "$fakebin" toon
1187
+ err="$home/toon.err"
1188
+ out=$(run "$home" "$fakebin" 2> "$err"); rc=$?
1189
+ [ "$rc" -ne 0 ] || fail "TOON rendering failure exited successfully"
1190
+ [ -z "$out" ] || fail "TOON rendering failure emitted output"
1191
+ grep -F 'TOON rendering failed' "$err" >/dev/null || fail "TOON failure lacked a diagnostic"
1192
+ pass "projection and TOON rendering failures exit nonzero with diagnostics"
1193
+ }
1194
+
1195
+ # The Lavish-103 defect, end to end: a COMPLETED scout that raised a decision and
1196
+ # then finished (done), whose report body reads like that decision, must surface as
1197
+ # a report POINTER only - never in decisions_open. Report prose must never open or
1198
+ # reopen a pending decision; only the keyed durable state does.
1199
+ test_completed_scout_report_not_pending() {
1200
+ local home fakebin json
1201
+ home=$(make_home completed-scout); write_fixture "$home"
1202
+ fakebin=$(make_fakebin "$home")
1203
+ mkdir -p "$home/projects/lav-wt" "$home/data/lavish-103"
1204
+ fm_write_meta "$home/state/lavish-103.meta" \
1205
+ "window=firstmate:fm-lavish-103" \
1206
+ "worktree=$home/projects/lav-wt" \
1207
+ "project=firstmate" \
1208
+ "harness=codex" \
1209
+ "kind=scout" \
1210
+ "mode=scout"
1211
+ printf 'needs-decision: adopt approach A or B for Lavish issue 103\n' > "$home/state/lavish-103.status"
1212
+ printf 'done: report ready at data/lavish-103/report.md\n' >> "$home/state/lavish-103.status"
1213
+ printf '# Lavish 103\nThe open question is whether to adopt approach A or B; this needs a captain decision.\n' > "$home/data/lavish-103/report.md"
1214
+ json=$(run "$home" "$fakebin" --json)
1215
+ printf '%s' "$json" | jq -e '
1216
+ (.decisions_open | any(.[]; .id == "lavish-103") | not)
1217
+ and (.reports | any(.[]; .id == "lavish-103"))
1218
+ ' >/dev/null || fail "completed scout must be a report pointer, never a pending decision: $json"
1219
+ pass "a completed scout with decision-like report prose is a pointer, not pending"
1220
+ }
1221
+
1222
+ # Recently Landed must include merges a secondmate managed. Those completion records
1223
+ # live in the secondmate home's OWN backlog, not the main one, so the projection must
1224
+ # roll them up. Local, deterministic, no GitHub call.
1225
+ test_landed_includes_secondmate_home_merges() {
1226
+ local home fakebin json
1227
+ home=$(make_home mate-landed); write_fixture "$home"
1228
+ fakebin=$(make_fakebin "$home"); : > "$home/net.log"
1229
+ json=$(run "$home" "$fakebin" --json)
1230
+ printf '%s' "$json" | jq -e '
1231
+ (.landed | any(.[]; .id == "mate-landed" and (.artifact | test("/pull/50"))))
1232
+ and (.landed | any(.[]; .id == "done-a"))
1233
+ ' >/dev/null || fail "landed must merge secondmate-home Done with main-home Done: $json"
1234
+ # Still zero network on this default path.
1235
+ [ ! -s "$home/net.log" ] || fail "landed roll-up must make no gh/gh-axi call, got: $(cat "$home/net.log")"
1236
+ pass "landed includes secondmate-managed merges alongside main-home merges"
1237
+ }
1238
+
1239
+ test_landed_default_balances_dominant_and_sparse_homes() {
1240
+ local home dominant sparse_a sparse_b sparse_c fakebin json i actual expected
1241
+ home=$(make_home landed-balanced-default)
1242
+ : > "$home/data/secondmates.md"
1243
+ printf '## Done\n' > "$home/data/backlog.md"
1244
+ dominant=$(make_landed_secondmate "$home" dominant)
1245
+ sparse_a=$(make_landed_secondmate "$home" sparse-a)
1246
+ sparse_b=$(make_landed_secondmate "$home" sparse-b)
1247
+ sparse_c=$(make_landed_secondmate "$home" sparse-c)
1248
+ i=1
1249
+ while [ "$i" -le 12 ]; do
1250
+ append_landed_row "$dominant" "$(printf 'dominant-landed-%02d' "$i")" \
1251
+ "$(printf 'Dominant landed %02d' "$i")" "$(printf '2026-07-%02d' "$((31 - i))")"
1252
+ i=$((i + 1))
1253
+ done
1254
+ i=1
1255
+ while [ "$i" -le 2 ]; do
1256
+ append_landed_row "$sparse_a" "$(printf 'sparse-a-landed-%02d' "$i")" \
1257
+ "$(printf 'Sparse A landed %02d' "$i")" "$(printf '2026-07-%02d' "$((12 - i))")"
1258
+ append_landed_row "$sparse_b" "$(printf 'sparse-b-landed-%02d' "$i")" \
1259
+ "$(printf 'Sparse B landed %02d' "$i")" "$(printf '2026-07-%02d' "$((10 - i))")"
1260
+ append_landed_row "$sparse_c" "$(printf 'sparse-c-landed-%02d' "$i")" \
1261
+ "$(printf 'Sparse C landed %02d' "$i")" "$(printf '2026-07-%02d' "$((8 - i))")"
1262
+ i=$((i + 1))
1263
+ done
1264
+ fakebin=$(make_fakebin "$home")
1265
+ json=$(run "$home" "$fakebin" --json)
1266
+ actual=$(printf '%s' "$json" | jq -r '.landed[] | "\(.owner)/\(.id)"')
1267
+ expected='dominant/dominant-landed-01
1268
+ sparse-a/sparse-a-landed-01
1269
+ sparse-b/sparse-b-landed-01
1270
+ sparse-c/sparse-c-landed-01
1271
+ dominant/dominant-landed-02
1272
+ sparse-a/sparse-a-landed-02'
1273
+ [ "$actual" = "$expected" ] || fail "default landed selection was not balanced across homes: $actual"
1274
+ printf '%s' "$json" | jq -e '
1275
+ (.landed | length) == 6
1276
+ and ([.landed[].owner] | unique | length) == 4
1277
+ and ([.omitted[].surface] | any(test("landed showing 6 of 12")))
1278
+ ' >/dev/null || fail "balanced landed default did not preserve cap disclosure: $json"
1279
+ pass "default landed selection balances one dominant home with sparse homes"
1280
+ }
1281
+
1282
+ test_landed_default_refills_capacity_after_sparse_homes_exhaust() {
1283
+ local home dominant sparse fakebin json actual expected i
1284
+ home=$(make_home landed-sparse-refill)
1285
+ : > "$home/data/secondmates.md"
1286
+ printf '## Done\n' > "$home/data/backlog.md"
1287
+ dominant=$(make_landed_secondmate "$home" dominant)
1288
+ sparse=$(make_landed_secondmate "$home" sparse)
1289
+ i=1
1290
+ while [ "$i" -le 5 ]; do
1291
+ append_landed_row "$dominant" "$(printf 'dominant-landed-%02d' "$i")" \
1292
+ "$(printf 'Dominant landed %02d' "$i")" "$(printf '2026-07-%02d' "$((20 - i))")"
1293
+ i=$((i + 1))
1294
+ done
1295
+ append_landed_row "$sparse" sparse-landed-01 "Sparse landed 01" 2026-07-01
1296
+ fakebin=$(make_fakebin "$home")
1297
+ json=$(run "$home" "$fakebin" --json)
1298
+ actual=$(printf '%s' "$json" | jq -r '.landed[] | "\(.owner)/\(.id)"')
1299
+ expected='dominant/dominant-landed-01
1300
+ sparse/sparse-landed-01
1301
+ dominant/dominant-landed-02
1302
+ dominant/dominant-landed-03
1303
+ dominant/dominant-landed-04
1304
+ dominant/dominant-landed-05'
1305
+ [ "$actual" = "$expected" ] || fail "sparse homes wasted landed capacity: $actual"
1306
+ pass "landed selection refills capacity after sparse homes exhaust"
1307
+ }
1308
+
1309
+ test_landed_default_uses_deterministic_home_order_when_homes_exceed_cap() {
1310
+ local home mate fakebin json actual expected i id
1311
+ home=$(make_home landed-home-order)
1312
+ : > "$home/data/secondmates.md"
1313
+ printf '## Done\n' > "$home/data/backlog.md"
1314
+ i=1
1315
+ while [ "$i" -le 8 ]; do
1316
+ id=$(printf 'home-%02d' "$i")
1317
+ mate=$(make_landed_secondmate "$home" "$id")
1318
+ append_landed_row "$mate" "$id-landed-01" "$id landed 01" 2026-07-10
1319
+ i=$((i + 1))
1320
+ done
1321
+ fakebin=$(make_fakebin "$home")
1322
+ json=$(run "$home" "$fakebin" --json)
1323
+ actual=$(printf '%s' "$json" | jq -r '.landed[] | "\(.owner)/\(.id)"')
1324
+ expected='home-01/home-01-landed-01
1325
+ home-02/home-02-landed-01
1326
+ home-03/home-03-landed-01
1327
+ home-04/home-04-landed-01
1328
+ home-05/home-05-landed-01
1329
+ home-06/home-06-landed-01'
1330
+ [ "$actual" = "$expected" ] || fail "landed home-order tie was not deterministic: $actual"
1331
+ printf '%s' "$json" | jq -e '
1332
+ ([.omitted[].surface] | any(test("landed showing 6 of 8")))
1333
+ ' >/dev/null || fail "more-homes-than-cap omission was not disclosed: $json"
1334
+ pass "landed selection uses deterministic home order when homes exceed the cap"
1335
+ }
1336
+
1337
+ test_landed_default_preserves_internal_order_for_ties() {
1338
+ local home tie_a tie_b fakebin json actual expected
1339
+ home=$(make_home landed-ties)
1340
+ : > "$home/data/secondmates.md"
1341
+ printf '## Done\n' > "$home/data/backlog.md"
1342
+ tie_a=$(make_landed_secondmate "$home" tie-a)
1343
+ tie_b=$(make_landed_secondmate "$home" tie-b)
1344
+ append_landed_row "$tie_b" tie-b-a "Tie B A" 2026-07-10
1345
+ append_landed_row "$tie_b" tie-b-z "Tie B Z" 2026-07-10
1346
+ append_landed_row "$tie_a" tie-a-a "Tie A A" 2026-07-10
1347
+ append_landed_row "$tie_a" tie-a-z "Tie A Z" 2026-07-10
1348
+ fakebin=$(make_fakebin "$home")
1349
+ json=$(run "$home" "$fakebin" --json)
1350
+ actual=$(printf '%s' "$json" | jq -r '.landed[] | "\(.owner)/\(.id)"')
1351
+ expected='tie-a/tie-a-z
1352
+ tie-b/tie-b-z
1353
+ tie-a/tie-a-a
1354
+ tie-b/tie-b-a'
1355
+ [ "$actual" = "$expected" ] || fail "landed tie ordering changed: $actual"
1356
+ pass "landed selection preserves deterministic home and internal tie ordering"
1357
+ }
1358
+
1359
+ test_landed_default_handles_no_landed_items() {
1360
+ local home fakebin json
1361
+ home=$(make_home landed-empty)
1362
+ : > "$home/data/secondmates.md"
1363
+ printf '## Done\n' > "$home/data/backlog.md"
1364
+ fakebin=$(make_fakebin "$home")
1365
+ json=$(run "$home" "$fakebin" --json)
1366
+ printf '%s' "$json" | jq -e '
1367
+ (.landed | length) == 0
1368
+ and ([.omitted[].surface] | any(test("landed")) | not)
1369
+ ' >/dev/null || fail "empty landed set was not handled cleanly: $json"
1370
+ pass "landed selection handles no landed items"
1371
+ }
1372
+
1373
+ test_all_landed_keeps_complete_global_order() {
1374
+ local home alpha beta fakebin json actual expected
1375
+ home=$(make_home landed-all-order)
1376
+ : > "$home/data/secondmates.md"
1377
+ printf '## Done\n' > "$home/data/backlog.md"
1378
+ alpha=$(make_landed_secondmate "$home" alpha)
1379
+ beta=$(make_landed_secondmate "$home" beta)
1380
+ append_landed_row "$alpha" alpha-old "Alpha old" 2026-07-01
1381
+ append_landed_row "$alpha" alpha-new "Alpha new" 2026-07-09
1382
+ append_landed_row "$beta" beta-new "Beta new" 2026-07-10
1383
+ append_landed_row "$beta" beta-mid "Beta mid" 2026-07-05
1384
+ fakebin=$(make_fakebin "$home")
1385
+ json=$(FM_BEARINGS_LANDED=1 run "$home" "$fakebin" --json --all-landed)
1386
+ actual=$(printf '%s' "$json" | jq -r '.landed[] | "\(.owner)/\(.id)"')
1387
+ expected='beta/beta-new
1388
+ alpha/alpha-new
1389
+ beta/beta-mid
1390
+ alpha/alpha-old'
1391
+ [ "$actual" = "$expected" ] || fail "--all-landed global order changed: $actual"
1392
+ printf '%s' "$json" | jq -e '
1393
+ (.landed | length) == 4
1394
+ and ([.omitted[].surface] | any(test("landed|snapshot layer")) | not)
1395
+ ' >/dev/null || fail "--all-landed no longer revealed the complete landed set: $json"
1396
+ pass "--all-landed keeps the complete global landed output"
1397
+ }
1398
+
1399
+ # The roll-up stays bounded: a per-home cap and an overall cap, both disclosed in
1400
+ # omitted[], with --all-landed as the counted expansion knob. This also covers the
1401
+ # previously-silent main-home landed truncation.
1402
+ test_landed_bounded_and_disclosed() {
1403
+ local home mate fakebin json i expected actual
1404
+ home=$(make_home mate-landed-caps); write_fixture "$home"
1405
+ mate=$(fixture_mate_home "$home")
1406
+ {
1407
+ printf '## In flight\n'
1408
+ printf '%s\n\n' '- [ ] mate - Decide subscription order (repo: firstmate) (kind: ship) (since 2026-07-11)'
1409
+ printf '## Done\n'
1410
+ } > "$mate/data/backlog.md"
1411
+ i=1
1412
+ while [ "$i" -le 12 ]; do
1413
+ printf -- '- [x] mate-landed-%02d - Secondmate fix %02d (repo: firstmate) (kind: ship) (merged 2026-06-%02d)\n' \
1414
+ "$i" "$i" "$((13 - i))" >> "$mate/data/backlog.md"
1415
+ i=$((i + 1))
1416
+ done
1417
+ fakebin=$(make_fakebin "$home")
1418
+ json=$(FM_BEARINGS_LANDED=20 run "$home" "$fakebin" --json)
1419
+ printf '%s' "$json" | jq -e '
1420
+ ([.landed[].id | select(startswith("mate-landed-"))] | length) == 10
1421
+ and ([.omitted[].surface] | any(test("snapshot layer")))
1422
+ ' >/dev/null || fail "default landed path must retain and disclose the snapshot per-home cap: $json"
1423
+ json=$(FM_BEARINGS_LANDED=1 run "$home" "$fakebin" --json --all-landed)
1424
+ expected=done-a
1425
+ i=1
1426
+ while [ "$i" -le 12 ]; do
1427
+ expected="$expected
1428
+ $(printf 'mate-landed-%02d' "$i")"
1429
+ i=$((i + 1))
1430
+ done
1431
+ expected=$(printf '%s\n' "$expected" | LC_ALL=C sort)
1432
+ actual=$(printf '%s' "$json" | jq -r '.landed[].id' | LC_ALL=C sort)
1433
+ [ "$actual" = "$expected" ] || fail "--all-landed returned wrong identities: $actual"
1434
+ printf '%s' "$json" | jq -e '
1435
+ (.landed | length) == 13
1436
+ and ([.omitted[].surface] | any(test("landed|snapshot layer")) | not)
1437
+ ' >/dev/null || fail "--all-landed must reveal the exact full landed set: $json"
1438
+ pass "landed stays bounded with per-home + overall caps and omitted[] disclosure"
1439
+ }
1440
+
1441
+ # Bearings projects authoritative structured state rather than inventing return
1442
+ # policy. A live blocked child remains a live in-flight record with state=blocked
1443
+ # and an open blocker; it must never be converted into a queued `gates` record.
1444
+ # The return-catch-up owner prevents this state from reaching ordinary rendering
1445
+ # during an away return, while this test pins Bearings' own projection boundary.
1446
+ test_live_blocker_is_not_charted_queue_work() {
1447
+ local home fakebin json
1448
+ home=$(make_home live-blocker); write_fixture "$home"
1449
+ printf 'blocked [key=synthetic-dependency]: firstmate can refresh the synthetic token\n' > "$home/state/ship-task.status"
1450
+ record_claude_state "$home/state" ship-task idle
1451
+ fakebin=$(make_fakebin "$home")
1452
+ json=$(run "$home" "$fakebin" --json)
1453
+ printf '%s' "$json" | jq -e '
1454
+ (.in_flight | any(.[]; .id == "ship-task" and .state == "blocked"))
1455
+ and (.decisions_open | any(.[]; .id == "ship-task") | not)
1456
+ and (.gates | any(.[]; .id == "ship-task") | not)
1457
+ ' >/dev/null || fail "live blocked work was projected as queued/deferred work: $json"
1458
+ pass "Bearings keeps a live blocker in structured live state and never converts it to Charted Next queue work"
1459
+ }
1460
+
1461
+ # Captain's Call is populated only from the durable keyed open-decision set. The
1462
+ # anti-leak guard: action-free highlights - a working task, a completed scout,
1463
+ # queued/gated items, landed work - must never surface as an open decision, so they
1464
+ # cannot leak into Captain's Call. The standard fixture has exactly one genuine open
1465
+ # decision (the secondmate's structured captain hold).
1466
+ test_captains_call_anti_leak() {
1467
+ local home fakebin json canonical
1468
+ home=$(make_home anti-leak); write_fixture "$home"
1469
+ fakebin=$(make_fakebin "$home")
1470
+ json=$(run "$home" "$fakebin" --json)
1471
+ canonical=$(PATH="$fakebin:$PATH" FM_HOME="$home" "$ROOT/bin/fm-fleet-snapshot.sh" --json)
1472
+ jq -n -e --argjson bearings "$json" --argjson canonical "$canonical" '
1473
+ ([$bearings.decisions_open[].id] == ["mate/mate-decision-race"])
1474
+ and ($canonical.secondmate_current.records[] | select(.id == "mate")
1475
+ | (.decisions_open | any(.source == "status"))
1476
+ and (.decisions_open | any(.source == "backlog")))
1477
+ and ([$bearings.decisions_open[].id] | index("ship-task") | not)
1478
+ and ([$bearings.decisions_open[].id] | index("scout-x") | not)
1479
+ and ([$bearings.decisions_open[].id] | index("external-wait") | not)
1480
+ and ([$bearings.decisions_open[].id] | index("done-a") | not)
1481
+ and ([$bearings.decisions_open[].id] | index("mate-landed") | not)
1482
+ and ([$bearings.decisions_open[].id] | index("live-gate") | not)
1483
+ and ([$bearings.decisions_open[].id] | index("dead-gate") | not)
1484
+ ' >/dev/null || fail "only genuine open decisions may feed Captain's Call: $json"
1485
+ pass "action-free items (working/done/queued/landed) do not leak into Captain's Call"
1486
+ }
1487
+
1488
+ # R1: main-home orphan in-flight and unstructured current rows must not vanish
1489
+ # silently. Meta remains the sole live-work inventory; disclosure is via
1490
+ # main_inventory + omitted[] + a Charted Next gate line, never fake Underway.
1491
+ test_main_orphan_in_flight_is_disclosed_not_invented() {
1492
+ local home fakebin json canonical
1493
+ home=$(make_home main-orphan)
1494
+ : > "$home/data/secondmates.md"
1495
+ cat > "$home/data/backlog.md" <<'EOF'
1496
+ ## In flight
1497
+ - [ ] only-orphan - Structured in flight without meta (repo: firstmate) (kind: ship) (since 2026-07-11)
1498
+
1499
+ ## Queued
1500
+
1501
+ ## Done
1502
+ EOF
1503
+ fakebin=$(make_fakebin "$home")
1504
+ canonical=$(PATH="$fakebin:$PATH" FM_HOME="$home" FM_SNAPSHOT_NOW=2026-07-11T18:00:00Z \
1505
+ "$ROOT/bin/fm-fleet-snapshot.sh" --json)
1506
+ printf '%s' "$canonical" | jq -e '
1507
+ .main_inventory.valid == false
1508
+ and .main_inventory.reason == "in-flight backlog item has no child metadata"
1509
+ and (.main_inventory.orphan_in_flight == ["only-orphan"])
1510
+ and .main_inventory.unstructured_current_count == 0
1511
+ and (.tasks | length) == 0
1512
+ ' >/dev/null || fail "canonical main inventory missed orphan: $canonical"
1513
+ json=$(run "$home" "$fakebin" --json)
1514
+ printf '%s' "$json" | jq -e '
1515
+ (.in_flight | length) == 0
1516
+ and ([.in_flight[].id] | index("only-orphan") | not)
1517
+ and ([.decisions_open[].id] | index("only-orphan") | not)
1518
+ and (.gates | any(.id == "(main-inventory)"
1519
+ and (.title | contains("in-flight backlog item has no child metadata"))))
1520
+ and (.omitted | any(.surface == "main in-flight backlog item(s) have no child metadata: 1"))
1521
+ ' >/dev/null || fail "orphan in-flight was invented or not disclosed: $json"
1522
+ pass "main orphan in-flight stays out of Underway and is disclosed in omitted/gates"
1523
+ }
1524
+
1525
+ test_main_unstructured_current_is_disclosed_with_structured_sibling() {
1526
+ local home fakebin json canonical
1527
+ home=$(make_home main-unstructured)
1528
+ : > "$home/data/secondmates.md"
1529
+ mkdir -p "$home/projects/structured-ship"
1530
+ cat > "$home/data/backlog.md" <<'EOF'
1531
+ ## In flight
1532
+ this current row is not structured
1533
+ - [ ] structured-ship - Visible structured sibling (repo: firstmate) (kind: ship) (since 2026-07-11)
1534
+
1535
+ ## Queued
1536
+ another free-form note without checkbox
1537
+ - [ ] structured-queued - Structured queued (repo: firstmate) (kind: ship)
1538
+
1539
+ ## Done
1540
+ EOF
1541
+ fm_write_meta "$home/state/structured-ship.meta" \
1542
+ "window=firstmate:fm-structured-ship" \
1543
+ "worktree=$home/projects/structured-ship" \
1544
+ "project=firstmate" \
1545
+ "harness=codex" \
1546
+ "kind=ship" \
1547
+ "mode=no-mistakes"
1548
+ printf 'working: structured sibling still projects\n' > "$home/state/structured-ship.status"
1549
+ fakebin=$(make_fakebin "$home")
1550
+ canonical=$(PATH="$fakebin:$PATH" FM_HOME="$home" FM_SNAPSHOT_NOW=2026-07-11T18:00:00Z \
1551
+ "$ROOT/bin/fm-fleet-snapshot.sh" --json)
1552
+ printf '%s' "$canonical" | jq -e '
1553
+ .main_inventory.valid == false
1554
+ and .main_inventory.reason == "unstructured current backlog row"
1555
+ and .main_inventory.unstructured_current_count == 2
1556
+ and (.main_inventory.orphan_in_flight | length) == 0
1557
+ ' >/dev/null || fail "canonical main inventory missed unstructured current: $canonical"
1558
+ json=$(run "$home" "$fakebin" --json)
1559
+ printf '%s' "$json" | jq -e '
1560
+ ([.in_flight[].id] == ["structured-ship"])
1561
+ and ([.gates[].id] | index("structured-queued") != null)
1562
+ and (.gates | any(.id == "(main-inventory)"
1563
+ and (.title | contains("unstructured current backlog row"))))
1564
+ and (.omitted | any(.surface == "main unstructured current backlog row(s): 2"))
1565
+ and ([.decisions_open[].id] | index("(main-inventory)") | not)
1566
+ ' >/dev/null || fail "unstructured current not disclosed or structured sibling lost: $json"
1567
+ pass "main unstructured current is disclosed while structured siblings still project"
1568
+ }
1569
+
1570
+ test_main_orphan_counterfactual_meta_clears_inventory_warning() {
1571
+ local home fakebin json_before json_after
1572
+ home=$(make_home main-orphan-counterfactual)
1573
+ : > "$home/data/secondmates.md"
1574
+ mkdir -p "$home/projects/orphan-ship"
1575
+ cat > "$home/data/backlog.md" <<'EOF'
1576
+ ## In flight
1577
+ - [ ] orphan-ship - Gains meta in counterfactual (repo: firstmate) (kind: ship) (since 2026-07-11)
1578
+ - [ ] visible-ship - Already live (repo: firstmate) (kind: ship) (since 2026-07-11)
1579
+
1580
+ ## Queued
1581
+ - [ ] queued-ship - Ordinary queue (repo: firstmate) (kind: ship)
1582
+
1583
+ ## Done
1584
+ EOF
1585
+ fm_write_meta "$home/state/visible-ship.meta" \
1586
+ "window=firstmate:fm-visible-ship" \
1587
+ "worktree=$home/projects/orphan-ship" \
1588
+ "project=firstmate" \
1589
+ "harness=codex" \
1590
+ "kind=ship" \
1591
+ "mode=no-mistakes"
1592
+ printf 'working: visible sibling\n' > "$home/state/visible-ship.status"
1593
+ fakebin=$(make_fakebin "$home")
1594
+ json_before=$(run "$home" "$fakebin" --json)
1595
+ printf '%s' "$json_before" | jq -e '
1596
+ ([.in_flight[].id] == ["visible-ship"])
1597
+ and ([.in_flight[].id] | index("orphan-ship") | not)
1598
+ and (.omitted | any(.surface == "main in-flight backlog item(s) have no child metadata: 1"))
1599
+ and (.gates | any(.id == "(main-inventory)"))
1600
+ and ([.gates[].id] | index("queued-ship") != null)
1601
+ ' >/dev/null || fail "pre-meta orphan fixture failed: $json_before"
1602
+ fm_write_meta "$home/state/orphan-ship.meta" \
1603
+ "window=firstmate:fm-orphan-ship" \
1604
+ "worktree=$home/projects/orphan-ship" \
1605
+ "project=firstmate" \
1606
+ "harness=codex" \
1607
+ "kind=ship" \
1608
+ "mode=no-mistakes"
1609
+ printf 'working: orphan now has meta\n' > "$home/state/orphan-ship.status"
1610
+ json_after=$(run "$home" "$fakebin" --json)
1611
+ printf '%s' "$json_after" | jq -e '
1612
+ ([.in_flight[].id] | sort) == ["orphan-ship", "visible-ship"]
1613
+ and ([.omitted[].surface] | any(test("main in-flight backlog item")) | not)
1614
+ and ([.gates[].id] | index("(main-inventory)") | not)
1615
+ and ([.decisions_open[].id] | index("orphan-ship") | not)
1616
+ ' >/dev/null || fail "adding meta did not clear inventory warning or project orphan: $json_after"
1617
+ pass "counterfactual meta clears main inventory warning and projects the live task"
1618
+ }
1619
+
1620
+ test_mixed_secondmate_roles_partial_state_and_captain_readiness() {
1621
+ local home fakebin hibit wheel sshhip ha canonical json
1622
+ home=$(make_home mixed-domain-regressions)
1623
+ : > "$home/data/secondmates.md"
1624
+ hibit="$TMP_ROOT/mixed-hibit-home"
1625
+ wheel="$TMP_ROOT/mixed-wheel-home"
1626
+ sshhip="$TMP_ROOT/mixed-sshhip-home"
1627
+ ha="$TMP_ROOT/mixed-ha-home"
1628
+ make_valid_secondmate_home hibit "$hibit"
1629
+ make_valid_secondmate_home wheel "$wheel"
1630
+ make_valid_secondmate_home sshhip "$sshhip"
1631
+ make_valid_secondmate_home home-assistant "$ha"
1632
+ append_secondmate_registry "$home" hibit "$hibit"
1633
+ append_secondmate_registry "$home" wheel "$wheel"
1634
+ append_secondmate_registry "$home" sshhip "$sshhip"
1635
+ append_secondmate_registry "$home" home-assistant "$ha"
1636
+
1637
+ mkdir -p "$hibit/projects/worker" "$wheel/projects/worker" "$sshhip/projects/child" "$ha/projects/prep"
1638
+ cat > "$hibit/data/backlog.md" <<'EOF'
1639
+ ## In flight
1640
+ - [ ] dogfood-program - Long-lived dogfood program (repo: hibit) (kind: program)
1641
+ - [ ] hibit-worker - Finalize progress (repo: hibit) (kind: ship)
1642
+
1643
+ ## Queued
1644
+
1645
+ ## Done
1646
+ EOF
1647
+ fm_write_meta "$hibit/state/hibit-worker.meta" \
1648
+ "window=firstmate:fm-hibit-worker" "worktree=$hibit/projects/worker" "project=hibit" \
1649
+ "harness=claude" "kind=ship" "mode=no-mistakes"
1650
+ record_claude_state "$hibit/state" hibit-worker busy
1651
+ printf 'working: finalizing progress\n' > "$hibit/state/hibit-worker.status"
1652
+
1653
+ cat > "$wheel/data/backlog.md" <<'EOF'
1654
+ ## In flight
1655
+ - [ ] production-observation - Observe production (repo: wheelhouse) (kind: scout) (hold: documented no live worker) (hold-kind: external)
1656
+ - [ ] wheel-worker - Initial triage (repo: wheelhouse) (kind: ship)
1657
+
1658
+ ## Queued
1659
+
1660
+ ## Done
1661
+ EOF
1662
+ fm_write_meta "$wheel/state/wheel-worker.meta" \
1663
+ "window=firstmate:fm-wheel-worker" "worktree=$wheel/projects/worker" "project=wheelhouse" \
1664
+ "harness=claude" "kind=ship" "mode=no-mistakes"
1665
+ record_claude_state "$wheel/state" wheel-worker busy
1666
+ printf 'working: active validation\n' > "$wheel/state/wheel-worker.status"
1667
+
1668
+ cat > "$sshhip/data/backlog.md" <<'EOF'
1669
+ ## In flight
1670
+ - [ ] unreadable-child - Submit App Store build (repo: sshhip) (kind: ship)
1671
+
1672
+ ## Queued
1673
+ - [ ] reviewer-decision - Choose reviewer remediation (repo: sshhip) (kind: captain) (hold: choose reviewer remediation A or B) (hold-kind: captain)
1674
+
1675
+ ## Done
1676
+ - [x] prior-release - Prior release (repo: sshhip) (kind: ship) (done 2026-07-21)
1677
+ EOF
1678
+ fm_write_meta "$sshhip/state/unreadable-child.meta" \
1679
+ "window=firstmate:dead-sshhip-child" "worktree=$sshhip/projects/child" "project=sshhip" \
1680
+ "harness=codex" "kind=ship" "mode=no-mistakes"
1681
+
1682
+ cat > "$ha/data/backlog.md" <<'EOF'
1683
+ ## In flight
1684
+ - [ ] prep - Prepare canary (repo: home-assistant) (kind: ship)
1685
+
1686
+ ## Queued
1687
+ - [ ] security - Security review (repo: home-assistant) (kind: ship)
1688
+ - [ ] captain-run - Run captain canary blocked-by: prep blocked-by: security (repo: home-assistant) (kind: captain) (hold: captain runs canary) (hold-kind: captain)
1689
+
1690
+ ## Done
1691
+ EOF
1692
+ fm_write_meta "$ha/state/prep.meta" \
1693
+ "window=firstmate:fm-prep" "worktree=$ha/projects/prep" "project=home-assistant" \
1694
+ "harness=claude" "kind=ship" "mode=no-mistakes"
1695
+ record_claude_state "$ha/state" prep busy
1696
+ printf 'working: preparing canary\n' > "$ha/state/prep.status"
1697
+
1698
+ fakebin=$(make_fakebin "$home")
1699
+ canonical=$(PATH="$fakebin:$PATH" FM_HOME="$home" FM_SNAPSHOT_NOW=2026-07-11T18:00:00Z \
1700
+ "$ROOT/bin/fm-fleet-snapshot.sh" --json)
1701
+ printf '%s' "$canonical" | jq -e '
1702
+ (.secondmate_current.records[] | select(.id == "hibit")
1703
+ | .current.state == "active_child_work"
1704
+ and [.active_children[].id] == ["hibit-worker"]
1705
+ and ([.endpoints[].id] | index("dogfood-program") | not))
1706
+ and (.secondmate_current.records[] | select(.id == "wheel")
1707
+ | .current.state == "active_child_work"
1708
+ and [.active_children[].id] == ["wheel-worker"]
1709
+ and [.queued[].id] == ["production-observation"]
1710
+ and [.holds[].id] == ["production-observation"])
1711
+ and (.secondmate_current.records[] | select(.id == "sshhip")
1712
+ | .current.state == "unknown"
1713
+ and (.current.reason | contains("child current state unavailable: unreadable-child"))
1714
+ and .provenance.selected == "structured-home"
1715
+ and .provenance.summary_valid == false
1716
+ and .provenance.trust == "partial-structured"
1717
+ and .invalidity == {kind:"child_current_unavailable",ids:["unreadable-child"]}
1718
+ and [.decisions_open[].id] == ["reviewer-decision"]
1719
+ and [.holds[].id] == ["reviewer-decision"]
1720
+ and [.queued[].id] == ["reviewer-decision"]
1721
+ and [.landed[].id] == ["prior-release"]
1722
+ and [.endpoints[].id] == ["unreadable-child"]
1723
+ and .counts.decisions_open == 1
1724
+ and .counts.holds == 1
1725
+ and .counts.queued == 1
1726
+ and .counts.landed == 1
1727
+ and .counts.endpoints == 1)
1728
+ and (.secondmate_landed.partial | length) == 1
1729
+ and (.secondmate_landed.partial[0] | endswith("/mixed-sshhip-home"))
1730
+ and (.secondmate_landed.unreadable | length) == 0
1731
+ and (.secondmate_current.records[] | select(.id == "home-assistant")
1732
+ | .current.state == "active_child_work"
1733
+ and .decisions_open == []
1734
+ and [.active_children[].id] == ["prep"]
1735
+ and (.queued[] | select(.id == "captain-run")
1736
+ | .blocked_by_ids == ["prep", "security"]
1737
+ and .unresolved_blocker_ids == ["prep", "security"]
1738
+ and .captain_actionable == false))
1739
+ ' >/dev/null || fail "canonical mixed-domain classification was wrong: $canonical"
1740
+ json=$(run "$home" "$fakebin" --json --fields bodies --all-landed)
1741
+ printf '%s' "$json" | jq -e '
1742
+ ([.in_flight[].id] | sort) == ["hibit", "home-assistant", "wheel"]
1743
+ and (.decisions_open | any(.id == "sshhip/reviewer-decision"))
1744
+ and (.decisions_open | any(.id == "home-assistant/captain-run") | not)
1745
+ and (.gates | any(.id == "production-observation" and .owner == "wheel"
1746
+ and .reason == "documented no live worker"))
1747
+ and (.gates | any(.id == "captain-run" and .owner == "home-assistant"
1748
+ and .blocked_by == "prep,security"))
1749
+ and (.secondmates | any(.id == "sshhip" and .state == "unknown"
1750
+ and (.reason | contains("unreadable-child"))))
1751
+ ' >/dev/null || fail "end-to-end mixed-domain projection was wrong: $json"
1752
+
1753
+ sed '/unreadable-child/a\
1754
+ - [ ] ordinary-orphan - Unowned release task (repo: sshhip) (kind: ship)' \
1755
+ "$sshhip/data/backlog.md" > "$sshhip/data/backlog.next"
1756
+ mv "$sshhip/data/backlog.next" "$sshhip/data/backlog.md"
1757
+ canonical=$(PATH="$fakebin:$PATH" FM_HOME="$home" FM_SNAPSHOT_NOW=2026-07-11T18:00:00Z \
1758
+ "$ROOT/bin/fm-fleet-snapshot.sh" --json)
1759
+ printf '%s' "$canonical" | jq -e '
1760
+ .secondmate_current.records[] | select(.id == "sshhip")
1761
+ | .current.state == "unknown"
1762
+ and (.current.reason | contains("in-flight backlog item has no child metadata: ordinary-orphan"))
1763
+ and .provenance.selected == "structured-home"
1764
+ and .provenance.trust == "partial-structured"
1765
+ and .invalidity == {kind:"orphan_in_flight",ids:["ordinary-orphan"]}
1766
+ and [.decisions_open[].id] == ["reviewer-decision"]
1767
+ and [.holds[].id] == ["reviewer-decision"]
1768
+ and [.queued[].id] == ["reviewer-decision"]
1769
+ and [.landed[].id] == ["prior-release"]
1770
+ and [.endpoints[].id] == ["unreadable-child"]
1771
+ ' >/dev/null || fail "an ordinary orphan discarded a readable home alongside an unknown child: $canonical"
1772
+ sed '/ordinary-orphan/d' "$sshhip/data/backlog.md" > "$sshhip/data/backlog.next"
1773
+ mv "$sshhip/data/backlog.next" "$sshhip/data/backlog.md"
1774
+
1775
+ sed '/unreadable-child/d' "$sshhip/data/backlog.md" > "$sshhip/data/backlog.next"
1776
+ mv "$sshhip/data/backlog.next" "$sshhip/data/backlog.md"
1777
+ canonical=$(PATH="$fakebin:$PATH" FM_HOME="$home" FM_SNAPSHOT_NOW=2026-07-11T18:00:00Z \
1778
+ "$ROOT/bin/fm-fleet-snapshot.sh" --json)
1779
+ printf '%s' "$canonical" | jq -e '
1780
+ .secondmate_current.records[] | select(.id == "sshhip")
1781
+ | .current.state == "unknown"
1782
+ and (.current.reason | contains("live child state has no in-flight backlog item: unreadable-child=unknown"))
1783
+ and .provenance.selected == "structured-home"
1784
+ and .provenance.trust == "partial-structured"
1785
+ and .invalidity == {kind:"unowned_current",ids:["unreadable-child"]}
1786
+ and [.decisions_open[].id] == ["reviewer-decision"]
1787
+ and [.holds[].id] == ["reviewer-decision"]
1788
+ and [.queued[].id] == ["reviewer-decision"]
1789
+ and [.landed[].id] == ["prior-release"]
1790
+ ' >/dev/null || fail "an unowned unknown child discarded the readable home: $canonical"
1791
+ sed '/## In flight/a\
1792
+ - [ ] unreadable-child - Submit App Store build (repo: sshhip) (kind: ship)' \
1793
+ "$sshhip/data/backlog.md" > "$sshhip/data/backlog.next"
1794
+ mv "$sshhip/data/backlog.next" "$sshhip/data/backlog.md"
1795
+
1796
+ fm_write_meta "$wheel/state/production-observation.meta" \
1797
+ "window=firstmate:fm-production-observation" "worktree=$wheel/projects/worker" "project=wheelhouse" \
1798
+ "harness=claude" "kind=scout" "mode=scout"
1799
+ record_claude_state "$wheel/state" production-observation idle
1800
+ printf 'paused: observation is deliberately held\n' > "$wheel/state/production-observation.status"
1801
+ canonical=$(PATH="$fakebin:$PATH" FM_HOME="$home" FM_SNAPSHOT_NOW=2026-07-11T18:00:00Z \
1802
+ "$ROOT/bin/fm-fleet-snapshot.sh" --json)
1803
+ printf '%s' "$canonical" | jq -e '
1804
+ .secondmate_current.records[] | select(.id == "wheel")
1805
+ | ([.queued[] | select(.id == "production-observation")] | length) == 1
1806
+ and ([.holds[] | select(.id == "production-observation")] | length) == 1
1807
+ and ([.endpoints[] | select(.id == "production-observation")] | length) == 1
1808
+ ' >/dev/null || fail "held metadata plus a real child duplicated or discarded the record: $canonical"
1809
+
1810
+ fm_write_meta "$sshhip/state/unreadable-child.meta" \
1811
+ "window=firstmate:fm-unreadable-child" "worktree=$sshhip/projects/child" "project=sshhip" \
1812
+ "harness=claude" "kind=ship" "mode=no-mistakes"
1813
+ record_claude_state "$sshhip/state" unreadable-child busy
1814
+ printf 'working: app store submission restored\n' > "$sshhip/state/unreadable-child.status"
1815
+ json=$(run "$home" "$fakebin" --json)
1816
+ printf '%s' "$json" | jq -e '
1817
+ (.secondmates | any(.id == "sshhip" and .state == "captain_decision" and .reason == "-"))
1818
+ and ([.decisions_open[] | select(.id == "sshhip/reviewer-decision")] | length) == 1
1819
+ ' >/dev/null || fail "restoring the SSHHIP child did not clear only its narrow warning: $json"
1820
+
1821
+ cat > "$ha/data/backlog.md" <<'EOF'
1822
+ ## In flight
1823
+
1824
+ ## Queued
1825
+ - [ ] security - Security review (repo: home-assistant) (kind: ship)
1826
+ - [ ] captain-run - Run captain canary blocked-by: prep blocked-by: security (repo: home-assistant) (kind: captain) (hold: captain runs canary) (hold-kind: captain)
1827
+
1828
+ ## Done
1829
+ - [x] prep - Prepare canary (repo: home-assistant) (kind: ship) (done 2026-07-22)
1830
+ EOF
1831
+ rm "$ha/state/prep.meta" "$ha/state/prep.status"
1832
+ json=$(run "$home" "$fakebin" --json)
1833
+ printf '%s' "$json" | jq -e '
1834
+ (.decisions_open | any(.id == "home-assistant/captain-run") | not)
1835
+ and (.gates | any(.id == "captain-run" and .owner == "home-assistant" and .blocked_by == "security"))
1836
+ ' >/dev/null || fail "one remaining Home Assistant blocker became actionable: $json"
1837
+
1838
+ cat > "$ha/data/backlog.md" <<'EOF'
1839
+ ## In flight
1840
+
1841
+ ## Queued
1842
+ - [ ] captain-run - Run captain canary blocked-by: prep blocked-by: security (repo: home-assistant) (kind: captain) (hold: captain runs canary) (hold-kind: captain)
1843
+
1844
+ ## Done
1845
+ - [x] prep - Prepare canary (repo: home-assistant) (kind: ship) (done 2026-07-22)
1846
+ - [x] security - Security review (repo: home-assistant) (kind: ship) (done 2026-07-22)
1847
+ EOF
1848
+ json=$(run "$home" "$fakebin" --json)
1849
+ printf '%s' "$json" | jq -e '
1850
+ ([.decisions_open[] | select(.id == "home-assistant/captain-run")] | length) == 1
1851
+ and (.gates | any(.id == "captain-run" and .owner == "home-assistant") | not)
1852
+ ' >/dev/null || fail "zero Home Assistant blockers did not yield exactly one captain action: $json"
1853
+
1854
+ sed 's/blocked-by: security/blocked-by: missing/' "$ha/data/backlog.md" > "$ha/data/backlog.next"
1855
+ mv "$ha/data/backlog.next" "$ha/data/backlog.md"
1856
+ json=$(run "$home" "$fakebin" --json)
1857
+ printf '%s' "$json" | jq -e '
1858
+ (.decisions_open | any(.id == "home-assistant/captain-run") | not)
1859
+ and (.gates | any(.id == "captain-run" and .owner == "home-assistant" and .blocked_by == "missing"))
1860
+ ' >/dev/null || fail "a missing Home Assistant blocker was treated as Done: $json"
1861
+
1862
+ sed 's/(kind: program)/(kind: mystery)/' "$hibit/data/backlog.md" > "$hibit/data/backlog.next"
1863
+ mv "$hibit/data/backlog.next" "$hibit/data/backlog.md"
1864
+ canonical=$(PATH="$fakebin:$PATH" FM_HOME="$home" FM_SNAPSHOT_NOW=2026-07-11T18:00:00Z \
1865
+ "$ROOT/bin/fm-fleet-snapshot.sh" --json)
1866
+ printf '%s' "$canonical" | jq -e '
1867
+ .secondmate_current.records[] | select(.id == "hibit")
1868
+ | .current.state == "active_child_work"
1869
+ and (.current.reason | contains("in-flight backlog item has no child metadata: dogfood-program"))
1870
+ and .provenance.selected == "structured-home"
1871
+ and .provenance.trust == "partial-structured"
1872
+ and .invalidity == {kind:"orphan_in_flight",ids:["dogfood-program"]}
1873
+ and [.active_children[].id] == ["hibit-worker"]
1874
+ and [.endpoints[].id] == ["hibit-worker"]
1875
+ ' >/dev/null || fail "an unrecognized worker kind hid the home's live work: $canonical"
1876
+ pass "mixed secondmate roles, partial state, and captain readiness project independently"
1877
+ }
1878
+
1879
+ test_main_captain_readiness_matches_secondmate_projection() {
1880
+ local home fakebin json
1881
+ home=$(make_home main-captain-readiness)
1882
+ : > "$home/data/secondmates.md"
1883
+ mkdir -p "$home/projects/prep" "$home/projects/observation"
1884
+ cat > "$home/data/backlog.md" <<'EOF'
1885
+ ## In flight
1886
+ - [ ] observation - Held observation (repo: firstmate) (kind: scout) (hold: watch production) (hold-kind: external)
1887
+ - [ ] prep - Prepare canary (repo: firstmate) (kind: ship)
1888
+
1889
+ ## Queued
1890
+ - [ ] review - Security review (repo: firstmate) (kind: ship)
1891
+ - [ ] captain-run - Run captain canary blocked-by: prep blocked-by: review (repo: firstmate) (kind: captain) (hold: captain runs canary) (hold-kind: captain)
1892
+
1893
+ ## Done
1894
+ EOF
1895
+ fm_write_meta "$home/state/prep.meta" \
1896
+ "window=firstmate:fm-prep" "worktree=$home/projects/prep" "project=firstmate" \
1897
+ "harness=codex" "kind=ship" "mode=no-mistakes"
1898
+ printf 'working: preparing main canary\n' > "$home/state/prep.status"
1899
+ fm_write_meta "$home/state/observation.meta" \
1900
+ "window=firstmate:fm-observation" "worktree=$home/projects/observation" "project=firstmate" \
1901
+ "harness=codex" "kind=scout" "mode=scout"
1902
+ printf 'paused: observation is deliberately held\n' > "$home/state/observation.status"
1903
+ fakebin=$(make_fakebin "$home")
1904
+ json=$(run "$home" "$fakebin" --json)
1905
+ printf '%s' "$json" | jq -e '
1906
+ (.in_flight | any(.id == "prep"))
1907
+ and (.in_flight | any(.id == "observation") | not)
1908
+ and ([.gates[] | select(.id == "observation" and .reason == "watch production")] | length) == 1
1909
+ and (.decisions_open | any(.id == "captain-run") | not)
1910
+ and (.gates | any(.id == "captain-run" and .blocked_by == "prep,review"))
1911
+ ' >/dev/null || fail "main blocked captain action or held-child projection was wrong: $json"
1912
+
1913
+ cat > "$home/data/backlog.md" <<'EOF'
1914
+ ## In flight
1915
+
1916
+ ## Queued
1917
+ - [ ] review - Security review (repo: firstmate) (kind: ship)
1918
+ - [ ] captain-run - Run captain canary blocked-by: prep blocked-by: review (repo: firstmate) (kind: captain) (hold: captain runs canary) (hold-kind: captain)
1919
+
1920
+ ## Done
1921
+ - [x] prep - Prepare canary (repo: firstmate) (kind: ship) (done 2026-07-22)
1922
+ EOF
1923
+ rm "$home/state/prep.meta" "$home/state/prep.status" \
1924
+ "$home/state/observation.meta" "$home/state/observation.status"
1925
+ json=$(run "$home" "$fakebin" --json)
1926
+ printf '%s' "$json" | jq -e '
1927
+ (.decisions_open | any(.id == "captain-run") | not)
1928
+ and (.gates | any(.id == "captain-run" and .blocked_by == "review"))
1929
+ ' >/dev/null || fail "main one-blocker captain action became premature: $json"
1930
+
1931
+ cat > "$home/data/backlog.md" <<'EOF'
1932
+ ## In flight
1933
+
1934
+ ## Queued
1935
+ - [ ] captain-run - Run captain canary blocked-by: prep blocked-by: review (repo: firstmate) (kind: captain) (hold: captain runs canary) (hold-kind: captain)
1936
+
1937
+ ## Done
1938
+ - [x] prep - Prepare canary (repo: firstmate) (kind: ship) (done 2026-07-22)
1939
+ - [x] review - Security review (repo: firstmate) (kind: ship) (done 2026-07-22)
1940
+ EOF
1941
+ json=$(run "$home" "$fakebin" --json)
1942
+ printf '%s' "$json" | jq -e '
1943
+ ([.decisions_open[] | select(.id == "captain-run")] | length) == 1
1944
+ and (.gates | any(.id == "captain-run") | not)
1945
+ ' >/dev/null || fail "main zero-blocker captain action was not projected exactly once: $json"
1946
+ pass "main and secondmate captain actionability use the same blocker readiness"
1947
+ }
1948
+
1949
+ test_domain_alpha_stale_parent_event_does_not_become_current_work
1950
+ test_gnu_stat_uses_file_formats_without_bsd_fallback_pollution
1951
+ test_parent_activity_evidence_is_bounded_and_disclosed
1952
+ test_active_child_overrides_old_parent_event
1953
+ test_structured_child_decision_reaches_captains_call
1954
+ test_bad_secondmate_homes_never_revive_parent_work
1955
+ test_oversized_secondmate_summary_stays_strict_unknown
1956
+ test_secondmate_and_child_bounds_are_disclosed
1957
+ test_parent_decision_is_untrusted_contradiction_only
1958
+ test_parent_evidence_reconciles_by_verb_and_key
1959
+ test_nonprogressing_child_states_are_explicit
1960
+ test_registry_unavailability_and_bounds_are_explicit
1961
+ test_current_landed_baseline_is_repeatable_and_prior_report_independent
1962
+ test_default_is_bounded_and_local_only
1963
+ test_toon_json_parity
1964
+ test_landed_includes_secondmate_home_merges
1965
+ test_landed_default_balances_dominant_and_sparse_homes
1966
+ test_landed_default_refills_capacity_after_sparse_homes_exhaust
1967
+ test_landed_default_uses_deterministic_home_order_when_homes_exceed_cap
1968
+ test_landed_default_preserves_internal_order_for_ties
1969
+ test_landed_default_handles_no_landed_items
1970
+ test_all_landed_keeps_complete_global_order
1971
+ test_landed_bounded_and_disclosed
1972
+ test_live_blocker_is_not_charted_queue_work
1973
+ test_captains_call_anti_leak
1974
+ test_main_orphan_in_flight_is_disclosed_not_invented
1975
+ test_main_unstructured_current_is_disclosed_with_structured_sibling
1976
+ test_main_orphan_counterfactual_meta_clears_inventory_warning
1977
+ test_mixed_secondmate_roles_partial_state_and_captain_readiness
1978
+ test_main_captain_readiness_matches_secondmate_projection
1979
+ test_completed_scout_report_not_pending
1980
+ test_open_decision_surfaces_end_to_end
1981
+ test_report_pointers_surface
1982
+ test_superseded_queued_item_dropped_by_default
1983
+ test_include_prs_is_the_only_fetch_path
1984
+ test_partial_github_failure_degrades
1985
+ test_perl_fallback_bounds_github_call
1986
+ test_section_caps_and_expansion_flags
1987
+ test_collapsed_captain_call_deferral_and_landed
1988
+ test_pr_repository_cap_and_expansion
1989
+ test_per_repository_pr_cap_is_disclosed
1990
+ test_projection_and_toon_fail_closed