@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.
- package/.agents/README.md +291 -0
- package/.agents/agents/bragi-marketer.md +111 -0
- package/.agents/agents/brokk.md +352 -0
- package/.agents/agents/forseti-reviewer.md +93 -0
- package/.agents/agents/frigg-consent.md +77 -0
- package/.agents/agents/groa-update.md +77 -0
- package/.agents/agents/hnoss-designer.md +69 -0
- package/.agents/agents/huginn-researcher.md +99 -0
- package/.agents/agents/jord-projects.md +78 -0
- package/.agents/agents/kvasir-scout.md +91 -0
- package/.agents/agents/mimir-planner.md +92 -0
- package/.agents/agents/muninn-stow.md +77 -0
- package/.agents/agents/saga-bearings.md +78 -0
- package/.agents/agents/sindri-developer.md +114 -0
- package/.agents/agents/snotra-documenter.md +105 -0
- package/.agents/agents/syn-recovery.md +78 -0
- package/.agents/agents/tyr-check.md +78 -0
- package/.agents/agents/urdh-hold.md +77 -0
- package/.agents/agents/volundr-smith.md +80 -0
- package/.agents/agents/vor-diagnostics.md +77 -0
- package/.agents/assets/agents/naming.md +50 -0
- package/.agents/assets/agents/registry.md +93 -0
- package/.agents/assets/agents/runtime.md +36 -0
- package/.agents/assets/agents/toon-tasks-cli.md +42 -0
- package/.agents/assets/templates/company_entity.template.md +37 -0
- package/.agents/backend/backends/cmux.sh +649 -0
- package/.agents/backend/backends/herdr-eventwait.py +157 -0
- package/.agents/backend/backends/herdr-workspace-move.py +114 -0
- package/.agents/backend/backends/herdr.sh +3386 -0
- package/.agents/backend/backends/orca.sh +290 -0
- package/.agents/backend/backends/tmux.sh +357 -0
- package/.agents/backend/backends/zellij.sh +700 -0
- package/.agents/backend/fm-afk-launch.sh +654 -0
- package/.agents/backend/fm-afk-return.sh +241 -0
- package/.agents/backend/fm-afk-start.sh +172 -0
- package/.agents/backend/fm-arm-command-policy.mjs +960 -0
- package/.agents/backend/fm-arm-pretool-check.sh +200 -0
- package/.agents/backend/fm-backend-hometag-lib.sh +52 -0
- package/.agents/backend/fm-backend.sh +981 -0
- package/.agents/backend/fm-backlog-handoff.sh +909 -0
- package/.agents/backend/fm-backlog-receive.sh +187 -0
- package/.agents/backend/fm-backlog-transition-lib.sh +779 -0
- package/.agents/backend/fm-bearings-board.sh +204 -0
- package/.agents/backend/fm-bearings-snapshot.sh +546 -0
- package/.agents/backend/fm-bootstrap.sh +1568 -0
- package/.agents/backend/fm-branch-outcome.sh +203 -0
- package/.agents/backend/fm-branch-prompt.sh +101 -0
- package/.agents/backend/fm-brief.sh +452 -0
- package/.agents/backend/fm-busy-event.sh +234 -0
- package/.agents/backend/fm-busy-lib.sh +986 -0
- package/.agents/backend/fm-captain-hold.sh +1001 -0
- package/.agents/backend/fm-cd-command-policy.mjs +153 -0
- package/.agents/backend/fm-cd-pretool-check.sh +190 -0
- package/.agents/backend/fm-check-lib.sh +74 -0
- package/.agents/backend/fm-check-register.sh +42 -0
- package/.agents/backend/fm-check-unregister.sh +52 -0
- package/.agents/backend/fm-classify-lib.sh +1770 -0
- package/.agents/backend/fm-claude-stop-autoarm.sh +334 -0
- package/.agents/backend/fm-composer-lib.sh +1416 -0
- package/.agents/backend/fm-config-inherit-lib.sh +1202 -0
- package/.agents/backend/fm-config-push.sh +254 -0
- package/.agents/backend/fm-control-lib.sh +251 -0
- package/.agents/backend/fm-control.sh +878 -0
- package/.agents/backend/fm-crew-state.sh +625 -0
- package/.agents/backend/fm-cursor-lib.sh +243 -0
- package/.agents/backend/fm-decision-hold.sh +232 -0
- package/.agents/backend/fm-doc-audience-check.sh +269 -0
- package/.agents/backend/fm-dod-lib.sh +67 -0
- package/.agents/backend/fm-ensure-agents-md.sh +253 -0
- package/.agents/backend/fm-extension-launch-barrier.mjs +129 -0
- package/.agents/backend/fm-extension.mjs +2577 -0
- package/.agents/backend/fm-extension.sh +16 -0
- package/.agents/backend/fm-ff-lib.sh +420 -0
- package/.agents/backend/fm-fleet-snapshot.sh +1463 -0
- package/.agents/backend/fm-fleet-sync.sh +458 -0
- package/.agents/backend/fm-fleet-view.sh +96 -0
- package/.agents/backend/fm-gate-refuse-lib.sh +102 -0
- package/.agents/backend/fm-guard.sh +242 -0
- package/.agents/backend/fm-harness.sh +195 -0
- package/.agents/backend/fm-herdr-ci-cleanup.sh +115 -0
- package/.agents/backend/fm-herdr-lab.sh +346 -0
- package/.agents/backend/fm-herdr-session-cleanup.sh +337 -0
- package/.agents/backend/fm-home-seed.sh +984 -0
- package/.agents/backend/fm-home-summary-refresh.sh +255 -0
- package/.agents/backend/fm-hook-host-lib.sh +36 -0
- package/.agents/backend/fm-inactive-reconcile.sh +531 -0
- package/.agents/backend/fm-inbox.sh +399 -0
- package/.agents/backend/fm-install-actionlint.sh +84 -0
- package/.agents/backend/fm-install-herdr.sh +96 -0
- package/.agents/backend/fm-install-shellcheck.sh +84 -0
- package/.agents/backend/fm-install-treehouse.sh +95 -0
- package/.agents/backend/fm-kimi-turnend-hook.sh +276 -0
- package/.agents/backend/fm-lease-lib.sh +218 -0
- package/.agents/backend/fm-lease.sh +189 -0
- package/.agents/backend/fm-line-cap-lib.sh +51 -0
- package/.agents/backend/fm-lint-workflows.sh +137 -0
- package/.agents/backend/fm-lint.sh +591 -0
- package/.agents/backend/fm-lock-lib.sh +104 -0
- package/.agents/backend/fm-lock.sh +107 -0
- package/.agents/backend/fm-marker-lib.sh +12 -0
- package/.agents/backend/fm-merge-local.sh +74 -0
- package/.agents/backend/fm-merge-outcome-lib.sh +138 -0
- package/.agents/backend/fm-nm-run-lib.sh +124 -0
- package/.agents/backend/fm-on.sh +125 -0
- package/.agents/backend/fm-operational-input.sh +253 -0
- package/.agents/backend/fm-peek.sh +46 -0
- package/.agents/backend/fm-pending-reply-lib.sh +1409 -0
- package/.agents/backend/fm-pr-check.sh +135 -0
- package/.agents/backend/fm-pr-lib.sh +1016 -0
- package/.agents/backend/fm-pr-merge.sh +707 -0
- package/.agents/backend/fm-pr-poll.sh +110 -0
- package/.agents/backend/fm-primary-scope-lib.sh +33 -0
- package/.agents/backend/fm-procevent-extension-capture.pl +259 -0
- package/.agents/backend/fm-procevent-lavish.sh +628 -0
- package/.agents/backend/fm-procevent-lib.sh +892 -0
- package/.agents/backend/fm-procevent-quota.sh +290 -0
- package/.agents/backend/fm-procevent-remote-reply.sh +577 -0
- package/.agents/backend/fm-procevent-when.sh +504 -0
- package/.agents/backend/fm-procevent.sh +1466 -0
- package/.agents/backend/fm-project-mode.sh +92 -0
- package/.agents/backend/fm-project-origin-lib.sh +180 -0
- package/.agents/backend/fm-promote.sh +282 -0
- package/.agents/backend/fm-public-followup-emit.sh +260 -0
- package/.agents/backend/fm-public-followup-lib.sh +398 -0
- package/.agents/backend/fm-public-followup.sh +1330 -0
- package/.agents/backend/fm-push-transition-lib.sh +168 -0
- package/.agents/backend/fm-quota-axi-lib.sh +93 -0
- package/.agents/backend/fm-quota-choose.sh +384 -0
- package/.agents/backend/fm-remote-delta-read.sh +189 -0
- package/.agents/backend/fm-remote-doctor.sh +800 -0
- package/.agents/backend/fm-remote-entrypoint.sh +193 -0
- package/.agents/backend/fm-remote-file.sh +234 -0
- package/.agents/backend/fm-remote-home-provision.sh +259 -0
- package/.agents/backend/fm-remote-home-seed.sh +259 -0
- package/.agents/backend/fm-remote-inherit-push.sh +90 -0
- package/.agents/backend/fm-remote-inherit.sh +179 -0
- package/.agents/backend/fm-remote-job-lib.sh +1241 -0
- package/.agents/backend/fm-remote-job-reap-orphans.sh +137 -0
- package/.agents/backend/fm-remote-job-worker.sh +1124 -0
- package/.agents/backend/fm-remote-readiness-lib.sh +44 -0
- package/.agents/backend/fm-remote-secondmate-control.sh +340 -0
- package/.agents/backend/fm-review-diff.sh +158 -0
- package/.agents/backend/fm-secondmate-charter-lib.sh +44 -0
- package/.agents/backend/fm-secondmate-nudge-lib.sh +68 -0
- package/.agents/backend/fm-secondmate-parent-lib.sh +70 -0
- package/.agents/backend/fm-secondmate-reconcile.sh +371 -0
- package/.agents/backend/fm-secondmate-registry-lib.sh +311 -0
- package/.agents/backend/fm-secondmate-report.sh +88 -0
- package/.agents/backend/fm-send.sh +1059 -0
- package/.agents/backend/fm-session-lock-lib.sh +176 -0
- package/.agents/backend/fm-session-start.sh +970 -0
- package/.agents/backend/fm-sessionstart-cursor.sh +40 -0
- package/.agents/backend/fm-sessionstart-nudge.sh +45 -0
- package/.agents/backend/fm-sessionstart-run.sh +145 -0
- package/.agents/backend/fm-spawn.sh +3153 -0
- package/.agents/backend/fm-startup-memory-budget-lib.sh +224 -0
- package/.agents/backend/fm-startup-memory-budget.sh +94 -0
- package/.agents/backend/fm-startup-network.sh +642 -0
- package/.agents/backend/fm-stow-cascade.sh +251 -0
- package/.agents/backend/fm-subagent-pretool-check.sh +207 -0
- package/.agents/backend/fm-supervise-daemon.sh +1732 -0
- package/.agents/backend/fm-supervision-instructions.sh +215 -0
- package/.agents/backend/fm-supervision-lib.sh +90 -0
- package/.agents/backend/fm-supervisor-target-lib.sh +78 -0
- package/.agents/backend/fm-tangle-lib.sh +53 -0
- package/.agents/backend/fm-task-inbox-lib.sh +402 -0
- package/.agents/backend/fm-tasks-axi-lib.sh +123 -0
- package/.agents/backend/fm-teardown.sh +2922 -0
- package/.agents/backend/fm-test-isolation-proof.sh +588 -0
- package/.agents/backend/fm-test-run.sh +2191 -0
- package/.agents/backend/fm-timeout-lib.sh +141 -0
- package/.agents/backend/fm-timing-lib.sh +179 -0
- package/.agents/backend/fm-tmux-lib.sh +291 -0
- package/.agents/backend/fm-tool-update-check.sh +898 -0
- package/.agents/backend/fm-trace-context-lib.sh +227 -0
- package/.agents/backend/fm-transition-lib.sh +103 -0
- package/.agents/backend/fm-turnend-guard-cursor.sh +391 -0
- package/.agents/backend/fm-turnend-guard-grok.sh +90 -0
- package/.agents/backend/fm-turnend-guard.sh +432 -0
- package/.agents/backend/fm-update.sh +110 -0
- package/.agents/backend/fm-vendor-auth-probe.sh +190 -0
- package/.agents/backend/fm-voice-client.py +1373 -0
- package/.agents/backend/fm-voice-relay.py +1256 -0
- package/.agents/backend/fm-wake-drain.sh +592 -0
- package/.agents/backend/fm-wake-grant.sh +127 -0
- package/.agents/backend/fm-wake-lib.sh +1849 -0
- package/.agents/backend/fm-watch-arm.sh +602 -0
- package/.agents/backend/fm-watch-checkpoint.sh +109 -0
- package/.agents/backend/fm-watch.sh +1962 -0
- package/.agents/backend/fm-x-dismiss.sh +116 -0
- package/.agents/backend/fm-x-followup.sh +286 -0
- package/.agents/backend/fm-x-lib.sh +1000 -0
- package/.agents/backend/fm-x-link.sh +234 -0
- package/.agents/backend/fm-x-poll.sh +185 -0
- package/.agents/backend/fm-x-reply.sh +385 -0
- package/.agents/backend/fm_voice_frame.py +166 -0
- package/.agents/backend/fm_voice_records.py +574 -0
- package/.agents/backend/model-bridge.py +290 -0
- package/.agents/backend/opencode-go-bridge.py +30 -0
- package/.agents/bus/README.md +12 -0
- package/.agents/bus/messages.json +1 -0
- package/.agents/config/agents.machine.example.yaml +16 -0
- package/.agents/config/agents.yaml +50 -0
- package/.agents/config/agents.yaml.example +72 -0
- package/.agents/config/crew-dispatch.json +22 -0
- package/.agents/config/cron.yaml +9 -0
- package/.agents/config/eindri-dispatch.json +67 -0
- package/.agents/config/eindri-harness +1 -0
- package/.agents/config/startup-memory-budget +1 -0
- package/.agents/config/tailscale-sync.yaml +27 -0
- package/.agents/filebrowser/.filebrowser.json +6 -0
- package/.agents/filebrowser/README.md +11 -0
- package/.agents/gateway/README.md +12 -0
- package/.agents/github/README.md +15 -0
- package/.agents/github/webhooks/.gitkeep +0 -0
- package/.agents/github/workflows/.gitkeep +0 -0
- package/.agents/harness/opencode/plugins/lib/rodd-operational-input.js +37 -0
- package/.agents/harness/opencode/plugins/package.json +4 -0
- package/.agents/harness/opencode/plugins/saga-sessionstart.js +77 -0
- package/.agents/harness/opencode/plugins/syn-cd-check.js +61 -0
- package/.agents/harness/opencode/plugins/syn-pretool-check.js +68 -0
- package/.agents/harness/opencode/plugins/syn-turnend-guard.js +105 -0
- package/.agents/harness/opencode/plugins/syn-watch-arm.js +521 -0
- package/.agents/memory/README.md +46 -0
- package/.agents/memory/kaia.engram +0 -0
- package/.agents/memory/kaia.engram-shm +0 -0
- package/.agents/memory/kaia.engram-wal +0 -0
- package/.agents/memory/runes_audit.md +5 -0
- package/.agents/memory/well/episodes.jsonl +367 -0
- package/.agents/memory/well/workspace.jsonl +27 -0
- package/.agents/migrations/0001-hodd-layout.sh +38 -0
- package/.agents/migrations/0002-a2a-mcp.sh +27 -0
- package/.agents/migrations/0003-private-data-separation.sh +111 -0
- package/.agents/migrations/0004-hoard-and-realms.sh +104 -0
- package/.agents/sandbox/Dockerfile.utgard +19 -0
- package/.agents/sandbox/README.md +14 -0
- package/.agents/sandbox/utgard.config.json +14 -0
- package/.agents/skills/README.md +59 -0
- package/.agents/skills/bragi-marketing/README.md +3 -0
- package/.agents/skills/bragi-marketing/SKILL.md +82 -0
- package/.agents/skills/eindri-homes/README.md +3 -0
- package/.agents/skills/eindri-homes/SKILL.md +291 -0
- package/.agents/skills/eindri-homes/assets/control-plane.md +122 -0
- package/.agents/skills/eindri-homes/assets/dispatch.md +80 -0
- package/.agents/skills/frigg-consent/README.md +3 -0
- package/.agents/skills/frigg-consent/SKILL.md +56 -0
- package/.agents/skills/galdr-ymirsystem/README.md +3 -0
- package/.agents/skills/galdr-ymirsystem/SKILL.md +158 -0
- package/.agents/skills/galdr-ymirsystem/assets/README.md +74 -0
- package/.agents/skills/galdr-ymirsystem/assets/brokk-distro-runtime.md +390 -0
- package/.agents/skills/galdr-ymirsystem/assets/build-method.md +113 -0
- package/.agents/skills/galdr-ymirsystem/assets/build-tool-categories.md +178 -0
- package/.agents/skills/galdr-ymirsystem/assets/eindri-orchestration.md +518 -0
- package/.agents/skills/galdr-ymirsystem/assets/eindri-profiles.md +136 -0
- package/.agents/skills/galdr-ymirsystem/assets/harness-integration/README.md +673 -0
- package/.agents/skills/galdr-ymirsystem/assets/harness-integration/claude-code.md +197 -0
- package/.agents/skills/galdr-ymirsystem/assets/harness-integration/codex.md +191 -0
- package/.agents/skills/galdr-ymirsystem/assets/harness-integration/cursor.md +193 -0
- package/.agents/skills/galdr-ymirsystem/assets/harness-integration/opencode.md +262 -0
- package/.agents/skills/galdr-ymirsystem/assets/harness-integration/pi.md +295 -0
- package/.agents/skills/galdr-ymirsystem/assets/hlidskjalf-ui.md +496 -0
- package/.agents/skills/galdr-ymirsystem/assets/installation.md +571 -0
- package/.agents/skills/galdr-ymirsystem/assets/local-models/HOST-DATA.example.md +72 -0
- package/.agents/skills/galdr-ymirsystem/assets/local-models.md +274 -0
- package/.agents/skills/galdr-ymirsystem/assets/memory-well.md +161 -0
- package/.agents/skills/galdr-ymirsystem/assets/nornir-jobs.md +377 -0
- package/.agents/skills/galdr-ymirsystem/assets/norse-naming.md +325 -0
- package/.agents/skills/galdr-ymirsystem/assets/odrerir-hall.md +129 -0
- package/.agents/skills/galdr-ymirsystem/assets/pi-boot/einherjar-spawn.schema.json +81 -0
- package/.agents/skills/galdr-ymirsystem/assets/pi-boot/herdr-profile.toml +75 -0
- package/.agents/skills/galdr-ymirsystem/assets/pi-boot/pi-profile.yml +96 -0
- package/.agents/skills/galdr-ymirsystem/assets/pi-boot/supervision-tree.yml +163 -0
- package/.agents/skills/galdr-ymirsystem/assets/pi-boot-guide.md +58 -0
- package/.agents/skills/galdr-ymirsystem/assets/porting-upstream-to-norse.md +301 -0
- package/.agents/skills/galdr-ymirsystem/assets/principles.md +385 -0
- package/.agents/skills/galdr-ymirsystem/assets/reference-adoption.md +99 -0
- package/.agents/skills/galdr-ymirsystem/assets/registry.md +379 -0
- package/.agents/skills/galdr-ymirsystem/assets/runtime-compliance.md +463 -0
- package/.agents/skills/galdr-ymirsystem/assets/runtime-components.md +553 -0
- package/.agents/skills/galdr-ymirsystem/assets/smidja.md +175 -0
- package/.agents/skills/galdr-ymirsystem/schemas/toon-schemas.md +190 -0
- package/.agents/skills/galdr-ymirsystem/scripts/README.md +45 -0
- package/.agents/skills/galdr-ymirsystem/scripts/bench-one.sh +248 -0
- package/.agents/skills/galdr-ymirsystem/scripts/compliance-check.sh +360 -0
- package/.agents/skills/galdr-ymirsystem/scripts/gpu-sample.sh +43 -0
- package/.agents/skills/galdr-ymirsystem/scripts/stop-all.sh +34 -0
- package/.agents/skills/galdr-ymirsystem/scripts/toon-check.py +188 -0
- package/.agents/skills/gjallarhorn-relay/README.md +3 -0
- package/.agents/skills/gjallarhorn-relay/SKILL.md +283 -0
- package/.agents/skills/groa-update/SKILL.md +70 -0
- package/.agents/skills/hamr-adapters/README.md +3 -0
- package/.agents/skills/hamr-adapters/SKILL.md +123 -0
- package/.agents/skills/hamr-adapters/references/common/control-and-recovery.md +37 -0
- package/.agents/skills/hamr-adapters/references/common/dispatch.md +32 -0
- package/.agents/skills/hamr-adapters/references/common/model-and-effort.md +42 -0
- package/.agents/skills/hamr-adapters/references/common/primary-hooks.md +40 -0
- package/.agents/skills/hamr-adapters/references/harness/claude.md +55 -0
- package/.agents/skills/hamr-adapters/references/harness/codex.md +43 -0
- package/.agents/skills/hamr-adapters/references/harness/cursor.md +75 -0
- package/.agents/skills/hamr-adapters/references/harness/grok.md +69 -0
- package/.agents/skills/hamr-adapters/references/harness/kimi.md +51 -0
- package/.agents/skills/hamr-adapters/references/harness/muse.md +70 -0
- package/.agents/skills/hamr-adapters/references/harness/opencode.md +42 -0
- package/.agents/skills/hamr-adapters/references/harness/pi.md +56 -0
- package/.agents/skills/herdr-panes/README.md +3 -0
- package/.agents/skills/herdr-panes/SKILL.md +92 -0
- package/.agents/skills/herdr-panes/assets/herdr-backend.md +346 -0
- package/.agents/skills/herdr-panes/assets/tmux-backend.md +115 -0
- package/.agents/skills/hnoss-design/DESIGN.md +19 -0
- package/.agents/skills/hnoss-design/README.md +3 -0
- package/.agents/skills/hnoss-design/SKILL.md +83 -0
- package/.agents/skills/hvild-afk/README.md +3 -0
- package/.agents/skills/hvild-afk/SKILL.md +241 -0
- package/.agents/skills/jord-projects/README.md +3 -0
- package/.agents/skills/jord-projects/SKILL.md +92 -0
- package/.agents/skills/lifecycle/SKILL.md +56 -0
- package/.agents/skills/lifecycle/smoke_test.sh +52 -0
- package/.agents/skills/lifecycle/start.sh +21 -0
- package/.agents/skills/lifecycle/status.sh +35 -0
- package/.agents/skills/lifecycle/stop.sh +16 -0
- package/.agents/skills/muninn-stow/README.md +3 -0
- package/.agents/skills/muninn-stow/SKILL.md +309 -0
- package/.agents/skills/no-mistakes/SKILL.md +379 -0
- package/.agents/skills/nornir-schedule/README.md +3 -0
- package/.agents/skills/nornir-schedule/SKILL.md +25 -0
- package/.agents/skills/nornir-schedule/assets/events.md +137 -0
- package/.agents/skills/nornir-schedule/assets/quota.md +131 -0
- package/.agents/skills/nsr-compliance/README.md +3 -0
- package/.agents/skills/nsr-compliance/SKILL.md +29 -0
- package/.agents/skills/nsr-compliance/assets/nsr/README.md +40 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/RULES/README.md +15 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/RULES/git-workflow.md +18 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/RULES/security.md +22 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/RULES/tech-stack.md +8 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/SKILL.md +71 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/config/core_four.yaml +16 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/gates/check_danger.sh +102 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/gates/check_wiring.sh +69 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/gates/validate_code.sh +29 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/gates/verify_docs.py +66 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/generate.sh +109 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/harness/envelopes/result_envelope.json +14 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/harness/envelopes/task_envelope.json +20 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/harness/runner.py +303 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/telemetry/logger.py +112 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/templates/compliance/README.md +18 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/templates/compliance/config/README.md +10 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/templates/compliance/gates/README.md +12 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/templates/compliance/gates/check_env.sh +23 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/templates/compliance/gates/check_paths.sh +20 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/templates/compliance/gates/check_platform.sh +23 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/templates/compliance/harness/README.md +11 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/templates/compliance/harness/envelopes/README.md +10 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/templates/compliance/installer/README.md +10 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/templates/compliance/installer/install.sh +125 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/templates/compliance/telemetry/README.md +11 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/README.md +20 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/features/README.md +36 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/features/SKILL-template.md +14 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/git_ops/README.md +25 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/git_ops/SKILL.md +13 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/git_ops/create_branch.sh +6 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/git_ops/safe_commit.sh +9 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/git_ops/sync_upstream.sh +6 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/lifecycle/README.md +27 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/lifecycle/SKILL.md +15 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/lifecycle/smoke_test.sh +12 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/lifecycle/start.sh +19 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/lifecycle/status.sh +12 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/lifecycle/stop.sh +15 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/README.md +18 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/config/README.md +10 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/config/core_four.yaml +13 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/gates/README.md +12 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/gates/check_danger.sh +102 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/gates/check_env.sh +23 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/gates/check_paths.sh +20 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/gates/check_platform.sh +23 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/gates/check_wiring.sh +69 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/harness/README.md +11 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/harness/envelopes/README.md +10 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/harness/envelopes/result_envelope.json +14 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/harness/envelopes/task_envelope.json +20 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/harness/runner.py +303 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/installer/README.md +10 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/installer/install.sh +125 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/telemetry/README.md +11 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/telemetry/logger.py +112 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/BEST_PRACTICES/README.md +14 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/BEST_PRACTICES/api.md +17 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/BEST_PRACTICES/compliance-wiring.md +155 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/BEST_PRACTICES/frontend.md +15 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/CI_CD/README.md +11 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/CI_CD/deployment/README.md +23 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/CI_CD/deployment/clients/README.md +16 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/CI_CD/deployment/clients/template-README.md +11 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/CI_CD/deployment/deploy.sh +49 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/CI_CD/deployment/envs/README.md +12 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/CI_CD/deployment/envs/development.env.example +17 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/CI_CD/deployment/envs/production.env.example +17 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/CI_CD/deployment/envs/staging.env.example +17 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/CI_CD/deployment/tenants/README.md +11 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/CI_CD/deployment/tenants/template.env.example +8 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/DEVELOPER_SETUP/README.md +26 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/DEVELOPER_SETUP/agent.md +7 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/DEVELOPER_SETUP/developers/dev-1.md +20 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/DEVELOPER_SETUP/linux.md +9 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/DEVELOPER_SETUP/macos.md +10 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/DEVELOPER_SETUP/shared.md +19 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/DEVELOPER_SETUP/windows.md +14 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/HOSTING/README.md +25 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/HOSTING/_template.md +41 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/HOSTING/hosting-1.md +41 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/README.md +21 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/RUNBOOK/README.md +15 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/RUNBOOK/deployment.md +19 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/RUNBOOK/disaster-recovery.md +15 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/RUNBOOK/ops-tasks.md +14 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/features/README.md +16 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/project-skills.md +146 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/research/README.md +11 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/research/software-compliance-research.md +269 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/working-with-agents.md +260 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/root/AGENTS.md +38 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/root/ARCHITECTURE.md +36 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/root/BEST_PRACTICES.md +27 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/root/CI_CD.md +23 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/root/FEATURES.md +15 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/root/README.md +22 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/root/RULES.md +37 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/root/STRUCTURE.md +45 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/root/TECH_STACK.md +28 -0
- package/.agents/skills/nsr-compliance/assets/nsr/scaffold-spec.md +63 -0
- package/.agents/skills/nsr-compliance/assets/nsr/scripts/scaffold.sh +103 -0
- package/.agents/skills/nsr-compliance/assets/nsr/scripts/validate.sh +52 -0
- package/.agents/skills/nsr-compliance/assets/nsrcompliance/SKILL.md +71 -0
- package/.agents/skills/nsr-compliance/assets/nsrcompliance/config/core_four.yaml +16 -0
- package/.agents/skills/nsr-compliance/assets/nsrcompliance/gates/check_danger.sh +102 -0
- package/.agents/skills/nsr-compliance/assets/nsrcompliance/gates/check_wiring.sh +69 -0
- package/.agents/skills/nsr-compliance/assets/nsrcompliance/gates/validate_code.sh +29 -0
- package/.agents/skills/nsr-compliance/assets/nsrcompliance/gates/verify_docs.py +66 -0
- package/.agents/skills/nsr-compliance/assets/nsrcompliance/generate.sh +109 -0
- package/.agents/skills/nsr-compliance/assets/nsrcompliance/harness/envelopes/result_envelope.json +14 -0
- package/.agents/skills/nsr-compliance/assets/nsrcompliance/harness/envelopes/task_envelope.json +20 -0
- package/.agents/skills/nsr-compliance/assets/nsrcompliance/harness/runner.py +303 -0
- package/.agents/skills/nsr-compliance/assets/nsrcompliance/telemetry/logger.py +112 -0
- package/.agents/skills/nsr-compliance/assets/nsrcompliance/templates/compliance/README.md +18 -0
- package/.agents/skills/nsr-compliance/assets/nsrcompliance/templates/compliance/config/README.md +10 -0
- package/.agents/skills/nsr-compliance/assets/nsrcompliance/templates/compliance/gates/README.md +12 -0
- package/.agents/skills/nsr-compliance/assets/nsrcompliance/templates/compliance/gates/check_env.sh +23 -0
- package/.agents/skills/nsr-compliance/assets/nsrcompliance/templates/compliance/gates/check_paths.sh +20 -0
- package/.agents/skills/nsr-compliance/assets/nsrcompliance/templates/compliance/gates/check_platform.sh +23 -0
- package/.agents/skills/nsr-compliance/assets/nsrcompliance/templates/compliance/harness/README.md +11 -0
- package/.agents/skills/nsr-compliance/assets/nsrcompliance/templates/compliance/harness/envelopes/README.md +10 -0
- package/.agents/skills/nsr-compliance/assets/nsrcompliance/templates/compliance/installer/README.md +10 -0
- package/.agents/skills/nsr-compliance/assets/nsrcompliance/templates/compliance/installer/install.sh +125 -0
- package/.agents/skills/nsr-compliance/assets/nsrcompliance/templates/compliance/telemetry/README.md +11 -0
- package/.agents/skills/pr-ops/README.md +3 -0
- package/.agents/skills/pr-ops/SKILL.md +109 -0
- package/.agents/skills/ratatoskr-a2a/README.md +3 -0
- package/.agents/skills/ratatoskr-a2a/SKILL.md +148 -0
- package/.agents/skills/rules-check-drift/SKILL.md +81 -0
- package/.agents/skills/saga-bearings/README.md +3 -0
- package/.agents/skills/saga-bearings/SKILL.md +26 -0
- package/.agents/skills/saga-bearings/assets/bearings.md +161 -0
- package/.agents/skills/saga-bearings/assets/board-template.html +731 -0
- package/.agents/skills/saga-bearings/assets/recap.md +56 -0
- package/.agents/skills/syn-recovery/README.md +3 -0
- package/.agents/skills/syn-recovery/SKILL.md +54 -0
- package/.agents/skills/tyr-check/README.md +3 -0
- package/.agents/skills/tyr-check/SKILL.md +218 -0
- package/.agents/skills/urdh-hold/README.md +3 -0
- package/.agents/skills/urdh-hold/SKILL.md +25 -0
- package/.agents/skills/urdh-hold/assets/decisions.md +15 -0
- package/.agents/skills/urdh-hold/assets/hold.md +54 -0
- package/.agents/skills/vor-diagnostics/README.md +3 -0
- package/.agents/skills/vor-diagnostics/SKILL.md +68 -0
- package/.agents/skills/ymir-host/README.md +3 -0
- package/.agents/skills/ymir-host/SKILL.md +34 -0
- package/.agents/skills/ymir-host/assets/desktop.md +36 -0
- package/.agents/skills/ymir-host/assets/omarchy.md +297 -0
- package/.agents/skills/ymir-host/assets/runbooks.md +15 -0
- package/.agents/skills/ymir-host/assets/thjazi.md +220 -0
- package/.agents/skills/ymir-host/assets/update.md +64 -0
- package/.agents/tests/assets/board-render-harness.mjs +117 -0
- package/.agents/tests/cmux-test-safety.sh +51 -0
- package/.agents/tests/fixtures.sh +291 -0
- package/.agents/tests/fm-afk-inject-e2e.test.sh +428 -0
- package/.agents/tests/fm-afk-inject-herdr-e2e.test.sh +536 -0
- package/.agents/tests/fm-afk-launch.test.sh +961 -0
- package/.agents/tests/fm-afk-pi-herdr-return-e2e.test.sh +297 -0
- package/.agents/tests/fm-afk-return.test.sh +283 -0
- package/.agents/tests/fm-arm-pretool-check.test.sh +482 -0
- package/.agents/tests/fm-ask-user-authority.test.sh +42 -0
- package/.agents/tests/fm-backend-autodetect-smoke.test.sh +176 -0
- package/.agents/tests/fm-backend-cmux-smoke.test.sh +188 -0
- package/.agents/tests/fm-backend-cmux.test.sh +1166 -0
- package/.agents/tests/fm-backend-herdr-eventwait-smoke.test.sh +136 -0
- package/.agents/tests/fm-backend-herdr-eventwait.test.py +106 -0
- package/.agents/tests/fm-backend-herdr-focus-flash-e2e.test.sh +409 -0
- package/.agents/tests/fm-backend-herdr-launcher-workspace-e2e.test.sh +437 -0
- package/.agents/tests/fm-backend-herdr-presentation-e2e.test.sh +1424 -0
- package/.agents/tests/fm-backend-herdr-prune-safety-e2e.test.sh +180 -0
- package/.agents/tests/fm-backend-herdr-respawn-idem-e2e.test.sh +182 -0
- package/.agents/tests/fm-backend-herdr-smoke.test.sh +355 -0
- package/.agents/tests/fm-backend-herdr-workspace-per-home-e2e.test.sh +251 -0
- package/.agents/tests/fm-backend-herdr.test.sh +4606 -0
- package/.agents/tests/fm-backend-orca.test.sh +1357 -0
- package/.agents/tests/fm-backend-tmux-smoke.test.sh +173 -0
- package/.agents/tests/fm-backend-zellij-smoke.test.sh +205 -0
- package/.agents/tests/fm-backend-zellij.test.sh +1360 -0
- package/.agents/tests/fm-backend.test.sh +1142 -0
- package/.agents/tests/fm-backlog-atomicity.test.sh +2302 -0
- package/.agents/tests/fm-backlog-handoff.test.sh +1360 -0
- package/.agents/tests/fm-bearings-board-render.test.sh +135 -0
- package/.agents/tests/fm-bearings-board.test.sh +414 -0
- package/.agents/tests/fm-bearings-snapshot.test.sh +1990 -0
- package/.agents/tests/fm-bootstrap-network-parallel.test.sh +316 -0
- package/.agents/tests/fm-bootstrap.test.sh +1178 -0
- package/.agents/tests/fm-branch-supervision.test.sh +552 -0
- package/.agents/tests/fm-brief.test.sh +785 -0
- package/.agents/tests/fm-busy-adapter-wiring.test.sh +324 -0
- package/.agents/tests/fm-busy-state.test.sh +465 -0
- package/.agents/tests/fm-calm-pi-extension.test.sh +3979 -0
- package/.agents/tests/fm-captain-hold-lifecycle.test.sh +1187 -0
- package/.agents/tests/fm-cd-pretool-check.test.sh +400 -0
- package/.agents/tests/fm-check-unregister.test.sh +198 -0
- package/.agents/tests/fm-classify-corr-token.test.sh +543 -0
- package/.agents/tests/fm-classify-decision-key.test.sh +340 -0
- package/.agents/tests/fm-claude-stop-autoarm-live-e2e.test.sh +164 -0
- package/.agents/tests/fm-claude-stop-autoarm.test.sh +1190 -0
- package/.agents/tests/fm-cmux-claude-composer-live-e2e.test.sh +110 -0
- package/.agents/tests/fm-codex-continuity-live-e2e.test.sh +55 -0
- package/.agents/tests/fm-composer-ghost.test.sh +716 -0
- package/.agents/tests/fm-composer-lib.test.sh +666 -0
- package/.agents/tests/fm-composer-matrix-live-e2e.test.sh +220 -0
- package/.agents/tests/fm-control-herdr-smoke.test.sh +149 -0
- package/.agents/tests/fm-control-relaunch.test.sh +1530 -0
- package/.agents/tests/fm-control.test.sh +910 -0
- package/.agents/tests/fm-crew-state.test.sh +1610 -0
- package/.agents/tests/fm-cursor-harness.test.sh +404 -0
- package/.agents/tests/fm-cursor-primary-live-e2e.test.sh +217 -0
- package/.agents/tests/fm-cursor-primary.test.sh +708 -0
- package/.agents/tests/fm-daemon.test.sh +2740 -0
- package/.agents/tests/fm-documentation-audiences.test.sh +141 -0
- package/.agents/tests/fm-ensure-agents-md.test.sh +371 -0
- package/.agents/tests/fm-extension-binding.test.sh +2187 -0
- package/.agents/tests/fm-fleet-snapshot-view.test.sh +816 -0
- package/.agents/tests/fm-fleet-sync.test.sh +721 -0
- package/.agents/tests/fm-gate-refuse.test.sh +356 -0
- package/.agents/tests/fm-gitignore-config.test.sh +89 -0
- package/.agents/tests/fm-gotmp.test.sh +230 -0
- package/.agents/tests/fm-grok-continuity-live-e2e.test.sh +112 -0
- package/.agents/tests/fm-grok-harness.test.sh +117 -0
- package/.agents/tests/fm-grok-stop-live-e2e.test.sh +219 -0
- package/.agents/tests/fm-guard-stale-banner.test.sh +709 -0
- package/.agents/tests/fm-harness-adapter-instructions-live-e2e.test.sh +134 -0
- package/.agents/tests/fm-harness-adapter-references.test.sh +30 -0
- package/.agents/tests/fm-harness-liveness-drift-live-e2e.test.sh +151 -0
- package/.agents/tests/fm-herdr-lab.test.sh +243 -0
- package/.agents/tests/fm-herdr-session-cleanup-e2e.test.sh +146 -0
- package/.agents/tests/fm-herdr-session-cleanup.test.sh +329 -0
- package/.agents/tests/fm-herdr-submit-confirm-live-e2e.test.sh +125 -0
- package/.agents/tests/fm-herdr-version-floor-live-e2e.test.sh +144 -0
- package/.agents/tests/fm-home-summary-refresh.test.sh +971 -0
- package/.agents/tests/fm-inactive-reconcile.test.sh +485 -0
- package/.agents/tests/fm-kimi-harness.test.sh +687 -0
- package/.agents/tests/fm-lint-workflows.test.sh +532 -0
- package/.agents/tests/fm-lint.test.sh +1023 -0
- package/.agents/tests/fm-muse-harness.test.sh +932 -0
- package/.agents/tests/fm-muse-signals-live-e2e.test.sh +205 -0
- package/.agents/tests/fm-no-mistakes-required.test.sh +72 -0
- package/.agents/tests/fm-on.test.sh +507 -0
- package/.agents/tests/fm-opencode-primary-live-e2e.test.sh +357 -0
- package/.agents/tests/fm-operational-input.test.sh +160 -0
- package/.agents/tests/fm-peek-remote.test.sh +110 -0
- package/.agents/tests/fm-pending-reply.test.sh +1288 -0
- package/.agents/tests/fm-pi-branch-extension.test.sh +3182 -0
- package/.agents/tests/fm-pi-branch-live-e2e.test.sh +527 -0
- package/.agents/tests/fm-pi-primary-live-e2e.test.sh +345 -0
- package/.agents/tests/fm-pi-primary-types.test.sh +68 -0
- package/.agents/tests/fm-pi-watch-extension.test.sh +2843 -0
- package/.agents/tests/fm-pr-check-security.test.sh +2140 -0
- package/.agents/tests/fm-pr-merge.test.sh +2135 -0
- package/.agents/tests/fm-procevent-quota.test.sh +225 -0
- package/.agents/tests/fm-procevent-when.test.sh +406 -0
- package/.agents/tests/fm-procevent.test.sh +1745 -0
- package/.agents/tests/fm-project-origin.test.sh +110 -0
- package/.agents/tests/fm-public-followup.test.sh +2272 -0
- package/.agents/tests/fm-quota-array-dispatch-live-e2e.test.sh +526 -0
- package/.agents/tests/fm-quota-choose.test.sh +626 -0
- package/.agents/tests/fm-remote-backlog-handoff.test.sh +453 -0
- package/.agents/tests/fm-remote-doctor.test.sh +625 -0
- package/.agents/tests/fm-remote-entrypoint.test.sh +59 -0
- package/.agents/tests/fm-remote-job-orphan-reap.test.sh +205 -0
- package/.agents/tests/fm-remote-job.test.sh +768 -0
- package/.agents/tests/fm-remote-reply.test.sh +520 -0
- package/.agents/tests/fm-remote-secondmate-lifecycle-e2e.test.sh +1256 -0
- package/.agents/tests/fm-remote-secondmate-parent-binding.test.sh +326 -0
- package/.agents/tests/fm-remote-secondmate-trace-context.test.sh +310 -0
- package/.agents/tests/fm-remote-transport-lanes.test.sh +431 -0
- package/.agents/tests/fm-review-diff.test.sh +176 -0
- package/.agents/tests/fm-secondmate-harness.test.sh +2600 -0
- package/.agents/tests/fm-secondmate-lifecycle-e2e.test.sh +259 -0
- package/.agents/tests/fm-secondmate-liveness.test.sh +559 -0
- package/.agents/tests/fm-secondmate-reconcile.test.sh +732 -0
- package/.agents/tests/fm-secondmate-safety.test.sh +2972 -0
- package/.agents/tests/fm-secondmate-sync.test.sh +913 -0
- package/.agents/tests/fm-send-inbox-doorbell-live-e2e.test.sh +206 -0
- package/.agents/tests/fm-send-inbox.test.sh +352 -0
- package/.agents/tests/fm-send-popup-settle.test.sh +167 -0
- package/.agents/tests/fm-send-remote-delivery.test.sh +796 -0
- package/.agents/tests/fm-send-resolve-key.test.sh +551 -0
- package/.agents/tests/fm-send-secondmate-marker-herdr-e2e.test.sh +187 -0
- package/.agents/tests/fm-send-secondmate-marker.test.sh +278 -0
- package/.agents/tests/fm-send-settle.test.sh +147 -0
- package/.agents/tests/fm-send-strict.test.sh +241 -0
- package/.agents/tests/fm-session-lock-ancestry.test.sh +365 -0
- package/.agents/tests/fm-session-start.test.sh +2515 -0
- package/.agents/tests/fm-sessionstart-hook-live-e2e.test.sh +631 -0
- package/.agents/tests/fm-sessionstart-instruction-refresh-live-e2e.test.sh +230 -0
- package/.agents/tests/fm-sessionstart-nudge.test.sh +1039 -0
- package/.agents/tests/fm-shared-captain-inheritance.test.sh +404 -0
- package/.agents/tests/fm-spawn-batch.test.sh +148 -0
- package/.agents/tests/fm-spawn-dispatch-profile.test.sh +827 -0
- package/.agents/tests/fm-spawn-pool-base-freshen.test.sh +440 -0
- package/.agents/tests/fm-spawn-worktree-settle.test.sh +147 -0
- package/.agents/tests/fm-startup-memory-budget.test.sh +333 -0
- package/.agents/tests/fm-startup-network.test.sh +712 -0
- package/.agents/tests/fm-stow-cascade.test.sh +370 -0
- package/.agents/tests/fm-subagent-pretool-check.test.sh +291 -0
- package/.agents/tests/fm-supervision-events.test.sh +156 -0
- package/.agents/tests/fm-supervision-instructions.test.sh +189 -0
- package/.agents/tests/fm-tangle-guard.test.sh +277 -0
- package/.agents/tests/fm-task-delivery.test.sh +420 -0
- package/.agents/tests/fm-task-inbox.test.sh +513 -0
- package/.agents/tests/fm-teardown-endpoint-safety.test.sh +374 -0
- package/.agents/tests/fm-teardown.test.sh +2665 -0
- package/.agents/tests/fm-test-fixture-cleanup.test.sh +172 -0
- package/.agents/tests/fm-test-fixtures.test.sh +132 -0
- package/.agents/tests/fm-test-isolation-proof.test.sh +278 -0
- package/.agents/tests/fm-test-run.test.sh +1196 -0
- package/.agents/tests/fm-tmux-agent-liveness.test.sh +354 -0
- package/.agents/tests/fm-tmux-submit-busy.test.sh +340 -0
- package/.agents/tests/fm-tool-update-check.test.sh +1044 -0
- package/.agents/tests/fm-trace-context-lib.test.sh +253 -0
- package/.agents/tests/fm-trace-context-spawn.test.sh +581 -0
- package/.agents/tests/fm-transition-lib.test.sh +54 -0
- package/.agents/tests/fm-turnend-guard.test.sh +1822 -0
- package/.agents/tests/fm-update.test.sh +304 -0
- package/.agents/tests/fm-vendor-auth-probe.test.sh +395 -0
- package/.agents/tests/fm-voice-relay.test.sh +4741 -0
- package/.agents/tests/fm-wake-daemon-lifecycle-e2e.test.sh +171 -0
- package/.agents/tests/fm-wake-drain-open-decisions-cursor.test.sh +356 -0
- package/.agents/tests/fm-wake-drain-open-decisions.test.sh +226 -0
- package/.agents/tests/fm-wake-drain-unread-status.test.sh +379 -0
- package/.agents/tests/fm-wake-queue.test.sh +1224 -0
- package/.agents/tests/fm-watch-arm.test.sh +821 -0
- package/.agents/tests/fm-watch-checkpoint.test.sh +87 -0
- package/.agents/tests/fm-watch-recovery-loop.test.sh +223 -0
- package/.agents/tests/fm-watch-triage.test.sh +3889 -0
- package/.agents/tests/fm-watcher-lock.test.sh +1133 -0
- package/.agents/tests/fm-x-mode.test.sh +3042 -0
- package/.agents/tests/gleipnir-guard-pretool-check.test.sh +51 -0
- package/.agents/tests/gleipnir-machine-lock.test.sh +130 -0
- package/.agents/tests/herdr-test-safety.sh +42 -0
- package/.agents/tests/lib.sh +315 -0
- package/.agents/tests/remote-herdr-fixture.sh +125 -0
- package/.agents/tests/secondmate-helpers.sh +193 -0
- package/.agents/tests/smoke.test.sh +93 -0
- package/.agents/tests/wake-helpers.sh +338 -0
- package/.agents/tests/zellij-test-safety.sh +60 -0
- package/.agents/tools/README.md +10 -0
- package/.agents/tools/bin/tasks-cli +267 -0
- package/.agents/tools/tasks-cli.ts +216 -0
- package/AGENTS.md +454 -0
- package/CHANGELOG.md +881 -0
- package/LICENSE +1 -1
- package/NOTICE +63 -0
- package/README.md +113 -39
- package/RULES/01-domains.md +51 -0
- package/RULES/02-agents.md +78 -0
- package/RULES/03-houses.md +61 -0
- package/RULES/04-hoard.md +65 -0
- package/RULES/05-platforms.md +67 -0
- package/RULES/06-append-only.md +58 -0
- package/RULES/07-config.md +45 -0
- package/RULES/08-delivery-gate.md +75 -0
- package/RULES/README.md +20 -0
- package/bin/README.md +107 -0
- package/bin/a2a-mcp.sh +98 -0
- package/bin/a2a-serve.py +103 -0
- package/bin/a2a-serve.sh +8 -0
- package/bin/a2a-talk.sh +50 -0
- package/bin/a2abridge-ensure.sh +108 -0
- package/bin/agent-run.sh +43 -0
- package/bin/agents-config.sh +233 -0
- package/bin/apodex-smoke-test.sh +92 -0
- package/bin/bifrost-bridge.sh +112 -0
- package/bin/bifrost-ingress.sh +77 -0
- package/bin/bootstrap-macos.sh +107 -0
- package/bin/bootstrap-windows.ps1 +112 -0
- package/bin/branch-guard.sh +64 -0
- package/bin/brokk +73 -0
- package/bin/brokk-classify-lib.sh +1770 -0
- package/bin/brokk-lease-lib.sh +222 -0
- package/bin/brokk-lease.sh +193 -0
- package/bin/brokk-lint.sh +65 -0
- package/bin/brokk-send.sh +38 -0
- package/bin/brokk-update.sh +9 -0
- package/bin/brokk-wake-grant.sh +127 -0
- package/bin/brokk-wake-lib.sh +1849 -0
- package/bin/changelog-guard.sh +165 -0
- package/bin/crash-sense.sh +160 -0
- package/bin/desktop-place.sh +224 -0
- package/bin/docs-guard.sh +39 -0
- package/bin/editor-place.sh +207 -0
- package/bin/eindri-acclaim.sh +34 -0
- package/bin/eindri-control.sh +28 -0
- package/bin/eindri-role.sh +83 -0
- package/bin/eindri-seen.sh +41 -0
- package/bin/eindri-send.sh +15 -0
- package/bin/eindri-start.sh +85 -0
- package/bin/eindri-watch.sh +57 -0
- package/bin/einherjar-spawn.sh +576 -0
- package/bin/eir-doctor.sh +112 -0
- package/bin/erindi-brief.sh +271 -0
- package/bin/fleet-apply.sh +68 -0
- package/bin/github-deploy.sh +81 -0
- package/bin/gjallarhorn-expose.sh +102 -0
- package/bin/gjallarhorn-notify.sh +58 -0
- package/bin/gjallarhorn-purge.sh +54 -0
- package/bin/gjallarhorn-tunnel.sh +72 -0
- package/bin/gleipnir-lock-lib.sh +284 -0
- package/bin/groa-update.sh +122 -0
- package/bin/hall-snapshot.sh +92 -0
- package/bin/hamr-harness.sh +246 -0
- package/bin/heimdall-ssh-keys.sh +374 -0
- package/bin/herdr-agents.py +44 -0
- package/bin/herdr-ensure.sh +104 -0
- package/bin/herdr-run.sh +431 -0
- package/bin/hermes-ensure.sh +88 -0
- package/bin/hoard-lib.sh +25 -0
- package/bin/hodd.sh +78 -0
- package/bin/host-sense.sh +199 -0
- package/bin/huginn-research-worker.sh +108 -0
- package/bin/local-model-lock.sh +51 -0
- package/bin/mimir-bridge.py +226 -0
- package/bin/mimir-bridge.sh +87 -0
- package/bin/mimir-ingest.sh +131 -0
- package/bin/mimir.sh +113 -0
- package/bin/mjollnir-webhook.sh +59 -0
- package/bin/mjollnir.sh +85 -0
- package/bin/model-hardware.sh +82 -0
- package/bin/model-resolve.sh +147 -0
- package/bin/models-detect.sh +178 -0
- package/bin/nornir-cron-start.sh +162 -0
- package/bin/nornir-job-daily-briefing.sh +191 -0
- package/bin/nornir-job-git-sync.sh +152 -0
- package/bin/nornir-job-memory-housekeeping.sh +114 -0
- package/bin/nornir-job-observer.sh +166 -0
- package/bin/nornir-job-skillopt-sleep.sh +76 -0
- package/bin/omarchy-hook-install.sh +60 -0
- package/bin/omarchy-install.sh +148 -0
- package/bin/omarchy-plugins.sh +174 -0
- package/bin/omarchy-sense.sh +149 -0
- package/bin/perm-guard.sh +30 -0
- package/bin/pi-agent.sh +68 -0
- package/bin/pi-ensure.sh +77 -0
- package/bin/pi-local.sh +37 -0
- package/bin/pi-seat.sh +63 -0
- package/bin/prereq-ensure.sh +166 -0
- package/bin/private-guard.sh +38 -0
- package/bin/project-git.sh +55 -0
- package/bin/public-guard.sh +70 -0
- package/bin/ratatoskr.sh +49 -0
- package/bin/realm-lib.sh +32 -0
- package/bin/repo-scrub.sh +62 -0
- package/bin/rodd-operational-input.sh +250 -0
- package/bin/runes-append.sh +198 -0
- package/bin/saga-session-start.sh +172 -0
- package/bin/saga-sessionstart-run.sh +79 -0
- package/bin/saga-wake-drain.sh +41 -0
- package/bin/secret-guard.sh +63 -0
- package/bin/sessrumnir-ensure.sh +100 -0
- package/bin/sessrumnir-sync.sh +237 -0
- package/bin/sessrumnir.sh +91 -0
- package/bin/skillopt-setup.sh +76 -0
- package/bin/skuld-branch-outcome.sh +203 -0
- package/bin/skuld-branch-prompt.sh +101 -0
- package/bin/smidja-bootstrap.sh +90 -0
- package/bin/smidja-observe.sh +86 -0
- package/bin/syn-arm-pretool-check.sh +57 -0
- package/bin/syn-asset-pretool-check.sh +76 -0
- package/bin/syn-cd-pretool-check.sh +28 -0
- package/bin/syn-guard-pretool-check.sh +83 -0
- package/bin/syn-turnend-guard.sh +36 -0
- package/bin/syn-watch-arm.sh +99 -0
- package/bin/tailscale-sync.sh +190 -0
- package/bin/telegram-bot.sh +69 -0
- package/bin/toolchain.sh +74 -0
- package/bin/utgard.sh +111 -0
- package/bin/valhalla.sh +115 -0
- package/bin/valknut-load.sh +260 -0
- package/bin/vor-crew-state.sh +124 -0
- package/bin/wedge-notify.sh +47 -0
- package/bin/workspace-provision.sh +80 -0
- package/bin/workspace-rag.sh +150 -0
- package/bin/wyrd-db.sh +47 -0
- package/bin/yggdrasil.sh +152 -0
- package/bin/ymir-edit.sh +145 -0
- package/bin/ymir-install.sh +641 -0
- package/bin/ymir-invite.sh +93 -0
- package/bin/ymir-isolation.sh +88 -0
- package/bin/ymir-migrate.sh +61 -0
- package/bin/ymir-platform.sh +348 -0
- package/bin/ymir-say.sh +97 -0
- package/bin/ymir-setup-auth.sh +108 -0
- package/bin/ymir-validate.sh +173 -0
- package/bin/ymir.js +74 -16
- package/docs/Architecture.md +339 -0
- package/docs/design.md +351 -0
- package/docs/installations/README.md +63 -0
- package/docs/installations/ymir-home.md +93 -0
- package/docs/integration.md +105 -0
- package/docs/lore.md +1056 -0
- package/docs/research/agent-harnesses.md +290 -0
- package/docs/runbooks/README.md +19 -0
- package/docs/runbooks/a2a.md +91 -0
- package/docs/runbooks/agent-permissions.md +99 -0
- package/docs/runbooks/agents.md +57 -0
- package/docs/runbooks/models.md +69 -0
- package/docs/runbooks/realm-onboarding.md +60 -0
- package/docs/runbooks/secrets-and-hoard.md +60 -0
- package/docs/runbooks/updates-and-migrations.md +110 -0
- package/docs/runbooks/worktrees-isolation.md +72 -0
- package/docs/session-start.md +105 -0
- package/docs/skillopt-integration.md +321 -0
- package/docs/sync-system.md +66 -0
- package/docs/workspaces.md +104 -0
- package/docs/ymir-rut.md +260 -0
- package/package.json +44 -14
- package/packaging/build.sh +74 -0
- package/packaging/macos/Ymir Installer.command +43 -0
- package/packaging/windows/ymir-setup.nsi +74 -0
- package/scripts/README.md +22 -0
- package/scripts/electron.sh +210 -0
- package/scripts/start.sh +167 -0
- package/scripts/stop.sh +113 -0
- 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
|
+
}
|