@zerwiz/ymir 0.1.4 → 0.1.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (850) hide show
  1. package/.agents/README.md +291 -0
  2. package/.agents/agents/bragi-marketer.md +111 -0
  3. package/.agents/agents/brokk.md +352 -0
  4. package/.agents/agents/forseti-reviewer.md +93 -0
  5. package/.agents/agents/frigg-consent.md +77 -0
  6. package/.agents/agents/groa-update.md +77 -0
  7. package/.agents/agents/hnoss-designer.md +69 -0
  8. package/.agents/agents/huginn-researcher.md +99 -0
  9. package/.agents/agents/jord-projects.md +78 -0
  10. package/.agents/agents/kvasir-scout.md +91 -0
  11. package/.agents/agents/mimir-planner.md +92 -0
  12. package/.agents/agents/muninn-stow.md +77 -0
  13. package/.agents/agents/saga-bearings.md +78 -0
  14. package/.agents/agents/sindri-developer.md +114 -0
  15. package/.agents/agents/snotra-documenter.md +105 -0
  16. package/.agents/agents/syn-recovery.md +78 -0
  17. package/.agents/agents/tyr-check.md +78 -0
  18. package/.agents/agents/urdh-hold.md +77 -0
  19. package/.agents/agents/volundr-smith.md +80 -0
  20. package/.agents/agents/vor-diagnostics.md +77 -0
  21. package/.agents/assets/agents/naming.md +50 -0
  22. package/.agents/assets/agents/registry.md +93 -0
  23. package/.agents/assets/agents/runtime.md +36 -0
  24. package/.agents/assets/agents/toon-tasks-cli.md +42 -0
  25. package/.agents/assets/templates/company_entity.template.md +37 -0
  26. package/.agents/backend/backends/cmux.sh +649 -0
  27. package/.agents/backend/backends/herdr-eventwait.py +157 -0
  28. package/.agents/backend/backends/herdr-workspace-move.py +114 -0
  29. package/.agents/backend/backends/herdr.sh +3386 -0
  30. package/.agents/backend/backends/orca.sh +290 -0
  31. package/.agents/backend/backends/tmux.sh +357 -0
  32. package/.agents/backend/backends/zellij.sh +700 -0
  33. package/.agents/backend/fm-afk-launch.sh +654 -0
  34. package/.agents/backend/fm-afk-return.sh +241 -0
  35. package/.agents/backend/fm-afk-start.sh +172 -0
  36. package/.agents/backend/fm-arm-command-policy.mjs +960 -0
  37. package/.agents/backend/fm-arm-pretool-check.sh +200 -0
  38. package/.agents/backend/fm-backend-hometag-lib.sh +52 -0
  39. package/.agents/backend/fm-backend.sh +981 -0
  40. package/.agents/backend/fm-backlog-handoff.sh +909 -0
  41. package/.agents/backend/fm-backlog-receive.sh +187 -0
  42. package/.agents/backend/fm-backlog-transition-lib.sh +779 -0
  43. package/.agents/backend/fm-bearings-board.sh +204 -0
  44. package/.agents/backend/fm-bearings-snapshot.sh +546 -0
  45. package/.agents/backend/fm-bootstrap.sh +1568 -0
  46. package/.agents/backend/fm-branch-outcome.sh +203 -0
  47. package/.agents/backend/fm-branch-prompt.sh +101 -0
  48. package/.agents/backend/fm-brief.sh +452 -0
  49. package/.agents/backend/fm-busy-event.sh +234 -0
  50. package/.agents/backend/fm-busy-lib.sh +986 -0
  51. package/.agents/backend/fm-captain-hold.sh +1001 -0
  52. package/.agents/backend/fm-cd-command-policy.mjs +153 -0
  53. package/.agents/backend/fm-cd-pretool-check.sh +190 -0
  54. package/.agents/backend/fm-check-lib.sh +74 -0
  55. package/.agents/backend/fm-check-register.sh +42 -0
  56. package/.agents/backend/fm-check-unregister.sh +52 -0
  57. package/.agents/backend/fm-classify-lib.sh +1770 -0
  58. package/.agents/backend/fm-claude-stop-autoarm.sh +334 -0
  59. package/.agents/backend/fm-composer-lib.sh +1416 -0
  60. package/.agents/backend/fm-config-inherit-lib.sh +1202 -0
  61. package/.agents/backend/fm-config-push.sh +254 -0
  62. package/.agents/backend/fm-control-lib.sh +251 -0
  63. package/.agents/backend/fm-control.sh +878 -0
  64. package/.agents/backend/fm-crew-state.sh +625 -0
  65. package/.agents/backend/fm-cursor-lib.sh +243 -0
  66. package/.agents/backend/fm-decision-hold.sh +232 -0
  67. package/.agents/backend/fm-doc-audience-check.sh +269 -0
  68. package/.agents/backend/fm-dod-lib.sh +67 -0
  69. package/.agents/backend/fm-ensure-agents-md.sh +253 -0
  70. package/.agents/backend/fm-extension-launch-barrier.mjs +129 -0
  71. package/.agents/backend/fm-extension.mjs +2577 -0
  72. package/.agents/backend/fm-extension.sh +16 -0
  73. package/.agents/backend/fm-ff-lib.sh +420 -0
  74. package/.agents/backend/fm-fleet-snapshot.sh +1463 -0
  75. package/.agents/backend/fm-fleet-sync.sh +458 -0
  76. package/.agents/backend/fm-fleet-view.sh +96 -0
  77. package/.agents/backend/fm-gate-refuse-lib.sh +102 -0
  78. package/.agents/backend/fm-guard.sh +242 -0
  79. package/.agents/backend/fm-harness.sh +195 -0
  80. package/.agents/backend/fm-herdr-ci-cleanup.sh +115 -0
  81. package/.agents/backend/fm-herdr-lab.sh +346 -0
  82. package/.agents/backend/fm-herdr-session-cleanup.sh +337 -0
  83. package/.agents/backend/fm-home-seed.sh +984 -0
  84. package/.agents/backend/fm-home-summary-refresh.sh +255 -0
  85. package/.agents/backend/fm-hook-host-lib.sh +36 -0
  86. package/.agents/backend/fm-inactive-reconcile.sh +531 -0
  87. package/.agents/backend/fm-inbox.sh +399 -0
  88. package/.agents/backend/fm-install-actionlint.sh +84 -0
  89. package/.agents/backend/fm-install-herdr.sh +96 -0
  90. package/.agents/backend/fm-install-shellcheck.sh +84 -0
  91. package/.agents/backend/fm-install-treehouse.sh +95 -0
  92. package/.agents/backend/fm-kimi-turnend-hook.sh +276 -0
  93. package/.agents/backend/fm-lease-lib.sh +218 -0
  94. package/.agents/backend/fm-lease.sh +189 -0
  95. package/.agents/backend/fm-line-cap-lib.sh +51 -0
  96. package/.agents/backend/fm-lint-workflows.sh +137 -0
  97. package/.agents/backend/fm-lint.sh +591 -0
  98. package/.agents/backend/fm-lock-lib.sh +104 -0
  99. package/.agents/backend/fm-lock.sh +107 -0
  100. package/.agents/backend/fm-marker-lib.sh +12 -0
  101. package/.agents/backend/fm-merge-local.sh +74 -0
  102. package/.agents/backend/fm-merge-outcome-lib.sh +138 -0
  103. package/.agents/backend/fm-nm-run-lib.sh +124 -0
  104. package/.agents/backend/fm-on.sh +125 -0
  105. package/.agents/backend/fm-operational-input.sh +253 -0
  106. package/.agents/backend/fm-peek.sh +46 -0
  107. package/.agents/backend/fm-pending-reply-lib.sh +1409 -0
  108. package/.agents/backend/fm-pr-check.sh +135 -0
  109. package/.agents/backend/fm-pr-lib.sh +1016 -0
  110. package/.agents/backend/fm-pr-merge.sh +707 -0
  111. package/.agents/backend/fm-pr-poll.sh +110 -0
  112. package/.agents/backend/fm-primary-scope-lib.sh +33 -0
  113. package/.agents/backend/fm-procevent-extension-capture.pl +259 -0
  114. package/.agents/backend/fm-procevent-lavish.sh +628 -0
  115. package/.agents/backend/fm-procevent-lib.sh +892 -0
  116. package/.agents/backend/fm-procevent-quota.sh +290 -0
  117. package/.agents/backend/fm-procevent-remote-reply.sh +577 -0
  118. package/.agents/backend/fm-procevent-when.sh +504 -0
  119. package/.agents/backend/fm-procevent.sh +1466 -0
  120. package/.agents/backend/fm-project-mode.sh +92 -0
  121. package/.agents/backend/fm-project-origin-lib.sh +180 -0
  122. package/.agents/backend/fm-promote.sh +282 -0
  123. package/.agents/backend/fm-public-followup-emit.sh +260 -0
  124. package/.agents/backend/fm-public-followup-lib.sh +398 -0
  125. package/.agents/backend/fm-public-followup.sh +1330 -0
  126. package/.agents/backend/fm-push-transition-lib.sh +168 -0
  127. package/.agents/backend/fm-quota-axi-lib.sh +93 -0
  128. package/.agents/backend/fm-quota-choose.sh +384 -0
  129. package/.agents/backend/fm-remote-delta-read.sh +189 -0
  130. package/.agents/backend/fm-remote-doctor.sh +800 -0
  131. package/.agents/backend/fm-remote-entrypoint.sh +193 -0
  132. package/.agents/backend/fm-remote-file.sh +234 -0
  133. package/.agents/backend/fm-remote-home-provision.sh +259 -0
  134. package/.agents/backend/fm-remote-home-seed.sh +259 -0
  135. package/.agents/backend/fm-remote-inherit-push.sh +90 -0
  136. package/.agents/backend/fm-remote-inherit.sh +179 -0
  137. package/.agents/backend/fm-remote-job-lib.sh +1241 -0
  138. package/.agents/backend/fm-remote-job-reap-orphans.sh +137 -0
  139. package/.agents/backend/fm-remote-job-worker.sh +1124 -0
  140. package/.agents/backend/fm-remote-readiness-lib.sh +44 -0
  141. package/.agents/backend/fm-remote-secondmate-control.sh +340 -0
  142. package/.agents/backend/fm-review-diff.sh +158 -0
  143. package/.agents/backend/fm-secondmate-charter-lib.sh +44 -0
  144. package/.agents/backend/fm-secondmate-nudge-lib.sh +68 -0
  145. package/.agents/backend/fm-secondmate-parent-lib.sh +70 -0
  146. package/.agents/backend/fm-secondmate-reconcile.sh +371 -0
  147. package/.agents/backend/fm-secondmate-registry-lib.sh +311 -0
  148. package/.agents/backend/fm-secondmate-report.sh +88 -0
  149. package/.agents/backend/fm-send.sh +1059 -0
  150. package/.agents/backend/fm-session-lock-lib.sh +176 -0
  151. package/.agents/backend/fm-session-start.sh +970 -0
  152. package/.agents/backend/fm-sessionstart-cursor.sh +40 -0
  153. package/.agents/backend/fm-sessionstart-nudge.sh +45 -0
  154. package/.agents/backend/fm-sessionstart-run.sh +145 -0
  155. package/.agents/backend/fm-spawn.sh +3153 -0
  156. package/.agents/backend/fm-startup-memory-budget-lib.sh +224 -0
  157. package/.agents/backend/fm-startup-memory-budget.sh +94 -0
  158. package/.agents/backend/fm-startup-network.sh +642 -0
  159. package/.agents/backend/fm-stow-cascade.sh +251 -0
  160. package/.agents/backend/fm-subagent-pretool-check.sh +207 -0
  161. package/.agents/backend/fm-supervise-daemon.sh +1732 -0
  162. package/.agents/backend/fm-supervision-instructions.sh +215 -0
  163. package/.agents/backend/fm-supervision-lib.sh +90 -0
  164. package/.agents/backend/fm-supervisor-target-lib.sh +78 -0
  165. package/.agents/backend/fm-tangle-lib.sh +53 -0
  166. package/.agents/backend/fm-task-inbox-lib.sh +402 -0
  167. package/.agents/backend/fm-tasks-axi-lib.sh +123 -0
  168. package/.agents/backend/fm-teardown.sh +2922 -0
  169. package/.agents/backend/fm-test-isolation-proof.sh +588 -0
  170. package/.agents/backend/fm-test-run.sh +2191 -0
  171. package/.agents/backend/fm-timeout-lib.sh +141 -0
  172. package/.agents/backend/fm-timing-lib.sh +179 -0
  173. package/.agents/backend/fm-tmux-lib.sh +291 -0
  174. package/.agents/backend/fm-tool-update-check.sh +898 -0
  175. package/.agents/backend/fm-trace-context-lib.sh +227 -0
  176. package/.agents/backend/fm-transition-lib.sh +103 -0
  177. package/.agents/backend/fm-turnend-guard-cursor.sh +391 -0
  178. package/.agents/backend/fm-turnend-guard-grok.sh +90 -0
  179. package/.agents/backend/fm-turnend-guard.sh +432 -0
  180. package/.agents/backend/fm-update.sh +110 -0
  181. package/.agents/backend/fm-vendor-auth-probe.sh +190 -0
  182. package/.agents/backend/fm-voice-client.py +1373 -0
  183. package/.agents/backend/fm-voice-relay.py +1256 -0
  184. package/.agents/backend/fm-wake-drain.sh +592 -0
  185. package/.agents/backend/fm-wake-grant.sh +127 -0
  186. package/.agents/backend/fm-wake-lib.sh +1849 -0
  187. package/.agents/backend/fm-watch-arm.sh +602 -0
  188. package/.agents/backend/fm-watch-checkpoint.sh +109 -0
  189. package/.agents/backend/fm-watch.sh +1962 -0
  190. package/.agents/backend/fm-x-dismiss.sh +116 -0
  191. package/.agents/backend/fm-x-followup.sh +286 -0
  192. package/.agents/backend/fm-x-lib.sh +1000 -0
  193. package/.agents/backend/fm-x-link.sh +234 -0
  194. package/.agents/backend/fm-x-poll.sh +185 -0
  195. package/.agents/backend/fm-x-reply.sh +385 -0
  196. package/.agents/backend/fm_voice_frame.py +166 -0
  197. package/.agents/backend/fm_voice_records.py +574 -0
  198. package/.agents/backend/model-bridge.py +290 -0
  199. package/.agents/backend/opencode-go-bridge.py +30 -0
  200. package/.agents/bus/README.md +12 -0
  201. package/.agents/bus/messages.json +1 -0
  202. package/.agents/config/agents.machine.example.yaml +16 -0
  203. package/.agents/config/agents.yaml +50 -0
  204. package/.agents/config/agents.yaml.example +72 -0
  205. package/.agents/config/crew-dispatch.json +22 -0
  206. package/.agents/config/cron.yaml +9 -0
  207. package/.agents/config/eindri-dispatch.json +67 -0
  208. package/.agents/config/eindri-harness +1 -0
  209. package/.agents/config/startup-memory-budget +1 -0
  210. package/.agents/config/tailscale-sync.yaml +27 -0
  211. package/.agents/filebrowser/.filebrowser.json +6 -0
  212. package/.agents/filebrowser/README.md +11 -0
  213. package/.agents/gateway/README.md +12 -0
  214. package/.agents/github/README.md +15 -0
  215. package/.agents/github/webhooks/.gitkeep +0 -0
  216. package/.agents/github/workflows/.gitkeep +0 -0
  217. package/.agents/harness/opencode/plugins/lib/rodd-operational-input.js +37 -0
  218. package/.agents/harness/opencode/plugins/package.json +4 -0
  219. package/.agents/harness/opencode/plugins/saga-sessionstart.js +77 -0
  220. package/.agents/harness/opencode/plugins/syn-cd-check.js +61 -0
  221. package/.agents/harness/opencode/plugins/syn-pretool-check.js +68 -0
  222. package/.agents/harness/opencode/plugins/syn-turnend-guard.js +105 -0
  223. package/.agents/harness/opencode/plugins/syn-watch-arm.js +521 -0
  224. package/.agents/memory/README.md +46 -0
  225. package/.agents/memory/kaia.engram +0 -0
  226. package/.agents/memory/kaia.engram-shm +0 -0
  227. package/.agents/memory/kaia.engram-wal +0 -0
  228. package/.agents/memory/runes_audit.md +5 -0
  229. package/.agents/memory/well/episodes.jsonl +367 -0
  230. package/.agents/memory/well/workspace.jsonl +27 -0
  231. package/.agents/migrations/0001-hodd-layout.sh +38 -0
  232. package/.agents/migrations/0002-a2a-mcp.sh +27 -0
  233. package/.agents/migrations/0003-private-data-separation.sh +111 -0
  234. package/.agents/migrations/0004-hoard-and-realms.sh +104 -0
  235. package/.agents/sandbox/Dockerfile.utgard +19 -0
  236. package/.agents/sandbox/README.md +14 -0
  237. package/.agents/sandbox/utgard.config.json +14 -0
  238. package/.agents/skills/README.md +59 -0
  239. package/.agents/skills/bragi-marketing/README.md +3 -0
  240. package/.agents/skills/bragi-marketing/SKILL.md +82 -0
  241. package/.agents/skills/eindri-homes/README.md +3 -0
  242. package/.agents/skills/eindri-homes/SKILL.md +291 -0
  243. package/.agents/skills/eindri-homes/assets/control-plane.md +122 -0
  244. package/.agents/skills/eindri-homes/assets/dispatch.md +80 -0
  245. package/.agents/skills/frigg-consent/README.md +3 -0
  246. package/.agents/skills/frigg-consent/SKILL.md +56 -0
  247. package/.agents/skills/galdr-ymirsystem/README.md +3 -0
  248. package/.agents/skills/galdr-ymirsystem/SKILL.md +158 -0
  249. package/.agents/skills/galdr-ymirsystem/assets/README.md +74 -0
  250. package/.agents/skills/galdr-ymirsystem/assets/brokk-distro-runtime.md +390 -0
  251. package/.agents/skills/galdr-ymirsystem/assets/build-method.md +113 -0
  252. package/.agents/skills/galdr-ymirsystem/assets/build-tool-categories.md +178 -0
  253. package/.agents/skills/galdr-ymirsystem/assets/eindri-orchestration.md +518 -0
  254. package/.agents/skills/galdr-ymirsystem/assets/eindri-profiles.md +136 -0
  255. package/.agents/skills/galdr-ymirsystem/assets/harness-integration/README.md +673 -0
  256. package/.agents/skills/galdr-ymirsystem/assets/harness-integration/claude-code.md +197 -0
  257. package/.agents/skills/galdr-ymirsystem/assets/harness-integration/codex.md +191 -0
  258. package/.agents/skills/galdr-ymirsystem/assets/harness-integration/cursor.md +193 -0
  259. package/.agents/skills/galdr-ymirsystem/assets/harness-integration/opencode.md +262 -0
  260. package/.agents/skills/galdr-ymirsystem/assets/harness-integration/pi.md +295 -0
  261. package/.agents/skills/galdr-ymirsystem/assets/hlidskjalf-ui.md +496 -0
  262. package/.agents/skills/galdr-ymirsystem/assets/installation.md +571 -0
  263. package/.agents/skills/galdr-ymirsystem/assets/local-models/HOST-DATA.example.md +72 -0
  264. package/.agents/skills/galdr-ymirsystem/assets/local-models.md +274 -0
  265. package/.agents/skills/galdr-ymirsystem/assets/memory-well.md +161 -0
  266. package/.agents/skills/galdr-ymirsystem/assets/nornir-jobs.md +377 -0
  267. package/.agents/skills/galdr-ymirsystem/assets/norse-naming.md +325 -0
  268. package/.agents/skills/galdr-ymirsystem/assets/odrerir-hall.md +129 -0
  269. package/.agents/skills/galdr-ymirsystem/assets/pi-boot/einherjar-spawn.schema.json +81 -0
  270. package/.agents/skills/galdr-ymirsystem/assets/pi-boot/herdr-profile.toml +75 -0
  271. package/.agents/skills/galdr-ymirsystem/assets/pi-boot/pi-profile.yml +96 -0
  272. package/.agents/skills/galdr-ymirsystem/assets/pi-boot/supervision-tree.yml +163 -0
  273. package/.agents/skills/galdr-ymirsystem/assets/pi-boot-guide.md +58 -0
  274. package/.agents/skills/galdr-ymirsystem/assets/porting-upstream-to-norse.md +301 -0
  275. package/.agents/skills/galdr-ymirsystem/assets/principles.md +385 -0
  276. package/.agents/skills/galdr-ymirsystem/assets/reference-adoption.md +99 -0
  277. package/.agents/skills/galdr-ymirsystem/assets/registry.md +379 -0
  278. package/.agents/skills/galdr-ymirsystem/assets/runtime-compliance.md +463 -0
  279. package/.agents/skills/galdr-ymirsystem/assets/runtime-components.md +553 -0
  280. package/.agents/skills/galdr-ymirsystem/assets/smidja.md +175 -0
  281. package/.agents/skills/galdr-ymirsystem/schemas/toon-schemas.md +190 -0
  282. package/.agents/skills/galdr-ymirsystem/scripts/README.md +45 -0
  283. package/.agents/skills/galdr-ymirsystem/scripts/bench-one.sh +248 -0
  284. package/.agents/skills/galdr-ymirsystem/scripts/compliance-check.sh +360 -0
  285. package/.agents/skills/galdr-ymirsystem/scripts/gpu-sample.sh +43 -0
  286. package/.agents/skills/galdr-ymirsystem/scripts/stop-all.sh +34 -0
  287. package/.agents/skills/galdr-ymirsystem/scripts/toon-check.py +188 -0
  288. package/.agents/skills/gjallarhorn-relay/README.md +3 -0
  289. package/.agents/skills/gjallarhorn-relay/SKILL.md +283 -0
  290. package/.agents/skills/groa-update/SKILL.md +70 -0
  291. package/.agents/skills/hamr-adapters/README.md +3 -0
  292. package/.agents/skills/hamr-adapters/SKILL.md +123 -0
  293. package/.agents/skills/hamr-adapters/references/common/control-and-recovery.md +37 -0
  294. package/.agents/skills/hamr-adapters/references/common/dispatch.md +32 -0
  295. package/.agents/skills/hamr-adapters/references/common/model-and-effort.md +42 -0
  296. package/.agents/skills/hamr-adapters/references/common/primary-hooks.md +40 -0
  297. package/.agents/skills/hamr-adapters/references/harness/claude.md +55 -0
  298. package/.agents/skills/hamr-adapters/references/harness/codex.md +43 -0
  299. package/.agents/skills/hamr-adapters/references/harness/cursor.md +75 -0
  300. package/.agents/skills/hamr-adapters/references/harness/grok.md +69 -0
  301. package/.agents/skills/hamr-adapters/references/harness/kimi.md +51 -0
  302. package/.agents/skills/hamr-adapters/references/harness/muse.md +70 -0
  303. package/.agents/skills/hamr-adapters/references/harness/opencode.md +42 -0
  304. package/.agents/skills/hamr-adapters/references/harness/pi.md +56 -0
  305. package/.agents/skills/herdr-panes/README.md +3 -0
  306. package/.agents/skills/herdr-panes/SKILL.md +92 -0
  307. package/.agents/skills/herdr-panes/assets/herdr-backend.md +346 -0
  308. package/.agents/skills/herdr-panes/assets/tmux-backend.md +115 -0
  309. package/.agents/skills/hnoss-design/DESIGN.md +19 -0
  310. package/.agents/skills/hnoss-design/README.md +3 -0
  311. package/.agents/skills/hnoss-design/SKILL.md +83 -0
  312. package/.agents/skills/hvild-afk/README.md +3 -0
  313. package/.agents/skills/hvild-afk/SKILL.md +241 -0
  314. package/.agents/skills/jord-projects/README.md +3 -0
  315. package/.agents/skills/jord-projects/SKILL.md +92 -0
  316. package/.agents/skills/lifecycle/SKILL.md +56 -0
  317. package/.agents/skills/lifecycle/smoke_test.sh +52 -0
  318. package/.agents/skills/lifecycle/start.sh +21 -0
  319. package/.agents/skills/lifecycle/status.sh +35 -0
  320. package/.agents/skills/lifecycle/stop.sh +16 -0
  321. package/.agents/skills/muninn-stow/README.md +3 -0
  322. package/.agents/skills/muninn-stow/SKILL.md +309 -0
  323. package/.agents/skills/no-mistakes/SKILL.md +379 -0
  324. package/.agents/skills/nornir-schedule/README.md +3 -0
  325. package/.agents/skills/nornir-schedule/SKILL.md +25 -0
  326. package/.agents/skills/nornir-schedule/assets/events.md +137 -0
  327. package/.agents/skills/nornir-schedule/assets/quota.md +131 -0
  328. package/.agents/skills/nsr-compliance/README.md +3 -0
  329. package/.agents/skills/nsr-compliance/SKILL.md +29 -0
  330. package/.agents/skills/nsr-compliance/assets/nsr/README.md +40 -0
  331. package/.agents/skills/nsr-compliance/assets/nsr/assets/RULES/README.md +15 -0
  332. package/.agents/skills/nsr-compliance/assets/nsr/assets/RULES/git-workflow.md +18 -0
  333. package/.agents/skills/nsr-compliance/assets/nsr/assets/RULES/security.md +22 -0
  334. package/.agents/skills/nsr-compliance/assets/nsr/assets/RULES/tech-stack.md +8 -0
  335. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/SKILL.md +71 -0
  336. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/config/core_four.yaml +16 -0
  337. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/gates/check_danger.sh +102 -0
  338. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/gates/check_wiring.sh +69 -0
  339. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/gates/validate_code.sh +29 -0
  340. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/gates/verify_docs.py +66 -0
  341. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/generate.sh +109 -0
  342. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/harness/envelopes/result_envelope.json +14 -0
  343. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/harness/envelopes/task_envelope.json +20 -0
  344. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/harness/runner.py +303 -0
  345. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/telemetry/logger.py +112 -0
  346. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/templates/compliance/README.md +18 -0
  347. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/templates/compliance/config/README.md +10 -0
  348. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/templates/compliance/gates/README.md +12 -0
  349. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/templates/compliance/gates/check_env.sh +23 -0
  350. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/templates/compliance/gates/check_paths.sh +20 -0
  351. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/templates/compliance/gates/check_platform.sh +23 -0
  352. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/templates/compliance/harness/README.md +11 -0
  353. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/templates/compliance/harness/envelopes/README.md +10 -0
  354. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/templates/compliance/installer/README.md +10 -0
  355. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/templates/compliance/installer/install.sh +125 -0
  356. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/templates/compliance/telemetry/README.md +11 -0
  357. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/README.md +20 -0
  358. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/features/README.md +36 -0
  359. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/features/SKILL-template.md +14 -0
  360. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/git_ops/README.md +25 -0
  361. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/git_ops/SKILL.md +13 -0
  362. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/git_ops/create_branch.sh +6 -0
  363. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/git_ops/safe_commit.sh +9 -0
  364. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/git_ops/sync_upstream.sh +6 -0
  365. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/lifecycle/README.md +27 -0
  366. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/lifecycle/SKILL.md +15 -0
  367. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/lifecycle/smoke_test.sh +12 -0
  368. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/lifecycle/start.sh +19 -0
  369. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/lifecycle/status.sh +12 -0
  370. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/lifecycle/stop.sh +15 -0
  371. package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/README.md +18 -0
  372. package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/config/README.md +10 -0
  373. package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/config/core_four.yaml +13 -0
  374. package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/gates/README.md +12 -0
  375. package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/gates/check_danger.sh +102 -0
  376. package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/gates/check_env.sh +23 -0
  377. package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/gates/check_paths.sh +20 -0
  378. package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/gates/check_platform.sh +23 -0
  379. package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/gates/check_wiring.sh +69 -0
  380. package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/harness/README.md +11 -0
  381. package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/harness/envelopes/README.md +10 -0
  382. package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/harness/envelopes/result_envelope.json +14 -0
  383. package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/harness/envelopes/task_envelope.json +20 -0
  384. package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/harness/runner.py +303 -0
  385. package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/installer/README.md +10 -0
  386. package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/installer/install.sh +125 -0
  387. package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/telemetry/README.md +11 -0
  388. package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/telemetry/logger.py +112 -0
  389. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/BEST_PRACTICES/README.md +14 -0
  390. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/BEST_PRACTICES/api.md +17 -0
  391. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/BEST_PRACTICES/compliance-wiring.md +155 -0
  392. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/BEST_PRACTICES/frontend.md +15 -0
  393. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/CI_CD/README.md +11 -0
  394. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/CI_CD/deployment/README.md +23 -0
  395. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/CI_CD/deployment/clients/README.md +16 -0
  396. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/CI_CD/deployment/clients/template-README.md +11 -0
  397. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/CI_CD/deployment/deploy.sh +49 -0
  398. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/CI_CD/deployment/envs/README.md +12 -0
  399. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/CI_CD/deployment/envs/development.env.example +17 -0
  400. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/CI_CD/deployment/envs/production.env.example +17 -0
  401. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/CI_CD/deployment/envs/staging.env.example +17 -0
  402. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/CI_CD/deployment/tenants/README.md +11 -0
  403. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/CI_CD/deployment/tenants/template.env.example +8 -0
  404. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/DEVELOPER_SETUP/README.md +26 -0
  405. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/DEVELOPER_SETUP/agent.md +7 -0
  406. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/DEVELOPER_SETUP/developers/dev-1.md +20 -0
  407. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/DEVELOPER_SETUP/linux.md +9 -0
  408. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/DEVELOPER_SETUP/macos.md +10 -0
  409. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/DEVELOPER_SETUP/shared.md +19 -0
  410. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/DEVELOPER_SETUP/windows.md +14 -0
  411. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/HOSTING/README.md +25 -0
  412. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/HOSTING/_template.md +41 -0
  413. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/HOSTING/hosting-1.md +41 -0
  414. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/README.md +21 -0
  415. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/RUNBOOK/README.md +15 -0
  416. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/RUNBOOK/deployment.md +19 -0
  417. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/RUNBOOK/disaster-recovery.md +15 -0
  418. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/RUNBOOK/ops-tasks.md +14 -0
  419. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/features/README.md +16 -0
  420. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/project-skills.md +146 -0
  421. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/research/README.md +11 -0
  422. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/research/software-compliance-research.md +269 -0
  423. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/working-with-agents.md +260 -0
  424. package/.agents/skills/nsr-compliance/assets/nsr/assets/root/AGENTS.md +38 -0
  425. package/.agents/skills/nsr-compliance/assets/nsr/assets/root/ARCHITECTURE.md +36 -0
  426. package/.agents/skills/nsr-compliance/assets/nsr/assets/root/BEST_PRACTICES.md +27 -0
  427. package/.agents/skills/nsr-compliance/assets/nsr/assets/root/CI_CD.md +23 -0
  428. package/.agents/skills/nsr-compliance/assets/nsr/assets/root/FEATURES.md +15 -0
  429. package/.agents/skills/nsr-compliance/assets/nsr/assets/root/README.md +22 -0
  430. package/.agents/skills/nsr-compliance/assets/nsr/assets/root/RULES.md +37 -0
  431. package/.agents/skills/nsr-compliance/assets/nsr/assets/root/STRUCTURE.md +45 -0
  432. package/.agents/skills/nsr-compliance/assets/nsr/assets/root/TECH_STACK.md +28 -0
  433. package/.agents/skills/nsr-compliance/assets/nsr/scaffold-spec.md +63 -0
  434. package/.agents/skills/nsr-compliance/assets/nsr/scripts/scaffold.sh +103 -0
  435. package/.agents/skills/nsr-compliance/assets/nsr/scripts/validate.sh +52 -0
  436. package/.agents/skills/nsr-compliance/assets/nsrcompliance/SKILL.md +71 -0
  437. package/.agents/skills/nsr-compliance/assets/nsrcompliance/config/core_four.yaml +16 -0
  438. package/.agents/skills/nsr-compliance/assets/nsrcompliance/gates/check_danger.sh +102 -0
  439. package/.agents/skills/nsr-compliance/assets/nsrcompliance/gates/check_wiring.sh +69 -0
  440. package/.agents/skills/nsr-compliance/assets/nsrcompliance/gates/validate_code.sh +29 -0
  441. package/.agents/skills/nsr-compliance/assets/nsrcompliance/gates/verify_docs.py +66 -0
  442. package/.agents/skills/nsr-compliance/assets/nsrcompliance/generate.sh +109 -0
  443. package/.agents/skills/nsr-compliance/assets/nsrcompliance/harness/envelopes/result_envelope.json +14 -0
  444. package/.agents/skills/nsr-compliance/assets/nsrcompliance/harness/envelopes/task_envelope.json +20 -0
  445. package/.agents/skills/nsr-compliance/assets/nsrcompliance/harness/runner.py +303 -0
  446. package/.agents/skills/nsr-compliance/assets/nsrcompliance/telemetry/logger.py +112 -0
  447. package/.agents/skills/nsr-compliance/assets/nsrcompliance/templates/compliance/README.md +18 -0
  448. package/.agents/skills/nsr-compliance/assets/nsrcompliance/templates/compliance/config/README.md +10 -0
  449. package/.agents/skills/nsr-compliance/assets/nsrcompliance/templates/compliance/gates/README.md +12 -0
  450. package/.agents/skills/nsr-compliance/assets/nsrcompliance/templates/compliance/gates/check_env.sh +23 -0
  451. package/.agents/skills/nsr-compliance/assets/nsrcompliance/templates/compliance/gates/check_paths.sh +20 -0
  452. package/.agents/skills/nsr-compliance/assets/nsrcompliance/templates/compliance/gates/check_platform.sh +23 -0
  453. package/.agents/skills/nsr-compliance/assets/nsrcompliance/templates/compliance/harness/README.md +11 -0
  454. package/.agents/skills/nsr-compliance/assets/nsrcompliance/templates/compliance/harness/envelopes/README.md +10 -0
  455. package/.agents/skills/nsr-compliance/assets/nsrcompliance/templates/compliance/installer/README.md +10 -0
  456. package/.agents/skills/nsr-compliance/assets/nsrcompliance/templates/compliance/installer/install.sh +125 -0
  457. package/.agents/skills/nsr-compliance/assets/nsrcompliance/templates/compliance/telemetry/README.md +11 -0
  458. package/.agents/skills/pr-ops/README.md +3 -0
  459. package/.agents/skills/pr-ops/SKILL.md +109 -0
  460. package/.agents/skills/ratatoskr-a2a/README.md +3 -0
  461. package/.agents/skills/ratatoskr-a2a/SKILL.md +148 -0
  462. package/.agents/skills/rules-check-drift/SKILL.md +81 -0
  463. package/.agents/skills/saga-bearings/README.md +3 -0
  464. package/.agents/skills/saga-bearings/SKILL.md +26 -0
  465. package/.agents/skills/saga-bearings/assets/bearings.md +161 -0
  466. package/.agents/skills/saga-bearings/assets/board-template.html +731 -0
  467. package/.agents/skills/saga-bearings/assets/recap.md +56 -0
  468. package/.agents/skills/syn-recovery/README.md +3 -0
  469. package/.agents/skills/syn-recovery/SKILL.md +54 -0
  470. package/.agents/skills/tyr-check/README.md +3 -0
  471. package/.agents/skills/tyr-check/SKILL.md +218 -0
  472. package/.agents/skills/urdh-hold/README.md +3 -0
  473. package/.agents/skills/urdh-hold/SKILL.md +25 -0
  474. package/.agents/skills/urdh-hold/assets/decisions.md +15 -0
  475. package/.agents/skills/urdh-hold/assets/hold.md +54 -0
  476. package/.agents/skills/vor-diagnostics/README.md +3 -0
  477. package/.agents/skills/vor-diagnostics/SKILL.md +68 -0
  478. package/.agents/skills/ymir-host/README.md +3 -0
  479. package/.agents/skills/ymir-host/SKILL.md +34 -0
  480. package/.agents/skills/ymir-host/assets/desktop.md +36 -0
  481. package/.agents/skills/ymir-host/assets/omarchy.md +297 -0
  482. package/.agents/skills/ymir-host/assets/runbooks.md +15 -0
  483. package/.agents/skills/ymir-host/assets/thjazi.md +220 -0
  484. package/.agents/skills/ymir-host/assets/update.md +64 -0
  485. package/.agents/tests/assets/board-render-harness.mjs +117 -0
  486. package/.agents/tests/cmux-test-safety.sh +51 -0
  487. package/.agents/tests/fixtures.sh +291 -0
  488. package/.agents/tests/fm-afk-inject-e2e.test.sh +428 -0
  489. package/.agents/tests/fm-afk-inject-herdr-e2e.test.sh +536 -0
  490. package/.agents/tests/fm-afk-launch.test.sh +961 -0
  491. package/.agents/tests/fm-afk-pi-herdr-return-e2e.test.sh +297 -0
  492. package/.agents/tests/fm-afk-return.test.sh +283 -0
  493. package/.agents/tests/fm-arm-pretool-check.test.sh +482 -0
  494. package/.agents/tests/fm-ask-user-authority.test.sh +42 -0
  495. package/.agents/tests/fm-backend-autodetect-smoke.test.sh +176 -0
  496. package/.agents/tests/fm-backend-cmux-smoke.test.sh +188 -0
  497. package/.agents/tests/fm-backend-cmux.test.sh +1166 -0
  498. package/.agents/tests/fm-backend-herdr-eventwait-smoke.test.sh +136 -0
  499. package/.agents/tests/fm-backend-herdr-eventwait.test.py +106 -0
  500. package/.agents/tests/fm-backend-herdr-focus-flash-e2e.test.sh +409 -0
  501. package/.agents/tests/fm-backend-herdr-launcher-workspace-e2e.test.sh +437 -0
  502. package/.agents/tests/fm-backend-herdr-presentation-e2e.test.sh +1424 -0
  503. package/.agents/tests/fm-backend-herdr-prune-safety-e2e.test.sh +180 -0
  504. package/.agents/tests/fm-backend-herdr-respawn-idem-e2e.test.sh +182 -0
  505. package/.agents/tests/fm-backend-herdr-smoke.test.sh +355 -0
  506. package/.agents/tests/fm-backend-herdr-workspace-per-home-e2e.test.sh +251 -0
  507. package/.agents/tests/fm-backend-herdr.test.sh +4606 -0
  508. package/.agents/tests/fm-backend-orca.test.sh +1357 -0
  509. package/.agents/tests/fm-backend-tmux-smoke.test.sh +173 -0
  510. package/.agents/tests/fm-backend-zellij-smoke.test.sh +205 -0
  511. package/.agents/tests/fm-backend-zellij.test.sh +1360 -0
  512. package/.agents/tests/fm-backend.test.sh +1142 -0
  513. package/.agents/tests/fm-backlog-atomicity.test.sh +2302 -0
  514. package/.agents/tests/fm-backlog-handoff.test.sh +1360 -0
  515. package/.agents/tests/fm-bearings-board-render.test.sh +135 -0
  516. package/.agents/tests/fm-bearings-board.test.sh +414 -0
  517. package/.agents/tests/fm-bearings-snapshot.test.sh +1990 -0
  518. package/.agents/tests/fm-bootstrap-network-parallel.test.sh +316 -0
  519. package/.agents/tests/fm-bootstrap.test.sh +1178 -0
  520. package/.agents/tests/fm-branch-supervision.test.sh +552 -0
  521. package/.agents/tests/fm-brief.test.sh +785 -0
  522. package/.agents/tests/fm-busy-adapter-wiring.test.sh +324 -0
  523. package/.agents/tests/fm-busy-state.test.sh +465 -0
  524. package/.agents/tests/fm-calm-pi-extension.test.sh +3979 -0
  525. package/.agents/tests/fm-captain-hold-lifecycle.test.sh +1187 -0
  526. package/.agents/tests/fm-cd-pretool-check.test.sh +400 -0
  527. package/.agents/tests/fm-check-unregister.test.sh +198 -0
  528. package/.agents/tests/fm-classify-corr-token.test.sh +543 -0
  529. package/.agents/tests/fm-classify-decision-key.test.sh +340 -0
  530. package/.agents/tests/fm-claude-stop-autoarm-live-e2e.test.sh +164 -0
  531. package/.agents/tests/fm-claude-stop-autoarm.test.sh +1190 -0
  532. package/.agents/tests/fm-cmux-claude-composer-live-e2e.test.sh +110 -0
  533. package/.agents/tests/fm-codex-continuity-live-e2e.test.sh +55 -0
  534. package/.agents/tests/fm-composer-ghost.test.sh +716 -0
  535. package/.agents/tests/fm-composer-lib.test.sh +666 -0
  536. package/.agents/tests/fm-composer-matrix-live-e2e.test.sh +220 -0
  537. package/.agents/tests/fm-control-herdr-smoke.test.sh +149 -0
  538. package/.agents/tests/fm-control-relaunch.test.sh +1530 -0
  539. package/.agents/tests/fm-control.test.sh +910 -0
  540. package/.agents/tests/fm-crew-state.test.sh +1610 -0
  541. package/.agents/tests/fm-cursor-harness.test.sh +404 -0
  542. package/.agents/tests/fm-cursor-primary-live-e2e.test.sh +217 -0
  543. package/.agents/tests/fm-cursor-primary.test.sh +708 -0
  544. package/.agents/tests/fm-daemon.test.sh +2740 -0
  545. package/.agents/tests/fm-documentation-audiences.test.sh +141 -0
  546. package/.agents/tests/fm-ensure-agents-md.test.sh +371 -0
  547. package/.agents/tests/fm-extension-binding.test.sh +2187 -0
  548. package/.agents/tests/fm-fleet-snapshot-view.test.sh +816 -0
  549. package/.agents/tests/fm-fleet-sync.test.sh +721 -0
  550. package/.agents/tests/fm-gate-refuse.test.sh +356 -0
  551. package/.agents/tests/fm-gitignore-config.test.sh +89 -0
  552. package/.agents/tests/fm-gotmp.test.sh +230 -0
  553. package/.agents/tests/fm-grok-continuity-live-e2e.test.sh +112 -0
  554. package/.agents/tests/fm-grok-harness.test.sh +117 -0
  555. package/.agents/tests/fm-grok-stop-live-e2e.test.sh +219 -0
  556. package/.agents/tests/fm-guard-stale-banner.test.sh +709 -0
  557. package/.agents/tests/fm-harness-adapter-instructions-live-e2e.test.sh +134 -0
  558. package/.agents/tests/fm-harness-adapter-references.test.sh +30 -0
  559. package/.agents/tests/fm-harness-liveness-drift-live-e2e.test.sh +151 -0
  560. package/.agents/tests/fm-herdr-lab.test.sh +243 -0
  561. package/.agents/tests/fm-herdr-session-cleanup-e2e.test.sh +146 -0
  562. package/.agents/tests/fm-herdr-session-cleanup.test.sh +329 -0
  563. package/.agents/tests/fm-herdr-submit-confirm-live-e2e.test.sh +125 -0
  564. package/.agents/tests/fm-herdr-version-floor-live-e2e.test.sh +144 -0
  565. package/.agents/tests/fm-home-summary-refresh.test.sh +971 -0
  566. package/.agents/tests/fm-inactive-reconcile.test.sh +485 -0
  567. package/.agents/tests/fm-kimi-harness.test.sh +687 -0
  568. package/.agents/tests/fm-lint-workflows.test.sh +532 -0
  569. package/.agents/tests/fm-lint.test.sh +1023 -0
  570. package/.agents/tests/fm-muse-harness.test.sh +932 -0
  571. package/.agents/tests/fm-muse-signals-live-e2e.test.sh +205 -0
  572. package/.agents/tests/fm-no-mistakes-required.test.sh +72 -0
  573. package/.agents/tests/fm-on.test.sh +507 -0
  574. package/.agents/tests/fm-opencode-primary-live-e2e.test.sh +357 -0
  575. package/.agents/tests/fm-operational-input.test.sh +160 -0
  576. package/.agents/tests/fm-peek-remote.test.sh +110 -0
  577. package/.agents/tests/fm-pending-reply.test.sh +1288 -0
  578. package/.agents/tests/fm-pi-branch-extension.test.sh +3182 -0
  579. package/.agents/tests/fm-pi-branch-live-e2e.test.sh +527 -0
  580. package/.agents/tests/fm-pi-primary-live-e2e.test.sh +345 -0
  581. package/.agents/tests/fm-pi-primary-types.test.sh +68 -0
  582. package/.agents/tests/fm-pi-watch-extension.test.sh +2843 -0
  583. package/.agents/tests/fm-pr-check-security.test.sh +2140 -0
  584. package/.agents/tests/fm-pr-merge.test.sh +2135 -0
  585. package/.agents/tests/fm-procevent-quota.test.sh +225 -0
  586. package/.agents/tests/fm-procevent-when.test.sh +406 -0
  587. package/.agents/tests/fm-procevent.test.sh +1745 -0
  588. package/.agents/tests/fm-project-origin.test.sh +110 -0
  589. package/.agents/tests/fm-public-followup.test.sh +2272 -0
  590. package/.agents/tests/fm-quota-array-dispatch-live-e2e.test.sh +526 -0
  591. package/.agents/tests/fm-quota-choose.test.sh +626 -0
  592. package/.agents/tests/fm-remote-backlog-handoff.test.sh +453 -0
  593. package/.agents/tests/fm-remote-doctor.test.sh +625 -0
  594. package/.agents/tests/fm-remote-entrypoint.test.sh +59 -0
  595. package/.agents/tests/fm-remote-job-orphan-reap.test.sh +205 -0
  596. package/.agents/tests/fm-remote-job.test.sh +768 -0
  597. package/.agents/tests/fm-remote-reply.test.sh +520 -0
  598. package/.agents/tests/fm-remote-secondmate-lifecycle-e2e.test.sh +1256 -0
  599. package/.agents/tests/fm-remote-secondmate-parent-binding.test.sh +326 -0
  600. package/.agents/tests/fm-remote-secondmate-trace-context.test.sh +310 -0
  601. package/.agents/tests/fm-remote-transport-lanes.test.sh +431 -0
  602. package/.agents/tests/fm-review-diff.test.sh +176 -0
  603. package/.agents/tests/fm-secondmate-harness.test.sh +2600 -0
  604. package/.agents/tests/fm-secondmate-lifecycle-e2e.test.sh +259 -0
  605. package/.agents/tests/fm-secondmate-liveness.test.sh +559 -0
  606. package/.agents/tests/fm-secondmate-reconcile.test.sh +732 -0
  607. package/.agents/tests/fm-secondmate-safety.test.sh +2972 -0
  608. package/.agents/tests/fm-secondmate-sync.test.sh +913 -0
  609. package/.agents/tests/fm-send-inbox-doorbell-live-e2e.test.sh +206 -0
  610. package/.agents/tests/fm-send-inbox.test.sh +352 -0
  611. package/.agents/tests/fm-send-popup-settle.test.sh +167 -0
  612. package/.agents/tests/fm-send-remote-delivery.test.sh +796 -0
  613. package/.agents/tests/fm-send-resolve-key.test.sh +551 -0
  614. package/.agents/tests/fm-send-secondmate-marker-herdr-e2e.test.sh +187 -0
  615. package/.agents/tests/fm-send-secondmate-marker.test.sh +278 -0
  616. package/.agents/tests/fm-send-settle.test.sh +147 -0
  617. package/.agents/tests/fm-send-strict.test.sh +241 -0
  618. package/.agents/tests/fm-session-lock-ancestry.test.sh +365 -0
  619. package/.agents/tests/fm-session-start.test.sh +2515 -0
  620. package/.agents/tests/fm-sessionstart-hook-live-e2e.test.sh +631 -0
  621. package/.agents/tests/fm-sessionstart-instruction-refresh-live-e2e.test.sh +230 -0
  622. package/.agents/tests/fm-sessionstart-nudge.test.sh +1039 -0
  623. package/.agents/tests/fm-shared-captain-inheritance.test.sh +404 -0
  624. package/.agents/tests/fm-spawn-batch.test.sh +148 -0
  625. package/.agents/tests/fm-spawn-dispatch-profile.test.sh +827 -0
  626. package/.agents/tests/fm-spawn-pool-base-freshen.test.sh +440 -0
  627. package/.agents/tests/fm-spawn-worktree-settle.test.sh +147 -0
  628. package/.agents/tests/fm-startup-memory-budget.test.sh +333 -0
  629. package/.agents/tests/fm-startup-network.test.sh +712 -0
  630. package/.agents/tests/fm-stow-cascade.test.sh +370 -0
  631. package/.agents/tests/fm-subagent-pretool-check.test.sh +291 -0
  632. package/.agents/tests/fm-supervision-events.test.sh +156 -0
  633. package/.agents/tests/fm-supervision-instructions.test.sh +189 -0
  634. package/.agents/tests/fm-tangle-guard.test.sh +277 -0
  635. package/.agents/tests/fm-task-delivery.test.sh +420 -0
  636. package/.agents/tests/fm-task-inbox.test.sh +513 -0
  637. package/.agents/tests/fm-teardown-endpoint-safety.test.sh +374 -0
  638. package/.agents/tests/fm-teardown.test.sh +2665 -0
  639. package/.agents/tests/fm-test-fixture-cleanup.test.sh +172 -0
  640. package/.agents/tests/fm-test-fixtures.test.sh +132 -0
  641. package/.agents/tests/fm-test-isolation-proof.test.sh +278 -0
  642. package/.agents/tests/fm-test-run.test.sh +1196 -0
  643. package/.agents/tests/fm-tmux-agent-liveness.test.sh +354 -0
  644. package/.agents/tests/fm-tmux-submit-busy.test.sh +340 -0
  645. package/.agents/tests/fm-tool-update-check.test.sh +1044 -0
  646. package/.agents/tests/fm-trace-context-lib.test.sh +253 -0
  647. package/.agents/tests/fm-trace-context-spawn.test.sh +581 -0
  648. package/.agents/tests/fm-transition-lib.test.sh +54 -0
  649. package/.agents/tests/fm-turnend-guard.test.sh +1822 -0
  650. package/.agents/tests/fm-update.test.sh +304 -0
  651. package/.agents/tests/fm-vendor-auth-probe.test.sh +395 -0
  652. package/.agents/tests/fm-voice-relay.test.sh +4741 -0
  653. package/.agents/tests/fm-wake-daemon-lifecycle-e2e.test.sh +171 -0
  654. package/.agents/tests/fm-wake-drain-open-decisions-cursor.test.sh +356 -0
  655. package/.agents/tests/fm-wake-drain-open-decisions.test.sh +226 -0
  656. package/.agents/tests/fm-wake-drain-unread-status.test.sh +379 -0
  657. package/.agents/tests/fm-wake-queue.test.sh +1224 -0
  658. package/.agents/tests/fm-watch-arm.test.sh +821 -0
  659. package/.agents/tests/fm-watch-checkpoint.test.sh +87 -0
  660. package/.agents/tests/fm-watch-recovery-loop.test.sh +223 -0
  661. package/.agents/tests/fm-watch-triage.test.sh +3889 -0
  662. package/.agents/tests/fm-watcher-lock.test.sh +1133 -0
  663. package/.agents/tests/fm-x-mode.test.sh +3042 -0
  664. package/.agents/tests/gleipnir-guard-pretool-check.test.sh +51 -0
  665. package/.agents/tests/gleipnir-machine-lock.test.sh +130 -0
  666. package/.agents/tests/herdr-test-safety.sh +42 -0
  667. package/.agents/tests/lib.sh +315 -0
  668. package/.agents/tests/remote-herdr-fixture.sh +125 -0
  669. package/.agents/tests/secondmate-helpers.sh +193 -0
  670. package/.agents/tests/smoke.test.sh +93 -0
  671. package/.agents/tests/wake-helpers.sh +338 -0
  672. package/.agents/tests/zellij-test-safety.sh +60 -0
  673. package/.agents/tools/README.md +10 -0
  674. package/.agents/tools/bin/tasks-cli +267 -0
  675. package/.agents/tools/tasks-cli.ts +216 -0
  676. package/AGENTS.md +454 -0
  677. package/CHANGELOG.md +881 -0
  678. package/LICENSE +1 -1
  679. package/NOTICE +63 -0
  680. package/README.md +113 -59
  681. package/RULES/01-domains.md +51 -0
  682. package/RULES/02-agents.md +78 -0
  683. package/RULES/03-houses.md +61 -0
  684. package/RULES/04-hoard.md +65 -0
  685. package/RULES/05-platforms.md +67 -0
  686. package/RULES/06-append-only.md +58 -0
  687. package/RULES/07-config.md +45 -0
  688. package/RULES/08-delivery-gate.md +75 -0
  689. package/RULES/README.md +20 -0
  690. package/bin/README.md +107 -0
  691. package/bin/a2a-mcp.sh +98 -0
  692. package/bin/a2a-serve.py +103 -0
  693. package/bin/a2a-serve.sh +8 -0
  694. package/bin/a2a-talk.sh +50 -0
  695. package/bin/a2abridge-ensure.sh +108 -0
  696. package/bin/agent-run.sh +43 -0
  697. package/bin/agents-config.sh +233 -0
  698. package/bin/apodex-smoke-test.sh +92 -0
  699. package/bin/bifrost-bridge.sh +112 -0
  700. package/bin/bifrost-ingress.sh +77 -0
  701. package/bin/bootstrap-macos.sh +107 -0
  702. package/bin/bootstrap-windows.ps1 +112 -0
  703. package/bin/branch-guard.sh +64 -0
  704. package/bin/brokk +73 -0
  705. package/bin/brokk-classify-lib.sh +1770 -0
  706. package/bin/brokk-lease-lib.sh +222 -0
  707. package/bin/brokk-lease.sh +193 -0
  708. package/bin/brokk-lint.sh +65 -0
  709. package/bin/brokk-send.sh +38 -0
  710. package/bin/brokk-update.sh +9 -0
  711. package/bin/brokk-wake-grant.sh +127 -0
  712. package/bin/brokk-wake-lib.sh +1849 -0
  713. package/bin/changelog-guard.sh +165 -0
  714. package/bin/crash-sense.sh +160 -0
  715. package/bin/desktop-place.sh +224 -0
  716. package/bin/docs-guard.sh +39 -0
  717. package/bin/editor-place.sh +207 -0
  718. package/bin/eindri-acclaim.sh +34 -0
  719. package/bin/eindri-control.sh +28 -0
  720. package/bin/eindri-role.sh +83 -0
  721. package/bin/eindri-seen.sh +41 -0
  722. package/bin/eindri-send.sh +15 -0
  723. package/bin/eindri-start.sh +85 -0
  724. package/bin/eindri-watch.sh +57 -0
  725. package/bin/einherjar-spawn.sh +576 -0
  726. package/bin/eir-doctor.sh +112 -0
  727. package/bin/erindi-brief.sh +271 -0
  728. package/bin/fleet-apply.sh +68 -0
  729. package/bin/github-deploy.sh +81 -0
  730. package/bin/gjallarhorn-expose.sh +102 -0
  731. package/bin/gjallarhorn-notify.sh +58 -0
  732. package/bin/gjallarhorn-purge.sh +54 -0
  733. package/bin/gjallarhorn-tunnel.sh +72 -0
  734. package/bin/gleipnir-lock-lib.sh +284 -0
  735. package/bin/groa-update.sh +122 -0
  736. package/bin/hall-snapshot.sh +92 -0
  737. package/bin/hamr-harness.sh +246 -0
  738. package/bin/heimdall-ssh-keys.sh +374 -0
  739. package/bin/herdr-agents.py +44 -0
  740. package/bin/herdr-ensure.sh +104 -0
  741. package/bin/herdr-run.sh +431 -0
  742. package/bin/hermes-ensure.sh +88 -0
  743. package/bin/hoard-lib.sh +25 -0
  744. package/bin/hodd.sh +78 -0
  745. package/bin/host-sense.sh +199 -0
  746. package/bin/huginn-research-worker.sh +108 -0
  747. package/bin/local-model-lock.sh +51 -0
  748. package/bin/mimir-bridge.py +226 -0
  749. package/bin/mimir-bridge.sh +87 -0
  750. package/bin/mimir-ingest.sh +131 -0
  751. package/bin/mimir.sh +113 -0
  752. package/bin/mjollnir-webhook.sh +59 -0
  753. package/bin/mjollnir.sh +85 -0
  754. package/bin/model-hardware.sh +82 -0
  755. package/bin/model-resolve.sh +147 -0
  756. package/bin/models-detect.sh +178 -0
  757. package/bin/nornir-cron-start.sh +162 -0
  758. package/bin/nornir-job-daily-briefing.sh +191 -0
  759. package/bin/nornir-job-git-sync.sh +152 -0
  760. package/bin/nornir-job-memory-housekeeping.sh +114 -0
  761. package/bin/nornir-job-observer.sh +166 -0
  762. package/bin/nornir-job-skillopt-sleep.sh +76 -0
  763. package/bin/omarchy-hook-install.sh +60 -0
  764. package/bin/omarchy-install.sh +148 -0
  765. package/bin/omarchy-plugins.sh +174 -0
  766. package/bin/omarchy-sense.sh +149 -0
  767. package/bin/perm-guard.sh +30 -0
  768. package/bin/pi-agent.sh +68 -0
  769. package/bin/pi-ensure.sh +77 -0
  770. package/bin/pi-local.sh +37 -0
  771. package/bin/pi-seat.sh +63 -0
  772. package/bin/prereq-ensure.sh +166 -0
  773. package/bin/private-guard.sh +38 -0
  774. package/bin/project-git.sh +55 -0
  775. package/bin/public-guard.sh +70 -0
  776. package/bin/ratatoskr.sh +49 -0
  777. package/bin/realm-lib.sh +32 -0
  778. package/bin/repo-scrub.sh +62 -0
  779. package/bin/rodd-operational-input.sh +250 -0
  780. package/bin/runes-append.sh +198 -0
  781. package/bin/saga-session-start.sh +172 -0
  782. package/bin/saga-sessionstart-run.sh +79 -0
  783. package/bin/saga-wake-drain.sh +41 -0
  784. package/bin/secret-guard.sh +63 -0
  785. package/bin/sessrumnir-ensure.sh +100 -0
  786. package/bin/sessrumnir-sync.sh +237 -0
  787. package/bin/sessrumnir.sh +91 -0
  788. package/bin/skillopt-setup.sh +76 -0
  789. package/bin/skuld-branch-outcome.sh +203 -0
  790. package/bin/skuld-branch-prompt.sh +101 -0
  791. package/bin/smidja-bootstrap.sh +90 -0
  792. package/bin/smidja-observe.sh +86 -0
  793. package/bin/syn-arm-pretool-check.sh +57 -0
  794. package/bin/syn-asset-pretool-check.sh +76 -0
  795. package/bin/syn-cd-pretool-check.sh +28 -0
  796. package/bin/syn-guard-pretool-check.sh +83 -0
  797. package/bin/syn-turnend-guard.sh +36 -0
  798. package/bin/syn-watch-arm.sh +99 -0
  799. package/bin/tailscale-sync.sh +190 -0
  800. package/bin/telegram-bot.sh +69 -0
  801. package/bin/toolchain.sh +74 -0
  802. package/bin/utgard.sh +111 -0
  803. package/bin/valhalla.sh +115 -0
  804. package/bin/valknut-load.sh +260 -0
  805. package/bin/vor-crew-state.sh +124 -0
  806. package/bin/wedge-notify.sh +47 -0
  807. package/bin/workspace-provision.sh +80 -0
  808. package/bin/workspace-rag.sh +150 -0
  809. package/bin/wyrd-db.sh +47 -0
  810. package/bin/yggdrasil.sh +152 -0
  811. package/bin/ymir-edit.sh +145 -0
  812. package/bin/ymir-install.sh +641 -0
  813. package/bin/ymir-invite.sh +93 -0
  814. package/bin/ymir-isolation.sh +88 -0
  815. package/bin/ymir-migrate.sh +61 -0
  816. package/bin/ymir-platform.sh +348 -0
  817. package/bin/ymir-say.sh +97 -0
  818. package/bin/ymir-setup-auth.sh +108 -0
  819. package/bin/ymir-validate.sh +173 -0
  820. package/bin/ymir.js +74 -16
  821. package/docs/Architecture.md +339 -0
  822. package/docs/design.md +351 -0
  823. package/docs/installations/README.md +63 -0
  824. package/docs/installations/ymir-home.md +93 -0
  825. package/docs/integration.md +105 -0
  826. package/docs/lore.md +1056 -0
  827. package/docs/research/agent-harnesses.md +290 -0
  828. package/docs/runbooks/README.md +19 -0
  829. package/docs/runbooks/a2a.md +91 -0
  830. package/docs/runbooks/agent-permissions.md +99 -0
  831. package/docs/runbooks/agents.md +57 -0
  832. package/docs/runbooks/models.md +69 -0
  833. package/docs/runbooks/realm-onboarding.md +60 -0
  834. package/docs/runbooks/secrets-and-hoard.md +60 -0
  835. package/docs/runbooks/updates-and-migrations.md +110 -0
  836. package/docs/runbooks/worktrees-isolation.md +72 -0
  837. package/docs/session-start.md +105 -0
  838. package/docs/skillopt-integration.md +321 -0
  839. package/docs/sync-system.md +66 -0
  840. package/docs/workspaces.md +104 -0
  841. package/docs/ymir-rut.md +260 -0
  842. package/package.json +44 -14
  843. package/packaging/build.sh +74 -0
  844. package/packaging/macos/Ymir Installer.command +43 -0
  845. package/packaging/windows/ymir-setup.nsi +74 -0
  846. package/scripts/README.md +22 -0
  847. package/scripts/electron.sh +210 -0
  848. package/scripts/start.sh +167 -0
  849. package/scripts/stop.sh +113 -0
  850. package/install.sh +0 -35
@@ -0,0 +1,1142 @@
1
+ #!/usr/bin/env bash
2
+ # tests/fm-backend.test.sh - P1 runtime-backend extraction conformance
3
+ # (data/fm-backend-design-d7/report.md, herdr-addendum.md "events as the core
4
+ # abstraction"). bin/fm-backend.sh and bin/backends/tmux.sh move the tmux
5
+ # command sequences that fm-send.sh, fm-peek.sh, fm-spawn.sh, and
6
+ # fm-teardown.sh used to run inline into named adapter functions. This suite:
7
+ #
8
+ # 1. Unit-tests bin/fm-backend.sh's selection, meta, and dispatch helpers.
9
+ # 2. Runs the PRE-REFACTOR versions of fm-send.sh, fm-peek.sh, fm-spawn.sh,
10
+ # and fm-teardown.sh (checked out from the merge-base with `main`, the
11
+ # commit this branch started from) against the SAME fake tmux/treehouse
12
+ # binaries and fixtures as the REFACTORED versions in this checkout, then
13
+ # diffs the two command logs byte-for-byte - the report's P1 checklist
14
+ # item "run current main scripts and refactored scripts against the same
15
+ # fake tools and compare command logs". The teardown old-vs-new case also
16
+ # overlays a content-historical permissive tmux kill fixture: after the
17
+ # exact-selector change lands on the default branch, merge-base with main
18
+ # collapses to HEAD and can no longer supply that baseline.
19
+ # 3. Asserts the `--backend`/`FM_BACKEND` selection refuses unknown backends
20
+ # and the blocked `codex-app` backend loudly.
21
+ #
22
+ # fm-watch.sh's signal/stale/check/heartbeat wake-string contract is already
23
+ # exercised end-to-end against this refactor by tests/fm-watch-triage.test.sh
24
+ # and tests/wake-helpers.sh (same fake-tmux convention, run against the
25
+ # now-refactored bin/fm-watch.sh); this suite adds one direct old-vs-new
26
+ # diff for the stale-pane path specifically, since that is the one wake path
27
+ # that now calls through fm_backend_capture instead of tmux directly.
28
+ # The real tmux smoke test (create session, send text + Enter, capture, list,
29
+ # kill) lives in tests/fm-backend-tmux-smoke.test.sh.
30
+ set -u
31
+
32
+ # shellcheck source=tests/lib.sh
33
+ . "$(dirname "${BASH_SOURCE[0]}")/lib.sh"
34
+ fm_git_identity fmtest fmtest@example.invalid
35
+
36
+ # shellcheck source=/dev/null
37
+ . "$ROOT/bin/fm-backend.sh"
38
+
39
+ TMP_ROOT=$(fm_test_tmproot fm-backend-tests)
40
+
41
+ # fm_backend_detect's cmux fallback (bundle id + process ancestry,
42
+ # docs/cmux-backend.md "Runtime auto-detection") consults uname, lsappinfo,
43
+ # and ps. FAKE_NONDARWIN_BIN pins uname to Linux so the whole fallback is
44
+ # deterministically inert for every assertion that expects NO detection,
45
+ # regardless of the ambient runtime this suite itself executes inside (a real
46
+ # cmux tab would otherwise leak a bundle-id or ancestry match into results).
47
+ FAKE_NONDARWIN_BIN="$TMP_ROOT/fake-nondarwin-bin"
48
+ mkdir -p "$FAKE_NONDARWIN_BIN"
49
+ printf '#!/bin/sh\necho Linux\n' > "$FAKE_NONDARWIN_BIN/uname"
50
+ chmod +x "$FAKE_NONDARWIN_BIN/uname"
51
+
52
+ # make_cmux_fallback_fakebin: PATH fakes for the DETECTING side of the cmux
53
+ # fallback - uname pinned to Darwin, lsappinfo echoing $FM_FAKE_LSAPPINFO_OUT
54
+ # (empty output mirrors the real lsappinfo's app-not-running behavior: prints
55
+ # nothing, exit 0), and a ps answering `-o ppid=/-o comm= -p <pid>` from the
56
+ # tab-separated "pid ppid comm" table file named by $FM_FAKE_PS_TABLE.
57
+ make_cmux_fallback_fakebin() { # <dir> -> echoes fakebin dir
58
+ local fb="$1/fakebin-cmux-fallback"
59
+ mkdir -p "$fb"
60
+ printf '#!/bin/sh\necho Darwin\n' > "$fb/uname"
61
+ cat > "$fb/lsappinfo" <<'SH'
62
+ #!/bin/sh
63
+ [ -n "${FM_FAKE_LSAPPINFO_OUT:-}" ] && printf '%s\n' "$FM_FAKE_LSAPPINFO_OUT"
64
+ exit 0
65
+ SH
66
+ cat > "$fb/ps" <<'SH'
67
+ #!/bin/sh
68
+ # supports exactly: ps -o ppid= -p <pid> / ps -o comm= -p <pid>
69
+ field=${2:-} pid=${4:-}
70
+ while IFS=" " read -r tpid tppid tcomm; do
71
+ if [ "$tpid" = "$pid" ]; then
72
+ case "$field" in
73
+ ppid=) printf '%s\n' "$tppid" ;;
74
+ comm=) printf '%s\n' "$tcomm" ;;
75
+ esac
76
+ exit 0
77
+ fi
78
+ done < "${FM_FAKE_PS_TABLE:?}"
79
+ exit 1
80
+ SH
81
+ chmod +x "$fb/uname" "$fb/lsappinfo" "$fb/ps"
82
+ printf '%s\n' "$fb"
83
+ }
84
+
85
+ # The commit this branch started from - the P1 "current main" baseline.
86
+ # Suitable for byte-identical old-vs-new checks while a branch still diverges
87
+ # from main. After a squash lands, merge-base(HEAD, main) collapses to HEAD, so
88
+ # callers that need a true pre-change fixture must not rely on this alone.
89
+ resolve_base_ref() {
90
+ local ref base
91
+ for ref in main refs/heads/main origin/main refs/remotes/origin/main origin/HEAD refs/remotes/origin/HEAD; do
92
+ if git -C "$ROOT" rev-parse --verify -q "$ref^{commit}" >/dev/null; then
93
+ base=$(git -C "$ROOT" merge-base HEAD "$ref" 2>/dev/null) || continue
94
+ [ -n "$base" ] || continue
95
+ printf '%s\n' "$base"
96
+ return 0
97
+ fi
98
+ done
99
+ return 1
100
+ }
101
+ BASE_REF=$(resolve_base_ref) \
102
+ || fail "fm-backend baseline requires local main or origin/main; fetch the default branch before running this test"
103
+
104
+ # Newest first-parent revision whose bin/backends/tmux.sh still uses the
105
+ # pre-exact permissive kill-window target. Content-addressed from history so the
106
+ # fixture stays historical on default-branch CI and on branches cut after the
107
+ # exact-selector change, where merge-base with main is self-referential.
108
+ resolve_permissive_tmux_kill_ref() {
109
+ local commit body
110
+ while IFS= read -r commit; do
111
+ [ -n "$commit" ] || continue
112
+ body=$(git -C "$ROOT" show "$commit:bin/backends/tmux.sh" 2>/dev/null) || continue
113
+ # shellcheck disable=SC2016
114
+ case "$body" in
115
+ *'tmux kill-window -t "=$session:=$window"'*) continue ;;
116
+ esac
117
+ # shellcheck disable=SC2016
118
+ case "$body" in
119
+ *'tmux kill-window -t "$1"'*|*'tmux kill-window -t "$target"'*)
120
+ printf '%s\n' "$commit"
121
+ return 0
122
+ ;;
123
+ esac
124
+ done < <(git -C "$ROOT" log --first-parent --format='%H' HEAD -- bin/backends/tmux.sh)
125
+ return 1
126
+ }
127
+
128
+ # --- shared: a pre-refactor bin/ shim --------------------------------------
129
+ #
130
+ # build_old_bin echoes a directory whose bin/ subdir is the complete bin/ tree
131
+ # from BASE_REF.
132
+ # Materializing the whole historical tree keeps every entrypoint and sourced
133
+ # sibling on the same revision, while avoiding a hand-maintained dependency
134
+ # list that can omit a newly sourced helper and make the old process abort
135
+ # before it reaches the behavior under test.
136
+ # FM_ROOT_OVERRIDE pointed at this dir's root makes
137
+ # "$FM_ROOT/bin/fm-project-mode.sh" (etc.) resolve correctly.
138
+ # The teardown conformance case applies its explicitly historical tmux adapter
139
+ # after this complete baseline has been materialized.
140
+
141
+ build_old_bin() { # <name> -> echoes root dir (root/bin/<script> is the entry point)
142
+ local name=$1 root archive
143
+ root="$TMP_ROOT/$name"
144
+ archive="$root/bin.tar"
145
+ mkdir -p "$root"
146
+ git -C "$ROOT" archive --format=tar "$BASE_REF" bin > "$archive" \
147
+ || fail "old-bin shim: could not archive bin/ from $BASE_REF"
148
+ tar -xf "$archive" -C "$root" \
149
+ || fail "old-bin shim: could not extract bin/ from $BASE_REF"
150
+ rm -f "$archive"
151
+ printf '%s\n' "$root"
152
+ }
153
+
154
+ # --- fm-backend.sh unit tests ------------------------------------------------
155
+
156
+ test_backend_name_precedence() {
157
+ local dir cfg
158
+ dir="$TMP_ROOT/name-precedence"; cfg="$dir/config"
159
+ mkdir -p "$cfg"
160
+
161
+ # TMUX/HERDR_ENV/CMUX_WORKSPACE_ID explicitly unset in a subshell so this
162
+ # stays deterministic regardless of the runtime this test suite itself
163
+ # happens to execute inside (e.g. a real tmux pane, which is the normal case
164
+ # for a captain's session).
165
+ # fm_backend_name reads FM_BACKEND_CONFIG_DIR (bound once, at fm-backend.sh
166
+ # source time, from FM_CONFIG_OVERRIDE); a later FM_CONFIG_OVERRIDE=... prefix
167
+ # on the function call itself does not re-bind it, so these calls set
168
+ # FM_BACKEND_CONFIG_DIR directly.
169
+ [ "$(unset TMUX HERDR_ENV CMUX_WORKSPACE_ID __CFBundleIdentifier; PATH="$FAKE_NONDARWIN_BIN:$PATH" FM_BACKEND='' FM_BACKEND_CONFIG_DIR="$cfg" fm_backend_name)" = tmux ] \
170
+ || fail "fm_backend_name should default to tmux with no env/config/detection markers"
171
+
172
+ printf 'tmux\n' > "$cfg/backend"
173
+ [ "$(unset TMUX HERDR_ENV CMUX_WORKSPACE_ID; FM_BACKEND='' FM_BACKEND_CONFIG_DIR="$cfg" fm_backend_name)" = tmux ] \
174
+ || fail "fm_backend_name should read config/backend"
175
+
176
+ [ "$(unset TMUX HERDR_ENV CMUX_WORKSPACE_ID; FM_BACKEND=tmux FM_BACKEND_CONFIG_DIR="$cfg" fm_backend_name)" = tmux ] \
177
+ || fail "FM_BACKEND env should win over config/backend"
178
+
179
+ pass "fm_backend_name: FM_BACKEND env > config/backend > default tmux"
180
+ }
181
+
182
+ # fm_backend_detect: environment-marker runtime auto-detection (mirrors
183
+ # fm-harness.sh's detect_own layer). Every case explicitly controls TMUX,
184
+ # HERDR_ENV, and CMUX_WORKSPACE_ID - and, where no detection is expected, the
185
+ # cmux fallback inputs (__CFBundleIdentifier plus a non-Darwin uname fake) -
186
+ # so results never depend on the ambient shell this suite runs inside (a real
187
+ # tmux pane or cmux tab, both normal cases for a captain's session).
188
+ test_backend_detect_precedence() {
189
+ local out
190
+
191
+ if out=$(unset TMUX HERDR_ENV CMUX_WORKSPACE_ID __CFBundleIdentifier; PATH="$FAKE_NONDARWIN_BIN:$PATH" fm_backend_detect); then
192
+ fail "fm_backend_detect should return 1 (undetected) with no markers set, got '$out'"
193
+ fi
194
+
195
+ out=$(unset TMUX CMUX_WORKSPACE_ID; HERDR_ENV=1 fm_backend_detect) \
196
+ || fail "fm_backend_detect should succeed when HERDR_ENV=1"
197
+ [ "$out" = herdr ] || fail "fm_backend_detect should report herdr for HERDR_ENV=1 alone, got '$out'"
198
+
199
+ out=$(unset HERDR_ENV CMUX_WORKSPACE_ID; TMUX='fake,1,0' fm_backend_detect) \
200
+ || fail "fm_backend_detect should succeed when \$TMUX is set"
201
+ [ "$out" = tmux ] || fail "fm_backend_detect should report tmux for \$TMUX alone, got '$out'"
202
+
203
+ out=$(unset TMUX HERDR_ENV; CMUX_WORKSPACE_ID='fake-uuid' fm_backend_detect) \
204
+ || fail "fm_backend_detect should succeed when CMUX_WORKSPACE_ID is set"
205
+ [ "$out" = cmux ] || fail "fm_backend_detect should report cmux for CMUX_WORKSPACE_ID alone, got '$out'"
206
+
207
+ # Nesting: tmux started inside a herdr pane carries BOTH markers. Innermost
208
+ # (tmux) must win, since that is the surface firstmate is actually running on.
209
+ out=$(unset CMUX_WORKSPACE_ID; TMUX='fake,1,0' HERDR_ENV=1 fm_backend_detect) \
210
+ || fail "fm_backend_detect should succeed with both markers present"
211
+ [ "$out" = tmux ] || fail "fm_backend_detect should resolve nesting innermost-first (tmux over herdr), got '$out'"
212
+
213
+ # Nesting: tmux started inside a cmux-provided shell carries BOTH markers.
214
+ # cmux is a terminal application, not a nestable multiplexer, so the
215
+ # innermost multiplexer (tmux) must still win.
216
+ out=$(unset HERDR_ENV; TMUX='fake,1,0' CMUX_WORKSPACE_ID='fake-uuid' fm_backend_detect) \
217
+ || fail "fm_backend_detect should succeed with tmux and cmux markers present"
218
+ [ "$out" = tmux ] || fail "fm_backend_detect should resolve nesting innermost-first (tmux over cmux), got '$out'"
219
+
220
+ # Nesting: herdr started inside a cmux-provided shell carries BOTH markers.
221
+ # Same reasoning: herdr (the innermost multiplexer) must win over cmux.
222
+ out=$(unset TMUX; HERDR_ENV=1 CMUX_WORKSPACE_ID='fake-uuid' fm_backend_detect) \
223
+ || fail "fm_backend_detect should succeed with herdr and cmux markers present"
224
+ [ "$out" = herdr ] || fail "fm_backend_detect should resolve nesting innermost-first (herdr over cmux), got '$out'"
225
+
226
+ # Pathological: all three markers present. tmux still wins (innermost of all).
227
+ out=$(TMUX='fake,1,0' HERDR_ENV=1 CMUX_WORKSPACE_ID='fake-uuid' fm_backend_detect) \
228
+ || fail "fm_backend_detect should succeed with all three markers present"
229
+ [ "$out" = tmux ] || fail "fm_backend_detect should resolve nesting innermost-first with all three markers (tmux wins), got '$out'"
230
+
231
+ pass "fm_backend_detect: no markers -> undetected, HERDR_ENV=1 -> herdr, \$TMUX -> tmux, CMUX_WORKSPACE_ID -> cmux, nested combinations resolve innermost-first"
232
+ }
233
+
234
+ # fm_backend_detect's cmux FALLBACK signals (docs/cmux-backend.md "Runtime
235
+ # auto-detection"): cmux's bundled claude wrapper strips every CMUX_* env var
236
+ # on its passthrough path, so a claude-under-cmux firstmate has no
237
+ # CMUX_WORKSPACE_ID; detection then falls back to __CFBundleIdentifier and,
238
+ # after that, a process-ancestry walk - macOS-only, and never outranking the
239
+ # $TMUX/HERDR_ENV innermost-first checks.
240
+ test_backend_detect_cmux_fallback_bundle_id() {
241
+ local dir fb out
242
+ dir="$TMP_ROOT/detect-fallback-bundle"; mkdir -p "$dir"
243
+ fb=$(make_cmux_fallback_fakebin "$dir")
244
+
245
+ out=$(unset TMUX HERDR_ENV CMUX_WORKSPACE_ID; PATH="$fb:$PATH" __CFBundleIdentifier='com.cmuxterm.app' fm_backend_detect) \
246
+ || fail "fm_backend_detect should fall back to the cmux bundle id when CMUX_WORKSPACE_ID is absent"
247
+ [ "$out" = cmux ] || fail "bundle-id fallback should report cmux, got '$out'"
248
+
249
+ (
250
+ unset TMUX HERDR_ENV CMUX_WORKSPACE_ID
251
+ PATH="$fb:$PATH" __CFBundleIdentifier='com.cmuxterm.app' fm_backend_detect >/dev/null || exit 1
252
+ [ "$FM_BACKEND_DETECT_SIGNAL" = bundle-id ] || exit 2
253
+ ) || fail "bundle-id fallback should set FM_BACKEND_DETECT_SIGNAL=bundle-id (subshell exit $?)"
254
+
255
+ # A foreign bundle id (an ordinary terminal app) must not match.
256
+ if out=$(unset TMUX HERDR_ENV CMUX_WORKSPACE_ID; PATH="$fb:$PATH" FM_FAKE_PS_TABLE="$dir/no-table" __CFBundleIdentifier='com.apple.Terminal' fm_backend_detect); then
257
+ fail "a non-cmux __CFBundleIdentifier should not detect cmux, got '$out'"
258
+ fi
259
+
260
+ pass "fm_backend_detect: falls back to __CFBundleIdentifier=com.cmuxterm.app when CMUX_WORKSPACE_ID is absent (signal bundle-id; foreign bundle ids rejected)"
261
+ }
262
+
263
+ test_backend_detect_cmux_fallback_requires_darwin() {
264
+ local out
265
+ if out=$(unset TMUX HERDR_ENV CMUX_WORKSPACE_ID; PATH="$FAKE_NONDARWIN_BIN:$PATH" __CFBundleIdentifier='com.cmuxterm.app' fm_backend_detect); then
266
+ fail "the cmux fallback must be macOS-only (cmux itself is), got '$out' on a non-Darwin uname"
267
+ fi
268
+ pass "fm_backend_detect: the cmux fallback signals are macOS-only (inert on a non-Darwin uname)"
269
+ }
270
+
271
+ # The false positive the innermost-first ordering must keep absorbing: a tmux
272
+ # server started from a cmux tab inherits __CFBundleIdentifier=com.cmuxterm.app
273
+ # into every pane (verified live, docs/cmux-backend.md), so the bundle-id
274
+ # fallback WILL match inside such panes - $TMUX winning first is what keeps
275
+ # the result correct. Same for a herdr pane whose server was started from a
276
+ # cmux tab.
277
+ test_backend_detect_cmux_fallback_tmux_nested_false_positive() {
278
+ local dir fb out
279
+ dir="$TMP_ROOT/detect-fallback-nested"; mkdir -p "$dir"
280
+ fb=$(make_cmux_fallback_fakebin "$dir")
281
+
282
+ out=$(unset HERDR_ENV CMUX_WORKSPACE_ID; PATH="$fb:$PATH" TMUX='fake,1,0' __CFBundleIdentifier='com.cmuxterm.app' fm_backend_detect) \
283
+ || fail "fm_backend_detect should still succeed with \$TMUX plus an inherited cmux bundle id"
284
+ [ "$out" = tmux ] || fail "\$TMUX must win over an inherited cmux bundle id (tmux-inside-cmux pane), got '$out'"
285
+
286
+ out=$(unset TMUX CMUX_WORKSPACE_ID; PATH="$fb:$PATH" HERDR_ENV=1 __CFBundleIdentifier='com.cmuxterm.app' fm_backend_detect) \
287
+ || fail "fm_backend_detect should still succeed with HERDR_ENV=1 plus an inherited cmux bundle id"
288
+ [ "$out" = herdr ] || fail "HERDR_ENV=1 must win over an inherited cmux bundle id (herdr-inside-cmux pane), got '$out'"
289
+
290
+ pass "fm_backend_detect: an inherited cmux bundle id never outranks \$TMUX or HERDR_ENV (tmux/herdr-inside-cmux false positive absorbed)"
291
+ }
292
+
293
+ test_backend_detect_cmux_fallback_ancestry_pid_match() {
294
+ local dir fb table
295
+ dir="$TMP_ROOT/detect-ancestry-pid"; mkdir -p "$dir"
296
+ fb=$(make_cmux_fallback_fakebin "$dir")
297
+ table="$dir/ps-table"
298
+ # $$ is this test script's own pid - the walk starts there. The cmux app
299
+ # pid (66666) is matched via the lsappinfo bundle-id resolution, with a
300
+ # deliberately non-standard install path so only the pid can match.
301
+ printf '%s\t77777\t/bin/zsh\n77777\t66666\t/usr/bin/login\n66666\t1\t/home/x/Custom.app/Contents/MacOS/custom\n' "$$" > "$table"
302
+
303
+ (
304
+ unset TMUX HERDR_ENV CMUX_WORKSPACE_ID __CFBundleIdentifier
305
+ PATH="$fb:$PATH" FM_FAKE_PS_TABLE="$table" FM_FAKE_LSAPPINFO_OUT='"pid"=66666' fm_backend_detect >/dev/null || exit 1
306
+ [ "$FM_BACKEND_DETECTED" = cmux ] || exit 2
307
+ [ "$FM_BACKEND_DETECT_SIGNAL" = ancestry ] || exit 3
308
+ ) || fail "ancestry fallback should detect cmux via the lsappinfo-resolved app pid (subshell exit $?)"
309
+
310
+ pass "fm_backend_detect: ancestry fallback matches the lsappinfo-resolved (bundle-id) cmux app pid in the parent chain"
311
+ }
312
+
313
+ test_backend_detect_cmux_fallback_ancestry_comm_match() {
314
+ local dir fb table
315
+ dir="$TMP_ROOT/detect-ancestry-comm"; mkdir -p "$dir"
316
+ fb=$(make_cmux_fallback_fakebin "$dir")
317
+ table="$dir/ps-table"
318
+ # lsappinfo resolves nothing (empty output, like the real one for a
319
+ # non-running or non-GUI-visible app); the bundle-shaped comm path is the
320
+ # remaining match, at a non-/Applications install location on purpose.
321
+ printf '%s\t77777\t/bin/zsh\n77777\t66666\t/usr/bin/login\n66666\t1\t/home/x/Applications/cmux.app/Contents/MacOS/cmux\n' "$$" > "$table"
322
+
323
+ (
324
+ unset TMUX HERDR_ENV CMUX_WORKSPACE_ID __CFBundleIdentifier FM_FAKE_LSAPPINFO_OUT
325
+ PATH="$fb:$PATH" FM_FAKE_PS_TABLE="$table" fm_backend_detect >/dev/null || exit 1
326
+ [ "$FM_BACKEND_DETECTED" = cmux ] || exit 2
327
+ [ "$FM_BACKEND_DETECT_SIGNAL" = ancestry ] || exit 3
328
+ ) || fail "ancestry fallback should detect cmux via a bundle-shaped comm path when lsappinfo resolves nothing (subshell exit $?)"
329
+
330
+ pass "fm_backend_detect: ancestry fallback matches a bundle-shaped cmux comm path at any install location when lsappinfo cannot resolve a pid"
331
+ }
332
+
333
+ # From inside tmux, ancestry can never reach cmux: the tmux server reparents
334
+ # to launchd (verified live - the reference machine's own tmux server, started
335
+ # from a cmux tab, has ppid 1), so the walk stops at ppid 1 undetected. This
336
+ # pins the walk's launchd stop as the structural guarantee behind that.
337
+ test_backend_detect_cmux_fallback_ancestry_stops_at_launchd() {
338
+ local dir fb table out
339
+ dir="$TMP_ROOT/detect-ancestry-stop"; mkdir -p "$dir"
340
+ fb=$(make_cmux_fallback_fakebin "$dir")
341
+ table="$dir/ps-table"
342
+ printf '%s\t77777\t/bin/zsh\n77777\t1\ttmux\n' "$$" > "$table"
343
+
344
+ if out=$(unset TMUX HERDR_ENV CMUX_WORKSPACE_ID __CFBundleIdentifier FM_FAKE_LSAPPINFO_OUT; PATH="$fb:$PATH" FM_FAKE_PS_TABLE="$table" fm_backend_detect); then
345
+ fail "ancestry fallback should stop undetected at a launchd-reparented chain, got '$out'"
346
+ fi
347
+ pass "fm_backend_detect: ancestry fallback stops undetected at launchd (a reparented tmux server never reaches cmux)"
348
+ }
349
+
350
+ # The auto-detect NOTICE must say when cmux was selected via a fallback
351
+ # signal, so a captain can tell a wrapper-stripped claude-under-cmux spawn
352
+ # apart from the primary-marker case.
353
+ test_backend_name_cmux_fallback_notice() {
354
+ local dir cfg fb out errfile
355
+ dir="$TMP_ROOT/name-fallback-notice"; cfg="$dir/config-empty"; mkdir -p "$cfg"
356
+ fb=$(make_cmux_fallback_fakebin "$dir")
357
+ errfile="$dir/err.txt"
358
+
359
+ : > "$errfile"
360
+ out=$(unset TMUX HERDR_ENV CMUX_WORKSPACE_ID; PATH="$fb:$PATH" __CFBundleIdentifier='com.cmuxterm.app' FM_BACKEND='' FM_BACKEND_CONFIG_DIR="$cfg" fm_backend_name 2>"$errfile")
361
+ [ "$out" = cmux ] || fail "fm_backend_name should auto-detect cmux via the bundle-id fallback, got '$out'"
362
+ assert_contains "$(cat "$errfile")" "FALLBACK signal __CFBundleIdentifier" \
363
+ "the fallback-detected cmux notice did not name the bundle-id fallback signal"
364
+ assert_contains "$(cat "$errfile")" "EXPERIMENTAL cmux backend" \
365
+ "the fallback-detected cmux notice lost the experimental warning"
366
+ assert_contains "$(cat "$errfile")" "--backend tmux" \
367
+ "the fallback-detected cmux notice lost the opt-out"
368
+
369
+ # The primary-marker notice is unchanged: it names CMUX_WORKSPACE_ID and
370
+ # carries no FALLBACK wording.
371
+ : > "$errfile"
372
+ out=$(unset TMUX HERDR_ENV; CMUX_WORKSPACE_ID='fake-uuid' FM_BACKEND='' FM_BACKEND_CONFIG_DIR="$cfg" fm_backend_name 2>"$errfile")
373
+ [ "$out" = cmux ] || fail "fm_backend_name should auto-detect cmux from CMUX_WORKSPACE_ID, got '$out'"
374
+ assert_contains "$(cat "$errfile")" "(CMUX_WORKSPACE_ID)" \
375
+ "the primary-marker cmux notice no longer names CMUX_WORKSPACE_ID"
376
+ case "$(cat "$errfile")" in
377
+ *FALLBACK*) fail "the primary-marker cmux notice must not carry FALLBACK wording" ;;
378
+ esac
379
+
380
+ pass "fm_backend_name: a fallback-detected cmux prints a NOTICE naming the fallback signal; the primary-marker notice is unchanged"
381
+ }
382
+
383
+ # fm_backend_name's auto-detect step: fires only when FM_BACKEND/config/backend
384
+ # are both absent, selects between the three markers exactly as
385
+ # fm_backend_detect does, and is loud only when it selects herdr or cmux -
386
+ # never when it selects tmux (today's default-path behavior must stay
387
+ # byte-for-byte silent).
388
+ test_backend_name_autodetect_notice() {
389
+ local dir cfg out errfile
390
+
391
+ dir="$TMP_ROOT/name-autodetect"; cfg="$dir/config-empty"; mkdir -p "$cfg"
392
+ errfile="$dir/err.txt"
393
+
394
+ : > "$errfile"
395
+ out=$(unset TMUX HERDR_ENV CMUX_WORKSPACE_ID __CFBundleIdentifier; PATH="$FAKE_NONDARWIN_BIN:$PATH" FM_BACKEND='' FM_BACKEND_CONFIG_DIR="$cfg" fm_backend_name 2>"$errfile")
396
+ [ "$out" = tmux ] || fail "fm_backend_name should default to tmux with no detection markers, got '$out'"
397
+ [ -s "$errfile" ] && fail "fm_backend_name must stay silent with no detection markers"$'\n'"$(cat "$errfile")"
398
+
399
+ : > "$errfile"
400
+ out=$(unset TMUX CMUX_WORKSPACE_ID; HERDR_ENV=1 FM_BACKEND='' FM_BACKEND_CONFIG_DIR="$cfg" fm_backend_name 2>"$errfile")
401
+ [ "$out" = herdr ] || fail "fm_backend_name should auto-detect herdr from HERDR_ENV=1, got '$out'"
402
+ assert_contains "$(cat "$errfile")" "EXPERIMENTAL herdr backend" \
403
+ "fm_backend_name did not print a loud notice when auto-detecting herdr"
404
+ assert_contains "$(cat "$errfile")" "config/backend" \
405
+ "fm_backend_name's auto-detect notice did not name the opt-out"
406
+
407
+ : > "$errfile"
408
+ out=$(unset HERDR_ENV CMUX_WORKSPACE_ID; TMUX='fake,1,0' FM_BACKEND='' FM_BACKEND_CONFIG_DIR="$cfg" fm_backend_name 2>"$errfile")
409
+ [ "$out" = tmux ] || fail "fm_backend_name should auto-detect tmux from \$TMUX, got '$out'"
410
+ [ -s "$errfile" ] && fail "auto-detecting tmux must stay silent (today's unchanged default-path behavior)"$'\n'"$(cat "$errfile")"
411
+
412
+ : > "$errfile"
413
+ out=$(unset TMUX HERDR_ENV; CMUX_WORKSPACE_ID='fake-uuid' FM_BACKEND='' FM_BACKEND_CONFIG_DIR="$cfg" fm_backend_name 2>"$errfile")
414
+ [ "$out" = cmux ] || fail "fm_backend_name should auto-detect cmux from CMUX_WORKSPACE_ID, got '$out'"
415
+ assert_contains "$(cat "$errfile")" "EXPERIMENTAL cmux backend" \
416
+ "fm_backend_name did not print a loud notice when auto-detecting cmux"
417
+ assert_contains "$(cat "$errfile")" "config/backend" \
418
+ "fm_backend_name's cmux auto-detect notice did not name the opt-out"
419
+ assert_contains "$(cat "$errfile")" "--backend tmux" \
420
+ "fm_backend_name's cmux auto-detect notice did not name the --backend tmux opt-out"
421
+
422
+ : > "$errfile"
423
+ out=$(unset CMUX_WORKSPACE_ID; TMUX='fake,1,0' HERDR_ENV=1 FM_BACKEND='' FM_BACKEND_CONFIG_DIR="$cfg" fm_backend_name 2>"$errfile")
424
+ [ "$out" = tmux ] || fail "nested tmux-in-herdr should auto-detect tmux (innermost first), got '$out'"
425
+ [ -s "$errfile" ] && fail "nested tmux-in-herdr auto-detect (result tmux) must stay silent"$'\n'"$(cat "$errfile")"
426
+
427
+ : > "$errfile"
428
+ out=$(unset HERDR_ENV; TMUX='fake,1,0' CMUX_WORKSPACE_ID='fake-uuid' FM_BACKEND='' FM_BACKEND_CONFIG_DIR="$cfg" fm_backend_name 2>"$errfile")
429
+ [ "$out" = tmux ] || fail "nested tmux-in-cmux should auto-detect tmux (innermost first), got '$out'"
430
+ [ -s "$errfile" ] && fail "nested tmux-in-cmux auto-detect (result tmux) must stay silent"$'\n'"$(cat "$errfile")"
431
+
432
+ pass "fm_backend_name: auto-detect selects herdr or cmux (loud notice) or tmux (silent, including nested tmux-in-herdr/tmux-in-cmux)"
433
+ }
434
+
435
+ # Explicit configuration (FM_BACKEND env or config/backend) always wins over
436
+ # runtime auto-detection, even when a detection marker points the other way.
437
+ test_backend_name_explicit_beats_detection() {
438
+ local dir cfg out
439
+
440
+ dir="$TMP_ROOT/name-explicit-beats-detect"
441
+ cfg="$dir/config-tmux"; mkdir -p "$cfg"; printf 'tmux\n' > "$cfg/backend"
442
+ mkdir -p "$dir/config-empty"
443
+
444
+ # fm_backend_name reads FM_BACKEND_CONFIG_DIR (bound once, at fm-backend.sh
445
+ # source time, from FM_CONFIG_OVERRIDE); a later FM_CONFIG_OVERRIDE=... prefix
446
+ # on the function call itself does not re-bind it, so these calls set
447
+ # FM_BACKEND_CONFIG_DIR directly to control which config dir is checked.
448
+ out=$(unset TMUX; HERDR_ENV=1 FM_BACKEND=tmux FM_BACKEND_CONFIG_DIR="$dir/config-empty" fm_backend_name)
449
+ [ "$out" = tmux ] || fail "FM_BACKEND=tmux should win over an ambient HERDR_ENV=1 auto-detect marker, got '$out'"
450
+
451
+ out=$(unset TMUX; HERDR_ENV=1 FM_BACKEND='' FM_BACKEND_CONFIG_DIR="$cfg" fm_backend_name)
452
+ [ "$out" = tmux ] || fail "config/backend=tmux should win over an ambient HERDR_ENV=1 auto-detect marker, got '$out'"
453
+
454
+ # The same opt-out must work for an ambient cmux auto-detect marker: a
455
+ # captain who is running firstmate inside a cmux terminal but explicitly
456
+ # wants tmux is never overridden by CMUX_WORKSPACE_ID.
457
+ out=$(unset TMUX HERDR_ENV; CMUX_WORKSPACE_ID='fake-uuid' FM_BACKEND=tmux FM_BACKEND_CONFIG_DIR="$dir/config-empty" fm_backend_name)
458
+ [ "$out" = tmux ] || fail "FM_BACKEND=tmux should win over an ambient CMUX_WORKSPACE_ID auto-detect marker, got '$out'"
459
+
460
+ out=$(unset TMUX HERDR_ENV; CMUX_WORKSPACE_ID='fake-uuid' FM_BACKEND='' FM_BACKEND_CONFIG_DIR="$cfg" fm_backend_name)
461
+ [ "$out" = tmux ] || fail "config/backend=tmux should win over an ambient CMUX_WORKSPACE_ID auto-detect marker, got '$out'"
462
+
463
+ pass "fm_backend_name: an explicit FM_BACKEND or config/backend setting always wins over runtime auto-detection, including an ambient cmux marker"
464
+ }
465
+
466
+ test_backend_validate_refuses_unknown() {
467
+ fm_backend_validate tmux 2>/dev/null || fail "fm_backend_validate should accept tmux"
468
+ fm_backend_validate orca 2>/dev/null || fail "fm_backend_validate should accept orca"
469
+ local out
470
+ # bogus names a backend with no adapter at all; tmux, herdr, zellij, orca,
471
+ # and cmux are all known adapters and spawn-supported.
472
+ out=$(fm_backend_validate bogus 2>&1) && fail "fm_backend_validate should refuse bogus (no such adapter)"
473
+ assert_contains "$out" "unknown backend 'bogus'" "fm_backend_validate did not name the rejected backend"
474
+ out=$(fm_backend_validate codex-app 2>&1) && fail "fm_backend_validate should refuse codex-app"
475
+ assert_contains "$out" "unknown backend 'codex-app'" "fm_backend_validate accepted codex-app"
476
+ out=$(fm_backend_validate "tmux herdr" 2>&1) && fail "fm_backend_validate should refuse a multi-token backend name"
477
+ assert_contains "$out" "unknown backend 'tmux herdr'" "fm_backend_validate accepted a multi-token backend name"
478
+ pass "fm_backend_validate: implemented adapters accepted, unknown and blocked codex-app backends refused loudly"
479
+ }
480
+
481
+ test_backend_source_shell_portable() {
482
+ local out status
483
+ # zsh does not word-split unquoted expansions; sourcing fm-backend.sh from
484
+ # an interactive zsh session must still recognize known backend names.
485
+ if command -v zsh >/dev/null 2>&1; then
486
+ zsh -c "cd '$ROOT' && source bin/fm-backend.sh && fm_backend_source herdr && whence -w fm_backend_herdr_capture >/dev/null" 2>/dev/null \
487
+ || fail "zsh: fm_backend_source herdr should load the adapter when sourced"
488
+ out=$(zsh -c "cd '$ROOT' && source bin/fm-backend.sh && fm_backend_source bogus" 2>&1) \
489
+ && fail "zsh: fm_backend_source bogus should fail"
490
+ assert_contains "$out" "unknown backend 'bogus'" \
491
+ "zsh: fm_backend_source did not reject bogus with the expected error"
492
+ pass "zsh: fm_backend_source recognizes known backends and rejects unknown ones"
493
+ else
494
+ pass "zsh: shell-portable backend matching skipped (zsh not found)"
495
+ fi
496
+
497
+ bash -c "cd '$ROOT' && source bin/fm-backend.sh && fm_backend_source herdr && declare -F fm_backend_herdr_capture >/dev/null" 2>/dev/null \
498
+ || fail "bash: fm_backend_source herdr should load the adapter when sourced"
499
+ out=$(bash -c "cd '$ROOT' && source bin/fm-backend.sh && fm_backend_source bogus" 2>&1) \
500
+ && fail "bash: fm_backend_source bogus should fail"
501
+ assert_contains "$out" "unknown backend 'bogus'" \
502
+ "bash: fm_backend_source did not reject bogus with the expected error"
503
+ pass "bash: fm_backend_source recognizes known backends and rejects unknown ones"
504
+ }
505
+
506
+ test_backend_validate_spawn_accepts_orca() {
507
+ local out
508
+ fm_backend_validate_spawn tmux 2>/dev/null || fail "fm_backend_validate_spawn should accept tmux"
509
+ fm_backend_validate_spawn herdr 2>/dev/null || fail "fm_backend_validate_spawn should accept herdr"
510
+ fm_backend_validate_spawn zellij 2>/dev/null || fail "fm_backend_validate_spawn should accept zellij"
511
+ fm_backend_validate_spawn orca 2>/dev/null || fail "fm_backend_validate_spawn should accept orca"
512
+ fm_backend_validate_spawn cmux 2>/dev/null || fail "fm_backend_validate_spawn should accept cmux"
513
+ out=$(fm_backend_validate_spawn bogus 2>&1) && fail "fm_backend_validate_spawn should still refuse unknown backends"
514
+ assert_contains "$out" "unknown backend 'bogus'" "fm_backend_validate_spawn did not preserve unknown-backend validation"
515
+ out=$(fm_backend_validate_spawn codex-app 2>&1) && fail "fm_backend_validate_spawn should refuse codex-app"
516
+ assert_contains "$out" "unknown backend 'codex-app'" "fm_backend_validate_spawn accepted codex-app"
517
+ out=$(fm_backend_validate_spawn "tmux herdr" 2>&1) && fail "fm_backend_validate_spawn should refuse a multi-token backend name"
518
+ assert_contains "$out" "unknown backend 'tmux herdr'" "fm_backend_validate_spawn accepted a multi-token backend name"
519
+ pass "fm_backend_validate_spawn: all implemented lifecycle backends are spawn-supported"
520
+ }
521
+
522
+ test_meta_get_and_backend_of_meta() {
523
+ local meta=$TMP_ROOT/meta-get.meta
524
+ fm_write_meta "$meta" "window=firstmate:fm-x1" "harness=claude"
525
+ [ "$(fm_meta_get "$meta" window)" = "firstmate:fm-x1" ] || fail "fm_meta_get did not read window="
526
+ [ "$(fm_meta_get "$meta" missing)" = "" ] || fail "fm_meta_get should print nothing for an absent key"
527
+ [ "$(fm_backend_of_meta "$meta")" = tmux ] || fail "fm_backend_of_meta should default absent backend= to tmux"
528
+
529
+ printf 'backend=tmux\n' >> "$meta"
530
+ [ "$(fm_backend_of_meta "$meta")" = tmux ] || fail "fm_backend_of_meta should read an explicit backend=tmux"
531
+
532
+ pass "fm_meta_get / fm_backend_of_meta: read key=value, default backend to tmux"
533
+ }
534
+
535
+ test_resolve_selector_three_forms() {
536
+ local state=$TMP_ROOT/resolve-state fakebin out
537
+ mkdir -p "$state"
538
+ fm_write_meta "$state/task1.meta" "window=firstmate:fm-task1"
539
+ fm_write_meta "$state/dotfiles-d6.meta" "window=default:wA:p2" "backend=herdr"
540
+ fm_write_meta "$state/fm-turnend-all-harnesses-v9.meta" "window=default:wB:p3" "backend=herdr"
541
+
542
+ [ "$(fm_backend_resolve_selector 'sess:win' "$state")" = "sess:win" ] \
543
+ || fail "explicit session:window should be used as-is"
544
+
545
+ [ "$(fm_backend_resolve_selector 'dotfiles-d6' "$state")" = "default:wA:p2" ] \
546
+ || fail "bare non-fm task id should resolve through exact metadata"
547
+ [ "$(fm_backend_of_selector 'dotfiles-d6' 'default:wA:p2' "$state")" = herdr ] \
548
+ || fail "bare non-fm task id should use its recorded backend"
549
+ [ "$(fm_backend_expected_label_of_selector 'dotfiles-d6' "$state")" = "fm-dotfiles-d6" ] \
550
+ || fail "bare non-fm task id should report the spawned fm-<id> label"
551
+
552
+ [ "$(fm_backend_resolve_selector 'fm-turnend-all-harnesses-v9' "$state")" = "default:wB:p3" ] \
553
+ || fail "exact fm-* task id should resolve through its exact metadata"
554
+ [ "$(fm_backend_of_selector 'fm-turnend-all-harnesses-v9' 'default:wB:p3' "$state")" = herdr ] \
555
+ || fail "exact fm-* task id should use exact metadata without stripping fm-"
556
+ [ "$(fm_backend_expected_label_of_selector 'fm-turnend-all-harnesses-v9' "$state")" = "fm-fm-turnend-all-harnesses-v9" ] \
557
+ || fail "exact fm-* task id should report the spawned fm-<id> label"
558
+
559
+ [ "$(fm_backend_resolve_selector 'fm-task1' "$state")" = "firstmate:fm-task1" ] \
560
+ || fail "legacy fm-<id> label should resolve through <id>.meta's window="
561
+ [ "$(fm_backend_expected_label_of_selector 'fm-task1' "$state")" = "fm-task1" ] \
562
+ || fail "legacy fm-<id> label should preserve its backend label"
563
+
564
+ out=$(fm_backend_resolve_selector 'fm-missing' "$state" 2>&1) && fail "fm-<id> with no meta should fail"
565
+ assert_contains "$out" "no metadata for fm-missing" "missing-meta error text changed"
566
+
567
+ fakebin="$TMP_ROOT/resolve-fakebin"; mkdir -p "$fakebin"
568
+ cat > "$fakebin/tmux" <<'SH'
569
+ #!/usr/bin/env bash
570
+ case "${1:-}" in
571
+ list-windows) printf 'firstmate:adhoc\nother:otherwin\n' ;;
572
+ esac
573
+ exit 0
574
+ SH
575
+ chmod +x "$fakebin/tmux"
576
+ out=$(PATH="$fakebin:$PATH" fm_backend_resolve_selector 'fm-adhoc' "$state" 2>&1) || true
577
+ # fm-adhoc carries no meta file, so it is NOT the bare-name fallback path - it
578
+ # is the fm-* meta-miss error path after exact-id and legacy-label metadata
579
+ # lookup both miss.
580
+ # Only a NON fm-* bare name falls through to the live-window search.
581
+ assert_contains "$out" "no metadata for fm-adhoc" "an fm-* selector must always require meta, not silently fall back to a live search"
582
+
583
+ out=$(PATH="$fakebin:$PATH" fm_backend_resolve_selector 'adhoc' "$state")
584
+ [ "$out" = "firstmate:adhoc" ] || fail "an ad hoc bare name should resolve via the tmux live-window fallback, got '$out'"
585
+
586
+ pass "fm_backend_resolve_selector: session:window literal, exact task id first, legacy fm-<id> label fallback, ad hoc bare name via tmux list-windows"
587
+ }
588
+
589
+ test_backend_of_selector_matches_explicit_target_meta() {
590
+ local state=$TMP_ROOT/backend-selector-state
591
+ mkdir -p "$state"
592
+ fm_write_meta "$state/herdr-task.meta" "window=default:w1:p2" "backend=herdr"
593
+ fm_write_meta "$state/dotfiles-d6.meta" "window=default:wA:p2" "backend=herdr"
594
+ fm_write_meta "$state/fm-turnend-all-harnesses-v9.meta" "window=default:wB:p3" "backend=herdr"
595
+ fm_write_meta "$state/tmux-task.meta" "window=firstmate:fm-tmux-task"
596
+ fm_write_meta "$state/custom-window-task.meta" "window=custom-window"
597
+ fm_write_meta "$state/orca-task.meta" "window=fm-orca-task" "terminal=term-orca-task" "backend=orca"
598
+
599
+ [ "$(fm_backend_of_selector 'dotfiles-d6' 'default:wA:p2' "$state")" = herdr ] \
600
+ || fail "bare non-fm task id selector should use its recorded backend"
601
+ [ "$(fm_backend_of_selector 'fm-turnend-all-harnesses-v9' 'default:wB:p3' "$state")" = herdr ] \
602
+ || fail "exact fm-* task id selector should use exact metadata before legacy stripping"
603
+ [ "$(fm_backend_of_selector 'fm-herdr-task' 'default:w1:p2' "$state")" = herdr ] \
604
+ || fail "legacy fm-<id> selector should use its recorded backend"
605
+ [ "$(fm_backend_resolve_selector 'fm-orca-task' "$state")" = term-orca-task ] \
606
+ || fail "Orca fm-<id> selector should resolve to terminal=, not window="
607
+ [ "$(fm_backend_resolve_selector 'term-orca-task' "$state")" = term-orca-task ] \
608
+ || fail "raw Orca terminal selector should resolve through metadata"
609
+ [ "$(fm_backend_resolve_selector 'custom-window' "$state")" = custom-window ] \
610
+ || fail "raw window selector matching metadata should not require tmux fallback"
611
+ [ "$(fm_backend_of_selector 'term-orca-task' 'term-orca-task' "$state")" = orca ] \
612
+ || fail "matching an explicit Orca terminal handle should inherit metadata backend"
613
+ [ "$(fm_backend_of_selector 'default:w1:p2' 'default:w1:p2' "$state")" = herdr ] \
614
+ || fail "explicit backend target matching metadata should use that task's backend"
615
+ [ "$(fm_backend_of_selector 'firstmate:fm-tmux-task' 'firstmate:fm-tmux-task' "$state")" = tmux ] \
616
+ || fail "explicit tmux-shaped target with absent backend= should default to tmux"
617
+ [ "$(fm_backend_of_selector 'manual:outside' 'manual:outside' "$state")" = tmux ] \
618
+ || fail "explicit target with no matching metadata should keep the tmux compatibility default"
619
+
620
+ pass "fm_backend_of_selector: exact task ids, legacy fm-<id> labels, and matching explicit targets inherit metadata backend"
621
+ }
622
+
623
+ # --- old vs new: fm-send.sh --------------------------------------------------
624
+
625
+ make_send_fakebin() { # <dir> -> echoes fakebin dir; logs every tmux call to $FM_TMUX_LOG
626
+ local dir=$1 fb="$1/fakebin"
627
+ mkdir -p "$fb"
628
+ cat > "$fb/tmux" <<'SH'
629
+ #!/usr/bin/env bash
630
+ set -u
631
+ { printf 'tmux'; for a in "$@"; do printf '\x1f%s' "$a"; done; printf '\n'; } >> "${FM_TMUX_LOG:?}"
632
+ case "${1:-}" in
633
+ send-keys) exit 0 ;;
634
+ display-message)
635
+ for a in "$@"; do case "$a" in *cursor_y*) printf '1\n'; exit 0 ;; esac; done
636
+ printf 'fakepane\n'; exit 0 ;;
637
+ capture-pane)
638
+ start= end=
639
+ while [ $# -gt 0 ]; do
640
+ case "$1" in
641
+ -S) start=$2; shift 2 ;;
642
+ -E) end=$2; shift 2 ;;
643
+ *) shift ;;
644
+ esac
645
+ done
646
+ if [ "$start" = 1 ] && [ "$end" = 1 ]; then
647
+ printf '│ │\n'
648
+ else
649
+ printf '╭────╮\n│ │\n╰────╯\n'
650
+ fi
651
+ exit 0 ;;
652
+ list-windows) exit 0 ;;
653
+ esac
654
+ exit 0
655
+ SH
656
+ chmod +x "$fb/tmux"
657
+ printf '%s\n' "$fb"
658
+ }
659
+
660
+ run_send_case() { # <bin-root> <fakebin> <log> <home> -- <send args...>
661
+ local bin=$1 fb=$2 log=$3 home=$4; shift 4
662
+ [ "${1:-}" = -- ] && shift
663
+ : > "$log"
664
+ env PATH="$fb:$PATH" FM_ROOT_OVERRIDE="$bin" FM_HOME="$home" FM_TMUX_LOG="$log" \
665
+ FM_SEND_SETTLE=0 FM_SEND_SLEEP=0 \
666
+ "$bin/bin/fm-send.sh" "$@" >/dev/null 2>&1
667
+ }
668
+
669
+ strip_send_preflight() { # <log>
670
+ local preflight
671
+ preflight=$'tmux\x1fdisplay-message\x1f-p\x1f-t\x1fsess:win\x1f#{pane_id}'
672
+ awk -v preflight="$preflight" '$0 != preflight { print }' "$1"
673
+ }
674
+
675
+ # The byte-identical old-vs-new tmux log comparison this test used to run
676
+ # covered the P1 backend extraction, which promised an unchanged command
677
+ # sequence. The composer consolidation (fm-composer-thin-adapter-refactor-r1)
678
+ # deliberately changed that sequence - the submit core reads a busy baseline
679
+ # before typing (its idle-to-busy turn-started confirmation) and the composer
680
+ # verdict comes from one full styled capture instead of a second band capture -
681
+ # so the current contract is asserted directly instead.
682
+ test_send_tmux_contract() {
683
+ local fb log home rc
684
+ fb=$(make_send_fakebin "$TMP_ROOT/send-fake")
685
+ home="$TMP_ROOT/send-home"; mkdir -p "$home/state"
686
+ log="$TMP_ROOT/send-new.log"
687
+
688
+ # Case 1: --key path - target verified, named key sent, no typing.
689
+ run_send_case "$ROOT" "$fb" "$log" "$home" -- "sess:win" --key Escape
690
+ rc=$?
691
+ expect_code 0 "$rc" "fm-send --key should succeed against a live fake pane"
692
+ assert_contains "$(cat "$log")" $'\x1f''display-message'$'\x1f''-p'$'\x1f''-t'$'\x1f''sess:win'$'\x1f''#{pane_id}' \
693
+ "fm-send --key did not verify the explicit tmux target before sending"
694
+ assert_contains "$(cat "$log")" $'\x1f''Escape' "fm-send --key did not send the named key"
695
+ assert_not_contains "$(cat "$log")" $'\x1f''-l'$'\x1f' "fm-send --key must not type literal text"
696
+
697
+ # Case 2: plain text - typed literally exactly once, submitted with Enter,
698
+ # confirmed against the bordered-empty fake composer.
699
+ run_send_case "$ROOT" "$fb" "$log" "$home" -- "sess:win" hello captain
700
+ rc=$?
701
+ expect_code 0 "$rc" "fm-send plain text should confirm against the empty fake composer"
702
+ assert_contains "$(cat "$log")" $'\x1f''send-keys'$'\x1f''-t'$'\x1f''sess:win'$'\x1f''-l'$'\x1f''hello captain' \
703
+ "fm-send did not send the literal text with send-keys -l"
704
+ [ "$(grep -c $'\x1f''-l'$'\x1f' "$log")" -eq 1 ] \
705
+ || fail "fm-send must type the text exactly once (Enter-only retries, never a retype)"
706
+ assert_contains "$(cat "$log")" $'\x1f''Enter' "fm-send did not submit with Enter"
707
+
708
+ # Case 3: a slash command still opens the popup-settle path (verified in
709
+ # tests/fm-send-popup-settle.test.sh) and ends in the same command shape:
710
+ # one literal type, then Enter.
711
+ run_send_case "$ROOT" "$fb" "$log" "$home" -- "sess:win" /some-skill
712
+ rc=$?
713
+ expect_code 0 "$rc" "fm-send /skill should confirm against the empty fake composer"
714
+ assert_contains "$(cat "$log")" $'\x1f''send-keys'$'\x1f''-t'$'\x1f''sess:win'$'\x1f''-l'$'\x1f''/some-skill' \
715
+ "fm-send /skill did not type the literal slash command"
716
+ [ "$(grep -c $'\x1f''-l'$'\x1f' "$log")" -eq 1 ] \
717
+ || fail "fm-send /skill must type the text exactly once"
718
+
719
+ pass "fm-send.sh: explicit tmux targets are verified; text types once and submits with Enter"
720
+ }
721
+
722
+ # --- old vs new: fm-peek.sh --------------------------------------------------
723
+
724
+ make_peek_fakebin() { # <dir> <capture-output> -> echoes fakebin dir
725
+ local dir=$1 payload=$2 fb="$1/fakebin"
726
+ mkdir -p "$fb"
727
+ printf '%s' "$payload" > "$dir/capture.out"
728
+ cat > "$fb/tmux" <<SH
729
+ #!/usr/bin/env bash
730
+ set -u
731
+ { printf 'tmux'; for a in "\$@"; do printf '\\x1f%s' "\$a"; done; printf '\\n'; } >> "\${FM_TMUX_LOG:?}"
732
+ case "\${1:-}" in
733
+ capture-pane) cat "$dir/capture.out" ;;
734
+ esac
735
+ exit 0
736
+ SH
737
+ chmod +x "$fb/tmux"
738
+ printf '%s\n' "$fb"
739
+ }
740
+
741
+ test_peek_conformance_old_vs_new() {
742
+ local old_bin fb log_old log_new home out_old out_new payload neutral_root
743
+ payload=$'line one\nline two\ncaptain on deck'
744
+ old_bin=$(build_old_bin peek-old)
745
+ fb=$(make_peek_fakebin "$TMP_ROOT/peek-fake" "$payload")
746
+ home="$TMP_ROOT/peek-home"; mkdir -p "$home/state"
747
+ log_old="$TMP_ROOT/peek-old.log"; log_new="$TMP_ROOT/peek-new.log"
748
+ # A fresh non-git dir keeps fm-guard.sh's worktree-tangle check inert (it warns
749
+ # to stderr, discarded below) - neither run needs FM_ROOT for anything beyond
750
+ # that guard, since STATE/HOME are already overridden directly.
751
+ neutral_root="$TMP_ROOT/peek-neutral-root"; mkdir -p "$neutral_root"
752
+
753
+ : > "$log_old"
754
+ out_old=$(PATH="$fb:$PATH" FM_ROOT_OVERRIDE="$neutral_root" FM_HOME="$home" FM_TMUX_LOG="$log_old" \
755
+ "$old_bin/bin/fm-peek.sh" "sess:win" 25 2>/dev/null)
756
+ : > "$log_new"
757
+ out_new=$(PATH="$fb:$PATH" FM_ROOT_OVERRIDE="$neutral_root" FM_HOME="$home" FM_TMUX_LOG="$log_new" \
758
+ "$ROOT/bin/fm-peek.sh" "sess:win" 25 2>/dev/null)
759
+
760
+ [ "$out_old" = "$out_new" ] || fail "fm-peek output differs old vs new"$'\n'"--- old ---"$'\n'"$out_old"$'\n'"--- new ---"$'\n'"$out_new"
761
+ [ "$out_new" = "$payload" ] || fail "fm-peek did not pass through the fake capture-pane output exactly"
762
+ diff -u "$log_old" "$log_new" > "$TMP_ROOT/peek-diff.txt" 2>&1 \
763
+ || fail "fm-peek: tmux command log differs old vs new"$'\n'"$(cat "$TMP_ROOT/peek-diff.txt")"
764
+ assert_contains "$(cat "$log_new")" $'\x1f''capture-pane'$'\x1f''-p'$'\x1f''-t'$'\x1f''sess:win'$'\x1f''-S'$'\x1f''-25' \
765
+ "fm-peek did not call capture-pane -p -t <target> -S -<lines> exactly"
766
+
767
+ pass "fm-peek.sh: capture-pane invocation and output are byte-identical old vs new"
768
+ }
769
+
770
+ # --- old vs new: fm-spawn.sh --------------------------------------------------
771
+
772
+ make_spawn_fakebin() { # <dir> <fake-worktree-path> -> echoes fakebin dir
773
+ local dir=$1 wt=$2 fb="$1/fakebin"
774
+ mkdir -p "$fb"
775
+ cat > "$fb/tmux" <<SH
776
+ #!/usr/bin/env bash
777
+ set -u
778
+ { printf 'tmux'; for a in "\$@"; do printf '\\x1f%s' "\$a"; done; printf '\\n'; } >> "\${FM_TMUX_LOG:?}"
779
+ case "\${1:-}" in
780
+ display-message)
781
+ for a in "\$@"; do case "\$a" in *pane_current_path*) printf '%s\\n' "$wt"; exit 0 ;; esac; done
782
+ printf 'firstmate\\n'; exit 0 ;;
783
+ list-windows) exit 0 ;;
784
+ esac
785
+ exit 0
786
+ SH
787
+ chmod +x "$fb/tmux"
788
+ fm_fake_exit0 "$fb" treehouse
789
+ printf '%s\n' "$fb"
790
+ }
791
+
792
+ run_spawn_case() { # <bin-root> <fakebin> <log> <state> <data> <config> <proj> -- <spawn args...>
793
+ local bin=$1 fb=$2 log=$3 state=$4 data=$5 config=$6 proj=$7; shift 7
794
+ [ "${1:-}" = -- ] && shift
795
+ : > "$log"
796
+ env PATH="$fb:$PATH" FM_ROOT_OVERRIDE="$bin" \
797
+ FM_STATE_OVERRIDE="$state" FM_DATA_OVERRIDE="$data" FM_CONFIG_OVERRIDE="$config" \
798
+ FM_PROJECTS_OVERRIDE="$TMP_ROOT/unused-projects" \
799
+ FM_SPAWN_NO_GUARD=1 TMUX="fake,1,0" FM_TMUX_LOG="$log" \
800
+ "$bin/bin/fm-spawn.sh" "$@"
801
+ }
802
+
803
+ # NOTE: the old-vs-new spawn command-log conformance test that used to live here
804
+ # was retired. It asserted the P1 backend refactor was a byte-for-byte pure
805
+ # extraction of the spawn window-creation/targeting sequence, but that sequence
806
+ # is now DELIBERATELY changed: fm-spawn drives the tmux backend to capture a
807
+ # stable window id, pin the window name (automatic-rename/allow-rename off), and
808
+ # target that id for the rename-critical spawn steps (robustness under a
809
+ # captain's non-default tmux config). A byte-identical old-vs-new diff can no
810
+ # longer hold there by design. That intended sequence is now authoritatively and
811
+ # comprehensively verified - via a recording fake-tmux - by
812
+ # tests/fm-tangle-guard.test.sh ("fm-spawn: appends windows by session-colon,
813
+ # pins the name, and targets the window id"), and the real tmux create/kill path
814
+ # by tests/fm-backend-tmux-smoke.test.sh. The send/peek/teardown conformance
815
+ # tests below remain pure extractions and stay. (make_spawn_fakebin and
816
+ # run_spawn_case are retained: test_spawn_default_backend_writes_no_meta_field
817
+ # uses make_spawn_fakebin, and #294's run_spawn_symlink_case uses run_spawn_case.)
818
+
819
+ # --- symlinked project prefix must not false-refuse the isolation guard -----
820
+ #
821
+ # docs/herdr-backend.md "Known gaps": a real backend's pane_current_path read
822
+ # (tmux, herdr) reports the OS-level PHYSICALLY-resolved cwd. When the project
823
+ # itself lives under a symlinked prefix (e.g. macOS's /tmp -> /private/tmp),
824
+ # fm-spawn.sh's PROJ_ABS - a logical `cd && pwd` - differs string-for-string
825
+ # from that physical read even before treehouse moves the pane at all, so the
826
+ # worktree-discovery poll used to mistake an UNMOVED pane for one that had
827
+ # already left the project, handing validate_spawn_worktree the project's own
828
+ # directory as "the worktree" and tripping its false isolation refusal.
829
+ # make_spawn_symlink_fakebin's tmux stub returns an unmoved project path on the
830
+ # first pane_current_path poll, then the real worktree path from the second poll
831
+ # onward, so this test fails loudly if the PROJ_ABS/PROJ_ABS_REAL
832
+ # canonicalization in bin/fm-spawn.sh ever regresses.
833
+ make_spawn_symlink_fakebin() { # <dir> <initial-project-path> <worktree-path> -> echoes fakebin dir
834
+ local dir=$1 initial_path=$2 wt=$3 fb="$1/fakebin" counter="$1/poll-count"
835
+ mkdir -p "$fb"
836
+ : > "$counter"
837
+ cat > "$fb/tmux" <<SH
838
+ #!/usr/bin/env bash
839
+ set -u
840
+ { printf 'tmux'; for a in "\$@"; do printf '\\x1f%s' "\$a"; done; printf '\\n'; } >> "\${FM_TMUX_LOG:?}"
841
+ case "\${1:-}" in
842
+ display-message)
843
+ for a in "\$@"; do case "\$a" in *pane_current_path*)
844
+ printf x >> "$counter"
845
+ if [ "\$(wc -c < "$counter")" -le 1 ]; then
846
+ printf '%s\\n' "$initial_path"
847
+ else
848
+ printf '%s\\n' "$wt"
849
+ fi
850
+ exit 0
851
+ ;; esac; done
852
+ printf 'firstmate\\n'; exit 0 ;;
853
+ list-windows) exit 0 ;;
854
+ esac
855
+ exit 0
856
+ SH
857
+ chmod +x "$fb/tmux"
858
+ fm_fake_exit0 "$fb" treehouse
859
+ printf '%s\n' "$fb"
860
+ }
861
+
862
+ run_spawn_symlink_case() { # <label> <physical|logical>
863
+ local label=$1 first_reply=$2 real_root link_root proj wt id fb data state config log out rc proj_phys initial_path
864
+ real_root="$TMP_ROOT/symlink-real-$label"; link_root="$TMP_ROOT/symlink-link-$label"
865
+ mkdir -p "$real_root"
866
+ ln -s "$real_root" "$link_root"
867
+ proj="$link_root/proj"
868
+ wt="$TMP_ROOT/symlink-wt-$label"
869
+ id="spawnsymlink$label"
870
+ fm_git_worktree "$real_root/proj" "$wt" "fm/$id"
871
+ # TMP_ROOT itself can already sit behind an OS-level symlink (e.g. macOS's
872
+ # /var -> /private/var), so resolve the fakebin's "physical" reply with
873
+ # pwd -P rather than string concatenation - it must match exactly what
874
+ # fm-spawn.sh's own PROJ_ABS_REAL computes, including any symlink layers
875
+ # ABOVE this test's own synthetic real_root/link_root pair.
876
+ proj_phys=$(cd "$real_root/proj" && pwd -P)
877
+ case "$first_reply" in
878
+ physical) initial_path=$proj_phys ;;
879
+ logical) initial_path=$proj ;;
880
+ *) fail "unknown symlink first-reply mode: $first_reply" ;;
881
+ esac
882
+ fb=$(make_spawn_symlink_fakebin "$TMP_ROOT/symlink-fake-$label" "$initial_path" "$wt")
883
+ data="$TMP_ROOT/symlink-data-$label"
884
+ mkdir -p "$data/$id"
885
+ printf 'test brief content\n' > "$data/$id/brief.md"
886
+ state="$TMP_ROOT/symlink-state-$label"; config="$TMP_ROOT/symlink-config-$label"
887
+ mkdir -p "$state" "$config"
888
+ log="$TMP_ROOT/symlink-spawn-$label.log"
889
+
890
+ out=$(run_spawn_case "$ROOT" "$fb" "$log" "$state" "$data" "$config" "$proj" -- "$id" "$proj" claude --mode no-mistakes --yolo off 2>&1)
891
+ rc=$?
892
+ expect_code 0 "$rc" "fm-spawn.sh should succeed for a project reached through a symlinked prefix when the backend reports $first_reply cwd"$'\n'"$out"
893
+ assert_contains "$out" "worktree=$wt" \
894
+ "fm-spawn.sh did not resolve a symlinked-prefix project to its real worktree when the backend reports $first_reply cwd"
895
+
896
+ rm -rf "/tmp/fm-$id"
897
+ }
898
+
899
+ test_spawn_symlinked_project_prefix_avoids_false_refusal() {
900
+ run_spawn_symlink_case physical physical
901
+ run_spawn_symlink_case logical logical
902
+ pass "fm-spawn.sh: a project reached through a symlinked prefix (e.g. macOS /tmp -> /private/tmp) does not trip the isolation guard's false refusal"
903
+ }
904
+
905
+ # --- old vs new: fm-teardown.sh ----------------------------------------------
906
+
907
+ make_teardown_fakebin() { # <dir> -> echoes fakebin dir; logs tmux+treehouse calls
908
+ local dir=$1 fb="$1/fakebin"
909
+ mkdir -p "$fb"
910
+ cat > "$fb/tmux" <<'SH'
911
+ #!/usr/bin/env bash
912
+ set -u
913
+ { printf 'tmux'; for a in "$@"; do printf '\x1f%s' "$a"; done; printf '\n'; } >> "${FM_TMUX_LOG:?}"
914
+ exit 0
915
+ SH
916
+ cat > "$fb/treehouse" <<'SH'
917
+ #!/usr/bin/env bash
918
+ set -u
919
+ { printf 'treehouse'; for a in "$@"; do printf '\x1f%s' "$a"; done; printf '\n'; } >> "${FM_TMUX_LOG:?}"
920
+ exit 0
921
+ SH
922
+ chmod +x "$fb/tmux" "$fb/treehouse"
923
+ printf '%s\n' "$fb"
924
+ }
925
+
926
+ # run_teardown_case <script> <fm-root-override> <fakebin> <log> <state> <data> <config> <id>
927
+ # FM_ROOT_OVERRIDE is passed separately from <script> so both the old and new
928
+ # runs can point it at the SAME neutral (non-git) shim root - that root's
929
+ # bin/fm-guard.sh is a symlink to the real, unchanged script, so the
930
+ # worktree-tangle check runs identically (and silently) for both, regardless
931
+ # of which fm-teardown.sh (old or new) is actually being invoked.
932
+ run_teardown_case() {
933
+ local script=$1 fmroot=$2 fb=$3 log=$4 state=$5 data=$6 config=$7 id=$8
934
+ : > "$log"
935
+ env PATH="$fb:$PATH" FM_ROOT_OVERRIDE="$fmroot" \
936
+ FM_STATE_OVERRIDE="$state" FM_DATA_OVERRIDE="$data" FM_CONFIG_OVERRIDE="$config" \
937
+ FM_TMUX_LOG="$log" \
938
+ "$script" "$id"
939
+ }
940
+
941
+ test_teardown_conformance_old_vs_new() {
942
+ local old_bin fb proj wt id old_tmux_ref saved_base_ref
943
+ local state_old state_new config_old config_new data log_old log_new out_old out_new rc_old rc_new
944
+ # Force the post-squash topology inside this case: merge-base with main may
945
+ # equal HEAD on default-branch CI, and that must not make the legacy kill
946
+ # fixture self-referential. build_old_bin still uses BASE_REF for entrypoints;
947
+ # only the tmux kill adapter is pinned to the content-historical permissive ref.
948
+ saved_base_ref=$BASE_REF
949
+ BASE_REF=$(git -C "$ROOT" rev-parse HEAD)
950
+ old_tmux_ref=$(resolve_permissive_tmux_kill_ref) \
951
+ || { BASE_REF=$saved_base_ref; fail "unable to locate a historical bin/backends/tmux.sh with permissive kill-window selectors"; }
952
+ old_bin=$(build_old_bin teardown-old)
953
+ git -C "$ROOT" show "$old_tmux_ref:bin/backends/tmux.sh" > "$old_bin/bin/backends/tmux.sh" \
954
+ || { BASE_REF=$saved_base_ref; fail "could not materialize historical tmux adapter from $old_tmux_ref"; }
955
+ BASE_REF=$saved_base_ref
956
+ proj="$TMP_ROOT/teardown-project"; wt="$TMP_ROOT/teardown-wt"
957
+ id="teardownconform1"
958
+ fm_git_worktree "$proj" "$wt" "fm/$id"
959
+ fb=$(make_teardown_fakebin "$TMP_ROOT/teardown-fake")
960
+
961
+ data="$TMP_ROOT/teardown-data"
962
+ mkdir -p "$data/$id"
963
+ printf 'scout findings\n' > "$data/$id/report.md"
964
+
965
+ state_old="$TMP_ROOT/teardown-state-old"; state_new="$TMP_ROOT/teardown-state-new"
966
+ config_old="$TMP_ROOT/teardown-config-old"; config_new="$TMP_ROOT/teardown-config-new"
967
+ mkdir -p "$state_old" "$state_new" "$config_old" "$config_new"
968
+
969
+ fm_write_meta "$state_old/$id.meta" \
970
+ "window=firstmate:fm-$id" "worktree=$wt" "project=$proj" "harness=claude" "kind=scout" "mode=no-mistakes" "yolo=off" \
971
+ "decisions_reviewed=1" "decision_keys="
972
+ fm_write_meta "$state_new/$id.meta" \
973
+ "window=firstmate:fm-$id" "worktree=$wt" "project=$proj" "harness=claude" "kind=scout" "mode=no-mistakes" "yolo=off" \
974
+ "decisions_reviewed=1" "decision_keys="
975
+ touch "$state_old/.last-watcher-beat" "$state_new/.last-watcher-beat"
976
+
977
+ log_old="$TMP_ROOT/teardown-old.log"; log_new="$TMP_ROOT/teardown-new.log"
978
+ out_old=$(run_teardown_case "$old_bin/bin/fm-teardown.sh" "$old_bin" "$fb" "$log_old" "$state_old" "$data" "$config_old" "$id" 2>&1)
979
+ rc_old=$?
980
+ out_new=$(run_teardown_case "$ROOT/bin/fm-teardown.sh" "$old_bin" "$fb" "$log_new" "$state_new" "$data" "$config_new" "$id" 2>&1)
981
+ rc_new=$?
982
+
983
+ expect_code 0 "$rc_old" "old fm-teardown.sh (scout, report present) should succeed"$'\n'"$out_old"
984
+ expect_code 0 "$rc_new" "new fm-teardown.sh (scout, report present) should succeed"$'\n'"$out_new"
985
+ assert_contains "$(cat "$log_new")" "treehouse"$'\x1f''return'$'\x1f''--force'$'\x1f'"$wt" \
986
+ "teardown did not call treehouse return --force <worktree>"
987
+ # The legacy fixture's adapter comes from BASE_REF, so its selector form is
988
+ # whatever the merge-base carried: permissive while the exact-selector change
989
+ # was still on a branch, exact for every branch cut after it landed on main.
990
+ # Pinning the old form here would make this case pass once and then fail
991
+ # forever, so the '=' exactness markers are normalized away and the legacy run
992
+ # is only required to have reached tmux window cleanup for this task. The
993
+ # exact-selector contract belongs to the current script, asserted below.
994
+ assert_contains "$(tr -d '=' < "$log_old")" "tmux"$'\x1f''kill-window'$'\x1f''-t'$'\x1f'"firstmate:fm-$id" \
995
+ "legacy teardown fixture did not exercise tmux window cleanup for the task"
996
+ assert_contains "$(cat "$log_new")" "tmux"$'\x1f''kill-window'$'\x1f''-t'$'\x1f'"=firstmate:=fm-$id" \
997
+ "teardown did not call tmux kill-window with exact session and window selectors"
998
+
999
+ pass "fm-teardown.sh: treehouse return remains compatible while tmux cleanup uses exact selectors"
1000
+ }
1001
+
1002
+ # --- backend selection loudly refuses an unknown backend --------------------
1003
+
1004
+ test_spawn_refuses_unknown_backend_flag() {
1005
+ local out status
1006
+ # bogus names a backend with no adapter at all; zellij and orca both
1007
+ # graduated to real adapters and have their own spawn tests.
1008
+ out=$(FM_ROOT_OVERRIDE='' FM_HOME='' FM_STATE_OVERRIDE='' FM_DATA_OVERRIDE='' \
1009
+ FM_PROJECTS_OVERRIDE='' FM_CONFIG_OVERRIDE='' FM_SPAWN_NO_GUARD=1 \
1010
+ "$ROOT/bin/fm-spawn.sh" nope-backend-z1 projects/none claude --mode no-mistakes --yolo off --backend bogus 2>&1)
1011
+ status=$?
1012
+ [ "$status" -ne 0 ] || fail "fm-spawn --backend bogus should refuse"
1013
+ assert_contains "$out" "unknown backend 'bogus'" "fm-spawn did not name the rejected backend"
1014
+ pass "fm-spawn.sh --backend bogus is refused loudly"
1015
+ }
1016
+
1017
+ test_spawn_refuses_codex_app_backend_flag() {
1018
+ local out status
1019
+ out=$(FM_ROOT_OVERRIDE='' FM_HOME='' FM_STATE_OVERRIDE='' FM_DATA_OVERRIDE='' \
1020
+ FM_PROJECTS_OVERRIDE='' FM_CONFIG_OVERRIDE='' FM_SPAWN_NO_GUARD=1 \
1021
+ "$ROOT/bin/fm-spawn.sh" nope-codex-app-z1 projects/none claude --mode no-mistakes --yolo off --backend codex-app 2>&1)
1022
+ status=$?
1023
+ [ "$status" -ne 0 ] || fail "fm-spawn --backend codex-app should refuse"
1024
+ assert_contains "$out" "unknown backend 'codex-app'" "fm-spawn did not preserve the blocked codex-app contract"
1025
+ pass "fm-spawn.sh --backend codex-app is refused"
1026
+ }
1027
+
1028
+ test_spawn_refuses_unknown_fm_backend_env() {
1029
+ local out status
1030
+ out=$(FM_ROOT_OVERRIDE='' FM_HOME='' FM_STATE_OVERRIDE='' FM_DATA_OVERRIDE='' \
1031
+ FM_PROJECTS_OVERRIDE='' FM_CONFIG_OVERRIDE='' FM_SPAWN_NO_GUARD=1 FM_BACKEND=bogus \
1032
+ "$ROOT/bin/fm-spawn.sh" nope-backend-z2 projects/none claude --mode no-mistakes --yolo off 2>&1)
1033
+ status=$?
1034
+ [ "$status" -ne 0 ] || fail "FM_BACKEND=bogus should refuse"
1035
+ assert_contains "$out" "unknown backend 'bogus'" "fm-spawn did not name the rejected FM_BACKEND"
1036
+ pass "fm-spawn.sh honors FM_BACKEND and refuses an unimplemented value loudly"
1037
+ }
1038
+
1039
+ test_spawn_default_backend_writes_no_meta_field() {
1040
+ local proj wt data id state config out
1041
+ proj="$TMP_ROOT/nobackend-project"; wt="$TMP_ROOT/nobackend-wt"; data="$TMP_ROOT/nobackend-data"
1042
+ id="nobackendz3"
1043
+ fm_git_worktree "$proj" "$wt" "fm/$id"
1044
+ local fb
1045
+ fb=$(make_spawn_fakebin "$TMP_ROOT/nobackend-fake" "$wt")
1046
+ mkdir -p "$data/$id"; printf 'brief\n' > "$data/$id/brief.md"
1047
+ state="$TMP_ROOT/nobackend-state"; config="$TMP_ROOT/nobackend-config"
1048
+ mkdir -p "$state" "$config"
1049
+
1050
+ out=$(PATH="$fb:$PATH" FM_ROOT_OVERRIDE="$ROOT" \
1051
+ FM_STATE_OVERRIDE="$state" FM_DATA_OVERRIDE="$data" FM_CONFIG_OVERRIDE="$config" \
1052
+ FM_PROJECTS_OVERRIDE="$TMP_ROOT/unused-projects" FM_SPAWN_NO_GUARD=1 TMUX="fake,1,0" \
1053
+ FM_TMUX_LOG="$TMP_ROOT/nobackend.log" \
1054
+ "$ROOT/bin/fm-spawn.sh" "$id" "$proj" claude --mode no-mistakes --yolo off --backend tmux 2>&1)
1055
+ expect_code 0 $? "explicit --backend tmux should spawn successfully"$'\n'"$out"
1056
+ assert_no_grep 'backend=' "$state/$id.meta" \
1057
+ "an explicit --backend tmux (the default) must not write backend= to meta (P1 compatibility contract)"
1058
+ rm -rf "/tmp/fm-$id"
1059
+ pass "fm-spawn.sh: an explicit --backend tmux resolves silently and writes no backend= (missing means tmux)"
1060
+ }
1061
+
1062
+ test_spawn_explicit_backend_flag_beats_autodetect_herdr_env() {
1063
+ local proj wt data id state config out fb
1064
+ proj="$TMP_ROOT/explicit-backend-project"; wt="$TMP_ROOT/explicit-backend-wt"; data="$TMP_ROOT/explicit-backend-data"
1065
+ id="explicitbackendz4"
1066
+ fm_git_worktree "$proj" "$wt" "fm/$id"
1067
+ fb=$(make_spawn_fakebin "$TMP_ROOT/explicit-backend-fake" "$wt")
1068
+ mkdir -p "$data/$id"; printf 'brief\n' > "$data/$id/brief.md"
1069
+ state="$TMP_ROOT/explicit-backend-state"; config="$TMP_ROOT/explicit-backend-config"
1070
+ mkdir -p "$state" "$config"
1071
+
1072
+ # HERDR_ENV=1 is present (as if firstmate itself were running under herdr),
1073
+ # but an explicit --backend tmux flag must still win outright.
1074
+ out=$(PATH="$fb:$PATH" FM_ROOT_OVERRIDE="$ROOT" \
1075
+ FM_STATE_OVERRIDE="$state" FM_DATA_OVERRIDE="$data" FM_CONFIG_OVERRIDE="$config" \
1076
+ FM_PROJECTS_OVERRIDE="$TMP_ROOT/unused-projects" FM_SPAWN_NO_GUARD=1 TMUX="fake,1,0" HERDR_ENV=1 \
1077
+ FM_TMUX_LOG="$TMP_ROOT/explicit-backend.log" \
1078
+ "$ROOT/bin/fm-spawn.sh" "$id" "$proj" claude --mode no-mistakes --yolo off --backend tmux 2>&1)
1079
+ expect_code 0 $? "explicit --backend tmux should spawn successfully even with HERDR_ENV=1 set"$'\n'"$out"
1080
+ assert_no_grep 'backend=' "$state/$id.meta" \
1081
+ "an explicit --backend tmux must win over an ambient HERDR_ENV=1 auto-detect marker"
1082
+ rm -rf "/tmp/fm-$id"
1083
+ pass "fm-spawn.sh: explicit --backend tmux wins over an ambient HERDR_ENV=1 auto-detect marker"
1084
+ }
1085
+
1086
+ test_spawn_autodetect_nesting_resolves_tmux_silently() {
1087
+ local proj wt data id state config out fb
1088
+ proj="$TMP_ROOT/nest-project"; wt="$TMP_ROOT/nest-wt"; data="$TMP_ROOT/nest-data"
1089
+ id="nestbackendz5"
1090
+ fm_git_worktree "$proj" "$wt" "fm/$id"
1091
+ fb=$(make_spawn_fakebin "$TMP_ROOT/nest-fake" "$wt")
1092
+ mkdir -p "$data/$id"; printf 'brief\n' > "$data/$id/brief.md"
1093
+ state="$TMP_ROOT/nest-state"; config="$TMP_ROOT/nest-config"
1094
+ mkdir -p "$state" "$config"
1095
+
1096
+ # No --backend, no FM_BACKEND, no config/backend: nothing is explicitly
1097
+ # configured, so auto-detect runs. $TMUX and HERDR_ENV=1 are both present
1098
+ # (tmux nested inside a herdr pane) - the full fm-spawn.sh pipeline, not just
1099
+ # fm_backend_name, must resolve this to tmux and stay completely silent about
1100
+ # it (today's default path, byte-identical).
1101
+ out=$(PATH="$fb:$PATH" FM_ROOT_OVERRIDE="$ROOT" \
1102
+ FM_STATE_OVERRIDE="$state" FM_DATA_OVERRIDE="$data" FM_CONFIG_OVERRIDE="$config" \
1103
+ FM_PROJECTS_OVERRIDE="$TMP_ROOT/unused-projects" FM_SPAWN_NO_GUARD=1 TMUX="fake,1,0" HERDR_ENV=1 \
1104
+ FM_TMUX_LOG="$TMP_ROOT/nest.log" \
1105
+ "$ROOT/bin/fm-spawn.sh" "$id" "$proj" claude --mode no-mistakes --yolo off 2>&1)
1106
+ expect_code 0 $? "fm-spawn.sh should auto-detect tmux and spawn successfully for nested tmux-in-herdr"$'\n'"$out"
1107
+ assert_no_grep 'backend=' "$state/$id.meta" \
1108
+ "auto-detected nested tmux-in-herdr must resolve to tmux (missing backend= means tmux)"
1109
+ case "$out" in
1110
+ *NOTICE*) fail "auto-detecting tmux (even nested inside herdr) must stay silent, no NOTICE expected"$'\n'"$out" ;;
1111
+ esac
1112
+ rm -rf "/tmp/fm-$id"
1113
+ pass "fm-spawn.sh: auto-detect resolves nested tmux-in-herdr to tmux and stays silent end to end"
1114
+ }
1115
+
1116
+ test_backend_name_precedence
1117
+ test_backend_detect_precedence
1118
+ test_backend_detect_cmux_fallback_bundle_id
1119
+ test_backend_detect_cmux_fallback_requires_darwin
1120
+ test_backend_detect_cmux_fallback_tmux_nested_false_positive
1121
+ test_backend_detect_cmux_fallback_ancestry_pid_match
1122
+ test_backend_detect_cmux_fallback_ancestry_comm_match
1123
+ test_backend_detect_cmux_fallback_ancestry_stops_at_launchd
1124
+ test_backend_name_cmux_fallback_notice
1125
+ test_backend_name_autodetect_notice
1126
+ test_backend_name_explicit_beats_detection
1127
+ test_backend_validate_refuses_unknown
1128
+ test_backend_source_shell_portable
1129
+ test_backend_validate_spawn_accepts_orca
1130
+ test_meta_get_and_backend_of_meta
1131
+ test_resolve_selector_three_forms
1132
+ test_backend_of_selector_matches_explicit_target_meta
1133
+ test_send_tmux_contract
1134
+ test_peek_conformance_old_vs_new
1135
+ test_spawn_symlinked_project_prefix_avoids_false_refusal
1136
+ test_teardown_conformance_old_vs_new
1137
+ test_spawn_refuses_unknown_backend_flag
1138
+ test_spawn_refuses_codex_app_backend_flag
1139
+ test_spawn_refuses_unknown_fm_backend_env
1140
+ test_spawn_default_backend_writes_no_meta_field
1141
+ test_spawn_explicit_backend_flag_beats_autodetect_herdr_env
1142
+ test_spawn_autodetect_nesting_resolves_tmux_silently