@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,2515 @@
1
+ #!/usr/bin/env bash
2
+ # tests/fm-session-start.test.sh - behavior tests for bin/fm-session-start.sh,
3
+ # the single command that collapses AGENTS.md sections 3 (bootstrap) and 5
4
+ # (recovery) into one ordered digest.
5
+ #
6
+ # Coverage:
7
+ # - absent-file markers vs empty-but-present files in the context digest
8
+ # - the lock-refusal read-only path: banner leads, every mutating step is
9
+ # skipped (including bootstrap's seven mutating sweeps, verified by their
10
+ # ABSENCE), the digest still completes
11
+ # - output section ordering: the safety preamble leads unchanged, live fleet
12
+ # state precedes the curated memory a truncated tail may take, and the
13
+ # read-once contract precedes both
14
+ # - context-aware next-step guidance for read-only, AFK, X mode, and normal
15
+ # watcher ownership
16
+ # - status-tail bounding, default and FM_SESSION_START_STATUS_TAIL override
17
+ # - the per-line status-tail cap and its truncation marker
18
+ # - startup backlog composition: done rows dropped, every in-flight/held/
19
+ # blocked row kept whole, the dispatchable queued listing bounded with an
20
+ # exact disclosed remainder
21
+ # - orphan status logs whose task meta has already disappeared
22
+ # - per-task endpoint-liveness lines for a live and a dead recorded target,
23
+ # tmux and herdr both
24
+ # - composition: the script invokes the real fm-lock.sh/fm-bootstrap.sh/
25
+ # fm-wake-drain.sh (their real, distinctive output appears verbatim), it
26
+ # does not reimplement their logic
27
+ # - the deferred network stage: an unreachable host delays a reported check
28
+ # rather than the digest, the sweeps it defers still run and land, a result
29
+ # surfaces exactly once (inline or as a wake, never both), a read-only
30
+ # session declares the checks it skipped, and the tasks-axi compatibility
31
+ # verdict is paid for once per session start
32
+ set -u
33
+
34
+ # shellcheck source=tests/lib.sh
35
+ . "$(dirname "${BASH_SOURCE[0]}")/lib.sh"
36
+ # shellcheck source=tests/wake-helpers.sh
37
+ . "$(dirname "${BASH_SOURCE[0]}")/wake-helpers.sh"
38
+
39
+ SESSION_START="$ROOT/bin/fm-session-start.sh"
40
+ BASE_PATH=${FM_TEST_BASE_PATH:-/usr/bin:/bin:/usr/sbin:/sbin}
41
+ TMP_ROOT=$(fm_test_tmproot fm-session-start-tests)
42
+ SESSION_START_TEST_HARNESS_PID=$$
43
+ SESSION_START_SECOND_MATE_ID="fmtest-sm-${TMP_ROOT##*.}"
44
+ SESSION_START_SECOND_MATE_TMP="/tmp/fm-$SESSION_START_SECOND_MATE_ID"
45
+ SESSION_START_HERDR_SECOND_MATE_ID="fmtest-herdr-${TMP_ROOT##*.}"
46
+ SESSION_START_HERDR_SECOND_MATE_TMP="/tmp/fm-$SESSION_START_HERDR_SECOND_MATE_ID"
47
+ FM_TEST_CLEANUP_DIRS+=("$TMP_ROOT" "$SESSION_START_SECOND_MATE_TMP" "$SESSION_START_HERDR_SECOND_MATE_TMP")
48
+ trap fm_test_cleanup EXIT
49
+ fm_git_identity fmtest fmtest@example.invalid
50
+
51
+ # --- world builders ----------------------------------------------------------
52
+
53
+ # new_world <name>: a real, throwaway git repo on `main` (so the worktree-tangle
54
+ # and default-branch checks behave exactly as they do against the real
55
+ # firstmate repo) to use as FM_ROOT_OVERRIDE, plus an empty FM_HOME with
56
+ # state/, data/, config/, and a fakebin. Echoes "<root-dir>|<home-dir>|<fakebin>".
57
+ new_world() {
58
+ local name=$1 w root home fakebin
59
+ w="$TMP_ROOT/$name"
60
+ root="$w/root"
61
+ home="$w/home"
62
+ fakebin="$w/fakebin"
63
+ mkdir -p "$home/state" "$home/data" "$home/config" "$fakebin"
64
+ git init -q -b main "$root"
65
+ git -C "$root" commit -q --allow-empty -m init
66
+ printf '%s|%s|%s\n' "$root" "$home" "$fakebin"
67
+ }
68
+
69
+ # make_fake_toolchain <fakebin>: every tool fm-bootstrap.sh detects, present
70
+ # and compatible, so its own detect-only section stays quiet except where a
71
+ # test deliberately breaks one. Mirrors fm-bootstrap.test.sh's fixture.
72
+ make_fake_toolchain() {
73
+ local fakebin=$1
74
+ fm_fake_exit0 "$fakebin" tmux node chrome-devtools-axi
75
+ fm_fake_version_tool "$fakebin" lavish-axi FM_FAKE_LAVISH_AXI_VERSION 0.1.46
76
+ cat > "$fakebin/gh-axi" <<'SH'
77
+ #!/usr/bin/env bash
78
+ if [ "${1:-}" = --version ]; then
79
+ printf '%s\n' '0.1.29'
80
+ exit 0
81
+ fi
82
+ exit 0
83
+ SH
84
+ chmod +x "$fakebin/gh-axi"
85
+ cat > "$fakebin/gh" <<'SH'
86
+ #!/usr/bin/env bash
87
+ exit 0
88
+ SH
89
+ chmod +x "$fakebin/gh"
90
+ cat > "$fakebin/treehouse" <<'SH'
91
+ #!/usr/bin/env bash
92
+ if [ "${1:-}" = get ] && [ "${2:-}" = --help ]; then
93
+ printf '%s\n' 'Usage: treehouse get [--lease]'
94
+ exit 0
95
+ fi
96
+ exit 0
97
+ SH
98
+ chmod +x "$fakebin/treehouse"
99
+ cat > "$fakebin/no-mistakes" <<'SH'
100
+ #!/usr/bin/env bash
101
+ if [ "${1:-}" = --version ]; then
102
+ printf '%s\n' 'no-mistakes version v1.46.0 (fake) 2026-06-27T00:02:18Z'
103
+ exit 0
104
+ fi
105
+ exit 0
106
+ SH
107
+ chmod +x "$fakebin/no-mistakes"
108
+ printf '%s\n' manual > "${fakebin%/*}/home-placeholder" 2>/dev/null || true
109
+ }
110
+
111
+ # make_fake_tasks_axi_compact <fakebin>: a tasks-axi boundary that answers the
112
+ # four group filters the startup listing composes (in-flight, held, blocked
113
+ # queued, and the dispatchable ready set) and REFUSES anything the recovery
114
+ # listing must never ask for: a body field, an unfiltered whole-backlog listing,
115
+ # or done rows. FM_FAKE_TASKS_AXI_READY sizes the ready set so the queued bound
116
+ # can be driven past its limit.
117
+ make_fake_tasks_axi_compact() {
118
+ local fakebin=$1
119
+ cat > "$fakebin/tasks-axi" <<'SH'
120
+ #!/usr/bin/env bash
121
+ set -u
122
+ log=${FM_FAKE_TASKS_AXI_LOG:-}
123
+ [ -n "$log" ] && printf '%s\n' "$*" >> "$log"
124
+ ready_count=${FM_FAKE_TASKS_AXI_READY:-2}
125
+ require_file() {
126
+ case "$*" in *'--file '*) return 0 ;; esac
127
+ printf '%s\n' 'missing explicit backlog file' >&2
128
+ exit 9
129
+ }
130
+ task_header() {
131
+ printf 'count: %s\n' "$1"
132
+ printf 'tasks[%s]{id,state,kind,repo,title,blocked_by,hold_kind,hold_reason}:\n' "$1"
133
+ }
134
+ list_help() {
135
+ printf 'help[1]:\n'
136
+ printf '%s\n' ' - Run `tasks-axi show <id> --full` for full notes on a task'
137
+ }
138
+ case "${1:-}" in
139
+ --version|-v|-V)
140
+ printf '%s\n' '0.2.4'
141
+ exit 0
142
+ ;;
143
+ update)
144
+ if [ "${2:-}" = --help ]; then
145
+ printf '%s\n' 'usage: tasks-axi update <id> [--archive-body]'
146
+ exit 0
147
+ fi
148
+ ;;
149
+ mv)
150
+ if [ "${2:-}" = --help ]; then
151
+ printf '%s\n' 'usage: tasks-axi mv <dest> [<id>...]'
152
+ exit 0
153
+ fi
154
+ ;;
155
+ ready)
156
+ require_file "$@"
157
+ printf 'count: %s\n' "$ready_count"
158
+ printf 'ready[%s]{id,state,kind,repo,title}:\n' "$ready_count"
159
+ i=1
160
+ while [ "$i" -le "$ready_count" ]; do
161
+ printf ' ready-%s,queued,ship,firstmate,Ready item %s\n' "$i" "$i"
162
+ i=$((i + 1))
163
+ done
164
+ printf 'ready_public_followups: 0 delivery-ready obligations\n'
165
+ printf 'help[1]:\n'
166
+ printf '%s\n' ' - Run `tasks-axi start <id>` to dispatch one of these'
167
+ exit 0
168
+ ;;
169
+ list)
170
+ case "$*" in
171
+ *'--fields '*'body'*|*'--fields='*'body'*)
172
+ printf '%s\n' 'unexpected body field requested' >&2
173
+ exit 9
174
+ ;;
175
+ esac
176
+ require_file "$@"
177
+ case "$*" in
178
+ *'--state done'*)
179
+ printf '%s\n' 'startup recovery must never list done rows' >&2
180
+ exit 9
181
+ ;;
182
+ *'--state in_flight'*)
183
+ task_header 1
184
+ printf '%s\n' ' compact-startup,in_flight,ship,firstmate,Compact startup digest,none,captain,captain choice pending'
185
+ ;;
186
+ *'--state held'*)
187
+ task_header 1
188
+ printf '%s\n' ' held-queued,queued,ship,firstmate,Held queued work,none,captain,captain choice pending'
189
+ ;;
190
+ *'--state queued'*'--blocked'*)
191
+ task_header 1
192
+ printf '%s\n' ' blocked-followup,queued,scout,firstmate,Follow compact startup,compact-startup,"-","-"'
193
+ ;;
194
+ *)
195
+ printf '%s\n' 'startup recovery must not request an unfiltered whole-backlog listing' >&2
196
+ exit 9
197
+ ;;
198
+ esac
199
+ list_help
200
+ exit 0
201
+ ;;
202
+ esac
203
+ exit 1
204
+ SH
205
+ chmod +x "$fakebin/tasks-axi"
206
+ }
207
+
208
+ # make_fake_ps_claude <fakebin>: harness_pid()/holder_alive() (fm-lock.sh) walk
209
+ # `ps` output looking for a harness command name; this fake reports EVERY
210
+ # queried pid as a live `claude` harness unless a stable harness pid is set.
211
+ make_fake_ps_claude() {
212
+ local fakebin=$1
213
+ make_fake_ps_harness "$fakebin" claude
214
+ }
215
+
216
+ make_fake_ps_harness() {
217
+ local fakebin=$1 harness=$2
218
+ cat > "$fakebin/ps" <<'SH'
219
+ #!/usr/bin/env bash
220
+ set -u
221
+ harness=${FM_FAKE_HARNESS:-claude}
222
+ pid=
223
+ previous=
224
+ for argument in "$@"; do
225
+ [ "$previous" = -p ] && pid=$argument
226
+ previous=$argument
227
+ done
228
+ case "$*" in
229
+ *"comm="*)
230
+ if [ -z "${FM_FAKE_HARNESS_PID:-}" ] || [ "$pid" = "$FM_FAKE_HARNESS_PID" ] \
231
+ || [ "$pid" = "${FM_FAKE_LIVE_HOLDER_PID:-}" ]; then
232
+ printf '/usr/local/bin/%s\n' "$harness"
233
+ else
234
+ printf '/bin/bash\n'
235
+ fi
236
+ exit 0
237
+ ;;
238
+ *"args="*)
239
+ if [ -z "${FM_FAKE_HARNESS_PID:-}" ] || [ "$pid" = "$FM_FAKE_HARNESS_PID" ] \
240
+ || [ "$pid" = "${FM_FAKE_LIVE_HOLDER_PID:-}" ]; then
241
+ printf '%s\n' "$harness"
242
+ else
243
+ printf 'bash\n'
244
+ fi
245
+ exit 0
246
+ ;;
247
+ *"ppid="*)
248
+ [ -n "${FM_FAKE_HARNESS_PID:-}" ] || exit 1
249
+ /bin/ps -o ppid= -p "$pid"
250
+ ;;
251
+ esac
252
+ exit 1
253
+ SH
254
+ chmod +x "$fakebin/ps"
255
+ printf '%s\n' "$harness" > "$fakebin/.harness-name"
256
+ }
257
+
258
+ make_fake_ps_pi_holder() {
259
+ local fakebin=$1 holder_pid=$2
260
+ cat > "$fakebin/ps" <<SH
261
+ #!/usr/bin/env bash
262
+ set -u
263
+ pid=""
264
+ prev=""
265
+ for arg in "\$@"; do
266
+ [ "\$prev" = "-p" ] && pid="\$arg"
267
+ prev="\$arg"
268
+ done
269
+ case "\$*" in
270
+ *"comm="*)
271
+ if [ "\$pid" = "$holder_pid" ]; then
272
+ printf '/usr/local/bin/pi\n'
273
+ else
274
+ printf '/bin/zsh\n'
275
+ fi
276
+ exit 0
277
+ ;;
278
+ *"args="*)
279
+ if [ "\$pid" = "$holder_pid" ]; then
280
+ printf 'pi\n'
281
+ else
282
+ printf 'zsh\n'
283
+ fi
284
+ exit 0
285
+ ;;
286
+ *"ppid="*) printf '%s\n' "$holder_pid"; exit 0 ;;
287
+ esac
288
+ exit 1
289
+ SH
290
+ chmod +x "$fakebin/ps"
291
+ }
292
+
293
+ # make_fake_tmux <fakebin> <live-target>: display-message succeeds only for
294
+ # the given "session:window" target - the exact primitive
295
+ # fm_backend_target_exists uses for a tmux endpoint liveness read.
296
+ make_fake_tmux() {
297
+ local fakebin=$1 live=$2
298
+ cat > "$fakebin/tmux" <<SH
299
+ #!/usr/bin/env bash
300
+ set -u
301
+ case "\${1:-}" in
302
+ display-message)
303
+ target=""
304
+ prev=""
305
+ for a in "\$@"; do
306
+ [ "\$prev" = "-t" ] && target="\$a"
307
+ prev="\$a"
308
+ done
309
+ [ "\$target" = "$live" ] && { printf '%%1\n'; exit 0; }
310
+ exit 1
311
+ ;;
312
+ esac
313
+ exit 1
314
+ SH
315
+ chmod +x "$fakebin/tmux"
316
+ }
317
+
318
+ # make_fake_tmux_secondmate_recovery <fakebin>: a stateful tmux boundary
319
+ # fixture for the real session-start -> bootstrap -> spawn path.
320
+ # FM_FAKE_TMUX_MODE selects missing, ambiguous, unreadable, or shell; missing
321
+ # reproduces real tmux's active-window fallback while inventory omits the mate.
322
+ make_fake_tmux_secondmate_recovery() {
323
+ local fakebin=$1
324
+ cat > "$fakebin/tmux" <<'SH'
325
+ #!/usr/bin/env bash
326
+ set -u
327
+ mode=${FM_FAKE_TMUX_MODE:?}
328
+ log=${FM_FAKE_TMUX_LOG:?}
329
+ spawned=${FM_FAKE_TMUX_SPAWNED:?}
330
+ killed=${spawned}.killed
331
+ mate_home=${FM_FAKE_SECOND_MATE_HOME:?}
332
+ mate_id=${FM_FAKE_SECOND_MATE_ID:?}
333
+ mate_window="fm-$mate_id"
334
+ case "${1:-}" in
335
+ display-message)
336
+ target=
337
+ format=
338
+ prev=
339
+ for arg in "$@"; do
340
+ [ "$prev" = -t ] && target=$arg
341
+ prev=$arg
342
+ case "$arg" in '#{'*) format=$arg ;; esac
343
+ done
344
+ if [ "${target#%}" != "$target" ]; then
345
+ case "$format" in
346
+ *pane_current_path*) printf '%s\n' "$mate_home" ;;
347
+ *pane_current_command*) printf '%s\n' node ;;
348
+ *) printf '%s\n' "$target" ;;
349
+ esac
350
+ exit 0
351
+ fi
352
+ if [ -e "$spawned" ]; then
353
+ case "$format" in
354
+ *pane_current_command*) printf '%s\n' node ;;
355
+ *) printf '%%1\n' ;;
356
+ esac
357
+ exit 0
358
+ fi
359
+ case "$mode" in
360
+ ambiguous)
361
+ case "$format" in *pane_current_command*) printf '%s\n' node ;; *) printf '%%1\n' ;; esac
362
+ exit 0
363
+ ;;
364
+ shell)
365
+ case "$format" in *pane_current_command*) printf '%s\n' zsh ;; *) printf '%%1\n' ;; esac
366
+ exit 0
367
+ ;;
368
+ missing)
369
+ case "$format" in *pane_current_command*) printf '%s\n' node ;; *) printf '%%fallback\n' ;; esac
370
+ exit 0
371
+ ;;
372
+ unreadable) exit 1 ;;
373
+ esac
374
+ ;;
375
+ list-windows)
376
+ if [ "$mode" = unreadable ] && [ ! -e "$spawned" ] && [ ! -e "$killed" ]; then
377
+ exit 1
378
+ fi
379
+ if [ -e "$spawned" ]; then
380
+ printf '%s\n' "$mate_window"
381
+ elif [ ! -e "$killed" ] && { [ "$mode" = ambiguous ] || [ "$mode" = shell ]; }; then
382
+ printf '%s\n' "$mate_window"
383
+ else
384
+ printf '%s\n' main
385
+ fi
386
+ exit 0
387
+ ;;
388
+ has-session) exit 0 ;;
389
+ kill-window)
390
+ printf '%s\n' "$*" >> "$log"
391
+ : > "$killed"
392
+ exit 0
393
+ ;;
394
+ new-window)
395
+ printf '%s\n' "$*" >> "$log"
396
+ : > "$spawned"
397
+ printf '%%1\n'
398
+ exit 0
399
+ ;;
400
+ set-window-option|send-keys) exit 0 ;;
401
+ esac
402
+ exit 0
403
+ SH
404
+ chmod +x "$fakebin/tmux"
405
+ }
406
+
407
+ make_fake_herdr_secondmate_recovery() {
408
+ local fakebin=$1
409
+ # The recovery kill now requires the shared named-session lock and an exact
410
+ # focus snapshot. Keep a focused sibling tab so this test's husk close is
411
+ # provably non-workspace-emptying and never needs to signal a fake shell pid.
412
+ cat > "$fakebin/herdr" <<'SH'
413
+ #!/usr/bin/env bash
414
+ set -u
415
+ log=${FM_FAKE_HERDR_LOG:?}
416
+ state=${FM_FAKE_HERDR_STATE:?}
417
+ mate_id=${FM_FAKE_SECOND_MATE_ID:?}
418
+ killed="${state}.killed"
419
+ spawned="${state}.spawned"
420
+ printf '%s\n' "$*" >> "$log"
421
+ case "${1:-} ${2:-}" in
422
+ "status --json")
423
+ printf '%s\n' '{"client":{"protocol":14,"version":"test"},"server":{"running":true}}'
424
+ ;;
425
+ "session list")
426
+ printf '{"sessions":[{"name":"default","running":true,"socket_path":"%s.sock"}]}\n' "$state"
427
+ ;;
428
+ "workspace list")
429
+ printf '{"result":{"workspaces":[{"workspace_id":"ws1","label":"2ndmate-%s","focused":true,"active_tab_id":"t-focus"}]}}\n' "$mate_id"
430
+ ;;
431
+ "tab list")
432
+ if [ -e "$spawned" ]; then
433
+ printf '{"result":{"tabs":[{"tab_id":"t-focus","workspace_id":"ws1","label":"captain","focused":true},{"tab_id":"t-new","workspace_id":"ws1","label":"fm-%s","focused":false}]}}\n' "$mate_id"
434
+ elif [ -e "$killed" ]; then
435
+ printf '%s\n' '{"result":{"tabs":[{"tab_id":"t-focus","workspace_id":"ws1","label":"captain","focused":true}]}}'
436
+ else
437
+ printf '{"result":{"tabs":[{"tab_id":"t-focus","workspace_id":"ws1","label":"captain","focused":true},{"tab_id":"t-old","workspace_id":"ws1","label":"fm-%s","focused":false}]}}\n' "$mate_id"
438
+ fi
439
+ ;;
440
+ "tab create")
441
+ : > "$spawned"
442
+ printf '%s\n' '{"result":{"tab":{"tab_id":"t-new"},"root_pane":{"pane_id":"p-new"}}}'
443
+ ;;
444
+ "pane list")
445
+ if [ -e "$spawned" ]; then
446
+ printf '%s\n' '{"result":{"panes":[{"pane_id":"p-new","tab_id":"t-new"}]}}'
447
+ elif [ ! -e "$killed" ]; then
448
+ printf '%s\n' '{"result":{"panes":[{"pane_id":"p-old","tab_id":"t-old"}]}}'
449
+ else
450
+ printf '%s\n' '{"result":{"panes":[]}}'
451
+ fi
452
+ ;;
453
+ "pane get")
454
+ pane=${3:-}
455
+ if [ "$pane" = p-new ] && [ -e "$spawned" ]; then
456
+ printf '%s\n' '{"result":{"pane":{"pane_id":"p-new","tab_id":"t-new","workspace_id":"ws1"}}}'
457
+ elif [ "$pane" = p-old ] && [ ! -e "$killed" ]; then
458
+ printf '%s\n' '{"result":{"pane":{"pane_id":"p-old","tab_id":"t-old","workspace_id":"ws1"}}}'
459
+ else
460
+ printf '%s\n' '{"error":{"code":"pane_not_found"}}' >&2
461
+ exit 1
462
+ fi
463
+ ;;
464
+ "agent get")
465
+ if [ "${3:-}" = p-new ] && [ -e "$spawned" ]; then
466
+ printf '%s\n' '{"result":{"agent":{"agent_status":"idle"}}}'
467
+ else
468
+ printf '%s\n' '{"error":{"code":"agent_not_found"}}' >&2
469
+ exit 1
470
+ fi
471
+ ;;
472
+ "pane close")
473
+ [ "${3:-}" = p-old ] && : > "$killed"
474
+ ;;
475
+ "pane run"|"pane send-text"|"pane send-keys"|"tab close")
476
+ ;;
477
+ *)
478
+ exit 1
479
+ ;;
480
+ esac
481
+ exit 0
482
+ SH
483
+ chmod +x "$fakebin/herdr"
484
+ }
485
+
486
+ # make_fake_herdr <fakebin> <live-pane>: `herdr pane get <pane>` succeeds only
487
+ # for the given pane id - the exact primitive fm_backend_target_exists uses
488
+ # for a herdr endpoint liveness read. No version/server-start calls: a
489
+ # liveness check must never auto-start a server (fm-backend.sh's contract).
490
+ make_fake_herdr() {
491
+ local fakebin=$1 live=$2
492
+ cat > "$fakebin/herdr" <<SH
493
+ #!/usr/bin/env bash
494
+ set -u
495
+ if [ "\${1:-}" = pane ] && [ "\${2:-}" = get ]; then
496
+ [ "\${3:-}" = "$live" ] && exit 0
497
+ exit 1
498
+ fi
499
+ exit 1
500
+ SH
501
+ chmod +x "$fakebin/herdr"
502
+ }
503
+
504
+ # run_session_start <home> <root> <path>
505
+ # Drop every harness env marker from bin/fm-harness.sh detect_own so the
506
+ # surrounding interactive shell cannot leak past the suite's fake ps harness.
507
+ # Markers today: CLAUDECODE (claude), PI_CODING_AGENT plus FM_PI_HARNESS
508
+ # (Pi family), GROK_AGENT (grok).
509
+ # codex and opencode have no env markers (ancestry only). Without this, a local
510
+ # claude/pi/grok session fails cases that pin a different fake harness while CI
511
+ # (no ambient markers) still passes.
512
+ run_session_start() {
513
+ local home=$1 root=$2 path=$3 pi_harness=${4:-}
514
+ if [ -n "$pi_harness" ]; then
515
+ env -u CLAUDECODE -u GROK_AGENT PI_CODING_AGENT=true FM_PI_HARNESS="$pi_harness" \
516
+ FM_HOME="$home" FM_ROOT_OVERRIDE="$root" PATH="$path" \
517
+ "$SESSION_START"
518
+ else
519
+ env -u CLAUDECODE -u PI_CODING_AGENT -u FM_PI_HARNESS -u GROK_AGENT \
520
+ FM_HOME="$home" FM_ROOT_OVERRIDE="$root" PATH="$path" \
521
+ "$SESSION_START"
522
+ fi
523
+ }
524
+
525
+ run_pi_session_start() { # <home> <root> <path> [fm-session-start args...]
526
+ local home=$1 root=$2 path=$3
527
+ shift 3
528
+ env -u CLAUDECODE -u GROK_AGENT PI_CODING_AGENT=true FM_PI_HARNESS=pi \
529
+ FM_FAKE_HARNESS_PID="$SESSION_START_TEST_HARNESS_PID" \
530
+ FM_HOME="$home" FM_ROOT_OVERRIDE="$root" PATH="$path" \
531
+ "$SESSION_START" "$@"
532
+ }
533
+
534
+ run_named_harness_session_start() { # <harness> <home> <root> <path> [fm-session-start args...]
535
+ local harness=$1 home=$2 root=$3 path=$4
536
+ shift 4
537
+ env -u CLAUDECODE -u PI_CODING_AGENT -u FM_PI_HARNESS -u GROK_AGENT \
538
+ FM_FAKE_HARNESS="$harness" FM_FAKE_HARNESS_PID="$SESSION_START_TEST_HARNESS_PID" \
539
+ FM_HOME="$home" FM_ROOT_OVERRIDE="$root" PATH="$path" \
540
+ "$SESSION_START" "$@"
541
+ }
542
+
543
+ # prepare_session_start_secondmate <name>: a throwaway main home and Pi
544
+ # secondmate home wired to the real spawn implementation through the fixture
545
+ # root. Echoes root|home|fakebin|mate|log|spawned.
546
+ prepare_session_start_secondmate() {
547
+ local name=$1 rec root home fakebin w mate log spawned id=$SESSION_START_SECOND_MATE_ID
548
+ rec=$(new_world "$name")
549
+ IFS='|' read -r root home fakebin <<EOF
550
+ $rec
551
+ EOF
552
+ w=${root%/root}
553
+ mate="$w/secondmate-$id"
554
+ log="$w/tmux.log"
555
+ spawned="$w/tmux.spawned"
556
+ mkdir -p "$mate/bin" "$mate/data" "$mate/state" "$mate/config" "$mate/projects"
557
+ printf '%s\n' "$id" > "$mate/.fm-secondmate-home"
558
+ printf '# Firstmate\n' > "$mate/AGENTS.md"
559
+ printf 'Second mate charter.\n' > "$mate/data/charter.md"
560
+ printf '%s\n' pi > "$home/config/secondmate-harness"
561
+ printf '%s\n' manual > "$home/config/backlog-backend"
562
+ touch "$home/state/.last-watcher-beat"
563
+ {
564
+ printf 'window=firstmate:fm-%s\n' "$id"
565
+ printf 'kind=secondmate\n'
566
+ printf 'harness=pi\n'
567
+ printf 'home=%s\n' "$mate"
568
+ } > "$home/state/$id.meta"
569
+ ln -s "$ROOT/bin" "$root/bin"
570
+ make_fake_toolchain "$fakebin"
571
+ make_fake_ps_claude "$fakebin"
572
+ fm_fake_exit0 "$fakebin" pi
573
+ make_fake_tmux_secondmate_recovery "$fakebin"
574
+ : > "$log"
575
+ printf '%s|%s|%s|%s|%s|%s\n' "$root" "$home" "$fakebin" "$mate" "$log" "$spawned"
576
+ }
577
+
578
+ run_session_start_secondmate() {
579
+ local root=$1 home=$2 fakebin=$3 mate=$4 log=$5 spawned=$6 mode=$7
580
+ TMUX='' FM_BACKEND=tmux FM_FAKE_TMUX_MODE="$mode" FM_FAKE_TMUX_LOG="$log" \
581
+ FM_FAKE_TMUX_SPAWNED="$spawned" FM_FAKE_SECOND_MATE_HOME="$mate" \
582
+ FM_FAKE_SECOND_MATE_ID="$SESSION_START_SECOND_MATE_ID" \
583
+ FM_FAKE_HARNESS_PID=$$ \
584
+ run_session_start "$home" "$root" "$fakebin:$BASE_PATH"
585
+ }
586
+
587
+ prepare_session_start_herdr_secondmate() {
588
+ local name=$1 rec root home fakebin w mate log state id=$SESSION_START_HERDR_SECOND_MATE_ID
589
+ rec=$(new_world "$name")
590
+ IFS='|' read -r root home fakebin <<EOF
591
+ $rec
592
+ EOF
593
+ w=${root%/root}
594
+ mate="$w/secondmate-$id"
595
+ log="$w/herdr.log"
596
+ state="$w/herdr.state"
597
+ mkdir -p "$mate/bin" "$mate/data" "$mate/state" "$mate/config" "$mate/projects"
598
+ printf '%s\n' "$id" > "$mate/.fm-secondmate-home"
599
+ printf '# Firstmate\n' > "$mate/AGENTS.md"
600
+ printf 'Second mate charter.\n' > "$mate/data/charter.md"
601
+ printf '%s\n' herdr > "$home/config/backend"
602
+ printf '%s\n' pi > "$home/config/secondmate-harness"
603
+ printf '%s\n' manual > "$home/config/backlog-backend"
604
+ touch "$home/state/.last-watcher-beat"
605
+ {
606
+ printf 'window=default:p-old\n'
607
+ printf 'kind=secondmate\n'
608
+ printf 'harness=pi\n'
609
+ printf 'home=%s\n' "$mate"
610
+ printf 'backend=herdr\n'
611
+ printf 'herdr_session=default\n'
612
+ printf 'herdr_workspace_id=ws1\n'
613
+ printf 'herdr_tab_id=t-old\n'
614
+ printf 'herdr_pane_id=p-old\n'
615
+ } > "$home/state/$id.meta"
616
+ ln -s "$ROOT/bin" "$root/bin"
617
+ make_fake_toolchain "$fakebin"
618
+ make_fake_ps_claude "$fakebin"
619
+ fm_fake_exit0 "$fakebin" pi
620
+ make_fake_herdr_secondmate_recovery "$fakebin"
621
+ : > "$log"
622
+ printf '%s|%s|%s|%s|%s|%s\n' "$root" "$home" "$fakebin" "$mate" "$log" "$state"
623
+ }
624
+
625
+ run_session_start_herdr_secondmate() {
626
+ local root=$1 home=$2 fakebin=$3 mate=$4 log=$5 state=$6
627
+ FM_BACKEND=herdr FM_FAKE_HERDR_LOG="$log" FM_FAKE_HERDR_STATE="$state" \
628
+ FM_FAKE_SECOND_MATE_ID="$SESSION_START_HERDR_SECOND_MATE_ID" \
629
+ FM_FAKE_HARNESS_PID=$$ \
630
+ run_session_start "$home" "$root" "$fakebin:$BASE_PATH"
631
+ }
632
+
633
+ # wait_for_network_stage <home> <root> [seconds]
634
+ # Block until the deferred network stage this home's session start launched has
635
+ # published. Only a TEST does this: the digest itself is required never to wait,
636
+ # which is exactly why the sweeps it used to run inline have to be re-asserted
637
+ # here instead of straight off the digest's own output.
638
+ wait_for_network_stage() {
639
+ local home=$1 root=$2 limit=${3:-30}
640
+ FM_HOME="$home" FM_ROOT_OVERRIDE="$root" \
641
+ "$ROOT/bin/fm-startup-network.sh" wait "$limit"
642
+ }
643
+
644
+ wait_for_network_wake() {
645
+ local home=$1 limit=${2:-30} waited=0
646
+ while ! grep -Fq $'check\tstartup-network' "$home/state/.wake-queue" 2>/dev/null \
647
+ && [ "$waited" -lt "$limit" ]; do
648
+ sleep 1
649
+ waited=$((waited + 1))
650
+ done
651
+ grep -Fq $'check\tstartup-network' "$home/state/.wake-queue" 2>/dev/null
652
+ }
653
+
654
+ network_stage_report() {
655
+ local home=$1 root=$2
656
+ FM_HOME="$home" FM_ROOT_OVERRIDE="$root" "$ROOT/bin/fm-startup-network.sh" report
657
+ }
658
+
659
+ hash_file_for_test() {
660
+ local file=$1
661
+ if command -v shasum >/dev/null 2>&1; then
662
+ shasum -a 256 "$file" | awk '{print "sha256:" $1}'
663
+ elif command -v sha256sum >/dev/null 2>&1; then
664
+ sha256sum "$file" | awk '{print "sha256:" $1}'
665
+ else
666
+ cksum "$file" | awk '{print "cksum:" $1 ":" $2}'
667
+ fi
668
+ }
669
+
670
+ install_pi_turnend_extension_fixture() {
671
+ local root=$1
672
+ mkdir -p "$root/.pi/extensions"
673
+ cp "$ROOT/.pi/extensions/fm-primary-turnend-guard.ts" "$root/.pi/extensions/fm-primary-turnend-guard.ts"
674
+ }
675
+
676
+ install_pi_watch_extension_fixture() {
677
+ local root=$1
678
+ mkdir -p "$root/.pi/extensions"
679
+ cp "$ROOT/.pi/extensions/fm-primary-pi-watch.ts" "$root/.pi/extensions/fm-primary-pi-watch.ts"
680
+ }
681
+
682
+ write_pi_watch_loaded_marker() {
683
+ local home=$1 root=$2 pid=$3 version
684
+ version=$(hash_file_for_test "$root/.pi/extensions/fm-primary-pi-watch.ts")
685
+ printf '%s\n%s\n' "$version" "$pid" > "$home/state/.pi-watch-extension-loaded"
686
+ }
687
+
688
+ write_pi_turnend_loaded_marker() {
689
+ local home=$1 root=$2 pid=$3 version
690
+ version=$(hash_file_for_test "$root/.pi/extensions/fm-primary-turnend-guard.ts")
691
+ printf '%s\n%s\n' "$version" "$pid" > "$home/state/.pi-turnend-extension-loaded"
692
+ }
693
+
694
+ write_pi_loaded_markers() {
695
+ local home=$1 root=$2 pid=$3
696
+ write_pi_watch_loaded_marker "$home" "$root" "$pid"
697
+ write_pi_turnend_loaded_marker "$home" "$root" "$pid"
698
+ }
699
+
700
+ # --- context digest: absent vs empty vs present -----------------------------
701
+
702
+ test_context_digest_absent_empty_present() {
703
+ local rec root home fakebin out
704
+ rec=$(new_world context-digest)
705
+ IFS='|' read -r root home fakebin <<EOF
706
+ $rec
707
+ EOF
708
+ make_fake_toolchain "$fakebin"
709
+ make_fake_ps_claude "$fakebin"
710
+
711
+ printf '%s\n' '- demo [no-mistakes] - a demo project (added 2026-07-01)' > "$home/data/projects.md"
712
+ : > "$home/data/captain.md"
713
+ # secondmates.md, captain-shared.md, and learnings.md deliberately absent
714
+
715
+ out=$(run_session_start "$home" "$root" "$fakebin:$BASE_PATH")
716
+
717
+ jq -e --arg home "$home" '
718
+ .schema == "fm-secondmate-home-summary.v1"
719
+ and .home == $home
720
+ and (.generated_epoch | type) == "number"
721
+ ' "$home/state/home-summary.json" >/dev/null \
722
+ || fail "a locked session start did not publish the home summary ledger"
723
+ assert_contains "$out" "data/projects.md" "digest did not label the projects.md section"
724
+ assert_contains "$out" "- demo [no-mistakes] - a demo project (added 2026-07-01)" "digest did not print projects.md content"
725
+
726
+ assert_contains "$out" "data/captain.md" "digest did not label the captain.md section"
727
+ assert_contains "$out" "data/captain-shared.md (shared, main-authoritative, read-only in secondmate homes)" \
728
+ "digest did not label the shared captain section"
729
+
730
+ assert_contains "$out" "data/secondmates.md" "digest did not label the secondmates.md section"
731
+ assert_contains "$out" "data/learnings.md" "digest did not label the learnings.md section"
732
+
733
+ # Exactly four context ABSENT markers (secondmates.md, captain-shared.md,
734
+ # learnings.md; backlog.md is covered by its own test) - and the
735
+ # present-but-empty captain.md must NOT print ABSENT.
736
+ absent_count=$(printf '%s\n' "$out" | grep -c '^ABSENT$')
737
+ [ "$absent_count" -eq 4 ] || fail "expected 4 ABSENT markers (secondmates.md, captain-shared.md, learnings.md, backlog.md), got $absent_count: $out"
738
+
739
+ cap_section=$(printf '%s\n' "$out" | awk '/^data\/captain\.md$/{flag=1;next}/^data\//{flag=0}flag')
740
+ assert_contains "$cap_section" "(present, empty)" "empty-but-present captain.md was not distinguished from ABSENT"
741
+
742
+ pass "context digest distinguishes ABSENT, empty-but-present, and populated files"
743
+ }
744
+
745
+ # --- lock refusal: read-only path --------------------------------------------
746
+
747
+ test_lock_refusal_read_only_path() {
748
+ local rec root home fakebin holder_pid out status
749
+ rec=$(new_world lock-refusal)
750
+ IFS='|' read -r root home fakebin <<EOF
751
+ $rec
752
+ EOF
753
+ make_fake_toolchain "$fakebin"
754
+ make_fake_ps_claude "$fakebin"
755
+
756
+ # A live secondmate meta with a window pointed at nothing real - if the
757
+ # bootstrap sweep's secondmate_sync ran (a MUTATING step), it would try to
758
+ # fast-forward this "home" and/or report a SECONDMATE_SYNC/NUDGE_SECONDMATES
759
+ # line. Absence of any such line is this test's proof that
760
+ # FM_BOOTSTRAP_DETECT_ONLY=1 actually suppressed the mutating sweep.
761
+ mkdir -p "$home/other-secondmate/state"
762
+ fm_write_secondmate_meta "$home/state/sm-x.meta" "$home/other-secondmate" "firstmate:fm-sm-x" alpha
763
+ append_wake "$home/state" signal sm-x "done: surfaced before refusal" || fail "seed wake failed"
764
+ git -C "$root" checkout -q -B fm/read-only-tangle
765
+
766
+ sleep 300 &
767
+ holder_pid=$!
768
+ printf '%s\n' "$holder_pid" > "$home/state/.lock"
769
+
770
+ status=0
771
+ out=$(run_session_start "$home" "$root" "$fakebin:$BASE_PATH") || status=$?
772
+ kill "$holder_pid" 2>/dev/null || true
773
+ wait "$holder_pid" 2>/dev/null || true
774
+
775
+ expect_code 0 "$status" "fm-session-start.sh must exit 0 even on a lock refusal"
776
+ assert_contains "$out" "READ-ONLY SESSION" "read-only banner missing on lock refusal"
777
+ assert_contains "$out" "another live firstmate session holds the lock" "read-only banner did not surface fm-lock.sh's own error text"
778
+ assert_contains "$out" "Skipping every mutating step" "read-only banner did not explain what was skipped"
779
+ assert_contains "$out" "skipped (read-only session)" "wake-queue section did not report itself skipped"
780
+ assert_contains "$out" "WATCHER DOWN - SUPERVISION IS OFF" "read-only guard did not surface watcher-liveness alarm"
781
+ assert_contains "$out" "queued wakes pending - left untouched because this session lacks verified fleet-lock ownership" "read-only guard did not leave queued wakes untouched without verified lock ownership"
782
+ assert_contains "$out" "TANGLE: primary checkout on feature branch 'fm/read-only-tangle'" "read-only bootstrap did not surface the tangle diagnostic"
783
+ assert_contains "$out" "read-only session must leave restore work" "read-only tangle diagnostic did not explain restore ownership"
784
+ assert_contains "$out" "Stay read-only: do not arm" "read-only next step did not block direct watcher repair"
785
+ assert_not_contains "$out" "drain them with bin/fm-wake-drain.sh" "read-only guard printed a mutating drain instruction"
786
+ assert_not_contains "$out" "After draining queued wakes" "read-only guard printed a drain-then-rearm instruction"
787
+ assert_not_contains "$out" "run bin/fm-watch-arm.sh" "read-only guard printed a mutating watcher-arm instruction"
788
+ assert_not_contains "$out" "git -C $root checkout main" "read-only bootstrap printed a state-changing checkout remediation"
789
+
790
+ # Detect-only bootstrap diagnostics still ran (the fakebin's PATH excludes
791
+ # tasks-axi, so bootstrap's own read-only tool-detection line fires
792
+ # deterministically regardless of what is installed on the test host).
793
+ assert_contains "$out" "MISSING: tasks-axi (install:" "detect-only bootstrap diagnostics did not run on the read-only path"
794
+
795
+ # The mutating secondmate sweep must NOT have run: no SECONDMATE_SYNC/
796
+ # NUDGE_SECONDMATES line, and the sowed secondmate meta's target dir is
797
+ # untouched (fm-ff-lib would have tried to fast-forward it otherwise).
798
+ assert_not_contains "$out" "SECONDMATE_SYNC" "mutating secondmate sweep ran during a lock refusal"
799
+ assert_not_contains "$out" "NUDGE_SECONDMATES" "mutating secondmate sweep ran during a lock refusal"
800
+
801
+ # The rest of the digest (read-only-safe) still completed.
802
+ assert_contains "$out" "FLEET STATE" "fleet-state digest section missing on the read-only path"
803
+ assert_contains "$out" "NEXT STEP" "closing reminder missing on the read-only path"
804
+
805
+ pass "a lock refusal prints a loud read-only banner, skips every mutating step, and still completes the digest"
806
+ }
807
+
808
+ test_lock_write_failure_read_only_path() {
809
+ local rec root home fakebin out status
810
+ rec=$(new_world lock-write-failure)
811
+ IFS='|' read -r root home fakebin <<EOF
812
+ $rec
813
+ EOF
814
+ make_fake_toolchain "$fakebin"
815
+ make_fake_ps_claude "$fakebin"
816
+ append_wake "$home/state" signal task-a "done: must remain queued" || fail "seed wake failed"
817
+ chmod 0500 "$home/state"
818
+
819
+ status=0
820
+ out=$(run_session_start "$home" "$root" "$fakebin:$BASE_PATH") || status=$?
821
+ chmod 0700 "$home/state"
822
+
823
+ expect_code 0 "$status" "fm-session-start.sh must exit 0 when lock publication fails"
824
+ assert_contains "$out" "cannot write session lock" "lock publication failure was not surfaced"
825
+ assert_contains "$out" "READ-ONLY SESSION" "lock publication failure did not force a read-only session"
826
+ assert_contains "$out" "FLEET LOCK OWNERSHIP WAS NOT VERIFIED" "lock publication failure was misreported as a live holder"
827
+ assert_contains "$out" "lacks verified fleet-lock ownership" "lock publication failure did not explain why queued wakes remain untouched"
828
+ assert_not_contains "$out" "ANOTHER LIVE FIRSTMATE SESSION HOLDS THE FLEET LOCK" "lock publication failure falsely claimed a live lock holder"
829
+ [ -s "$home/state/.wake-queue" ] || fail "lock publication failure allowed the wake queue to mutate"
830
+
831
+ pass "session start stays read-only when lock ownership cannot be published"
832
+ }
833
+
834
+ test_trace_context_effective_state_is_frozen_after_lock() {
835
+ local rec root home fakebin out frozen
836
+ rec=$(new_world trace-context-session-state)
837
+ IFS='|' read -r root home fakebin <<EOF
838
+ $rec
839
+ EOF
840
+ make_fake_toolchain "$fakebin"
841
+ make_fake_ps_claude "$fakebin"
842
+ : > "$home/config/trace-context"
843
+
844
+ FM_TRACE_CONTEXT=off run_session_start "$home" "$root" "$fakebin:$BASE_PATH" >/dev/null
845
+ [ "$(awk '{print $2}' "$home/state/.trace-context-effective")" = off ] \
846
+ || fail "session start must freeze an env-off override over a present config flag"
847
+
848
+ rm "$home/config/trace-context"
849
+ FM_TRACE_CONTEXT=on run_session_start "$home" "$root" "$fakebin:$BASE_PATH" >/dev/null
850
+ [ "$(awk '{print $2}' "$home/state/.trace-context-effective")" = on ] \
851
+ || fail "a new session start must freeze an env-on override over an absent config flag"
852
+ frozen=$(cat "$home/state/.trace-context-effective")
853
+
854
+ sleep 300 &
855
+ holder_pid=$!
856
+ printf '%s\n' "$holder_pid" > "$home/state/.lock"
857
+ out=$(FM_TRACE_CONTEXT=off run_session_start "$home" "$root" "$fakebin:$BASE_PATH")
858
+ kill "$holder_pid" 2>/dev/null || true
859
+ wait "$holder_pid" 2>/dev/null || true
860
+ assert_contains "$out" "READ-ONLY SESSION" "trace-context refusal fixture did not enter read-only mode"
861
+ [ "$(cat "$home/state/.trace-context-effective")" = "$frozen" ] \
862
+ || fail "a lock-refused session must not mutate the frozen trace-context state"
863
+
864
+ pass "locked session start freezes trace context and lock refusal leaves it unchanged"
865
+ }
866
+
867
+ test_session_lock_concurrent_single_winner() {
868
+ local rec root home fakebin ready completed winners pids i pid count
869
+ rec=$(new_world lock-concurrency)
870
+ IFS='|' read -r root home fakebin <<EOF
871
+ $rec
872
+ EOF
873
+ ready="$home/ready"
874
+ completed="$home/done"
875
+ winners="$home/winners"
876
+ mkdir -p "$ready" "$completed"
877
+ : > "$winners"
878
+ cat > "$fakebin/ps" <<'SH'
879
+ #!/usr/bin/env bash
880
+ set -u
881
+ pid=
882
+ previous=
883
+ for argument in "$@"; do
884
+ [ "$previous" = -p ] && pid=$argument
885
+ previous=$argument
886
+ done
887
+ case "$*" in
888
+ *"comm="*)
889
+ if [ -f "$FM_FAKE_LOCK_STATE/harness-$pid" ]; then
890
+ printf '%s\n' /usr/local/bin/claude
891
+ else
892
+ printf '%s\n' /bin/bash
893
+ fi
894
+ ;;
895
+ *"args="*)
896
+ if [ -f "$FM_FAKE_LOCK_STATE/harness-$pid" ]; then
897
+ printf '%s\n' claude
898
+ else
899
+ printf '%s\n' bash
900
+ fi
901
+ ;;
902
+ *"ppid="*) printf '%s\n' "$FM_FAKE_HARNESS_PID" ;;
903
+ *) exit 1 ;;
904
+ esac
905
+ SH
906
+ chmod +x "$fakebin/ps"
907
+
908
+ pids=
909
+ i=1
910
+ while [ "$i" -le 40 ]; do
911
+ (
912
+ harness_pid=$(sh -c 'printf "%s\n" "$PPID"')
913
+ : > "$home/state/harness-$harness_pid"
914
+ : > "$ready/$i"
915
+ while [ "$(find "$ready" -type f | wc -l | tr -d ' ')" -lt 40 ]; do
916
+ sleep 0.01
917
+ done
918
+ if FM_HOME="$home" FM_FAKE_LOCK_STATE="$home/state" \
919
+ FM_FAKE_HARNESS_PID="$harness_pid" PATH="$fakebin:$BASE_PATH" \
920
+ "$ROOT/bin/fm-lock.sh" >/dev/null 2>&1; then
921
+ printf '%s\n' "$harness_pid" >> "$winners"
922
+ fi
923
+ : > "$completed/$i"
924
+ while [ "$(find "$completed" -type f | wc -l | tr -d ' ')" -lt 40 ]; do
925
+ sleep 0.01
926
+ done
927
+ ) &
928
+ pids="$pids $!"
929
+ i=$((i + 1))
930
+ done
931
+ for pid in $pids; do
932
+ wait "$pid" 2>/dev/null || true
933
+ done
934
+ count=$(awk 'NF { count++ } END { print count + 0 }' "$winners")
935
+ [ "$count" -eq 1 ] || fail "concurrent session-lock acquisition produced $count winners"
936
+
937
+ pass "concurrent session-lock acquisition admits exactly one live harness"
938
+ }
939
+
940
+ # --- output ordering ----------------------------------------------------------
941
+
942
+ # The digest is delivered through a harness that truncates from the TAIL, so
943
+ # section order decides what a truncated startup loses. The safety preamble
944
+ # still leads, live fleet identity now outranks curated memory, and the
945
+ # read-once contract arrives before the payload it governs.
946
+ test_output_ordering_diagnostics_lead() {
947
+ local rec root home fakebin out lock_line boot_line wake_line read_once_line
948
+ local context_line fleet_line next_line inventory_line missing_line
949
+ rec=$(new_world ordering)
950
+ IFS='|' read -r root home fakebin <<EOF
951
+ $rec
952
+ EOF
953
+ make_fake_toolchain "$fakebin"
954
+ make_fake_ps_claude "$fakebin"
955
+ # Force a MISSING diagnostic line so the bootstrap section is non-trivial.
956
+ rm -f "$fakebin/node"
957
+
958
+ printf 'window=fm-sess:w1\nkind=ship\n' > "$home/state/task-a.meta"
959
+ printf 'Captain memory that may be truncated away safely.\n' > "$home/data/captain.md"
960
+
961
+ out=$(run_session_start "$home" "$root" "$fakebin:$BASE_PATH")
962
+
963
+ lock_line=$(printf '%s\n' "$out" | grep -n '^LOCK$' | head -1 | cut -d: -f1)
964
+ boot_line=$(printf '%s\n' "$out" | grep -n '^BOOTSTRAP$' | head -1 | cut -d: -f1)
965
+ wake_line=$(printf '%s\n' "$out" | grep -n '^WAKE QUEUE$' | head -1 | cut -d: -f1)
966
+ read_once_line=$(printf '%s\n' "$out" | grep -n '^READ-ONCE CONTRACT$' | head -1 | cut -d: -f1)
967
+ context_line=$(printf '%s\n' "$out" | grep -n '^CONTEXT$' | head -1 | cut -d: -f1)
968
+ fleet_line=$(printf '%s\n' "$out" | grep -n '^FLEET STATE$' | head -1 | cut -d: -f1)
969
+ next_line=$(printf '%s\n' "$out" | grep -n '^NEXT STEP$' | head -1 | cut -d: -f1)
970
+ inventory_line=$(printf '%s\n' "$out" | grep -n '^--- task-a ---$' | head -1 | cut -d: -f1)
971
+
972
+ if [ -z "$lock_line" ] || [ -z "$boot_line" ] || [ -z "$wake_line" ] \
973
+ || [ -z "$read_once_line" ] || [ -z "$context_line" ] || [ -z "$fleet_line" ] \
974
+ || [ -z "$next_line" ] || [ -z "$inventory_line" ]; then
975
+ fail "one or more section headers missing from digest: $out"
976
+ fi
977
+
978
+ # The safety preamble's order is unchanged: mutation authority, then
979
+ # diagnostics, then this turn's work queue, before anything bulky is read.
980
+ [ "$lock_line" -lt "$boot_line" ] || fail "LOCK did not precede BOOTSTRAP"
981
+ [ "$boot_line" -lt "$wake_line" ] || fail "BOOTSTRAP did not precede WAKE QUEUE"
982
+ [ "$wake_line" -lt "$read_once_line" ] || fail "WAKE QUEUE did not precede the read-once contract"
983
+
984
+ [ "$read_once_line" -lt "$fleet_line" ] || fail "the read-once contract did not precede FLEET STATE"
985
+ [ "$fleet_line" -lt "$context_line" ] || fail "FLEET STATE did not precede CONTEXT"
986
+ [ "$context_line" -lt "$next_line" ] || fail "CONTEXT did not precede NEXT STEP"
987
+
988
+ # The live-task inventory - the record recovery actually depends on - must sit
989
+ # ahead of the curated memory a truncated tail is allowed to take.
990
+ [ "$inventory_line" -lt "$context_line" ] \
991
+ || fail "the live-task inventory was buried behind the curated memory files"
992
+ assert_contains "$out" "Captain memory that may be truncated away safely." \
993
+ "the ordering fixture did not actually print a memory file"
994
+
995
+ missing_line=$(printf '%s\n' "$out" | grep -n 'MISSING: node' | head -1 | cut -d: -f1)
996
+ [ -n "$missing_line" ] || fail "MISSING diagnostic did not appear at all"
997
+ [ "$missing_line" -lt "$fleet_line" ] || fail "actionable MISSING diagnostic was buried after the bulk fleet-state digest"
998
+
999
+ pass "digest sections are ordered safety-preamble first, live fleet state before curated memory"
1000
+ }
1001
+
1002
+ # The contract has to survive tail truncation and stay honest once it precedes
1003
+ # the sections it governs, so it carries the truncated-stage escape itself.
1004
+ test_read_once_contract_is_stated_once_before_its_subject() {
1005
+ local rec root home fakebin out contract_count
1006
+ rec=$(new_world read-once)
1007
+ IFS='|' read -r root home fakebin <<EOF
1008
+ $rec
1009
+ EOF
1010
+ make_fake_toolchain "$fakebin"
1011
+ make_fake_ps_claude "$fakebin"
1012
+
1013
+ out=$(run_session_start "$home" "$root" "$fakebin:$BASE_PATH")
1014
+
1015
+ assert_contains "$out" "Do NOT re-read any of them after reading this digest" \
1016
+ "the read-once contract lost its core instruction"
1017
+ assert_contains "$out" "STARTUP TRUNCATED banner named the stage that would have printed it" \
1018
+ "the read-once contract does not void itself for a stage that never ran"
1019
+ assert_contains "$out" "The READ-ONCE CONTRACT" \
1020
+ "the closing reminder does not point back at the contract"
1021
+
1022
+ contract_count=$(printf '%s\n' "$out" | grep -c 'Do NOT re-read any of them')
1023
+ [ "$contract_count" -eq 1 ] \
1024
+ || fail "the read-once contract is stated $contract_count times instead of once: $out"
1025
+
1026
+ pass "the read-once contract is stated once, ahead of the sources it governs"
1027
+ }
1028
+
1029
+ test_herdr_backend_diagnostics_follow_real_session_start() {
1030
+ local mode rec root home fakebin mask out
1031
+ for mode in configured autodetected; do
1032
+ rec=$(new_world "herdr-$mode")
1033
+ IFS='|' read -r root home fakebin <<EOF
1034
+ $rec
1035
+ EOF
1036
+ make_fake_toolchain "$fakebin"
1037
+ make_fake_ps_claude "$fakebin"
1038
+ rm -f "$fakebin/tmux"
1039
+ fm_fake_exit0 "$fakebin" herdr jq
1040
+ printf '%s\n' manual > "$home/config/backlog-backend"
1041
+ mask="$home/mask-tmux.bash"
1042
+ cat > "$mask" <<'SH'
1043
+ command() {
1044
+ if [ "${1:-}" = -v ] && [ "${2:-}" = tmux ]; then
1045
+ return 1
1046
+ fi
1047
+ builtin command "$@"
1048
+ }
1049
+ SH
1050
+ if [ "$mode" = configured ]; then
1051
+ printf '%s\n' herdr > "$home/config/backend"
1052
+ out=$(TMUX='' HERDR_ENV='' BASH_ENV="$mask" run_session_start "$home" "$root" "$fakebin:$BASE_PATH")
1053
+ assert_not_contains "$out" "NOTICE: auto-detected herdr runtime" \
1054
+ "an explicit Herdr home should not be reported as auto-detected"
1055
+ else
1056
+ out=$(TMUX='' HERDR_ENV=1 BASH_ENV="$mask" run_session_start "$home" "$root" "$fakebin:$BASE_PATH")
1057
+ assert_contains "$out" "NOTICE: auto-detected herdr runtime (HERDR_ENV=1)" \
1058
+ "session start did not preserve the Herdr runtime auto-detection fallback"
1059
+ fi
1060
+ assert_contains "$out" "SESSION START - $home" "the real session-start path did not run in the throwaway home"
1061
+ assert_not_contains "$out" "MISSING: tmux" "Herdr session start falsely required masked tmux"
1062
+ assert_not_contains "$out" "MISSING: herdr" "Herdr session start missed its available session CLI"
1063
+ assert_not_contains "$out" "MISSING: jq" "Herdr session start missed its available JSON dependency"
1064
+ assert_not_contains "$out" "MISSING: treehouse" "Herdr session start missed its available worktree provider"
1065
+ done
1066
+ pass "session start: configured and auto-detected Herdr homes never require tmux"
1067
+ }
1068
+
1069
+ # --- status tail bounding -----------------------------------------------------
1070
+
1071
+ test_status_tail_bounding() {
1072
+ local rec root home fakebin out
1073
+ rec=$(new_world status-tail)
1074
+ IFS='|' read -r root home fakebin <<EOF
1075
+ $rec
1076
+ EOF
1077
+ make_fake_toolchain "$fakebin"
1078
+ make_fake_ps_claude "$fakebin"
1079
+ make_fake_tmux "$fakebin" "fm-sess:live"
1080
+
1081
+ printf 'window=fm-sess:live\nkind=ship\n' > "$home/state/task-a.meta"
1082
+ printf 'working: step 1\nworking: step 2\nworking: step 3\nworking: step 4\nworking: step 5\nworking: step 6\nworking: step 7\n' \
1083
+ > "$home/state/task-a.status"
1084
+
1085
+ out=$(run_session_start "$home" "$root" "$fakebin:$BASE_PATH")
1086
+ assert_contains "$out" "working: step 7" "default status tail missing the most recent line"
1087
+ assert_contains "$out" "working: step 3" "default status tail (5 lines) missing an expected recent line"
1088
+ assert_not_contains "$out" "working: step 1" "default status tail (5 lines) leaked an older line"
1089
+ assert_contains "$out" "$home/state/task-a.status" "digest did not print the full status log path for a deeper read"
1090
+ assert_contains "$out" "a bounded tail of every state/*.status" "read-once contract does not distinguish bounded status tails"
1091
+
1092
+ out=$(FM_SESSION_START_STATUS_TAIL=2 run_session_start "$home" "$root" "$fakebin:$BASE_PATH")
1093
+ assert_contains "$out" "working: step 7" "FM_SESSION_START_STATUS_TAIL=2 tail missing the most recent line"
1094
+ assert_not_contains "$out" "working: step 5" "FM_SESSION_START_STATUS_TAIL=2 did not bound the tail to 2 lines"
1095
+
1096
+ pass "status tail is bounded to the configured line count, with the full log path always printed"
1097
+ }
1098
+
1099
+ # A crewmate writes its own status lines, so nothing upstream bounds their
1100
+ # length: an observed one ran 865 characters. The tail is a wake-EVENT view
1101
+ # whose full log path is printed beside it, so a long line is cut, marked, and
1102
+ # left recoverable rather than allowed to scale the digest with fleet load.
1103
+ test_status_tail_line_cap() {
1104
+ local rec root home fakebin out lede longest capped tail_section
1105
+ rec=$(new_world status-line-cap)
1106
+ IFS='|' read -r root home fakebin <<EOF
1107
+ $rec
1108
+ EOF
1109
+ make_fake_toolchain "$fakebin"
1110
+ make_fake_ps_claude "$fakebin"
1111
+ make_fake_tmux "$fakebin" "fm-sess:live"
1112
+
1113
+ lede='needs-decision: [key=cap] pick the rendering strategy'
1114
+ printf 'window=fm-sess:live\nkind=ship\n' > "$home/state/task-cap.meta"
1115
+ {
1116
+ printf '%s' "$lede"
1117
+ awk 'BEGIN { while (i++ < 400) printf " padding" }'
1118
+ printf '\n'
1119
+ printf 'working: short line kept whole\n'
1120
+ } > "$home/state/task-cap.status"
1121
+
1122
+ out=$(run_session_start "$home" "$root" "$fakebin:$BASE_PATH")
1123
+
1124
+ assert_contains "$out" "$lede" "the cap discarded the lede that carries the state word and decision key"
1125
+ assert_contains "$out" " [truncated]" "an over-long status line was not marked as truncated"
1126
+ assert_contains "$out" "working: short line kept whole" "the cap mangled a status line already under it"
1127
+ assert_contains "$out" "each capped at 220 characters" "the status tail header does not disclose its per-line cap"
1128
+ assert_contains "$out" "$home/state/task-cap.status" "a capped tail dropped the full log path that recovers the rest"
1129
+
1130
+ # Nothing the tail emits may exceed the cap, and the padded line really was
1131
+ # long enough to exercise it.
1132
+ tail_section=$(printf '%s\n' "$out" | awk '/^status tail \(/ { flag = 1; next } flag && /^$/ { flag = 0 } flag')
1133
+ longest=$(printf '%s\n' "$tail_section" | awk '{ if (length($0) > max) max = length($0) } END { print max + 0 }')
1134
+ [ "$longest" -le 220 ] || fail "a status tail line ran $longest characters past the 220-character cap"
1135
+ capped=$(printf '%s\n' "$tail_section" | grep -c ' \[truncated\]$')
1136
+ [ "$capped" -eq 1 ] || fail "expected exactly one truncated tail line, got $capped: $tail_section"
1137
+
1138
+ pass "status tail lines are capped with a truncation marker while the full log stays reachable"
1139
+ }
1140
+
1141
+ test_orphan_status_logs_are_printed() {
1142
+ local rec root home fakebin out matched_count orphan_count
1143
+ rec=$(new_world orphan-status)
1144
+ IFS='|' read -r root home fakebin <<EOF
1145
+ $rec
1146
+ EOF
1147
+ make_fake_toolchain "$fakebin"
1148
+ make_fake_ps_claude "$fakebin"
1149
+
1150
+ printf 'kind=ship\n' > "$home/state/task-a.meta"
1151
+ printf 'matched: surfaced once\n' > "$home/state/task-a.status"
1152
+ printf 'orphan: step 1\norphan: step 2\norphan: step 3\norphan: step 4\norphan: step 5\norphan: step 6\n' \
1153
+ > "$home/state/task-orphan.status"
1154
+
1155
+ out=$(run_session_start "$home" "$root" "$fakebin:$BASE_PATH")
1156
+
1157
+ assert_contains "$out" "Orphan status logs (state/*.status without matching .meta)" "digest did not label orphan status logs"
1158
+ assert_contains "$out" "--- task-orphan ---" "digest did not print the orphan status id"
1159
+ assert_contains "$out" "orphan: step 6" "orphan status tail missing the newest line"
1160
+ assert_not_contains "$out" "orphan: step 1" "orphan status tail was not bounded"
1161
+ assert_contains "$out" "$home/state/task-orphan.status" "orphan status tail did not print the full log path"
1162
+
1163
+ matched_count=$(printf '%s\n' "$out" | grep -F -c 'matched: surfaced once')
1164
+ orphan_count=$(printf '%s\n' "$out" | grep -F -c 'orphan: step 6')
1165
+ [ "$matched_count" -eq 1 ] || fail "matched status log was printed $matched_count times: $out"
1166
+ [ "$orphan_count" -eq 1 ] || fail "orphan status log was printed $orphan_count times: $out"
1167
+
1168
+ pass "orphan status logs are printed once with bounded tails"
1169
+ }
1170
+
1171
+ # --- session-start secondmate recovery boundary -----------------------------
1172
+
1173
+ test_session_start_relaunches_missing_pi_secondmate() {
1174
+ local rec root home fakebin mate log spawned out first_calls second_calls
1175
+ rec=$(prepare_session_start_secondmate secondmate-missing-pi)
1176
+ IFS='|' read -r root home fakebin mate log spawned <<EOF
1177
+ $rec
1178
+ EOF
1179
+
1180
+ out=$(run_session_start_secondmate "$root" "$home" "$fakebin" "$mate" "$log" "$spawned" missing)
1181
+
1182
+ # The relaunch now runs off the blocking path, so the digest's own liveness
1183
+ # read may legitimately still show the pre-relaunch endpoint. What must NOT
1184
+ # happen is silence: the section names the relaunch as either done or not yet
1185
+ # confirmed.
1186
+ assert_contains "$out" "NETWORK CHECKS" "the digest lost its deferred network-check section"
1187
+ assert_contains "$out" "dead-secondmate relaunch" \
1188
+ "the digest never accounted for the dead-secondmate relaunch"
1189
+
1190
+ wait_for_network_stage "$home" "$root" \
1191
+ || fail "the deferred network stage never published: $(network_stage_report "$home" "$root")"
1192
+
1193
+ assert_not_contains "$(network_stage_report "$home" "$root")" "SECONDMATE_LIVENESS:" \
1194
+ "successful missing-window recovery should stay non-actionable"
1195
+ assert_contains "$(cat "$log")" "new-window" "the deferred stage did not relaunch the missing Pi secondmate"
1196
+ assert_not_contains "$(cat "$log")" "kill-window" "the deferred stage tried to kill an already-absent window"
1197
+ assert_grep 'harness=pi' "$home/state/$SESSION_START_SECOND_MATE_ID.meta" \
1198
+ "the real respawn path did not preserve the Pi harness: $(cat "$home/state/$SESSION_START_SECOND_MATE_ID.meta")"
1199
+
1200
+ first_calls=$(grep -c 'new-window' "$log" || true)
1201
+ rm -f "$home/state/.lock"
1202
+ run_session_start_secondmate "$root" "$home" "$fakebin" "$mate" "$log" "$spawned" missing >/dev/null
1203
+ wait_for_network_stage "$home" "$root" \
1204
+ || fail "the second pass's deferred network stage never published"
1205
+ second_calls=$(grep -c 'new-window' "$log" || true)
1206
+ [ "$first_calls" -eq 1 ] && [ "$second_calls" -eq 1 ] \
1207
+ || fail "a second session-start pass duplicated the relaunched Pi secondmate: $(cat "$log")"
1208
+ pass "session start: an absent recorded tmux window relaunches its Pi secondmate exactly once, off the blocking path"
1209
+ }
1210
+
1211
+ # The relaunch is the sharpest deferral: it mutates the very endpoint record the
1212
+ # digest printed moments earlier. Silence would leave that stale record looking
1213
+ # authoritative, so the deferred pass reports it whether or not verbose facts are
1214
+ # on, and the report says the digest's records are now behind.
1215
+ test_deferred_relaunch_is_always_reported() {
1216
+ local rec root home fakebin mate log spawned report
1217
+ rec=$(prepare_session_start_secondmate secondmate-relaunch-reported)
1218
+ IFS='|' read -r root home fakebin mate log spawned <<EOF
1219
+ $rec
1220
+ EOF
1221
+
1222
+ run_session_start_secondmate "$root" "$home" "$fakebin" "$mate" "$log" "$spawned" missing >/dev/null
1223
+ wait_for_network_stage "$home" "$root" || fail "the deferred network stage never published"
1224
+
1225
+ report=$(network_stage_report "$home" "$root")
1226
+ assert_contains "$report" "secondmate $SESSION_START_SECOND_MATE_ID relaunched" \
1227
+ "a relaunch performed after the digest was composed went unreported"
1228
+ assert_contains "$report" "re-read any record" \
1229
+ "the report did not tell the reader the digest's records are now behind"
1230
+ pass "session start: a deferred relaunch is always reported, so the digest's stale endpoint record cannot stand"
1231
+ }
1232
+
1233
+ test_session_start_preserves_ambiguous_pi_process() {
1234
+ local rec root home fakebin mate log spawned out
1235
+ rec=$(prepare_session_start_secondmate secondmate-ambiguous-pi)
1236
+ IFS='|' read -r root home fakebin mate log spawned <<EOF
1237
+ $rec
1238
+ EOF
1239
+
1240
+ out=$(run_session_start_secondmate "$root" "$home" "$fakebin" "$mate" "$log" "$spawned" ambiguous)
1241
+ wait_for_network_stage "$home" "$root" || fail "the deferred network stage never published"
1242
+
1243
+ assert_contains "$(network_stage_report "$home" "$root")" \
1244
+ "SECONDMATE_LIVENESS: secondmate $SESSION_START_SECOND_MATE_ID: skipped: existing endpoint has ambiguous agent process (backend=tmux)" \
1245
+ "session start did not distinguish an existing Pi-shaped process from a missing window"
1246
+ [ ! -s "$log" ] || fail "session start touched an ambiguous existing Pi process: $(cat "$log")"
1247
+ assert_contains "$out" "endpoint: alive (backend=tmux window=firstmate:fm-$SESSION_START_SECOND_MATE_ID)" \
1248
+ "the later fleet read should still see the ambiguous endpoint"
1249
+ pass "session start: an existing ambiguous Pi process prevents duplicate recovery"
1250
+ }
1251
+
1252
+ test_session_start_preserves_transiently_unreadable_tmux() {
1253
+ local rec root home fakebin mate log spawned out
1254
+ rec=$(prepare_session_start_secondmate secondmate-unreadable-pi)
1255
+ IFS='|' read -r root home fakebin mate log spawned <<EOF
1256
+ $rec
1257
+ EOF
1258
+
1259
+ out=$(run_session_start_secondmate "$root" "$home" "$fakebin" "$mate" "$log" "$spawned" unreadable)
1260
+ wait_for_network_stage "$home" "$root" || fail "the deferred network stage never published"
1261
+
1262
+ assert_contains "$(network_stage_report "$home" "$root")" \
1263
+ "SECONDMATE_LIVENESS: secondmate $SESSION_START_SECOND_MATE_ID: skipped: endpoint probe unreadable (backend=tmux)" \
1264
+ "session start did not distinguish transient unreadability from absence"
1265
+ [ ! -s "$log" ] || fail "session start touched a transiently unreadable target: $(cat "$log")"
1266
+ assert_contains "$out" "endpoint: dead (backend=tmux window=firstmate:fm-$SESSION_START_SECOND_MATE_ID)" \
1267
+ "the later cheap presence read should preserve the visible offline symptom"
1268
+ pass "session start: transient tmux unreadability never licenses a relaunch"
1269
+ }
1270
+
1271
+ test_session_start_preserves_proven_bare_shell_recovery() {
1272
+ local rec root home fakebin mate log spawned out
1273
+ rec=$(prepare_session_start_secondmate secondmate-bare-shell)
1274
+ IFS='|' read -r root home fakebin mate log spawned <<EOF
1275
+ $rec
1276
+ EOF
1277
+
1278
+ run_session_start_secondmate "$root" "$home" "$fakebin" "$mate" "$log" "$spawned" shell >/dev/null
1279
+ wait_for_network_stage "$home" "$root" || fail "the deferred network stage never published"
1280
+
1281
+ out=$(network_stage_report "$home" "$root")
1282
+ assert_not_contains "$out" "SECONDMATE_LIVENESS:" "successful bare-shell recovery should stay non-actionable"
1283
+ assert_contains "$(cat "$log")" "kill-window -t =firstmate:=fm-$SESSION_START_SECOND_MATE_ID" \
1284
+ "the proven bare-shell path did not remove its existing dead endpoint"
1285
+ assert_contains "$(cat "$log")" "new-window" "the proven bare-shell path did not relaunch"
1286
+ pass "session start: the proven bare-shell recovery path remains intact"
1287
+ }
1288
+
1289
+ test_session_start_relaunches_herdr_husk_secondmate() {
1290
+ local rec root home fakebin mate log state out
1291
+ rec=$(prepare_session_start_herdr_secondmate secondmate-herdr-husk)
1292
+ IFS='|' read -r root home fakebin mate log state <<EOF
1293
+ $rec
1294
+ EOF
1295
+
1296
+ run_session_start_herdr_secondmate "$root" "$home" "$fakebin" "$mate" "$log" "$state" >/dev/null
1297
+ wait_for_network_stage "$home" "$root" || fail "the deferred network stage never published"
1298
+
1299
+ out=$(network_stage_report "$home" "$root")
1300
+ assert_not_contains "$out" "SECONDMATE_LIVENESS:" "successful Herdr husk recovery should stay non-actionable"
1301
+ assert_contains "$(cat "$log")" "pane close p-old" "session start did not close the confirmed Herdr husk"
1302
+ assert_contains "$(cat "$log")" "tab create" "session start did not relaunch the Herdr secondmate"
1303
+ assert_grep 'herdr_pane_id=p-new' "$home/state/$SESSION_START_HERDR_SECOND_MATE_ID.meta" \
1304
+ "the real respawn path did not record the replacement Herdr pane"
1305
+ pass "session start: a confirmed Herdr husk is closed and relaunched"
1306
+ }
1307
+
1308
+ # --- endpoint liveness: tmux and herdr, live and dead ------------------------
1309
+
1310
+ test_endpoint_liveness_tmux() {
1311
+ local rec root home fakebin out
1312
+ rec=$(new_world liveness-tmux)
1313
+ IFS='|' read -r root home fakebin <<EOF
1314
+ $rec
1315
+ EOF
1316
+ make_fake_toolchain "$fakebin"
1317
+ make_fake_ps_claude "$fakebin"
1318
+ make_fake_tmux "$fakebin" "fm-sess:live-window"
1319
+
1320
+ printf 'window=fm-sess:live-window\nkind=ship\n' > "$home/state/task-live.meta"
1321
+ printf 'window=fm-sess:dead-window\nkind=ship\n' > "$home/state/task-dead.meta"
1322
+
1323
+ out=$(run_session_start "$home" "$root" "$fakebin:$BASE_PATH")
1324
+ assert_contains "$out" "endpoint: alive (backend=tmux window=fm-sess:live-window)" "live tmux endpoint not reported alive"
1325
+ assert_contains "$out" "endpoint: dead (backend=tmux window=fm-sess:dead-window)" "dead tmux endpoint not reported dead"
1326
+
1327
+ pass "tmux endpoint liveness is reported per task: alive for a live window, dead for a gone one"
1328
+ }
1329
+
1330
+ test_endpoint_liveness_herdr() {
1331
+ local rec root home fakebin out
1332
+ rec=$(new_world liveness-herdr)
1333
+ IFS='|' read -r root home fakebin <<EOF
1334
+ $rec
1335
+ EOF
1336
+ make_fake_toolchain "$fakebin"
1337
+ make_fake_ps_claude "$fakebin"
1338
+ make_fake_herdr "$fakebin" "p-live"
1339
+
1340
+ printf 'window=sess:p-live\nkind=ship\nbackend=herdr\n' > "$home/state/task-live.meta"
1341
+ printf 'window=sess:p-dead\nkind=ship\nbackend=herdr\n' > "$home/state/task-dead.meta"
1342
+
1343
+ out=$(run_session_start "$home" "$root" "$fakebin:$BASE_PATH")
1344
+ assert_contains "$out" "endpoint: alive (backend=herdr window=sess:p-live)" "live herdr endpoint not reported alive"
1345
+ assert_contains "$out" "endpoint: dead (backend=herdr window=sess:p-dead)" "dead herdr endpoint not reported dead"
1346
+
1347
+ pass "herdr endpoint liveness is reported per task: alive for a live pane, dead for a gone one"
1348
+ }
1349
+
1350
+ # --- composition: real scripts run, not reimplemented ------------------------
1351
+
1352
+ test_composition_invokes_real_scripts() {
1353
+ local rec root home fakebin out
1354
+ rec=$(new_world composition)
1355
+ IFS='|' read -r root home fakebin <<EOF
1356
+ $rec
1357
+ EOF
1358
+ make_fake_toolchain "$fakebin"
1359
+ make_fake_ps_claude "$fakebin"
1360
+ rm -f "$fakebin/node"
1361
+
1362
+ printf 'needs-decision: pick a library\n' > "$home/state/task-z.status"
1363
+ append_wake "$home/state" signal task-z.status "needs-decision: pick a library"
1364
+
1365
+ out=$(run_session_start "$home" "$root" "$fakebin:$BASE_PATH")
1366
+
1367
+ # fm-lock.sh's own exact success text.
1368
+ assert_contains "$out" "lock acquired: harness pid" "fm-lock.sh's real output did not appear (composition, not reimplementation)"
1369
+ # fm-bootstrap.sh's own exact MISSING-tool line format.
1370
+ assert_contains "$out" "MISSING: node (install:" "fm-bootstrap.sh's real detect line did not appear verbatim"
1371
+ # fm-wake-drain.sh's real drained record (raw tab-separated queue line).
1372
+ assert_contains "$out" "$(printf 'signal\ttask-z.status\tneeds-decision: pick a library')" "fm-wake-drain.sh's real drained record did not appear"
1373
+ assert_contains "$out" "wake annotation: latest wake-EVENT observed at drain, not current state: task-z.status: needs-decision: pick a library" "fm-session-start.sh did not preserve the drain's separate annotation line"
1374
+
1375
+ pass "fm-session-start.sh composes the real fm-lock.sh, fm-bootstrap.sh, and fm-wake-drain.sh output verbatim"
1376
+ }
1377
+
1378
+ test_branch_outcome_replay_and_lease_sweep() {
1379
+ local rec root home fakebin out
1380
+ rec=$(new_world branch-recovery)
1381
+ IFS='|' read -r root home fakebin <<EOF
1382
+ $rec
1383
+ EOF
1384
+ make_fake_toolchain "$fakebin"
1385
+ make_fake_ps_harness "$fakebin" pi
1386
+
1387
+ # A crash window the locked start must close: the supervision branch stored
1388
+ # an outcome durably that never reached main, plus one lease whose
1389
+ # supervising process died and one still held by a live process.
1390
+ FM_HOME="$home" "$ROOT/bin/fm-branch-outcome.sh" append \
1391
+ --task task-b --verdict captain --summary 'PR https://example.com/pr/b checks green' >/dev/null \
1392
+ || fail "could not seed the unread branch outcome"
1393
+ printf 'branch\t999999\t123\n' > "$home/state/.lease-task-dead"
1394
+ FM_HOME="$home" FM_SUPERVISION_ACTOR=branch FM_LEASE_HOLDER_PID=$$ "$ROOT/bin/fm-lease.sh" claim task-live --actor branch \
1395
+ || fail "could not seed the live lease"
1396
+
1397
+ out=$(run_pi_session_start "$home" "$root" "$fakebin:$BASE_PATH")
1398
+ assert_contains "$out" "BRANCH OUTCOMES (handled by the supervision branch, not yet seen by this session):" \
1399
+ "locked start did not replay the unread branch outcome"
1400
+ assert_contains "$out" "https://example.com/pr/b" "replayed outcome lost its content"
1401
+ [ ! -e "$home/state/.lease-task-dead" ] || fail "locked start left a provably dead lease in place"
1402
+ [ -e "$home/state/.lease-task-live" ] || fail "locked start swept a live lease"
1403
+
1404
+ # Replay is one-shot: presenting the digest is the delivery, so the next
1405
+ # locked start stays silent about the same outcome.
1406
+ out=$(run_pi_session_start "$home" "$root" "$fakebin:$BASE_PATH")
1407
+ case "$out" in
1408
+ *"BRANCH OUTCOMES"*) fail "second start re-presented already-replayed branch outcomes" ;;
1409
+ esac
1410
+ pass "locked Pi session start replays unread branch outcomes once and sweeps only dead leases"
1411
+ }
1412
+
1413
+ test_non_pi_session_start_leaves_branch_state_untouched() {
1414
+ local rec root home fakebin out
1415
+ rec=$(new_world non-pi-branch-recovery)
1416
+ IFS='|' read -r root home fakebin <<EOF
1417
+ $rec
1418
+ EOF
1419
+ make_fake_toolchain "$fakebin"
1420
+ make_fake_ps_claude "$fakebin"
1421
+
1422
+ FM_HOME="$home" "$ROOT/bin/fm-branch-outcome.sh" append \
1423
+ --task task-b --verdict captain --summary 'unread Pi branch outcome' >/dev/null \
1424
+ || fail "could not seed the non-Pi unread branch outcome"
1425
+ rm -f "$home/state/.branch-outcomes-cursor"
1426
+ printf 'branch\t999999\t123\n' > "$home/state/.lease-task-dead"
1427
+
1428
+ out=$(run_session_start "$home" "$root" "$fakebin:$BASE_PATH")
1429
+ case "$out" in
1430
+ *"BRANCH OUTCOMES"*|*"unread Pi branch outcome"*) fail "non-Pi session replayed Pi branch outcomes" ;;
1431
+ esac
1432
+ [ -e "$home/state/.lease-task-dead" ] || fail "non-Pi session swept a Pi branch lease"
1433
+ [ ! -e "$home/state/.branch-outcomes-cursor" ] || fail "non-Pi session marked a Pi branch outcome read"
1434
+ pass "non-Pi session start neither sweeps nor replays Pi branch state"
1435
+ }
1436
+
1437
+ # --- deferred network stage -------------------------------------------------
1438
+
1439
+ # install_slow_gh <fakebin> <seconds>: one external-network call the digest used
1440
+ # to make directly. Making it pathologically slow is how a test stands in for an
1441
+ # unreachable host without touching one: if any part of the blocking path still
1442
+ # waits on the network, the digest cannot finish before this does.
1443
+ install_slow_gh() {
1444
+ local fakebin=$1 seconds=$2 finished_marker=${3:-}
1445
+ cat > "$fakebin/gh" <<SH
1446
+ #!/usr/bin/env bash
1447
+ if [ "\${1:-}" = auth ]; then
1448
+ sleep $seconds
1449
+ [ -z '$finished_marker' ] || : > '$finished_marker'
1450
+ exit 1
1451
+ fi
1452
+ exit 0
1453
+ SH
1454
+ chmod +x "$fakebin/gh"
1455
+ }
1456
+
1457
+ # The headline guarantee: an unreachable host delays a reported CHECK, never the
1458
+ # startup. The fake host hangs for 12s; the digest must be done long before that,
1459
+ # must say so rather than implying the checks passed, and the sweeps must still
1460
+ # run and land afterwards.
1461
+ test_unreachable_network_never_blocks_the_digest() {
1462
+ local rec root home fakebin mate log spawned network_finished out started elapsed
1463
+ rec=$(prepare_session_start_secondmate secondmate-slow-network)
1464
+ IFS='|' read -r root home fakebin mate log spawned <<EOF
1465
+ $rec
1466
+ EOF
1467
+ network_finished="${root%/root}/network-finished"
1468
+ install_slow_gh "$fakebin" 12 "$network_finished"
1469
+
1470
+ started=$(date +%s)
1471
+ out=$(run_session_start_secondmate "$root" "$home" "$fakebin" "$mate" "$log" "$spawned" missing)
1472
+ elapsed=$(( $(date +%s) - started ))
1473
+
1474
+ [ ! -e "$network_finished" ] \
1475
+ || fail "the digest waited for the 12s unreachable-host probe instead of returning from local state (${elapsed}s)"
1476
+ assert_contains "$out" "SESSION START" "the digest did not complete"
1477
+ assert_contains "$out" "IN PROGRESS - the deferred network checks have not finished yet." \
1478
+ "the digest did not disclose that its network checks were still running"
1479
+ assert_contains "$out" "NOT yet confirmed: GitHub authentication, dead-secondmate relaunch" \
1480
+ "the digest did not name the checks it has not confirmed"
1481
+ assert_not_contains "$out" "NEEDS_GH_AUTH" \
1482
+ "the digest reported a GitHub-auth verdict it could not yet have"
1483
+
1484
+ # ... and the work itself still happens, off the blocking path.
1485
+ wait_for_network_stage "$home" "$root" 60 \
1486
+ || fail "the deferred stage never finished: $(network_stage_report "$home" "$root")"
1487
+ assert_contains "$(network_stage_report "$home" "$root")" "NEEDS_GH_AUTH" \
1488
+ "the deferred stage lost the GitHub-auth verdict it was deferring"
1489
+ assert_contains "$(cat "$log")" "new-window" \
1490
+ "the deferred stage lost the dead-secondmate relaunch"
1491
+ pass "session start: an unreachable host delays a reported check, not the digest"
1492
+ }
1493
+
1494
+ # A result the digest could not print must still reach the agent by itself. The
1495
+ # opposite half of the handshake - a printed result never ALSO queuing a wake -
1496
+ # is asserted deterministically in tests/fm-startup-network.test.sh, where the
1497
+ # claim can be set up directly instead of raced against digest composition.
1498
+ test_deferred_result_reaches_the_agent_when_the_digest_cannot_print_it() {
1499
+ local rec root home fakebin mate log spawned queue
1500
+ rec=$(prepare_session_start_secondmate secondmate-wake-once)
1501
+ IFS='|' read -r root home fakebin mate log spawned <<EOF
1502
+ $rec
1503
+ EOF
1504
+ install_slow_gh "$fakebin" 8
1505
+ queue="$home/state/.wake-queue"
1506
+
1507
+ run_session_start_secondmate "$root" "$home" "$fakebin" "$mate" "$log" "$spawned" missing >/dev/null
1508
+ wait_for_network_stage "$home" "$root" 60 || fail "the deferred stage never finished"
1509
+ wait_for_network_wake "$home" 60 || fail "the deferred stage never settled wake delivery"
1510
+ assert_grep 'check startup-network' "$queue" \
1511
+ "a result the digest could not print never reached the agent: $(cat "$queue" 2>/dev/null)"
1512
+ pass "session start: a deferred result the digest outran still reaches the agent as a wake"
1513
+ }
1514
+
1515
+ # A read-only session has no lock, so it neither owns the mutating sweeps nor has
1516
+ # any action a GitHub-auth verdict would gate. It must say that plainly instead of
1517
+ # quietly dropping the checks.
1518
+ test_read_only_session_declares_skipped_network_checks() {
1519
+ local rec root home fakebin out
1520
+ rec=$(new_world network-read-only)
1521
+ IFS='|' read -r root home fakebin <<EOF
1522
+ $rec
1523
+ EOF
1524
+ make_fake_toolchain "$fakebin"
1525
+ make_fake_ps_claude "$fakebin"
1526
+ printf '999999\n' > "$home/state/.lock"
1527
+ cat > "$fakebin/ps" <<'SH'
1528
+ #!/usr/bin/env bash
1529
+ set -u
1530
+ case "$*" in
1531
+ *"-p 999999"*) printf 'claude\n'; exit 0 ;;
1532
+ *"comm="*|*"args="*) printf 'bash\n'; exit 0 ;;
1533
+ esac
1534
+ exit 0
1535
+ SH
1536
+ chmod +x "$fakebin/ps"
1537
+
1538
+ out=$(run_session_start "$home" "$root" "$fakebin:$BASE_PATH")
1539
+
1540
+ assert_contains "$out" "READ-ONLY SESSION" "the read-only fixture did not actually refuse the lock"
1541
+ assert_contains "$out" "skipped (read-only session) - GitHub authentication" \
1542
+ "a read-only session did not declare its skipped network checks"
1543
+ assert_absent "$home/state/.startup-network.status" \
1544
+ "a read-only session started the deferred stage it has no authority for"
1545
+ pass "session start: a read-only session declares its skipped network checks rather than dropping them"
1546
+ }
1547
+
1548
+ # The compatibility verdict costs three tasks-axi subprocesses and one session
1549
+ # start needs it twice. The digest must pay for it once.
1550
+ test_tasks_axi_compatibility_is_probed_once() {
1551
+ local rec root home fakebin log probes
1552
+ rec=$(new_world tasks-axi-once)
1553
+ IFS='|' read -r root home fakebin <<EOF
1554
+ $rec
1555
+ EOF
1556
+ make_fake_toolchain "$fakebin"
1557
+ make_fake_ps_claude "$fakebin"
1558
+ make_fake_tasks_axi_compact "$fakebin"
1559
+ log="$home/tasks-axi.log"
1560
+ printf '# Backlog\n\n## In flight\n\n## Queued\n' > "$home/data/backlog.md"
1561
+
1562
+ FM_FAKE_TASKS_AXI_LOG="$log" run_session_start "$home" "$root" "$fakebin:$BASE_PATH" >/dev/null
1563
+
1564
+ probes=$(grep -c -- '--version' "$log" || true)
1565
+ [ "$probes" -eq 1 ] \
1566
+ || fail "tasks-axi was version-probed $probes times in one session start: $(cat "$log")"
1567
+ probes=$(grep -c -- 'update --help' "$log" || true)
1568
+ [ "$probes" -eq 1 ] \
1569
+ || fail "tasks-axi update --help ran $probes times in one session start: $(cat "$log")"
1570
+ assert_grep 'ready --file' "$log" "the backlog listing never ran, so the verdict was not actually reused"
1571
+ pass "session start: the tasks-axi compatibility verdict is computed once and reused"
1572
+ }
1573
+
1574
+ # --- fleet-state digest: compact backlog rendering --------------------------
1575
+
1576
+ # A backlog whose Done section, held row, blocked row, and plain queued rows can
1577
+ # each be told apart in the rendered digest. DONE-ROW-LINE and the *-BODY-LINE
1578
+ # markers exist so a leak is unmistakable.
1579
+ write_long_body_backlog() {
1580
+ local path=$1 i=1
1581
+ cat > "$path" <<'EOF'
1582
+ # Backlog
1583
+
1584
+ ## In flight
1585
+ - [ ] compact-startup - Compact startup digest (repo: firstmate) (kind: ship) (since 2026-07-15) (hold: captain choice pending) (hold-kind: captain)
1586
+ OVERSIZED-BODY-LINE current startup leaks task note bodies into the session digest.
1587
+ Another long body line that should not be printed after the fix.
1588
+
1589
+ ## Queued
1590
+ - [ ] blocked-followup - Follow compact startup blocked-by: compact-startup - waits for implementation (repo: firstmate) (kind: scout) (since 2026-07-15)
1591
+ QUEUED-BODY-LINE this is another long multiline note.
1592
+ - [ ] held-queued - Held queued work (repo: firstmate) (kind: ship) (hold: captain choice pending) (hold-kind: captain)
1593
+ EOF
1594
+ while [ "$i" -le 25 ]; do
1595
+ printf -- '- [ ] plain-%s - Plain queued item %s (repo: firstmate) (kind: ship)\n' "$i" "$i" >> "$path"
1596
+ i=$((i + 1))
1597
+ done
1598
+ cat >> "$path" <<'EOF'
1599
+
1600
+ ## Done
1601
+ - [x] landed-earlier - DONE-ROW-LINE already landed and torn down (repo: firstmate) (kind: ship)
1602
+ EOF
1603
+ }
1604
+
1605
+ test_backlog_compact_tasks_axi_omits_bodies_and_keeps_metadata() {
1606
+ local rec root home fakebin out log
1607
+ rec=$(new_world backlog-compact-tasks-axi)
1608
+ IFS='|' read -r root home fakebin <<EOF
1609
+ $rec
1610
+ EOF
1611
+ make_fake_toolchain "$fakebin"
1612
+ make_fake_tasks_axi_compact "$fakebin"
1613
+ make_fake_ps_claude "$fakebin"
1614
+ write_long_body_backlog "$home/data/backlog.md"
1615
+ mkdir -p "$home/projects/firstmate"
1616
+ printf 'window=fm-sess:compact\nworktree=%s\nproject=firstmate\nkind=ship\n' "$home/projects/firstmate" \
1617
+ > "$home/state/compact-startup.meta"
1618
+ log="$home/tasks-axi.log"
1619
+
1620
+ out=$(FM_FAKE_TASKS_AXI_LOG="$log" FM_FAKE_TASKS_AXI_READY=3 \
1621
+ run_session_start "$home" "$root" "$fakebin:$BASE_PATH")
1622
+
1623
+ assert_contains "$out" "compact backlog listing (tasks-axi; done rows omitted; every in-flight, held, and blocked row shown in full; ready queued bounded to 20; task bodies omitted)" \
1624
+ "compatible tasks-axi backend did not render the compact backlog listing"
1625
+ assert_contains "$out" "tasks[1]{id,state,kind,repo,title,blocked_by,hold_kind,hold_reason}:" \
1626
+ "tasks-axi compact listing omitted the expected structured field header"
1627
+ assert_contains "$out" "compact-startup,in_flight,ship,firstmate,Compact startup digest,none,captain,captain choice pending" \
1628
+ "tasks-axi compact listing omitted in-flight identity, state, or hold metadata"
1629
+ assert_contains "$out" "held-queued,queued,ship,firstmate,Held queued work,none,captain,captain choice pending" \
1630
+ "tasks-axi compact listing omitted a held row or its hold metadata"
1631
+ assert_contains "$out" 'blocked-followup,queued,scout,firstmate,Follow compact startup,compact-startup,"-","-"' \
1632
+ "tasks-axi compact listing omitted blocked-by metadata"
1633
+ assert_contains "$out" "ready-3,queued,ship,firstmate,Ready item 3" \
1634
+ "tasks-axi compact listing omitted a dispatchable queued row inside the bound"
1635
+ assert_not_contains "$out" "OVERSIZED-BODY-LINE" "tasks-axi compact digest leaked an in-flight task body"
1636
+ assert_not_contains "$out" "QUEUED-BODY-LINE" "tasks-axi compact digest leaked a queued task body"
1637
+ assert_not_contains "$out" "DONE-ROW-LINE" "tasks-axi compact digest listed a done row at startup"
1638
+ assert_contains "$out" "--- compact-startup ---" "in-flight meta identity disappeared from startup recovery digest"
1639
+ assert_contains "$out" "worktree=$home/projects/firstmate" "in-flight recovery worktree identity disappeared from startup digest"
1640
+ assert_contains "$out" "Full task bodies remain available on demand: tasks-axi show <id> --full" \
1641
+ "compact digest omitted the full-body lookup pointer"
1642
+ assert_contains "$out" "ready_public_followups: 0 delivery-ready obligations" \
1643
+ "the composed listing dropped a real signal from the dispatchable set"
1644
+ # One section pointer, not one repeated help block per composed group.
1645
+ assert_not_contains "$out" "help[1]:" \
1646
+ "the composed listing repeated tasks-axi's per-group help block"
1647
+
1648
+ # The fake refuses a body field, an unfiltered listing, and a done listing, so
1649
+ # a clean render already proves those were never asked for; pin the group
1650
+ # filters the listing is built from.
1651
+ assert_grep "--state in_flight --fields blocked_by,hold_kind,hold_reason" "$log" \
1652
+ "session start did not ask tasks-axi for the in-flight group"
1653
+ assert_grep "--state held --fields blocked_by,hold_kind,hold_reason" "$log" \
1654
+ "session start did not ask tasks-axi for the held group"
1655
+ assert_grep "--state queued --blocked --fields blocked_by,hold_kind,hold_reason" "$log" \
1656
+ "session start did not ask tasks-axi for the blocked queued group"
1657
+ assert_grep "ready --file $home/data/backlog.md" "$log" \
1658
+ "session start did not ask tasks-axi for the dispatchable queued set"
1659
+
1660
+ pass "compatible tasks-axi backlog rendering drops done rows and keeps every in-flight, held, and blocked row"
1661
+ }
1662
+
1663
+ # The bound may only ever cut the dispatchable-now listing, and whatever it cuts
1664
+ # must be disclosed with an exact count and the command that shows the rest.
1665
+ test_backlog_queued_bound_discloses_its_remainder() {
1666
+ local rec root home fakebin out
1667
+ rec=$(new_world backlog-queued-bound)
1668
+ IFS='|' read -r root home fakebin <<EOF
1669
+ $rec
1670
+ EOF
1671
+ make_fake_toolchain "$fakebin"
1672
+ make_fake_tasks_axi_compact "$fakebin"
1673
+ make_fake_ps_claude "$fakebin"
1674
+ write_long_body_backlog "$home/data/backlog.md"
1675
+
1676
+ out=$(FM_FAKE_TASKS_AXI_READY=7 FM_SESSION_START_QUEUED_LIMIT=3 \
1677
+ run_session_start "$home" "$root" "$fakebin:$BASE_PATH")
1678
+
1679
+ assert_contains "$out" "ready-3,queued,ship,firstmate,Ready item 3" \
1680
+ "the queued bound dropped a row inside its own limit"
1681
+ assert_not_contains "$out" "ready-4,queued" "the queued bound did not actually bound the ready listing"
1682
+ assert_contains "$out" "(shown 3 of 7 ready queued item(s))" \
1683
+ "the bounded queued listing did not report what it showed"
1684
+ assert_contains "$out" "(4 more queued - tasks-axi ready --file $home/data/backlog.md)" \
1685
+ "the bounded queued listing did not disclose an exact remainder and how to see it"
1686
+
1687
+ # The bound is for dispatchable work only: held and blocked rows stay whole.
1688
+ assert_contains "$out" "held-queued,queued,ship,firstmate,Held queued work,none,captain,captain choice pending" \
1689
+ "the queued bound swallowed a held row"
1690
+ assert_contains "$out" 'blocked-followup,queued,scout,firstmate,Follow compact startup,compact-startup,"-","-"' \
1691
+ "the queued bound swallowed a blocked row"
1692
+ assert_contains "$out" "compact-startup,in_flight,ship,firstmate,Compact startup digest,none,captain,captain choice pending" \
1693
+ "the queued bound swallowed an in-flight row"
1694
+
1695
+ pass "the startup backlog bound cuts only dispatchable queued rows and discloses the remainder exactly"
1696
+ }
1697
+
1698
+ test_backlog_compact_manual_backend_skips_indented_bodies() {
1699
+ local rec root home fakebin out
1700
+ rec=$(new_world backlog-compact-manual)
1701
+ IFS='|' read -r root home fakebin <<EOF
1702
+ $rec
1703
+ EOF
1704
+ make_fake_toolchain "$fakebin"
1705
+ make_fake_ps_claude "$fakebin"
1706
+ printf '%s\n' manual > "$home/config/backlog-backend"
1707
+ write_long_body_backlog "$home/data/backlog.md"
1708
+
1709
+ out=$(FM_SESSION_START_QUEUED_LIMIT=4 run_session_start "$home" "$root" "$fakebin:$BASE_PATH")
1710
+
1711
+ assert_contains "$out" "compact backlog listing (manual backend; done rows omitted; every in-flight, held, and blocked title line kept; other queued bounded to 4; indented task bodies omitted)" \
1712
+ "manual backend did not use compact title-line rendering"
1713
+ assert_contains "$out" "## In flight" "manual compact rendering omitted the in-flight section heading"
1714
+ assert_contains "$out" "- [ ] compact-startup - Compact startup digest" \
1715
+ "manual compact rendering omitted the in-flight title line"
1716
+ assert_contains "$out" "(hold: captain choice pending) (hold-kind: captain)" \
1717
+ "manual compact rendering omitted hold metadata"
1718
+ assert_contains "$out" "blocked-by: compact-startup - waits for implementation" \
1719
+ "manual compact rendering omitted blocker metadata"
1720
+ assert_contains "$out" "- [ ] held-queued - Held queued work" \
1721
+ "manual compact rendering dropped a held queued title line"
1722
+ assert_not_contains "$out" "OVERSIZED-BODY-LINE" "manual compact digest leaked an in-flight task body"
1723
+ assert_not_contains "$out" "QUEUED-BODY-LINE" "manual compact digest leaked a queued task body"
1724
+ assert_not_contains "$out" "DONE-ROW-LINE" "manual compact digest listed a done row at startup"
1725
+ assert_not_contains "$out" "## Done" "manual compact digest printed the done heading it never fills"
1726
+ assert_contains "$out" "- [ ] plain-4 - Plain queued item 4" \
1727
+ "manual compact rendering dropped a queued title line inside its bound"
1728
+ assert_not_contains "$out" "- [ ] plain-5 - Plain queued item 5" \
1729
+ "manual compact rendering did not bound its plain queued listing"
1730
+ assert_contains "$out" "(shown 1 in-flight, 2 held or blocked queued, 4 of 25 other queued title line(s); 1 done row(s) omitted)" \
1731
+ "manual compact rendering did not report its bound accounting"
1732
+ assert_contains "$out" "(21 more queued - raise FM_SESSION_START_QUEUED_LIMIT or read data/backlog.md for the rest)" \
1733
+ "manual compact rendering did not disclose an exact queued remainder"
1734
+ assert_contains "$out" "or data/backlog.md" "manual compact digest omitted the data/backlog.md full-body pointer"
1735
+
1736
+ pass "manual backlog rendering drops done rows, keeps every held or blocked title line, and bounds the rest"
1737
+ }
1738
+
1739
+ test_backlog_compact_tasks_axi_unavailable_uses_manual_fallback() {
1740
+ local rec root home fakebin out
1741
+ rec=$(new_world backlog-compact-unavailable)
1742
+ IFS='|' read -r root home fakebin <<EOF
1743
+ $rec
1744
+ EOF
1745
+ make_fake_toolchain "$fakebin"
1746
+ make_fake_ps_claude "$fakebin"
1747
+ write_long_body_backlog "$home/data/backlog.md"
1748
+
1749
+ out=$(run_session_start "$home" "$root" "$fakebin:$BASE_PATH")
1750
+
1751
+ assert_contains "$out" "compact backlog listing (tasks-axi unavailable or incompatible; done rows omitted;" \
1752
+ "unavailable tasks-axi did not fall back to compact title-line rendering"
1753
+ assert_contains "$out" "- [ ] compact-startup - Compact startup digest" \
1754
+ "unavailable tasks-axi fallback omitted a backlog title line"
1755
+ assert_not_contains "$out" "OVERSIZED-BODY-LINE" "unavailable tasks-axi fallback leaked an in-flight task body"
1756
+ assert_not_contains "$out" "DONE-ROW-LINE" "unavailable tasks-axi fallback listed a done row at startup"
1757
+
1758
+ pass "unavailable or incompatible tasks-axi falls back to compact manual backlog rendering"
1759
+ }
1760
+
1761
+ # --- runtime bound -----------------------------------------------------------
1762
+ #
1763
+ # The digest runs on a session-open hook that blocks session initialization, so
1764
+ # it must have a guaranteed upper bound. These cases drive REAL processes that
1765
+ # really hang, and assert the outcome the hook depends on: whatever the digest
1766
+ # already emitted survives, the agent is told exactly what it never saw, and
1767
+ # the command still exits 0 so the session can open.
1768
+
1769
+ # make_hanging_tool <fakebin> <name>: a real, unkillable-by-timeout-alone
1770
+ # subprocess of the digest. `git` is the honest choice - the bootstrap stage
1771
+ # shells out to it - and it also proves the bound reaches a GRANDCHILD, because
1772
+ # bootstrap runs it inside its own command substitution.
1773
+ make_hanging_tool() {
1774
+ local fakebin=$1 name=$2
1775
+ cat > "$fakebin/$name" <<'SH'
1776
+ #!/usr/bin/env bash
1777
+ trap '' TERM
1778
+ sleep 600
1779
+ SH
1780
+ chmod +x "$fakebin/$name"
1781
+ }
1782
+
1783
+ make_term_escalating_timeout() {
1784
+ local fakebin=$1
1785
+ cat > "$fakebin/timeout" <<'SH'
1786
+ #!/usr/bin/env perl
1787
+ use strict;
1788
+ use warnings;
1789
+ (shift @ARGV) eq '-k' or exit 64;
1790
+ my $kill_after = shift @ARGV;
1791
+ my $seconds = shift @ARGV;
1792
+ my $pid = fork;
1793
+ defined $pid or die "fork failed";
1794
+ if (!$pid) {
1795
+ setpgrp(0, 0);
1796
+ exec @ARGV;
1797
+ }
1798
+ local $SIG{ALRM} = sub {
1799
+ kill 'TERM', -$pid;
1800
+ select undef, undef, undef, $kill_after;
1801
+ kill 'KILL', -$pid;
1802
+ waitpid $pid, 0;
1803
+ exit 137;
1804
+ };
1805
+ alarm $seconds;
1806
+ waitpid $pid, 0;
1807
+ alarm 0;
1808
+ exit($? >> 8);
1809
+ SH
1810
+ chmod +x "$fakebin/timeout"
1811
+ }
1812
+
1813
+ test_runtime_bound_truncates_loudly_and_exits_zero() {
1814
+ local rec root home fakebin out status=0 stray mechanism
1815
+ rec=$(new_world runtime-bound)
1816
+ IFS='|' read -r root home fakebin <<EOF
1817
+ $rec
1818
+ EOF
1819
+ make_fake_toolchain "$fakebin"
1820
+ make_fake_ps_claude "$fakebin"
1821
+ make_hanging_tool "$fakebin" git
1822
+
1823
+ mechanism=$(FM_TIMEOUT_MECHANISM_OVERRIDE=bash bash -c '. "$1"; fm_timeout_mechanism' \
1824
+ _ "$ROOT/bin/fm-timeout-lib.sh")
1825
+ [ "$mechanism" = bash ] || fail "the forced pure-Bash timeout fixture selected '$mechanism'"
1826
+
1827
+ out=$(FM_TIMEOUT_MECHANISM_OVERRIDE=bash FM_SESSION_START_TIMEOUT=3 FM_STARTUP_NETWORK_TIMEOUT=2 \
1828
+ run_session_start "$home" "$root" "$fakebin:$BASE_PATH") || status=$?
1829
+
1830
+ expect_code 0 "$status" "a truncated session start must still exit 0 so the session can open"
1831
+ assert_contains "$out" "SESSION START - $home" "the truncated digest lost the output it had already produced"
1832
+ assert_contains "$out" "LOCK" "the truncated digest lost a stage that had completed"
1833
+ assert_contains "$out" "STARTUP TRUNCATED - SESSION START HIT ITS" "a truncated session start did not say so"
1834
+ assert_contains "$out" "RUNTIME BOUND" "the truncation banner did not name the bound it hit"
1835
+ assert_contains "$out" 'stopped during the "bootstrap" stage' "the truncation banner did not name the incomplete stage"
1836
+ assert_contains "$out" "RECONCILE these stages" "the truncation banner did not tell the agent what to reconcile"
1837
+ assert_contains "$out" "wake-queue supervision-instructions read-once fleet-state network-checks context next-step" \
1838
+ "the truncation banner did not list every stage that never ran"
1839
+ assert_not_contains "$out" "NEXT STEP" "a truncated digest claimed to have reached its closing reminder"
1840
+ assert_absent "$home/state/.session-start-complete" \
1841
+ "a truncated startup recorded itself as complete"
1842
+
1843
+ # The bound must reach the whole process group: a hung grandchild that
1844
+ # outlives the digest would keep holding whatever the digest was waiting on.
1845
+ # There are now TWO bounds, deliberately independent - the digest's, and the
1846
+ # deferred network stage's own - because a truncated digest must not kill work
1847
+ # it was never waiting for. So the guarantee asserted here is the one that
1848
+ # actually matters: once BOTH deadlines have passed, nothing hung is left.
1849
+ FM_HOME="$home" FM_ROOT_OVERRIDE="$root" FM_STARTUP_NETWORK_TIMEOUT=2 \
1850
+ "$ROOT/bin/fm-startup-network.sh" wait 30 >/dev/null || true
1851
+ sleep 1
1852
+ stray=$(pgrep -f "$fakebin/git" 2>/dev/null | wc -l | tr -d ' ')
1853
+ [ "$stray" -eq 0 ] || fail "the runtime bound left $stray hung subprocess(es) behind"
1854
+
1855
+ status=0
1856
+ FM_TIMEOUT_MECHANISM_OVERRIDE=bash bash -c \
1857
+ '. "$1"; fm_run_timed 2 bash -c "exit 137"' _ "$ROOT/bin/fm-timeout-lib.sh" || status=$?
1858
+ expect_code 137 "$status" "pure-Bash natural command exit 137"
1859
+
1860
+ pass "the pure-Bash watchdog bounds session start, kills its hung grandchild, and emits the truncation contract"
1861
+ }
1862
+
1863
+ test_portable_timeout_escalates_term_resistant_process() {
1864
+ local fakebin="$TMP_ROOT/portable-kill-after" driver status=0
1865
+ mkdir -p "$fakebin"
1866
+ make_term_escalating_timeout "$fakebin"
1867
+ driver="$TMP_ROOT/portable-kill-after-driver.sh"
1868
+ cat > "$driver" <<'SH'
1869
+ #!/usr/bin/env bash
1870
+ . "$1"
1871
+ shift
1872
+ fm_run_timed 1 "$@"
1873
+ SH
1874
+ chmod +x "$driver"
1875
+
1876
+ perl -e '
1877
+ my $pid = fork;
1878
+ die "fork failed" unless defined $pid;
1879
+ if (!$pid) { setpgrp(0, 0); exec @ARGV }
1880
+ local $SIG{ALRM} = sub { kill "KILL", -$pid; waitpid $pid, 0; exit 99 };
1881
+ alarm 5;
1882
+ waitpid $pid, 0;
1883
+ exit($? >> 8);
1884
+ ' env PATH="$fakebin:$BASE_PATH" "$driver" "$ROOT/bin/fm-timeout-lib.sh" \
1885
+ perl -e '$SIG{TERM} = "IGNORE"; sleep 600' || status=$?
1886
+
1887
+ expect_code 124 "$status" "portable timeout TERM-resistant escalation"
1888
+ status=0
1889
+ env PATH="$fakebin:$BASE_PATH" "$driver" "$ROOT/bin/fm-timeout-lib.sh" \
1890
+ bash -c 'exit 137' || status=$?
1891
+ expect_code 137 "$status" "natural command exit 137"
1892
+ pass "the portable timeout path force-kills a command that ignores TERM"
1893
+ }
1894
+
1895
+ test_runtime_bound_leaves_a_healthy_digest_untouched() {
1896
+ local rec root home fakebin out
1897
+ rec=$(new_world runtime-bound-healthy)
1898
+ IFS='|' read -r root home fakebin <<EOF
1899
+ $rec
1900
+ EOF
1901
+ make_fake_toolchain "$fakebin"
1902
+ make_fake_ps_claude "$fakebin"
1903
+
1904
+ out=$(run_session_start "$home" "$root" "$fakebin:$BASE_PATH")
1905
+
1906
+ # The banner line itself, not the phrase: the read-once contract names the
1907
+ # banner as the condition that voids it, and that mention is not a banner.
1908
+ assert_not_contains "$out" "STARTUP TRUNCATED - SESSION START HIT ITS" \
1909
+ "a digest that finished in time reported itself truncated"
1910
+ assert_contains "$out" "NEXT STEP" "a digest that finished in time lost its closing reminder"
1911
+ assert_absent "${TMPDIR:-/tmp}/fm-session-start-stage" "the stage breadcrumb leaked a fixed-name file"
1912
+
1913
+ pass "a session start inside its budget prints no truncation banner"
1914
+ }
1915
+
1916
+ test_runtime_bound_leaves_harness_ancestry_headroom() {
1917
+ local rec root home fakebin nest out
1918
+ rec=$(new_world runtime-bound-ancestry)
1919
+ IFS='|' read -r root home fakebin <<EOF
1920
+ $rec
1921
+ EOF
1922
+ make_fake_toolchain "$fakebin"
1923
+
1924
+ # Only ONE pid in the whole tree is the harness, and it sits at the very top.
1925
+ # fm-session-lock-lib.sh walks a BOUNDED sixteen parents to find it, and the
1926
+ # runtime bound spends some of that budget on its own wrapper processes, so
1927
+ # this pins that the budget still reaches a realistically deep session.
1928
+ cat > "$fakebin/ps" <<'SH'
1929
+ #!/usr/bin/env bash
1930
+ set -u
1931
+ pid=
1932
+ previous=
1933
+ for argument in "$@"; do
1934
+ [ "$previous" = -p ] && pid=$argument
1935
+ previous=$argument
1936
+ done
1937
+ case "$*" in
1938
+ *"comm="*)
1939
+ if [ "$pid" = "${FM_FAKE_HARNESS_PID:-}" ]; then printf '%s\n' /usr/local/bin/claude
1940
+ else printf '%s\n' /bin/bash; fi
1941
+ ;;
1942
+ *"args="*)
1943
+ if [ "$pid" = "${FM_FAKE_HARNESS_PID:-}" ]; then printf '%s\n' claude
1944
+ else printf '%s\n' bash; fi
1945
+ ;;
1946
+ *"ppid="*) /bin/ps -o ppid= -p "$pid" ;;
1947
+ *) exit 1 ;;
1948
+ esac
1949
+ SH
1950
+ chmod +x "$fakebin/ps"
1951
+
1952
+ # Each level forks rather than execs, so the counter really is process depth.
1953
+ nest="$home/nest.sh"
1954
+ cat > "$nest" <<'SH'
1955
+ #!/usr/bin/env bash
1956
+ set -u
1957
+ levels=$1
1958
+ shift
1959
+ if [ "$levels" -gt 0 ]; then
1960
+ bash "$0" $((levels - 1)) "$@"
1961
+ exit $?
1962
+ fi
1963
+ exec "$@"
1964
+ SH
1965
+ chmod +x "$nest"
1966
+
1967
+ # shellcheck disable=SC2016 # $$ must expand in the launched shell, not here.
1968
+ out=$(env -u CLAUDECODE -u PI_CODING_AGENT -u FM_PI_HARNESS -u GROK_AGENT \
1969
+ FM_HOME="$home" FM_ROOT_OVERRIDE="$root" PATH="$fakebin:$BASE_PATH" \
1970
+ bash -c 'export FM_FAKE_HARNESS_PID=$$; exec "$1" 8 "$2"' _ "$nest" "$SESSION_START")
1971
+
1972
+ assert_contains "$out" "lock acquired: harness pid" \
1973
+ "the runtime bound's wrapper processes pushed the harness out of the bounded ancestry walk"
1974
+ assert_not_contains "$out" "READ-ONLY SESSION" \
1975
+ "a session start eight shells below its harness was wrongly refused the lock"
1976
+
1977
+ pass "the runtime bound leaves enough ancestry headroom for a deeply nested session to take the lock"
1978
+ }
1979
+
1980
+ # --- context re-emit (--reemit) ----------------------------------------------
1981
+
1982
+ test_reemit_skips_startup_sweeps_but_keeps_the_wake_drain() {
1983
+ local rec root home fakebin network_report reemit sequence generation
1984
+ rec=$(new_world reemit)
1985
+ IFS='|' read -r root home fakebin <<EOF
1986
+ $rec
1987
+ EOF
1988
+ make_fake_toolchain "$fakebin"
1989
+ make_fake_ps_claude "$fakebin"
1990
+ mkdir -p "$home/other-secondmate/state"
1991
+ fm_write_secondmate_meta "$home/state/sm-r.meta" "$home/other-secondmate" "firstmate:fm-sm-r" alpha
1992
+ append_wake "$home/state" signal task-r "done: queued after startup" || fail "seed wake failed"
1993
+
1994
+ # A full startup reconciles the secondmate sweep and reports it.
1995
+ FM_FAKE_HARNESS_PID=$$ run_session_start "$home" "$root" "$fakebin:$BASE_PATH" >/dev/null
1996
+ wait_for_network_stage "$home" "$root" \
1997
+ || fail "the full startup fixture's deferred network stage never published"
1998
+ network_report=$(network_stage_report "$home" "$root")
1999
+ assert_contains "$network_report" "SECONDMATE_LIVENESS" \
2000
+ "the full startup fixture did not exercise a mutating sweep"
2001
+
2002
+ append_wake "$home/state" signal task-r "done: queued after the re-emit too" || fail "seed second wake failed"
2003
+ reemit=$(FM_HOME="$home" FM_ROOT_OVERRIDE="$root" FM_FAKE_HARNESS_PID=$$ PATH="$fakebin:$BASE_PATH" \
2004
+ env -u CLAUDECODE -u PI_CODING_AGENT -u FM_PI_HARNESS -u GROK_AGENT \
2005
+ "$SESSION_START" --reemit)
2006
+
2007
+ assert_contains "$reemit" "SESSION START (CONTEXT RE-EMIT) - $home" "--reemit did not label itself"
2008
+ assert_not_contains "$reemit" "SECONDMATE_LIVENESS" "--reemit repeated a mutating sweep startup already ran"
2009
+ assert_contains "$reemit" "done: queued after the re-emit too" "--reemit did not drain the wake queue"
2010
+ [ -s "$home/state/.wake-queue" ] || fail "--reemit removed the wake before its handling acknowledgement"
2011
+ sequence=$(printf '%s\n' "$reemit" | sed -n 's/^WAKE_ACK_REQUIRED:.*--ack-through \([0-9][0-9]*\) --recovery-generation [A-Za-z0-9._-][A-Za-z0-9._-]*$/\1/p' | tail -1)
2012
+ generation=$(printf '%s\n' "$reemit" | sed -n 's/^WAKE_ACK_REQUIRED:.*--ack-through [0-9][0-9]* --recovery-generation \([A-Za-z0-9._-][A-Za-z0-9._-]*\)$/\1/p' | tail -1)
2013
+ [ -n "$sequence" ] && [ -n "$generation" ] \
2014
+ || fail "--reemit omitted the generation-bound wake acknowledgement"
2015
+ FM_STATE_OVERRIDE="$home/state" "$ROOT/bin/fm-wake-drain.sh" --ack-through "$sequence" \
2016
+ --recovery-generation "$generation" || fail "--reemit wake acknowledgement failed"
2017
+ [ ! -s "$home/state/.wake-queue" ] || fail "--reemit acknowledgement left queued wakes behind"
2018
+ assert_contains "$reemit" "CONTEXT" "--reemit dropped the context digest"
2019
+ assert_contains "$reemit" "FLEET STATE" "--reemit dropped the fleet-state digest"
2020
+ assert_contains "$reemit" "NEXT STEP" "--reemit dropped the closing reminder"
2021
+
2022
+ pass "--reemit reprints the digest without repeating startup's mutating sweeps and still drains queued wakes"
2023
+ }
2024
+
2025
+ test_agents_baseline_stays_at_true_start_and_reemits_on_every_drifted_pi_compact() {
2026
+ local rec root home fakebin startup compact_equal compact_first compact_second clear_out resume_out reset_out baseline baseline_after expected_hash refresh_line bootstrap_line
2027
+ rec=$(new_world agents-refresh)
2028
+ IFS='|' read -r root home fakebin <<EOF
2029
+ $rec
2030
+ EOF
2031
+ make_fake_toolchain "$fakebin"
2032
+ make_fake_ps_harness "$fakebin" pi
2033
+ cat > "$root/AGENTS.md" <<'EOF'
2034
+ FIRSTMATE_TEST_INSTRUCTION=original
2035
+ Keep this original instruction.
2036
+ EOF
2037
+
2038
+ startup=$(FM_FAKE_HARNESS=pi run_pi_session_start "$home" "$root" "$fakebin:$BASE_PATH" --source startup)
2039
+ assert_contains "$startup" "SESSION START - $home" "true startup did not run the full digest"
2040
+ assert_present "$home/state/.session-start-agents-baseline" "true startup did not record an AGENTS baseline"
2041
+ baseline=$(cat "$home/state/.session-start-agents-baseline")
2042
+ expected_hash=$(hash_file_for_test "$root/AGENTS.md")
2043
+ [ "$(printf '%s\n' "$baseline" | sed -n '2p')" = "$expected_hash" ] \
2044
+ || fail "true startup baseline did not record the original AGENTS hash: $baseline"
2045
+
2046
+ compact_equal=$(FM_FAKE_HARNESS=pi run_pi_session_start "$home" "$root" "$fakebin:$BASE_PATH" --reemit --source compact)
2047
+ assert_not_contains "$compact_equal" "CURRENT AGENTS.md - INSTRUCTION REFRESH" \
2048
+ "an unchanged AGENTS file was unnecessarily re-emitted"
2049
+ [ "$(cat "$home/state/.session-start-agents-baseline")" = "$baseline" ] \
2050
+ || fail "a no-drift compact rewrote the true-start baseline"
2051
+
2052
+ cat > "$root/AGENTS.md" <<'EOF'
2053
+ FIRSTMATE_TEST_INSTRUCTION=updated
2054
+ The complete updated instruction must survive every stale rebuild.
2055
+ EOF
2056
+ resume_out=$(FM_FAKE_HARNESS=pi run_pi_session_start "$home" "$root" "$fakebin:$BASE_PATH" --source resume)
2057
+ assert_not_contains "$resume_out" "CURRENT AGENTS.md - INSTRUCTION REFRESH" \
2058
+ "a context-preserving continuation emitted a replacement contract"
2059
+ [ "$(cat "$home/state/.session-start-agents-baseline")" = "$baseline" ] \
2060
+ || fail "a context-preserving continuation rebased the true-start baseline"
2061
+
2062
+ compact_first=$(FM_FAKE_HARNESS=pi run_pi_session_start "$home" "$root" "$fakebin:$BASE_PATH" --reemit --source compact)
2063
+ assert_contains "$compact_first" "CURRENT AGENTS.md - INSTRUCTION REFRESH" \
2064
+ "a drifted Pi compact did not emit the replacement instructions"
2065
+ assert_contains "$compact_first" "FIRSTMATE_TEST_INSTRUCTION=updated" \
2066
+ "a drifted Pi compact did not emit the complete current AGENTS content"
2067
+ refresh_line=$(printf '%s\n' "$compact_first" | grep -n '^CURRENT AGENTS.md - INSTRUCTION REFRESH$' | head -1 | cut -d: -f1)
2068
+ bootstrap_line=$(printf '%s\n' "$compact_first" | grep -n '^BOOTSTRAP$' | head -1 | cut -d: -f1)
2069
+ [ -n "$refresh_line" ] && [ -n "$bootstrap_line" ] && [ "$refresh_line" -lt "$bootstrap_line" ] \
2070
+ || fail "replacement instructions were not emitted before the bulky digest"
2071
+ [ "$(cat "$home/state/.session-start-agents-baseline")" = "$baseline" ] \
2072
+ || fail "a drifted compact rebased the original-session baseline"
2073
+
2074
+ compact_second=$(FM_FAKE_HARNESS=pi run_pi_session_start "$home" "$root" "$fakebin:$BASE_PATH" --reemit --source compact)
2075
+ assert_contains "$compact_second" "FIRSTMATE_TEST_INSTRUCTION=updated" \
2076
+ "a second drifted compact suppressed the required replacement instructions"
2077
+ [ "$(cat "$home/state/.session-start-agents-baseline")" = "$baseline" ] \
2078
+ || fail "a repeated compact rebased the original-session baseline"
2079
+
2080
+ clear_out=$(FM_FAKE_HARNESS=pi run_pi_session_start "$home" "$root" "$fakebin:$BASE_PATH" --reemit --source clear)
2081
+ assert_not_contains "$clear_out" "CURRENT AGENTS.md - INSTRUCTION REFRESH" \
2082
+ "a Pi clear, which creates a fresh runtime, unnecessarily emitted a replacement contract"
2083
+ [ "$(cat "$home/state/.session-start-agents-baseline")" = "$baseline" ] \
2084
+ || fail "a clear rebuild rebased the original-session baseline"
2085
+
2086
+ reset_out=$(FM_FAKE_HARNESS=pi run_pi_session_start "$home" "$root" "$fakebin:$BASE_PATH" --source reset)
2087
+ assert_not_contains "$reset_out" "CURRENT AGENTS.md - INSTRUCTION REFRESH" \
2088
+ "an unrecognized reset source emitted a replacement contract"
2089
+ [ "$(cat "$home/state/.session-start-agents-baseline")" = "$baseline" ] \
2090
+ || fail "reset rebased the original-session baseline"
2091
+
2092
+ rm -f "$home/state/.session-start-agents-baseline"
2093
+ compact_first=$(FM_FAKE_HARNESS=pi run_pi_session_start "$home" "$root" "$fakebin:$BASE_PATH" --reemit --source compact)
2094
+ assert_contains "$compact_first" "FIRSTMATE_TEST_INSTRUCTION=updated" \
2095
+ "a missing baseline did not trigger first-post-fix replacement instructions"
2096
+ assert_absent "$home/state/.session-start-agents-baseline" \
2097
+ "a rebuild fabricated a baseline instead of preserving true-start-only ownership"
2098
+
2099
+ printf 'wrong-session\n%s\n' "$(hash_file_for_test "$root/AGENTS.md")" > "$home/state/.session-start-agents-baseline"
2100
+ compact_first=$(FM_FAKE_HARNESS=pi run_pi_session_start "$home" "$root" "$fakebin:$BASE_PATH" --reemit --source compact)
2101
+ assert_contains "$compact_first" "FIRSTMATE_TEST_INSTRUCTION=updated" \
2102
+ "a wrong-session baseline did not trigger replacement instructions"
2103
+ baseline_after=$(cat "$home/state/.session-start-agents-baseline")
2104
+ [ "$baseline_after" = "wrong-session
2105
+ $(hash_file_for_test "$root/AGENTS.md")" ] \
2106
+ || fail "a wrong-session baseline was rewritten during a rebuild"
2107
+
2108
+ pass "true-start AGENTS baselines stay immutable while every drifted Pi compact re-emits the current contract"
2109
+ }
2110
+
2111
+ test_read_only_pi_compact_refreshes_against_its_own_session_identity() {
2112
+ local rec root home fakebin holder_pid out baseline_before completion_before
2113
+ rec=$(new_world agents-refresh-read-only)
2114
+ IFS='|' read -r root home fakebin <<EOF
2115
+ $rec
2116
+ EOF
2117
+ make_fake_toolchain "$fakebin"
2118
+ make_fake_ps_harness "$fakebin" pi
2119
+ printf '%s\n' 'READ_ONLY_AGENTS=current' > "$root/AGENTS.md"
2120
+ FM_FAKE_HARNESS=pi run_pi_session_start "$home" "$root" "$fakebin:$BASE_PATH" --source startup >/dev/null
2121
+
2122
+ sleep 300 &
2123
+ holder_pid=$!
2124
+ printf '%s\n%s\n' "$holder_pid" "$(hash_file_for_test "$root/AGENTS.md")" \
2125
+ > "$home/state/.session-start-agents-baseline"
2126
+ printf '%s\n' "$holder_pid" > "$home/state/.lock"
2127
+ baseline_before=$(cat "$home/state/.session-start-agents-baseline")
2128
+ completion_before=$(cat "$home/state/.session-start-complete")
2129
+
2130
+ out=$(FM_FAKE_HARNESS=pi FM_FAKE_LIVE_HOLDER_PID="$holder_pid" \
2131
+ run_pi_session_start "$home" "$root" "$fakebin:$BASE_PATH" --reemit --source compact)
2132
+ kill "$holder_pid" 2>/dev/null || true
2133
+ wait "$holder_pid" 2>/dev/null || true
2134
+
2135
+ assert_contains "$out" "READ-ONLY SESSION" "competing live lock owner did not force read-only mode"
2136
+ assert_contains "$out" "READ_ONLY_AGENTS=current" \
2137
+ "read-only compact trusted another session's equal baseline"
2138
+ [ "$(cat "$home/state/.session-start-agents-baseline")" = "$baseline_before" ] \
2139
+ || fail "read-only compact mutated the competing session's baseline"
2140
+ [ "$(cat "$home/state/.session-start-complete")" = "$completion_before" ] \
2141
+ || fail "read-only compact mutated startup completion state"
2142
+
2143
+ pass "read-only Pi compact refreshes against the rebuilding session identity without mutation"
2144
+ }
2145
+
2146
+ test_codex_unreachable_reset_sources_do_not_claim_instruction_refresh() {
2147
+ local rec root home fakebin startup baseline clear_out compact_out
2148
+ rec=$(new_world codex-instruction-refresh)
2149
+ IFS='|' read -r root home fakebin <<EOF
2150
+ $rec
2151
+ EOF
2152
+ make_fake_toolchain "$fakebin"
2153
+ make_fake_ps_harness "$fakebin" codex
2154
+ printf '%s\n' 'CODEX_TEST_INSTRUCTION=original' > "$root/AGENTS.md"
2155
+
2156
+ startup=$(run_named_harness_session_start codex "$home" "$root" "$fakebin:$BASE_PATH" --source startup)
2157
+ assert_contains "$startup" "primary harness: codex" "codex fixture did not select the codex run tier"
2158
+ baseline=$(cat "$home/state/.session-start-agents-baseline")
2159
+ printf '%s\n' 'CODEX_TEST_INSTRUCTION=updated' > "$root/AGENTS.md"
2160
+
2161
+ clear_out=$(run_named_harness_session_start codex "$home" "$root" "$fakebin:$BASE_PATH" --reemit --source clear)
2162
+ compact_out=$(run_named_harness_session_start codex "$home" "$root" "$fakebin:$BASE_PATH" --reemit --source compact)
2163
+ assert_not_contains "$clear_out" "CURRENT AGENTS.md - INSTRUCTION REFRESH" \
2164
+ "Codex clear claimed an instruction-refresh channel unavailable to the tracked transport"
2165
+ assert_not_contains "$compact_out" "CURRENT AGENTS.md - INSTRUCTION REFRESH" \
2166
+ "Codex compact claimed an instruction-refresh channel unavailable to the tracked transport"
2167
+ [ "$(cat "$home/state/.session-start-agents-baseline")" = "$baseline" ] \
2168
+ || fail "an unsupported Codex rebuild rewrote the true-start baseline"
2169
+
2170
+ pass "Codex reset sources do not claim an unavailable instruction-refresh channel"
2171
+ }
2172
+
2173
+ test_agents_baseline_requires_sha256_and_successful_completion() {
2174
+ local rec root home fakebin compact_out
2175
+ rec=$(new_world agents-baseline-failures)
2176
+ IFS='|' read -r root home fakebin <<EOF
2177
+ $rec
2178
+ EOF
2179
+ make_fake_toolchain "$fakebin"
2180
+ make_fake_ps_harness "$fakebin" pi
2181
+ printf '%s\n' 'AGENTS_SHA_TEST=original' > "$root/AGENTS.md"
2182
+ printf '#!/usr/bin/env bash\nexit 1\n' > "$fakebin/shasum"
2183
+ printf '#!/usr/bin/env bash\nexit 1\n' > "$fakebin/sha256sum"
2184
+ chmod +x "$fakebin/shasum" "$fakebin/sha256sum"
2185
+
2186
+ FM_FAKE_HARNESS=pi run_pi_session_start "$home" "$root" "$fakebin:$BASE_PATH" --source startup >/dev/null
2187
+ assert_absent "$home/state/.session-start-agents-baseline" \
2188
+ "startup recorded a non-SHA-256 instruction baseline when both SHA-256 tools failed"
2189
+ printf '%s\n' 'AGENTS_SHA_TEST=updated' > "$root/AGENTS.md"
2190
+ compact_out=$(FM_FAKE_HARNESS=pi run_pi_session_start "$home" "$root" "$fakebin:$BASE_PATH" --reemit --source compact)
2191
+ assert_contains "$compact_out" "AGENTS_SHA_TEST=updated" \
2192
+ "a missing SHA-256 baseline did not conservatively refresh a supported rebuild"
2193
+
2194
+ rm -f "$fakebin/shasum" "$fakebin/sha256sum" "$home/state/.session-start-complete"
2195
+ cat > "$fakebin/mv" <<SH
2196
+ #!/usr/bin/env bash
2197
+ case "\${*: -1}" in
2198
+ "$home/state/.session-start-complete") exit 1 ;;
2199
+ esac
2200
+ exec /bin/mv "\$@"
2201
+ SH
2202
+ chmod +x "$fakebin/mv"
2203
+ FM_FAKE_HARNESS=pi run_pi_session_start "$home" "$root" "$fakebin:$BASE_PATH" --source startup >/dev/null
2204
+ assert_absent "$home/state/.session-start-complete" \
2205
+ "startup published completion despite the atomic completion write failure"
2206
+ assert_absent "$home/state/.session-start-agents-baseline" \
2207
+ "startup recorded an instruction baseline after completion publication failed"
2208
+
2209
+ pass "instruction baselines require SHA-256 and successful startup completion"
2210
+ }
2211
+
2212
+ test_reemit_keeps_repair_ownership_with_the_lock_holder() {
2213
+ local rec root home fakebin reemit readonly_out holder_pid
2214
+ rec=$(new_world reemit-tangle)
2215
+ IFS='|' read -r root home fakebin <<EOF
2216
+ $rec
2217
+ EOF
2218
+ make_fake_toolchain "$fakebin"
2219
+ make_fake_ps_claude "$fakebin"
2220
+ git -C "$root" checkout -q -B fm/reemit-tangle
2221
+
2222
+ reemit=$(FM_HOME="$home" FM_ROOT_OVERRIDE="$root" PATH="$fakebin:$BASE_PATH" \
2223
+ env -u CLAUDECODE -u PI_CODING_AGENT -u FM_PI_HARNESS -u GROK_AGENT \
2224
+ "$SESSION_START" --reemit)
2225
+
2226
+ # A re-emit skips the sweeps because it ALREADY ran them, not because it lacks
2227
+ # the lock, so it must still own repair rather than deferring to a lock holder.
2228
+ assert_contains "$reemit" "restore the primary with: git -C $root checkout main" \
2229
+ "--reemit disowned a repair it is entitled to perform"
2230
+ assert_not_contains "$reemit" "must leave restore work to the session holding the fleet lock" \
2231
+ "--reemit misreported itself as an unlocked read-only session"
2232
+
2233
+ rm -f "$home/state/.lock"
2234
+ sleep 300 &
2235
+ holder_pid=$!
2236
+ printf '%s\n' "$holder_pid" > "$home/state/.lock"
2237
+ readonly_out=$(FM_HOME="$home" FM_ROOT_OVERRIDE="$root" PATH="$fakebin:$BASE_PATH" \
2238
+ env -u CLAUDECODE -u PI_CODING_AGENT -u FM_PI_HARNESS -u GROK_AGENT \
2239
+ "$SESSION_START" --reemit)
2240
+ kill "$holder_pid" 2>/dev/null || true
2241
+ wait "$holder_pid" 2>/dev/null || true
2242
+
2243
+ assert_contains "$readonly_out" "READ-ONLY SESSION" \
2244
+ "--reemit assumed lock ownership instead of re-verifying it"
2245
+ assert_contains "$readonly_out" "must leave restore work to the session holding the fleet lock" \
2246
+ "a lock-refused --reemit still claimed repair ownership"
2247
+
2248
+ pass "--reemit re-verifies lock ownership and keeps repair ownership with whoever holds it"
2249
+ }
2250
+
2251
+ # --- fleet-state digest: no in-flight tasks ----------------------------------
2252
+
2253
+ test_fleet_digest_empty_fleet() {
2254
+ local rec root home fakebin out
2255
+ rec=$(new_world empty-fleet)
2256
+ IFS='|' read -r root home fakebin <<EOF
2257
+ $rec
2258
+ EOF
2259
+ make_fake_toolchain "$fakebin"
2260
+ make_fake_ps_claude "$fakebin"
2261
+
2262
+ out=$(run_session_start "$home" "$root" "$fakebin:$BASE_PATH")
2263
+ assert_contains "$out" "(none)" "empty fleet did not report (none) for in-flight tasks"
2264
+ assert_contains "$out" "absent" "empty fleet's AFK section did not report absent"
2265
+
2266
+ pass "an empty fleet reports (none) for in-flight tasks and an absent AFK flag"
2267
+ }
2268
+
2269
+ test_next_step_sources_x_mode_cadence() {
2270
+ local rec root home fakebin out
2271
+ rec=$(new_world next-step-x)
2272
+ IFS='|' read -r root home fakebin <<EOF
2273
+ $rec
2274
+ EOF
2275
+ make_fake_toolchain "$fakebin"
2276
+ make_fake_ps_claude "$fakebin"
2277
+ fm_fake_exit0 "$fakebin" curl jq
2278
+ printf 'FMX_PAIRING_TOKEN=tok-next-step\n' > "$home/.env"
2279
+
2280
+ out=$(run_session_start "$home" "$root" "$fakebin:$BASE_PATH")
2281
+
2282
+ assert_contains "$out" "FMX: X mode on" "bootstrap did not activate X mode"
2283
+ assert_contains "$out" "SUPERVISION OPERATING INSTRUCTIONS - primary harness: claude" "supervision block missing"
2284
+ assert_contains "$out" "- X mode: active" "supervision block did not mention X cadence"
2285
+ assert_contains "$out" "Follow the supervision operating instructions block above" "next step did not point back to the emitted supervision block"
2286
+
2287
+ pass "session start emits X-mode cadence guidance in the harness supervision block"
2288
+ }
2289
+
2290
+ test_next_step_afk_delegates_to_daemon() {
2291
+ local rec root home fakebin out
2292
+ rec=$(new_world next-step-afk)
2293
+ IFS='|' read -r root home fakebin <<EOF
2294
+ $rec
2295
+ EOF
2296
+ make_fake_toolchain "$fakebin"
2297
+ make_fake_ps_claude "$fakebin"
2298
+ : > "$home/state/.afk"
2299
+
2300
+ out=$(run_session_start "$home" "$root" "$fakebin:$BASE_PATH")
2301
+
2302
+ assert_contains "$out" "away-mode supervision is active" "AFK digest did not report away mode"
2303
+ assert_contains "$out" "Away mode is active" "next step did not switch to AFK guidance"
2304
+ assert_contains "$out" "daemon owns the watcher" "next step did not delegate watcher ownership to the daemon"
2305
+ assert_contains "$out" "- Away mode: active" "supervision block did not include active AFK state"
2306
+ assert_not_contains "$out" " bin/fm-watch-arm.sh" "AFK next step still told the agent to arm the watcher directly"
2307
+
2308
+ pass "next step delegates watcher ownership to the AFK daemon"
2309
+ }
2310
+
2311
+ test_supervision_block_exactly_one_and_pi_diagnostic() {
2312
+ local rec root home fakebin out block_count wake_line sup_line context_line
2313
+ rec=$(new_world pi-supervision-block)
2314
+ IFS='|' read -r root home fakebin <<EOF
2315
+ $rec
2316
+ EOF
2317
+ make_fake_toolchain "$fakebin"
2318
+ make_fake_ps_harness "$fakebin" pi
2319
+
2320
+ out=$(FM_FAKE_HARNESS=pi run_session_start "$home" "$root" "$fakebin:$BASE_PATH")
2321
+
2322
+ block_count=$(printf '%s\n' "$out" | grep -c '^SUPERVISION OPERATING INSTRUCTIONS - primary harness:')
2323
+ [ "$block_count" -eq 1 ] || fail "expected exactly one supervision block, got $block_count"
2324
+ assert_contains "$out" "SUPERVISION OPERATING INSTRUCTIONS - primary harness: pi" "pi supervision block missing"
2325
+ assert_contains "$out" "Mode: Pi extension background wake." "pi snippet missing from session start"
2326
+ assert_contains "$out" "PI_WATCH_EXTENSION: not loaded" "pi extension load diagnostic missing"
2327
+ assert_contains "$out" "restart plain pi so $root/.pi/extensions/fm-primary-turnend-guard.ts and $root/.pi/extensions/fm-primary-pi-watch.ts auto-load" "pi extension load diagnostic omits the turn-end guard extension"
2328
+
2329
+ wake_line=$(printf '%s\n' "$out" | grep -n '^WAKE QUEUE$' | head -1 | cut -d: -f1)
2330
+ sup_line=$(printf '%s\n' "$out" | grep -n '^SUPERVISION OPERATING INSTRUCTIONS' | head -1 | cut -d: -f1)
2331
+ context_line=$(printf '%s\n' "$out" | grep -n '^CONTEXT$' | head -1 | cut -d: -f1)
2332
+ [ "$wake_line" -lt "$sup_line" ] || fail "supervision block did not follow wake queue"
2333
+ [ "$sup_line" -lt "$context_line" ] || fail "supervision block did not precede context"
2334
+
2335
+ pass "session start emits exactly one detected harness block and reports Pi extension load state"
2336
+ }
2337
+
2338
+ test_pi_signed_primary_uses_pi_extensions_without_identity_normalization() {
2339
+ local rec root home fakebin out
2340
+ rec=$(new_world pi-signed-supervision-block)
2341
+ IFS='|' read -r root home fakebin <<EOF
2342
+ $rec
2343
+ EOF
2344
+ make_fake_toolchain "$fakebin"
2345
+ make_fake_ps_harness "$fakebin" pi-signed
2346
+
2347
+ out=$(FM_FAKE_HARNESS=pi-signed run_session_start "$home" "$root" "$fakebin:$BASE_PATH" pi-signed)
2348
+
2349
+ assert_contains "$out" "SUPERVISION OPERATING INSTRUCTIONS - primary harness: pi-signed" \
2350
+ "session start normalized a pi-signed primary to pi"
2351
+ assert_contains "$out" "Mode: Pi extension background wake." \
2352
+ "pi-signed primary did not reuse Pi's supervision protocol"
2353
+ assert_contains "$out" "PI_WATCH_EXTENSION: not loaded" \
2354
+ "pi-signed primary skipped Pi extension validation"
2355
+ assert_contains "$out" "restart pi-signed so $root/.pi/extensions/fm-primary-turnend-guard.ts and $root/.pi/extensions/fm-primary-pi-watch.ts auto-load" \
2356
+ "pi-signed extension diagnostic did not preserve the executable identity"
2357
+
2358
+ pass "session start preserves pi-signed primary identity while applying Pi extension guarantees"
2359
+ }
2360
+
2361
+ test_pi_diagnostic_rejects_stale_loaded_marker() {
2362
+ local rec root home fakebin out marker holder_pid
2363
+ rec=$(new_world pi-stale-loaded-marker)
2364
+ IFS='|' read -r root home fakebin <<EOF
2365
+ $rec
2366
+ EOF
2367
+ make_fake_toolchain "$fakebin"
2368
+
2369
+ sleep 300 &
2370
+ holder_pid=$!
2371
+ make_fake_ps_pi_holder "$fakebin" "$holder_pid"
2372
+ install_pi_turnend_extension_fixture "$root"
2373
+ install_pi_watch_extension_fixture "$root"
2374
+ marker="$home/state/.pi-watch-extension-loaded"
2375
+ printf 'stale-extension-version\n%s\n' "$holder_pid" > "$marker"
2376
+ write_pi_turnend_loaded_marker "$home" "$root" "$holder_pid"
2377
+ touch -t 203001010000 "$marker" 2>/dev/null || touch "$marker"
2378
+
2379
+ out=$(FM_FAKE_HARNESS=pi run_session_start "$home" "$root" "$fakebin:$BASE_PATH")
2380
+ kill "$holder_pid" 2>/dev/null || true
2381
+ wait "$holder_pid" 2>/dev/null || true
2382
+
2383
+ assert_contains "$out" "PI_WATCH_EXTENSION: not loaded" "pi diagnostic trusted a stale loaded marker"
2384
+
2385
+ pass "session start rejects stale Pi loaded markers"
2386
+ }
2387
+
2388
+ test_pi_diagnostic_accepts_prelock_loaded_marker() {
2389
+ local rec root home fakebin out holder_pid
2390
+ rec=$(new_world pi-prelock-loaded-marker)
2391
+ IFS='|' read -r root home fakebin <<EOF
2392
+ $rec
2393
+ EOF
2394
+ make_fake_toolchain "$fakebin"
2395
+
2396
+ sleep 300 &
2397
+ holder_pid=$!
2398
+ make_fake_ps_pi_holder "$fakebin" "$holder_pid"
2399
+ install_pi_turnend_extension_fixture "$root"
2400
+ install_pi_watch_extension_fixture "$root"
2401
+
2402
+ write_pi_loaded_markers "$home" "$root" "$holder_pid"
2403
+
2404
+ out=$(FM_FAKE_HARNESS=pi run_session_start "$home" "$root" "$fakebin:$BASE_PATH")
2405
+ kill "$holder_pid" 2>/dev/null || true
2406
+ wait "$holder_pid" 2>/dev/null || true
2407
+
2408
+ assert_not_contains "$out" "PI_WATCH_EXTENSION: not loaded" "pi diagnostic rejected a current pre-lock loaded marker"
2409
+
2410
+ pass "session start accepts current Pi markers written before lock acquisition"
2411
+ }
2412
+
2413
+ test_pi_diagnostic_rejects_missing_turnend_guard_marker() {
2414
+ local rec root home fakebin out holder_pid
2415
+ rec=$(new_world pi-missing-turnend-marker)
2416
+ IFS='|' read -r root home fakebin <<EOF
2417
+ $rec
2418
+ EOF
2419
+ make_fake_toolchain "$fakebin"
2420
+
2421
+ sleep 300 &
2422
+ holder_pid=$!
2423
+ make_fake_ps_pi_holder "$fakebin" "$holder_pid"
2424
+ install_pi_turnend_extension_fixture "$root"
2425
+ install_pi_watch_extension_fixture "$root"
2426
+
2427
+ write_pi_watch_loaded_marker "$home" "$root" "$holder_pid"
2428
+
2429
+ out=$(FM_FAKE_HARNESS=pi run_session_start "$home" "$root" "$fakebin:$BASE_PATH")
2430
+ kill "$holder_pid" 2>/dev/null || true
2431
+ wait "$holder_pid" 2>/dev/null || true
2432
+
2433
+ assert_contains "$out" "PI_WATCH_EXTENSION: not loaded" "pi diagnostic trusted a session without the turn-end guard extension"
2434
+
2435
+ pass "session start rejects Pi sessions missing the turn-end guard marker"
2436
+ }
2437
+
2438
+ test_pi_diagnostic_rejects_previous_session_loaded_marker() {
2439
+ local rec root home fakebin out marker version holder_pid
2440
+ rec=$(new_world pi-previous-session-loaded-marker)
2441
+ IFS='|' read -r root home fakebin <<EOF
2442
+ $rec
2443
+ EOF
2444
+ make_fake_toolchain "$fakebin"
2445
+
2446
+ sleep 300 &
2447
+ holder_pid=$!
2448
+ make_fake_ps_pi_holder "$fakebin" "$holder_pid"
2449
+ install_pi_turnend_extension_fixture "$root"
2450
+ install_pi_watch_extension_fixture "$root"
2451
+ marker="$home/state/.pi-watch-extension-loaded"
2452
+ version=$(hash_file_for_test "$root/.pi/extensions/fm-primary-pi-watch.ts")
2453
+ printf '%s\n999999\n' "$version" > "$marker"
2454
+ write_pi_turnend_loaded_marker "$home" "$root" "$holder_pid"
2455
+
2456
+ out=$(FM_FAKE_HARNESS=pi run_session_start "$home" "$root" "$fakebin:$BASE_PATH")
2457
+ kill "$holder_pid" 2>/dev/null || true
2458
+ wait "$holder_pid" 2>/dev/null || true
2459
+
2460
+ assert_contains "$out" "PI_WATCH_EXTENSION: not loaded" "pi diagnostic trusted a marker from a previous Pi process"
2461
+
2462
+ pass "session start rejects Pi loaded markers from previous sessions"
2463
+ }
2464
+
2465
+ test_context_digest_absent_empty_present
2466
+ test_lock_refusal_read_only_path
2467
+ test_lock_write_failure_read_only_path
2468
+ test_trace_context_effective_state_is_frozen_after_lock
2469
+ test_session_lock_concurrent_single_winner
2470
+ test_output_ordering_diagnostics_lead
2471
+ test_read_once_contract_is_stated_once_before_its_subject
2472
+ test_herdr_backend_diagnostics_follow_real_session_start
2473
+ test_session_start_relaunches_missing_pi_secondmate
2474
+ test_deferred_relaunch_is_always_reported
2475
+ test_unreachable_network_never_blocks_the_digest
2476
+ test_deferred_result_reaches_the_agent_when_the_digest_cannot_print_it
2477
+ test_read_only_session_declares_skipped_network_checks
2478
+ test_tasks_axi_compatibility_is_probed_once
2479
+ test_session_start_preserves_ambiguous_pi_process
2480
+ test_session_start_preserves_transiently_unreadable_tmux
2481
+ test_session_start_preserves_proven_bare_shell_recovery
2482
+ test_session_start_relaunches_herdr_husk_secondmate
2483
+ test_status_tail_bounding
2484
+ test_status_tail_line_cap
2485
+ test_orphan_status_logs_are_printed
2486
+ test_endpoint_liveness_tmux
2487
+ test_endpoint_liveness_herdr
2488
+ test_composition_invokes_real_scripts
2489
+ test_branch_outcome_replay_and_lease_sweep
2490
+ test_non_pi_session_start_leaves_branch_state_untouched
2491
+ test_backlog_compact_tasks_axi_omits_bodies_and_keeps_metadata
2492
+ test_backlog_queued_bound_discloses_its_remainder
2493
+ test_backlog_compact_manual_backend_skips_indented_bodies
2494
+ test_backlog_compact_tasks_axi_unavailable_uses_manual_fallback
2495
+ test_fleet_digest_empty_fleet
2496
+ test_next_step_sources_x_mode_cadence
2497
+ test_next_step_afk_delegates_to_daemon
2498
+ test_supervision_block_exactly_one_and_pi_diagnostic
2499
+ test_pi_signed_primary_uses_pi_extensions_without_identity_normalization
2500
+ test_pi_diagnostic_rejects_stale_loaded_marker
2501
+ test_pi_diagnostic_accepts_prelock_loaded_marker
2502
+ test_pi_diagnostic_rejects_missing_turnend_guard_marker
2503
+ test_pi_diagnostic_rejects_previous_session_loaded_marker
2504
+ test_runtime_bound_truncates_loudly_and_exits_zero
2505
+ test_portable_timeout_escalates_term_resistant_process
2506
+ test_runtime_bound_leaves_a_healthy_digest_untouched
2507
+ test_runtime_bound_leaves_harness_ancestry_headroom
2508
+ test_reemit_skips_startup_sweeps_but_keeps_the_wake_drain
2509
+ test_agents_baseline_stays_at_true_start_and_reemits_on_every_drifted_pi_compact
2510
+ test_read_only_pi_compact_refreshes_against_its_own_session_identity
2511
+ test_codex_unreachable_reset_sources_do_not_claim_instruction_refresh
2512
+ test_agents_baseline_requires_sha256_and_successful_completion
2513
+ test_reemit_keeps_repair_ownership_with_the_lock_holder
2514
+
2515
+ echo "# fm-session-start.test.sh: all assertions passed"