@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,1416 @@
1
+ #!/usr/bin/env bash
2
+ # bin/fm-composer-lib.sh - the ONE fleet-wide owner of composer classification:
3
+ # every shape a verified harness draws, every glyph, every container proof, and
4
+ # the empty|pending|pending-unproven|unknown verdict, shared by every
5
+ # session-provider adapter (tmux via bin/fm-tmux-lib.sh, and
6
+ # bin/backends/{herdr,orca,cmux,zellij}.sh) and by fm-spawn.sh's kimi
7
+ # launch-readiness check.
8
+ #
9
+ # WHY THIS EXISTS (tasks fm-composer-shellglyph-safety and
10
+ # fm-composer-thin-adapter-refactor-r1): the adapters each carried their own
11
+ # copy of composer shape knowledge, and every copy drifted. The audited result
12
+ # (data/fm-composer-consolidation-audit-s1) was a 5-adapter x 6-harness matrix
13
+ # in which no adapter was right about more than five harnesses, no two adapters
14
+ # were wrong in the same places, and one harness was unreadable everywhere.
15
+ # The consolidation rule that prevents a recurrence: an adapter CAPTURES a
16
+ # screen and DESCRIBES its capabilities; it never classifies. A new harness
17
+ # shape is taught to fm_composer_classify_screen below, once, and every backend
18
+ # that can capture a screen learns it in the same commit.
19
+ #
20
+ # THE CAPABILITY MODEL: adapters differ in what their capture primitive can
21
+ # see, and those differences enter here as DATA (the <caps> argument), never as
22
+ # adapter code. Capability differences change how CONFIDENTLY a shape can be
23
+ # judged; they never change what the shapes ARE:
24
+ # styled=1 the capture preserves ANSI styling, so ghost/placeholder text
25
+ # is detectable and can be stripped (tmux -e, herdr --format
26
+ # ansi, zellij dump-screen --ansi). With styled=0 (cmux, orca)
27
+ # ghost text is unreadable, so a bare glyph row or left-bar row
28
+ # carrying trailing non-idle text degrades to `unknown` rather
29
+ # than `pending`: the text may be the harness's own idle
30
+ # suggestion, and a false `pending` blocks every safe caller.
31
+ # cursor=1 a cursor row is supplied (tmux #{cursor_y} only). The cursor
32
+ # anchors shape selection: the shape containing the cursor is the
33
+ # composer. Without it, the bottom-most shape wins.
34
+ # identity=1 a native agent identity/state probe exists (herdr `agent get`;
35
+ # the tmux pi foreground-process probe). Identity is what makes
36
+ # Pi's blank separated composer provable; with identity=0 that
37
+ # shape stays `unknown`.
38
+ # rows=<n> the capture's bounded row count (informational).
39
+ #
40
+ # THE STRICT BLANK-ROW RULE (captain decision blank-row-injection-posture,
41
+ # 2026-08-09): a blank or otherwise unidentified input row with no positive
42
+ # container proof is `unknown` and callers defer. This replaced tmux's
43
+ # permissive "blank cursor row = empty = safe to inject" rule fleet-wide: a
44
+ # blank row under the cursor can be a modal dialog, a dead shell between
45
+ # transcript rules, or a mid-redraw pane, and the away-mode injector types
46
+ # escalations into whatever it calls empty. Positive container proof means one
47
+ # of the shapes in the catalogue below.
48
+ #
49
+ # THE SHAPE CATALOGUE (all verified against real harnesses; byte-level
50
+ # captures in data/fm-composer-consolidation-audit-s1/report.md and
51
+ # docs/verification/runtime-backends.md):
52
+ # bordered - a complete boxed composer: a top border, side-bordered content
53
+ # rows of the same family, and a bottom border (grok, kimi,
54
+ # older claude). The bottom border may carry a TITLE (grok
55
+ # writes its model name there); a titled bottom border that
56
+ # still starts and ends with the family's rule glyph is
57
+ # tolerated, not ambiguity.
58
+ # bare - an agent prompt glyph row with no border at all (claude `❯`,
59
+ # codex `›`, muse `⟩`, cursor `→`). The agent glyph is itself the container
60
+ # proof; a bare SHELL glyph (`>` `$` `%` `#`) never is.
61
+ # left-bar - opencode: rows prefixed by a heavy left bar `┃` with no
62
+ # closing border, holding the idle hint, blank rows, and a
63
+ # mode/model footer line.
64
+ # separated - pi: content rows between two solid horizontal `─` rules, no
65
+ # glyph and no side border. Provable only with a live agent
66
+ # identity reporting an idle/done pi (herdr `agent
67
+ # get`; the tmux foreground-process probe), because a blank
68
+ # region between two transcript rules is otherwise exactly the
69
+ # strict rule's unidentifiable blank row.
70
+ #
71
+ # THE SAFETY RULE for glyphs: a bare shell prompt glyph (`>` `$` `%` `#`) -
72
+ # what a pane shows once its agent has exited to a plain login shell - is a
73
+ # genuine empty agent composer ONLY inside a bordered container. On a bare row
74
+ # it is a dead-shell prompt and classifies `unknown` (never a safe injection
75
+ # target). The AGENT glyphs `❯` (claude), `›` (codex), `⟩` (U+27E9, muse),
76
+ # and `→` (U+2192, cursor) are a genuine empty agent composer either way.
77
+ # Both glyph sets are declared
78
+ # exactly once below; every decision reaches them through the declarations.
79
+ #
80
+ # GHOST/PLACEHOLDER TEXT (task afk-herdr-false-pending): a harness fills an
81
+ # otherwise-empty composer with de-emphasized ghost text - claude's rotating
82
+ # prompt suggestion, codex's idle suggestion, grok's placeholder, or cursor's
83
+ # idle placeholder - which a
84
+ # plain capture cannot tell apart from text a human typed.
85
+ # fm_composer_strip_ghost is the ONE ANSI-aware extractor of "real typed
86
+ # content": it drops every de-emphasized run - dim/faint (SGR 2) AND a
87
+ # dark/muted TRUECOLOR foreground - and keeps only normal-intensity,
88
+ # normally-coloured text.
89
+ #
90
+ # UNICODE WHITESPACE (issue #1988; open PRs #1995/#2047 target the same
91
+ # defect and #1995's naming is adopted here so the implementations converge):
92
+ # a harness may separate its prompt glyph from composer content with a
93
+ # non-ASCII space. Real claude 2.x draws its EMPTY composer as exactly `❯`
94
+ # followed by U+00A0 NO-BREAK SPACE. POSIX `[[:space:]]` includes U+00A0 only
95
+ # under some locales, so every trim used to be locale-dependent: the same live
96
+ # pane read `empty` under a UTF-8 shell and `pending` under LC_ALL=C (a
97
+ # daemon, launchd, or ssh context), deferring every away-mode escalation.
98
+ # fm_composer_normalize_trim_var is the one fix: it maps every code point
99
+ # Unicode gives the property White_Space=Yes outside ASCII onto a plain ASCII
100
+ # space before any trim or comparison, byte-exactly, so the verdict cannot
101
+ # depend on the ambient locale. Glyph strips use literal byte-exact pattern
102
+ # removal for the same reason: `${v#?}` removes one BYTE under LC_ALL=C and
103
+ # one CHARACTER under UTF-8, which used to leave partial multibyte residue.
104
+ #
105
+ # Re-sourcing is a cheap idempotent redefinition, so this file needs no
106
+ # include guard (matching bin/fm-tmux-lib.sh).
107
+
108
+ # fm_composer_strip_ansi: drop every CSI escape sequence, leaving plain text.
109
+ # Used for STRUCTURAL row/shape detection, where ghost text must be KEPT so the
110
+ # composer box border or bare prompt glyph is still visible; content extraction
111
+ # uses fm_composer_strip_ghost instead. Reads the styled text on stdin and prints
112
+ # plain text (stdin-only, matching fm_composer_strip_ghost). The character class
113
+ # includes ':' so an ITU colon-form SGR (38:2::r:g:b) is stripped whole, not left
114
+ # with a dangling tail.
115
+ fm_composer_strip_ansi() {
116
+ local esc; esc=$(printf '\033')
117
+ LC_ALL=C sed "s/${esc}\\[[0-9;:?]*[[:alpha:]]//g"
118
+ }
119
+
120
+ # Every code point Unicode gives the property White_Space=Yes that lies OUTSIDE
121
+ # ASCII, as UTF-8 byte sequences. Built from octal escapes rather than written
122
+ # literally so each entry stays reviewable in source instead of being an
123
+ # invisible character:
124
+ # U+0085 NEXT LINE U+00A0 NO-BREAK SPACE
125
+ # U+1680 OGHAM SPACE MARK U+2000..U+200A EN QUAD..HAIR SPACE
126
+ # U+2028 LINE SEPARATOR U+2029 PARAGRAPH SEPARATOR
127
+ # U+202F NARROW NO-BREAK SPACE U+205F MEDIUM MATHEMATICAL SPACE
128
+ # U+3000 IDEOGRAPHIC SPACE
129
+ # ASCII whitespace is absent because POSIX `[[:space:]]` already covers it.
130
+ # U+200B ZERO WIDTH SPACE is deliberately absent: Unicode gives it
131
+ # White_Space=No (a format character), so listing it would substitute this
132
+ # owner's own guess for the property it claims to follow. The live harness
133
+ # guard (bin/fm-test-run.sh, live-harness-optin) is what catches a harness
134
+ # that starts drawing its composer with a character outside this property.
135
+ FM_COMPOSER_UNICODE_SPACES=()
136
+ for _fm_composer_space_octal in \
137
+ '\0302\0205' '\0302\0240' '\0341\0232\0200' \
138
+ '\0342\0200\0200' '\0342\0200\0201' '\0342\0200\0202' '\0342\0200\0203' \
139
+ '\0342\0200\0204' '\0342\0200\0205' '\0342\0200\0206' '\0342\0200\0207' \
140
+ '\0342\0200\0210' '\0342\0200\0211' '\0342\0200\0212' \
141
+ '\0342\0200\0250' '\0342\0200\0251' '\0342\0200\0257' \
142
+ '\0342\0201\0237' '\0343\0200\0200'; do
143
+ printf -v _fm_composer_space_utf8 '%b' "$_fm_composer_space_octal"
144
+ FM_COMPOSER_UNICODE_SPACES+=("$_fm_composer_space_utf8")
145
+ done
146
+ unset -v _fm_composer_space_octal _fm_composer_space_utf8
147
+
148
+ # fm_composer_normalize_spaces_var: the ONE Unicode-whitespace mapping.
149
+ # Replaces in place through the named variable so no caller needs a subshell.
150
+ # Substitution, never deletion: deleting would silently join "foo<NBSP>bar"
151
+ # into one token, while a space preserves the separation the harness drew.
152
+ fm_composer_normalize_spaces_var() { # <varname>
153
+ local __fmns_name=$1 __fmns_text=${!1} __fmns_space
154
+ for __fmns_space in "${FM_COMPOSER_UNICODE_SPACES[@]}"; do
155
+ __fmns_text=${__fmns_text//"$__fmns_space"/ }
156
+ done
157
+ printf -v "$__fmns_name" '%s' "$__fmns_text"
158
+ }
159
+
160
+ # fm_composer_normalize_trim_var: the one whitespace-normalizing trim shared by
161
+ # this owner and every structural row scan - map Unicode whitespace onto ASCII
162
+ # space, then strip leading and trailing whitespace, in place through the named
163
+ # variable. Idempotent, locale-independent.
164
+ fm_composer_normalize_trim_var() { # <varname>
165
+ local __fmnt_name=$1 __fmnt_text
166
+ fm_composer_normalize_spaces_var "$__fmnt_name"
167
+ __fmnt_text=${!__fmnt_name}
168
+ __fmnt_text="${__fmnt_text#"${__fmnt_text%%[![:space:]]*}"}"
169
+ __fmnt_text="${__fmnt_text%"${__fmnt_text##*[![:space:]]}"}"
170
+ printf -v "$__fmnt_name" '%s' "$__fmnt_text"
171
+ }
172
+
173
+ # fm_composer_strip_ghost: the ONE fleet-wide ANSI-aware extractor of "real typed
174
+ # content" from a captured, styled composer row. Reads the styled line on stdin
175
+ # (from `tmux capture-pane -e`, `herdr pane read --format ansi`, or
176
+ # `zellij action dump-screen --ansi`) and prints the
177
+ # plain, non-ghost text on stdout, dropping:
178
+ # - dim/faint runs (SGR 2): how claude and codex render ghost/suggestion text.
179
+ # A reset (SGR 0) or normal-intensity (SGR 22) ends a dim run.
180
+ # - dark/muted TRUECOLOR foreground runs (SGR 38;2;r;g;b or the colon form
181
+ # 38:2::r:g:b) whose perceived luminance (0.299R + 0.587G + 0.114B) is below
182
+ # FM_COMPOSER_GHOST_LUMA_MAX (default 128): how grok renders its placeholder
183
+ # and hint text. A reset (SGR 0), a default-foreground (SGR 39), any base
184
+ # foreground colour (30-37 / 90-97), or a lighter 38;2 foreground ends the
185
+ # dark-foreground run. This assumes a DARK terminal theme, the firstmate
186
+ # fleet reality, where real typed input is bright and only de-emphasised UI
187
+ # is dark; the SGR-2 signal above stays theme-independent. A 256-colour
188
+ # foreground (38;5;n) is NOT luminance-tested - it is palette-dependent and
189
+ # no fleet harness uses it for ghost text, so it is kept (real text wins:
190
+ # under-stripping merely defers, which the max-defer alarm surfaces, while
191
+ # over-stripping would inject over real input).
192
+ # Raising FM_COMPOSER_GHOST_LUMA_MAX is not free: muse draws its `⟩` prompt glyph
193
+ # in truecolor 38;2;90;160;255, luminance ~149.9 (verified, muse 0.1.0-R708.1),
194
+ # the tightest margin over the 128 default in the fleet. Above ~150 that glyph is
195
+ # stripped as ghost text, which is why the bare-glyph fallback below must also
196
+ # recognise every agent glyph from the UNSTRIPPED plain row.
197
+ # The dim/faint and dark-foreground states are tracked together as "de-emphasis";
198
+ # codes are processed left to right within a sequence, so "ESC[0;2m" reads as dim.
199
+ # LC_ALL=C makes awk walk bytes, so multibyte glyphs (e.g. ❯) and de-emphasised
200
+ # runs alike pass through or drop intact without locale-dependent classes.
201
+ fm_composer_strip_ghost() {
202
+ LC_ALL=C awk -v lumamax="${FM_COMPOSER_GHOST_LUMA_MAX:-128}" '
203
+ function sgr_code(v, b) {
204
+ b = v
205
+ sub(/:.*/, "", b)
206
+ if (b == "") b = "0"
207
+ return b
208
+ }
209
+ function skip_color_payload(a, p, k, mode, code) {
210
+ if (index(a[p], ":") > 0) return p
211
+ if (p >= k) return p
212
+ mode = a[p + 1]
213
+ code = sgr_code(mode)
214
+ if (index(mode, ":") > 0) return p + 1
215
+ if (code == "5") return p + 2
216
+ if (code == "2") return p + 4
217
+ return p + 1
218
+ }
219
+ # fg38_is_dark: 1 when the SGR 38 foreground starting at param p is a
220
+ # TRUECOLOR (38;2 / 38:2) whose luminance is below lumamax; 0 otherwise
221
+ # (a 38;5 palette colour, a bright truecolor, or a malformed run).
222
+ function fg38_is_dark(a, p, k, lumamax, spec, nf, f, r, g, b) {
223
+ spec = a[p]
224
+ if (index(spec, ":") > 0) { # colon form: whole colour in a[p]
225
+ nf = split(spec, f, ":")
226
+ if (f[2] != "2" || nf < 5) return 0
227
+ r = f[nf - 2] + 0; g = f[nf - 1] + 0; b = f[nf] + 0
228
+ return ((299*r + 587*g + 114*b) / 1000 < lumamax) ? 1 : 0
229
+ }
230
+ if (p + 1 > k || a[p + 1] != "2" || p + 4 > k) return 0
231
+ r = a[p + 2] + 0; g = a[p + 3] + 0; b = a[p + 4] + 0
232
+ return ((299*r + 587*g + 114*b) / 1000 < lumamax) ? 1 : 0
233
+ }
234
+ {
235
+ line = $0; out = ""; dim = 0; darkfg = 0; n = length(line); i = 1
236
+ while (i <= n) {
237
+ c = substr(line, i, 1)
238
+ if (c == "\033") { # ESC: consume a CSI ... final-byte sequence
239
+ j = i + 1
240
+ if (substr(line, j, 1) == "[") {
241
+ j++; params = ""
242
+ while (j <= n) {
243
+ cc = substr(line, j, 1)
244
+ if (cc ~ /[@-~]/) break
245
+ params = params cc; j++
246
+ }
247
+ if (j <= n && substr(line, j, 1) == "m") { # SGR: update de-emphasis
248
+ if (params == "") params = "0"
249
+ k = split(params, a, ";")
250
+ for (p = 1; p <= k; p++) {
251
+ v = a[p]; code = sgr_code(v)
252
+ if (code == "38") {
253
+ darkfg = fg38_is_dark(a, p, k, lumamax)
254
+ p = skip_color_payload(a, p, k)
255
+ } else if (code == "48" || code == "58") {
256
+ p = skip_color_payload(a, p, k)
257
+ } else if (code == "2") dim = 1
258
+ else if (code == "0") { dim = 0; darkfg = 0 }
259
+ else if (code == "22") dim = 0
260
+ else if (code == "39") darkfg = 0
261
+ else if (code + 0 >= 30 && code + 0 <= 37) darkfg = 0
262
+ else if (code + 0 >= 90 && code + 0 <= 97) darkfg = 0
263
+ }
264
+ }
265
+ if (j <= n) { i = j + 1; continue }
266
+ }
267
+ i = i + 1; continue # lone/other ESC: drop the ESC byte only
268
+ }
269
+ if (dim == 0 && darkfg == 0) out = out c # keep only non-de-emphasised bytes
270
+ i++
271
+ }
272
+ print out
273
+ }
274
+ '
275
+ }
276
+
277
+
278
+ # --- Delivery-only rendered busy footers (backend-agnostic) -------------------
279
+ #
280
+ # These live here, in the ONE shared composer/delivery owner, rather than in any
281
+ # single backend adapter, because every backend needs them for the SAME job:
282
+ # proving a submitted Enter actually landed. Keeping them in bin/fm-tmux-lib.sh
283
+ # made cursor's signature reachable only from tmux, even though herdr, zellij,
284
+ # cmux, and orca run the same harnesses and face the same acknowledgement
285
+ # problem.
286
+ #
287
+ # This is a DELIVERY guard, deliberately NOT a worker-state source. The semantic
288
+ # busy contract - what firstmate records and supervises on - is owned by
289
+ # bin/fm-busy-lib.sh, which forbids classifying a harness from rendered text.
290
+ # Matching a footer to confirm a keystroke landed is a different question from
291
+ # asking what a worker is doing, and the two must not be conflated.
292
+ # Delivery-only rendered busy footers per harness. claude/codex: "esc to
293
+ # interrupt"; opencode: "esc interrupt"; pi: "Working..."; grok: "Ctrl+c:cancel".
294
+ # Claude's current spinner has a rotating glyph and word, but every active-turn
295
+ # line has an ellipsis followed by a parenthesized elapsed duration. Keep this
296
+ # signature separate from the shared default because that shape is not generic
297
+ # enough to classify arbitrary harness output safely.
298
+ # Kimi's anchored moon-phase spinner is separate because bare moon glyphs in
299
+ # ordinary output must not classify another harness as busy. Leading whitespace is
300
+ # OPTIONAL; whitespace on both sides of the separator is REQUIRED because every
301
+ # captured spinner row had it. A zero-whitespace form has NEVER been observed and
302
+ # is deliberately not matched. The line end is intentionally unanchored because
303
+ # rotating tip text follows and is not required to be present. The idle status
304
+ # bar's lowercase `thinking` label and independently rotating tip text are not
305
+ # busy signals on their own.
306
+ # The full moon-phase set remains locale- and emoji-font-sensitive because Kimi
307
+ # exposes no stable ASCII busy token.
308
+ # The harness-less default is the UNION of the per-harness tokens below, used
309
+ # when a caller has no recorded harness for the pane (the submit cores read the
310
+ # baseline and the post-Enter transition this way). cursor's `ctrl+c to stop` is
311
+ # part of that union for the same reason the others are: without it a cursor
312
+ # submit could never be acknowledged, because cursor parks its terminal cursor
313
+ # outside its composer and the composer verdict is therefore always `unknown`.
314
+ FM_DELIVERY_BUSY_REGEX_DEFAULT='esc (to )?interrupt|Working\.\.\.|Ctrl\+c:cancel|ctrl\+c to stop'
315
+ FM_DELIVERY_CLAUDE_BUSY_REGEX_DEFAULT='esc to interrupt|…[[:space:]]+\([0-9]+[smh]'
316
+ FM_DELIVERY_CODEX_BUSY_REGEX_DEFAULT='esc to interrupt'
317
+ FM_DELIVERY_OPENCODE_BUSY_REGEX_DEFAULT='esc interrupt'
318
+ FM_DELIVERY_PI_BUSY_REGEX_DEFAULT='Working\.\.\.'
319
+ FM_DELIVERY_GROK_BUSY_REGEX_DEFAULT='Ctrl\+c:cancel'
320
+ # cursor-agent's busy footer. The TOKEN is matched, not the spinner verb: the
321
+ # same version rendered both `Working` and `Running` beside its braille spinner
322
+ # in two consecutive turns, while `ctrl+c to stop` was present for the whole
323
+ # turn and absent the instant it ended (verified live, 2026.08.11-e8db854).
324
+ # This is a DELIVERY guard only - it acknowledges a submit and gates away-mode
325
+ # injection. Cursor's recorded worker state comes from its transcript fold in
326
+ # bin/fm-busy-lib.sh, never from this row.
327
+ FM_DELIVERY_CURSOR_BUSY_REGEX_DEFAULT='ctrl\+c to stop'
328
+ FM_DELIVERY_KIMI_BUSY_REGEX_DEFAULT='^[[:space:]]*(🌑|🌒|🌓|🌔|🌕|🌖|🌗|🌘)[[:space:]]+·[[:space:]]+'
329
+
330
+ fm_busy_lines_match() { # [harness]
331
+ local harness=${1:-} lines regex
332
+ IFS= read -r -d '' lines || true
333
+ if [ -n "${FM_BUSY_REGEX:-}" ]; then
334
+ regex=$FM_BUSY_REGEX
335
+ else
336
+ case "$harness" in
337
+ claude) regex=$FM_DELIVERY_CLAUDE_BUSY_REGEX_DEFAULT ;;
338
+ codex) regex=$FM_DELIVERY_CODEX_BUSY_REGEX_DEFAULT ;;
339
+ opencode) regex=$FM_DELIVERY_OPENCODE_BUSY_REGEX_DEFAULT ;;
340
+ pi|pi-signed) regex=$FM_DELIVERY_PI_BUSY_REGEX_DEFAULT ;;
341
+ grok) regex=$FM_DELIVERY_GROK_BUSY_REGEX_DEFAULT ;;
342
+ kimi) regex=$FM_DELIVERY_KIMI_BUSY_REGEX_DEFAULT ;;
343
+ cursor) regex=$FM_DELIVERY_CURSOR_BUSY_REGEX_DEFAULT ;;
344
+ '') regex=$FM_DELIVERY_BUSY_REGEX_DEFAULT ;;
345
+ *)
346
+ # A supplied harness must never borrow another harness's signature.
347
+ # Register its verified signature explicitly before classifying it busy.
348
+ regex=
349
+ ;;
350
+ esac
351
+ fi
352
+ [ -n "$regex" ] && printf '%s' "$lines" | grep -qiE "$regex"
353
+ }
354
+
355
+ # The prompt glyphs, each declared exactly once (see THE SAFETY RULE above).
356
+ # AGENT glyphs are a genuine empty agent composer on any row, bordered or bare.
357
+ # SHELL glyphs are one only INSIDE a composer container; on a bare row they are
358
+ # a dead-shell prompt and must never read `empty`. Newline-separated and
359
+ # consumed by `read` rather than word splitting, so `$`, `%`, and `#` stay
360
+ # literal and no entry is ever exposed to pathname expansion.
361
+ FM_COMPOSER_AGENT_PROMPT_GLYPHS=$(printf '%s\n' '❯' '›' '⟩' '→')
362
+ FM_COMPOSER_SHELL_PROMPT_GLYPHS=$(printf '%s\n' '>' '$' '%' '#')
363
+
364
+ # The ONE fleet-wide idle-placeholder set: composer text a harness renders in
365
+ # an EMPTY composer that a plain capture cannot tell from typed text. Grok's
366
+ # bordered placeholder and opencode's left-bar hint (which continues with a
367
+ # rotating quoted suggestion, hence the unanchored tail). cursor-agent renders
368
+ # two, both anchored: `Plan, search, build anything` in a fresh session and
369
+ # `Add a follow-up` once a turn has completed (verified live on cursor-agent
370
+ # 2026.08.11-e8db854). FM_COMPOSER_IDLE_RE overrides for an unverified harness;
371
+ # matching is case-insensitive.
372
+ FM_COMPOSER_IDLE_RE_DEFAULT='^Type a message\.\.\.$|^Ask anything\.\.\.|^Plan, search, build anything$|^Add a follow-up$'
373
+
374
+ # Opencode draws a mode/model footer line INSIDE its left-bar composer
375
+ # ("Build · GPT-5.5 Fast OpenAI · high"). It is composer furniture, not typed
376
+ # text, and only the run's LAST row is ever matched against it.
377
+ FM_COMPOSER_LEFTBAR_FOOTER_RE_DEFAULT='^(Build|Plan)[[:space:]]+·[[:space:]]+'
378
+
379
+ # The bounded row window adapters should capture for a composer read. One
380
+ # shared policy (previously three per-backend variables that had drifted to
381
+ # 20/20/200): the composer is bottom-anchored, so a small tail window is
382
+ # sufficient and keeps stale scrollback (startup banners, old transcript
383
+ # boxes) from ever competing with the live composer.
384
+ FM_COMPOSER_CAPTURE_LINES=${FM_COMPOSER_CAPTURE_LINES:-20}
385
+
386
+ # Pi allows a multi-line composer between its horizontal separators. Bound the
387
+ # structural candidate so two unrelated transcript rules with an arbitrarily
388
+ # large region between them can never be promoted into a composer.
389
+ FM_COMPOSER_PI_MAX_LINES=${FM_COMPOSER_PI_MAX_LINES:-8}
390
+
391
+ # 0 when <content> is exactly one glyph drawn from <glyph-list>.
392
+ _fm_composer_is_prompt_glyph() { # <content> <glyph-list>
393
+ local content=$1 glyph
394
+ while IFS= read -r glyph; do
395
+ [ -n "$glyph" ] || continue
396
+ [ "$content" = "$glyph" ] && return 0
397
+ done <<EOF
398
+ $2
399
+ EOF
400
+ return 1
401
+ }
402
+
403
+ # fm_composer_leading_prompt_glyph_var: set <out-varname> to the ONE prompt
404
+ # glyph <content> begins with once its leading whitespace is ignored, or to the
405
+ # empty string (returning 1) when it begins with none. Both glyph lists are
406
+ # reached here, so no caller can respell them and drift. Returning the matched
407
+ # glyph as a LITERAL string lets every caller remove it byte-exactly with
408
+ # `${v#"$glyph"}`, which is correct in every locale.
409
+ fm_composer_leading_prompt_glyph_var() { # <out-varname> <content>
410
+ local __fmpg_out=$1 __fmpg_text=$2 __fmpg_glyph
411
+ __fmpg_text="${__fmpg_text#"${__fmpg_text%%[![:space:]]*}"}"
412
+ while IFS= read -r __fmpg_glyph; do
413
+ [ -n "$__fmpg_glyph" ] || continue
414
+ case "$__fmpg_text" in
415
+ "$__fmpg_glyph"*) printf -v "$__fmpg_out" '%s' "$__fmpg_glyph"; return 0 ;;
416
+ esac
417
+ done <<EOF
418
+ $FM_COMPOSER_AGENT_PROMPT_GLYPHS
419
+ $FM_COMPOSER_SHELL_PROMPT_GLYPHS
420
+ EOF
421
+ printf -v "$__fmpg_out" '%s' ''
422
+ return 1
423
+ }
424
+
425
+ # fm_composer_leading_agent_glyph_var: like the above but AGENT glyphs only.
426
+ # The bare-row shape must never be anchored by a shell glyph (dead-shell rule).
427
+ fm_composer_leading_agent_glyph_var() { # <out-varname> <content>
428
+ local __fmag_out=$1 __fmag_text=$2 __fmag_glyph
429
+ __fmag_text="${__fmag_text#"${__fmag_text%%[![:space:]]*}"}"
430
+ while IFS= read -r __fmag_glyph; do
431
+ [ -n "$__fmag_glyph" ] || continue
432
+ case "$__fmag_text" in
433
+ "$__fmag_glyph"*) printf -v "$__fmag_out" '%s' "$__fmag_glyph"; return 0 ;;
434
+ esac
435
+ done <<EOF
436
+ $FM_COMPOSER_AGENT_PROMPT_GLYPHS
437
+ EOF
438
+ printf -v "$__fmag_out" '%s' ''
439
+ return 1
440
+ }
441
+
442
+ fm_composer_leading_shell_glyph_var() { # <out-varname> <content>
443
+ local __fmsg_out=$1 __fmsg_text=$2 __fmsg_glyph
444
+ __fmsg_text="${__fmsg_text#"${__fmsg_text%%[![:space:]]*}"}"
445
+ while IFS= read -r __fmsg_glyph; do
446
+ [ -n "$__fmsg_glyph" ] || continue
447
+ case "$__fmsg_text" in
448
+ "$__fmsg_glyph"*) printf -v "$__fmsg_out" '%s' "$__fmsg_glyph"; return 0 ;;
449
+ esac
450
+ done <<EOF
451
+ $FM_COMPOSER_SHELL_PROMPT_GLYPHS
452
+ EOF
453
+ printf -v "$__fmsg_out" '%s' ''
454
+ return 1
455
+ }
456
+
457
+ fm_composer_idle_matches() {
458
+ local content=$1 idle_re=$2 idle_case=$3
459
+ [ -n "$idle_re" ] || return 1
460
+ case "$idle_case" in
461
+ insensitive) printf '%s' "$content" | grep -qiE "$idle_re" ;;
462
+ *) printf '%s' "$content" | grep -qE "$idle_re" ;;
463
+ esac
464
+ }
465
+
466
+ # fm_composer_classify_content: the single shared composer-content verdict.
467
+ # <bordered> 1 when <content> came from a genuine agent-composer container (a
468
+ # bordered composer box, an identity-proven separated composer, or
469
+ # a structurally-identified left-bar row); 0 for a bare
470
+ # agent-glyph row, where only the agent glyph itself is proof.
471
+ # <content> the candidate composer content, border-stripped by the caller.
472
+ # [idle_re] optional idle-placeholder regex; empty means no idle matching.
473
+ # The screen classifier below passes the resolved fleet-wide idle
474
+ # set; this parameter stays pure so a direct caller's semantics
475
+ # cannot shift underneath it.
476
+ # [idle_case] `sensitive` (default) or `insensitive`.
477
+ # [plain_content] the UNSTRIPPED plain row, consulted when ghost stripping
478
+ # emptied an unbordered row: muse's `⟩` sits at luminance ~150,
479
+ # close enough to the ghost threshold that a raised threshold
480
+ # strips it, and the plain row is what keeps that pane readable.
481
+ # Content and plain_content are normalized and re-trimmed on entry, so the
482
+ # verdict never depends on which whitespace alphabet the calling adapter
483
+ # trimmed with.
484
+ fm_composer_classify_content() { # <bordered> <content> [idle_re] [idle_case] [plain_content] [placeholder-position] [styled]
485
+ local bordered=$1 idle_re=${3:-} idle_case=${4:-sensitive} content plain_content glyph=''
486
+ local placeholder_position=${6:-0} styled=${7:-1} idle_collision=0
487
+ content=$2
488
+ fm_composer_normalize_trim_var content
489
+ plain_content=${5:-$2}
490
+ fm_composer_normalize_trim_var plain_content
491
+ if [ "$bordered" != 1 ] && [ -z "$content" ] && [ -n "$plain_content" ]; then
492
+ if _fm_composer_is_prompt_glyph "$plain_content" "$FM_COMPOSER_AGENT_PROMPT_GLYPHS"; then
493
+ printf 'empty'; return 0
494
+ fi
495
+ printf 'unknown'; return 0
496
+ fi
497
+ if _fm_composer_is_prompt_glyph "$content" "$FM_COMPOSER_AGENT_PROMPT_GLYPHS"; then
498
+ printf 'empty'; return 0
499
+ fi
500
+ if _fm_composer_is_prompt_glyph "$content" "$FM_COMPOSER_SHELL_PROMPT_GLYPHS"; then
501
+ if [ "$bordered" = 1 ]; then printf 'empty'; else printf 'unknown'; fi
502
+ return 0
503
+ fi
504
+ [ -n "$content" ] || { printf 'empty'; return 0; }
505
+ fm_composer_idle_matches "$content" "$idle_re" "$idle_case" && idle_collision=1
506
+ if fm_composer_leading_prompt_glyph_var glyph "$content"; then
507
+ content=${content#*"$glyph"}
508
+ fi
509
+ fm_composer_normalize_trim_var content
510
+ [ -n "$content" ] || { printf 'empty'; return 0; }
511
+ fm_composer_idle_matches "$content" "$idle_re" "$idle_case" && idle_collision=1
512
+ # Ghost stripping can leave a REMNANT of an idle placeholder rather than
513
+ # emptying it, because a terminal draws the cell under its cursor in reverse
514
+ # video (SGR 7) - neither dim/faint nor a dark foreground, so that one
515
+ # character survives a stripper built for the other two. cursor-agent renders
516
+ # exactly this shape: a dim `Plan, search, build anything` whose first
517
+ # character is reverse-video, leaving a lone `P` (verified live on
518
+ # cursor-agent 2026.08.11-e8db854). Judging that remnant on its own reads
519
+ # `pending` on a genuinely idle pane.
520
+ # The plain row is the styling-independent signal, so consult it here. This
521
+ # stays safe in the false-EMPTY direction because it demands the remnant be a
522
+ # PROPER, strictly shorter substring of a plain row that matches a full
523
+ # anchored placeholder: real typed text is uniformly bright, so stripping
524
+ # leaves it EQUAL to the plain row and it falls through to `pending` below.
525
+ # Typing a strict substring of a placeholder is equally safe - the plain row
526
+ # is then that substring, which the anchored placeholder pattern cannot match.
527
+ if [ "$idle_collision" != 1 ] && [ "$styled" = 1 ] && [ -n "$plain_content" ]; then
528
+ local plain_body=$plain_content plain_glyph=''
529
+ if fm_composer_leading_prompt_glyph_var plain_glyph "$plain_body"; then
530
+ plain_body=${plain_body#*"$plain_glyph"}
531
+ fi
532
+ fm_composer_normalize_trim_var plain_body
533
+ if [ "${#content}" -lt "${#plain_body}" ] \
534
+ && fm_composer_idle_matches "$plain_body" "$idle_re" "$idle_case"; then
535
+ case "$plain_body" in
536
+ *"$content"*) printf 'empty'; return 0 ;;
537
+ esac
538
+ fi
539
+ fi
540
+ if [ "$idle_collision" = 1 ]; then
541
+ if [ "$placeholder_position" = 1 ] && [ "$bordered" = 1 ] && [ "$styled" != 1 ]; then
542
+ printf 'empty'; return 0
543
+ fi
544
+ if [ "$styled" != 1 ]; then
545
+ printf 'unknown'; return 0
546
+ fi
547
+ fi
548
+ printf 'pending'; return 0
549
+ }
550
+
551
+ # --- The screen classifier ---------------------------------------------------
552
+ #
553
+ # fm_composer_classify_screen <caps> <screen> [cursor_row] [identity]
554
+ # <caps> newline-separated key=value capability facts (see header).
555
+ # <screen> the captured screen: ANSI-preserving when styled=1, plain
556
+ # otherwise.
557
+ # [cursor_row] zero-based row index of the cursor within <screen>, only
558
+ # meaningful when caps carry cursor=1.
559
+ # [identity] "<agent>\t<status>" from the backend's native identity probe,
560
+ # or `probe-absent` when the probe found no live identity; only
561
+ # meaningful when caps carry identity=1.
562
+ # Prints exactly one verdict: empty | pending | pending-unproven | unknown,
563
+ # or the internal sentinel `need-identity` when caps declare identity=1, no
564
+ # identity result was supplied, and the verdict depends on it. Adapters answer
565
+ # `need-identity` by running their identity probe once and re-calling with
566
+ # either its result or `probe-absent`; the sentinel never escapes an adapter.
567
+ # Identity stays a lazy second pass so the common non-pi read never pays for
568
+ # the probe.
569
+ #
570
+ # Consumers that can overwrite input or confirm delivery must accept only the
571
+ # exact positive proof they require (`empty`), so unrecognized future verdicts
572
+ # fail safe by default.
573
+
574
+ # _fm_composer_pi_separator_row: a solid pi separator - nothing but `─`, at
575
+ # least 8 columns wide. The width floor is a literal substring test so it is
576
+ # byte-exact in every locale.
577
+ _fm_composer_pi_separator_row() { # <trimmed-row>
578
+ local row=$1
579
+ [ -n "$row" ] || return 1
580
+ [ -z "${row//─/}" ] || return 1
581
+ case "$row" in
582
+ *────────*) return 0 ;;
583
+ esac
584
+ return 1
585
+ }
586
+
587
+ # Row-scan results are returned through FM_COMPOSER_SCAN_* globals (bash 3.2
588
+ # has no nameref); they are internal to this owner.
589
+ _fm_composer_scan_screen() { # <plain-screen> <cursor-or-empty> [extract-wrap]
590
+ local pane=$1 cy=${2:-}
591
+ local line indent left_stripped trimmed kind family side_family
592
+ local top_inner top_spaces='' geometry_check=0 geometry_ambiguous=0
593
+ local content_inner content_spaces bottom_inner bottom_spaces glyph
594
+ local current_indent='' current_family='' row=0 top=-1 valid=0 content_rows=0
595
+ # Complete-box results: the box containing the cursor (cursor mode) or the
596
+ # bottom-most complete box (no cursor).
597
+ FM_COMPOSER_SCAN_BOX_TOP=-1
598
+ FM_COMPOSER_SCAN_BOX_BOTTOM=-1
599
+ FM_COMPOSER_SCAN_BOX_AMBIG=0
600
+ FM_COMPOSER_SCAN_INCOMPLETE_BOX_FROM=-1
601
+ FM_COMPOSER_SCAN_UNSAFE=0
602
+ FM_COMPOSER_SCAN_CURSOR_EDGE=0
603
+ FM_COMPOSER_SCAN_BARE_ROW=-1
604
+ FM_COMPOSER_SCAN_SHELL_ROW=-1
605
+ FM_COMPOSER_SCAN_LEFTBAR_START=-1
606
+ FM_COMPOSER_SCAN_LEFTBAR_END=-1
607
+ FM_COMPOSER_SCAN_PI_PAIR_FOUND=0
608
+ FM_COMPOSER_SCAN_PI_PAIR_VALID=0
609
+ FM_COMPOSER_SCAN_PI_OPEN=-1
610
+ FM_COMPOSER_SCAN_PI_CLOSE=-1
611
+ FM_COMPOSER_SCAN_PI_LAST_SEPARATOR=-1
612
+ local leftbar_start=-1 pi_open=-1 pi_lines=0 pi_max
613
+ pi_max=$FM_COMPOSER_PI_MAX_LINES
614
+ case "$pi_max" in ''|*[!0-9]*|0) pi_max=8 ;; esac
615
+ while IFS= read -r line; do
616
+ indent=${line%%[![:space:]]*}
617
+ left_stripped="${line#"${line%%[![:space:]]*}"}"
618
+ trimmed=$left_stripped
619
+ fm_composer_normalize_trim_var trimmed
620
+ kind=
621
+ family=
622
+ case "$trimmed" in
623
+ '╭'*'╮') kind=top; family=rounded ;;
624
+ '┌'*'┐') kind=top; family=light ;;
625
+ '╔'*'╗') kind=top; family=double ;;
626
+ '┏'*'┓') kind=top; family=heavy ;;
627
+ '╰'*'╯') kind=bottom; family=rounded ;;
628
+ '└'*'┘') kind=bottom; family=light ;;
629
+ '╚'*'╝') kind=bottom; family=double ;;
630
+ '┗'*'┛') kind=bottom; family=heavy ;;
631
+ '+'*'+') kind=ascii; family=ascii ;;
632
+ esac
633
+ # Pi separator rows: a solid `─` rule at least 8 columns wide. A separator
634
+ # closes the preceding candidate and immediately opens the next, so an
635
+ # earlier transcript rule can never outrank the live bottom composer pair.
636
+ if _fm_composer_pi_separator_row "$trimmed"; then
637
+ FM_COMPOSER_SCAN_PI_LAST_SEPARATOR=$row
638
+ if [ "$pi_open" -ge 0 ]; then
639
+ FM_COMPOSER_SCAN_PI_PAIR_FOUND=1
640
+ FM_COMPOSER_SCAN_PI_OPEN=$pi_open
641
+ FM_COMPOSER_SCAN_PI_CLOSE=$row
642
+ if [ "$pi_lines" -le "$pi_max" ]; then
643
+ FM_COMPOSER_SCAN_PI_PAIR_VALID=1
644
+ else
645
+ FM_COMPOSER_SCAN_PI_PAIR_VALID=0
646
+ fi
647
+ fi
648
+ pi_open=$row
649
+ pi_lines=0
650
+ elif [ "$pi_open" -ge 0 ]; then
651
+ pi_lines=$((pi_lines + 1))
652
+ fi
653
+ # Left-bar rows (opencode): a heavy left bar `┃` opening the row with no
654
+ # closing side border. A `┃…┃` row is a bordered box row, not a left bar.
655
+ case "$trimmed" in
656
+ '┃'*'┃') leftbar_start=-1 ;;
657
+ '┃'*)
658
+ if [ "$leftbar_start" -lt 0 ]; then leftbar_start=$row; fi
659
+ FM_COMPOSER_SCAN_LEFTBAR_START=$leftbar_start
660
+ FM_COMPOSER_SCAN_LEFTBAR_END=$row
661
+ ;;
662
+ *) leftbar_start=-1 ;;
663
+ esac
664
+ # Bare agent-glyph rows: the glyph itself is the container proof. Bare
665
+ # shell glyphs are deliberately not candidates (dead-shell rule). Keep
666
+ # lower shell prompts as staleness evidence for cursorless selection.
667
+ if [ "$top" -lt 0 ] && fm_composer_leading_shell_glyph_var glyph "$trimmed"; then
668
+ FM_COMPOSER_SCAN_SHELL_ROW=$row
669
+ elif fm_composer_leading_agent_glyph_var glyph "$trimmed"; then
670
+ FM_COMPOSER_SCAN_BARE_ROW=$row
671
+ fi
672
+ # Cursor safety: a cursor sitting on a structural edge row is never an
673
+ # input row.
674
+ if [ -n "$cy" ] && [ "$row" -eq "$cy" ] && fm_composer_row_has_edge "$trimmed"; then
675
+ FM_COMPOSER_SCAN_CURSOR_EDGE=1
676
+ fi
677
+ # Complete-box state machine (all border families, geometry, ambiguity).
678
+ if [ "$kind" = top ] || { [ "$kind" = ascii ] && [ "$top" -lt 0 ]; }; then
679
+ if [ -n "$cy" ] && [ "$top" -ge 0 ] && [ "$top" -lt "$cy" ] && [ "$cy" -le "$row" ]; then
680
+ FM_COMPOSER_SCAN_UNSAFE=1
681
+ fi
682
+ top=$row
683
+ FM_COMPOSER_SCAN_INCOMPLETE_BOX_FROM=$row
684
+ current_family=$family
685
+ current_indent=$indent
686
+ valid=1
687
+ content_rows=0
688
+ geometry_ambiguous=0
689
+ geometry_check=1
690
+ top_inner=$trimmed
691
+ case "$family" in
692
+ rounded) top_inner=${top_inner#╭}; top_inner=${top_inner%╮}; top_spaces=${top_inner//─/ } ;;
693
+ light) top_inner=${top_inner#┌}; top_inner=${top_inner%┐}; top_spaces=${top_inner//─/ } ;;
694
+ double) top_inner=${top_inner#╔}; top_inner=${top_inner%╗}; top_spaces=${top_inner//═/ } ;;
695
+ heavy) top_inner=${top_inner#┏}; top_inner=${top_inner%┓}; top_spaces=${top_inner//━/ } ;;
696
+ ascii) top_inner=${top_inner#+}; top_inner=${top_inner%+}; top_spaces=${top_inner//-/ } ;;
697
+ esac
698
+ case "$top_spaces" in
699
+ *[![:space:]]*) geometry_check=0; geometry_ambiguous=1 ;;
700
+ esac
701
+ elif [ "$kind" = bottom ] || { [ "$kind" = ascii ] && [ "$top" -ge 0 ]; }; then
702
+ if [ "$top" -ge 0 ] && [ "$family" = "$current_family" ] \
703
+ && [ "$valid" = 1 ] && [ "$content_rows" -gt 0 ]; then
704
+ [ "$indent" = "$current_indent" ] || geometry_ambiguous=1
705
+ if [ "$geometry_check" = 1 ]; then
706
+ bottom_inner=$trimmed
707
+ case "$family" in
708
+ rounded) bottom_inner=${bottom_inner#╰}; bottom_inner=${bottom_inner%╯}; bottom_spaces=${bottom_inner//─/ } ;;
709
+ light) bottom_inner=${bottom_inner#└}; bottom_inner=${bottom_inner%┘}; bottom_spaces=${bottom_inner//─/ } ;;
710
+ double) bottom_inner=${bottom_inner#╚}; bottom_inner=${bottom_inner%╝}; bottom_spaces=${bottom_inner//═/ } ;;
711
+ heavy) bottom_inner=${bottom_inner#┗}; bottom_inner=${bottom_inner%┛}; bottom_spaces=${bottom_inner//━/ } ;;
712
+ ascii) bottom_inner=${bottom_inner#+}; bottom_inner=${bottom_inner%+}; bottom_spaces=${bottom_inner//-/ } ;;
713
+ esac
714
+ if [ "$bottom_spaces" != "$top_spaces" ]; then
715
+ # A TITLED bottom border (grok writes its model name there) is
716
+ # tolerated when the inner still starts and ends with the family's
717
+ # own rule glyph: the corners, family, indent, and every content
718
+ # row's geometry were already proven. Anything else is ambiguity.
719
+ if ! _fm_composer_titled_bottom_ok "$family" "$bottom_inner" "$top_spaces"; then
720
+ geometry_ambiguous=1
721
+ fi
722
+ fi
723
+ fi
724
+ if [ -n "$cy" ]; then
725
+ if [ "$top" -lt "$cy" ] && [ "$cy" -le "$row" ]; then
726
+ FM_COMPOSER_SCAN_BOX_TOP=$top
727
+ FM_COMPOSER_SCAN_BOX_BOTTOM=$row
728
+ FM_COMPOSER_SCAN_BOX_AMBIG=$geometry_ambiguous
729
+ fi
730
+ else
731
+ FM_COMPOSER_SCAN_BOX_TOP=$top
732
+ FM_COMPOSER_SCAN_BOX_BOTTOM=$row
733
+ FM_COMPOSER_SCAN_BOX_AMBIG=$geometry_ambiguous
734
+ fi
735
+ FM_COMPOSER_SCAN_INCOMPLETE_BOX_FROM=-1
736
+ else
737
+ if [ "$FM_COMPOSER_SCAN_INCOMPLETE_BOX_FROM" -lt 0 ]; then
738
+ FM_COMPOSER_SCAN_INCOMPLETE_BOX_FROM=$row
739
+ fi
740
+ if [ -n "$cy" ]; then
741
+ if { [ "$top" -ge 0 ] && [ "$top" -lt "$cy" ] && [ "$cy" -le "$row" ]; } \
742
+ || [ "$row" -eq "$cy" ]; then
743
+ FM_COMPOSER_SCAN_UNSAFE=1
744
+ fi
745
+ fi
746
+ fi
747
+ top=-1
748
+ current_family=
749
+ current_indent=
750
+ valid=0
751
+ content_rows=0
752
+ elif [ "$top" -ge 0 ]; then
753
+ side_family=
754
+ case "$trimmed" in
755
+ '│'*'│') side_family=single ;;
756
+ '┃'*'┃') side_family=heavy ;;
757
+ '║'*'║') side_family=double ;;
758
+ '|'*'|') side_family=ascii ;;
759
+ esac
760
+ case "$current_family:$side_family" in
761
+ rounded:single|light:single|heavy:heavy|double:double|ascii:ascii)
762
+ content_rows=$((content_rows + 1))
763
+ [ "$indent" = "$current_indent" ] || geometry_ambiguous=1
764
+ if [ "$geometry_check" = 1 ]; then
765
+ content_inner=$trimmed
766
+ case "$side_family" in
767
+ single) content_inner=${content_inner#│}; content_inner=${content_inner%│} ;;
768
+ heavy) content_inner=${content_inner#┃}; content_inner=${content_inner%┃} ;;
769
+ double) content_inner=${content_inner#║}; content_inner=${content_inner%║} ;;
770
+ ascii) content_inner=${content_inner#|}; content_inner=${content_inner%|} ;;
771
+ esac
772
+ if content_spaces=$(fm_composer_geometry_spaces "$content_inner"); then
773
+ [ "$content_spaces" = "$top_spaces" ] || geometry_ambiguous=1
774
+ else
775
+ geometry_ambiguous=1
776
+ fi
777
+ fi
778
+ ;;
779
+ *) valid=0 ;;
780
+ esac
781
+ fi
782
+ row=$((row + 1))
783
+ done <<EOF
784
+ $pane
785
+ EOF
786
+ if [ -n "$cy" ] && [ "$top" -ge 0 ] && [ "$top" -lt "$cy" ]; then
787
+ FM_COMPOSER_SCAN_UNSAFE=1
788
+ fi
789
+ }
790
+
791
+ # 0 when a mismatched bottom border reads as a legitimate TITLE: the trimmed
792
+ # inner (corners already stripped) still starts and ends with the family's own
793
+ # rule glyph, so the title is embedded IN the rule rather than replacing it.
794
+ _fm_composer_titled_bottom_ok() { # <family> <bottom-inner> <top-spaces>
795
+ local family=$1 inner=$2 expected=$3 dash spaces
796
+ fm_composer_normalize_trim_var inner
797
+ case "$family" in
798
+ rounded|light) dash='─' ;;
799
+ double) dash='═' ;;
800
+ heavy) dash='━' ;;
801
+ ascii) dash='-' ;;
802
+ *) return 1 ;;
803
+ esac
804
+ case "$inner" in
805
+ "$dash"*"$dash") ;;
806
+ *) return 1 ;;
807
+ esac
808
+ spaces=${inner//"$dash"/ }
809
+ spaces=$(printf '%s' "$spaces" | LC_ALL=C sed 's/[!-~]/ /g')
810
+ case "$spaces" in
811
+ *[![:space:]]*) return 1 ;;
812
+ esac
813
+ [ "$spaces" = "$expected" ]
814
+ }
815
+
816
+ # fm_composer_row_has_edge: 0 when the trimmed row starts or ends with a
817
+ # box-drawing/edge glyph - a structural row, never an input row.
818
+ # The half-block glyphs are edges too. Herdr draws a composer's top and bottom
819
+ # rules with ▄ and ▀ instead of the box-drawing family, so without them a bare
820
+ # composer's WRAP region walks straight through its own closing rule and
821
+ # swallows the footer below it - which reads as real typed text and turns an
822
+ # idle pane into a false `pending`. Measured live on a herdr cursor pane, where
823
+ # the wrap region ran from the composer row through the model and path rows.
824
+ fm_composer_row_has_edge() { # <trimmed-row>
825
+ local row=$1
826
+ fm_composer_normalize_trim_var row
827
+ case "$row" in
828
+ '│'*|*'│'|'┃'*|*'┃'|'║'*|*'║'|'╭'*|*'╭'|'╮'*|*'╮'|\
829
+ '┌'*|*'┌'|'┐'*|*'┐'|'╔'*|*'╔'|'╗'*|*'╗'|'┏'*|*'┏'|'┓'*|*'┓'|\
830
+ '╰'*|*'╰'|'╯'*|*'╯'|'└'*|*'└'|'┘'*|*'┘'|'╚'*|*'╚'|'╝'*|*'╝'|\
831
+ '┗'*|*'┗'|'┛'*|*'┛'|'─'*|*'─'|'━'*|*'━'|'═'*|*'═'|'|'*|*'|'|'+'*|*'+'|\
832
+ '▀'*|*'▀'|'▄'*|*'▄'|'▁'*|*'▁'|'▔'*|*'▔')
833
+ return 0
834
+ ;;
835
+ esac
836
+ return 1
837
+ }
838
+
839
+ # fm_composer_geometry_spaces: prove a box content row blank to the same width
840
+ # as its border. One leading prompt glyph is blanked (every prompt glyph
841
+ # occupies one column), the content is normalized so a Unicode space cannot
842
+ # defeat the blankness proof, then every remaining ASCII-printable is mapped to
843
+ # a space; any other residue fails the proof.
844
+ fm_composer_geometry_spaces() { # <content-inner> -> spaces
845
+ local content=$1 glyph
846
+ fm_composer_normalize_spaces_var content
847
+ if fm_composer_leading_prompt_glyph_var glyph "$content"; then
848
+ content=${content/"$glyph"/ }
849
+ fi
850
+ content=$(printf '%s' "$content" | LC_ALL=C sed 's/[!-~]/ /g')
851
+ case "$content" in
852
+ *[![:space:]]*) return 1 ;;
853
+ esac
854
+ printf '%s' "$content"
855
+ }
856
+
857
+ # _fm_composer_screen_row: print row <n> (zero-based) of <screen>.
858
+ _fm_composer_screen_row() { # <n> <screen>
859
+ printf '%s\n' "$2" | sed -n "$(($1 + 1))p"
860
+ }
861
+
862
+ # _fm_composer_row_content: extract the classification content of one raw row:
863
+ # ghost-strip when styled, plain otherwise, normalize-trim, and strip one
864
+ # matching pair of side border glyphs.
865
+ _fm_composer_row_content() { # <raw-row> <styled> -> content on stdout
866
+ local raw=$1 styled=$2 stripped
867
+ if [ "$styled" = 1 ]; then
868
+ stripped=$(printf '%s\n' "$raw" | fm_composer_strip_ghost)
869
+ else
870
+ stripped=$(printf '%s\n' "$raw" | fm_composer_strip_ansi)
871
+ fi
872
+ fm_composer_normalize_trim_var stripped
873
+ case "$stripped" in
874
+ '│'*'│') stripped=${stripped#│}; stripped=${stripped%│} ;;
875
+ '┃'*'┃') stripped=${stripped#┃}; stripped=${stripped%┃} ;;
876
+ '║'*'║') stripped=${stripped#║}; stripped=${stripped%║} ;;
877
+ '|'*'|') stripped=${stripped#|}; stripped=${stripped%|} ;;
878
+ esac
879
+ fm_composer_normalize_trim_var stripped
880
+ printf '%s' "$stripped"
881
+ }
882
+
883
+ # _fm_composer_classify_rows: shared multi-row container verdict for the box
884
+ # and separated shapes: pending beats empty, an unreadable row is unknown, and
885
+ # geometry ambiguity turns pending into pending-unproven and empty into
886
+ # unknown (an ambiguous container is not positive proof).
887
+ _fm_composer_classify_rows() { # <screen> <styled> <ambiguous> <first-row> <last-row>
888
+ local screen=$1 styled=$2 ambiguous=$3 first=$4 last=$5
889
+ local row raw content plain state unknown_seen=0
890
+ row=$first
891
+ while [ "$row" -le "$last" ]; do
892
+ raw=$(_fm_composer_screen_row "$row" "$screen")
893
+ content=$(_fm_composer_row_content "$raw" "$styled")
894
+ plain=$(_fm_composer_row_content "$raw" 0)
895
+ state=$(fm_composer_classify_content 1 "$content" \
896
+ "${FM_COMPOSER_IDLE_RE:-$FM_COMPOSER_IDLE_RE_DEFAULT}" insensitive "$plain" 1 "$styled")
897
+ case "$state" in
898
+ pending)
899
+ if [ "$ambiguous" = 1 ]; then printf 'pending-unproven'; else printf 'pending'; fi
900
+ return 0
901
+ ;;
902
+ unknown) unknown_seen=1 ;;
903
+ esac
904
+ row=$((row + 1))
905
+ done
906
+ if [ "$unknown_seen" = 1 ] || [ "$ambiguous" = 1 ]; then
907
+ printf 'unknown'
908
+ else
909
+ printf 'empty'
910
+ fi
911
+ }
912
+
913
+ # _fm_composer_classify_bare_row: the bare agent-glyph row verdict, including
914
+ # the styled=0 degradation: without styling, trailing text after the glyph may
915
+ # be the harness's own idle suggestion (claude's rotating dim hint, codex's
916
+ # `Use /skills ...`), so it must read `unknown` rather than a false `pending`.
917
+ _fm_composer_classify_bare_row() { # <screen> <styled> <row>
918
+ local screen=$1 styled=$2 row=$3 raw content plain state
919
+ raw=$(_fm_composer_screen_row "$row" "$screen")
920
+ content=$(_fm_composer_row_content "$raw" "$styled")
921
+ plain=$(_fm_composer_row_content "$raw" 0)
922
+ state=$(fm_composer_classify_content 0 "$content" \
923
+ "${FM_COMPOSER_IDLE_RE:-$FM_COMPOSER_IDLE_RE_DEFAULT}" insensitive "$plain" 0 "$styled")
924
+ if [ "$styled" != 1 ] && [ "$state" = pending ]; then
925
+ printf 'unknown'
926
+ return 0
927
+ fi
928
+ printf '%s' "$state"
929
+ }
930
+
931
+ # _fm_composer_wrap_region_ok: 0 when every row STRICTLY BELOW <glyph-row>
932
+ # through <cursor-row> is non-blank and carries no structural edge - the
933
+ # contiguity proof that those rows are the bare composer's wrapped input
934
+ # rather than unrelated screen content.
935
+ _fm_composer_wrap_region_ok() { # <plain-screen> <glyph-row> <cursor-row>
936
+ local plain=$1 g=$2 cy=$3 row line trimmed glyph
937
+ row=$((g + 1))
938
+ while [ "$row" -le "$cy" ]; do
939
+ line=$(_fm_composer_screen_row "$row" "$plain")
940
+ trimmed=$line
941
+ fm_composer_normalize_trim_var trimmed
942
+ [ -n "$trimmed" ] || return 1
943
+ if fm_composer_row_has_edge "$trimmed"; then return 1; fi
944
+ if fm_composer_leading_shell_glyph_var glyph "$trimmed"; then return 1; fi
945
+ row=$((row + 1))
946
+ done
947
+ return 0
948
+ }
949
+
950
+ # _fm_composer_classify_bare_wrap: the bare composer plus its wrap region.
951
+ # Content is the glyph row (glyph stripped) plus every continuation row down
952
+ # to the cursor. Ghost-stripped-to-nothing rows are an empty composer whose
953
+ # suggestion happened to wrap; any surviving text is pending when styling can
954
+ # prove it real and unknown otherwise (the same styled=0 degradation as the
955
+ # glyph row itself).
956
+ _fm_composer_classify_bare_wrap() { # <screen> <styled> <glyph-row> <cursor-row>
957
+ local screen=$1 styled=$2 g=$3 cy=$4 row raw content glyph='' text_seen=0
958
+ row=$g
959
+ while [ "$row" -le "$cy" ]; do
960
+ raw=$(_fm_composer_screen_row "$row" "$screen")
961
+ content=$(_fm_composer_row_content "$raw" "$styled")
962
+ if [ "$row" -eq "$g" ] && fm_composer_leading_agent_glyph_var glyph "$content"; then
963
+ content=${content#*"$glyph"}
964
+ fi
965
+ fm_composer_normalize_trim_var content
966
+ [ -z "$content" ] || text_seen=1
967
+ row=$((row + 1))
968
+ done
969
+ if [ "$text_seen" = 0 ]; then
970
+ printf 'empty'
971
+ return 0
972
+ fi
973
+ if [ "$styled" = 1 ]; then printf 'pending'; else printf 'unknown'; fi
974
+ }
975
+
976
+ # _fm_composer_classify_leftbar: opencode's left-bar composer. Blank rows and
977
+ # the idle hint read empty; the run's LAST row may be the mode/model footer
978
+ # (composer furniture, never typed text). Real content is pending when styling
979
+ # can prove it real, unknown otherwise.
980
+ _fm_composer_classify_leftbar() { # <screen> <styled> <first-row> <last-row>
981
+ local screen=$1 styled=$2 first=$3 last=$4
982
+ local row raw content pending_seen=0 footer_re leading_blank=1 placeholder_position=0
983
+ footer_re=${FM_COMPOSER_LEFTBAR_FOOTER_RE:-$FM_COMPOSER_LEFTBAR_FOOTER_RE_DEFAULT}
984
+ row=$first
985
+ while [ "$row" -le "$last" ]; do
986
+ raw=$(_fm_composer_screen_row "$row" "$screen")
987
+ content=$(_fm_composer_row_content "$raw" "$styled")
988
+ case "$content" in
989
+ '┃'*) content=${content#┃} ;;
990
+ esac
991
+ fm_composer_normalize_trim_var content
992
+ if [ -z "$content" ]; then row=$((row + 1)); continue; fi
993
+ if [ "$leading_blank" = 1 ] && [ "$row" -gt "$first" ]; then
994
+ placeholder_position=1
995
+ else
996
+ placeholder_position=0
997
+ fi
998
+ leading_blank=0
999
+ if [ "$placeholder_position" = 1 ] \
1000
+ && fm_composer_idle_matches "$content" "${FM_COMPOSER_IDLE_RE:-$FM_COMPOSER_IDLE_RE_DEFAULT}" insensitive; then
1001
+ row=$((row + 1)); continue
1002
+ fi
1003
+ if [ "$row" -eq "$last" ] \
1004
+ && fm_composer_idle_matches "$content" "$footer_re" sensitive; then
1005
+ row=$((row + 1)); continue
1006
+ fi
1007
+ pending_seen=1
1008
+ row=$((row + 1))
1009
+ done
1010
+ if [ "$pending_seen" = 1 ]; then
1011
+ if [ "$styled" = 1 ]; then printf 'pending'; else printf 'unknown'; fi
1012
+ else
1013
+ printf 'empty'
1014
+ fi
1015
+ }
1016
+
1017
+ _fm_composer_leftbar_floor_row() { # <trimmed-row>
1018
+ local row=$1 blocks
1019
+ case "$row" in
1020
+ '╹▀'*) blocks=${row#╹} ;;
1021
+ *) return 1 ;;
1022
+ esac
1023
+ [ -z "${blocks//▀/}" ]
1024
+ }
1025
+
1026
+ _fm_composer_select_cursorless() {
1027
+ local plain=$1 generic=-1 next boundary raw trimmed
1028
+ FM_COMPOSER_SELECTED_KIND=
1029
+ FM_COMPOSER_SELECTED_FIRST=-1
1030
+ FM_COMPOSER_SELECTED_LAST=-1
1031
+ FM_COMPOSER_SELECTED_AMBIG=0
1032
+ if [ "$FM_COMPOSER_SCAN_BOX_BOTTOM" -ge 0 ]; then
1033
+ generic=$FM_COMPOSER_SCAN_BOX_BOTTOM
1034
+ FM_COMPOSER_SELECTED_KIND=box
1035
+ FM_COMPOSER_SELECTED_FIRST=$((FM_COMPOSER_SCAN_BOX_TOP + 1))
1036
+ FM_COMPOSER_SELECTED_LAST=$((FM_COMPOSER_SCAN_BOX_BOTTOM - 1))
1037
+ FM_COMPOSER_SELECTED_AMBIG=$FM_COMPOSER_SCAN_BOX_AMBIG
1038
+ fi
1039
+ if [ "$FM_COMPOSER_SCAN_BARE_ROW" -gt "$generic" ]; then
1040
+ generic=$FM_COMPOSER_SCAN_BARE_ROW
1041
+ FM_COMPOSER_SELECTED_KIND=bare
1042
+ FM_COMPOSER_SELECTED_FIRST=$FM_COMPOSER_SCAN_BARE_ROW
1043
+ FM_COMPOSER_SELECTED_LAST=$FM_COMPOSER_SCAN_BARE_ROW
1044
+ fi
1045
+ if [ "$FM_COMPOSER_SCAN_LEFTBAR_END" -gt "$generic" ]; then
1046
+ generic=$FM_COMPOSER_SCAN_LEFTBAR_END
1047
+ FM_COMPOSER_SELECTED_KIND=leftbar
1048
+ FM_COMPOSER_SELECTED_FIRST=$FM_COMPOSER_SCAN_LEFTBAR_START
1049
+ FM_COMPOSER_SELECTED_LAST=$FM_COMPOSER_SCAN_LEFTBAR_END
1050
+ fi
1051
+ if [ "$FM_COMPOSER_SCAN_INCOMPLETE_BOX_FROM" -gt "$generic" ]; then
1052
+ FM_COMPOSER_SELECTED_KIND=
1053
+ return 1
1054
+ fi
1055
+ if [ "$FM_COMPOSER_SCAN_PI_PAIR_FOUND" = 1 ] \
1056
+ && [ "$FM_COMPOSER_SCAN_PI_CLOSE" -gt "$generic" ] \
1057
+ && [ "$generic" -lt "$FM_COMPOSER_SCAN_PI_OPEN" ]; then
1058
+ generic=$FM_COMPOSER_SCAN_PI_CLOSE
1059
+ FM_COMPOSER_SELECTED_KIND=pi
1060
+ FM_COMPOSER_SELECTED_FIRST=$((FM_COMPOSER_SCAN_PI_OPEN + 1))
1061
+ FM_COMPOSER_SELECTED_LAST=$((FM_COMPOSER_SCAN_PI_CLOSE - 1))
1062
+ fi
1063
+ if [ "$FM_COMPOSER_SCAN_PI_PAIR_FOUND" = 0 ] \
1064
+ && [ "$FM_COMPOSER_SCAN_PI_LAST_SEPARATOR" -gt "$generic" ]; then
1065
+ FM_COMPOSER_SELECTED_KIND=
1066
+ return 1
1067
+ fi
1068
+ if [ "$FM_COMPOSER_SCAN_SHELL_ROW" -gt "$generic" ]; then
1069
+ FM_COMPOSER_SELECTED_KIND=
1070
+ return 1
1071
+ fi
1072
+ if [ "$FM_COMPOSER_SELECTED_KIND" = bare ]; then
1073
+ next=$((FM_COMPOSER_SELECTED_LAST + 1))
1074
+ while :; do
1075
+ raw=$(_fm_composer_screen_row "$next" "$plain")
1076
+ trimmed=$raw
1077
+ fm_composer_normalize_trim_var trimmed
1078
+ [ -n "$trimmed" ] || break
1079
+ fm_composer_row_has_edge "$trimmed" && break
1080
+ FM_COMPOSER_SELECTED_LAST=$next
1081
+ next=$((next + 1))
1082
+ done
1083
+ fi
1084
+ if [ "$FM_COMPOSER_SELECTED_KIND" = box ] \
1085
+ || [ "$FM_COMPOSER_SELECTED_KIND" = leftbar ]; then
1086
+ boundary=$FM_COMPOSER_SELECTED_LAST
1087
+ if [ "$FM_COMPOSER_SELECTED_KIND" = box ]; then
1088
+ boundary=$FM_COMPOSER_SCAN_BOX_BOTTOM
1089
+ else
1090
+ next=$((boundary + 1))
1091
+ raw=$(_fm_composer_screen_row "$next" "$plain")
1092
+ trimmed=$raw
1093
+ fm_composer_normalize_trim_var trimmed
1094
+ if _fm_composer_leftbar_floor_row "$trimmed"; then
1095
+ boundary=$next
1096
+ fi
1097
+ fi
1098
+ next=$((boundary + 1))
1099
+ raw=$(_fm_composer_screen_row "$next" "$plain")
1100
+ trimmed=$raw
1101
+ fm_composer_normalize_trim_var trimmed
1102
+ if [ -n "$trimmed" ] && ! fm_composer_row_has_edge "$trimmed"; then
1103
+ FM_COMPOSER_SELECTED_KIND=
1104
+ return 1
1105
+ fi
1106
+ fi
1107
+ [ -n "$FM_COMPOSER_SELECTED_KIND" ]
1108
+ }
1109
+
1110
+ fm_composer_extract_selected_content() { # <caps> <screen>
1111
+ local caps=$1 screen=$2 styled=0 kv plain row raw content glyph joined='' footer_re prompt_row=-1
1112
+ local leading_blank=1 placeholder_position=0 prompt_is_shell=0
1113
+ footer_re=${FM_COMPOSER_LEFTBAR_FOOTER_RE:-$FM_COMPOSER_LEFTBAR_FOOTER_RE_DEFAULT}
1114
+ while IFS= read -r kv; do
1115
+ [ "$kv" = styled=1 ] && styled=1
1116
+ done <<EOF
1117
+ $caps
1118
+ EOF
1119
+ plain=$(printf '%s\n' "$screen" | fm_composer_strip_ansi)
1120
+ _fm_composer_scan_screen "$plain" '' 1
1121
+ _fm_composer_select_cursorless "$plain" || return 1
1122
+ row=$FM_COMPOSER_SELECTED_FIRST
1123
+ while [ "$row" -le "$FM_COMPOSER_SELECTED_LAST" ]; do
1124
+ raw=$(_fm_composer_screen_row "$row" "$screen")
1125
+ content=$(_fm_composer_row_content "$raw" "$styled")
1126
+ placeholder_position=0
1127
+ case "$FM_COMPOSER_SELECTED_KIND" in
1128
+ bare)
1129
+ if [ "$row" -eq "$FM_COMPOSER_SELECTED_FIRST" ] \
1130
+ && fm_composer_leading_agent_glyph_var glyph "$content"; then
1131
+ content=${content#*"$glyph"}
1132
+ fi
1133
+ ;;
1134
+ leftbar)
1135
+ case "$content" in '┃'*) content=${content#┃} ;; esac
1136
+ fm_composer_normalize_trim_var content
1137
+ if [ -z "$content" ]; then
1138
+ :
1139
+ elif [ "$leading_blank" = 1 ] && [ "$row" -gt "$FM_COMPOSER_SELECTED_FIRST" ]; then
1140
+ placeholder_position=1
1141
+ leading_blank=0
1142
+ else
1143
+ leading_blank=0
1144
+ fi
1145
+ ;;
1146
+ box)
1147
+ if [ "$prompt_row" -lt 0 ] \
1148
+ && fm_composer_leading_prompt_glyph_var glyph "$content"; then
1149
+ prompt_row=$row
1150
+ placeholder_position=1
1151
+ if _fm_composer_is_prompt_glyph "$glyph" "$FM_COMPOSER_SHELL_PROMPT_GLYPHS"; then
1152
+ prompt_is_shell=1
1153
+ fi
1154
+ content=${content#*"$glyph"}
1155
+ elif [ "$prompt_row" -lt 0 ]; then
1156
+ placeholder_position=1
1157
+ fi
1158
+ ;;
1159
+ esac
1160
+ fm_composer_normalize_spaces_var content
1161
+ fm_composer_normalize_trim_var content
1162
+ # A styled agent-glyph placeholder disappears above when ghost stripping
1163
+ # proves it is furniture. If the same placeholder-looking bytes survive
1164
+ # styling, they are real user input and must remain in the extracted content
1165
+ # (the zellij paste proof depends on observing exactly what was typed).
1166
+ # OpenCode's left-bar hint and legacy shell-glyph boxed placeholders have no
1167
+ # such styling proof, so their structurally fixed positions remain the two
1168
+ # idle-regex exceptions here.
1169
+ if [ -z "$content" ] \
1170
+ || { { [ "$FM_COMPOSER_SELECTED_KIND" = leftbar ] \
1171
+ || { [ "$FM_COMPOSER_SELECTED_KIND" = box ] && [ "$prompt_is_shell" = 1 ]; }; } \
1172
+ && [ "$placeholder_position" = 1 ] \
1173
+ && fm_composer_idle_matches "$content" "${FM_COMPOSER_IDLE_RE:-$FM_COMPOSER_IDLE_RE_DEFAULT}" insensitive; } \
1174
+ || { [ "$FM_COMPOSER_SELECTED_KIND" = leftbar ] \
1175
+ && [ "$row" -eq "$FM_COMPOSER_SELECTED_LAST" ] \
1176
+ && fm_composer_idle_matches "$content" "$footer_re" sensitive; }; then
1177
+ row=$((row + 1))
1178
+ continue
1179
+ fi
1180
+ joined="${joined}${joined:+ }$content"
1181
+ row=$((row + 1))
1182
+ done
1183
+ printf '%s\n' "$joined" | LC_ALL=C awk '{$1=$1; printf "%s", $0}'
1184
+ }
1185
+
1186
+ fm_composer_classify_screen() { # <caps> <screen> [cursor_row] [identity]
1187
+ local caps=$1 screen=$2 cy=${3:-} identity=${4:-}
1188
+ local styled=0 cursor=0 has_identity=0 kv plain
1189
+ while IFS= read -r kv; do
1190
+ case "$kv" in
1191
+ styled=1) styled=1 ;;
1192
+ cursor=1) cursor=1 ;;
1193
+ identity=1) has_identity=1 ;;
1194
+ esac
1195
+ done <<EOF
1196
+ $caps
1197
+ EOF
1198
+ [ "$cursor" = 1 ] || cy=''
1199
+ if [ -n "$cy" ]; then
1200
+ case "$cy" in *[!0-9]*) printf 'unknown'; return 0 ;; esac
1201
+ fi
1202
+ plain=$(printf '%s\n' "$screen" | fm_composer_strip_ansi)
1203
+ _fm_composer_scan_screen "$plain" "$cy"
1204
+ if [ -n "$cy" ]; then
1205
+ # Cursor mode (tmux): the shape CONTAINING the cursor is the composer.
1206
+ if [ "$FM_COMPOSER_SCAN_UNSAFE" = 1 ]; then
1207
+ printf 'unknown'; return 0
1208
+ fi
1209
+ if [ "$FM_COMPOSER_SCAN_BOX_TOP" -ge 0 ]; then
1210
+ _fm_composer_classify_rows "$screen" "$styled" "$FM_COMPOSER_SCAN_BOX_AMBIG" \
1211
+ "$((FM_COMPOSER_SCAN_BOX_TOP + 1))" "$((FM_COMPOSER_SCAN_BOX_BOTTOM - 1))"
1212
+ return 0
1213
+ fi
1214
+ if [ "$FM_COMPOSER_SCAN_LEFTBAR_START" -ge 0 ] \
1215
+ && [ "$cy" -ge "$FM_COMPOSER_SCAN_LEFTBAR_START" ] \
1216
+ && [ "$cy" -le "$FM_COMPOSER_SCAN_LEFTBAR_END" ]; then
1217
+ _fm_composer_classify_leftbar "$screen" "$styled" \
1218
+ "$FM_COMPOSER_SCAN_LEFTBAR_START" "$FM_COMPOSER_SCAN_LEFTBAR_END"
1219
+ return 0
1220
+ fi
1221
+ if [ "$FM_COMPOSER_SCAN_BARE_ROW" -ge 0 ] && [ "$cy" -eq "$FM_COMPOSER_SCAN_BARE_ROW" ]; then
1222
+ if [ "$FM_COMPOSER_SCAN_PI_PAIR_FOUND" = 1 ] \
1223
+ && [ "$cy" -gt "$FM_COMPOSER_SCAN_PI_OPEN" ] \
1224
+ && [ "$cy" -lt "$FM_COMPOSER_SCAN_PI_CLOSE" ]; then
1225
+ _fm_composer_classify_bare_pi_overlap "$screen" "$styled" "$has_identity" "$identity" "$cy"
1226
+ else
1227
+ _fm_composer_classify_bare_row "$screen" "$styled" "$cy"
1228
+ fi
1229
+ return 0
1230
+ fi
1231
+ # A bare composer's WRAP region: long typed input wraps below the glyph
1232
+ # row, and the cursor lands on a continuation row that carries no glyph of
1233
+ # its own. When every row from the glyph row down to the cursor is
1234
+ # non-blank and non-structural, the cursor is inside that composer's
1235
+ # wrapped input - an IDENTIFIED region, so the strict blank-row rule does
1236
+ # not apply and a swallowed Enter on a long message still reads pending
1237
+ # and earns its retry.
1238
+ if [ "$FM_COMPOSER_SCAN_BARE_ROW" -ge 0 ] && [ "$cy" -gt "$FM_COMPOSER_SCAN_BARE_ROW" ] \
1239
+ && _fm_composer_wrap_region_ok "$plain" "$FM_COMPOSER_SCAN_BARE_ROW" "$cy"; then
1240
+ _fm_composer_classify_bare_wrap "$screen" "$styled" "$FM_COMPOSER_SCAN_BARE_ROW" "$cy"
1241
+ return 0
1242
+ fi
1243
+ if [ "$FM_COMPOSER_SCAN_PI_PAIR_FOUND" = 1 ] \
1244
+ && [ "$cy" -gt "$FM_COMPOSER_SCAN_PI_OPEN" ] \
1245
+ && [ "$cy" -lt "$FM_COMPOSER_SCAN_PI_CLOSE" ]; then
1246
+ _fm_composer_pi_verdict "$screen" "$styled" "$has_identity" "$identity"
1247
+ return 0
1248
+ fi
1249
+ if [ "$FM_COMPOSER_SCAN_CURSOR_EDGE" = 1 ]; then
1250
+ printf 'unknown'; return 0
1251
+ fi
1252
+ # STRICT: a blank or otherwise unidentified cursor row has no positive
1253
+ # container proof. This replaced the permissive blank-cursor-row rule
1254
+ # (captain decision blank-row-injection-posture).
1255
+ printf 'unknown'
1256
+ return 0
1257
+ fi
1258
+ # No cursor: the bottom-most shape wins, with the pi-separator staleness
1259
+ # rules layered on (a live pi composer pair below the generic candidate
1260
+ # proves that candidate stale).
1261
+ if ! _fm_composer_select_cursorless "$plain"; then
1262
+ printf 'unknown'
1263
+ return 0
1264
+ fi
1265
+ case "$FM_COMPOSER_SELECTED_KIND" in
1266
+ pi)
1267
+ _fm_composer_pi_verdict "$screen" "$styled" "$has_identity" "$identity"
1268
+ ;;
1269
+ box)
1270
+ _fm_composer_classify_rows "$screen" "$styled" "$FM_COMPOSER_SELECTED_AMBIG" \
1271
+ "$FM_COMPOSER_SELECTED_FIRST" "$FM_COMPOSER_SELECTED_LAST"
1272
+ ;;
1273
+ bare)
1274
+ if [ "$FM_COMPOSER_SELECTED_LAST" -gt "$FM_COMPOSER_SELECTED_FIRST" ]; then
1275
+ _fm_composer_classify_bare_wrap "$screen" "$styled" \
1276
+ "$FM_COMPOSER_SELECTED_FIRST" "$FM_COMPOSER_SELECTED_LAST"
1277
+ elif [ "$FM_COMPOSER_SCAN_PI_PAIR_FOUND" = 1 ] \
1278
+ && [ "$FM_COMPOSER_SCAN_BARE_ROW" -gt "$FM_COMPOSER_SCAN_PI_OPEN" ] \
1279
+ && [ "$FM_COMPOSER_SCAN_BARE_ROW" -lt "$FM_COMPOSER_SCAN_PI_CLOSE" ]; then
1280
+ _fm_composer_classify_bare_pi_overlap "$screen" "$styled" "$has_identity" "$identity" \
1281
+ "$FM_COMPOSER_SCAN_BARE_ROW"
1282
+ else
1283
+ _fm_composer_classify_bare_row "$screen" "$styled" "$FM_COMPOSER_SCAN_BARE_ROW"
1284
+ fi
1285
+ ;;
1286
+ leftbar)
1287
+ _fm_composer_classify_leftbar "$screen" "$styled" \
1288
+ "$FM_COMPOSER_SELECTED_FIRST" "$FM_COMPOSER_SELECTED_LAST"
1289
+ ;;
1290
+ esac
1291
+ }
1292
+
1293
+ # fm_composer_submit_retry_core: the ONE verify-and-retry-Enter submit loop
1294
+ # for the cursor-less backends (cmux, orca, zellij), parameterised by the
1295
+ # adapter's send-key and composer-state functions. The caller has already
1296
+ # typed the text ONCE (send_literal) and settled; this loop submits with
1297
+ # Enter, re-reading the composer verdict, and retries Enter ONLY - never
1298
+ # retypes, because a swallowed Enter leaves the text in the composer and
1299
+ # retyping would duplicate it. Proven pending (and pending-unproven) retries
1300
+ # consume the budget; any other verdict returns immediately, so `unknown`
1301
+ # stays a loud refusal rather than a blind retry into an unreadable pane.
1302
+ # tmux and herdr keep richer cores that consume this same shared verdict plus
1303
+ # fm_composer_queued_enter_verdict; no shape knowledge lives in any loop.
1304
+ fm_composer_submit_retry_core() { # <send-key-fn> <state-fn> <target> <retries> <enter-sleep> [expected-label]
1305
+ local send_key_fn=$1 state_fn=$2 target=$3 retries=$4 sleep_s=$5 expected_label=${6:-} i=0 state
1306
+ while :; do
1307
+ "$send_key_fn" "$target" Enter "$expected_label" || true
1308
+ sleep "$sleep_s"
1309
+ state=$("$state_fn" "$target" "$expected_label")
1310
+ case "$state" in
1311
+ pending|pending-unproven) ;;
1312
+ *) printf '%s' "$state"; return 0 ;;
1313
+ esac
1314
+ i=$((i + 1))
1315
+ [ "$i" -lt "$retries" ] || { printf '%s' "$state"; return 0; }
1316
+ done
1317
+ }
1318
+
1319
+ # fm_composer_queued_enter_verdict: the ONE busy-queued-Enter policy.
1320
+ # After Enter retries are spent, convert a structurally proven pending
1321
+ # composer given a delivery-busy signal from the adapter:
1322
+ # pending + busy -> empty (Enter was accepted and queued; do not re-send)
1323
+ # pending + idle -> pending (genuine swallow; caller must not assume delivery)
1324
+ # pending + unknown -> pending (unreadable busy is not proof of a queue)
1325
+ # Every other composer verdict is returned unchanged, so pending-unproven,
1326
+ # empty, and unknown never receive this conversion.
1327
+ # Adapters supply their own busy primitive (tmux: fm_pane_is_busy; herdr:
1328
+ # native agent_status=working, or a rendered busy footer on an idle native
1329
+ # baseline). This function does not read a pane.
1330
+ fm_composer_queued_enter_verdict() { # <composer-state> <busy|idle|unknown>
1331
+ local state=$1 busy=${2:-}
1332
+ [ "$state" = pending ] || { printf '%s' "$state"; return 0; }
1333
+ if [ "$busy" = busy ]; then
1334
+ printf 'empty'
1335
+ else
1336
+ printf 'pending'
1337
+ fi
1338
+ }
1339
+
1340
+ _fm_composer_classify_pi_rows() { # <screen> <styled>
1341
+ local screen=$1 styled=$2 row raw content
1342
+ row=$((FM_COMPOSER_SCAN_PI_OPEN + 1))
1343
+ while [ "$row" -lt "$FM_COMPOSER_SCAN_PI_CLOSE" ]; do
1344
+ raw=$(_fm_composer_screen_row "$row" "$screen")
1345
+ content=$(_fm_composer_row_content "$raw" "$styled")
1346
+ fm_composer_normalize_trim_var content
1347
+ if [ -n "$content" ]; then
1348
+ printf 'pending'
1349
+ return 0
1350
+ fi
1351
+ row=$((row + 1))
1352
+ done
1353
+ printf 'empty'
1354
+ }
1355
+
1356
+ _fm_composer_classify_bare_pi_overlap() { # <screen> <styled> <has-identity> <identity> <bare-row>
1357
+ local screen=$1 styled=$2 has_identity=$3 identity=$4 row=$5 agent
1358
+ if [ "$has_identity" != 1 ]; then
1359
+ _fm_composer_classify_bare_row "$screen" "$styled" "$row"
1360
+ return 0
1361
+ fi
1362
+ if [ -z "$identity" ]; then
1363
+ printf 'need-identity'
1364
+ return 0
1365
+ fi
1366
+ if [ "$identity" = probe-absent ]; then
1367
+ _fm_composer_classify_bare_row "$screen" "$styled" "$row"
1368
+ return 0
1369
+ fi
1370
+ agent=${identity%%$'\t'*}
1371
+ if [ "$agent" = pi ]; then
1372
+ _fm_composer_pi_verdict "$screen" "$styled" "$has_identity" "$identity"
1373
+ else
1374
+ _fm_composer_classify_bare_row "$screen" "$styled" "$row"
1375
+ fi
1376
+ }
1377
+
1378
+ # The pi separated-shape verdict: identity + structure conjunction (herdr's
1379
+ # rule, now fleet-wide). A missing identity capability keeps the shape
1380
+ # unknown; an unfetched identity on an identity-capable backend asks the
1381
+ # adapter to probe (lazily) and re-call. Proven input remains pending for every
1382
+ # live pi state, while only an idle/done pi proves an empty composer. A blocked
1383
+ # pi is parked on an interactive prompt waiting for a human keystroke: its menu
1384
+ # is drawn above the separator pair, so the composer region looks free while the
1385
+ # keys would answer the prompt instead of composing (issue #2797). Structure
1386
+ # cannot disprove that, so a blocked pi defers rather than claiming empty.
1387
+ _fm_composer_pi_verdict() { # <screen> <styled> <has_identity> <identity>
1388
+ local screen=$1 styled=$2 has_identity=$3 identity=$4 agent agent_status state
1389
+ if [ "$has_identity" != 1 ]; then
1390
+ printf 'unknown'
1391
+ return 0
1392
+ fi
1393
+ if [ -z "$identity" ]; then
1394
+ printf 'need-identity'
1395
+ return 0
1396
+ fi
1397
+ if [ "$identity" = probe-absent ]; then
1398
+ printf 'unknown'
1399
+ return 0
1400
+ fi
1401
+ agent=${identity%%$'\t'*}
1402
+ agent_status=${identity#*$'\t'}
1403
+ if [ "$agent" != pi ] || [ "$FM_COMPOSER_SCAN_PI_PAIR_VALID" != 1 ]; then
1404
+ printf 'unknown'
1405
+ return 0
1406
+ fi
1407
+ state=$(_fm_composer_classify_pi_rows "$screen" "$styled")
1408
+ if [ "$state" = pending ]; then
1409
+ printf 'pending'
1410
+ return 0
1411
+ fi
1412
+ case "$agent_status" in
1413
+ idle|done) printf 'empty' ;;
1414
+ *) printf 'unknown' ;;
1415
+ esac
1416
+ }