@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,1360 @@
1
+ #!/usr/bin/env bash
2
+ # tests/fm-backend-zellij.test.sh - fake-zellij-CLI unit tests for the zellij
3
+ # session-provider adapter (bin/backends/zellij.sh), P3 of
4
+ # data/fm-backend-design-d7 (report.md "Zellij Backend"). Mirrors
5
+ # tests/fm-backend-herdr.test.sh's fakebin/command-log convention: a small,
6
+ # LOG-based, canned-response fake `zellij` + real `jq` (jq is a real required
7
+ # tool for this backend, not faked). The real-binary smoke test lives in
8
+ # tests/fm-backend-zellij-smoke.test.sh, gated on the zellij binary actually
9
+ # being installed.
10
+ set -u
11
+
12
+ # shellcheck source=tests/lib.sh
13
+ . "$(dirname "${BASH_SOURCE[0]}")/lib.sh"
14
+
15
+ command -v jq >/dev/null 2>&1 || { echo "skip: jq not found (required by the zellij adapter)"; exit 0; }
16
+
17
+ TMP_ROOT=$(fm_test_tmproot fm-backend-zellij-tests)
18
+
19
+ # make_zellij_fakebin: a `zellij` stub that logs every invocation (one line,
20
+ # unit-separated args, to $FM_ZELLIJ_LOG) and returns the canned response for
21
+ # that call read from $FM_ZELLIJ_RESPONSES/<n>.out, consumed IN ORDER (call 1
22
+ # reads 1.out, call 2 reads 2.out, ...), mirroring
23
+ # tests/fm-backend-herdr.test.sh's make_herdr_fakebin. A missing response file
24
+ # means "succeed with empty stdout" (paste/send-keys/close-* are silent on
25
+ # success on the real CLI). `--version` and `list-sessions` are handled
26
+ # specially (not call-counted) since fm_backend_zellij_session_exists calls
27
+ # list-sessions on every op as a passive liveness probe and must not consume
28
+ # the ordered response queue - its canned membership is controlled by
29
+ # FM_ZELLIJ_SESSION_LIST instead.
30
+ make_zellij_fakebin() { # <dir> -> echoes fakebin dir
31
+ local dir=$1 fb="$1/fakebin"
32
+ mkdir -p "$fb"
33
+ cat > "$fb/zellij" <<'SH'
34
+ #!/usr/bin/env bash
35
+ set -u
36
+ LOG="${FM_ZELLIJ_LOG:?}"
37
+ RESP="${FM_ZELLIJ_RESPONSES:?}"
38
+ COUNT_FILE="$RESP/.count"
39
+ {
40
+ printf 'ZELLIJ_SESSION_NAME=%s' "${ZELLIJ_SESSION_NAME:-}"
41
+ for a in "$@"; do printf '\x1f%s' "$a"; done
42
+ printf '\n'
43
+ } >> "$LOG"
44
+
45
+ if [ "${1:-}" = --version ]; then
46
+ printf 'zellij %s\n' "${FM_ZELLIJ_FAKE_VERSION:-0.44.0}"
47
+ exit 0
48
+ fi
49
+ if [ "${1:-}" = list-sessions ]; then
50
+ printf '%s\n' "${FM_ZELLIJ_SESSION_LIST:-}"
51
+ exit 0
52
+ fi
53
+ if [ "${1:-}" = attach ]; then
54
+ exit "${FM_ZELLIJ_ATTACH_EXIT:-0}"
55
+ fi
56
+
57
+ next=$(( $(cat "$COUNT_FILE" 2>/dev/null || echo 0) + 1 ))
58
+ n=$next
59
+ echo "$n" > "$COUNT_FILE"
60
+ if [ -f "$RESP/$n.exit" ]; then
61
+ exit "$(cat "$RESP/$n.exit")"
62
+ fi
63
+ [ -f "$RESP/$n.out" ] && cat "$RESP/$n.out"
64
+ exit 0
65
+ SH
66
+ chmod +x "$fb/zellij"
67
+ printf '%s\n' "$fb"
68
+ }
69
+
70
+ zellij_pane_response() {
71
+ local dir=$1 n=$2 pane=${3:-7} tab=${4:-3}
72
+ printf '[{"id":%s,"tab_id":%s,"is_plugin":false}]\n' "$pane" "$tab" > "$dir/responses/$n.out"
73
+ }
74
+
75
+ zellij_tab_response() {
76
+ local dir=$1 n=$2 tab=${3:-3} name=${4:-fm-task}
77
+ printf '[{"tab_id":%s,"name":"%s"}]\n' "$tab" "$name" > "$dir/responses/$n.out"
78
+ }
79
+
80
+ zellij_assert_call_order() {
81
+ local log=$1 before=$2 after=$3 msg=$4 before_line after_line
82
+ before_line=$(grep -anF -- "$before" "$log" | head -1 | cut -d: -f1)
83
+ after_line=$(grep -anF -- "$after" "$log" | head -1 | cut -d: -f1)
84
+ [ -n "$before_line" ] || fail "$msg (missing before call: '$before')"
85
+ [ -n "$after_line" ] || fail "$msg (missing after call: '$after')"
86
+ [ "$before_line" -lt "$after_line" ] || fail "$msg"
87
+ }
88
+
89
+ # zellij_multi_tab_response: a list-tabs --json canned response holding
90
+ # several tabs at once (tab_id/name pairs), for ambiguity/foreign-tag tests.
91
+ zellij_multi_tab_response() { # <dir> <n> <tab1> <name1> [<tab2> <name2> ...]
92
+ local dir=$1 n=$2 json first=1
93
+ shift 2
94
+ json='['
95
+ while [ $# -ge 2 ]; do
96
+ [ "$first" -eq 1 ] || json="$json,"
97
+ json="$json{\"tab_id\":$1,\"name\":\"$2\"}"
98
+ first=0
99
+ shift 2
100
+ done
101
+ json="$json]"
102
+ printf '%s' "$json" > "$dir/responses/$n.out"
103
+ }
104
+
105
+ # zellij_expected_home_label / zellij_expected_scoped_title: bash-only
106
+ # reimplementations of fm_backend_zellij_home_label/scoped_title (mirroring
107
+ # tests/fm-backend-cmux.test.sh's identical cmux_expected_* helpers), used to
108
+ # build canned fixtures for the home-scoped tab titles this adapter now
109
+ # creates and matches.
110
+ zellij_expected_root_hash() { # <root>
111
+ local root real
112
+ root=$1
113
+ real=$(cd "$root" && pwd -P) || return 1
114
+ if command -v shasum >/dev/null 2>&1; then
115
+ printf '%s' "$real" | shasum -a 256 | awk '{print substr($1,1,8)}'
116
+ elif command -v sha256sum >/dev/null 2>&1; then
117
+ printf '%s' "$real" | sha256sum | awk '{print substr($1,1,8)}'
118
+ else
119
+ printf '%s' "$real" | cksum | awk '{printf "%08x", $1}'
120
+ fi
121
+ }
122
+
123
+ zellij_expected_home_label() { # [home] [root]
124
+ local home=${1:-$ROOT} root=${2:-$ROOT} marker id prefix
125
+ marker="$home/.fm-secondmate-home"
126
+ if [ -f "$marker" ]; then
127
+ id=$(tr -d '[:space:]' < "$marker" 2>/dev/null)
128
+ if [ -n "$id" ]; then
129
+ prefix="2ndmate-$id"
130
+ else
131
+ prefix="firstmate"
132
+ fi
133
+ else
134
+ prefix="firstmate"
135
+ fi
136
+ printf '%s-%s' "$prefix" "$(zellij_expected_root_hash "$root")"
137
+ }
138
+
139
+ zellij_expected_scoped_title() { # <fm-task-label> [home] [root]
140
+ local label=$1 home=${2:-$ROOT} root=${3:-$ROOT} rest
141
+ case "$label" in
142
+ fm-*) rest=${label#fm-} ;;
143
+ *) rest=$label ;;
144
+ esac
145
+ printf 'fm-%s-%s' "$(zellij_expected_home_label "$home" "$root")" "$rest"
146
+ }
147
+
148
+ # --- version_check / tool_check ----------------------------------------------
149
+
150
+ test_version_check_accepts_current_version() {
151
+ local dir fb status
152
+ dir="$TMP_ROOT/version-ok"; mkdir -p "$dir/responses"
153
+ fb=$(make_zellij_fakebin "$dir")
154
+ PATH="$fb:$PATH" FM_ZELLIJ_LOG="$dir/log" FM_ZELLIJ_RESPONSES="$dir/responses" FM_ZELLIJ_FAKE_VERSION=0.44.0 \
155
+ bash -c '. "$0/bin/backends/zellij.sh"; fm_backend_zellij_version_check' "$ROOT"
156
+ status=$?
157
+ expect_code 0 "$status" "version_check should accept 0.44.0 (the verified minimum)"
158
+ pass "fm_backend_zellij_version_check: accepts the verified minimum (0.44.0)"
159
+ }
160
+
161
+ test_version_check_accepts_newer_version() {
162
+ local dir fb status
163
+ dir="$TMP_ROOT/version-newer"; mkdir -p "$dir/responses"
164
+ fb=$(make_zellij_fakebin "$dir")
165
+ PATH="$fb:$PATH" FM_ZELLIJ_LOG="$dir/log" FM_ZELLIJ_RESPONSES="$dir/responses" FM_ZELLIJ_FAKE_VERSION=0.45.2 \
166
+ bash -c '. "$0/bin/backends/zellij.sh"; fm_backend_zellij_version_check' "$ROOT"
167
+ status=$?
168
+ expect_code 0 "$status" "version_check should accept a newer minor (0.45.2)"
169
+ pass "fm_backend_zellij_version_check: accepts a newer version (0.45.2)"
170
+ }
171
+
172
+ test_version_check_refuses_old_version() {
173
+ local dir fb out status
174
+ dir="$TMP_ROOT/version-old"; mkdir -p "$dir/responses"
175
+ fb=$(make_zellij_fakebin "$dir")
176
+ out=$( PATH="$fb:$PATH" FM_ZELLIJ_LOG="$dir/log" FM_ZELLIJ_RESPONSES="$dir/responses" FM_ZELLIJ_FAKE_VERSION=0.38.1 \
177
+ bash -c '. "$0/bin/backends/zellij.sh"; fm_backend_zellij_version_check' "$ROOT" 2>&1 )
178
+ status=$?
179
+ [ "$status" -ne 0 ] || fail "version_check should refuse 0.38.1 (below the 0.44 minimum)"
180
+ assert_contains "$out" "0.38.1" "version_check error did not name the rejected version"
181
+ pass "fm_backend_zellij_version_check: refuses an old version loudly"
182
+ }
183
+
184
+ test_version_check_refuses_missing_zellij() {
185
+ local dir out status
186
+ dir="$TMP_ROOT/version-missing"; mkdir -p "$dir/empty-fakebin"
187
+ out=$( PATH="$dir/empty-fakebin:/usr/bin:/bin" \
188
+ bash -c '. "$0/bin/backends/zellij.sh"; fm_backend_zellij_version_check' "$ROOT" 2>&1 )
189
+ status=$?
190
+ [ "$status" -ne 0 ] || fail "version_check should refuse when zellij is not installed"
191
+ assert_contains "$out" "not installed" "version_check did not report zellij as missing"
192
+ pass "fm_backend_zellij_version_check: refuses loudly when zellij is not installed"
193
+ }
194
+
195
+ # --- session name resolution --------------------------------------------------
196
+
197
+ test_session_defaults_to_firstmate() {
198
+ local out
199
+ out=$( bash -c '. "$0/bin/backends/zellij.sh"; fm_backend_zellij_session' "$ROOT" )
200
+ [ "$out" = firstmate ] || fail "default session should be 'firstmate', got '$out'"
201
+ pass "fm_backend_zellij_session: defaults to 'firstmate' when FM_ZELLIJ_SESSION is unset"
202
+ }
203
+
204
+ test_session_honors_override() {
205
+ local out
206
+ out=$( FM_ZELLIJ_SESSION=fm-test-iso bash -c '. "$0/bin/backends/zellij.sh"; fm_backend_zellij_session' "$ROOT" )
207
+ [ "$out" = fm-test-iso ] || fail "FM_ZELLIJ_SESSION override was not honored, got '$out'"
208
+ pass "fm_backend_zellij_session: honors the FM_ZELLIJ_SESSION test-isolation override"
209
+ }
210
+
211
+ # --- target parsing, key normalization ---------------------------------------
212
+
213
+ test_parse_target() {
214
+ ( . "$ROOT/bin/backends/zellij.sh"
215
+ fm_backend_zellij_parse_target "firstmate:5" || exit 1
216
+ [ "$FM_BACKEND_ZELLIJ_SESSION" = firstmate ] || { echo "session mismatch: $FM_BACKEND_ZELLIJ_SESSION" >&2; exit 1; }
217
+ [ "$FM_BACKEND_ZELLIJ_PANE" = "5" ] || { echo "pane mismatch: $FM_BACKEND_ZELLIJ_PANE" >&2; exit 1; }
218
+ ) || fail "fm_backend_zellij_parse_target did not split session:pane correctly"
219
+ pass "fm_backend_zellij_parse_target: splits '<session>:<pane_id>' on the first colon"
220
+ }
221
+
222
+ test_normalize_key() {
223
+ ( . "$ROOT/bin/backends/zellij.sh"
224
+ [ "$(fm_backend_zellij_normalize_key Enter)" = Enter ] || { echo "Enter failed" >&2; exit 1; }
225
+ [ "$(fm_backend_zellij_normalize_key Escape)" = Esc ] || { echo "Escape failed" >&2; exit 1; }
226
+ [ "$(fm_backend_zellij_normalize_key Esc)" = Esc ] || { echo "Esc failed" >&2; exit 1; }
227
+ [ "$(fm_backend_zellij_normalize_key C-c)" = "Ctrl c" ] || { echo "C-c failed" >&2; exit 1; }
228
+ [ "$(fm_backend_zellij_normalize_key ctrl+c)" = "Ctrl c" ] || { echo "ctrl+c failed" >&2; exit 1; }
229
+ ) || fail "fm_backend_zellij_normalize_key did not map firstmate's key vocabulary to zellij's verified names"
230
+ pass "fm_backend_zellij_normalize_key: Enter/Escape/C-c map to zellij's verified Enter/Esc/'Ctrl c'"
231
+ }
232
+
233
+ # --- home-scoped tab titles (cross-home collision fix) ------------------------
234
+
235
+ test_scoped_title_uses_primary_home_label() {
236
+ local dir out expected
237
+ dir="$TMP_ROOT/scoped-title-primary"; mkdir -p "$dir"
238
+ expected=$(zellij_expected_scoped_title fm-task1 "$dir")
239
+ out=$( FM_HOME="$dir" bash -c '. "$0/bin/backends/zellij.sh"; fm_backend_zellij_scoped_title fm-task1' "$ROOT" )
240
+ [ "$out" = "$expected" ] || fail "primary scoped title should be $expected, got '$out'"
241
+ pass "fm_backend_zellij_scoped_title: scopes a primary task title with firstmate plus root hash"
242
+ }
243
+
244
+ test_scoped_title_uses_secondmate_home_label() {
245
+ local dir out expected
246
+ dir="$TMP_ROOT/scoped-title-secondmate"; mkdir -p "$dir"
247
+ printf 'sm-one\n' > "$dir/.fm-secondmate-home"
248
+ expected=$(zellij_expected_scoped_title fm-task1 "$dir")
249
+ out=$( FM_HOME="$dir" bash -c '. "$0/bin/backends/zellij.sh"; fm_backend_zellij_scoped_title fm-task1' "$ROOT" )
250
+ [ "$out" = "$expected" ] || fail "secondmate scoped title should be $expected, got '$out'"
251
+ pass "fm_backend_zellij_scoped_title: scopes a secondmate task title with the home marker plus root hash"
252
+ }
253
+
254
+ test_scoped_title_changes_with_root_path() {
255
+ local dir home root_one root_two out_one out_two expected_one expected_two
256
+ dir="$TMP_ROOT/scoped-title-root-hash"; home="$dir/home"; root_one="$dir/root-one"; root_two="$dir/root-two"
257
+ mkdir -p "$home" "$root_one" "$root_two"
258
+ expected_one=$(zellij_expected_scoped_title fm-task1 "$home" "$root_one")
259
+ expected_two=$(zellij_expected_scoped_title fm-task1 "$home" "$root_two")
260
+ out_one=$( FM_HOME="$home" FM_ROOT_OVERRIDE="$root_one" bash -c '. "$0/bin/backends/zellij.sh"; fm_backend_zellij_scoped_title fm-task1' "$ROOT" )
261
+ out_two=$( FM_HOME="$home" FM_ROOT_OVERRIDE="$root_two" bash -c '. "$0/bin/backends/zellij.sh"; fm_backend_zellij_scoped_title fm-task1' "$ROOT" )
262
+ [ "$out_one" = "$expected_one" ] || fail "scoped title should include root-one hash as $expected_one, got '$out_one'"
263
+ [ "$out_two" = "$expected_two" ] || fail "scoped title should include root-two hash as $expected_two, got '$out_two'"
264
+ [ "$out_one" != "$out_two" ] || fail "scoped titles should differ for distinct FM_ROOT paths"
265
+ pass "fm_backend_zellij_scoped_title: includes the resolved FM_ROOT hash in the home label"
266
+ }
267
+
268
+ test_expected_label_accepts_unambiguous_untagged_legacy_tab() {
269
+ local dir fb
270
+ dir="$TMP_ROOT/label-legacy-unambiguous"; mkdir -p "$dir/responses"
271
+ zellij_pane_response "$dir" 1 7 3
272
+ # 2: list-tabs --json -> exactly ONE live tab, still carrying its
273
+ # pre-migration untagged bare title (never re-tagged) - unambiguous, so
274
+ # the legacy fallback in fm_backend_zellij_tab_matches_label trusts it.
275
+ zellij_tab_response "$dir" 2 3 fm-legacy
276
+ fb=$(make_zellij_fakebin "$dir")
277
+ PATH="$fb:$PATH" FM_ZELLIJ_LOG="$dir/log" FM_ZELLIJ_RESPONSES="$dir/responses" \
278
+ FM_ZELLIJ_SESSION_LIST="firstmate" \
279
+ bash -c '. "$0/bin/backends/zellij.sh"; fm_backend_zellij_send_key firstmate:7 Escape fm-legacy' "$ROOT"
280
+ expect_code 0 $? "send_key should still reach a task tab spawned before home-scoping shipped, when its untagged title is unambiguous"
281
+ assert_contains "$(cat "$dir/log")" $'\x1f''send-keys'$'\x1f''--pane-id'$'\x1f''7'$'\x1f''Esc' \
282
+ "send_key did not send after accepting the unambiguous legacy label"
283
+ pass "fm_backend_zellij_tab_matches_label: accepts an untagged legacy fm-<id> title when it is the only live tab carrying it"
284
+ }
285
+
286
+ test_expected_label_refuses_ambiguous_untagged_tab() {
287
+ local dir fb status
288
+ dir="$TMP_ROOT/label-ambiguous-untagged"; mkdir -p "$dir/responses"
289
+ zellij_pane_response "$dir" 1 7 3
290
+ # 2: list-tabs --json -> TWO different tabs sharing the exact same bare,
291
+ # untagged legacy title "fm-shared" (our pane's owning tab_id=3 is one of
292
+ # them, but so is an unrelated tab_id=9) - the migration posture requires
293
+ # an untagged bare match to be UNIQUE across the whole session before it
294
+ # is trusted; ambiguity here must refuse loudly rather than assume ours.
295
+ zellij_multi_tab_response "$dir" 2 3 fm-shared 9 fm-shared
296
+ fb=$(make_zellij_fakebin "$dir")
297
+ PATH="$fb:$PATH" FM_ZELLIJ_LOG="$dir/log" FM_ZELLIJ_RESPONSES="$dir/responses" \
298
+ FM_ZELLIJ_SESSION_LIST="firstmate" \
299
+ bash -c '. "$0/bin/backends/zellij.sh"; fm_backend_zellij_send_key firstmate:7 Escape fm-shared' "$ROOT"
300
+ status=$?
301
+ [ "$status" -ne 0 ] || fail "send_key should refuse an ambiguous untagged legacy label shared by 2+ live tabs"
302
+ assert_not_contains "$(cat "$dir/log")" $'\x1f''send-keys' \
303
+ "send_key should not send after an ambiguous untagged-label match refuses"
304
+ pass "fm_backend_zellij_tab_matches_label: refuses an untagged legacy label match when 2+ live tabs share it (migration ambiguity guard)"
305
+ }
306
+
307
+ test_list_live_scopes_to_own_home_tag() {
308
+ local dir fb out own_title foreign_title other_root
309
+ dir="$TMP_ROOT/list-live-scope"; mkdir -p "$dir/responses"
310
+ other_root="$dir/other-root"; mkdir -p "$other_root"
311
+ own_title=$(zellij_expected_scoped_title fm-task1)
312
+ foreign_title=$(zellij_expected_scoped_title fm-task2 "$ROOT" "$other_root")
313
+ # 1: list-tabs --json -> our own home-scoped tab, a DIFFERENT installation's
314
+ # home-scoped tab (same prefix shape, different FM_ROOT hash), and an
315
+ # unrelated non-firstmate tab.
316
+ zellij_multi_tab_response "$dir" 1 \
317
+ 3 "$own_title" \
318
+ 4 "$foreign_title" \
319
+ 5 "zsh"
320
+ # 2: list-panes for our own home's task1 tab only
321
+ zellij_pane_response "$dir" 2 7 3
322
+ fb=$(make_zellij_fakebin "$dir")
323
+ out=$( PATH="$fb:$PATH" FM_ZELLIJ_LOG="$dir/log" FM_ZELLIJ_RESPONSES="$dir/responses" \
324
+ FM_ZELLIJ_SESSION_LIST="firstmate" \
325
+ bash -c '. "$0/bin/backends/zellij.sh"; fm_backend_zellij_list_live firstmate' "$ROOT" )
326
+ [ "$out" = $'firstmate:7\tfm-task1' ] \
327
+ || fail "list_live should list only this home's own tagged tab with its plain fm-<id> label, got '$out'"
328
+ pass "fm_backend_zellij_list_live: scopes to this home's own tag - excludes a different installation's tagged tab and unrelated tabs"
329
+ }
330
+
331
+ test_resolve_bare_selector_prefers_scoped_title() {
332
+ local dir fb out title
333
+ dir="$TMP_ROOT/resolve-scoped"; mkdir -p "$dir/responses"
334
+ title=$(zellij_expected_scoped_title fm-resolve1)
335
+ # 1: list-tabs --json -> the home-scoped tagged tab
336
+ printf '[{"tab_id":6,"name":"%s"}]\n' "$title" > "$dir/responses/1.out"
337
+ # 2: list-panes for tab 6
338
+ zellij_pane_response "$dir" 2 12 6
339
+ fb=$(make_zellij_fakebin "$dir")
340
+ out=$( PATH="$fb:$PATH" FM_ZELLIJ_LOG="$dir/log" FM_ZELLIJ_RESPONSES="$dir/responses" \
341
+ FM_ZELLIJ_SESSION_LIST="firstmate" \
342
+ bash -c '. "$0/bin/backends/zellij.sh"; fm_backend_zellij_resolve_bare_selector fm-resolve1' "$ROOT" )
343
+ [ "$out" = "firstmate:12" ] || fail "resolve_bare_selector should resolve the home-scoped tagged tab, got '$out'"
344
+ pass "fm_backend_zellij_resolve_bare_selector: matches the home-scoped tagged title first"
345
+ }
346
+
347
+ test_resolve_bare_selector_refuses_ambiguous_untagged() {
348
+ local dir fb out status
349
+ dir="$TMP_ROOT/resolve-ambiguous"; mkdir -p "$dir/responses"
350
+ # 1: list-tabs --json -> two DIFFERENT tabs sharing the same untagged bare name
351
+ zellij_multi_tab_response "$dir" 1 3 fm-resolve2 9 fm-resolve2
352
+ zellij_multi_tab_response "$dir" 2 3 fm-resolve2 9 fm-resolve2
353
+ fb=$(make_zellij_fakebin "$dir")
354
+ out=$( PATH="$fb:$PATH" FM_ZELLIJ_LOG="$dir/log" FM_ZELLIJ_RESPONSES="$dir/responses" \
355
+ FM_ZELLIJ_SESSION_LIST="firstmate" \
356
+ bash -c '. "$0/bin/backends/zellij.sh"; fm_backend_zellij_resolve_bare_selector fm-resolve2' "$ROOT" 2>&1 )
357
+ status=$?
358
+ [ "$status" -ne 0 ] || fail "resolve_bare_selector should refuse an ambiguous untagged label shared by 2+ live tabs"
359
+ assert_contains "$out" "no zellij tab named" "resolve_bare_selector's refusal did not report the expected not-found error"
360
+ pass "fm_backend_zellij_resolve_bare_selector: refuses an ambiguous untagged legacy label shared by 2+ live tabs"
361
+ }
362
+
363
+ test_resolve_bare_selector_prefers_later_session_scoped_title_over_legacy() {
364
+ local dir fb out title
365
+ dir="$TMP_ROOT/resolve-later-scoped"; mkdir -p "$dir/responses"
366
+ title=$(zellij_expected_scoped_title fm-resolve3)
367
+ zellij_tab_response "$dir" 1 3 fm-resolve3
368
+ zellij_tab_response "$dir" 2 6 "$title"
369
+ zellij_pane_response "$dir" 3 12 6
370
+ fb=$(make_zellij_fakebin "$dir")
371
+ out=$( PATH="$fb:$PATH" FM_ZELLIJ_LOG="$dir/log" FM_ZELLIJ_RESPONSES="$dir/responses" \
372
+ FM_ZELLIJ_SESSION_LIST=$'legacy-session\nscoped-session' \
373
+ bash -c '. "$0/bin/backends/zellij.sh"; fm_backend_zellij_resolve_bare_selector fm-resolve3' "$ROOT" )
374
+ [ "$out" = "scoped-session:12" ] || fail "resolve_bare_selector should prefer a later session's scoped title over an earlier legacy title, got '$out'"
375
+ pass "fm_backend_zellij_resolve_bare_selector: checks every session for the scoped title before legacy fallback"
376
+ }
377
+
378
+ test_resolve_bare_selector_refuses_cross_session_ambiguous_untagged() {
379
+ local dir fb out status
380
+ dir="$TMP_ROOT/resolve-cross-session-ambiguous"; mkdir -p "$dir/responses"
381
+ zellij_tab_response "$dir" 1 3 fm-resolve4
382
+ zellij_tab_response "$dir" 2 9 fm-resolve4
383
+ zellij_tab_response "$dir" 3 3 fm-resolve4
384
+ zellij_tab_response "$dir" 4 9 fm-resolve4
385
+ fb=$(make_zellij_fakebin "$dir")
386
+ out=$( PATH="$fb:$PATH" FM_ZELLIJ_LOG="$dir/log" FM_ZELLIJ_RESPONSES="$dir/responses" \
387
+ FM_ZELLIJ_SESSION_LIST=$'one-session\ntwo-session' \
388
+ bash -c '. "$0/bin/backends/zellij.sh"; fm_backend_zellij_resolve_bare_selector fm-resolve4' "$ROOT" 2>&1 )
389
+ status=$?
390
+ [ "$status" -ne 0 ] || fail "resolve_bare_selector should refuse an untagged legacy label that appears once in each of two sessions"
391
+ assert_contains "$out" "no zellij tab named" "resolve_bare_selector's cross-session ambiguity refusal did not report the expected not-found error"
392
+ pass "fm_backend_zellij_resolve_bare_selector: requires untagged legacy labels to be globally unique across sessions"
393
+ }
394
+
395
+ # --- session_exists / server_ensure -------------------------------------------
396
+
397
+ test_session_exists_true_when_listed() {
398
+ local dir fb out
399
+ dir="$TMP_ROOT/exists-true"; mkdir -p "$dir/responses"
400
+ fb=$(make_zellij_fakebin "$dir")
401
+ PATH="$fb:$PATH" FM_ZELLIJ_LOG="$dir/log" FM_ZELLIJ_RESPONSES="$dir/responses" \
402
+ FM_ZELLIJ_SESSION_LIST=$'firstmate\nother-session' \
403
+ bash -c '. "$0/bin/backends/zellij.sh"; fm_backend_zellij_session_exists firstmate' "$ROOT"
404
+ expect_code 0 $? "session_exists should report true when the session is in the list"
405
+ pass "fm_backend_zellij_session_exists: true when the session name is listed"
406
+ }
407
+
408
+ test_session_exists_false_when_absent() {
409
+ local dir fb
410
+ dir="$TMP_ROOT/exists-false"; mkdir -p "$dir/responses"
411
+ fb=$(make_zellij_fakebin "$dir")
412
+ out=$( PATH="$fb:$PATH" FM_ZELLIJ_LOG="$dir/log" FM_ZELLIJ_RESPONSES="$dir/responses" \
413
+ FM_ZELLIJ_SESSION_LIST=$'other-session' \
414
+ bash -c '. "$0/bin/backends/zellij.sh"; fm_backend_zellij_session_exists firstmate' "$ROOT" 2>&1 )
415
+ status=$?
416
+ [ "$status" -ne 0 ] || fail "session_exists should report false when the session is absent"
417
+ pass "fm_backend_zellij_session_exists: false when the session name is not listed"
418
+ }
419
+
420
+ test_server_ensure_skips_attach_when_already_exists() {
421
+ local dir fb
422
+ dir="$TMP_ROOT/server-reuse"; mkdir -p "$dir/responses"
423
+ fb=$(make_zellij_fakebin "$dir")
424
+ PATH="$fb:$PATH" FM_ZELLIJ_LOG="$dir/log" FM_ZELLIJ_RESPONSES="$dir/responses" \
425
+ FM_ZELLIJ_SESSION_LIST="firstmate" \
426
+ bash -c '. "$0/bin/backends/zellij.sh"; fm_backend_zellij_server_ensure firstmate' "$ROOT"
427
+ expect_code 0 $? "server_ensure should succeed immediately when the session already exists"
428
+ assert_not_contains "$(cat "$dir/log")" $'\x1f''attach' "server_ensure should not call attach when the session already exists"
429
+ pass "fm_backend_zellij_server_ensure: reuses an existing session without calling attach"
430
+ }
431
+
432
+ # --- dispatch wiring (fm-backend.sh) ------------------------------------------
433
+
434
+ test_dispatch_routes_zellij_backend() {
435
+ fm_backend_validate zellij 2>/dev/null || fail "fm_backend_validate should accept zellij (P3 adds it to FM_BACKEND_KNOWN)"
436
+ pass "fm_backend_validate: zellij is a known backend (P3)"
437
+ }
438
+
439
+ test_dispatch_busy_state_unknown_for_zellij() {
440
+ # shellcheck source=/dev/null
441
+ . "$ROOT/bin/fm-backend.sh"
442
+ [ "$(fm_backend_busy_state zellij 'firstmate:5')" = unknown ] \
443
+ || fail "fm_backend_busy_state should report unknown for zellij (no native agent-state primitive; D5: watcher falls back to regex, same as tmux)"
444
+ pass "fm_backend_busy_state: zellij (no native primitive) always reports unknown, same as tmux"
445
+ }
446
+
447
+ # --- create_task: duplicate refusal, id parsing, focus-restore mitigation ----
448
+
449
+ test_create_task_refuses_duplicate_label() {
450
+ local dir fb out status title
451
+ dir="$TMP_ROOT/dup-task"; mkdir -p "$dir/responses"
452
+ title=$(zellij_expected_scoped_title fm-dup1)
453
+ # 1: list-tabs --json -> existing tab already carrying the home-scoped title
454
+ printf '[{"tab_id":2,"name":"%s","active":false}]\n' "$title" > "$dir/responses/1.out"
455
+ fb=$(make_zellij_fakebin "$dir")
456
+ out=$( PATH="$fb:$PATH" FM_ZELLIJ_LOG="$dir/log" FM_ZELLIJ_RESPONSES="$dir/responses" \
457
+ FM_ZELLIJ_SESSION_LIST="firstmate" \
458
+ bash -c '. "$0/bin/backends/zellij.sh"; fm_backend_zellij_create_task firstmate fm-dup1 /tmp/proj' "$ROOT" 2>&1 )
459
+ status=$?
460
+ [ "$status" -ne 0 ] || fail "create_task should refuse an existing tab name (zellij itself does not enforce uniqueness)"
461
+ assert_contains "$out" "already exists" "create_task did not report the duplicate name"
462
+ assert_contains "$out" "$title" "create_task's duplicate error did not name the home-scoped title"
463
+ pass "fm_backend_zellij_create_task: refuses a duplicate home-scoped tab title (zellij's own new-tab has no uniqueness check)"
464
+ }
465
+
466
+ test_create_task_creates_and_parses_ids() {
467
+ local dir fb out title
468
+ dir="$TMP_ROOT/create-task"; mkdir -p "$dir/responses"
469
+ title=$(zellij_expected_scoped_title fm-newtask)
470
+ # 1: list-tabs --json -> no existing tabs, none active
471
+ printf '[]\n' > "$dir/responses/1.out"
472
+ # 2: new-tab --cwd --name -> bare tab id on stdout
473
+ printf '3\n' > "$dir/responses/2.out"
474
+ # 3: list-panes --json -> the new tab's terminal pane
475
+ printf '[{"id":7,"tab_id":3,"is_plugin":false}]\n' > "$dir/responses/3.out"
476
+ fb=$(make_zellij_fakebin "$dir")
477
+ out=$( PATH="$fb:$PATH" FM_ZELLIJ_LOG="$dir/log" FM_ZELLIJ_RESPONSES="$dir/responses" \
478
+ FM_ZELLIJ_SESSION_LIST="firstmate" \
479
+ bash -c '. "$0/bin/backends/zellij.sh"; fm_backend_zellij_create_task firstmate fm-newtask /tmp/proj' "$ROOT" )
480
+ [ "$out" = "3 7" ] || fail "create_task should echo '<tab_id> <pane_id>', got '$out'"
481
+ assert_contains "$(cat "$dir/log")" $'\x1f''new-tab'$'\x1f''--cwd'$'\x1f''/tmp/proj'$'\x1f''--name'$'\x1f'"$title" \
482
+ "create_task did not call new-tab with the right cwd/home-scoped name"
483
+ pass "fm_backend_zellij_create_task: creates a home-scoped tab and parses tab_id/pane_id from the response"
484
+ }
485
+
486
+ test_create_task_restores_previously_active_tab() {
487
+ local dir fb out
488
+ dir="$TMP_ROOT/focus-restore"; mkdir -p "$dir/responses"
489
+ # 1: list-tabs --json -> tab 0 is currently active
490
+ printf '[{"tab_id":0,"name":"Tab #1","active":true}]\n' > "$dir/responses/1.out"
491
+ # 2: new-tab -> id 4 (this steals focus on the real CLI)
492
+ printf '4\n' > "$dir/responses/2.out"
493
+ # 3: list-panes --json -> tab 4's terminal pane
494
+ printf '[{"id":9,"tab_id":4,"is_plugin":false}]\n' > "$dir/responses/3.out"
495
+ # 4: go-to-tab-by-id 0 (the restore call) - silent on success
496
+ fb=$(make_zellij_fakebin "$dir")
497
+ out=$( PATH="$fb:$PATH" FM_ZELLIJ_LOG="$dir/log" FM_ZELLIJ_RESPONSES="$dir/responses" \
498
+ FM_ZELLIJ_SESSION_LIST="firstmate" \
499
+ bash -c '. "$0/bin/backends/zellij.sh"; fm_backend_zellij_create_task firstmate fm-focustest /tmp/proj' "$ROOT" )
500
+ [ "$out" = "4 9" ] || fail "create_task should still echo '<tab_id> <pane_id>', got '$out'"
501
+ assert_contains "$(cat "$dir/log")" $'\x1f''go-to-tab-by-id'$'\x1f''0' \
502
+ "create_task did not restore focus to the previously-active tab (verified real-zellij focus-steal mitigation)"
503
+ pass "fm_backend_zellij_create_task: restores focus to the previously-active tab after the steal-focus new-tab call"
504
+ }
505
+
506
+ test_create_task_no_restore_when_new_tab_was_already_active() {
507
+ local dir fb out
508
+ dir="$TMP_ROOT/focus-noop"; mkdir -p "$dir/responses"
509
+ # No active tab at all (no client attached - the common unattended case)
510
+ printf '[]\n' > "$dir/responses/1.out"
511
+ printf '5\n' > "$dir/responses/2.out"
512
+ printf '[{"id":11,"tab_id":5,"is_plugin":false}]\n' > "$dir/responses/3.out"
513
+ fb=$(make_zellij_fakebin "$dir")
514
+ out=$( PATH="$fb:$PATH" FM_ZELLIJ_LOG="$dir/log" FM_ZELLIJ_RESPONSES="$dir/responses" \
515
+ FM_ZELLIJ_SESSION_LIST="firstmate" \
516
+ bash -c '. "$0/bin/backends/zellij.sh"; fm_backend_zellij_create_task firstmate fm-noclient /tmp/proj' "$ROOT" )
517
+ [ "$out" = "5 11" ] || fail "create_task should still echo '<tab_id> <pane_id>', got '$out'"
518
+ assert_not_contains "$(cat "$dir/log")" $'\x1f''go-to-tab-by-id' \
519
+ "create_task should not call go-to-tab-by-id when there was no previously-active tab (no attached client)"
520
+ pass "fm_backend_zellij_create_task: skips the restore call when there was no previously-active tab"
521
+ }
522
+
523
+ # --- capture / send_key / send_literal / current_path / kill -----------------
524
+
525
+ test_capture_small_reads_use_viewport_and_trim() {
526
+ local dir fb out
527
+ dir="$TMP_ROOT/capture"; mkdir -p "$dir/responses"
528
+ zellij_pane_response "$dir" 1 7 3
529
+ printf 'line one\nline two\nline three\nline four\n' > "$dir/responses/2.out"
530
+ fb=$(make_zellij_fakebin "$dir")
531
+ out=$( PATH="$fb:$PATH" FM_ZELLIJ_LOG="$dir/log" FM_ZELLIJ_RESPONSES="$dir/responses" \
532
+ FM_ZELLIJ_SESSION_LIST="firstmate" \
533
+ bash -c '. "$0/bin/backends/zellij.sh"; fm_backend_zellij_capture firstmate:7 2' "$ROOT" )
534
+ [ "$out" = $'line three\nline four' ] || fail "capture should trim to the last N lines locally, got '$out'"
535
+ zellij_assert_call_order "$dir/log" $'\x1f''list-panes'$'\x1f''--json' $'\x1f''dump-screen' \
536
+ "capture did not verify the pane before dump-screen"
537
+ assert_contains "$(cat "$dir/log")" $'\x1f''dump-screen'$'\x1f''--pane-id'$'\x1f''7' \
538
+ "capture did not call dump-screen --pane-id <pane>"
539
+ assert_not_contains "$(cat "$dir/log")" $'\x1f''--full' \
540
+ "small capture should use zellij's viewport-only dump-screen path"
541
+ pass "fm_backend_zellij_capture: small reads use viewport-only dump-screen and trim to N lines locally"
542
+ }
543
+
544
+ test_capture_large_reads_use_full_scrollback_and_trim() {
545
+ local dir fb out
546
+ dir="$TMP_ROOT/capture-full"; mkdir -p "$dir/responses"
547
+ zellij_pane_response "$dir" 1 7 3
548
+ printf 'line one\nline two\nline three\nline four\n' > "$dir/responses/2.out"
549
+ fb=$(make_zellij_fakebin "$dir")
550
+ out=$( PATH="$fb:$PATH" FM_ZELLIJ_LOG="$dir/log" FM_ZELLIJ_RESPONSES="$dir/responses" \
551
+ FM_ZELLIJ_SESSION_LIST="firstmate" \
552
+ bash -c '. "$0/bin/backends/zellij.sh"; fm_backend_zellij_capture firstmate:7 80' "$ROOT" )
553
+ [ "$out" = $'line one\nline two\nline three\nline four' ] || fail "large capture should keep available output when fewer than N lines exist, got '$out'"
554
+ zellij_assert_call_order "$dir/log" $'\x1f''list-panes'$'\x1f''--json' $'\x1f''dump-screen' \
555
+ "large capture did not verify the pane before dump-screen"
556
+ assert_contains "$(cat "$dir/log")" $'\x1f''dump-screen'$'\x1f''--pane-id'$'\x1f''7'$'\x1f''--full' \
557
+ "large capture did not request --full scrollback"
558
+ pass "fm_backend_zellij_capture: reads above the watcher-size threshold request --full scrollback"
559
+ }
560
+
561
+ test_capture_fails_when_pane_absent() {
562
+ local dir fb out status
563
+ dir="$TMP_ROOT/capture-no-pane"; mkdir -p "$dir/responses"
564
+ printf '[]\n' > "$dir/responses/1.out"
565
+ fb=$(make_zellij_fakebin "$dir")
566
+ out=$( PATH="$fb:$PATH" FM_ZELLIJ_LOG="$dir/log" FM_ZELLIJ_RESPONSES="$dir/responses" \
567
+ FM_ZELLIJ_SESSION_LIST="firstmate" \
568
+ bash -c '. "$0/bin/backends/zellij.sh"; fm_backend_zellij_capture firstmate:7 5' "$ROOT" 2>&1 )
569
+ status=$?
570
+ [ "$status" -ne 0 ] || fail "capture should fail when the session exists but the pane does not"
571
+ assert_not_contains "$(cat "$dir/log")" $'\x1f''dump-screen' \
572
+ "capture should not call dump-screen after pane readiness fails"
573
+ pass "fm_backend_zellij_capture: fails when the specific pane is absent"
574
+ }
575
+
576
+ test_capture_fails_when_session_absent() {
577
+ local dir fb out status
578
+ dir="$TMP_ROOT/capture-no-session"; mkdir -p "$dir/responses"
579
+ fb=$(make_zellij_fakebin "$dir")
580
+ out=$( PATH="$fb:$PATH" FM_ZELLIJ_LOG="$dir/log" FM_ZELLIJ_RESPONSES="$dir/responses" \
581
+ FM_ZELLIJ_SESSION_LIST="" \
582
+ bash -c '. "$0/bin/backends/zellij.sh"; fm_backend_zellij_capture firstmate:7 5' "$ROOT" 2>&1 )
583
+ status=$?
584
+ [ "$status" -ne 0 ] || fail "capture should fail when the session does not exist (never trust the CLI's unconditional exit 0)"
585
+ pass "fm_backend_zellij_capture: fails when the target session is not listed as active (session_exists pre-check)"
586
+ }
587
+
588
+ test_send_key_normalizes_and_targets_pane() {
589
+ local dir fb
590
+ dir="$TMP_ROOT/sendkey"; mkdir -p "$dir/responses"
591
+ zellij_pane_response "$dir" 1 7 3
592
+ fb=$(make_zellij_fakebin "$dir")
593
+ PATH="$fb:$PATH" FM_ZELLIJ_LOG="$dir/log" FM_ZELLIJ_RESPONSES="$dir/responses" \
594
+ FM_ZELLIJ_SESSION_LIST="firstmate" \
595
+ bash -c '. "$0/bin/backends/zellij.sh"; fm_backend_zellij_send_key firstmate:7 Escape' "$ROOT"
596
+ expect_code 0 $? "send_key should succeed"
597
+ zellij_assert_call_order "$dir/log" $'\x1f''list-panes'$'\x1f''--json' $'\x1f''send-keys' \
598
+ "send_key did not verify the pane before send-keys"
599
+ assert_contains "$(cat "$dir/log")" $'\x1f''send-keys'$'\x1f''--pane-id'$'\x1f''7'$'\x1f''Esc' "send_key did not normalize Escape to Esc"
600
+ pass "fm_backend_zellij_send_key: normalizes the key (Escape -> Esc) and targets the explicit pane id"
601
+ }
602
+
603
+ test_send_literal_uses_paste_separator_for_option_shaped_text() {
604
+ local dir fb
605
+ dir="$TMP_ROOT/sendliteral"; mkdir -p "$dir/responses"
606
+ zellij_pane_response "$dir" 1 7 3
607
+ fb=$(make_zellij_fakebin "$dir")
608
+ PATH="$fb:$PATH" FM_ZELLIJ_LOG="$dir/log" FM_ZELLIJ_RESPONSES="$dir/responses" \
609
+ FM_ZELLIJ_SESSION_LIST="firstmate" \
610
+ bash -c '. "$0/bin/backends/zellij.sh"; fm_backend_zellij_send_literal firstmate:7 "--help"' "$ROOT"
611
+ expect_code 0 $? "send_literal should succeed"
612
+ zellij_assert_call_order "$dir/log" $'\x1f''list-panes'$'\x1f''--json' $'\x1f''paste' \
613
+ "send_literal did not verify the pane before paste"
614
+ assert_contains "$(cat "$dir/log")" $'\x1f''paste'$'\x1f''--pane-id'$'\x1f''7'$'\x1f''--'$'\x1f''--help' \
615
+ "send_literal did not call paste with a -- separator before the literal payload"
616
+ pass "fm_backend_zellij_send_literal: calls paste with an explicit pane id and a -- separator"
617
+ }
618
+
619
+ test_send_text_line_clears_partial_input_when_enter_fails() {
620
+ local dir fb status log
621
+ dir="$TMP_ROOT/sendline-enter-failure"; mkdir -p "$dir/responses"
622
+ zellij_pane_response "$dir" 1 7 3
623
+ zellij_pane_response "$dir" 3 7 3
624
+ printf '1\n' > "$dir/responses/4.exit"
625
+ zellij_pane_response "$dir" 5 7 3
626
+ fb=$(make_zellij_fakebin "$dir")
627
+
628
+ PATH="$fb:$PATH" FM_ZELLIJ_LOG="$dir/log" FM_ZELLIJ_RESPONSES="$dir/responses" \
629
+ FM_ZELLIJ_SESSION_LIST="firstmate" bash -c \
630
+ '. "$0/bin/backends/zellij.sh"; fm_backend_zellij_send_text_line "firstmate:7" "export TRACEPARENT=carrier"' "$ROOT"
631
+ status=$?
632
+ [ "$status" -ne 0 ] || fail "send_text_line should report a failed Enter"
633
+ log=$(cat "$dir/log")
634
+ assert_contains "$log" $'\x1f''paste'$'\x1f''--pane-id'$'\x1f''7'$'\x1f''--'$'\x1f''export TRACEPARENT=carrier' \
635
+ "send_text_line did not paste the trace export before the simulated Enter failure"
636
+ zellij_assert_call_order "$dir/log" $'\x1f''Enter' $'\x1f''Ctrl c' \
637
+ "send_text_line did not clear the partial input after Enter failed"
638
+ pass "fm_backend_zellij_send_text_line: clears partial input when Enter fails"
639
+ }
640
+
641
+ test_send_text_line_reports_unsafe_input_when_cleanup_fails() {
642
+ local dir fb status
643
+ dir="$TMP_ROOT/sendline-cleanup-failure"; mkdir -p "$dir/responses"
644
+ zellij_pane_response "$dir" 1 7 3
645
+ zellij_pane_response "$dir" 3 7 3
646
+ printf '1\n' > "$dir/responses/4.exit"
647
+ zellij_pane_response "$dir" 5 7 3
648
+ printf '1\n' > "$dir/responses/6.exit"
649
+ fb=$(make_zellij_fakebin "$dir")
650
+
651
+ PATH="$fb:$PATH" FM_ZELLIJ_LOG="$dir/log" FM_ZELLIJ_RESPONSES="$dir/responses" \
652
+ FM_ZELLIJ_SESSION_LIST="firstmate" bash -c \
653
+ '. "$0/bin/backends/zellij.sh"; fm_backend_zellij_send_text_line "firstmate:7" "export TRACEPARENT=carrier"' "$ROOT"
654
+ status=$?
655
+ expect_code 2 "$status" "send_text_line should distinguish uncleared input"
656
+ zellij_assert_call_order "$dir/log" $'\x1f''Enter' $'\x1f''Ctrl c' \
657
+ "send_text_line did not attempt cleanup after Enter failed"
658
+ pass "fm_backend_zellij_send_text_line: reports unsafe input when cleanup also fails"
659
+ }
660
+
661
+ test_expected_label_allows_matching_task_tab() {
662
+ local dir fb
663
+ dir="$TMP_ROOT/label-match"; mkdir -p "$dir/responses"
664
+ zellij_pane_response "$dir" 1 7 3
665
+ zellij_tab_response "$dir" 2 3 fm-label
666
+ fb=$(make_zellij_fakebin "$dir")
667
+ PATH="$fb:$PATH" FM_ZELLIJ_LOG="$dir/log" FM_ZELLIJ_RESPONSES="$dir/responses" \
668
+ FM_ZELLIJ_SESSION_LIST="firstmate" \
669
+ bash -c '. "$0/bin/backends/zellij.sh"; fm_backend_zellij_send_key firstmate:7 Escape fm-label' "$ROOT"
670
+ expect_code 0 $? "send_key should succeed when the pane belongs to the expected fm-id tab"
671
+ zellij_assert_call_order "$dir/log" $'\x1f''list-panes'$'\x1f''--json' $'\x1f''list-tabs'$'\x1f''--json' \
672
+ "expected-label readiness did not resolve the pane's owning tab before label verification"
673
+ zellij_assert_call_order "$dir/log" $'\x1f''list-tabs'$'\x1f''--json' $'\x1f''send-keys' \
674
+ "send_key ran before verifying the owning tab label"
675
+ pass "fm_backend_zellij_target_ready: expected labels allow matching fm-<id> tabs"
676
+ }
677
+
678
+ test_expected_label_rejects_reused_pane_id() {
679
+ local dir fb status
680
+ dir="$TMP_ROOT/label-mismatch"; mkdir -p "$dir/responses"
681
+ zellij_pane_response "$dir" 1 7 3
682
+ zellij_tab_response "$dir" 2 3 not-the-task
683
+ fb=$(make_zellij_fakebin "$dir")
684
+ PATH="$fb:$PATH" FM_ZELLIJ_LOG="$dir/log" FM_ZELLIJ_RESPONSES="$dir/responses" \
685
+ FM_ZELLIJ_SESSION_LIST="firstmate" \
686
+ bash -c '. "$0/bin/backends/zellij.sh"; fm_backend_zellij_send_key firstmate:7 Escape fm-label' "$ROOT"
687
+ status=$?
688
+ [ "$status" -ne 0 ] || fail "send_key should reject a pane whose tab name does not match the expected fm-id label"
689
+ zellij_assert_call_order "$dir/log" $'\x1f''list-panes'$'\x1f''--json' $'\x1f''list-tabs'$'\x1f''--json' \
690
+ "expected-label readiness did not check the pane's owning tab"
691
+ assert_not_contains "$(cat "$dir/log")" $'\x1f''send-keys' \
692
+ "send_key should not run after expected-label readiness fails"
693
+ pass "fm_backend_zellij_target_ready: expected labels reject stale pane ids reused by another tab"
694
+ }
695
+
696
+ test_current_path_probes_with_marker_and_ignores_prompt_paths() {
697
+ local dir fb out
698
+ # Verified real-zellij pitfall (docs/zellij-backend.md "Worktree-path
699
+ # discovery: pane_cwd does not track a subshell"): pane_cwd never updates
700
+ # once a subshell (e.g. treehouse get) takes over, so current_path actively
701
+ # prints a marked cwd line and reads only that marker from the capture,
702
+ # rather than reading a JSON field.
703
+ dir="$TMP_ROOT/cwd"; mkdir -p "$dir/responses"
704
+ zellij_pane_response "$dir" 1 7 3
705
+ zellij_pane_response "$dir" 2 7 3
706
+ zellij_pane_response "$dir" 4 7 3
707
+ zellij_pane_response "$dir" 6 7 3
708
+ printf '%s\n' 'scratch-e2e-project HEAD' \
709
+ '/home/fixture/src/project ❯ printf marker' \
710
+ '__FM_ZELLIJ_CWD_BEGIN__' \
711
+ '/home/fixture/.treehouse/fake-' \
712
+ 'worktree' \
713
+ '__FM_ZELLIJ_CWD_END__' \
714
+ '/home/fixture/.treehouse/fake-worktree ❯' \
715
+ > "$dir/responses/7.out"
716
+ fb=$(make_zellij_fakebin "$dir")
717
+ out=$( PATH="$fb:$PATH" FM_ZELLIJ_LOG="$dir/log" FM_ZELLIJ_RESPONSES="$dir/responses" \
718
+ FM_ZELLIJ_SESSION_LIST="firstmate" \
719
+ bash -c '. "$0/bin/backends/zellij.sh"; fm_backend_zellij_current_path firstmate:7' "$ROOT" )
720
+ [ "$out" = "/home/fixture/.treehouse/fake-worktree" ] || fail "current_path should read only the marked cwd line, got '$out'"
721
+ zellij_assert_call_order "$dir/log" $'\x1f''list-panes'$'\x1f''--json' $'\x1f''paste' \
722
+ "current_path did not verify the pane before the cwd probe paste"
723
+ zellij_assert_call_order "$dir/log" $'\x1f''list-panes'$'\x1f''--json' $'\x1f''dump-screen' \
724
+ "current_path did not verify the pane before capture"
725
+ assert_contains "$(cat "$dir/log")" "__FM_ZELLIJ_CWD_BEGIN__" "current_path did not send the cwd begin marker via paste"
726
+ assert_contains "$(cat "$dir/log")" "pwd;" "current_path did not send the pwd probe via paste"
727
+ assert_contains "$(cat "$dir/log")" $'\x1f''send-keys'$'\x1f''--pane-id'$'\x1f''7'$'\x1f''Enter' "current_path did not submit the cwd probe with Enter"
728
+ assert_contains "$(cat "$dir/log")" $'\x1f''dump-screen'$'\x1f''--pane-id'$'\x1f''7'$'\x1f''--full' "current_path did not capture the pane after probing"
729
+ pass "fm_backend_zellij_current_path: actively probes with marked begin/end lines and reconstructs wrapped cwd output"
730
+ }
731
+
732
+ test_current_path_ignores_tilde_prefixed_banner_lines() {
733
+ local dir fb out
734
+ dir="$TMP_ROOT/cwd-tilde"; mkdir -p "$dir/responses"
735
+ zellij_pane_response "$dir" 1 7 3
736
+ zellij_pane_response "$dir" 2 7 3
737
+ zellij_pane_response "$dir" 4 7 3
738
+ zellij_pane_response "$dir" 6 7 3
739
+ printf '%s\n' "🌳 Entered worktree at ~/.treehouse/scratch-e2e-project/1. Type 'exit' to return." \
740
+ 'scratch-e2e-project HEAD' '__FM_ZELLIJ_CWD_BEGIN__' '/home/fixture/.treehouse/real-worktree' '__FM_ZELLIJ_CWD_END__' '❯' \
741
+ > "$dir/responses/7.out"
742
+ fb=$(make_zellij_fakebin "$dir")
743
+ out=$( PATH="$fb:$PATH" FM_ZELLIJ_LOG="$dir/log" FM_ZELLIJ_RESPONSES="$dir/responses" \
744
+ FM_ZELLIJ_SESSION_LIST="firstmate" \
745
+ bash -c '. "$0/bin/backends/zellij.sh"; fm_backend_zellij_current_path firstmate:7' "$ROOT" )
746
+ [ "$out" = "/home/fixture/.treehouse/real-worktree" ] || fail "current_path should skip the ~-prefixed banner line and read the marked cwd output, got '$out'"
747
+ pass "fm_backend_zellij_current_path: never picks up a ~-prefixed banner line as the answer"
748
+ }
749
+
750
+ test_kill_resolves_tab_and_closes_by_id() {
751
+ local dir fb
752
+ dir="$TMP_ROOT/kill"; mkdir -p "$dir/responses"
753
+ zellij_pane_response "$dir" 1 7 3
754
+ printf '[{"id":7,"tab_id":3,"is_plugin":false}]\n' > "$dir/responses/2.out"
755
+ fb=$(make_zellij_fakebin "$dir")
756
+ PATH="$fb:$PATH" FM_ZELLIJ_LOG="$dir/log" FM_ZELLIJ_RESPONSES="$dir/responses" \
757
+ FM_ZELLIJ_SESSION_LIST="firstmate" \
758
+ bash -c '. "$0/bin/backends/zellij.sh"; fm_backend_zellij_kill firstmate:7' "$ROOT"
759
+ expect_code 0 $? "kill should succeed (best-effort)"
760
+ zellij_assert_call_order "$dir/log" $'\x1f''list-panes'$'\x1f''--json' $'\x1f''close-tab-by-id' \
761
+ "kill did not verify the pane before close-tab-by-id"
762
+ assert_contains "$(cat "$dir/log")" $'\x1f''close-tab-by-id'$'\x1f''3' \
763
+ "kill did not resolve the owning tab id and call close-tab-by-id (verified: close-pane alone leaves an empty ghost tab)"
764
+ pass "fm_backend_zellij_kill: resolves the owning tab id fresh and calls close-tab-by-id (never a bare close-pane)"
765
+ }
766
+
767
+ test_kill_falls_back_to_close_pane_when_tab_lookup_empty() {
768
+ local dir fb
769
+ dir="$TMP_ROOT/kill-fallback"; mkdir -p "$dir/responses"
770
+ printf '[]\n' > "$dir/responses/1.out"
771
+ fb=$(make_zellij_fakebin "$dir")
772
+ PATH="$fb:$PATH" FM_ZELLIJ_LOG="$dir/log" FM_ZELLIJ_RESPONSES="$dir/responses" \
773
+ FM_ZELLIJ_SESSION_LIST="firstmate" \
774
+ bash -c '. "$0/bin/backends/zellij.sh"; fm_backend_zellij_kill firstmate:7' "$ROOT"
775
+ expect_code 0 $? "kill must stay best-effort even when the tab lookup comes up empty"
776
+ zellij_assert_call_order "$dir/log" $'\x1f''list-panes'$'\x1f''--json' $'\x1f''close-pane' \
777
+ "kill did not verify the pane before close-pane fallback"
778
+ assert_contains "$(cat "$dir/log")" $'\x1f''close-pane'$'\x1f''--pane-id'$'\x1f''7' \
779
+ "kill did not fall back to a direct close-pane when no owning tab could be resolved"
780
+ pass "fm_backend_zellij_kill: falls back to close-pane when the owning tab cannot be resolved"
781
+ }
782
+
783
+ test_kill_closes_recorded_tab_when_pane_already_gone() {
784
+ local dir fb
785
+ dir="$TMP_ROOT/kill-recorded-tab"; mkdir -p "$dir/responses"
786
+ printf '[]\n' > "$dir/responses/1.out"
787
+ printf '[{"tab_id":3,"name":"fm-zghost"}]\n' > "$dir/responses/2.out"
788
+ fb=$(make_zellij_fakebin "$dir")
789
+ PATH="$fb:$PATH" FM_ZELLIJ_LOG="$dir/log" FM_ZELLIJ_RESPONSES="$dir/responses" \
790
+ FM_ZELLIJ_SESSION_LIST="firstmate" \
791
+ bash -c '. "$0/bin/backends/zellij.sh"; fm_backend_zellij_kill firstmate:7 3 fm-zghost' "$ROOT"
792
+ expect_code 0 $? "kill must stay best-effort even when only the recorded tab id is usable"
793
+ zellij_assert_call_order "$dir/log" $'\x1f''list-panes'$'\x1f''--json' $'\x1f''list-tabs'$'\x1f''--json' \
794
+ "kill did not verify the recorded tab id by label before closing it"
795
+ zellij_assert_call_order "$dir/log" $'\x1f''list-tabs'$'\x1f''--json' $'\x1f''close-tab-by-id' \
796
+ "kill closed the recorded tab id before verifying its label"
797
+ assert_contains "$(cat "$dir/log")" $'\x1f''close-tab-by-id'$'\x1f''3' \
798
+ "kill did not close the verified recorded tab id when the pane was already gone"
799
+ assert_not_contains "$(cat "$dir/log")" $'\x1f''close-pane' \
800
+ "kill should close the verified recorded tab id instead of leaving an empty ghost tab"
801
+ pass "fm_backend_zellij_kill: closes the recorded tab id only after label verification"
802
+ }
803
+
804
+ test_kill_skips_recorded_tab_when_label_mismatches() {
805
+ local dir fb
806
+ dir="$TMP_ROOT/kill-recorded-tab-mismatch"; mkdir -p "$dir/responses"
807
+ printf '[]\n' > "$dir/responses/1.out"
808
+ printf '[{"tab_id":3,"name":"not-the-task"}]\n' > "$dir/responses/2.out"
809
+ fb=$(make_zellij_fakebin "$dir")
810
+ PATH="$fb:$PATH" FM_ZELLIJ_LOG="$dir/log" FM_ZELLIJ_RESPONSES="$dir/responses" \
811
+ FM_ZELLIJ_SESSION_LIST="firstmate" \
812
+ bash -c '. "$0/bin/backends/zellij.sh"; fm_backend_zellij_kill firstmate:7 3 fm-zghost' "$ROOT"
813
+ expect_code 0 $? "kill must stay best-effort when the recorded tab id no longer belongs to the task"
814
+ zellij_assert_call_order "$dir/log" $'\x1f''list-panes'$'\x1f''--json' $'\x1f''list-tabs'$'\x1f''--json' \
815
+ "kill did not verify the recorded tab id by label"
816
+ assert_not_contains "$(cat "$dir/log")" $'\x1f''close-tab-by-id' \
817
+ "kill should not close a recorded tab id whose name does not match the task"
818
+ assert_not_contains "$(cat "$dir/log")" $'\x1f''close-pane' \
819
+ "kill should not fall back to closing a pane once an expected task label is available"
820
+ pass "fm_backend_zellij_kill: skips a stale recorded tab id whose label does not match"
821
+ }
822
+
823
+ test_kill_is_noop_when_session_absent() {
824
+ local dir fb
825
+ dir="$TMP_ROOT/kill-no-session"; mkdir -p "$dir/responses"
826
+ fb=$(make_zellij_fakebin "$dir")
827
+ PATH="$fb:$PATH" FM_ZELLIJ_LOG="$dir/log" FM_ZELLIJ_RESPONSES="$dir/responses" \
828
+ FM_ZELLIJ_SESSION_LIST="" \
829
+ bash -c '. "$0/bin/backends/zellij.sh"; fm_backend_zellij_kill firstmate:7' "$ROOT"
830
+ expect_code 0 $? "kill must stay best-effort (never fail) even when the session is already gone"
831
+ pass "fm_backend_zellij_kill: never fails when the target session no longer exists"
832
+ }
833
+
834
+ test_teardown_passes_recorded_tab_id_to_zellij_kill() {
835
+ local dir state data config project fb out status
836
+ dir="$TMP_ROOT/teardown-zellij-ghost"; state="$dir/state"; data="$dir/data"; config="$dir/config"; project="$dir/project"
837
+ mkdir -p "$state" "$data/zghost" "$config" "$project" "$dir/responses"
838
+ printf 'report\n' > "$data/zghost/report.md"
839
+ fm_write_meta "$state/zghost.meta" \
840
+ "window=firstmate:7" \
841
+ "endpoint_task_id=zghost" \
842
+ "backend=zellij" \
843
+ "zellij_session=firstmate" \
844
+ "zellij_tab_id=3" \
845
+ "zellij_pane_id=7" \
846
+ "worktree=$dir/missing-worktree" \
847
+ "project=$project" \
848
+ "kind=scout" \
849
+ "decisions_reviewed=1" \
850
+ "decision_keys="
851
+ printf '[]\n' > "$dir/responses/1.out"
852
+ printf '[{"tab_id":3,"name":"fm-zghost"}]\n' > "$dir/responses/2.out"
853
+ fb=$(make_zellij_fakebin "$dir")
854
+ out=$( PATH="$fb:$PATH" FM_STATE_OVERRIDE="$state" FM_DATA_OVERRIDE="$data" FM_CONFIG_OVERRIDE="$config" \
855
+ FM_ZELLIJ_LOG="$dir/log" FM_ZELLIJ_RESPONSES="$dir/responses" FM_ZELLIJ_SESSION_LIST="firstmate" \
856
+ "$ROOT/bin/fm-teardown.sh" zghost 2>&1 )
857
+ status=$?
858
+ expect_code 0 "$status" "fm-teardown should succeed for a zellij scout whose worktree is already gone: $out"
859
+ zellij_assert_call_order "$dir/log" $'\x1f''list-panes'$'\x1f''--json' $'\x1f''list-tabs'$'\x1f''--json' \
860
+ "fm-teardown did not verify the recorded zellij_tab_id against the task label"
861
+ assert_contains "$(cat "$dir/log")" $'\x1f''close-tab-by-id'$'\x1f''3' \
862
+ "fm-teardown did not pass a verified recorded zellij_tab_id through to kill"
863
+ assert_not_contains "$(cat "$dir/log")" $'\x1f''close-pane' \
864
+ "fm-teardown should close the recorded tab id instead of falling back to close-pane"
865
+ pass "fm-teardown.sh: passes recorded zellij_tab_id with the expected task label"
866
+ }
867
+
868
+ test_forced_secondmate_teardown_kills_zellij_children_with_child_home_tag() {
869
+ local dir state data config home project fb out status child_title
870
+ dir="$TMP_ROOT/teardown-zellij-secondmate-child"; state="$dir/state"; data="$dir/data"; config="$dir/config"; home="$dir/secondmate-home"; project="$dir/project"
871
+ mkdir -p "$state" "$data" "$config" "$home/state" "$home/data" "$home/config" "$home/projects" "$project" "$dir/responses"
872
+ printf 'smz\n' > "$home/.fm-secondmate-home"
873
+ fm_write_meta "$state/smz.meta" \
874
+ "window=firstmate:99" \
875
+ "endpoint_task_id=smz" \
876
+ "backend=zellij" \
877
+ "zellij_session=firstmate" \
878
+ "zellij_tab_id=99" \
879
+ "zellij_pane_id=99" \
880
+ "worktree=$home" \
881
+ "project=$home" \
882
+ "kind=secondmate" \
883
+ "mode=secondmate" \
884
+ "home=$home"
885
+ fm_write_meta "$home/state/childz.meta" \
886
+ "window=firstmate:7" \
887
+ "endpoint_task_id=childz" \
888
+ "backend=zellij" \
889
+ "zellij_session=firstmate" \
890
+ "zellij_tab_id=4" \
891
+ "zellij_pane_id=7" \
892
+ "worktree=$dir/missing-child-worktree" \
893
+ "project=$project" \
894
+ "kind=scout"
895
+ child_title=$(zellij_expected_scoped_title fm-childz "$home" "$home")
896
+ zellij_pane_response "$dir" 1 7 4
897
+ zellij_tab_response "$dir" 2 4 "$child_title"
898
+ printf '[]\n' > "$dir/responses/3.out"
899
+ fb=$(make_zellij_fakebin "$dir")
900
+ out=$( PATH="$fb:$PATH" FM_STATE_OVERRIDE="$state" FM_DATA_OVERRIDE="$data" FM_CONFIG_OVERRIDE="$config" \
901
+ FM_ROOT_OVERRIDE="$ROOT" \
902
+ FM_ZELLIJ_LOG="$dir/log" FM_ZELLIJ_RESPONSES="$dir/responses" FM_ZELLIJ_SESSION_LIST="firstmate" \
903
+ "$ROOT/bin/fm-teardown.sh" smz --force 2>&1 )
904
+ status=$?
905
+ expect_code 0 "$status" "fm-teardown should force-retire a secondmate with a zellij child: $out"
906
+ assert_contains "$(cat "$dir/log")" $'\x1f''close-tab-by-id'$'\x1f''4' \
907
+ "forced secondmate teardown did not close a child zellij tab scoped to the child home"
908
+ pass "fm-teardown.sh: force cleanup kills zellij children using the child home tag"
909
+ }
910
+
911
+ # --- send_text_submit: classifier-based verify-and-retry ---------------------
912
+ #
913
+ # The old content-diff strategy ("pane changed after Enter = submitted") was
914
+ # the fleet's only FALSE-POSITIVE delivery confirmation and is deleted; these
915
+ # tests pin its replacement: the shared composer classifier read through
916
+ # `dump-screen --ansi` (styled=1), where only a positively classified empty
917
+ # composer confirms delivery.
918
+ # Call numbering per attempt: list-panes + paste, then per Enter attempt
919
+ # list-panes + send-keys followed by list-panes + dump-screen --ansi.
920
+
921
+ test_send_text_submit_detects_landed_send() {
922
+ local dir fb out
923
+ dir="$TMP_ROOT/submit-ok"; mkdir -p "$dir/responses"
924
+ zellij_pane_response "$dir" 1 7 3
925
+ printf '%s' $'❯ ' > "$dir/responses/2.out"
926
+ zellij_pane_response "$dir" 3 7 3
927
+ zellij_pane_response "$dir" 5 7 3
928
+ printf '%s' $'❯ hello captain' > "$dir/responses/6.out"
929
+ zellij_pane_response "$dir" 7 7 3
930
+ zellij_pane_response "$dir" 9 7 3
931
+ printf '%s' $'hello captain\n❯ ' > "$dir/responses/10.out"
932
+ fb=$(make_zellij_fakebin "$dir")
933
+ out=$( PATH="$fb:$PATH" FM_ZELLIJ_LOG="$dir/log" FM_ZELLIJ_RESPONSES="$dir/responses" \
934
+ FM_ZELLIJ_SESSION_LIST="firstmate" \
935
+ bash -c '. "$0/bin/backends/zellij.sh"; fm_backend_zellij_send_text_submit firstmate:7 "hello captain" 3 0.01 0.01' "$ROOT" )
936
+ [ "$out" = empty ] || fail "send_text_submit should report empty once the composer positively classifies empty, got '$out'"
937
+ zellij_assert_call_order "$dir/log" $'\x1f''list-panes'$'\x1f''--json' $'\x1f''paste' \
938
+ "send_text_submit did not verify the pane before paste"
939
+ zellij_assert_call_order "$dir/log" $'\x1f''list-panes'$'\x1f''--json' $'\x1f''dump-screen' \
940
+ "send_text_submit did not verify the pane before capture"
941
+ assert_contains "$(cat "$dir/log")" $'\x1f''dump-screen'$'\x1f''--pane-id'$'\x1f''7'$'\x1f''--ansi' \
942
+ "send_text_submit did not read the composer through the styled dump"
943
+ assert_contains "$(cat "$dir/log")" $'\x1f''paste'$'\x1f''--pane-id'$'\x1f''7'$'\x1f''--'$'\x1f''hello captain' "send_text_submit did not type the literal text first"
944
+ pass "fm_backend_zellij_send_text_submit: reports 'empty' once the composer classifies empty (submitted)"
945
+ }
946
+
947
+ test_send_text_submit_detects_swallowed_enter() {
948
+ local dir fb out
949
+ dir="$TMP_ROOT/submit-swallow"; mkdir -p "$dir/responses"
950
+ zellij_pane_response "$dir" 1 7 3
951
+ printf '%s' $'❯ ' > "$dir/responses/2.out"
952
+ zellij_pane_response "$dir" 3 7 3
953
+ zellij_pane_response "$dir" 5 7 3
954
+ printf '%s' $'❯ hello captain' > "$dir/responses/6.out"
955
+ zellij_pane_response "$dir" 7 7 3
956
+ zellij_pane_response "$dir" 9 7 3
957
+ printf '%s' $'❯ hello captain' > "$dir/responses/10.out"
958
+ zellij_pane_response "$dir" 11 7 3
959
+ zellij_pane_response "$dir" 13 7 3
960
+ printf '%s' $'❯ hello captain' > "$dir/responses/14.out"
961
+ fb=$(make_zellij_fakebin "$dir")
962
+ out=$( PATH="$fb:$PATH" FM_ZELLIJ_LOG="$dir/log" FM_ZELLIJ_RESPONSES="$dir/responses" \
963
+ FM_ZELLIJ_SESSION_LIST="firstmate" \
964
+ bash -c '. "$0/bin/backends/zellij.sh"; fm_backend_zellij_send_text_submit firstmate:7 "hello captain" 2 0.01 0.01' "$ROOT" )
965
+ [ "$out" = pending ] || fail "send_text_submit should report pending once retries are exhausted with the text still in the composer, got '$out'"
966
+ zellij_assert_call_order "$dir/log" $'\x1f''list-panes'$'\x1f''--json' $'\x1f''send-keys' \
967
+ "send_text_submit did not verify the pane before send-keys"
968
+ pass "fm_backend_zellij_send_text_submit: reports 'pending' when the composer still holds the text after retried Enters (swallowed)"
969
+ }
970
+
971
+ test_send_text_submit_unrelated_change_is_not_delivery() {
972
+ # THE false-positive regression (audit fm-composer-consolidation-audit-s1,
973
+ # section 3.5, verified live): a pane whose content changes for reasons
974
+ # unrelated to submission - a clock, a spinner, streaming output - must NOT
975
+ # read as delivered while the typed text still sits in the composer. The
976
+ # deleted content-diff heuristic reported `empty` here and let fm-send close
977
+ # --resolve-key decision records for a message the crew never received.
978
+ local dir fb out
979
+ dir="$TMP_ROOT/submit-false-positive"; mkdir -p "$dir/responses"
980
+ zellij_pane_response "$dir" 1 7 3
981
+ printf '%s' $'clock 11:59:59\n❯ ' > "$dir/responses/2.out"
982
+ zellij_pane_response "$dir" 3 7 3
983
+ zellij_pane_response "$dir" 5 7 3
984
+ printf '%s' $'clock 12:00:00\n❯ hello captain' > "$dir/responses/6.out"
985
+ zellij_pane_response "$dir" 7 7 3
986
+ zellij_pane_response "$dir" 9 7 3
987
+ printf '%s' $'clock 12:00:01\n❯ hello captain' > "$dir/responses/10.out"
988
+ zellij_pane_response "$dir" 11 7 3
989
+ zellij_pane_response "$dir" 13 7 3
990
+ printf '%s' $'clock 12:00:02\n❯ hello captain' > "$dir/responses/14.out"
991
+ fb=$(make_zellij_fakebin "$dir")
992
+ out=$( PATH="$fb:$PATH" FM_ZELLIJ_LOG="$dir/log" FM_ZELLIJ_RESPONSES="$dir/responses" \
993
+ FM_ZELLIJ_SESSION_LIST="firstmate" \
994
+ bash -c '. "$0/bin/backends/zellij.sh"; fm_backend_zellij_send_text_submit firstmate:7 "hello captain" 2 0.01 0.01' "$ROOT" )
995
+ [ "$out" != empty ] || fail "an unrelated pane change must never read as delivered (the content-diff false positive)"
996
+ [ "$out" = pending ] || fail "the still-typed composer should classify pending, got '$out'"
997
+ pass "fm_backend_zellij_send_text_submit: an unrelated pane change is not a delivery confirmation (false-positive regression)"
998
+ }
999
+
1000
+ test_send_text_submit_rejects_unobserved_paste() {
1001
+ local dir fb out
1002
+ dir="$TMP_ROOT/submit-unobserved"; mkdir -p "$dir/responses"
1003
+ zellij_pane_response "$dir" 1 7 3
1004
+ printf '%s' $'transcript line\n❯ ' > "$dir/responses/2.out"
1005
+ zellij_pane_response "$dir" 3 7 3
1006
+ zellij_pane_response "$dir" 5 7 3
1007
+ printf '%s' $'transcript line\n❯ ' > "$dir/responses/6.out"
1008
+ fb=$(make_zellij_fakebin "$dir")
1009
+ out=$( PATH="$fb:$PATH" FM_ZELLIJ_LOG="$dir/log" FM_ZELLIJ_RESPONSES="$dir/responses" \
1010
+ FM_ZELLIJ_SESSION_LIST="firstmate" \
1011
+ bash -c '. "$0/bin/backends/zellij.sh"; fm_backend_zellij_send_text_submit firstmate:7 "hello captain" 2 0.01 0.01' "$ROOT" )
1012
+ [ "$out" = send-failed ] || fail "an unobserved paste should report send-failed, got '$out'"
1013
+ assert_not_contains "$(cat "$dir/log")" $'\x1f''send-keys' \
1014
+ "send_text_submit should not send Enter when the pasted text was not observed"
1015
+ pass "fm_backend_zellij_send_text_submit: refuses confirmation when paste exits successfully without typing"
1016
+ }
1017
+
1018
+ test_send_text_submit_rejects_transcript_echo_with_unrelated_draft() {
1019
+ local dir fb out
1020
+ dir="$TMP_ROOT/submit-transcript-echo"; mkdir -p "$dir/responses"
1021
+ zellij_pane_response "$dir" 1 7 3
1022
+ printf '%s' $'hello captain\n❯ unrelated draft' > "$dir/responses/2.out"
1023
+ zellij_pane_response "$dir" 3 7 3
1024
+ zellij_pane_response "$dir" 5 7 3
1025
+ printf '%s' $'hello captain\n❯ unrelated draft' > "$dir/responses/6.out"
1026
+ fb=$(make_zellij_fakebin "$dir")
1027
+ out=$( PATH="$fb:$PATH" FM_ZELLIJ_LOG="$dir/log" FM_ZELLIJ_RESPONSES="$dir/responses" \
1028
+ FM_ZELLIJ_SESSION_LIST="firstmate" \
1029
+ bash -c '. "$0/bin/backends/zellij.sh"; fm_backend_zellij_send_text_submit firstmate:7 "hello captain" 2 0.01 0.01' "$ROOT" )
1030
+ [ "$out" = send-failed ] || fail "a transcript echo outside an unrelated draft should report send-failed, got '$out'"
1031
+ assert_not_contains "$(cat "$dir/log")" $'\x1f''send-keys' \
1032
+ "send_text_submit should not send Enter when only a transcript echo matches the intended text"
1033
+ pass "fm_backend_zellij_send_text_submit: transcript echoes outside the selected composer cannot prove typing"
1034
+ }
1035
+
1036
+ test_send_text_submit_rejects_existing_intended_text_after_noop_paste() {
1037
+ local dir fb out
1038
+ dir="$TMP_ROOT/submit-existing-text-noop"; mkdir -p "$dir/responses"
1039
+ zellij_pane_response "$dir" 1 7 3
1040
+ printf '%s' $'❯ hello captain' > "$dir/responses/2.out"
1041
+ zellij_pane_response "$dir" 3 7 3
1042
+ zellij_pane_response "$dir" 5 7 3
1043
+ printf '%s' $'❯ hello captain' > "$dir/responses/6.out"
1044
+ fb=$(make_zellij_fakebin "$dir")
1045
+ out=$( PATH="$fb:$PATH" FM_ZELLIJ_LOG="$dir/log" FM_ZELLIJ_RESPONSES="$dir/responses" \
1046
+ FM_ZELLIJ_SESSION_LIST="firstmate" \
1047
+ bash -c '. "$0/bin/backends/zellij.sh"; fm_backend_zellij_send_text_submit firstmate:7 "hello captain" 2 0.01 0.01' "$ROOT" )
1048
+ [ "$out" = send-failed ] || fail "pre-existing intended text after a no-op paste should report send-failed, got '$out'"
1049
+ assert_not_contains "$(cat "$dir/log")" $'\x1f''send-keys' \
1050
+ "send_text_submit should not send Enter without an observed composer delta"
1051
+ pass "fm_backend_zellij_send_text_submit: pre-existing text cannot prove a no-op paste landed"
1052
+ }
1053
+
1054
+ test_send_text_submit_rejects_furniture_match_after_noop_paste() {
1055
+ local dir fb out
1056
+ dir="$TMP_ROOT/submit-furniture-noop"; mkdir -p "$dir/responses"
1057
+ zellij_pane_response "$dir" 1 7 3
1058
+ printf '%s' $'┃ unrelated draft\n┃ Build · GPT-5.5 Fast OpenAI · high' > "$dir/responses/2.out"
1059
+ zellij_pane_response "$dir" 3 7 3
1060
+ zellij_pane_response "$dir" 5 7 3
1061
+ printf '%s' $'┃ unrelated draft\n┃ Build · GPT-5.5 Fast OpenAI · high' > "$dir/responses/6.out"
1062
+ fb=$(make_zellij_fakebin "$dir")
1063
+ out=$( PATH="$fb:$PATH" FM_ZELLIJ_LOG="$dir/log" FM_ZELLIJ_RESPONSES="$dir/responses" \
1064
+ FM_ZELLIJ_SESSION_LIST="firstmate" \
1065
+ bash -c '. "$0/bin/backends/zellij.sh"; fm_backend_zellij_send_text_submit firstmate:7 "high" 2 0.01 0.01' "$ROOT" )
1066
+ [ "$out" = send-failed ] || fail "footer furniture matching a short steer should report send-failed, got '$out'"
1067
+ assert_not_contains "$(cat "$dir/log")" $'\x1f''send-keys' \
1068
+ "send_text_submit should not send Enter when only furniture matches the steer"
1069
+ pass "fm_backend_zellij_send_text_submit: unrelated drafts and furniture cannot prove typing"
1070
+ }
1071
+
1072
+ test_send_text_submit_accepts_wrapped_boxed_text() {
1073
+ local dir fb out
1074
+ dir="$TMP_ROOT/submit-wrapped-box"; mkdir -p "$dir/responses"
1075
+ zellij_pane_response "$dir" 1 7 3
1076
+ printf '%s' $'╭────────────────────╮\n│ > Type a message...│\n╰────────────────────╯' > "$dir/responses/2.out"
1077
+ zellij_pane_response "$dir" 3 7 3
1078
+ zellij_pane_response "$dir" 5 7 3
1079
+ printf '%s' $'╭────────────────────╮\n│ > hello │\n│ captain │\n╰────────────────────╯' > "$dir/responses/6.out"
1080
+ zellij_pane_response "$dir" 7 7 3
1081
+ zellij_pane_response "$dir" 9 7 3
1082
+ printf '%s' $'╭────────────────────╮\n│ ❯ │\n╰────────────────────╯' > "$dir/responses/10.out"
1083
+ fb=$(make_zellij_fakebin "$dir")
1084
+ out=$( PATH="$fb:$PATH" FM_ZELLIJ_LOG="$dir/log" FM_ZELLIJ_RESPONSES="$dir/responses" \
1085
+ FM_ZELLIJ_SESSION_LIST="firstmate" \
1086
+ bash -c '. "$0/bin/backends/zellij.sh"; fm_backend_zellij_send_text_submit firstmate:7 "hello captain" 2 0.01 0.01' "$ROOT" )
1087
+ [ "$out" = empty ] || fail "wrapped text replacing a shell-prompt placeholder should be observed and submitted, got '$out'"
1088
+ assert_contains "$(cat "$dir/log")" $'\x1f''send-keys' \
1089
+ "send_text_submit should send Enter after observing wrapped boxed text"
1090
+ pass "fm_backend_zellij_send_text_submit: observes wrapped text replacing a shell-prompt placeholder"
1091
+ }
1092
+
1093
+ test_send_text_submit_accepts_wrapped_bare_text() {
1094
+ local dir fb out text
1095
+ dir="$TMP_ROOT/submit-wrapped-bare"; mkdir -p "$dir/responses"
1096
+ text='this deliberately long steer wraps across a bare continuation row'
1097
+ zellij_pane_response "$dir" 1 7 3
1098
+ printf '%s' $'❯ ' > "$dir/responses/2.out"
1099
+ zellij_pane_response "$dir" 3 7 3
1100
+ zellij_pane_response "$dir" 5 7 3
1101
+ printf '%s' $'❯ this deliberately long steer\nwraps across a bare continuation row' > "$dir/responses/6.out"
1102
+ zellij_pane_response "$dir" 7 7 3
1103
+ zellij_pane_response "$dir" 9 7 3
1104
+ printf '%s' $'this deliberately long steer wraps across a bare continuation row\n❯ ' > "$dir/responses/10.out"
1105
+ fb=$(make_zellij_fakebin "$dir")
1106
+ out=$( PATH="$fb:$PATH" FM_ZELLIJ_LOG="$dir/log" FM_ZELLIJ_RESPONSES="$dir/responses" \
1107
+ FM_ZELLIJ_SESSION_LIST="firstmate" \
1108
+ bash -c '. "$0/bin/backends/zellij.sh"; fm_backend_zellij_send_text_submit firstmate:7 "$1" 2 0.01 0.01' "$ROOT" "$text" )
1109
+ [ "$out" = empty ] || fail "wrapped text in a bare composer should be observed and submitted, got '$out'"
1110
+ assert_contains "$(cat "$dir/log")" $'\x1f''send-keys' \
1111
+ "send_text_submit should send Enter after observing wrapped bare text"
1112
+ pass "fm_backend_zellij_send_text_submit: observes wrapped text in a bare composer"
1113
+ }
1114
+
1115
+ test_send_text_submit_preserves_agent_glyph_within_wrapped_content() {
1116
+ local dir fb out text
1117
+ dir="$TMP_ROOT/submit-wrapped-agent-glyph"; mkdir -p "$dir/responses"
1118
+ text='hello ❯ captain'
1119
+ zellij_pane_response "$dir" 1 7 3
1120
+ printf '%s' $'❯ ' > "$dir/responses/2.out"
1121
+ zellij_pane_response "$dir" 3 7 3
1122
+ zellij_pane_response "$dir" 5 7 3
1123
+ printf '%s' $'❯ hello ❯\ncaptain' > "$dir/responses/6.out"
1124
+ zellij_pane_response "$dir" 7 7 3
1125
+ zellij_pane_response "$dir" 9 7 3
1126
+ printf '%s' $'hello ❯ captain\n❯ ' > "$dir/responses/10.out"
1127
+ fb=$(make_zellij_fakebin "$dir")
1128
+ out=$( PATH="$fb:$PATH" FM_ZELLIJ_LOG="$dir/log" FM_ZELLIJ_RESPONSES="$dir/responses" \
1129
+ FM_ZELLIJ_SESSION_LIST="firstmate" \
1130
+ bash -c '. "$0/bin/backends/zellij.sh"; fm_backend_zellij_send_text_submit firstmate:7 "$1" 2 0.01 0.01' "$ROOT" "$text" )
1131
+ [ "$out" = empty ] || fail "an agent glyph within wrapped content should remain user content, got '$out'"
1132
+ assert_contains "$(cat "$dir/log")" $'\x1f''send-keys' \
1133
+ "send_text_submit should send Enter after preserving a mid-row agent glyph"
1134
+ pass "fm_backend_zellij_send_text_submit: preserves agent glyphs within wrapped content"
1135
+ }
1136
+
1137
+ test_send_text_submit_rejects_stale_composer_above_live_shell() {
1138
+ local dir fb out
1139
+ dir="$TMP_ROOT/submit-live-shell"; mkdir -p "$dir/responses"
1140
+ zellij_pane_response "$dir" 1 7 3
1141
+ printf '%s' $'❯\n$ ' > "$dir/responses/2.out"
1142
+ fb=$(make_zellij_fakebin "$dir")
1143
+ out=$( PATH="$fb:$PATH" FM_ZELLIJ_LOG="$dir/log" FM_ZELLIJ_RESPONSES="$dir/responses" \
1144
+ FM_ZELLIJ_SESSION_LIST="firstmate" \
1145
+ bash -c '. "$0/bin/backends/zellij.sh"; fm_backend_zellij_send_text_submit firstmate:7 "claude" 2 0.01 0.01' "$ROOT" )
1146
+ [ "$out" = send-failed ] || fail "a stale composer above a live shell should report send-failed, got '$out'"
1147
+ assert_not_contains "$(cat "$dir/log")" $'\x1f''paste' \
1148
+ "send_text_submit must not paste into a live shell below a stale composer"
1149
+ pass "fm_backend_zellij_send_text_submit: refuses a live shell below a stale composer"
1150
+ }
1151
+
1152
+ test_composer_state_reads_styled_dump() {
1153
+ local dir fb out
1154
+ dir="$TMP_ROOT/composer-styled"; mkdir -p "$dir/responses"
1155
+ zellij_pane_response "$dir" 1 7 3
1156
+ # Real claude-in-zellij capture shape (audit section 3.5): ESC[m ❯ U+00A0.
1157
+ printf 'transcript line\n\033[m\342\235\257\302\240' > "$dir/responses/2.out"
1158
+ fb=$(make_zellij_fakebin "$dir")
1159
+ out=$( PATH="$fb:$PATH" FM_ZELLIJ_LOG="$dir/log" FM_ZELLIJ_RESPONSES="$dir/responses" \
1160
+ FM_ZELLIJ_SESSION_LIST="firstmate" \
1161
+ bash -c '. "$0/bin/backends/zellij.sh"; fm_backend_zellij_composer_state firstmate:7' "$ROOT" )
1162
+ [ "$out" = empty ] || fail "the real claude-in-zellij ANSI dump should classify empty, got '$out'"
1163
+ assert_contains "$(cat "$dir/log")" $'\x1f''dump-screen'$'\x1f''--pane-id'$'\x1f''7'$'\x1f''--ansi' \
1164
+ "composer_state did not request the styled dump"
1165
+ pass "fm_backend_zellij_composer_state: classifies the real claude-in-zellij --ansi dump as empty"
1166
+ }
1167
+
1168
+ test_composer_state_dead_pane_is_unknown() {
1169
+ # The unconditional-exit-0 CLI quirk (file header): a dead target dumps
1170
+ # nothing. Both the styled and the plain fallback come back empty, so the
1171
+ # verdict must be unknown - never a confirmation.
1172
+ local dir fb out
1173
+ dir="$TMP_ROOT/composer-dead"; mkdir -p "$dir/responses"
1174
+ zellij_pane_response "$dir" 1 7 3
1175
+ zellij_pane_response "$dir" 3 7 3
1176
+ : > "$dir/responses/2.out"
1177
+ : > "$dir/responses/4.out"
1178
+ fb=$(make_zellij_fakebin "$dir")
1179
+ out=$( PATH="$fb:$PATH" FM_ZELLIJ_LOG="$dir/log" FM_ZELLIJ_RESPONSES="$dir/responses" \
1180
+ FM_ZELLIJ_SESSION_LIST="firstmate" \
1181
+ bash -c '. "$0/bin/backends/zellij.sh"; fm_backend_zellij_composer_state firstmate:7' "$ROOT" )
1182
+ [ "$out" = unknown ] || fail "a dead pane's empty dumps must classify unknown, got '$out'"
1183
+ pass "fm_backend_zellij_composer_state: a dead pane (empty dumps) reads unknown, never a confirmation"
1184
+ }
1185
+
1186
+ test_send_text_submit_send_failed_when_session_absent() {
1187
+ local dir fb out
1188
+ dir="$TMP_ROOT/submit-no-session"; mkdir -p "$dir/responses"
1189
+ fb=$(make_zellij_fakebin "$dir")
1190
+ out=$( PATH="$fb:$PATH" FM_ZELLIJ_LOG="$dir/log" FM_ZELLIJ_RESPONSES="$dir/responses" \
1191
+ FM_ZELLIJ_SESSION_LIST="" \
1192
+ bash -c '. "$0/bin/backends/zellij.sh"; fm_backend_zellij_send_text_submit firstmate:7 "x" 2 0.01 0.01' "$ROOT" )
1193
+ [ "$out" = send-failed ] || fail "send_text_submit should report send-failed when the session does not exist, got '$out'"
1194
+ pass "fm_backend_zellij_send_text_submit: reports 'send-failed' when the target session is not active"
1195
+ }
1196
+
1197
+ test_send_text_submit_send_failed_when_pane_absent() {
1198
+ local dir fb out
1199
+ dir="$TMP_ROOT/submit-no-pane"; mkdir -p "$dir/responses"
1200
+ printf '[]\n' > "$dir/responses/1.out"
1201
+ fb=$(make_zellij_fakebin "$dir")
1202
+ out=$( PATH="$fb:$PATH" FM_ZELLIJ_LOG="$dir/log" FM_ZELLIJ_RESPONSES="$dir/responses" \
1203
+ FM_ZELLIJ_SESSION_LIST="firstmate" \
1204
+ bash -c '. "$0/bin/backends/zellij.sh"; fm_backend_zellij_send_text_submit firstmate:7 "x" 2 0.01 0.01' "$ROOT" )
1205
+ [ "$out" = send-failed ] || fail "send_text_submit should report send-failed when the pane does not exist, got '$out'"
1206
+ assert_not_contains "$(cat "$dir/log")" $'\x1f''paste' \
1207
+ "send_text_submit should not paste text after pane readiness fails"
1208
+ pass "fm_backend_zellij_send_text_submit: reports 'send-failed' when the target pane is absent"
1209
+ }
1210
+
1211
+ # --- fm-*.sh script routing via explicit backend-tagged meta ------------------
1212
+
1213
+ test_scripts_route_explicit_target_through_meta_backend() {
1214
+ local dir state fb neutral out
1215
+ dir="$TMP_ROOT/script-explicit-target"; state="$dir/state"; mkdir -p "$state" "$dir/responses"
1216
+ neutral="$dir/neutral-root"; mkdir -p "$neutral"
1217
+ fm_write_meta "$state/zellij-stale.meta" "window=firstmate:7" "backend=zellij"
1218
+ touch "$state/.last-watcher-beat"
1219
+ zellij_pane_response "$dir" 1 7 3
1220
+ printf 'captured zellij pane\n' > "$dir/responses/2.out"
1221
+ zellij_pane_response "$dir" 3 7 3
1222
+ fb=$(make_zellij_fakebin "$dir")
1223
+ cat > "$fb/tmux" <<'SH'
1224
+ #!/usr/bin/env bash
1225
+ set -u
1226
+ printf 'tmux should not be used for a metadata-matched zellij target\n' >&2
1227
+ exit 42
1228
+ SH
1229
+ chmod +x "$fb/tmux"
1230
+
1231
+ out=$( PATH="$fb:$PATH" FM_ROOT_OVERRIDE="$neutral" FM_STATE_OVERRIDE="$state" \
1232
+ FM_ZELLIJ_LOG="$dir/log" FM_ZELLIJ_RESPONSES="$dir/responses" FM_ZELLIJ_SESSION_LIST="firstmate" \
1233
+ "$ROOT/bin/fm-peek.sh" firstmate:7 5 2>/dev/null )
1234
+ [ "$out" = "captured zellij pane" ] || fail "fm-peek did not capture through zellij for an explicit metadata-matched target, got '$out'"
1235
+ zellij_assert_call_order "$dir/log" $'\x1f''list-panes'$'\x1f''--json' $'\x1f''dump-screen' \
1236
+ "fm-peek did not verify the pane before capture"
1237
+ assert_contains "$(cat "$dir/log")" $'\x1f''dump-screen'$'\x1f''--pane-id'$'\x1f''7' \
1238
+ "fm-peek did not route the explicit metadata-matched target through zellij capture"
1239
+
1240
+ : > "$dir/log"
1241
+ PATH="$fb:$PATH" FM_ROOT_OVERRIDE="$neutral" FM_HOME="$neutral" FM_STATE_OVERRIDE="$state" \
1242
+ FM_ZELLIJ_LOG="$dir/log" FM_ZELLIJ_RESPONSES="$dir/responses" FM_ZELLIJ_SESSION_LIST="firstmate" \
1243
+ "$ROOT/bin/fm-send.sh" firstmate:7 --key Escape >/dev/null 2>&1
1244
+ expect_code 0 $? "fm-send --key should route an explicit metadata-matched target through zellij"
1245
+ zellij_assert_call_order "$dir/log" $'\x1f''list-panes'$'\x1f''--json' $'\x1f''send-keys' \
1246
+ "fm-send did not verify the pane before send-key"
1247
+ assert_contains "$(cat "$dir/log")" $'\x1f''send-keys'$'\x1f''--pane-id'$'\x1f''7'$'\x1f''Esc' \
1248
+ "fm-send did not route the explicit metadata-matched target through zellij send-key"
1249
+
1250
+ pass "fm-peek/fm-send: explicit metadata-matched targets use the recorded zellij backend"
1251
+ }
1252
+
1253
+ test_scripts_verify_label_for_fm_targets() {
1254
+ local dir state fb neutral out
1255
+ dir="$TMP_ROOT/script-fm-target-label"; state="$dir/state"; mkdir -p "$state" "$dir/responses"
1256
+ neutral="$dir/neutral-root"; mkdir -p "$neutral"
1257
+ fm_write_meta "$state/zlabel.meta" "window=firstmate:7" "backend=zellij"
1258
+ touch "$state/.last-watcher-beat"
1259
+ zellij_pane_response "$dir" 1 7 3
1260
+ zellij_tab_response "$dir" 2 3 fm-zlabel
1261
+ printf 'captured through fm-id\n' > "$dir/responses/3.out"
1262
+ fb=$(make_zellij_fakebin "$dir")
1263
+
1264
+ out=$( PATH="$fb:$PATH" FM_ROOT_OVERRIDE="$neutral" FM_STATE_OVERRIDE="$state" \
1265
+ FM_ZELLIJ_LOG="$dir/log" FM_ZELLIJ_RESPONSES="$dir/responses" FM_ZELLIJ_SESSION_LIST="firstmate" \
1266
+ "$ROOT/bin/fm-peek.sh" fm-zlabel 5 2>/dev/null )
1267
+ [ "$out" = "captured through fm-id" ] || fail "fm-peek did not capture through zellij for an fm-id target with a matching tab label, got '$out'"
1268
+ zellij_assert_call_order "$dir/log" $'\x1f''list-tabs'$'\x1f''--json' $'\x1f''dump-screen' \
1269
+ "fm-peek did not verify the fm-id tab label before capture"
1270
+
1271
+ pass "fm-peek: fm-id zellij targets verify the owning tab label before capture"
1272
+ }
1273
+
1274
+ test_scripts_reject_fm_target_label_mismatch() {
1275
+ local dir state fb neutral status
1276
+ dir="$TMP_ROOT/script-fm-target-label-mismatch"; state="$dir/state"; mkdir -p "$state" "$dir/responses"
1277
+ neutral="$dir/neutral-root"; mkdir -p "$neutral"
1278
+ fm_write_meta "$state/zreuse.meta" "window=firstmate:7" "backend=zellij"
1279
+ touch "$state/.last-watcher-beat"
1280
+ zellij_pane_response "$dir" 1 7 3
1281
+ zellij_tab_response "$dir" 2 3 not-the-task
1282
+ fb=$(make_zellij_fakebin "$dir")
1283
+
1284
+ PATH="$fb:$PATH" FM_ROOT_OVERRIDE="$neutral" FM_HOME="$neutral" FM_STATE_OVERRIDE="$state" \
1285
+ FM_ZELLIJ_LOG="$dir/log" FM_ZELLIJ_RESPONSES="$dir/responses" FM_ZELLIJ_SESSION_LIST="firstmate" \
1286
+ "$ROOT/bin/fm-send.sh" fm-zreuse --key Escape >/dev/null 2>&1
1287
+ status=$?
1288
+ [ "$status" -ne 0 ] || fail "fm-send --key should reject an fm-id zellij target whose pane belongs to a differently named tab"
1289
+ assert_not_contains "$(cat "$dir/log")" $'\x1f''send-keys' \
1290
+ "fm-send should not send a key after fm-id label verification fails"
1291
+ pass "fm-send: fm-id zellij targets reject pane ids whose tab label no longer matches"
1292
+ }
1293
+
1294
+ # shellcheck source=/dev/null
1295
+ . "$ROOT/bin/fm-backend.sh"
1296
+
1297
+ test_version_check_accepts_current_version
1298
+ test_version_check_accepts_newer_version
1299
+ test_version_check_refuses_old_version
1300
+ test_version_check_refuses_missing_zellij
1301
+ test_session_defaults_to_firstmate
1302
+ test_session_honors_override
1303
+ test_parse_target
1304
+ test_normalize_key
1305
+ test_scoped_title_uses_primary_home_label
1306
+ test_scoped_title_uses_secondmate_home_label
1307
+ test_scoped_title_changes_with_root_path
1308
+ test_expected_label_accepts_unambiguous_untagged_legacy_tab
1309
+ test_expected_label_refuses_ambiguous_untagged_tab
1310
+ test_list_live_scopes_to_own_home_tag
1311
+ test_resolve_bare_selector_prefers_scoped_title
1312
+ test_resolve_bare_selector_refuses_ambiguous_untagged
1313
+ test_resolve_bare_selector_prefers_later_session_scoped_title_over_legacy
1314
+ test_resolve_bare_selector_refuses_cross_session_ambiguous_untagged
1315
+ test_session_exists_true_when_listed
1316
+ test_session_exists_false_when_absent
1317
+ test_server_ensure_skips_attach_when_already_exists
1318
+ test_dispatch_routes_zellij_backend
1319
+ test_dispatch_busy_state_unknown_for_zellij
1320
+ test_create_task_refuses_duplicate_label
1321
+ test_create_task_creates_and_parses_ids
1322
+ test_create_task_restores_previously_active_tab
1323
+ test_create_task_no_restore_when_new_tab_was_already_active
1324
+ test_capture_small_reads_use_viewport_and_trim
1325
+ test_capture_large_reads_use_full_scrollback_and_trim
1326
+ test_capture_fails_when_pane_absent
1327
+ test_capture_fails_when_session_absent
1328
+ test_send_key_normalizes_and_targets_pane
1329
+ test_send_literal_uses_paste_separator_for_option_shaped_text
1330
+ test_send_text_line_clears_partial_input_when_enter_fails
1331
+ test_send_text_line_reports_unsafe_input_when_cleanup_fails
1332
+ test_expected_label_allows_matching_task_tab
1333
+ test_expected_label_rejects_reused_pane_id
1334
+ test_current_path_probes_with_marker_and_ignores_prompt_paths
1335
+ test_current_path_ignores_tilde_prefixed_banner_lines
1336
+ test_kill_resolves_tab_and_closes_by_id
1337
+ test_kill_falls_back_to_close_pane_when_tab_lookup_empty
1338
+ test_kill_closes_recorded_tab_when_pane_already_gone
1339
+ test_kill_skips_recorded_tab_when_label_mismatches
1340
+ test_kill_is_noop_when_session_absent
1341
+ test_teardown_passes_recorded_tab_id_to_zellij_kill
1342
+ test_forced_secondmate_teardown_kills_zellij_children_with_child_home_tag
1343
+ test_send_text_submit_detects_landed_send
1344
+ test_send_text_submit_detects_swallowed_enter
1345
+ test_send_text_submit_unrelated_change_is_not_delivery
1346
+ test_send_text_submit_rejects_unobserved_paste
1347
+ test_send_text_submit_rejects_transcript_echo_with_unrelated_draft
1348
+ test_send_text_submit_rejects_existing_intended_text_after_noop_paste
1349
+ test_send_text_submit_rejects_furniture_match_after_noop_paste
1350
+ test_send_text_submit_accepts_wrapped_boxed_text
1351
+ test_send_text_submit_accepts_wrapped_bare_text
1352
+ test_send_text_submit_preserves_agent_glyph_within_wrapped_content
1353
+ test_send_text_submit_rejects_stale_composer_above_live_shell
1354
+ test_composer_state_reads_styled_dump
1355
+ test_composer_state_dead_pane_is_unknown
1356
+ test_send_text_submit_send_failed_when_session_absent
1357
+ test_send_text_submit_send_failed_when_pane_absent
1358
+ test_scripts_route_explicit_target_through_meta_backend
1359
+ test_scripts_verify_label_for_fm_targets
1360
+ test_scripts_reject_fm_target_label_mismatch