@zerwiz/ymir 0.1.3 → 0.1.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (850) hide show
  1. package/.agents/README.md +291 -0
  2. package/.agents/agents/bragi-marketer.md +111 -0
  3. package/.agents/agents/brokk.md +352 -0
  4. package/.agents/agents/forseti-reviewer.md +93 -0
  5. package/.agents/agents/frigg-consent.md +77 -0
  6. package/.agents/agents/groa-update.md +77 -0
  7. package/.agents/agents/hnoss-designer.md +69 -0
  8. package/.agents/agents/huginn-researcher.md +99 -0
  9. package/.agents/agents/jord-projects.md +78 -0
  10. package/.agents/agents/kvasir-scout.md +91 -0
  11. package/.agents/agents/mimir-planner.md +92 -0
  12. package/.agents/agents/muninn-stow.md +77 -0
  13. package/.agents/agents/saga-bearings.md +78 -0
  14. package/.agents/agents/sindri-developer.md +114 -0
  15. package/.agents/agents/snotra-documenter.md +105 -0
  16. package/.agents/agents/syn-recovery.md +78 -0
  17. package/.agents/agents/tyr-check.md +78 -0
  18. package/.agents/agents/urdh-hold.md +77 -0
  19. package/.agents/agents/volundr-smith.md +80 -0
  20. package/.agents/agents/vor-diagnostics.md +77 -0
  21. package/.agents/assets/agents/naming.md +50 -0
  22. package/.agents/assets/agents/registry.md +93 -0
  23. package/.agents/assets/agents/runtime.md +36 -0
  24. package/.agents/assets/agents/toon-tasks-cli.md +42 -0
  25. package/.agents/assets/templates/company_entity.template.md +37 -0
  26. package/.agents/backend/backends/cmux.sh +649 -0
  27. package/.agents/backend/backends/herdr-eventwait.py +157 -0
  28. package/.agents/backend/backends/herdr-workspace-move.py +114 -0
  29. package/.agents/backend/backends/herdr.sh +3386 -0
  30. package/.agents/backend/backends/orca.sh +290 -0
  31. package/.agents/backend/backends/tmux.sh +357 -0
  32. package/.agents/backend/backends/zellij.sh +700 -0
  33. package/.agents/backend/fm-afk-launch.sh +654 -0
  34. package/.agents/backend/fm-afk-return.sh +241 -0
  35. package/.agents/backend/fm-afk-start.sh +172 -0
  36. package/.agents/backend/fm-arm-command-policy.mjs +960 -0
  37. package/.agents/backend/fm-arm-pretool-check.sh +200 -0
  38. package/.agents/backend/fm-backend-hometag-lib.sh +52 -0
  39. package/.agents/backend/fm-backend.sh +981 -0
  40. package/.agents/backend/fm-backlog-handoff.sh +909 -0
  41. package/.agents/backend/fm-backlog-receive.sh +187 -0
  42. package/.agents/backend/fm-backlog-transition-lib.sh +779 -0
  43. package/.agents/backend/fm-bearings-board.sh +204 -0
  44. package/.agents/backend/fm-bearings-snapshot.sh +546 -0
  45. package/.agents/backend/fm-bootstrap.sh +1568 -0
  46. package/.agents/backend/fm-branch-outcome.sh +203 -0
  47. package/.agents/backend/fm-branch-prompt.sh +101 -0
  48. package/.agents/backend/fm-brief.sh +452 -0
  49. package/.agents/backend/fm-busy-event.sh +234 -0
  50. package/.agents/backend/fm-busy-lib.sh +986 -0
  51. package/.agents/backend/fm-captain-hold.sh +1001 -0
  52. package/.agents/backend/fm-cd-command-policy.mjs +153 -0
  53. package/.agents/backend/fm-cd-pretool-check.sh +190 -0
  54. package/.agents/backend/fm-check-lib.sh +74 -0
  55. package/.agents/backend/fm-check-register.sh +42 -0
  56. package/.agents/backend/fm-check-unregister.sh +52 -0
  57. package/.agents/backend/fm-classify-lib.sh +1770 -0
  58. package/.agents/backend/fm-claude-stop-autoarm.sh +334 -0
  59. package/.agents/backend/fm-composer-lib.sh +1416 -0
  60. package/.agents/backend/fm-config-inherit-lib.sh +1202 -0
  61. package/.agents/backend/fm-config-push.sh +254 -0
  62. package/.agents/backend/fm-control-lib.sh +251 -0
  63. package/.agents/backend/fm-control.sh +878 -0
  64. package/.agents/backend/fm-crew-state.sh +625 -0
  65. package/.agents/backend/fm-cursor-lib.sh +243 -0
  66. package/.agents/backend/fm-decision-hold.sh +232 -0
  67. package/.agents/backend/fm-doc-audience-check.sh +269 -0
  68. package/.agents/backend/fm-dod-lib.sh +67 -0
  69. package/.agents/backend/fm-ensure-agents-md.sh +253 -0
  70. package/.agents/backend/fm-extension-launch-barrier.mjs +129 -0
  71. package/.agents/backend/fm-extension.mjs +2577 -0
  72. package/.agents/backend/fm-extension.sh +16 -0
  73. package/.agents/backend/fm-ff-lib.sh +420 -0
  74. package/.agents/backend/fm-fleet-snapshot.sh +1463 -0
  75. package/.agents/backend/fm-fleet-sync.sh +458 -0
  76. package/.agents/backend/fm-fleet-view.sh +96 -0
  77. package/.agents/backend/fm-gate-refuse-lib.sh +102 -0
  78. package/.agents/backend/fm-guard.sh +242 -0
  79. package/.agents/backend/fm-harness.sh +195 -0
  80. package/.agents/backend/fm-herdr-ci-cleanup.sh +115 -0
  81. package/.agents/backend/fm-herdr-lab.sh +346 -0
  82. package/.agents/backend/fm-herdr-session-cleanup.sh +337 -0
  83. package/.agents/backend/fm-home-seed.sh +984 -0
  84. package/.agents/backend/fm-home-summary-refresh.sh +255 -0
  85. package/.agents/backend/fm-hook-host-lib.sh +36 -0
  86. package/.agents/backend/fm-inactive-reconcile.sh +531 -0
  87. package/.agents/backend/fm-inbox.sh +399 -0
  88. package/.agents/backend/fm-install-actionlint.sh +84 -0
  89. package/.agents/backend/fm-install-herdr.sh +96 -0
  90. package/.agents/backend/fm-install-shellcheck.sh +84 -0
  91. package/.agents/backend/fm-install-treehouse.sh +95 -0
  92. package/.agents/backend/fm-kimi-turnend-hook.sh +276 -0
  93. package/.agents/backend/fm-lease-lib.sh +218 -0
  94. package/.agents/backend/fm-lease.sh +189 -0
  95. package/.agents/backend/fm-line-cap-lib.sh +51 -0
  96. package/.agents/backend/fm-lint-workflows.sh +137 -0
  97. package/.agents/backend/fm-lint.sh +591 -0
  98. package/.agents/backend/fm-lock-lib.sh +104 -0
  99. package/.agents/backend/fm-lock.sh +107 -0
  100. package/.agents/backend/fm-marker-lib.sh +12 -0
  101. package/.agents/backend/fm-merge-local.sh +74 -0
  102. package/.agents/backend/fm-merge-outcome-lib.sh +138 -0
  103. package/.agents/backend/fm-nm-run-lib.sh +124 -0
  104. package/.agents/backend/fm-on.sh +125 -0
  105. package/.agents/backend/fm-operational-input.sh +253 -0
  106. package/.agents/backend/fm-peek.sh +46 -0
  107. package/.agents/backend/fm-pending-reply-lib.sh +1409 -0
  108. package/.agents/backend/fm-pr-check.sh +135 -0
  109. package/.agents/backend/fm-pr-lib.sh +1016 -0
  110. package/.agents/backend/fm-pr-merge.sh +707 -0
  111. package/.agents/backend/fm-pr-poll.sh +110 -0
  112. package/.agents/backend/fm-primary-scope-lib.sh +33 -0
  113. package/.agents/backend/fm-procevent-extension-capture.pl +259 -0
  114. package/.agents/backend/fm-procevent-lavish.sh +628 -0
  115. package/.agents/backend/fm-procevent-lib.sh +892 -0
  116. package/.agents/backend/fm-procevent-quota.sh +290 -0
  117. package/.agents/backend/fm-procevent-remote-reply.sh +577 -0
  118. package/.agents/backend/fm-procevent-when.sh +504 -0
  119. package/.agents/backend/fm-procevent.sh +1466 -0
  120. package/.agents/backend/fm-project-mode.sh +92 -0
  121. package/.agents/backend/fm-project-origin-lib.sh +180 -0
  122. package/.agents/backend/fm-promote.sh +282 -0
  123. package/.agents/backend/fm-public-followup-emit.sh +260 -0
  124. package/.agents/backend/fm-public-followup-lib.sh +398 -0
  125. package/.agents/backend/fm-public-followup.sh +1330 -0
  126. package/.agents/backend/fm-push-transition-lib.sh +168 -0
  127. package/.agents/backend/fm-quota-axi-lib.sh +93 -0
  128. package/.agents/backend/fm-quota-choose.sh +384 -0
  129. package/.agents/backend/fm-remote-delta-read.sh +189 -0
  130. package/.agents/backend/fm-remote-doctor.sh +800 -0
  131. package/.agents/backend/fm-remote-entrypoint.sh +193 -0
  132. package/.agents/backend/fm-remote-file.sh +234 -0
  133. package/.agents/backend/fm-remote-home-provision.sh +259 -0
  134. package/.agents/backend/fm-remote-home-seed.sh +259 -0
  135. package/.agents/backend/fm-remote-inherit-push.sh +90 -0
  136. package/.agents/backend/fm-remote-inherit.sh +179 -0
  137. package/.agents/backend/fm-remote-job-lib.sh +1241 -0
  138. package/.agents/backend/fm-remote-job-reap-orphans.sh +137 -0
  139. package/.agents/backend/fm-remote-job-worker.sh +1124 -0
  140. package/.agents/backend/fm-remote-readiness-lib.sh +44 -0
  141. package/.agents/backend/fm-remote-secondmate-control.sh +340 -0
  142. package/.agents/backend/fm-review-diff.sh +158 -0
  143. package/.agents/backend/fm-secondmate-charter-lib.sh +44 -0
  144. package/.agents/backend/fm-secondmate-nudge-lib.sh +68 -0
  145. package/.agents/backend/fm-secondmate-parent-lib.sh +70 -0
  146. package/.agents/backend/fm-secondmate-reconcile.sh +371 -0
  147. package/.agents/backend/fm-secondmate-registry-lib.sh +311 -0
  148. package/.agents/backend/fm-secondmate-report.sh +88 -0
  149. package/.agents/backend/fm-send.sh +1059 -0
  150. package/.agents/backend/fm-session-lock-lib.sh +176 -0
  151. package/.agents/backend/fm-session-start.sh +970 -0
  152. package/.agents/backend/fm-sessionstart-cursor.sh +40 -0
  153. package/.agents/backend/fm-sessionstart-nudge.sh +45 -0
  154. package/.agents/backend/fm-sessionstart-run.sh +145 -0
  155. package/.agents/backend/fm-spawn.sh +3153 -0
  156. package/.agents/backend/fm-startup-memory-budget-lib.sh +224 -0
  157. package/.agents/backend/fm-startup-memory-budget.sh +94 -0
  158. package/.agents/backend/fm-startup-network.sh +642 -0
  159. package/.agents/backend/fm-stow-cascade.sh +251 -0
  160. package/.agents/backend/fm-subagent-pretool-check.sh +207 -0
  161. package/.agents/backend/fm-supervise-daemon.sh +1732 -0
  162. package/.agents/backend/fm-supervision-instructions.sh +215 -0
  163. package/.agents/backend/fm-supervision-lib.sh +90 -0
  164. package/.agents/backend/fm-supervisor-target-lib.sh +78 -0
  165. package/.agents/backend/fm-tangle-lib.sh +53 -0
  166. package/.agents/backend/fm-task-inbox-lib.sh +402 -0
  167. package/.agents/backend/fm-tasks-axi-lib.sh +123 -0
  168. package/.agents/backend/fm-teardown.sh +2922 -0
  169. package/.agents/backend/fm-test-isolation-proof.sh +588 -0
  170. package/.agents/backend/fm-test-run.sh +2191 -0
  171. package/.agents/backend/fm-timeout-lib.sh +141 -0
  172. package/.agents/backend/fm-timing-lib.sh +179 -0
  173. package/.agents/backend/fm-tmux-lib.sh +291 -0
  174. package/.agents/backend/fm-tool-update-check.sh +898 -0
  175. package/.agents/backend/fm-trace-context-lib.sh +227 -0
  176. package/.agents/backend/fm-transition-lib.sh +103 -0
  177. package/.agents/backend/fm-turnend-guard-cursor.sh +391 -0
  178. package/.agents/backend/fm-turnend-guard-grok.sh +90 -0
  179. package/.agents/backend/fm-turnend-guard.sh +432 -0
  180. package/.agents/backend/fm-update.sh +110 -0
  181. package/.agents/backend/fm-vendor-auth-probe.sh +190 -0
  182. package/.agents/backend/fm-voice-client.py +1373 -0
  183. package/.agents/backend/fm-voice-relay.py +1256 -0
  184. package/.agents/backend/fm-wake-drain.sh +592 -0
  185. package/.agents/backend/fm-wake-grant.sh +127 -0
  186. package/.agents/backend/fm-wake-lib.sh +1849 -0
  187. package/.agents/backend/fm-watch-arm.sh +602 -0
  188. package/.agents/backend/fm-watch-checkpoint.sh +109 -0
  189. package/.agents/backend/fm-watch.sh +1962 -0
  190. package/.agents/backend/fm-x-dismiss.sh +116 -0
  191. package/.agents/backend/fm-x-followup.sh +286 -0
  192. package/.agents/backend/fm-x-lib.sh +1000 -0
  193. package/.agents/backend/fm-x-link.sh +234 -0
  194. package/.agents/backend/fm-x-poll.sh +185 -0
  195. package/.agents/backend/fm-x-reply.sh +385 -0
  196. package/.agents/backend/fm_voice_frame.py +166 -0
  197. package/.agents/backend/fm_voice_records.py +574 -0
  198. package/.agents/backend/model-bridge.py +290 -0
  199. package/.agents/backend/opencode-go-bridge.py +30 -0
  200. package/.agents/bus/README.md +12 -0
  201. package/.agents/bus/messages.json +1 -0
  202. package/.agents/config/agents.machine.example.yaml +16 -0
  203. package/.agents/config/agents.yaml +50 -0
  204. package/.agents/config/agents.yaml.example +72 -0
  205. package/.agents/config/crew-dispatch.json +22 -0
  206. package/.agents/config/cron.yaml +9 -0
  207. package/.agents/config/eindri-dispatch.json +67 -0
  208. package/.agents/config/eindri-harness +1 -0
  209. package/.agents/config/startup-memory-budget +1 -0
  210. package/.agents/config/tailscale-sync.yaml +27 -0
  211. package/.agents/filebrowser/.filebrowser.json +6 -0
  212. package/.agents/filebrowser/README.md +11 -0
  213. package/.agents/gateway/README.md +12 -0
  214. package/.agents/github/README.md +15 -0
  215. package/.agents/github/webhooks/.gitkeep +0 -0
  216. package/.agents/github/workflows/.gitkeep +0 -0
  217. package/.agents/harness/opencode/plugins/lib/rodd-operational-input.js +37 -0
  218. package/.agents/harness/opencode/plugins/package.json +4 -0
  219. package/.agents/harness/opencode/plugins/saga-sessionstart.js +77 -0
  220. package/.agents/harness/opencode/plugins/syn-cd-check.js +61 -0
  221. package/.agents/harness/opencode/plugins/syn-pretool-check.js +68 -0
  222. package/.agents/harness/opencode/plugins/syn-turnend-guard.js +105 -0
  223. package/.agents/harness/opencode/plugins/syn-watch-arm.js +521 -0
  224. package/.agents/memory/README.md +46 -0
  225. package/.agents/memory/kaia.engram +0 -0
  226. package/.agents/memory/kaia.engram-shm +0 -0
  227. package/.agents/memory/kaia.engram-wal +0 -0
  228. package/.agents/memory/runes_audit.md +5 -0
  229. package/.agents/memory/well/episodes.jsonl +367 -0
  230. package/.agents/memory/well/workspace.jsonl +27 -0
  231. package/.agents/migrations/0001-hodd-layout.sh +38 -0
  232. package/.agents/migrations/0002-a2a-mcp.sh +27 -0
  233. package/.agents/migrations/0003-private-data-separation.sh +111 -0
  234. package/.agents/migrations/0004-hoard-and-realms.sh +104 -0
  235. package/.agents/sandbox/Dockerfile.utgard +19 -0
  236. package/.agents/sandbox/README.md +14 -0
  237. package/.agents/sandbox/utgard.config.json +14 -0
  238. package/.agents/skills/README.md +59 -0
  239. package/.agents/skills/bragi-marketing/README.md +3 -0
  240. package/.agents/skills/bragi-marketing/SKILL.md +82 -0
  241. package/.agents/skills/eindri-homes/README.md +3 -0
  242. package/.agents/skills/eindri-homes/SKILL.md +291 -0
  243. package/.agents/skills/eindri-homes/assets/control-plane.md +122 -0
  244. package/.agents/skills/eindri-homes/assets/dispatch.md +80 -0
  245. package/.agents/skills/frigg-consent/README.md +3 -0
  246. package/.agents/skills/frigg-consent/SKILL.md +56 -0
  247. package/.agents/skills/galdr-ymirsystem/README.md +3 -0
  248. package/.agents/skills/galdr-ymirsystem/SKILL.md +158 -0
  249. package/.agents/skills/galdr-ymirsystem/assets/README.md +74 -0
  250. package/.agents/skills/galdr-ymirsystem/assets/brokk-distro-runtime.md +390 -0
  251. package/.agents/skills/galdr-ymirsystem/assets/build-method.md +113 -0
  252. package/.agents/skills/galdr-ymirsystem/assets/build-tool-categories.md +178 -0
  253. package/.agents/skills/galdr-ymirsystem/assets/eindri-orchestration.md +518 -0
  254. package/.agents/skills/galdr-ymirsystem/assets/eindri-profiles.md +136 -0
  255. package/.agents/skills/galdr-ymirsystem/assets/harness-integration/README.md +673 -0
  256. package/.agents/skills/galdr-ymirsystem/assets/harness-integration/claude-code.md +197 -0
  257. package/.agents/skills/galdr-ymirsystem/assets/harness-integration/codex.md +191 -0
  258. package/.agents/skills/galdr-ymirsystem/assets/harness-integration/cursor.md +193 -0
  259. package/.agents/skills/galdr-ymirsystem/assets/harness-integration/opencode.md +262 -0
  260. package/.agents/skills/galdr-ymirsystem/assets/harness-integration/pi.md +295 -0
  261. package/.agents/skills/galdr-ymirsystem/assets/hlidskjalf-ui.md +496 -0
  262. package/.agents/skills/galdr-ymirsystem/assets/installation.md +571 -0
  263. package/.agents/skills/galdr-ymirsystem/assets/local-models/HOST-DATA.example.md +72 -0
  264. package/.agents/skills/galdr-ymirsystem/assets/local-models.md +274 -0
  265. package/.agents/skills/galdr-ymirsystem/assets/memory-well.md +161 -0
  266. package/.agents/skills/galdr-ymirsystem/assets/nornir-jobs.md +377 -0
  267. package/.agents/skills/galdr-ymirsystem/assets/norse-naming.md +325 -0
  268. package/.agents/skills/galdr-ymirsystem/assets/odrerir-hall.md +129 -0
  269. package/.agents/skills/galdr-ymirsystem/assets/pi-boot/einherjar-spawn.schema.json +81 -0
  270. package/.agents/skills/galdr-ymirsystem/assets/pi-boot/herdr-profile.toml +75 -0
  271. package/.agents/skills/galdr-ymirsystem/assets/pi-boot/pi-profile.yml +96 -0
  272. package/.agents/skills/galdr-ymirsystem/assets/pi-boot/supervision-tree.yml +163 -0
  273. package/.agents/skills/galdr-ymirsystem/assets/pi-boot-guide.md +58 -0
  274. package/.agents/skills/galdr-ymirsystem/assets/porting-upstream-to-norse.md +301 -0
  275. package/.agents/skills/galdr-ymirsystem/assets/principles.md +385 -0
  276. package/.agents/skills/galdr-ymirsystem/assets/reference-adoption.md +99 -0
  277. package/.agents/skills/galdr-ymirsystem/assets/registry.md +379 -0
  278. package/.agents/skills/galdr-ymirsystem/assets/runtime-compliance.md +463 -0
  279. package/.agents/skills/galdr-ymirsystem/assets/runtime-components.md +553 -0
  280. package/.agents/skills/galdr-ymirsystem/assets/smidja.md +175 -0
  281. package/.agents/skills/galdr-ymirsystem/schemas/toon-schemas.md +190 -0
  282. package/.agents/skills/galdr-ymirsystem/scripts/README.md +45 -0
  283. package/.agents/skills/galdr-ymirsystem/scripts/bench-one.sh +248 -0
  284. package/.agents/skills/galdr-ymirsystem/scripts/compliance-check.sh +360 -0
  285. package/.agents/skills/galdr-ymirsystem/scripts/gpu-sample.sh +43 -0
  286. package/.agents/skills/galdr-ymirsystem/scripts/stop-all.sh +34 -0
  287. package/.agents/skills/galdr-ymirsystem/scripts/toon-check.py +188 -0
  288. package/.agents/skills/gjallarhorn-relay/README.md +3 -0
  289. package/.agents/skills/gjallarhorn-relay/SKILL.md +283 -0
  290. package/.agents/skills/groa-update/SKILL.md +70 -0
  291. package/.agents/skills/hamr-adapters/README.md +3 -0
  292. package/.agents/skills/hamr-adapters/SKILL.md +123 -0
  293. package/.agents/skills/hamr-adapters/references/common/control-and-recovery.md +37 -0
  294. package/.agents/skills/hamr-adapters/references/common/dispatch.md +32 -0
  295. package/.agents/skills/hamr-adapters/references/common/model-and-effort.md +42 -0
  296. package/.agents/skills/hamr-adapters/references/common/primary-hooks.md +40 -0
  297. package/.agents/skills/hamr-adapters/references/harness/claude.md +55 -0
  298. package/.agents/skills/hamr-adapters/references/harness/codex.md +43 -0
  299. package/.agents/skills/hamr-adapters/references/harness/cursor.md +75 -0
  300. package/.agents/skills/hamr-adapters/references/harness/grok.md +69 -0
  301. package/.agents/skills/hamr-adapters/references/harness/kimi.md +51 -0
  302. package/.agents/skills/hamr-adapters/references/harness/muse.md +70 -0
  303. package/.agents/skills/hamr-adapters/references/harness/opencode.md +42 -0
  304. package/.agents/skills/hamr-adapters/references/harness/pi.md +56 -0
  305. package/.agents/skills/herdr-panes/README.md +3 -0
  306. package/.agents/skills/herdr-panes/SKILL.md +92 -0
  307. package/.agents/skills/herdr-panes/assets/herdr-backend.md +346 -0
  308. package/.agents/skills/herdr-panes/assets/tmux-backend.md +115 -0
  309. package/.agents/skills/hnoss-design/DESIGN.md +19 -0
  310. package/.agents/skills/hnoss-design/README.md +3 -0
  311. package/.agents/skills/hnoss-design/SKILL.md +83 -0
  312. package/.agents/skills/hvild-afk/README.md +3 -0
  313. package/.agents/skills/hvild-afk/SKILL.md +241 -0
  314. package/.agents/skills/jord-projects/README.md +3 -0
  315. package/.agents/skills/jord-projects/SKILL.md +92 -0
  316. package/.agents/skills/lifecycle/SKILL.md +56 -0
  317. package/.agents/skills/lifecycle/smoke_test.sh +52 -0
  318. package/.agents/skills/lifecycle/start.sh +21 -0
  319. package/.agents/skills/lifecycle/status.sh +35 -0
  320. package/.agents/skills/lifecycle/stop.sh +16 -0
  321. package/.agents/skills/muninn-stow/README.md +3 -0
  322. package/.agents/skills/muninn-stow/SKILL.md +309 -0
  323. package/.agents/skills/no-mistakes/SKILL.md +379 -0
  324. package/.agents/skills/nornir-schedule/README.md +3 -0
  325. package/.agents/skills/nornir-schedule/SKILL.md +25 -0
  326. package/.agents/skills/nornir-schedule/assets/events.md +137 -0
  327. package/.agents/skills/nornir-schedule/assets/quota.md +131 -0
  328. package/.agents/skills/nsr-compliance/README.md +3 -0
  329. package/.agents/skills/nsr-compliance/SKILL.md +29 -0
  330. package/.agents/skills/nsr-compliance/assets/nsr/README.md +40 -0
  331. package/.agents/skills/nsr-compliance/assets/nsr/assets/RULES/README.md +15 -0
  332. package/.agents/skills/nsr-compliance/assets/nsr/assets/RULES/git-workflow.md +18 -0
  333. package/.agents/skills/nsr-compliance/assets/nsr/assets/RULES/security.md +22 -0
  334. package/.agents/skills/nsr-compliance/assets/nsr/assets/RULES/tech-stack.md +8 -0
  335. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/SKILL.md +71 -0
  336. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/config/core_four.yaml +16 -0
  337. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/gates/check_danger.sh +102 -0
  338. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/gates/check_wiring.sh +69 -0
  339. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/gates/validate_code.sh +29 -0
  340. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/gates/verify_docs.py +66 -0
  341. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/generate.sh +109 -0
  342. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/harness/envelopes/result_envelope.json +14 -0
  343. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/harness/envelopes/task_envelope.json +20 -0
  344. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/harness/runner.py +303 -0
  345. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/telemetry/logger.py +112 -0
  346. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/templates/compliance/README.md +18 -0
  347. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/templates/compliance/config/README.md +10 -0
  348. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/templates/compliance/gates/README.md +12 -0
  349. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/templates/compliance/gates/check_env.sh +23 -0
  350. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/templates/compliance/gates/check_paths.sh +20 -0
  351. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/templates/compliance/gates/check_platform.sh +23 -0
  352. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/templates/compliance/harness/README.md +11 -0
  353. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/templates/compliance/harness/envelopes/README.md +10 -0
  354. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/templates/compliance/installer/README.md +10 -0
  355. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/templates/compliance/installer/install.sh +125 -0
  356. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/templates/compliance/telemetry/README.md +11 -0
  357. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/README.md +20 -0
  358. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/features/README.md +36 -0
  359. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/features/SKILL-template.md +14 -0
  360. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/git_ops/README.md +25 -0
  361. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/git_ops/SKILL.md +13 -0
  362. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/git_ops/create_branch.sh +6 -0
  363. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/git_ops/safe_commit.sh +9 -0
  364. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/git_ops/sync_upstream.sh +6 -0
  365. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/lifecycle/README.md +27 -0
  366. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/lifecycle/SKILL.md +15 -0
  367. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/lifecycle/smoke_test.sh +12 -0
  368. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/lifecycle/start.sh +19 -0
  369. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/lifecycle/status.sh +12 -0
  370. package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/lifecycle/stop.sh +15 -0
  371. package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/README.md +18 -0
  372. package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/config/README.md +10 -0
  373. package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/config/core_four.yaml +13 -0
  374. package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/gates/README.md +12 -0
  375. package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/gates/check_danger.sh +102 -0
  376. package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/gates/check_env.sh +23 -0
  377. package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/gates/check_paths.sh +20 -0
  378. package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/gates/check_platform.sh +23 -0
  379. package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/gates/check_wiring.sh +69 -0
  380. package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/harness/README.md +11 -0
  381. package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/harness/envelopes/README.md +10 -0
  382. package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/harness/envelopes/result_envelope.json +14 -0
  383. package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/harness/envelopes/task_envelope.json +20 -0
  384. package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/harness/runner.py +303 -0
  385. package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/installer/README.md +10 -0
  386. package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/installer/install.sh +125 -0
  387. package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/telemetry/README.md +11 -0
  388. package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/telemetry/logger.py +112 -0
  389. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/BEST_PRACTICES/README.md +14 -0
  390. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/BEST_PRACTICES/api.md +17 -0
  391. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/BEST_PRACTICES/compliance-wiring.md +155 -0
  392. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/BEST_PRACTICES/frontend.md +15 -0
  393. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/CI_CD/README.md +11 -0
  394. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/CI_CD/deployment/README.md +23 -0
  395. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/CI_CD/deployment/clients/README.md +16 -0
  396. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/CI_CD/deployment/clients/template-README.md +11 -0
  397. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/CI_CD/deployment/deploy.sh +49 -0
  398. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/CI_CD/deployment/envs/README.md +12 -0
  399. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/CI_CD/deployment/envs/development.env.example +17 -0
  400. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/CI_CD/deployment/envs/production.env.example +17 -0
  401. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/CI_CD/deployment/envs/staging.env.example +17 -0
  402. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/CI_CD/deployment/tenants/README.md +11 -0
  403. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/CI_CD/deployment/tenants/template.env.example +8 -0
  404. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/DEVELOPER_SETUP/README.md +26 -0
  405. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/DEVELOPER_SETUP/agent.md +7 -0
  406. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/DEVELOPER_SETUP/developers/dev-1.md +20 -0
  407. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/DEVELOPER_SETUP/linux.md +9 -0
  408. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/DEVELOPER_SETUP/macos.md +10 -0
  409. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/DEVELOPER_SETUP/shared.md +19 -0
  410. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/DEVELOPER_SETUP/windows.md +14 -0
  411. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/HOSTING/README.md +25 -0
  412. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/HOSTING/_template.md +41 -0
  413. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/HOSTING/hosting-1.md +41 -0
  414. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/README.md +21 -0
  415. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/RUNBOOK/README.md +15 -0
  416. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/RUNBOOK/deployment.md +19 -0
  417. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/RUNBOOK/disaster-recovery.md +15 -0
  418. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/RUNBOOK/ops-tasks.md +14 -0
  419. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/features/README.md +16 -0
  420. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/project-skills.md +146 -0
  421. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/research/README.md +11 -0
  422. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/research/software-compliance-research.md +269 -0
  423. package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/working-with-agents.md +260 -0
  424. package/.agents/skills/nsr-compliance/assets/nsr/assets/root/AGENTS.md +38 -0
  425. package/.agents/skills/nsr-compliance/assets/nsr/assets/root/ARCHITECTURE.md +36 -0
  426. package/.agents/skills/nsr-compliance/assets/nsr/assets/root/BEST_PRACTICES.md +27 -0
  427. package/.agents/skills/nsr-compliance/assets/nsr/assets/root/CI_CD.md +23 -0
  428. package/.agents/skills/nsr-compliance/assets/nsr/assets/root/FEATURES.md +15 -0
  429. package/.agents/skills/nsr-compliance/assets/nsr/assets/root/README.md +22 -0
  430. package/.agents/skills/nsr-compliance/assets/nsr/assets/root/RULES.md +37 -0
  431. package/.agents/skills/nsr-compliance/assets/nsr/assets/root/STRUCTURE.md +45 -0
  432. package/.agents/skills/nsr-compliance/assets/nsr/assets/root/TECH_STACK.md +28 -0
  433. package/.agents/skills/nsr-compliance/assets/nsr/scaffold-spec.md +63 -0
  434. package/.agents/skills/nsr-compliance/assets/nsr/scripts/scaffold.sh +103 -0
  435. package/.agents/skills/nsr-compliance/assets/nsr/scripts/validate.sh +52 -0
  436. package/.agents/skills/nsr-compliance/assets/nsrcompliance/SKILL.md +71 -0
  437. package/.agents/skills/nsr-compliance/assets/nsrcompliance/config/core_four.yaml +16 -0
  438. package/.agents/skills/nsr-compliance/assets/nsrcompliance/gates/check_danger.sh +102 -0
  439. package/.agents/skills/nsr-compliance/assets/nsrcompliance/gates/check_wiring.sh +69 -0
  440. package/.agents/skills/nsr-compliance/assets/nsrcompliance/gates/validate_code.sh +29 -0
  441. package/.agents/skills/nsr-compliance/assets/nsrcompliance/gates/verify_docs.py +66 -0
  442. package/.agents/skills/nsr-compliance/assets/nsrcompliance/generate.sh +109 -0
  443. package/.agents/skills/nsr-compliance/assets/nsrcompliance/harness/envelopes/result_envelope.json +14 -0
  444. package/.agents/skills/nsr-compliance/assets/nsrcompliance/harness/envelopes/task_envelope.json +20 -0
  445. package/.agents/skills/nsr-compliance/assets/nsrcompliance/harness/runner.py +303 -0
  446. package/.agents/skills/nsr-compliance/assets/nsrcompliance/telemetry/logger.py +112 -0
  447. package/.agents/skills/nsr-compliance/assets/nsrcompliance/templates/compliance/README.md +18 -0
  448. package/.agents/skills/nsr-compliance/assets/nsrcompliance/templates/compliance/config/README.md +10 -0
  449. package/.agents/skills/nsr-compliance/assets/nsrcompliance/templates/compliance/gates/README.md +12 -0
  450. package/.agents/skills/nsr-compliance/assets/nsrcompliance/templates/compliance/gates/check_env.sh +23 -0
  451. package/.agents/skills/nsr-compliance/assets/nsrcompliance/templates/compliance/gates/check_paths.sh +20 -0
  452. package/.agents/skills/nsr-compliance/assets/nsrcompliance/templates/compliance/gates/check_platform.sh +23 -0
  453. package/.agents/skills/nsr-compliance/assets/nsrcompliance/templates/compliance/harness/README.md +11 -0
  454. package/.agents/skills/nsr-compliance/assets/nsrcompliance/templates/compliance/harness/envelopes/README.md +10 -0
  455. package/.agents/skills/nsr-compliance/assets/nsrcompliance/templates/compliance/installer/README.md +10 -0
  456. package/.agents/skills/nsr-compliance/assets/nsrcompliance/templates/compliance/installer/install.sh +125 -0
  457. package/.agents/skills/nsr-compliance/assets/nsrcompliance/templates/compliance/telemetry/README.md +11 -0
  458. package/.agents/skills/pr-ops/README.md +3 -0
  459. package/.agents/skills/pr-ops/SKILL.md +109 -0
  460. package/.agents/skills/ratatoskr-a2a/README.md +3 -0
  461. package/.agents/skills/ratatoskr-a2a/SKILL.md +148 -0
  462. package/.agents/skills/rules-check-drift/SKILL.md +81 -0
  463. package/.agents/skills/saga-bearings/README.md +3 -0
  464. package/.agents/skills/saga-bearings/SKILL.md +26 -0
  465. package/.agents/skills/saga-bearings/assets/bearings.md +161 -0
  466. package/.agents/skills/saga-bearings/assets/board-template.html +731 -0
  467. package/.agents/skills/saga-bearings/assets/recap.md +56 -0
  468. package/.agents/skills/syn-recovery/README.md +3 -0
  469. package/.agents/skills/syn-recovery/SKILL.md +54 -0
  470. package/.agents/skills/tyr-check/README.md +3 -0
  471. package/.agents/skills/tyr-check/SKILL.md +218 -0
  472. package/.agents/skills/urdh-hold/README.md +3 -0
  473. package/.agents/skills/urdh-hold/SKILL.md +25 -0
  474. package/.agents/skills/urdh-hold/assets/decisions.md +15 -0
  475. package/.agents/skills/urdh-hold/assets/hold.md +54 -0
  476. package/.agents/skills/vor-diagnostics/README.md +3 -0
  477. package/.agents/skills/vor-diagnostics/SKILL.md +68 -0
  478. package/.agents/skills/ymir-host/README.md +3 -0
  479. package/.agents/skills/ymir-host/SKILL.md +34 -0
  480. package/.agents/skills/ymir-host/assets/desktop.md +36 -0
  481. package/.agents/skills/ymir-host/assets/omarchy.md +297 -0
  482. package/.agents/skills/ymir-host/assets/runbooks.md +15 -0
  483. package/.agents/skills/ymir-host/assets/thjazi.md +220 -0
  484. package/.agents/skills/ymir-host/assets/update.md +64 -0
  485. package/.agents/tests/assets/board-render-harness.mjs +117 -0
  486. package/.agents/tests/cmux-test-safety.sh +51 -0
  487. package/.agents/tests/fixtures.sh +291 -0
  488. package/.agents/tests/fm-afk-inject-e2e.test.sh +428 -0
  489. package/.agents/tests/fm-afk-inject-herdr-e2e.test.sh +536 -0
  490. package/.agents/tests/fm-afk-launch.test.sh +961 -0
  491. package/.agents/tests/fm-afk-pi-herdr-return-e2e.test.sh +297 -0
  492. package/.agents/tests/fm-afk-return.test.sh +283 -0
  493. package/.agents/tests/fm-arm-pretool-check.test.sh +482 -0
  494. package/.agents/tests/fm-ask-user-authority.test.sh +42 -0
  495. package/.agents/tests/fm-backend-autodetect-smoke.test.sh +176 -0
  496. package/.agents/tests/fm-backend-cmux-smoke.test.sh +188 -0
  497. package/.agents/tests/fm-backend-cmux.test.sh +1166 -0
  498. package/.agents/tests/fm-backend-herdr-eventwait-smoke.test.sh +136 -0
  499. package/.agents/tests/fm-backend-herdr-eventwait.test.py +106 -0
  500. package/.agents/tests/fm-backend-herdr-focus-flash-e2e.test.sh +409 -0
  501. package/.agents/tests/fm-backend-herdr-launcher-workspace-e2e.test.sh +437 -0
  502. package/.agents/tests/fm-backend-herdr-presentation-e2e.test.sh +1424 -0
  503. package/.agents/tests/fm-backend-herdr-prune-safety-e2e.test.sh +180 -0
  504. package/.agents/tests/fm-backend-herdr-respawn-idem-e2e.test.sh +182 -0
  505. package/.agents/tests/fm-backend-herdr-smoke.test.sh +355 -0
  506. package/.agents/tests/fm-backend-herdr-workspace-per-home-e2e.test.sh +251 -0
  507. package/.agents/tests/fm-backend-herdr.test.sh +4606 -0
  508. package/.agents/tests/fm-backend-orca.test.sh +1357 -0
  509. package/.agents/tests/fm-backend-tmux-smoke.test.sh +173 -0
  510. package/.agents/tests/fm-backend-zellij-smoke.test.sh +205 -0
  511. package/.agents/tests/fm-backend-zellij.test.sh +1360 -0
  512. package/.agents/tests/fm-backend.test.sh +1142 -0
  513. package/.agents/tests/fm-backlog-atomicity.test.sh +2302 -0
  514. package/.agents/tests/fm-backlog-handoff.test.sh +1360 -0
  515. package/.agents/tests/fm-bearings-board-render.test.sh +135 -0
  516. package/.agents/tests/fm-bearings-board.test.sh +414 -0
  517. package/.agents/tests/fm-bearings-snapshot.test.sh +1990 -0
  518. package/.agents/tests/fm-bootstrap-network-parallel.test.sh +316 -0
  519. package/.agents/tests/fm-bootstrap.test.sh +1178 -0
  520. package/.agents/tests/fm-branch-supervision.test.sh +552 -0
  521. package/.agents/tests/fm-brief.test.sh +785 -0
  522. package/.agents/tests/fm-busy-adapter-wiring.test.sh +324 -0
  523. package/.agents/tests/fm-busy-state.test.sh +465 -0
  524. package/.agents/tests/fm-calm-pi-extension.test.sh +3979 -0
  525. package/.agents/tests/fm-captain-hold-lifecycle.test.sh +1187 -0
  526. package/.agents/tests/fm-cd-pretool-check.test.sh +400 -0
  527. package/.agents/tests/fm-check-unregister.test.sh +198 -0
  528. package/.agents/tests/fm-classify-corr-token.test.sh +543 -0
  529. package/.agents/tests/fm-classify-decision-key.test.sh +340 -0
  530. package/.agents/tests/fm-claude-stop-autoarm-live-e2e.test.sh +164 -0
  531. package/.agents/tests/fm-claude-stop-autoarm.test.sh +1190 -0
  532. package/.agents/tests/fm-cmux-claude-composer-live-e2e.test.sh +110 -0
  533. package/.agents/tests/fm-codex-continuity-live-e2e.test.sh +55 -0
  534. package/.agents/tests/fm-composer-ghost.test.sh +716 -0
  535. package/.agents/tests/fm-composer-lib.test.sh +666 -0
  536. package/.agents/tests/fm-composer-matrix-live-e2e.test.sh +220 -0
  537. package/.agents/tests/fm-control-herdr-smoke.test.sh +149 -0
  538. package/.agents/tests/fm-control-relaunch.test.sh +1530 -0
  539. package/.agents/tests/fm-control.test.sh +910 -0
  540. package/.agents/tests/fm-crew-state.test.sh +1610 -0
  541. package/.agents/tests/fm-cursor-harness.test.sh +404 -0
  542. package/.agents/tests/fm-cursor-primary-live-e2e.test.sh +217 -0
  543. package/.agents/tests/fm-cursor-primary.test.sh +708 -0
  544. package/.agents/tests/fm-daemon.test.sh +2740 -0
  545. package/.agents/tests/fm-documentation-audiences.test.sh +141 -0
  546. package/.agents/tests/fm-ensure-agents-md.test.sh +371 -0
  547. package/.agents/tests/fm-extension-binding.test.sh +2187 -0
  548. package/.agents/tests/fm-fleet-snapshot-view.test.sh +816 -0
  549. package/.agents/tests/fm-fleet-sync.test.sh +721 -0
  550. package/.agents/tests/fm-gate-refuse.test.sh +356 -0
  551. package/.agents/tests/fm-gitignore-config.test.sh +89 -0
  552. package/.agents/tests/fm-gotmp.test.sh +230 -0
  553. package/.agents/tests/fm-grok-continuity-live-e2e.test.sh +112 -0
  554. package/.agents/tests/fm-grok-harness.test.sh +117 -0
  555. package/.agents/tests/fm-grok-stop-live-e2e.test.sh +219 -0
  556. package/.agents/tests/fm-guard-stale-banner.test.sh +709 -0
  557. package/.agents/tests/fm-harness-adapter-instructions-live-e2e.test.sh +134 -0
  558. package/.agents/tests/fm-harness-adapter-references.test.sh +30 -0
  559. package/.agents/tests/fm-harness-liveness-drift-live-e2e.test.sh +151 -0
  560. package/.agents/tests/fm-herdr-lab.test.sh +243 -0
  561. package/.agents/tests/fm-herdr-session-cleanup-e2e.test.sh +146 -0
  562. package/.agents/tests/fm-herdr-session-cleanup.test.sh +329 -0
  563. package/.agents/tests/fm-herdr-submit-confirm-live-e2e.test.sh +125 -0
  564. package/.agents/tests/fm-herdr-version-floor-live-e2e.test.sh +144 -0
  565. package/.agents/tests/fm-home-summary-refresh.test.sh +971 -0
  566. package/.agents/tests/fm-inactive-reconcile.test.sh +485 -0
  567. package/.agents/tests/fm-kimi-harness.test.sh +687 -0
  568. package/.agents/tests/fm-lint-workflows.test.sh +532 -0
  569. package/.agents/tests/fm-lint.test.sh +1023 -0
  570. package/.agents/tests/fm-muse-harness.test.sh +932 -0
  571. package/.agents/tests/fm-muse-signals-live-e2e.test.sh +205 -0
  572. package/.agents/tests/fm-no-mistakes-required.test.sh +72 -0
  573. package/.agents/tests/fm-on.test.sh +507 -0
  574. package/.agents/tests/fm-opencode-primary-live-e2e.test.sh +357 -0
  575. package/.agents/tests/fm-operational-input.test.sh +160 -0
  576. package/.agents/tests/fm-peek-remote.test.sh +110 -0
  577. package/.agents/tests/fm-pending-reply.test.sh +1288 -0
  578. package/.agents/tests/fm-pi-branch-extension.test.sh +3182 -0
  579. package/.agents/tests/fm-pi-branch-live-e2e.test.sh +527 -0
  580. package/.agents/tests/fm-pi-primary-live-e2e.test.sh +345 -0
  581. package/.agents/tests/fm-pi-primary-types.test.sh +68 -0
  582. package/.agents/tests/fm-pi-watch-extension.test.sh +2843 -0
  583. package/.agents/tests/fm-pr-check-security.test.sh +2140 -0
  584. package/.agents/tests/fm-pr-merge.test.sh +2135 -0
  585. package/.agents/tests/fm-procevent-quota.test.sh +225 -0
  586. package/.agents/tests/fm-procevent-when.test.sh +406 -0
  587. package/.agents/tests/fm-procevent.test.sh +1745 -0
  588. package/.agents/tests/fm-project-origin.test.sh +110 -0
  589. package/.agents/tests/fm-public-followup.test.sh +2272 -0
  590. package/.agents/tests/fm-quota-array-dispatch-live-e2e.test.sh +526 -0
  591. package/.agents/tests/fm-quota-choose.test.sh +626 -0
  592. package/.agents/tests/fm-remote-backlog-handoff.test.sh +453 -0
  593. package/.agents/tests/fm-remote-doctor.test.sh +625 -0
  594. package/.agents/tests/fm-remote-entrypoint.test.sh +59 -0
  595. package/.agents/tests/fm-remote-job-orphan-reap.test.sh +205 -0
  596. package/.agents/tests/fm-remote-job.test.sh +768 -0
  597. package/.agents/tests/fm-remote-reply.test.sh +520 -0
  598. package/.agents/tests/fm-remote-secondmate-lifecycle-e2e.test.sh +1256 -0
  599. package/.agents/tests/fm-remote-secondmate-parent-binding.test.sh +326 -0
  600. package/.agents/tests/fm-remote-secondmate-trace-context.test.sh +310 -0
  601. package/.agents/tests/fm-remote-transport-lanes.test.sh +431 -0
  602. package/.agents/tests/fm-review-diff.test.sh +176 -0
  603. package/.agents/tests/fm-secondmate-harness.test.sh +2600 -0
  604. package/.agents/tests/fm-secondmate-lifecycle-e2e.test.sh +259 -0
  605. package/.agents/tests/fm-secondmate-liveness.test.sh +559 -0
  606. package/.agents/tests/fm-secondmate-reconcile.test.sh +732 -0
  607. package/.agents/tests/fm-secondmate-safety.test.sh +2972 -0
  608. package/.agents/tests/fm-secondmate-sync.test.sh +913 -0
  609. package/.agents/tests/fm-send-inbox-doorbell-live-e2e.test.sh +206 -0
  610. package/.agents/tests/fm-send-inbox.test.sh +352 -0
  611. package/.agents/tests/fm-send-popup-settle.test.sh +167 -0
  612. package/.agents/tests/fm-send-remote-delivery.test.sh +796 -0
  613. package/.agents/tests/fm-send-resolve-key.test.sh +551 -0
  614. package/.agents/tests/fm-send-secondmate-marker-herdr-e2e.test.sh +187 -0
  615. package/.agents/tests/fm-send-secondmate-marker.test.sh +278 -0
  616. package/.agents/tests/fm-send-settle.test.sh +147 -0
  617. package/.agents/tests/fm-send-strict.test.sh +241 -0
  618. package/.agents/tests/fm-session-lock-ancestry.test.sh +365 -0
  619. package/.agents/tests/fm-session-start.test.sh +2515 -0
  620. package/.agents/tests/fm-sessionstart-hook-live-e2e.test.sh +631 -0
  621. package/.agents/tests/fm-sessionstart-instruction-refresh-live-e2e.test.sh +230 -0
  622. package/.agents/tests/fm-sessionstart-nudge.test.sh +1039 -0
  623. package/.agents/tests/fm-shared-captain-inheritance.test.sh +404 -0
  624. package/.agents/tests/fm-spawn-batch.test.sh +148 -0
  625. package/.agents/tests/fm-spawn-dispatch-profile.test.sh +827 -0
  626. package/.agents/tests/fm-spawn-pool-base-freshen.test.sh +440 -0
  627. package/.agents/tests/fm-spawn-worktree-settle.test.sh +147 -0
  628. package/.agents/tests/fm-startup-memory-budget.test.sh +333 -0
  629. package/.agents/tests/fm-startup-network.test.sh +712 -0
  630. package/.agents/tests/fm-stow-cascade.test.sh +370 -0
  631. package/.agents/tests/fm-subagent-pretool-check.test.sh +291 -0
  632. package/.agents/tests/fm-supervision-events.test.sh +156 -0
  633. package/.agents/tests/fm-supervision-instructions.test.sh +189 -0
  634. package/.agents/tests/fm-tangle-guard.test.sh +277 -0
  635. package/.agents/tests/fm-task-delivery.test.sh +420 -0
  636. package/.agents/tests/fm-task-inbox.test.sh +513 -0
  637. package/.agents/tests/fm-teardown-endpoint-safety.test.sh +374 -0
  638. package/.agents/tests/fm-teardown.test.sh +2665 -0
  639. package/.agents/tests/fm-test-fixture-cleanup.test.sh +172 -0
  640. package/.agents/tests/fm-test-fixtures.test.sh +132 -0
  641. package/.agents/tests/fm-test-isolation-proof.test.sh +278 -0
  642. package/.agents/tests/fm-test-run.test.sh +1196 -0
  643. package/.agents/tests/fm-tmux-agent-liveness.test.sh +354 -0
  644. package/.agents/tests/fm-tmux-submit-busy.test.sh +340 -0
  645. package/.agents/tests/fm-tool-update-check.test.sh +1044 -0
  646. package/.agents/tests/fm-trace-context-lib.test.sh +253 -0
  647. package/.agents/tests/fm-trace-context-spawn.test.sh +581 -0
  648. package/.agents/tests/fm-transition-lib.test.sh +54 -0
  649. package/.agents/tests/fm-turnend-guard.test.sh +1822 -0
  650. package/.agents/tests/fm-update.test.sh +304 -0
  651. package/.agents/tests/fm-vendor-auth-probe.test.sh +395 -0
  652. package/.agents/tests/fm-voice-relay.test.sh +4741 -0
  653. package/.agents/tests/fm-wake-daemon-lifecycle-e2e.test.sh +171 -0
  654. package/.agents/tests/fm-wake-drain-open-decisions-cursor.test.sh +356 -0
  655. package/.agents/tests/fm-wake-drain-open-decisions.test.sh +226 -0
  656. package/.agents/tests/fm-wake-drain-unread-status.test.sh +379 -0
  657. package/.agents/tests/fm-wake-queue.test.sh +1224 -0
  658. package/.agents/tests/fm-watch-arm.test.sh +821 -0
  659. package/.agents/tests/fm-watch-checkpoint.test.sh +87 -0
  660. package/.agents/tests/fm-watch-recovery-loop.test.sh +223 -0
  661. package/.agents/tests/fm-watch-triage.test.sh +3889 -0
  662. package/.agents/tests/fm-watcher-lock.test.sh +1133 -0
  663. package/.agents/tests/fm-x-mode.test.sh +3042 -0
  664. package/.agents/tests/gleipnir-guard-pretool-check.test.sh +51 -0
  665. package/.agents/tests/gleipnir-machine-lock.test.sh +130 -0
  666. package/.agents/tests/herdr-test-safety.sh +42 -0
  667. package/.agents/tests/lib.sh +315 -0
  668. package/.agents/tests/remote-herdr-fixture.sh +125 -0
  669. package/.agents/tests/secondmate-helpers.sh +193 -0
  670. package/.agents/tests/smoke.test.sh +93 -0
  671. package/.agents/tests/wake-helpers.sh +338 -0
  672. package/.agents/tests/zellij-test-safety.sh +60 -0
  673. package/.agents/tools/README.md +10 -0
  674. package/.agents/tools/bin/tasks-cli +267 -0
  675. package/.agents/tools/tasks-cli.ts +216 -0
  676. package/AGENTS.md +454 -0
  677. package/CHANGELOG.md +881 -0
  678. package/LICENSE +1 -1
  679. package/NOTICE +63 -0
  680. package/README.md +113 -39
  681. package/RULES/01-domains.md +51 -0
  682. package/RULES/02-agents.md +78 -0
  683. package/RULES/03-houses.md +61 -0
  684. package/RULES/04-hoard.md +65 -0
  685. package/RULES/05-platforms.md +67 -0
  686. package/RULES/06-append-only.md +58 -0
  687. package/RULES/07-config.md +45 -0
  688. package/RULES/08-delivery-gate.md +75 -0
  689. package/RULES/README.md +20 -0
  690. package/bin/README.md +107 -0
  691. package/bin/a2a-mcp.sh +98 -0
  692. package/bin/a2a-serve.py +103 -0
  693. package/bin/a2a-serve.sh +8 -0
  694. package/bin/a2a-talk.sh +50 -0
  695. package/bin/a2abridge-ensure.sh +108 -0
  696. package/bin/agent-run.sh +43 -0
  697. package/bin/agents-config.sh +233 -0
  698. package/bin/apodex-smoke-test.sh +92 -0
  699. package/bin/bifrost-bridge.sh +112 -0
  700. package/bin/bifrost-ingress.sh +77 -0
  701. package/bin/bootstrap-macos.sh +107 -0
  702. package/bin/bootstrap-windows.ps1 +112 -0
  703. package/bin/branch-guard.sh +64 -0
  704. package/bin/brokk +73 -0
  705. package/bin/brokk-classify-lib.sh +1770 -0
  706. package/bin/brokk-lease-lib.sh +222 -0
  707. package/bin/brokk-lease.sh +193 -0
  708. package/bin/brokk-lint.sh +65 -0
  709. package/bin/brokk-send.sh +38 -0
  710. package/bin/brokk-update.sh +9 -0
  711. package/bin/brokk-wake-grant.sh +127 -0
  712. package/bin/brokk-wake-lib.sh +1849 -0
  713. package/bin/changelog-guard.sh +165 -0
  714. package/bin/crash-sense.sh +160 -0
  715. package/bin/desktop-place.sh +224 -0
  716. package/bin/docs-guard.sh +39 -0
  717. package/bin/editor-place.sh +207 -0
  718. package/bin/eindri-acclaim.sh +34 -0
  719. package/bin/eindri-control.sh +28 -0
  720. package/bin/eindri-role.sh +83 -0
  721. package/bin/eindri-seen.sh +41 -0
  722. package/bin/eindri-send.sh +15 -0
  723. package/bin/eindri-start.sh +85 -0
  724. package/bin/eindri-watch.sh +57 -0
  725. package/bin/einherjar-spawn.sh +576 -0
  726. package/bin/eir-doctor.sh +112 -0
  727. package/bin/erindi-brief.sh +271 -0
  728. package/bin/fleet-apply.sh +68 -0
  729. package/bin/github-deploy.sh +81 -0
  730. package/bin/gjallarhorn-expose.sh +102 -0
  731. package/bin/gjallarhorn-notify.sh +58 -0
  732. package/bin/gjallarhorn-purge.sh +54 -0
  733. package/bin/gjallarhorn-tunnel.sh +72 -0
  734. package/bin/gleipnir-lock-lib.sh +284 -0
  735. package/bin/groa-update.sh +122 -0
  736. package/bin/hall-snapshot.sh +92 -0
  737. package/bin/hamr-harness.sh +246 -0
  738. package/bin/heimdall-ssh-keys.sh +374 -0
  739. package/bin/herdr-agents.py +44 -0
  740. package/bin/herdr-ensure.sh +104 -0
  741. package/bin/herdr-run.sh +431 -0
  742. package/bin/hermes-ensure.sh +88 -0
  743. package/bin/hoard-lib.sh +25 -0
  744. package/bin/hodd.sh +78 -0
  745. package/bin/host-sense.sh +199 -0
  746. package/bin/huginn-research-worker.sh +108 -0
  747. package/bin/local-model-lock.sh +51 -0
  748. package/bin/mimir-bridge.py +226 -0
  749. package/bin/mimir-bridge.sh +87 -0
  750. package/bin/mimir-ingest.sh +131 -0
  751. package/bin/mimir.sh +113 -0
  752. package/bin/mjollnir-webhook.sh +59 -0
  753. package/bin/mjollnir.sh +85 -0
  754. package/bin/model-hardware.sh +82 -0
  755. package/bin/model-resolve.sh +147 -0
  756. package/bin/models-detect.sh +178 -0
  757. package/bin/nornir-cron-start.sh +162 -0
  758. package/bin/nornir-job-daily-briefing.sh +191 -0
  759. package/bin/nornir-job-git-sync.sh +152 -0
  760. package/bin/nornir-job-memory-housekeeping.sh +114 -0
  761. package/bin/nornir-job-observer.sh +166 -0
  762. package/bin/nornir-job-skillopt-sleep.sh +76 -0
  763. package/bin/omarchy-hook-install.sh +60 -0
  764. package/bin/omarchy-install.sh +148 -0
  765. package/bin/omarchy-plugins.sh +174 -0
  766. package/bin/omarchy-sense.sh +149 -0
  767. package/bin/perm-guard.sh +30 -0
  768. package/bin/pi-agent.sh +68 -0
  769. package/bin/pi-ensure.sh +77 -0
  770. package/bin/pi-local.sh +37 -0
  771. package/bin/pi-seat.sh +63 -0
  772. package/bin/prereq-ensure.sh +166 -0
  773. package/bin/private-guard.sh +38 -0
  774. package/bin/project-git.sh +55 -0
  775. package/bin/public-guard.sh +70 -0
  776. package/bin/ratatoskr.sh +49 -0
  777. package/bin/realm-lib.sh +32 -0
  778. package/bin/repo-scrub.sh +62 -0
  779. package/bin/rodd-operational-input.sh +250 -0
  780. package/bin/runes-append.sh +198 -0
  781. package/bin/saga-session-start.sh +172 -0
  782. package/bin/saga-sessionstart-run.sh +79 -0
  783. package/bin/saga-wake-drain.sh +41 -0
  784. package/bin/secret-guard.sh +63 -0
  785. package/bin/sessrumnir-ensure.sh +100 -0
  786. package/bin/sessrumnir-sync.sh +237 -0
  787. package/bin/sessrumnir.sh +91 -0
  788. package/bin/skillopt-setup.sh +76 -0
  789. package/bin/skuld-branch-outcome.sh +203 -0
  790. package/bin/skuld-branch-prompt.sh +101 -0
  791. package/bin/smidja-bootstrap.sh +90 -0
  792. package/bin/smidja-observe.sh +86 -0
  793. package/bin/syn-arm-pretool-check.sh +57 -0
  794. package/bin/syn-asset-pretool-check.sh +76 -0
  795. package/bin/syn-cd-pretool-check.sh +28 -0
  796. package/bin/syn-guard-pretool-check.sh +83 -0
  797. package/bin/syn-turnend-guard.sh +36 -0
  798. package/bin/syn-watch-arm.sh +99 -0
  799. package/bin/tailscale-sync.sh +190 -0
  800. package/bin/telegram-bot.sh +69 -0
  801. package/bin/toolchain.sh +74 -0
  802. package/bin/utgard.sh +111 -0
  803. package/bin/valhalla.sh +115 -0
  804. package/bin/valknut-load.sh +260 -0
  805. package/bin/vor-crew-state.sh +124 -0
  806. package/bin/wedge-notify.sh +47 -0
  807. package/bin/workspace-provision.sh +80 -0
  808. package/bin/workspace-rag.sh +150 -0
  809. package/bin/wyrd-db.sh +47 -0
  810. package/bin/yggdrasil.sh +152 -0
  811. package/bin/ymir-edit.sh +145 -0
  812. package/bin/ymir-install.sh +641 -0
  813. package/bin/ymir-invite.sh +93 -0
  814. package/bin/ymir-isolation.sh +88 -0
  815. package/bin/ymir-migrate.sh +61 -0
  816. package/bin/ymir-platform.sh +348 -0
  817. package/bin/ymir-say.sh +97 -0
  818. package/bin/ymir-setup-auth.sh +108 -0
  819. package/bin/ymir-validate.sh +173 -0
  820. package/bin/ymir.js +74 -16
  821. package/docs/Architecture.md +339 -0
  822. package/docs/design.md +351 -0
  823. package/docs/installations/README.md +63 -0
  824. package/docs/installations/ymir-home.md +93 -0
  825. package/docs/integration.md +105 -0
  826. package/docs/lore.md +1056 -0
  827. package/docs/research/agent-harnesses.md +290 -0
  828. package/docs/runbooks/README.md +19 -0
  829. package/docs/runbooks/a2a.md +91 -0
  830. package/docs/runbooks/agent-permissions.md +99 -0
  831. package/docs/runbooks/agents.md +57 -0
  832. package/docs/runbooks/models.md +69 -0
  833. package/docs/runbooks/realm-onboarding.md +60 -0
  834. package/docs/runbooks/secrets-and-hoard.md +60 -0
  835. package/docs/runbooks/updates-and-migrations.md +110 -0
  836. package/docs/runbooks/worktrees-isolation.md +72 -0
  837. package/docs/session-start.md +105 -0
  838. package/docs/skillopt-integration.md +321 -0
  839. package/docs/sync-system.md +66 -0
  840. package/docs/workspaces.md +104 -0
  841. package/docs/ymir-rut.md +260 -0
  842. package/package.json +44 -14
  843. package/packaging/build.sh +74 -0
  844. package/packaging/macos/Ymir Installer.command +43 -0
  845. package/packaging/windows/ymir-setup.nsi +74 -0
  846. package/scripts/README.md +22 -0
  847. package/scripts/electron.sh +210 -0
  848. package/scripts/start.sh +167 -0
  849. package/scripts/stop.sh +113 -0
  850. package/install.sh +0 -35
@@ -0,0 +1,1166 @@
1
+ #!/usr/bin/env bash
2
+ # tests/fm-backend-cmux.test.sh - fake-cmux-CLI unit tests for the cmux
3
+ # session-provider adapter (bin/backends/cmux.sh), verified against the real
4
+ # cmux 0.64.17 binary (docs/cmux-backend.md). Mirrors
5
+ # tests/fm-backend-zellij.test.sh's/tests/fm-backend-herdr.test.sh's
6
+ # fakebin/command-log convention: a small, LOG-based, canned-response fake
7
+ # `cmux` + real `jq` (jq is a real required tool for this backend, not
8
+ # faked). The real-binary smoke test lives in
9
+ # tests/fm-backend-cmux-smoke.test.sh, gated on the cmux binary actually
10
+ # being installed and reachable.
11
+ set -u
12
+
13
+ # shellcheck source=tests/lib.sh
14
+ . "$(dirname "${BASH_SOURCE[0]}")/lib.sh"
15
+
16
+ command -v jq >/dev/null 2>&1 || { echo "skip: jq not found (required by the cmux adapter)"; exit 0; }
17
+
18
+ TMP_ROOT=$(fm_test_tmproot fm-backend-cmux-tests)
19
+
20
+ # make_cmux_fakebin: a `cmux` stub that logs every invocation (one line,
21
+ # unit-separated args, to $FM_CMUX_LOG) and returns the canned response for
22
+ # that call read from $FM_CMUX_RESPONSES/<n>.out, consumed IN ORDER (call 1
23
+ # reads 1.out, call 2 reads 2.out, ...), mirroring
24
+ # tests/fm-backend-zellij.test.sh's make_zellij_fakebin. A missing response
25
+ # file means "succeed with empty stdout" (new-workspace/send/send-key/
26
+ # close-* are silent on success on the real CLI). `version` and `ping` are
27
+ # handled specially (not call-counted, not consuming the ordered response
28
+ # queue) since fm_backend_cmux_version_check/fm_backend_cmux_ping_state are
29
+ # called at points a test may not want to hand-count, exactly mirroring
30
+ # zellij's --version/list-sessions special-casing.
31
+ make_cmux_fakebin() { # <dir> -> echoes fakebin dir
32
+ local dir=$1 fb="$1/fakebin"
33
+ mkdir -p "$fb"
34
+ cat > "$fb/cmux" <<'SH'
35
+ #!/usr/bin/env bash
36
+ set -u
37
+ LOG="${FM_CMUX_LOG:?}"
38
+ RESP="${FM_CMUX_RESPONSES:?}"
39
+ COUNT_FILE="$RESP/.count"
40
+ {
41
+ printf 'CMUX_SOCKET_PASSWORD=%s' "${CMUX_SOCKET_PASSWORD:-}"
42
+ for a in "$@"; do printf '\x1f%s' "$a"; done
43
+ printf '\n'
44
+ } >> "$LOG"
45
+
46
+ if [ "${1:-}" = version ]; then
47
+ printf 'cmux %s (97) [abcdef1]\n' "${FM_CMUX_FAKE_VERSION:-0.64.17}"
48
+ exit 0
49
+ fi
50
+ if [ "${1:-}" = ping ]; then
51
+ printf '%s\n' "${FM_CMUX_FAKE_PING:-PONG}"
52
+ exit "${FM_CMUX_FAKE_PING_EXIT:-0}"
53
+ fi
54
+
55
+ next=$(( $(cat "$COUNT_FILE" 2>/dev/null || echo 0) + 1 ))
56
+ n=$next
57
+ echo "$n" > "$COUNT_FILE"
58
+ if [ -f "$RESP/$n.exit" ]; then
59
+ exit "$(cat "$RESP/$n.exit")"
60
+ fi
61
+ [ -f "$RESP/$n.out" ] && cat "$RESP/$n.out"
62
+ exit 0
63
+ SH
64
+ chmod +x "$fb/cmux"
65
+ printf '%s\n' "$fb"
66
+ }
67
+
68
+ cmux_workspace_list_response() { # <dir> <n> <id1> <title1> [<id2> <title2> ...]
69
+ local dir=$1 n=$2 json first=1
70
+ shift 2
71
+ json='{"workspaces":['
72
+ while [ $# -ge 2 ]; do
73
+ [ "$first" -eq 1 ] || json="$json,"
74
+ json="$json{\"id\":\"$1\",\"title\":\"$2\"}"
75
+ first=0
76
+ shift 2
77
+ done
78
+ json="$json]}"
79
+ printf '%s' "$json" > "$dir/responses/$n.out"
80
+ }
81
+
82
+ cmux_panes_response() { # <dir> <n> <surface_id>
83
+ printf '{"panes":[{"selected_surface_id":"%s","surface_ids":["%s"]}]}' "$3" "$3" > "$1/responses/$2.out"
84
+ }
85
+
86
+ cmux_windows_response() { # <dir> <n> <window_id1> <count1> [<window_id2> <count2> ...]
87
+ local dir=$1 n=$2 json first=1
88
+ shift 2
89
+ json='['
90
+ while [ $# -ge 2 ]; do
91
+ [ "$first" -eq 1 ] || json="$json,"
92
+ json="$json{\"id\":\"$1\",\"workspace_count\":$2}"
93
+ first=0
94
+ shift 2
95
+ done
96
+ json="$json]"
97
+ printf '%s' "$json" > "$dir/responses/$n.out"
98
+ }
99
+
100
+ cmux_panes_empty_response() { # <dir> <n>
101
+ printf '{"panes":[]}' > "$1/responses/$2.out"
102
+ }
103
+
104
+ cmux_read_screen_response() { # <dir> <n> <text>
105
+ jq -n --arg t "$3" '{text:$t}' > "$1/responses/$2.out"
106
+ }
107
+
108
+ cmux_expected_root_hash() { # <root>
109
+ local root real
110
+ root=$1
111
+ real=$(cd "$root" && pwd -P) || return 1
112
+ if command -v shasum >/dev/null 2>&1; then
113
+ printf '%s' "$real" | shasum -a 256 | awk '{print substr($1,1,8)}'
114
+ elif command -v sha256sum >/dev/null 2>&1; then
115
+ printf '%s' "$real" | sha256sum | awk '{print substr($1,1,8)}'
116
+ else
117
+ printf '%s' "$real" | cksum | awk '{printf "%08x", $1}'
118
+ fi
119
+ }
120
+
121
+ cmux_expected_home_label() { # [home] [root]
122
+ local home=${1:-$ROOT} root=${2:-$ROOT} marker id prefix
123
+ marker="$home/.fm-secondmate-home"
124
+ if [ -f "$marker" ]; then
125
+ id=$(tr -d '[:space:]' < "$marker" 2>/dev/null)
126
+ if [ -n "$id" ]; then
127
+ prefix="2ndmate-$id"
128
+ else
129
+ prefix="firstmate"
130
+ fi
131
+ else
132
+ prefix="firstmate"
133
+ fi
134
+ printf '%s-%s' "$prefix" "$(cmux_expected_root_hash "$root")"
135
+ }
136
+
137
+ cmux_expected_scoped_title() { # <fm-task-label> [home] [root]
138
+ local label=$1 home=${2:-$ROOT} root=${3:-$ROOT} rest
139
+ case "$label" in
140
+ fm-*) rest=${label#fm-} ;;
141
+ *) rest=$label ;;
142
+ esac
143
+ printf 'fm-%s-%s' "$(cmux_expected_home_label "$home" "$root")" "$rest"
144
+ }
145
+
146
+ cmux_assert_call_order() {
147
+ local log=$1 before=$2 after=$3 msg=$4 before_line after_line
148
+ before_line=$(grep -anF -- "$before" "$log" | head -1 | cut -d: -f1)
149
+ after_line=$(grep -anF -- "$after" "$log" | head -1 | cut -d: -f1)
150
+ [ -n "$before_line" ] || fail "$msg (missing before call: '$before')"
151
+ [ -n "$after_line" ] || fail "$msg (missing after call: '$after')"
152
+ [ "$before_line" -lt "$after_line" ] || fail "$msg"
153
+ }
154
+
155
+ # --- version_check / tool_check ----------------------------------------------
156
+
157
+ test_version_check_accepts_current_version() {
158
+ local dir fb status
159
+ dir="$TMP_ROOT/version-ok"; mkdir -p "$dir/responses"
160
+ fb=$(make_cmux_fakebin "$dir")
161
+ PATH="$fb:$PATH" FM_CMUX_LOG="$dir/log" FM_CMUX_RESPONSES="$dir/responses" FM_CMUX_FAKE_VERSION=0.64.17 \
162
+ bash -c '. "$0/bin/backends/cmux.sh"; fm_backend_cmux_version_check' "$ROOT"
163
+ status=$?
164
+ expect_code 0 "$status" "version_check should accept 0.64.17 (the verified minimum)"
165
+ pass "fm_backend_cmux_version_check: accepts the verified minimum (0.64.17)"
166
+ }
167
+
168
+ test_version_check_accepts_newer_version() {
169
+ local dir fb status
170
+ dir="$TMP_ROOT/version-newer"; mkdir -p "$dir/responses"
171
+ fb=$(make_cmux_fakebin "$dir")
172
+ PATH="$fb:$PATH" FM_CMUX_LOG="$dir/log" FM_CMUX_RESPONSES="$dir/responses" FM_CMUX_FAKE_VERSION=0.70.0 \
173
+ bash -c '. "$0/bin/backends/cmux.sh"; fm_backend_cmux_version_check' "$ROOT"
174
+ status=$?
175
+ expect_code 0 "$status" "version_check should accept a newer minor (0.70.0)"
176
+ pass "fm_backend_cmux_version_check: accepts a newer version (0.70.0)"
177
+ }
178
+
179
+ test_version_check_refuses_old_version() {
180
+ local dir fb out status
181
+ dir="$TMP_ROOT/version-old"; mkdir -p "$dir/responses"
182
+ fb=$(make_cmux_fakebin "$dir")
183
+ out=$( PATH="$fb:$PATH" FM_CMUX_LOG="$dir/log" FM_CMUX_RESPONSES="$dir/responses" FM_CMUX_FAKE_VERSION=0.50.0 \
184
+ bash -c '. "$0/bin/backends/cmux.sh"; fm_backend_cmux_version_check' "$ROOT" 2>&1 )
185
+ status=$?
186
+ [ "$status" -ne 0 ] || fail "version_check should refuse 0.50.0 (below the 0.64 minimum)"
187
+ assert_contains "$out" "0.50.0" "version_check error did not name the rejected version"
188
+ pass "fm_backend_cmux_version_check: refuses an old version loudly"
189
+ }
190
+
191
+ test_version_check_refuses_missing_cmux() {
192
+ local dir out status
193
+ dir="$TMP_ROOT/version-missing"; mkdir -p "$dir/empty-fakebin"
194
+ # FM_BACKEND_CMUX_BUNDLE_BIN must also be overridden to a nonexistent path:
195
+ # this test may run on a machine (like the one that verified this adapter)
196
+ # where cmux really is installed at the real bundle path, which the plain
197
+ # PATH-emptying above would not hide.
198
+ out=$( PATH="$dir/empty-fakebin:/usr/bin:/bin" FM_BACKEND_CMUX_BUNDLE_BIN="$dir/no-such-cmux" \
199
+ bash -c '. "$0/bin/backends/cmux.sh"; fm_backend_cmux_version_check' "$ROOT" 2>&1 )
200
+ status=$?
201
+ [ "$status" -ne 0 ] || fail "version_check should refuse when cmux is not installed"
202
+ assert_contains "$out" "not found" "version_check did not report cmux as missing"
203
+ pass "fm_backend_cmux_version_check: refuses loudly when cmux is not found on PATH or at the bundle path"
204
+ }
205
+
206
+ # --- password resolution -------------------------------------------------
207
+
208
+ test_password_reads_from_config_file() {
209
+ local dir out
210
+ dir="$TMP_ROOT/password-file"; mkdir -p "$dir/config"
211
+ printf 'sekret-pw\n' > "$dir/config/cmux-socket-password"
212
+ out=$( FM_HOME="$dir" bash -c '. "$0/bin/backends/cmux.sh"; fm_backend_cmux_password' "$ROOT" )
213
+ [ "$out" = "sekret-pw" ] || fail "password should be read from config/cmux-socket-password, got '$out'"
214
+ pass "fm_backend_cmux_password: reads the first non-empty line of config/cmux-socket-password"
215
+ }
216
+
217
+ test_password_preserves_config_file_whitespace() {
218
+ local dir out
219
+ dir="$TMP_ROOT/password-file-whitespace"; mkdir -p "$dir/config"
220
+ printf '\nsek ret\t pw \n' > "$dir/config/cmux-socket-password"
221
+ out=$( FM_HOME="$dir" bash -c '. "$0/bin/backends/cmux.sh"; fm_backend_cmux_password' "$ROOT" )
222
+ [ "$out" = $'sek ret\t pw ' ] || fail "password should preserve spaces and tabs from config/cmux-socket-password, got '$out'"
223
+ pass "fm_backend_cmux_password: preserves spaces and tabs in config/cmux-socket-password"
224
+ }
225
+
226
+ test_password_respects_config_override() {
227
+ local dir home_cfg override_cfg out
228
+ dir="$TMP_ROOT/password-config-override"; home_cfg="$dir/home/config"; override_cfg="$dir/override-config"
229
+ mkdir -p "$home_cfg" "$override_cfg"
230
+ printf 'home-pw\n' > "$home_cfg/cmux-socket-password"
231
+ printf 'override-pw\n' > "$override_cfg/cmux-socket-password"
232
+ out=$( FM_HOME="$dir/home" FM_CONFIG_OVERRIDE="$override_cfg" bash -c '. "$0/bin/backends/cmux.sh"; fm_backend_cmux_password' "$ROOT" )
233
+ [ "$out" = "override-pw" ] || fail "password should be read from FM_CONFIG_OVERRIDE, got '$out'"
234
+ pass "fm_backend_cmux_password: respects FM_CONFIG_OVERRIDE"
235
+ }
236
+
237
+ test_password_empty_when_config_absent() {
238
+ local dir out
239
+ dir="$TMP_ROOT/password-absent"; mkdir -p "$dir/config"
240
+ out=$( FM_HOME="$dir" bash -c '. "$0/bin/backends/cmux.sh"; fm_backend_cmux_password' "$ROOT" )
241
+ [ -z "$out" ] || fail "password should be empty when config/cmux-socket-password is absent, got '$out'"
242
+ pass "fm_backend_cmux_password: empty when config/cmux-socket-password is absent"
243
+ }
244
+
245
+ test_cli_exports_password_only_when_configured() {
246
+ local dir fb
247
+ dir="$TMP_ROOT/password-export"; mkdir -p "$dir/config" "$dir/responses"
248
+ printf 'sekret-pw\n' > "$dir/config/cmux-socket-password"
249
+ fb=$(make_cmux_fakebin "$dir")
250
+ PATH="$fb:$PATH" FM_HOME="$dir" FM_CMUX_LOG="$dir/log" FM_CMUX_RESPONSES="$dir/responses" \
251
+ bash -c '. "$0/bin/backends/cmux.sh"; fm_backend_cmux_cli ping' "$ROOT" >/dev/null
252
+ assert_contains "$(cat "$dir/log")" "CMUX_SOCKET_PASSWORD=sekret-pw" \
253
+ "fm_backend_cmux_cli did not export the configured password"
254
+ pass "fm_backend_cmux_cli: exports CMUX_SOCKET_PASSWORD when config/cmux-socket-password is set"
255
+ }
256
+
257
+ # --- target parsing, key normalization ---------------------------------------
258
+
259
+ test_parse_target() {
260
+ ( . "$ROOT/bin/backends/cmux.sh"
261
+ fm_backend_cmux_parse_target "11111111-1111-1111-1111-111111111111:22222222-2222-2222-2222-222222222222" || exit 1
262
+ [ "$FM_BACKEND_CMUX_WORKSPACE" = "11111111-1111-1111-1111-111111111111" ] || { echo "workspace mismatch: $FM_BACKEND_CMUX_WORKSPACE" >&2; exit 1; }
263
+ [ "$FM_BACKEND_CMUX_SURFACE" = "22222222-2222-2222-2222-222222222222" ] || { echo "surface mismatch: $FM_BACKEND_CMUX_SURFACE" >&2; exit 1; }
264
+ ) || fail "fm_backend_cmux_parse_target did not split workspace:surface correctly"
265
+ pass "fm_backend_cmux_parse_target: splits '<workspace_uuid>:<surface_uuid>' on the first colon"
266
+ }
267
+
268
+ test_normalize_key() {
269
+ ( . "$ROOT/bin/backends/cmux.sh"
270
+ [ "$(fm_backend_cmux_normalize_key Enter)" = enter ] || { echo "Enter failed" >&2; exit 1; }
271
+ [ "$(fm_backend_cmux_normalize_key Escape)" = escape ] || { echo "Escape failed" >&2; exit 1; }
272
+ [ "$(fm_backend_cmux_normalize_key Esc)" = escape ] || { echo "Esc failed" >&2; exit 1; }
273
+ [ "$(fm_backend_cmux_normalize_key C-c)" = ctrl-c ] || { echo "C-c failed" >&2; exit 1; }
274
+ [ "$(fm_backend_cmux_normalize_key ctrl+c)" = ctrl-c ] || { echo "ctrl+c failed" >&2; exit 1; }
275
+ ) || fail "fm_backend_cmux_normalize_key did not map firstmate's key vocabulary to cmux's verified names"
276
+ pass "fm_backend_cmux_normalize_key: Enter/Escape/C-c map to cmux's verified enter/escape/ctrl-c"
277
+ }
278
+
279
+ test_scoped_title_uses_primary_home_label() {
280
+ local dir out expected
281
+ dir="$TMP_ROOT/scoped-title-primary"; mkdir -p "$dir"
282
+ expected=$(cmux_expected_scoped_title fm-task1 "$dir")
283
+ out=$( FM_HOME="$dir" bash -c '. "$0/bin/backends/cmux.sh"; fm_backend_cmux_scoped_title fm-task1' "$ROOT" )
284
+ [ "$out" = "$expected" ] || fail "primary scoped title should be $expected, got '$out'"
285
+ pass "fm_backend_cmux_scoped_title: scopes a primary task title with firstmate plus root hash"
286
+ }
287
+
288
+ test_scoped_title_uses_secondmate_home_label() {
289
+ local dir out expected
290
+ dir="$TMP_ROOT/scoped-title-secondmate"; mkdir -p "$dir"
291
+ printf 'sm-one\n' > "$dir/.fm-secondmate-home"
292
+ expected=$(cmux_expected_scoped_title fm-task1 "$dir")
293
+ out=$( FM_HOME="$dir" bash -c '. "$0/bin/backends/cmux.sh"; fm_backend_cmux_scoped_title fm-task1' "$ROOT" )
294
+ [ "$out" = "$expected" ] || fail "secondmate scoped title should be $expected, got '$out'"
295
+ pass "fm_backend_cmux_scoped_title: scopes a secondmate task title with the home marker plus root hash"
296
+ }
297
+
298
+ test_scoped_title_changes_with_root_path() {
299
+ local dir home root_one root_two out_one out_two expected_one expected_two
300
+ dir="$TMP_ROOT/scoped-title-root-hash"; home="$dir/home"; root_one="$dir/root-one"; root_two="$dir/root-two"
301
+ mkdir -p "$home" "$root_one" "$root_two"
302
+ expected_one=$(cmux_expected_scoped_title fm-task1 "$home" "$root_one")
303
+ expected_two=$(cmux_expected_scoped_title fm-task1 "$home" "$root_two")
304
+ out_one=$( FM_HOME="$home" FM_ROOT_OVERRIDE="$root_one" bash -c '. "$0/bin/backends/cmux.sh"; fm_backend_cmux_scoped_title fm-task1' "$ROOT" )
305
+ out_two=$( FM_HOME="$home" FM_ROOT_OVERRIDE="$root_two" bash -c '. "$0/bin/backends/cmux.sh"; fm_backend_cmux_scoped_title fm-task1' "$ROOT" )
306
+ [ "$out_one" = "$expected_one" ] || fail "scoped title should include root-one hash as $expected_one, got '$out_one'"
307
+ [ "$out_two" = "$expected_two" ] || fail "scoped title should include root-two hash as $expected_two, got '$out_two'"
308
+ [ "$out_one" != "$out_two" ] || fail "scoped titles should differ for distinct FM_ROOT paths"
309
+ pass "fm_backend_cmux_scoped_title: includes the resolved FM_ROOT hash in the home label"
310
+ }
311
+
312
+ # --- dispatch wiring (fm-backend.sh) ------------------------------------------
313
+
314
+ test_dispatch_routes_cmux_backend() {
315
+ fm_backend_validate cmux 2>/dev/null || fail "fm_backend_validate should accept cmux"
316
+ pass "fm_backend_validate: cmux is a known backend"
317
+ }
318
+
319
+ test_dispatch_busy_state_unknown_for_cmux() {
320
+ # shellcheck source=/dev/null
321
+ . "$ROOT/bin/fm-backend.sh"
322
+ [ "$(fm_backend_busy_state cmux '11111111-1111-1111-1111-111111111111:22222222-2222-2222-2222-222222222222')" = unknown ] \
323
+ || fail "fm_backend_busy_state should report unknown for cmux (no native agent-state primitive)"
324
+ pass "fm_backend_busy_state: cmux (no native primitive) always reports unknown, same as tmux/zellij/orca"
325
+ }
326
+
327
+ test_dispatch_composer_state_routes_cmux() {
328
+ local dir fb out target
329
+ dir="$TMP_ROOT/dispatch-composer"; mkdir -p "$dir/responses"
330
+ target="aaaaaaaa-0000-0000-0000-000000000000:bbbbbbbb-1111-1111-1111-111111111111"
331
+ cmux_panes_response "$dir" 1 "bbbbbbbb-1111-1111-1111-111111111111"
332
+ cmux_read_screen_response "$dir" 2 $' ╭────────────────────────╮\n │ ❯ hello captain │\n ╰──────── Composer ──────╯'
333
+ fb=$(make_cmux_fakebin "$dir")
334
+ out=$( PATH="$fb:$PATH" FM_CMUX_LOG="$dir/log" FM_CMUX_RESPONSES="$dir/responses" \
335
+ bash -c '. "$0/bin/fm-backend.sh"; fm_backend_composer_state cmux "$1"' "$ROOT" "$target" )
336
+ [ "$out" = pending ] || fail "fm_backend_composer_state should route cmux to its classifier, got '$out'"
337
+ pass "fm_backend_composer_state: routes cmux to the cmux composer classifier"
338
+ }
339
+
340
+ # --- ping_state / ensure_running ---------------------------------------------
341
+
342
+ test_ping_state_ok() {
343
+ local dir fb out
344
+ dir="$TMP_ROOT/ping-ok"; mkdir -p "$dir/responses"
345
+ fb=$(make_cmux_fakebin "$dir")
346
+ out=$( PATH="$fb:$PATH" FM_CMUX_LOG="$dir/log" FM_CMUX_RESPONSES="$dir/responses" FM_CMUX_FAKE_PING=PONG \
347
+ bash -c '. "$0/bin/backends/cmux.sh"; fm_backend_cmux_ping_state' "$ROOT" )
348
+ [ "$out" = ok ] || fail "ping_state should report ok on PONG, got '$out'"
349
+ pass "fm_backend_cmux_ping_state: reports 'ok' on PONG"
350
+ }
351
+
352
+ test_ping_state_denied() {
353
+ local dir fb out
354
+ dir="$TMP_ROOT/ping-denied"; mkdir -p "$dir/responses"
355
+ fb=$(make_cmux_fakebin "$dir")
356
+ out=$( PATH="$fb:$PATH" FM_CMUX_LOG="$dir/log" FM_CMUX_RESPONSES="$dir/responses" FM_CMUX_FAKE_PING_EXIT=1 \
357
+ FM_CMUX_FAKE_PING="Error: ERROR: Access denied - only processes started inside cmux can connect" \
358
+ bash -c '. "$0/bin/backends/cmux.sh"; fm_backend_cmux_ping_state' "$ROOT" )
359
+ [ "$out" = denied ] || fail "ping_state should report denied on the cmuxOnly rejection text, got '$out'"
360
+ pass "fm_backend_cmux_ping_state: reports 'denied' when socketControlMode=cmuxOnly rejects the connection"
361
+ }
362
+
363
+ test_ping_state_unauth() {
364
+ local dir fb out
365
+ dir="$TMP_ROOT/ping-unauth"; mkdir -p "$dir/responses"
366
+ fb=$(make_cmux_fakebin "$dir")
367
+ out=$( PATH="$fb:$PATH" FM_CMUX_LOG="$dir/log" FM_CMUX_RESPONSES="$dir/responses" FM_CMUX_FAKE_PING_EXIT=1 \
368
+ FM_CMUX_FAKE_PING="Error: ERROR: Authentication required - send auth <password> first" \
369
+ bash -c '. "$0/bin/backends/cmux.sh"; fm_backend_cmux_ping_state' "$ROOT" )
370
+ [ "$out" = unauth ] || fail "ping_state should report unauth when no password was presented, got '$out'"
371
+ pass "fm_backend_cmux_ping_state: reports 'unauth' when password mode rejects a missing/wrong password"
372
+ }
373
+
374
+ test_ping_state_invalid_password() {
375
+ local dir fb out
376
+ dir="$TMP_ROOT/ping-invalid-pw"; mkdir -p "$dir/responses"
377
+ fb=$(make_cmux_fakebin "$dir")
378
+ out=$( PATH="$fb:$PATH" FM_CMUX_LOG="$dir/log" FM_CMUX_RESPONSES="$dir/responses" FM_CMUX_FAKE_PING_EXIT=1 \
379
+ FM_CMUX_FAKE_PING="Error: ERROR: Invalid password" \
380
+ bash -c '. "$0/bin/backends/cmux.sh"; fm_backend_cmux_ping_state' "$ROOT" )
381
+ [ "$out" = unauth ] || fail "ping_state should report unauth on the wrong-password rejection text, got '$out'"
382
+ pass "fm_backend_cmux_ping_state: reports 'unauth' when password mode rejects a wrong password (Invalid password)"
383
+ }
384
+
385
+ test_ping_state_down() {
386
+ local dir fb out
387
+ dir="$TMP_ROOT/ping-down"; mkdir -p "$dir/responses"
388
+ fb=$(make_cmux_fakebin "$dir")
389
+ out=$( PATH="$fb:$PATH" FM_CMUX_LOG="$dir/log" FM_CMUX_RESPONSES="$dir/responses" FM_CMUX_FAKE_PING_EXIT=1 \
390
+ FM_CMUX_FAKE_PING="Error: Socket not found at /home/x/.local/state/cmux/cmux.sock" \
391
+ bash -c '. "$0/bin/backends/cmux.sh"; fm_backend_cmux_ping_state' "$ROOT" )
392
+ [ "$out" = down ] || fail "ping_state should report down when the socket does not exist yet, got '$out'"
393
+ pass "fm_backend_cmux_ping_state: reports 'down' when the app is not running yet"
394
+ }
395
+
396
+ test_ensure_running_returns_immediately_when_already_ok() {
397
+ local dir fb status
398
+ dir="$TMP_ROOT/ensure-ok"; mkdir -p "$dir/responses"
399
+ fb=$(make_cmux_fakebin "$dir")
400
+ cat > "$fb/open" <<'SH'
401
+ #!/usr/bin/env bash
402
+ echo "open should not be called when cmux is already reachable" >&2
403
+ exit 1
404
+ SH
405
+ chmod +x "$fb/open"
406
+ PATH="$fb:$PATH" FM_CMUX_LOG="$dir/log" FM_CMUX_RESPONSES="$dir/responses" FM_CMUX_FAKE_PING=PONG \
407
+ bash -c '. "$0/bin/backends/cmux.sh"; fm_backend_cmux_ensure_running' "$ROOT"
408
+ status=$?
409
+ expect_code 0 "$status" "ensure_running should succeed immediately when already reachable"
410
+ pass "fm_backend_cmux_ensure_running: returns immediately when cmux is already reachable"
411
+ }
412
+
413
+ test_ensure_running_fails_fast_on_denied_without_launching() {
414
+ local dir fb out status
415
+ dir="$TMP_ROOT/ensure-denied"; mkdir -p "$dir/responses"
416
+ fb=$(make_cmux_fakebin "$dir")
417
+ cat > "$fb/open" <<'SH'
418
+ #!/usr/bin/env bash
419
+ echo "LAUNCHED" >> "${FM_CMUX_LAUNCH_MARKER:?}"
420
+ exit 0
421
+ SH
422
+ chmod +x "$fb/open"
423
+ out=$( PATH="$fb:$PATH" FM_CMUX_LOG="$dir/log" FM_CMUX_RESPONSES="$dir/responses" FM_CMUX_FAKE_PING_EXIT=1 \
424
+ FM_CMUX_FAKE_PING="Error: ERROR: Access denied - only processes started inside cmux can connect" \
425
+ FM_CMUX_LAUNCH_MARKER="$dir/launched" \
426
+ bash -c '. "$0/bin/backends/cmux.sh"; fm_backend_cmux_ensure_running' "$ROOT" 2>&1 )
427
+ status=$?
428
+ [ "$status" -ne 0 ] || fail "ensure_running should refuse when the socket is denied (relaunching cannot fix a config problem)"
429
+ [ ! -f "$dir/launched" ] || fail "ensure_running should not attempt to launch cmux on a denied socket"
430
+ assert_contains "$out" "docs/cmux-backend.md" "ensure_running's denied error did not point at the setup docs"
431
+ assert_contains "$out" "Automation mode" "ensure_running's denied error did not name the recommended Automation mode"
432
+ assert_contains "$out" "Password mode" "ensure_running's denied error did not name the Password mode alternative"
433
+ assert_contains "$out" "Full open access" "ensure_running's denied error did not name (and caveat) Full open access"
434
+ pass "fm_backend_cmux_ensure_running: fails fast on a denied socket without attempting to launch, naming every viable mode"
435
+ }
436
+
437
+ test_ensure_running_fails_fast_on_unauth_without_launching() {
438
+ local dir fb out status
439
+ dir="$TMP_ROOT/ensure-unauth"; mkdir -p "$dir/responses"
440
+ fb=$(make_cmux_fakebin "$dir")
441
+ cat > "$fb/open" <<'SH'
442
+ #!/usr/bin/env bash
443
+ echo "LAUNCHED" >> "${FM_CMUX_LAUNCH_MARKER:?}"
444
+ exit 0
445
+ SH
446
+ chmod +x "$fb/open"
447
+ out=$( PATH="$fb:$PATH" FM_CMUX_LOG="$dir/log" FM_CMUX_RESPONSES="$dir/responses" FM_CMUX_FAKE_PING_EXIT=1 \
448
+ FM_CMUX_FAKE_PING="Error: ERROR: Authentication required - send auth <password> first" \
449
+ FM_CMUX_LAUNCH_MARKER="$dir/launched" \
450
+ bash -c '. "$0/bin/backends/cmux.sh"; fm_backend_cmux_ensure_running' "$ROOT" 2>&1 )
451
+ status=$?
452
+ [ "$status" -ne 0 ] || fail "ensure_running should refuse when the socket is unauthenticated (relaunching cannot fix a password problem)"
453
+ [ ! -f "$dir/launched" ] || fail "ensure_running should not attempt to launch cmux on an unauthenticated socket"
454
+ assert_contains "$out" "config/cmux-socket-password" "ensure_running's unauth error did not name the password config file"
455
+ assert_contains "$out" "Automation mode" "ensure_running's unauth error did not name the recommended no-password Automation mode"
456
+ assert_contains "$out" "docs/cmux-backend.md" "ensure_running's unauth error did not point at the setup docs"
457
+ pass "fm_backend_cmux_ensure_running: fails fast on an unauthenticated socket, naming the password config and the Automation mode alternative"
458
+ }
459
+
460
+ # --- create_task: duplicate refusal, id resolution ---------------------------
461
+
462
+ test_create_task_refuses_duplicate_label() {
463
+ local dir fb out status title
464
+ dir="$TMP_ROOT/dup-task"; mkdir -p "$dir/responses"
465
+ title=$(cmux_expected_scoped_title fm-dup1)
466
+ cmux_workspace_list_response "$dir" 1 "aaaaaaaa-0000-0000-0000-000000000000" "$title"
467
+ fb=$(make_cmux_fakebin "$dir")
468
+ out=$( PATH="$fb:$PATH" FM_CMUX_LOG="$dir/log" FM_CMUX_RESPONSES="$dir/responses" \
469
+ bash -c '. "$0/bin/backends/cmux.sh"; fm_backend_cmux_create_task fm-dup1 /tmp/proj' "$ROOT" 2>&1 )
470
+ status=$?
471
+ [ "$status" -ne 0 ] || fail "create_task should refuse an existing workspace title (cmux itself does not enforce uniqueness)"
472
+ assert_contains "$out" "already exists" "create_task did not report the duplicate name"
473
+ pass "fm_backend_cmux_create_task: refuses a duplicate workspace title (cmux's own new-workspace has no uniqueness check)"
474
+ }
475
+
476
+ test_create_task_creates_and_parses_ids() {
477
+ local dir fb out title
478
+ dir="$TMP_ROOT/create-task"; mkdir -p "$dir/responses"
479
+ title=$(cmux_expected_scoped_title fm-newtask)
480
+ # 1: workspace list --json (pre-create duplicate check) -> no match
481
+ printf '{"workspaces":[]}' > "$dir/responses/1.out"
482
+ # 2: new-workspace (silent on success)
483
+ # 3: workspace list --json (post-create id resolution) -> match
484
+ cmux_workspace_list_response "$dir" 3 "bbbbbbbb-1111-1111-1111-111111111111" "$title"
485
+ # 4: list-panes --json --id-format uuids -> default surface id
486
+ cmux_panes_response "$dir" 4 "cccccccc-2222-2222-2222-222222222222"
487
+ fb=$(make_cmux_fakebin "$dir")
488
+ out=$( PATH="$fb:$PATH" FM_CMUX_LOG="$dir/log" FM_CMUX_RESPONSES="$dir/responses" \
489
+ bash -c '. "$0/bin/backends/cmux.sh"; fm_backend_cmux_create_task fm-newtask /tmp/proj' "$ROOT" )
490
+ [ "$out" = "bbbbbbbb-1111-1111-1111-111111111111 cccccccc-2222-2222-2222-222222222222" ] \
491
+ || fail "create_task should echo '<workspace_id> <surface_id>', got '$out'"
492
+ assert_contains "$(cat "$dir/log")" $'\x1f''new-workspace'$'\x1f''--name'$'\x1f'"$title"$'\x1f''--cwd'$'\x1f''/tmp/proj' \
493
+ "create_task did not call new-workspace with the right name/cwd"
494
+ assert_contains "$(cat "$dir/log")" $'\x1f''--focus'$'\x1f''false' \
495
+ "create_task did not pass --focus false"
496
+ pass "fm_backend_cmux_create_task: creates a workspace and parses workspace_id/surface_id from list responses"
497
+ }
498
+
499
+ # --- target_ready / capture ---------------------------------------------------
500
+
501
+ test_target_ready_fails_when_target_absent() {
502
+ local dir fb status
503
+ dir="$TMP_ROOT/ready-absent"; mkdir -p "$dir/responses"
504
+ # 1: list-panes --json --id-format uuids -> no panes at all (surface absent)
505
+ cmux_panes_empty_response "$dir" 1
506
+ fb=$(make_cmux_fakebin "$dir")
507
+ PATH="$fb:$PATH" FM_CMUX_LOG="$dir/log" FM_CMUX_RESPONSES="$dir/responses" \
508
+ bash -c '. "$0/bin/backends/cmux.sh"; fm_backend_cmux_target_ready "aaaaaaaa-0000-0000-0000-000000000000:bbbbbbbb-1111-1111-1111-111111111111"' "$ROOT"
509
+ status=$?
510
+ [ "$status" -ne 0 ] || fail "target_ready should fail when list-panes reports the surface not found"
511
+ pass "fm_backend_cmux_target_ready: fails when the workspace/surface is not found (list-panes structural check)"
512
+ }
513
+
514
+ test_target_ready_checks_expected_label() {
515
+ local dir fb title
516
+ dir="$TMP_ROOT/ready-label-ok"; mkdir -p "$dir/responses"
517
+ title=$(cmux_expected_scoped_title fm-label)
518
+ cmux_workspace_list_response "$dir" 1 "aaaaaaaa-0000-0000-0000-000000000000" "$title"
519
+ # 2: list-panes --json --id-format uuids -> matching surface
520
+ cmux_panes_response "$dir" 2 "bbbbbbbb-1111-1111-1111-111111111111"
521
+ fb=$(make_cmux_fakebin "$dir")
522
+ PATH="$fb:$PATH" FM_CMUX_LOG="$dir/log" FM_CMUX_RESPONSES="$dir/responses" \
523
+ bash -c '. "$0/bin/backends/cmux.sh"; fm_backend_cmux_target_ready "aaaaaaaa-0000-0000-0000-000000000000:bbbbbbbb-1111-1111-1111-111111111111" fm-label' "$ROOT"
524
+ expect_code 0 $? "target_ready should succeed when the workspace title matches the expected label"
525
+ cmux_assert_call_order "$dir/log" $'\x1f''workspace'$'\x1f''list' $'\x1f''list-panes' \
526
+ "target_ready did not check the label before list-panes"
527
+ pass "fm_backend_cmux_target_ready: verifies the workspace title against the expected label first"
528
+ }
529
+
530
+ test_target_ready_rejects_label_mismatch() {
531
+ local dir fb status
532
+ dir="$TMP_ROOT/ready-label-mismatch"; mkdir -p "$dir/responses"
533
+ cmux_workspace_list_response "$dir" 1 "aaaaaaaa-0000-0000-0000-000000000000" "not-the-task"
534
+ fb=$(make_cmux_fakebin "$dir")
535
+ PATH="$fb:$PATH" FM_CMUX_LOG="$dir/log" FM_CMUX_RESPONSES="$dir/responses" \
536
+ bash -c '. "$0/bin/backends/cmux.sh"; fm_backend_cmux_target_ready "aaaaaaaa-0000-0000-0000-000000000000:bbbbbbbb-1111-1111-1111-111111111111" fm-label' "$ROOT"
537
+ status=$?
538
+ [ "$status" -ne 0 ] || fail "target_ready should reject a workspace whose title does not match the expected label"
539
+ assert_not_contains "$(cat "$dir/log")" $'\x1f''list-panes' \
540
+ "target_ready should not call list-panes after a label mismatch"
541
+ pass "fm_backend_cmux_target_ready: rejects a workspace id reused under a different title"
542
+ }
543
+
544
+ test_capture_trims_locally() {
545
+ local dir fb out
546
+ dir="$TMP_ROOT/capture"; mkdir -p "$dir/responses"
547
+ # 1: list-panes --json --id-format uuids (target_ready)
548
+ cmux_panes_response "$dir" 1 "bbbbbbbb-1111-1111-1111-111111111111"
549
+ # 2: read-screen --scrollback --lines 200 --json (actual fetch)
550
+ cmux_read_screen_response "$dir" 2 $'line one\nline two\nline three\nline four'
551
+ fb=$(make_cmux_fakebin "$dir")
552
+ out=$( PATH="$fb:$PATH" FM_CMUX_LOG="$dir/log" FM_CMUX_RESPONSES="$dir/responses" \
553
+ bash -c '. "$0/bin/backends/cmux.sh"; fm_backend_cmux_capture "aaaaaaaa-0000-0000-0000-000000000000:bbbbbbbb-1111-1111-1111-111111111111" 2' "$ROOT" )
554
+ [ "$out" = $'line three\nline four' ] || fail "capture should trim to the last N lines locally, got '$out'"
555
+ cmux_assert_call_order "$dir/log" $'\x1f''list-panes'$'\x1f''--workspace'$'\x1f''aaaaaaaa-0000-0000-0000-000000000000' \
556
+ $'\x1f''--scrollback' "capture did not verify readiness before the actual read"
557
+ pass "fm_backend_cmux_capture: fetches generously and trims to N lines locally"
558
+ }
559
+
560
+ test_capture_fails_when_read_screen_fails_empty() {
561
+ local dir fb status
562
+ dir="$TMP_ROOT/capture-read-fail"; mkdir -p "$dir/responses"
563
+ # 1: list-panes --json --id-format uuids (target_ready)
564
+ cmux_panes_response "$dir" 1 "bbbbbbbb-1111-1111-1111-111111111111"
565
+ # 2: read-screen exits nonzero with no stdout
566
+ printf '1' > "$dir/responses/2.exit"
567
+ fb=$(make_cmux_fakebin "$dir")
568
+ PATH="$fb:$PATH" FM_CMUX_LOG="$dir/log" FM_CMUX_RESPONSES="$dir/responses" \
569
+ bash -c '. "$0/bin/backends/cmux.sh"; fm_backend_cmux_capture "aaaaaaaa-0000-0000-0000-000000000000:bbbbbbbb-1111-1111-1111-111111111111" 5' "$ROOT"
570
+ status=$?
571
+ [ "$status" -ne 0 ] || fail "capture should fail when read-screen exits nonzero with no stdout"
572
+ assert_contains "$(cat "$dir/log")" $'\x1f''read-screen' \
573
+ "capture should attempt read-screen after readiness succeeds"
574
+ pass "fm_backend_cmux_capture: propagates a read-screen failure even when stdout is empty"
575
+ }
576
+
577
+ test_capture_fails_when_target_not_ready() {
578
+ local dir fb status
579
+ dir="$TMP_ROOT/capture-not-ready"; mkdir -p "$dir/responses"
580
+ # 1: list-panes --json --id-format uuids -> no matching surface
581
+ cmux_panes_empty_response "$dir" 1
582
+ fb=$(make_cmux_fakebin "$dir")
583
+ PATH="$fb:$PATH" FM_CMUX_LOG="$dir/log" FM_CMUX_RESPONSES="$dir/responses" \
584
+ bash -c '. "$0/bin/backends/cmux.sh"; fm_backend_cmux_capture "aaaaaaaa-0000-0000-0000-000000000000:bbbbbbbb-1111-1111-1111-111111111111" 5' "$ROOT"
585
+ status=$?
586
+ [ "$status" -ne 0 ] || fail "capture should fail when the target is not ready"
587
+ assert_not_contains "$(cat "$dir/log")" $'\x1f''--scrollback' \
588
+ "capture should not fetch after readiness fails"
589
+ pass "fm_backend_cmux_capture: fails when the target surface is absent"
590
+ }
591
+
592
+ # --- send_key / send_literal --------------------------------------------------
593
+
594
+ test_send_key_normalizes_and_targets() {
595
+ local dir fb
596
+ dir="$TMP_ROOT/sendkey"; mkdir -p "$dir/responses"
597
+ # 1: list-panes --json --id-format uuids (target_ready)
598
+ cmux_panes_response "$dir" 1 "bbbbbbbb-1111-1111-1111-111111111111"
599
+ fb=$(make_cmux_fakebin "$dir")
600
+ PATH="$fb:$PATH" FM_CMUX_LOG="$dir/log" FM_CMUX_RESPONSES="$dir/responses" \
601
+ bash -c '. "$0/bin/backends/cmux.sh"; fm_backend_cmux_send_key "aaaaaaaa-0000-0000-0000-000000000000:bbbbbbbb-1111-1111-1111-111111111111" Escape' "$ROOT"
602
+ expect_code 0 $? "send_key should succeed"
603
+ assert_contains "$(cat "$dir/log")" $'\x1f''send-key'$'\x1f''--workspace'$'\x1f''aaaaaaaa-0000-0000-0000-000000000000'$'\x1f''--surface'$'\x1f''bbbbbbbb-1111-1111-1111-111111111111'$'\x1f''escape' \
604
+ "send_key did not normalize Escape to escape and target the explicit workspace/surface"
605
+ pass "fm_backend_cmux_send_key: normalizes the key (Escape -> escape) and targets the explicit workspace/surface"
606
+ }
607
+
608
+ test_send_key_recovers_stale_target_by_label() {
609
+ local dir fb title
610
+ dir="$TMP_ROOT/sendkey-stale-target"; mkdir -p "$dir/responses"
611
+ title=$(cmux_expected_scoped_title fm-label)
612
+ cmux_workspace_list_response "$dir" 1 "cccccccc-2222-2222-2222-222222222222" "$title"
613
+ cmux_workspace_list_response "$dir" 2 "cccccccc-2222-2222-2222-222222222222" "$title"
614
+ cmux_panes_response "$dir" 3 "dddddddd-3333-3333-3333-333333333333"
615
+ fb=$(make_cmux_fakebin "$dir")
616
+ PATH="$fb:$PATH" FM_CMUX_LOG="$dir/log" FM_CMUX_RESPONSES="$dir/responses" \
617
+ bash -c '. "$0/bin/backends/cmux.sh"; fm_backend_cmux_send_key "aaaaaaaa-0000-0000-0000-000000000000:bbbbbbbb-1111-1111-1111-111111111111" Enter fm-label' "$ROOT"
618
+ expect_code 0 $? "send_key should recover a stale cmux target when the expected label is live"
619
+ assert_contains "$(cat "$dir/log")" $'\x1f''send-key'$'\x1f''--workspace'$'\x1f''cccccccc-2222-2222-2222-222222222222'$'\x1f''--surface'$'\x1f''dddddddd-3333-3333-3333-333333333333'$'\x1f''enter' \
620
+ "send_key did not use the refreshed cmux workspace/surface ids"
621
+ assert_not_contains "$(cat "$dir/log")" $'\x1f''send-key'$'\x1f''--workspace'$'\x1f''aaaaaaaa-0000-0000-0000-000000000000' \
622
+ "send_key should not target the stale cmux workspace id after label recovery"
623
+ pass "fm_backend_cmux_send_key: recovers stale workspace/surface ids by expected label"
624
+ }
625
+
626
+ test_send_literal_uses_separator_for_option_shaped_text() {
627
+ local dir fb
628
+ dir="$TMP_ROOT/sendliteral"; mkdir -p "$dir/responses"
629
+ # 1: list-panes --json --id-format uuids (target_ready)
630
+ cmux_panes_response "$dir" 1 "bbbbbbbb-1111-1111-1111-111111111111"
631
+ fb=$(make_cmux_fakebin "$dir")
632
+ PATH="$fb:$PATH" FM_CMUX_LOG="$dir/log" FM_CMUX_RESPONSES="$dir/responses" \
633
+ bash -c '. "$0/bin/backends/cmux.sh"; fm_backend_cmux_send_literal "aaaaaaaa-0000-0000-0000-000000000000:bbbbbbbb-1111-1111-1111-111111111111" "--help"' "$ROOT"
634
+ expect_code 0 $? "send_literal should succeed"
635
+ assert_contains "$(cat "$dir/log")" $'\x1f''send'$'\x1f''--workspace'$'\x1f''aaaaaaaa-0000-0000-0000-000000000000'$'\x1f''--surface'$'\x1f''bbbbbbbb-1111-1111-1111-111111111111'$'\x1f''--'$'\x1f''--help' \
636
+ "send_literal did not call send with a -- separator before the literal payload"
637
+ pass "fm_backend_cmux_send_literal: calls send with an explicit workspace/surface and a -- separator"
638
+ }
639
+
640
+ test_send_text_line_clears_partial_input_when_enter_fails() {
641
+ local dir fb status log
642
+ dir="$TMP_ROOT/sendline-enter-failure"; mkdir -p "$dir/responses"
643
+ cmux_panes_response "$dir" 1 "bbbbbbbb-1111-1111-1111-111111111111"
644
+ cmux_panes_response "$dir" 3 "bbbbbbbb-1111-1111-1111-111111111111"
645
+ printf '1\n' > "$dir/responses/4.exit"
646
+ cmux_panes_response "$dir" 5 "bbbbbbbb-1111-1111-1111-111111111111"
647
+ fb=$(make_cmux_fakebin "$dir")
648
+
649
+ PATH="$fb:$PATH" FM_CMUX_LOG="$dir/log" FM_CMUX_RESPONSES="$dir/responses" \
650
+ bash -c '. "$0/bin/backends/cmux.sh"; fm_backend_cmux_send_text_line "aaaaaaaa-0000-0000-0000-000000000000:bbbbbbbb-1111-1111-1111-111111111111" "export TRACEPARENT=carrier"' "$ROOT"
651
+ status=$?
652
+ [ "$status" -ne 0 ] || fail "send_text_line should report a failed Enter"
653
+ log=$(cat "$dir/log")
654
+ assert_contains "$log" $'\x1f''send'$'\x1f''--workspace'$'\x1f''aaaaaaaa-0000-0000-0000-000000000000'$'\x1f''--surface'$'\x1f''bbbbbbbb-1111-1111-1111-111111111111'$'\x1f''--'$'\x1f''export TRACEPARENT=carrier' \
655
+ "send_text_line did not send the trace export before the simulated Enter failure"
656
+ assert_contains "$log" $'\x1f''send-key'$'\x1f''--workspace'$'\x1f''aaaaaaaa-0000-0000-0000-000000000000'$'\x1f''--surface'$'\x1f''bbbbbbbb-1111-1111-1111-111111111111'$'\x1f''ctrl-c' \
657
+ "send_text_line did not clear the partial input after Enter failed"
658
+ pass "fm_backend_cmux_send_text_line: clears partial input when Enter fails"
659
+ }
660
+
661
+ test_send_text_line_reports_unsafe_input_when_cleanup_fails() {
662
+ local dir fb status log
663
+ dir="$TMP_ROOT/sendline-cleanup-failure"; mkdir -p "$dir/responses"
664
+ cmux_panes_response "$dir" 1 "bbbbbbbb-1111-1111-1111-111111111111"
665
+ cmux_panes_response "$dir" 3 "bbbbbbbb-1111-1111-1111-111111111111"
666
+ printf '1\n' > "$dir/responses/4.exit"
667
+ cmux_panes_response "$dir" 5 "bbbbbbbb-1111-1111-1111-111111111111"
668
+ printf '1\n' > "$dir/responses/6.exit"
669
+ fb=$(make_cmux_fakebin "$dir")
670
+
671
+ PATH="$fb:$PATH" FM_CMUX_LOG="$dir/log" FM_CMUX_RESPONSES="$dir/responses" \
672
+ bash -c '. "$0/bin/backends/cmux.sh"; fm_backend_cmux_send_text_line "aaaaaaaa-0000-0000-0000-000000000000:bbbbbbbb-1111-1111-1111-111111111111" "export TRACEPARENT=carrier"' "$ROOT"
673
+ status=$?
674
+ expect_code 2 "$status" "send_text_line should distinguish uncleared input"
675
+ log=$(cat "$dir/log")
676
+ assert_contains "$log" $'\x1f''send-key'$'\x1f''--workspace'$'\x1f''aaaaaaaa-0000-0000-0000-000000000000'$'\x1f''--surface'$'\x1f''bbbbbbbb-1111-1111-1111-111111111111'$'\x1f''ctrl-c' \
677
+ "send_text_line did not attempt cleanup after Enter failed"
678
+ pass "fm_backend_cmux_send_text_line: reports unsafe input when cleanup also fails"
679
+ }
680
+
681
+ # --- current_path: pwd-marker-probe (zellij-shape) ---------------------------
682
+
683
+ test_current_path_probes_with_marker() {
684
+ local dir fb out
685
+ # Verified real-cmux pitfall (docs/cmux-backend.md finding #2): the surface's
686
+ # cwd is frozen at creation time (the top-level shell's cwd), never following
687
+ # a foreground subshell (e.g. treehouse get) - so current_path actively
688
+ # prints a marked cwd line and reads only that marker from the capture.
689
+ dir="$TMP_ROOT/cwd"; mkdir -p "$dir/responses"
690
+ # 1: list-panes (current_path's own target_ready)
691
+ # 2: list-panes (target_ready, called by send_text_line->send_literal)
692
+ # 3: send (literal probe text)
693
+ # 4: list-panes (target_ready, called by send_text_line->send_key)
694
+ # 5: send-key enter
695
+ # 6: list-panes (target_ready, called by capture)
696
+ # 7: read-screen --scrollback --lines 200 --json (actual fetch)
697
+ cmux_panes_response "$dir" 1 "bbbbbbbb-1111-1111-1111-111111111111"
698
+ cmux_panes_response "$dir" 2 "bbbbbbbb-1111-1111-1111-111111111111"
699
+ cmux_panes_response "$dir" 4 "bbbbbbbb-1111-1111-1111-111111111111"
700
+ cmux_panes_response "$dir" 6 "bbbbbbbb-1111-1111-1111-111111111111"
701
+ cmux_read_screen_response "$dir" 7 $'/tmp/proj\n❯ printf marker\n__FM_CMUX_CWD_BEGIN__\n/home/fixture/.treehouse/fake-worktree\n__FM_CMUX_CWD_END__\n/home/fixture/.treehouse/fake-worktree ❯'
702
+ fb=$(make_cmux_fakebin "$dir")
703
+ out=$( PATH="$fb:$PATH" FM_CMUX_LOG="$dir/log" FM_CMUX_RESPONSES="$dir/responses" \
704
+ bash -c '. "$0/bin/backends/cmux.sh"; fm_backend_cmux_current_path "aaaaaaaa-0000-0000-0000-000000000000:bbbbbbbb-1111-1111-1111-111111111111"' "$ROOT" )
705
+ [ "$out" = "/home/fixture/.treehouse/fake-worktree" ] || fail "current_path should read only the marked cwd line, got '$out'"
706
+ assert_contains "$(cat "$dir/log")" "__FM_CMUX_CWD_BEGIN__" "current_path did not send the cwd begin marker"
707
+ assert_contains "$(cat "$dir/log")" "pwd;" "current_path did not send the pwd probe"
708
+ assert_contains "$(cat "$dir/log")" $'\x1f''send-key'$'\x1f''--workspace'$'\x1f''aaaaaaaa-0000-0000-0000-000000000000'$'\x1f''--surface'$'\x1f''bbbbbbbb-1111-1111-1111-111111111111'$'\x1f''enter' \
709
+ "current_path did not submit the cwd probe with Enter"
710
+ pass "fm_backend_cmux_current_path: actively probes with marked begin/end lines (zellij-shape frozen cwd)"
711
+ }
712
+
713
+ # --- composer_state: structural border-row classification (adapted from herdr) ----
714
+
715
+ test_composer_state_bare_prompt_is_empty() {
716
+ local dir fb out
717
+ dir="$TMP_ROOT/composer-bare"; mkdir -p "$dir/responses"
718
+ # 1: list-panes (target_ready via capture)
719
+ # 2: read-screen --scrollback --lines <N> --json (composer capture)
720
+ cmux_panes_response "$dir" 1 "bbbbbbbb-1111-1111-1111-111111111111"
721
+ cmux_read_screen_response "$dir" 2 $' ╭────────────────────────╮\n │ ❯ │\n ╰──────── Composer ──────╯\n\n Enter:send'
722
+ fb=$(make_cmux_fakebin "$dir")
723
+ out=$( PATH="$fb:$PATH" FM_CMUX_LOG="$dir/log" FM_CMUX_RESPONSES="$dir/responses" \
724
+ bash -c '. "$0/bin/backends/cmux.sh"; fm_backend_cmux_composer_state "aaaaaaaa-0000-0000-0000-000000000000:bbbbbbbb-1111-1111-1111-111111111111"' "$ROOT" )
725
+ [ "$out" = empty ] || fail "a bare prompt glyph should read as empty, got '$out'"
726
+ pass "fm_backend_cmux_composer_state: a bare '❯' composer row reads empty"
727
+ }
728
+
729
+ test_composer_state_borderless_claude_prompt_is_empty() {
730
+ local dir fb out
731
+ dir="$TMP_ROOT/composer-borderless-claude"; mkdir -p "$dir/responses"
732
+ cmux_panes_response "$dir" 1 "bbbbbbbb-1111-1111-1111-111111111111"
733
+ cmux_read_screen_response "$dir" 2 $'────────────────────────\n❯\n────────────────────────\nHaiku 4.5'
734
+ fb=$(make_cmux_fakebin "$dir")
735
+ out=$( PATH="$fb:$PATH" FM_CMUX_LOG="$dir/log" FM_CMUX_RESPONSES="$dir/responses" \
736
+ bash -c '. "$0/bin/backends/cmux.sh"; fm_backend_cmux_composer_state "aaaaaaaa-0000-0000-0000-000000000000:bbbbbbbb-1111-1111-1111-111111111111"' "$ROOT" )
737
+ [ "$out" = empty ] || fail "a borderless Claude '❯' row bounded by horizontal rules should read empty, got '$out'"
738
+ pass "fm_backend_cmux_composer_state: a borderless Claude '❯' composer row reads empty"
739
+ }
740
+
741
+ test_composer_state_borderless_claude_prompt_outranks_stale_bordered_row() {
742
+ local dir fb out
743
+ dir="$TMP_ROOT/composer-borderless-claude-after-bordered"; mkdir -p "$dir/responses"
744
+ cmux_panes_response "$dir" 1 "bbbbbbbb-1111-1111-1111-111111111111"
745
+ cmux_read_screen_response "$dir" 2 $'│ ❯ stale input │\n────────────────────────\n❯\n────────────────────────\nHaiku 4.5'
746
+ fb=$(make_cmux_fakebin "$dir")
747
+ out=$( PATH="$fb:$PATH" FM_CMUX_LOG="$dir/log" FM_CMUX_RESPONSES="$dir/responses" \
748
+ bash -c '. "$0/bin/backends/cmux.sh"; fm_backend_cmux_composer_state "aaaaaaaa-0000-0000-0000-000000000000:bbbbbbbb-1111-1111-1111-111111111111"' "$ROOT" )
749
+ [ "$out" = empty ] || fail "a current borderless Claude row should outrank stale bordered scrollback, got '$out'"
750
+ pass "fm_backend_cmux_composer_state: a borderless Claude row outranks stale bordered scrollback"
751
+ }
752
+
753
+ test_composer_state_borderless_claude_nbsp_prompt_is_empty() {
754
+ local dir fb out
755
+ dir="$TMP_ROOT/composer-borderless-claude-nbsp"; mkdir -p "$dir/responses"
756
+ cmux_panes_response "$dir" 1 "bbbbbbbb-1111-1111-1111-111111111111"
757
+ cmux_read_screen_response "$dir" 2 $'────────────────────────\n❯\302\240\n────────────────────────\nHaiku 4.5'
758
+ fb=$(make_cmux_fakebin "$dir")
759
+ out=$( LC_ALL=C PATH="$fb:$PATH" FM_CMUX_LOG="$dir/log" FM_CMUX_RESPONSES="$dir/responses" \
760
+ bash -c '. "$0/bin/backends/cmux.sh"; fm_backend_cmux_composer_state "aaaaaaaa-0000-0000-0000-000000000000:bbbbbbbb-1111-1111-1111-111111111111"' "$ROOT" )
761
+ [ "$out" = empty ] || fail "a borderless Claude '❯'+NBSP row bounded by horizontal rules should read empty under LC_ALL=C, got '$out'"
762
+ pass "fm_backend_cmux_composer_state: a borderless Claude '❯'+NBSP composer row reads empty under LC_ALL=C"
763
+ }
764
+
765
+ test_composer_state_borderless_claude_text_is_unknown_plain() {
766
+ # Capability degradation (the consolidated classifier's styled=0 rule): on
767
+ # cmux's plain-text capture, text after a bare agent glyph is unreadable -
768
+ # it may be the harness's own idle suggestion (claude's rotating dim hint,
769
+ # codex's "Use /skills ..."), which a plain read cannot tell from typed
770
+ # input. The verdict is `unknown` (defer, loud refusal at fm-send), never a
771
+ # false `pending` that would misreport an idle pane as holding unsent text.
772
+ # The same bytes on a styled backend (tmux/herdr/zellij) classify pending
773
+ # when bright and empty when ghost - pinned in tests/fm-composer-lib.test.sh.
774
+ local dir fb out
775
+ dir="$TMP_ROOT/composer-borderless-claude-text"; mkdir -p "$dir/responses"
776
+ cmux_panes_response "$dir" 1 "bbbbbbbb-1111-1111-1111-111111111111"
777
+ cmux_read_screen_response "$dir" 2 $'────────────────────────\n❯ retain this message\n────────────────────────\nHaiku 4.5'
778
+ fb=$(make_cmux_fakebin "$dir")
779
+ out=$( PATH="$fb:$PATH" FM_CMUX_LOG="$dir/log" FM_CMUX_RESPONSES="$dir/responses" \
780
+ bash -c '. "$0/bin/backends/cmux.sh"; fm_backend_cmux_composer_state "aaaaaaaa-0000-0000-0000-000000000000:bbbbbbbb-1111-1111-1111-111111111111"' "$ROOT" )
781
+ [ "$out" = unknown ] || fail "plain-capture text after a bare glyph must degrade to unknown, got '$out'"
782
+ pass "fm_backend_cmux_composer_state: plain-capture text after a bare glyph degrades to unknown (never false pending)"
783
+ }
784
+
785
+ test_composer_state_ghost_placeholder_is_empty() {
786
+ local dir fb out
787
+ dir="$TMP_ROOT/composer-ghost"; mkdir -p "$dir/responses"
788
+ cmux_panes_response "$dir" 1 "bbbbbbbb-1111-1111-1111-111111111111"
789
+ cmux_read_screen_response "$dir" 2 $' ╭────────────────────────╮\n │ ❯ Type a message... │\n ╰──────── Composer ──────╯'
790
+ fb=$(make_cmux_fakebin "$dir")
791
+ out=$( PATH="$fb:$PATH" FM_CMUX_LOG="$dir/log" FM_CMUX_RESPONSES="$dir/responses" \
792
+ bash -c '. "$0/bin/backends/cmux.sh"; fm_backend_cmux_composer_state "aaaaaaaa-0000-0000-0000-000000000000:bbbbbbbb-1111-1111-1111-111111111111"' "$ROOT" )
793
+ [ "$out" = empty ] || fail "the known ghost placeholder 'Type a message...' should read as empty, got '$out'"
794
+ pass "fm_backend_cmux_composer_state: the ghost placeholder text reads empty, not pending"
795
+ }
796
+
797
+ test_composer_state_real_text_is_pending() {
798
+ local dir fb out
799
+ dir="$TMP_ROOT/composer-pending"; mkdir -p "$dir/responses"
800
+ cmux_panes_response "$dir" 1 "bbbbbbbb-1111-1111-1111-111111111111"
801
+ cmux_read_screen_response "$dir" 2 $' ╭────────────────────────╮\n │ ❯ hello captain │\n ╰──────── Composer ──────╯\n\n Enter:send'
802
+ fb=$(make_cmux_fakebin "$dir")
803
+ out=$( PATH="$fb:$PATH" FM_CMUX_LOG="$dir/log" FM_CMUX_RESPONSES="$dir/responses" \
804
+ bash -c '. "$0/bin/backends/cmux.sh"; fm_backend_cmux_composer_state "aaaaaaaa-0000-0000-0000-000000000000:bbbbbbbb-1111-1111-1111-111111111111"' "$ROOT" )
805
+ [ "$out" = pending ] || fail "real unsubmitted text should read as pending, got '$out'"
806
+ pass "fm_backend_cmux_composer_state: real composer text reads pending"
807
+ }
808
+
809
+ # The popup-placeholder/second-Enter regression class (2026-07-03 herdr
810
+ # incident, docs/herdr-backend.md): a slash command's first Enter can close a
811
+ # completion popup and EXPAND the composer into an argument-hint placeholder
812
+ # rather than submitting. A raw content-diff check would misread the popup
813
+ # vanishing as "submitted"; the structural composer-row read must still call
814
+ # this pending so the caller retries a genuine second Enter.
815
+ test_composer_state_popup_placeholder_fill_is_pending() {
816
+ local dir fb out
817
+ dir="$TMP_ROOT/composer-popup-placeholder"; mkdir -p "$dir/responses"
818
+ cmux_panes_response "$dir" 1 "bbbbbbbb-1111-1111-1111-111111111111"
819
+ cmux_read_screen_response "$dir" 2 $' ╭──────────────────────────────────────╮\n │ ❯ /compact compaction instructions │\n ╰──────────────── Composer ────────────╯\n\n Enter:send'
820
+ fb=$(make_cmux_fakebin "$dir")
821
+ out=$( PATH="$fb:$PATH" FM_CMUX_LOG="$dir/log" FM_CMUX_RESPONSES="$dir/responses" \
822
+ bash -c '. "$0/bin/backends/cmux.sh"; fm_backend_cmux_composer_state "aaaaaaaa-0000-0000-0000-000000000000:bbbbbbbb-1111-1111-1111-111111111111"' "$ROOT" )
823
+ [ "$out" = pending ] || fail "a popup-close-with-placeholder-fill must still read as pending (not yet submitted), got '$out'"
824
+ pass "fm_backend_cmux_composer_state: a slash-command popup's argument-hint placeholder still reads pending (the incident fix)"
825
+ }
826
+
827
+ test_composer_state_unknown_on_capture_failure() {
828
+ local dir fb out status
829
+ dir="$TMP_ROOT/composer-capture-fail"; mkdir -p "$dir/responses"
830
+ printf '1\n' > "$dir/responses/1.exit"
831
+ fb=$(make_cmux_fakebin "$dir")
832
+ out=$( PATH="$fb:$PATH" FM_CMUX_LOG="$dir/log" FM_CMUX_RESPONSES="$dir/responses" \
833
+ bash -c '. "$0/bin/backends/cmux.sh"; fm_backend_cmux_composer_state "aaaaaaaa-0000-0000-0000-000000000000:bbbbbbbb-1111-1111-1111-111111111111"' "$ROOT" )
834
+ status=$?
835
+ [ "$status" -eq 0 ] || fail "composer_state should not itself fail the caller"
836
+ [ "$out" = unknown ] || fail "an unreadable surface should read as unknown, got '$out'"
837
+ pass "fm_backend_cmux_composer_state: reports unknown when the surface cannot be captured"
838
+ }
839
+
840
+ test_composer_state_unknown_when_no_composer_row_found() {
841
+ local dir fb out
842
+ dir="$TMP_ROOT/composer-no-row"; mkdir -p "$dir/responses"
843
+ cmux_panes_response "$dir" 1 "bbbbbbbb-1111-1111-1111-111111111111"
844
+ cmux_read_screen_response "$dir" 2 'plain-shell-prompt$ '
845
+ fb=$(make_cmux_fakebin "$dir")
846
+ out=$( PATH="$fb:$PATH" FM_CMUX_LOG="$dir/log" FM_CMUX_RESPONSES="$dir/responses" \
847
+ bash -c '. "$0/bin/backends/cmux.sh"; fm_backend_cmux_composer_state "aaaaaaaa-0000-0000-0000-000000000000:bbbbbbbb-1111-1111-1111-111111111111"' "$ROOT" )
848
+ [ "$out" = unknown ] || fail "a capture with no recognizable composer row should read as unknown, got '$out'"
849
+ pass "fm_backend_cmux_composer_state: reports unknown when no border-delimited composer row is found"
850
+ }
851
+
852
+ # --- send_text_submit: structural composer-row verify-and-retry --------------
853
+
854
+ test_send_text_submit_detects_landed_send() {
855
+ local dir fb out
856
+ dir="$TMP_ROOT/submit-ok"; mkdir -p "$dir/responses"
857
+ # 1: list-panes (target_ready via send_literal)
858
+ # 2: send (literal text)
859
+ # 3: list-panes (target_ready via send_key Enter)
860
+ # 4: send-key enter
861
+ # 5: list-panes (target_ready via composer_state's capture)
862
+ # 6: read-screen --scrollback --lines N --json -> composer reads empty (submitted)
863
+ cmux_panes_response "$dir" 1 "bbbbbbbb-1111-1111-1111-111111111111"
864
+ cmux_panes_response "$dir" 3 "bbbbbbbb-1111-1111-1111-111111111111"
865
+ cmux_panes_response "$dir" 5 "bbbbbbbb-1111-1111-1111-111111111111"
866
+ cmux_read_screen_response "$dir" 6 $' ╭────────────────────────╮\n │ ❯ │\n ╰──────── Composer ──────╯'
867
+ fb=$(make_cmux_fakebin "$dir")
868
+ out=$( PATH="$fb:$PATH" FM_CMUX_LOG="$dir/log" FM_CMUX_RESPONSES="$dir/responses" \
869
+ bash -c '. "$0/bin/backends/cmux.sh"; fm_backend_cmux_send_text_submit "aaaaaaaa-0000-0000-0000-000000000000:bbbbbbbb-1111-1111-1111-111111111111" "hello captain" 3 0.01 0.01' "$ROOT" )
870
+ [ "$out" = empty ] || fail "send_text_submit should report empty (submitted) once the composer row reads empty, got '$out'"
871
+ assert_contains "$(cat "$dir/log")" $'\x1f''send'$'\x1f''--workspace'$'\x1f''aaaaaaaa-0000-0000-0000-000000000000'$'\x1f''--surface'$'\x1f''bbbbbbbb-1111-1111-1111-111111111111'$'\x1f''--'$'\x1f''hello captain' \
872
+ "send_text_submit did not type the literal text first"
873
+ enter_count=$(grep -c $'\x1f''send-key'$'\x1f''--workspace'$'\x1f''aaaaaaaa-0000-0000-0000-000000000000'$'\x1f''--surface'$'\x1f''bbbbbbbb-1111-1111-1111-111111111111'$'\x1f''enter' "$dir/log")
874
+ [ "$enter_count" -eq 1 ] || fail "send_text_submit should not need a second Enter for a plain message with no popup, sent $enter_count Enter(s)"
875
+ pass "fm_backend_cmux_send_text_submit: reports 'empty' once the composer row reads empty after one Enter"
876
+ }
877
+
878
+ test_send_text_submit_detects_swallowed_enter() {
879
+ local dir fb out
880
+ dir="$TMP_ROOT/submit-swallow"; mkdir -p "$dir/responses"
881
+ cmux_panes_response "$dir" 1 "bbbbbbbb-1111-1111-1111-111111111111"
882
+ cmux_panes_response "$dir" 3 "bbbbbbbb-1111-1111-1111-111111111111"
883
+ cmux_panes_response "$dir" 5 "bbbbbbbb-1111-1111-1111-111111111111"
884
+ cmux_panes_response "$dir" 7 "bbbbbbbb-1111-1111-1111-111111111111"
885
+ cmux_panes_response "$dir" 9 "bbbbbbbb-1111-1111-1111-111111111111"
886
+ cmux_read_screen_response "$dir" 6 $' ╭────────────────────────╮\n │ ❯ hello captain │\n ╰──────── Composer ──────╯\n\n Enter:send'
887
+ cmux_read_screen_response "$dir" 10 $' ╭────────────────────────╮\n │ ❯ hello captain │\n ╰──────── Composer ──────╯\n\n Enter:send'
888
+ fb=$(make_cmux_fakebin "$dir")
889
+ out=$( PATH="$fb:$PATH" FM_CMUX_LOG="$dir/log" FM_CMUX_RESPONSES="$dir/responses" \
890
+ bash -c '. "$0/bin/backends/cmux.sh"; fm_backend_cmux_send_text_submit "aaaaaaaa-0000-0000-0000-000000000000:bbbbbbbb-1111-1111-1111-111111111111" "hello captain" 2 0.01 0.01' "$ROOT" )
891
+ [ "$out" = pending ] || fail "send_text_submit should report pending once retries are exhausted with no visible change, got '$out'"
892
+ pass "fm_backend_cmux_send_text_submit: reports 'pending' when the composer never clears after retried Enters (swallowed)"
893
+ }
894
+
895
+ # The regression test for the popup-placeholder/second-Enter class (mirrors
896
+ # herdr's 2026-07-03 incident test): Enter #1 closes the popup and fills an
897
+ # argument-hint placeholder (still pending); Enter #2 actually submits. The
898
+ # adapter must retry past the first Enter instead of declaring victory on a
899
+ # raw content change, and must actually issue the second Enter.
900
+ test_send_text_submit_popup_autocomplete_requires_second_enter() {
901
+ local dir fb out
902
+ dir="$TMP_ROOT/submit-popup-autocomplete"; mkdir -p "$dir/responses"
903
+ # 1: list-panes (target_ready via send_literal)
904
+ # 2: send "/compact"
905
+ # 3: list-panes (target_ready via send_key Enter #1)
906
+ # 4: send-key enter (#1) - closes the popup, fills the placeholder
907
+ # 5: list-panes (target_ready via composer_state capture)
908
+ # 6: composer still reads real (pending) text
909
+ cmux_panes_response "$dir" 1 "bbbbbbbb-1111-1111-1111-111111111111"
910
+ cmux_panes_response "$dir" 3 "bbbbbbbb-1111-1111-1111-111111111111"
911
+ cmux_panes_response "$dir" 5 "bbbbbbbb-1111-1111-1111-111111111111"
912
+ cmux_read_screen_response "$dir" 6 $' ╭──────────────────────────────────────╮\n │ ❯ /compact compaction instructions │\n ╰──────────────── Composer ────────────╯\n\n Enter:send'
913
+ # 7: list-panes (target_ready via send_key Enter #2)
914
+ # 8: send-key enter (#2) - actually submits
915
+ # 9: list-panes (target_ready via composer_state capture)
916
+ # 10: composer now reads empty
917
+ cmux_panes_response "$dir" 7 "bbbbbbbb-1111-1111-1111-111111111111"
918
+ cmux_panes_response "$dir" 9 "bbbbbbbb-1111-1111-1111-111111111111"
919
+ cmux_read_screen_response "$dir" 10 $' ╭────────────────────────╮\n │ ❯ │\n ╰──────── Composer ──────╯'
920
+ fb=$(make_cmux_fakebin "$dir")
921
+ out=$( PATH="$fb:$PATH" FM_CMUX_LOG="$dir/log" FM_CMUX_RESPONSES="$dir/responses" \
922
+ bash -c '. "$0/bin/backends/cmux.sh"; fm_backend_cmux_send_text_submit "aaaaaaaa-0000-0000-0000-000000000000:bbbbbbbb-1111-1111-1111-111111111111" "/compact" 3 0.01 0.01' "$ROOT" )
923
+ [ "$out" = empty ] || fail "send_text_submit should eventually report empty once the SECOND Enter actually clears the composer, got '$out'"
924
+ enter_count=$(grep -c $'\x1f''send-key'$'\x1f''--workspace'$'\x1f''aaaaaaaa-0000-0000-0000-000000000000'$'\x1f''--surface'$'\x1f''bbbbbbbb-1111-1111-1111-111111111111'$'\x1f''enter' "$dir/log")
925
+ [ "$enter_count" -eq 2 ] || fail "send_text_submit should have sent exactly 2 Enters (popup-close, then real submit), sent $enter_count"
926
+ assert_not_contains "$(cat "$dir/log")" $'\x1f''send'$'\x1f''--workspace'$'\x1f''aaaaaaaa-0000-0000-0000-000000000000'$'\x1f''--surface'$'\x1f''bbbbbbbb-1111-1111-1111-111111111111'$'\x1f''--'$'\x1f''/compact compaction instructions' \
927
+ "send_text_submit should never retype - only retry Enter"
928
+ pass "fm_backend_cmux_send_text_submit: retries past a popup-placeholder-fill Enter and lands the real second Enter (the incident fix)"
929
+ }
930
+
931
+ test_send_text_submit_send_failed_when_target_absent() {
932
+ local dir fb out
933
+ dir="$TMP_ROOT/submit-no-target"; mkdir -p "$dir/responses"
934
+ printf '1\n' > "$dir/responses/1.exit"
935
+ fb=$(make_cmux_fakebin "$dir")
936
+ out=$( PATH="$fb:$PATH" FM_CMUX_LOG="$dir/log" FM_CMUX_RESPONSES="$dir/responses" \
937
+ bash -c '. "$0/bin/backends/cmux.sh"; fm_backend_cmux_send_text_submit "aaaaaaaa-0000-0000-0000-000000000000:bbbbbbbb-1111-1111-1111-111111111111" "x" 2 0.01 0.01' "$ROOT" )
938
+ [ "$out" = send-failed ] || fail "send_text_submit should report send-failed when the target is absent, got '$out'"
939
+ pass "fm_backend_cmux_send_text_submit: reports 'send-failed' when the target workspace/surface is absent"
940
+ }
941
+
942
+ # --- window_of_workspace: which window holds a workspace, and its count ------
943
+
944
+ test_window_of_workspace_finds_window_and_count() {
945
+ local dir fb out
946
+ dir="$TMP_ROOT/win-of-ws"; mkdir -p "$dir/responses"
947
+ # 1: list-windows --json -> two windows
948
+ cmux_windows_response "$dir" 1 "e1111111-0000-0000-0000-000000000000" 2 "e2222222-0000-0000-0000-000000000000" 2
949
+ # 2: workspace list --window e1111111 -> does NOT contain the target
950
+ cmux_workspace_list_response "$dir" 2 "ffffffff-0000-0000-0000-000000000000" "other"
951
+ # 3: workspace list --window e2222222 -> contains the target
952
+ cmux_workspace_list_response "$dir" 3 "aaaaaaaa-0000-0000-0000-000000000000" "the-task"
953
+ fb=$(make_cmux_fakebin "$dir")
954
+ out=$( PATH="$fb:$PATH" FM_CMUX_LOG="$dir/log" FM_CMUX_RESPONSES="$dir/responses" \
955
+ bash -c '. "$0/bin/backends/cmux.sh"; fm_backend_cmux_window_of_workspace "aaaaaaaa-0000-0000-0000-000000000000"' "$ROOT" )
956
+ [ "$out" = "e2222222-0000-0000-0000-000000000000 1" ] \
957
+ || fail "window_of_workspace should echo the owning window and its matched-list count, got '$out'"
958
+ cmux_assert_call_order "$dir/log" $'\x1f''list-windows' $'\x1f''workspace'$'\x1f''list'$'\x1f''--json'$'\x1f''--id-format'$'\x1f''uuids'$'\x1f''--window'$'\x1f''e1111111-0000-0000-0000-000000000000' \
959
+ "window_of_workspace did not list windows before scanning per-window workspaces"
960
+ pass "fm_backend_cmux_window_of_workspace: walks windows and counts the membership-confirming workspace list"
961
+ }
962
+
963
+ test_window_of_workspace_empty_when_not_found() {
964
+ local dir fb out
965
+ dir="$TMP_ROOT/win-of-ws-none"; mkdir -p "$dir/responses"
966
+ cmux_windows_response "$dir" 1 "e1111111-0000-0000-0000-000000000000" 1
967
+ cmux_workspace_list_response "$dir" 2 "ffffffff-0000-0000-0000-000000000000" "other"
968
+ fb=$(make_cmux_fakebin "$dir")
969
+ out=$( PATH="$fb:$PATH" FM_CMUX_LOG="$dir/log" FM_CMUX_RESPONSES="$dir/responses" \
970
+ bash -c '. "$0/bin/backends/cmux.sh"; fm_backend_cmux_window_of_workspace "aaaaaaaa-0000-0000-0000-000000000000"' "$ROOT" )
971
+ [ -z "$out" ] || fail "window_of_workspace should echo nothing when the workspace is not found, got '$out'"
972
+ pass "fm_backend_cmux_window_of_workspace: echoes nothing when no window holds the workspace"
973
+ }
974
+
975
+ # --- kill: close the task workspace, adding a sibling when it is the last one -
976
+
977
+ # The common case: the task workspace shares its window with at least one other
978
+ # workspace, so cmux closes it directly with no sibling dance.
979
+ test_kill_closes_workspace_directly_when_not_last() {
980
+ local dir fb
981
+ dir="$TMP_ROOT/kill-workspace"; mkdir -p "$dir/responses"
982
+ # 1: list-windows -> the owning window has 2 workspaces (target is NOT last)
983
+ cmux_windows_response "$dir" 1 "eeeeeeee-0000-0000-0000-000000000000" 2
984
+ # 2: workspace list --window eeeeeeee -> contains the target
985
+ cmux_workspace_list_response "$dir" 2 "aaaaaaaa-0000-0000-0000-000000000000" "the-task" "ffffffff-0000-0000-0000-000000000000" "other"
986
+ fb=$(make_cmux_fakebin "$dir")
987
+ PATH="$fb:$PATH" FM_CMUX_LOG="$dir/log" FM_CMUX_RESPONSES="$dir/responses" \
988
+ bash -c '. "$0/bin/backends/cmux.sh"; fm_backend_cmux_kill "aaaaaaaa-0000-0000-0000-000000000000:bbbbbbbb-1111-1111-1111-111111111111"' "$ROOT"
989
+ assert_contains "$(cat "$dir/log")" $'\x1f''close-workspace'$'\x1f''--workspace'$'\x1f''aaaaaaaa-0000-0000-0000-000000000000' \
990
+ "kill did not close the task workspace"
991
+ assert_not_contains "$(cat "$dir/log")" $'\x1f''new-workspace' \
992
+ "kill should not add a sibling workspace when the target is not the last one in its window"
993
+ assert_not_contains "$(cat "$dir/log")" $'\x1f''close-surface' \
994
+ "kill should close the whole workspace directly"
995
+ pass "fm_backend_cmux_kill: closes the task workspace directly when it is not the last in its window"
996
+ }
997
+
998
+ # The selected-workspace teardown bug: cmux refuses to close the only workspace
999
+ # in a window (returns OK but no-ops), so kill first creates a throwaway sibling
1000
+ # and only then closes the target - which now succeeds.
1001
+ test_kill_adds_sibling_when_last_in_window() {
1002
+ local dir fb
1003
+ dir="$TMP_ROOT/kill-last-in-window"; mkdir -p "$dir/responses"
1004
+ cmux_windows_response "$dir" 1 "eeeeeeee-0000-0000-0000-000000000000" 2
1005
+ # 2: workspace list --window eeeeeeee -> contains the target
1006
+ cmux_workspace_list_response "$dir" 2 "aaaaaaaa-0000-0000-0000-000000000000" "the-task"
1007
+ fb=$(make_cmux_fakebin "$dir")
1008
+ PATH="$fb:$PATH" FM_CMUX_LOG="$dir/log" FM_CMUX_RESPONSES="$dir/responses" \
1009
+ bash -c '. "$0/bin/backends/cmux.sh"; fm_backend_cmux_kill "aaaaaaaa-0000-0000-0000-000000000000:bbbbbbbb-1111-1111-1111-111111111111"' "$ROOT"
1010
+ assert_contains "$(cat "$dir/log")" $'\x1f''new-workspace'$'\x1f''--window'$'\x1f''eeeeeeee-0000-0000-0000-000000000000'$'\x1f''--focus'$'\x1f''false' \
1011
+ "kill did not add a throwaway sibling in the target's own window before closing the last workspace"
1012
+ assert_not_contains "$(cat "$dir/log")" $'\x1f''new-workspace'$'\x1f''--name' \
1013
+ "the throwaway sibling must stay an unnamed default workspace, never an fm- task title"
1014
+ assert_contains "$(cat "$dir/log")" $'\x1f''close-workspace'$'\x1f''--workspace'$'\x1f''aaaaaaaa-0000-0000-0000-000000000000' \
1015
+ "kill did not close the target workspace after adding the sibling"
1016
+ cmux_assert_call_order "$dir/log" $'\x1f''new-workspace'$'\x1f''--window' $'\x1f''close-workspace'$'\x1f''--workspace'$'\x1f''aaaaaaaa-0000-0000-0000-000000000000' \
1017
+ "kill must add the sibling BEFORE closing the last workspace, or the close still no-ops"
1018
+ assert_not_contains "$(cat "$dir/log")" $'\x1f''close-surface' \
1019
+ "kill should not call close-surface"
1020
+ pass "fm_backend_cmux_kill: adds a throwaway sibling then closes the target when it is the last workspace in its window"
1021
+ }
1022
+
1023
+ test_kill_is_best_effort_when_close_workspace_fails() {
1024
+ local dir fb
1025
+ dir="$TMP_ROOT/kill-workspace-fail"; mkdir -p "$dir/responses"
1026
+ # 1: list-windows (not last), 2: workspace list --window, 3: close-workspace fails
1027
+ cmux_windows_response "$dir" 1 "eeeeeeee-0000-0000-0000-000000000000" 2
1028
+ cmux_workspace_list_response "$dir" 2 "aaaaaaaa-0000-0000-0000-000000000000" "the-task" "ffffffff-0000-0000-0000-000000000000" "other"
1029
+ printf '1\n' > "$dir/responses/3.exit"
1030
+ fb=$(make_cmux_fakebin "$dir")
1031
+ PATH="$fb:$PATH" FM_CMUX_LOG="$dir/log" FM_CMUX_RESPONSES="$dir/responses" \
1032
+ bash -c '. "$0/bin/backends/cmux.sh"; fm_backend_cmux_kill "aaaaaaaa-0000-0000-0000-000000000000:bbbbbbbb-1111-1111-1111-111111111111"' "$ROOT"
1033
+ expect_code 0 $? "kill must stay best-effort (never fail) even when close-workspace fails"
1034
+ assert_contains "$(cat "$dir/log")" $'\x1f''close-workspace'$'\x1f''--workspace'$'\x1f''aaaaaaaa-0000-0000-0000-000000000000' \
1035
+ "kill should still attempt close-workspace"
1036
+ assert_not_contains "$(cat "$dir/log")" $'\x1f''close-surface' \
1037
+ "kill should not call close-surface"
1038
+ pass "fm_backend_cmux_kill: never fails even when close-workspace fails"
1039
+ }
1040
+
1041
+ test_kill_recovers_stale_target_by_label() {
1042
+ local dir fb title
1043
+ dir="$TMP_ROOT/kill-stale-target"; mkdir -p "$dir/responses"
1044
+ title=$(cmux_expected_scoped_title fm-label)
1045
+ # target_ready label recovery: 1 workspace list (title lookup, misses stale id),
1046
+ # 2 workspace list (id-for-label -> refreshed id), 3 list-panes (surface id).
1047
+ cmux_workspace_list_response "$dir" 1 "cccccccc-2222-2222-2222-222222222222" "$title"
1048
+ cmux_workspace_list_response "$dir" 2 "cccccccc-2222-2222-2222-222222222222" "$title"
1049
+ cmux_panes_response "$dir" 3 "dddddddd-3333-3333-3333-333333333333"
1050
+ # window_of_workspace on the REFRESHED id: 4 list-windows (not last), 5 workspace list --window.
1051
+ cmux_windows_response "$dir" 4 "eeeeeeee-0000-0000-0000-000000000000" 2
1052
+ cmux_workspace_list_response "$dir" 5 "cccccccc-2222-2222-2222-222222222222" "$title" "ffffffff-0000-0000-0000-000000000000" "other"
1053
+ fb=$(make_cmux_fakebin "$dir")
1054
+ PATH="$fb:$PATH" FM_CMUX_LOG="$dir/log" FM_CMUX_RESPONSES="$dir/responses" \
1055
+ bash -c '. "$0/bin/backends/cmux.sh"; fm_backend_cmux_kill "aaaaaaaa-0000-0000-0000-000000000000:bbbbbbbb-1111-1111-1111-111111111111" "" fm-label' "$ROOT"
1056
+ expect_code 0 $? "kill should recover a stale cmux target when the expected label is live"
1057
+ assert_contains "$(cat "$dir/log")" $'\x1f''close-workspace'$'\x1f''--workspace'$'\x1f''cccccccc-2222-2222-2222-222222222222' \
1058
+ "kill did not use the refreshed cmux workspace/surface ids"
1059
+ assert_not_contains "$(cat "$dir/log")" $'\x1f''close-workspace'$'\x1f''--workspace'$'\x1f''aaaaaaaa-0000-0000-0000-000000000000' \
1060
+ "kill should not target the stale cmux workspace id after label recovery"
1061
+ assert_not_contains "$(cat "$dir/log")" $'\x1f''close-surface' \
1062
+ "kill should not call close-surface"
1063
+ pass "fm_backend_cmux_kill: recovers stale workspace/surface ids by expected label"
1064
+ }
1065
+
1066
+ # --- list_live: label-based orphan discovery ---------------------------------
1067
+
1068
+ test_list_live_filters_by_title_prefix() {
1069
+ local dir fb out title other_title other_root
1070
+ dir="$TMP_ROOT/list-live"; mkdir -p "$dir/responses"
1071
+ other_root="$dir/other-root"; mkdir -p "$other_root"
1072
+ title=$(cmux_expected_scoped_title fm-task1)
1073
+ other_title=$(cmux_expected_scoped_title fm-task2 "$ROOT" "$other_root")
1074
+ # 1: workspace list --json --id-format uuids -> one in-home task, two unrelated
1075
+ cmux_workspace_list_response "$dir" 1 \
1076
+ "aaaaaaaa-0000-0000-0000-000000000000" "$title" \
1077
+ "dddddddd-8888-8888-8888-888888888888" "$other_title" \
1078
+ "cccccccc-9999-9999-9999-999999999999" "zsh"
1079
+ # 2: list-panes for this home's task1 workspace
1080
+ cmux_panes_response "$dir" 2 "bbbbbbbb-1111-1111-1111-111111111111"
1081
+ fb=$(make_cmux_fakebin "$dir")
1082
+ out=$( PATH="$fb:$PATH" FM_CMUX_LOG="$dir/log" FM_CMUX_RESPONSES="$dir/responses" \
1083
+ bash -c '. "$0/bin/backends/cmux.sh"; fm_backend_cmux_list_live' "$ROOT" )
1084
+ [ "$out" = $'aaaaaaaa-0000-0000-0000-000000000000:bbbbbbbb-1111-1111-1111-111111111111\tfm-task1' ] \
1085
+ || fail "list_live should list only the in-home task workspace with its plain label and surface id, got '$out'"
1086
+ pass "fm_backend_cmux_list_live: lists only this home's scoped task workspaces using plain fm-<id> labels"
1087
+ }
1088
+
1089
+ # --- fm-spawn.sh: --secondmate refuses backend=cmux --------------------------
1090
+
1091
+ test_secondmate_spawn_refuses_cmux_backend() {
1092
+ local dir state data config projects out status
1093
+ dir="$TMP_ROOT/secondmate-refuse"; state="$dir/state"; data="$dir/data"; config="$dir/config"; projects="$dir/projects"
1094
+ mkdir -p "$state" "$data" "$config" "$projects"
1095
+ out=$( FM_STATE_OVERRIDE="$state" FM_DATA_OVERRIDE="$data" FM_CONFIG_OVERRIDE="$config" FM_PROJECTS_OVERRIDE="$projects" \
1096
+ "$ROOT/bin/fm-spawn.sh" sm-cmux-test --secondmate --backend cmux 2>&1 )
1097
+ status=$?
1098
+ [ "$status" -ne 0 ] || fail "fm-spawn.sh should refuse a --secondmate spawn with --backend cmux"
1099
+ assert_contains "$out" "does not support --secondmate" "fm-spawn.sh did not report the cmux secondmate refusal"
1100
+ pass "fm-spawn.sh: refuses backend=cmux for --secondmate spawns (mirrors Orca's refusal; no secondmate launch design exists yet)"
1101
+ }
1102
+
1103
+ # shellcheck source=/dev/null
1104
+ . "$ROOT/bin/fm-backend.sh"
1105
+
1106
+ test_version_check_accepts_current_version
1107
+ test_version_check_accepts_newer_version
1108
+ test_version_check_refuses_old_version
1109
+ test_version_check_refuses_missing_cmux
1110
+ test_password_reads_from_config_file
1111
+ test_password_preserves_config_file_whitespace
1112
+ test_password_respects_config_override
1113
+ test_password_empty_when_config_absent
1114
+ test_cli_exports_password_only_when_configured
1115
+ test_parse_target
1116
+ test_normalize_key
1117
+ test_scoped_title_uses_primary_home_label
1118
+ test_scoped_title_uses_secondmate_home_label
1119
+ test_scoped_title_changes_with_root_path
1120
+ test_dispatch_routes_cmux_backend
1121
+ test_dispatch_busy_state_unknown_for_cmux
1122
+ test_dispatch_composer_state_routes_cmux
1123
+ test_ping_state_ok
1124
+ test_ping_state_denied
1125
+ test_ping_state_unauth
1126
+ test_ping_state_invalid_password
1127
+ test_ping_state_down
1128
+ test_ensure_running_returns_immediately_when_already_ok
1129
+ test_ensure_running_fails_fast_on_denied_without_launching
1130
+ test_ensure_running_fails_fast_on_unauth_without_launching
1131
+ test_create_task_refuses_duplicate_label
1132
+ test_create_task_creates_and_parses_ids
1133
+ test_target_ready_fails_when_target_absent
1134
+ test_target_ready_checks_expected_label
1135
+ test_target_ready_rejects_label_mismatch
1136
+ test_capture_trims_locally
1137
+ test_capture_fails_when_read_screen_fails_empty
1138
+ test_capture_fails_when_target_not_ready
1139
+ test_send_key_normalizes_and_targets
1140
+ test_send_key_recovers_stale_target_by_label
1141
+ test_send_literal_uses_separator_for_option_shaped_text
1142
+ test_send_text_line_clears_partial_input_when_enter_fails
1143
+ test_send_text_line_reports_unsafe_input_when_cleanup_fails
1144
+ test_current_path_probes_with_marker
1145
+ test_composer_state_bare_prompt_is_empty
1146
+ test_composer_state_borderless_claude_prompt_is_empty
1147
+ test_composer_state_borderless_claude_prompt_outranks_stale_bordered_row
1148
+ test_composer_state_borderless_claude_nbsp_prompt_is_empty
1149
+ test_composer_state_borderless_claude_text_is_unknown_plain
1150
+ test_composer_state_ghost_placeholder_is_empty
1151
+ test_composer_state_real_text_is_pending
1152
+ test_composer_state_popup_placeholder_fill_is_pending
1153
+ test_composer_state_unknown_on_capture_failure
1154
+ test_composer_state_unknown_when_no_composer_row_found
1155
+ test_send_text_submit_detects_landed_send
1156
+ test_send_text_submit_detects_swallowed_enter
1157
+ test_send_text_submit_popup_autocomplete_requires_second_enter
1158
+ test_send_text_submit_send_failed_when_target_absent
1159
+ test_window_of_workspace_finds_window_and_count
1160
+ test_window_of_workspace_empty_when_not_found
1161
+ test_kill_closes_workspace_directly_when_not_last
1162
+ test_kill_adds_sibling_when_last_in_window
1163
+ test_kill_is_best_effort_when_close_workspace_fails
1164
+ test_kill_recovers_stale_target_by_label
1165
+ test_list_live_filters_by_title_prefix
1166
+ test_secondmate_spawn_refuses_cmux_backend