@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,3153 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Spawn a direct report: a crewmate in a treehouse or Orca worktree, or a
|
|
3
|
+
# secondmate in its isolated firstmate home.
|
|
4
|
+
# Usage: fm-spawn.sh <task-id> <project-dir> --mode <no-mistakes|direct-PR|local-only> --yolo <on|off> [--harness <name>|harness|launch-command] [--model <name>] [--effort <level>] [--backend <name>]
|
|
5
|
+
# fm-spawn.sh <task-id> <project-dir> --scout [--harness <name>|harness|launch-command] [--model <name>] [--effort <level>] [--backend <name>]
|
|
6
|
+
# fm-spawn.sh <task-id> [<firstmate-home>] [--harness <name>|harness|launch-command] [--model <name>] [--effort <level>] [--backend <name>] --secondmate
|
|
7
|
+
# --mode and --yolo are this task's delivery contract, REQUIRED for every ship
|
|
8
|
+
# spawn and refused on --scout and --secondmate spawns. Firstmate resolves both
|
|
9
|
+
# per task at intake (AGENTS.md section 7); data/projects.md holds the captain's
|
|
10
|
+
# standing posture as context, not as this task's answer, so a spawn never looks
|
|
11
|
+
# the mode up. A ship spawn additionally reads the brief's recorded
|
|
12
|
+
# "Delivery contract: mode=<mode>" line and REFUSES a mismatch, so the worker's
|
|
13
|
+
# instructions and the recorded task delivery cannot drift apart; a brief
|
|
14
|
+
# scaffolded before that line existed warns once and launches on the flag. When
|
|
15
|
+
# the explicit mode carries less rigor than the project's standing posture, a
|
|
16
|
+
# loud one-line deviation notice is printed and the spawn continues.
|
|
17
|
+
# no-mistakes-prod-only is a registry policy rather than a task mode and is
|
|
18
|
+
# refused as a flag value.
|
|
19
|
+
# fm-spawn.sh <task-id> --relaunch [--harness <name>] [--model <name>] [--effort <level>]
|
|
20
|
+
# --relaunch launches a replacement agent for an EXISTING task into that
|
|
21
|
+
# task's own recorded endpoint and worktree instead of creating either. It is
|
|
22
|
+
# the launch half of the control plane (bin/fm-control.sh relaunch), which
|
|
23
|
+
# owns the checkpoint, the progress note, stopping the previous agent, and the
|
|
24
|
+
# transaction; call fm-control rather than this flag directly unless you are
|
|
25
|
+
# deliberately re-launching an already-stopped task. Every identity axis -
|
|
26
|
+
# backend, kind, project or home, worktree, endpoint - comes from the task's
|
|
27
|
+
# validated state/<id>.meta, so --backend, --scout, --secondmate, a project
|
|
28
|
+
# positional, and batch pairs are all refused alongside it; only harness,
|
|
29
|
+
# model, and effort may change, which is what makes a harness switch one
|
|
30
|
+
# ordinary relaunch. It refuses unless the recorded endpoint is positively
|
|
31
|
+
# agent-free on a backend with a recovery-grade agent-state classifier (tmux
|
|
32
|
+
# or herdr), refuses unless the endpoint's shell is sitting in the recorded
|
|
33
|
+
# worktree, and clears the previous harness's per-task wiring before arming
|
|
34
|
+
# the new incarnation.
|
|
35
|
+
# --harness <name> is the explicit per-spawn harness/profile adapter. The old
|
|
36
|
+
# positional harness arg still works for back-compat.
|
|
37
|
+
# --model <name> and --effort <low|medium|high|xhigh|max> are concrete profile
|
|
38
|
+
# axes chosen by firstmate at intake. They are only threaded into harnesses whose
|
|
39
|
+
# installed CLIs were verified to support that axis; unsupported axes are omitted
|
|
40
|
+
# from that harness's launch rather than guessed.
|
|
41
|
+
# --backend <name> is the explicit runtime session-provider backend for this
|
|
42
|
+
# exact task only (docs/configuration.md "Runtime backend" owns when that flag
|
|
43
|
+
# is authorized). Without it, the script resolves FM_BACKEND, then
|
|
44
|
+
# config/backend, then runtime auto-detection from the runtime firstmate's
|
|
45
|
+
# environment: $TMUX, HERDR_ENV=1, or cmux runtime signals (via
|
|
46
|
+
# bin/fm-backend.sh's fm_backend_detect, with cmux fallback details in
|
|
47
|
+
# docs/cmux-backend.md),
|
|
48
|
+
# then tmux.
|
|
49
|
+
# Spawn-capable backends are the reference tmux adapter and experimental
|
|
50
|
+
# herdr, zellij, orca, and cmux. Orca owns both the task worktree and
|
|
51
|
+
# terminal, so ship/scout Orca spawns do not run treehouse get; cmux is a
|
|
52
|
+
# session provider only, exactly like herdr/zellij, so it does. An
|
|
53
|
+
# auto-detected herdr or cmux spawn prints a loud stderr notice;
|
|
54
|
+
# auto-detected tmux stays silent; zellij and orca are never auto-detected.
|
|
55
|
+
# codex-app is not a known backend yet; docs/codex-app-backend.md owns that
|
|
56
|
+
# blocked backend contract. Default tmux spawns do not write backend= to meta;
|
|
57
|
+
# absent backend= means tmux. cmux does not support --secondmate spawns yet.
|
|
58
|
+
# A backend spawn refusal (missing dependency, version gate, unauthenticated
|
|
59
|
+
# socket, or unsupported secondmate mode) is terminal for that selected backend;
|
|
60
|
+
# callers must surface it instead of silently retrying another backend.
|
|
61
|
+
# A herdr crewmate or scout is placed in the exact workspace of the firstmate
|
|
62
|
+
# or secondmate process launching it, resolved from that process's own herdr
|
|
63
|
+
# pane rather than from a workspace label (herdr enforces no label uniqueness,
|
|
64
|
+
# so a label cannot tell two "firstmate" workspaces apart). A claimed parent
|
|
65
|
+
# identity that is unreadable, contradictory, stale, or from another herdr
|
|
66
|
+
# session stops the spawn before any worker endpoint exists. A launcher
|
|
67
|
+
# outside herdr has no workspace to inherit and uses this home's own labeled
|
|
68
|
+
# workspace, which must then match exactly one. --secondmate is the deliberate
|
|
69
|
+
# exception: it stands up that secondmate home's own workspace.
|
|
70
|
+
# Herdr additionally uses a presentation-only layout by default when the
|
|
71
|
+
# selected client and running server meet the Herdr 0.8.0 floor. The local
|
|
72
|
+
# config/herdr-presentation-spaces file can say off to disable it or on to
|
|
73
|
+
# opt in below that floor; an empty file remains the historical opt-in form.
|
|
74
|
+
# A clean fresh task first writes state/<id>.herdr-presentation atomically,
|
|
75
|
+
# then creates a disposable
|
|
76
|
+
# workspace containing only the ordinary task pane. A successful clean create
|
|
77
|
+
# upgrades its attempt journal with exact home, session, workspace, tab, pane,
|
|
78
|
+
# parent, and label bindings. On a same-identity restart, that complete binding
|
|
79
|
+
# plus authoritative metadata may replace one exact agent-free husk in place.
|
|
80
|
+
# The journal, visible token, and labels alone are never endpoint or ownership
|
|
81
|
+
# authority, and every ambiguous recovery stays on the flat fallback after
|
|
82
|
+
# duplicate-agent risk is independently absent. Treehouse allocation and task
|
|
83
|
+
# metadata are unchanged.
|
|
84
|
+
# A clean projected create or exact resume makes one bounded attempt to hold
|
|
85
|
+
# the one session-scoped presentation-order lock (keyed by named session plus
|
|
86
|
+
# canonical socket, outside any home's state/) through launch handoff. Lock
|
|
87
|
+
# contention warns and falls back to the ordinary flat layout before any
|
|
88
|
+
# projection mutation. The exact response-derived new workspace is inserted
|
|
89
|
+
# immediately after its owning parent (firstmate or 2ndmate-<id>) contiguous
|
|
90
|
+
# child block. Ordering never authorizes lifecycle cleanup, and any
|
|
91
|
+
# unavailable, ambiguous, or failed move warns while the spawn continues.
|
|
92
|
+
# Every projected create, prune, and move captures and verifies the named
|
|
93
|
+
# session's exact active workspace and tab. A detected focus change restores
|
|
94
|
+
# only that exact tab id; an ambiguous pre-operation snapshot refuses the
|
|
95
|
+
# focus-sensitive presentation mutation.
|
|
96
|
+
# Every single-task invocation holds one task-id-scoped lock across backend
|
|
97
|
+
# creation through metadata publication, so concurrent same-id spawns serialize
|
|
98
|
+
# even when they select different backends. A fresh spawn first takes the
|
|
99
|
+
# per-home task-set lock and refuses rather than waits when forced teardown owns
|
|
100
|
+
# it; relaunch is exempt because the existing task's control lock covers it.
|
|
101
|
+
# With no harness arg, a crewmate/scout spawn resolves the CREW harness only when
|
|
102
|
+
# config/crew-dispatch.json is absent. When that file exists, crewmate/scout
|
|
103
|
+
# spawns require an explicit harness so firstmate cannot silently skip dispatch
|
|
104
|
+
# profile consultation. A --secondmate spawn is exempt and resolves the SECONDMATE
|
|
105
|
+
# harness (config/secondmate-harness -> config/crew-harness -> own), so the
|
|
106
|
+
# secondmate-vs-crewmate split is DURABLE across every respawn (recovery,
|
|
107
|
+
# /updatefirstmate, restart). A bare adapter name (claude|codex|opencode|pi|pi-signed|grok|kimi|cursor|muse)
|
|
108
|
+
# overrides it for this spawn (either kind). A non-flag string containing
|
|
109
|
+
# whitespace is treated as a RAW launch command - the escape hatch for verifying
|
|
110
|
+
# new adapters. For pi and pi-signed, fm-spawn resolves the selected executable
|
|
111
|
+
# name from PATH once, probes that concrete path with --help, and launches the
|
|
112
|
+
# same path. It adds --tui-mode regular only when that help advertises the flag;
|
|
113
|
+
# a failed or inconclusive probe omits it so older Pi versions remain launchable.
|
|
114
|
+
# A missing selected executable refuses before endpoint creation, and pi-signed
|
|
115
|
+
# never falls back to pi.
|
|
116
|
+
# config/secondmate-harness may also carry an optional model and effort as extra
|
|
117
|
+
# whitespace-separated tokens ("<harness> [<model>] [<effort>]"). For a
|
|
118
|
+
# --secondmate spawn, those tokens apply only when this spawn also resolves its
|
|
119
|
+
# harness from config/secondmate-harness. An explicit per-spawn --harness,
|
|
120
|
+
# positional harness arg, or raw launch command starts with clean model/effort
|
|
121
|
+
# defaults unless the caller also passes explicit --model/--effort flags. When
|
|
122
|
+
# the file governs the spawn, its model/effort tokens are re-resolved on every
|
|
123
|
+
# respawn exactly like the harness axis, and explicit --model/--effort flags
|
|
124
|
+
# still win over the file's tokens.
|
|
125
|
+
# A --secondmate spawn also propagates the primary's declared inherited local
|
|
126
|
+
# material, so the secondmate's OWN crewmates inherit primary config and the
|
|
127
|
+
# secondmate receives the primary's read-only shared captain-preference file
|
|
128
|
+
# (fm-config-inherit-lib.sh). A successful launch clears pending inherited
|
|
129
|
+
# config reread generations because the new agent reads the converged files.
|
|
130
|
+
# --scout records kind=scout in the task's meta (report deliverable, scratch worktree;
|
|
131
|
+
# see AGENTS.md task lifecycle); --secondmate records kind=secondmate and launches in a
|
|
132
|
+
# provisioned firstmate home; the default is kind=ship.
|
|
133
|
+
# Before a secondmate launch, the home is locally fast-forwarded to the primary
|
|
134
|
+
# default-branch commit when safe; skipped syncs warn and launch unchanged.
|
|
135
|
+
# Ship/scout spawns refuse to launch unless the resolved task path is a real
|
|
136
|
+
# git worktree root distinct from the primary project checkout.
|
|
137
|
+
# Before a fresh ship or scout worker starts, its clean task worktree fetches
|
|
138
|
+
# origin, resolves the current remote default branch, and resets to its tip.
|
|
139
|
+
# An unreachable origin, unresolved default branch, or non-clean worktree
|
|
140
|
+
# refuses the spawn rather than risking a PR based on stale history.
|
|
141
|
+
# A slot whose only deviation is a stale submodule gitlink is refused by that
|
|
142
|
+
# same clean check, but is reported as a stale checkout naming each submodule
|
|
143
|
+
# and both pins; nothing is converged or removed, and no remedy is suggested.
|
|
144
|
+
# That report is only reached when each submodule's checked-out commit is
|
|
145
|
+
# already contained in one of its remotes, so a submodule carrying an unpushed
|
|
146
|
+
# commit keeps the conservative uncommitted-work refusal instead. That
|
|
147
|
+
# containment test reads local refs only and never fetches, so this gate stays
|
|
148
|
+
# usable offline; a stale remote-tracking ref can therefore make an unpushed
|
|
149
|
+
# commit look contained, which is exactly why no remedy command is printed.
|
|
150
|
+
# Batch dispatch: pass one or more `id=repo` pairs instead of a single <id> <project>, e.g.
|
|
151
|
+
# fm-spawn.sh fix-a-k3=projects/foo add-b-q7=projects/bar [--scout]
|
|
152
|
+
# Each pair re-execs this script in single-task mode, so the single path stays the only
|
|
153
|
+
# source of truth; shared --scout/--harness/--model/--effort/--backend/--mode/--yolo
|
|
154
|
+
# applies to every pair. A ship batch therefore carries one delivery contract, and each
|
|
155
|
+
# pair still checks it against its own brief; a batch spanning modes is two invocations.
|
|
156
|
+
# If config/crew-dispatch.json exists, shared --harness is required for crewmate
|
|
157
|
+
# and scout batches. The loop lives here, in bash, so callers never hand-write a
|
|
158
|
+
# multi-task shell loop (the tool shell is zsh, which does not word-split unquoted
|
|
159
|
+
# $vars and silently breaks ad-hoc `for ... in $pairs` loops).
|
|
160
|
+
# Launch templates live in launch_template() below; placeholders replaced before launch:
|
|
161
|
+
# __BRIEF__ absolute path to data/<task-id>/brief.md
|
|
162
|
+
# __PIBIN__ quoted concrete Pi-family executable path resolved from PATH
|
|
163
|
+
# __PITUIMODE__ optional --tui-mode regular when that executable advertises it
|
|
164
|
+
# __TURNEND__ absolute path to state/<task-id>.turn-ended (for harnesses whose
|
|
165
|
+
# turn-end signal rides the launch command, e.g. codex -c notify=[...])
|
|
166
|
+
# __PIEXT__ absolute path to state/<task-id>.pi-ext.ts (pi turn-end extension,
|
|
167
|
+
# written by this script; outside the worktree to avoid pi's trust gate)
|
|
168
|
+
# __PITURNEND__ absolute path to .pi/extensions/fm-primary-turnend-guard.ts in a pi secondmate home
|
|
169
|
+
# __PIWATCH__ absolute path to .pi/extensions/fm-primary-pi-watch.ts in a pi secondmate home
|
|
170
|
+
# __OPINPUT__ absolute path to the canonical operational-input encoder
|
|
171
|
+
# __WORKTREE__ absolute path to the task worktree
|
|
172
|
+
# __CURSORBIN__ resolved, cursor-verified executable for a cursor launch
|
|
173
|
+
# Verified per-harness turn-end hooks are installed automatically where enabled; some live outside the worktree.
|
|
174
|
+
# Kimi uses one surgically installed Firstmate region in $HOME/.kimi-code/config.toml,
|
|
175
|
+
# a firstmate-owned global hook and registry, and a gitignored per-task pointer.
|
|
176
|
+
# grok uses a firstmate-owned global hook under ${GROK_HOME:-$HOME/.grok}/hooks
|
|
177
|
+
# plus a gitignored .fm-grok-turnend worktree pointer and a state token.
|
|
178
|
+
# muse installs no hook at all - its plugin engine is off in the default build - so
|
|
179
|
+
# it writes state/<id>.muse-session to bind the pane to muse's own session event
|
|
180
|
+
# log; muse is crewmate/scout only and is refused for --secondmate.
|
|
181
|
+
# cursor installs no per-task hook either: it writes state/<id>.cursor-session to
|
|
182
|
+
# bind the pane to cursor's own conversation transcript (projects root, the exact
|
|
183
|
+
# workspace path cursor records in .workspace-trusted, and the conversations that
|
|
184
|
+
# already existed for that workspace). It is launched through the verified binary
|
|
185
|
+
# resolver because `cursor` is not the CLI name. A cursor SECONDMATE instead runs
|
|
186
|
+
# the tracked project-scope .cursor/hooks.json in its own home, whose stop-hook
|
|
187
|
+
# park owns that home's supervision (docs/supervision-protocols/cursor.md).
|
|
188
|
+
# Publishing the record and moving this home's backlog item to In flight are one
|
|
189
|
+
# step, not two: bin/fm-backlog-transition-lib.sh owns that invariant, and this
|
|
190
|
+
# script performs the transition under the task's own meta lock before it reports
|
|
191
|
+
# success. A ship or scout dispatch therefore REFUSES up front, before any
|
|
192
|
+
# endpoint, worktree, or record exists, unless the home's backlog has an
|
|
193
|
+
# unheld, unblocked Queued or In flight item for the id; a transition that fails
|
|
194
|
+
# after publication removes the record it just wrote rather than leaving a
|
|
195
|
+
# worker the backlog does not own. A relaunch re-reads the row instead of
|
|
196
|
+
# re-running the transition, so an eligible In-flight item is left untouched.
|
|
197
|
+
# The transition is
|
|
198
|
+
# skipped entirely for --secondmate spawns (persistent agents are not work
|
|
199
|
+
# items), on a config/backlog-backend=manual home, and in a home that keeps no
|
|
200
|
+
# data/backlog.md. An automatic-backend home with a backlog but no compatible
|
|
201
|
+
# tasks-axi refuses before creating any lifecycle state.
|
|
202
|
+
# On success prints: spawned <id> harness=<name> kind=<ship|scout|secondmate> [mode=<mode> yolo=<on|off>] window=<backend-target> worktree=<path>
|
|
203
|
+
# A ship task records the explicit mode/yolo it was passed; a secondmate spawn records
|
|
204
|
+
# mode=secondmate, yolo=off, home=, and projects=; a scout records neither, and both the
|
|
205
|
+
# success line and state/<id>.meta omit them.
|
|
206
|
+
# Every fresh spawn or relaunch records a new spawn_gen= incarnation token so durable
|
|
207
|
+
# consumers can distinguish a replacement worker that reuses the same task id.
|
|
208
|
+
# When the home session's frozen trace-context decision is enabled (see
|
|
209
|
+
# docs/configuration.md and bin/fm-trace-context-lib.sh), the meta also records
|
|
210
|
+
# one W3C traceparent= carrier, the same value injected into the pane as
|
|
211
|
+
# TRACEPARENT; the default-off path writes neither, leaving the generated meta
|
|
212
|
+
# and launch environment unchanged.
|
|
213
|
+
# --traceparent <carrier> delivers a carrier that a REMOTE parent already
|
|
214
|
+
# resolved and will record, instead of resolving one from this home's frozen
|
|
215
|
+
# decision. It is accepted only for --secondmate spawns, only as a strictly
|
|
216
|
+
# validated W3C traceparent, and exists because a remote secondmate's task
|
|
217
|
+
# identity is owned by the parent home that holds its task metadata, while the
|
|
218
|
+
# pane export happens on the remote host (bin/fm-remote-secondmate-control.sh).
|
|
219
|
+
# Local spawns never pass it and resolve their own carrier exactly as before.
|
|
220
|
+
set -eu
|
|
221
|
+
|
|
222
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
223
|
+
|
|
224
|
+
usage() {
|
|
225
|
+
# The whole leading comment block, ending at the first line that is not a
|
|
226
|
+
# comment. Derived rather than a fixed line range, which silently truncated
|
|
227
|
+
# this help mid-sentence every time the header above grew.
|
|
228
|
+
sed -n '2,${/^#/!q;p;}' "$0" | sed 's/^# \{0,1\}//'
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
case "${1:-}" in
|
|
232
|
+
-h|--help) usage; exit 0 ;;
|
|
233
|
+
esac
|
|
234
|
+
|
|
235
|
+
FM_ROOT="${FM_ROOT_OVERRIDE:-$(cd "$SCRIPT_DIR/.." && pwd)}"
|
|
236
|
+
FM_HOME="${FM_HOME:-${FM_ROOT_OVERRIDE:-$FM_ROOT}}"
|
|
237
|
+
|
|
238
|
+
# shellcheck source=bin/fm-tasks-axi-lib.sh
|
|
239
|
+
. "$SCRIPT_DIR/fm-tasks-axi-lib.sh"
|
|
240
|
+
# shellcheck source=bin/fm-backlog-transition-lib.sh
|
|
241
|
+
. "$SCRIPT_DIR/fm-backlog-transition-lib.sh"
|
|
242
|
+
|
|
243
|
+
resolve_directory_input() {
|
|
244
|
+
local name=$1 path=$2 resolved raw_bytes
|
|
245
|
+
raw_bytes=$(fm_backlog_bytes_of_string "$path") || return 1
|
|
246
|
+
if ! fm_backlog_control_bytes_valid 0 "$raw_bytes"; then
|
|
247
|
+
echo "error: $name directory contains an invalid control byte" >&2
|
|
248
|
+
return 1
|
|
249
|
+
fi
|
|
250
|
+
case "$path" in
|
|
251
|
+
/*) printf '%s\n' "$path"; return 0 ;;
|
|
252
|
+
esac
|
|
253
|
+
resolved=$(CDPATH='' cd -- "$path" 2>/dev/null && pwd -P) || {
|
|
254
|
+
echo "error: $name directory cannot be resolved: $path" >&2
|
|
255
|
+
return 1
|
|
256
|
+
}
|
|
257
|
+
printf '%s\n' "$resolved"
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
FM_HOME=$(resolve_directory_input FM_HOME "$FM_HOME") || exit 1
|
|
261
|
+
if [ -n "${FM_STATE_OVERRIDE:-}" ]; then
|
|
262
|
+
FM_STATE_OVERRIDE=$(resolve_directory_input FM_STATE_OVERRIDE "$FM_STATE_OVERRIDE") || exit 1
|
|
263
|
+
fi
|
|
264
|
+
if [ -n "${FM_DATA_OVERRIDE:-}" ]; then
|
|
265
|
+
FM_DATA_OVERRIDE=$(resolve_directory_input FM_DATA_OVERRIDE "$FM_DATA_OVERRIDE") || exit 1
|
|
266
|
+
fi
|
|
267
|
+
STATE="${FM_STATE_OVERRIDE:-$FM_HOME/state}"
|
|
268
|
+
DATA="${FM_DATA_OVERRIDE:-$FM_HOME/data}"
|
|
269
|
+
PROJECTS="${FM_PROJECTS_OVERRIDE:-$FM_HOME/projects}"
|
|
270
|
+
CONFIG="${FM_CONFIG_OVERRIDE:-$FM_HOME/config}"
|
|
271
|
+
SUB_HOME_MARKER=".fm-secondmate-home"
|
|
272
|
+
if [ -e "$STATE" ] || [ -L "$STATE" ]; then
|
|
273
|
+
fm_backlog_directory_present "$STATE" "state directory" || {
|
|
274
|
+
echo "error: spawn refused: $FM_BACKLOG_TRANSITION_ERROR" >&2
|
|
275
|
+
exit 1
|
|
276
|
+
}
|
|
277
|
+
fi
|
|
278
|
+
# shellcheck source=bin/fm-ff-lib.sh
|
|
279
|
+
. "$SCRIPT_DIR/fm-ff-lib.sh"
|
|
280
|
+
# shellcheck source=bin/fm-wake-lib.sh
|
|
281
|
+
. "$SCRIPT_DIR/fm-wake-lib.sh"
|
|
282
|
+
fm_backlog_directory_present "$STATE" "state directory" || {
|
|
283
|
+
echo "error: spawn refused: $FM_BACKLOG_TRANSITION_ERROR" >&2
|
|
284
|
+
exit 1
|
|
285
|
+
}
|
|
286
|
+
# shellcheck source=bin/fm-secondmate-nudge-lib.sh
|
|
287
|
+
. "$SCRIPT_DIR/fm-secondmate-nudge-lib.sh"
|
|
288
|
+
# shellcheck source=bin/fm-config-inherit-lib.sh
|
|
289
|
+
. "$SCRIPT_DIR/fm-config-inherit-lib.sh"
|
|
290
|
+
# shellcheck source=bin/fm-backend.sh
|
|
291
|
+
. "$SCRIPT_DIR/fm-backend.sh"
|
|
292
|
+
# shellcheck source=bin/fm-control-lib.sh
|
|
293
|
+
. "$SCRIPT_DIR/fm-control-lib.sh"
|
|
294
|
+
# shellcheck source=bin/fm-gate-refuse-lib.sh
|
|
295
|
+
. "$SCRIPT_DIR/fm-gate-refuse-lib.sh"
|
|
296
|
+
# shellcheck source=bin/fm-busy-lib.sh
|
|
297
|
+
. "$SCRIPT_DIR/fm-busy-lib.sh"
|
|
298
|
+
# shellcheck source=bin/fm-cursor-lib.sh
|
|
299
|
+
. "$SCRIPT_DIR/fm-cursor-lib.sh"
|
|
300
|
+
# shellcheck source=bin/fm-pr-lib.sh
|
|
301
|
+
. "$SCRIPT_DIR/fm-pr-lib.sh"
|
|
302
|
+
# shellcheck source=bin/fm-trace-context-lib.sh
|
|
303
|
+
. "$SCRIPT_DIR/fm-trace-context-lib.sh"
|
|
304
|
+
# shellcheck source=bin/fm-remote-readiness-lib.sh
|
|
305
|
+
. "$SCRIPT_DIR/fm-remote-readiness-lib.sh"
|
|
306
|
+
# Fail closed before any fleet mutation: a no-mistakes gate agent must never spawn
|
|
307
|
+
# a direct report (see bin/fm-gate-refuse-lib.sh).
|
|
308
|
+
fm_refuse_if_gate_agent
|
|
309
|
+
# Skip the watcher guard when re-exec'd for one pair of a batch (FM_SPAWN_NO_GUARD is
|
|
310
|
+
# set by the batch loop below), so the guard runs once for the batch, not once per pair.
|
|
311
|
+
[ -n "${FM_SPAWN_NO_GUARD:-}" ] || "$FM_ROOT/bin/fm-guard.sh" || true
|
|
312
|
+
KIND=ship
|
|
313
|
+
KIND_SET=0
|
|
314
|
+
HARNESS_ARG=
|
|
315
|
+
MODEL=
|
|
316
|
+
EFFORT=
|
|
317
|
+
BACKEND_ARG=
|
|
318
|
+
MODE=
|
|
319
|
+
YOLO=
|
|
320
|
+
TRACEPARENT_ARG=
|
|
321
|
+
HARNESS_SET=0
|
|
322
|
+
MODEL_SET=0
|
|
323
|
+
EFFORT_SET=0
|
|
324
|
+
BACKEND_SET=0
|
|
325
|
+
MODE_SET=0
|
|
326
|
+
YOLO_SET=0
|
|
327
|
+
TRACEPARENT_SET=0
|
|
328
|
+
RELAUNCH=0
|
|
329
|
+
POS=()
|
|
330
|
+
want_value=
|
|
331
|
+
for a in "$@"; do
|
|
332
|
+
if [ -n "$want_value" ]; then
|
|
333
|
+
case "$a" in
|
|
334
|
+
--*) echo "error: --$want_value requires a value" >&2; exit 1 ;;
|
|
335
|
+
esac
|
|
336
|
+
case "$want_value" in
|
|
337
|
+
harness) HARNESS_ARG=$a; HARNESS_SET=1 ;;
|
|
338
|
+
model) MODEL=$a; MODEL_SET=1 ;;
|
|
339
|
+
effort) EFFORT=$a; EFFORT_SET=1 ;;
|
|
340
|
+
backend) BACKEND_ARG=$a; BACKEND_SET=1 ;;
|
|
341
|
+
mode) MODE=$a; MODE_SET=1 ;;
|
|
342
|
+
yolo) YOLO=$a; YOLO_SET=1 ;;
|
|
343
|
+
traceparent) TRACEPARENT_ARG=$a; TRACEPARENT_SET=1 ;;
|
|
344
|
+
*) echo "error: internal parser state for --$want_value" >&2; exit 1 ;;
|
|
345
|
+
esac
|
|
346
|
+
want_value=
|
|
347
|
+
continue
|
|
348
|
+
fi
|
|
349
|
+
case "$a" in
|
|
350
|
+
--scout) KIND=scout; KIND_SET=1 ;;
|
|
351
|
+
--secondmate) KIND=secondmate; KIND_SET=1 ;;
|
|
352
|
+
--relaunch) RELAUNCH=1 ;;
|
|
353
|
+
--harness) want_value=harness ;;
|
|
354
|
+
--harness=*) HARNESS_ARG=${a#--harness=}; HARNESS_SET=1 ;;
|
|
355
|
+
--model) want_value=model ;;
|
|
356
|
+
--model=*) MODEL=${a#--model=}; MODEL_SET=1 ;;
|
|
357
|
+
--effort) want_value=effort ;;
|
|
358
|
+
--effort=*) EFFORT=${a#--effort=}; EFFORT_SET=1 ;;
|
|
359
|
+
--backend) want_value=backend ;;
|
|
360
|
+
--backend=*) BACKEND_ARG=${a#--backend=}; BACKEND_SET=1 ;;
|
|
361
|
+
--mode) want_value=mode ;;
|
|
362
|
+
--mode=*) MODE=${a#--mode=}; MODE_SET=1 ;;
|
|
363
|
+
--yolo) want_value=yolo ;;
|
|
364
|
+
--yolo=*) YOLO=${a#--yolo=}; YOLO_SET=1 ;;
|
|
365
|
+
--traceparent) want_value=traceparent ;;
|
|
366
|
+
--traceparent=*) TRACEPARENT_ARG=${a#--traceparent=}; TRACEPARENT_SET=1 ;;
|
|
367
|
+
*) POS+=("$a") ;;
|
|
368
|
+
esac
|
|
369
|
+
done
|
|
370
|
+
[ -z "$want_value" ] || { echo "error: --$want_value requires a value" >&2; exit 1; }
|
|
371
|
+
[ "$HARNESS_SET" -eq 0 ] || [ -n "$HARNESS_ARG" ] || { echo "error: --harness requires a non-empty value" >&2; exit 1; }
|
|
372
|
+
[ "$MODEL_SET" -eq 0 ] || [ -n "$MODEL" ] || { echo "error: --model requires a non-empty value" >&2; exit 1; }
|
|
373
|
+
[ "$EFFORT_SET" -eq 0 ] || [ -n "$EFFORT" ] || { echo "error: --effort requires a non-empty value" >&2; exit 1; }
|
|
374
|
+
[ "$BACKEND_SET" -eq 0 ] || [ -n "$BACKEND_ARG" ] || { echo "error: --backend requires a non-empty value" >&2; exit 1; }
|
|
375
|
+
[ "$MODE_SET" -eq 0 ] || [ -n "$MODE" ] || { echo "error: --mode requires a non-empty value" >&2; exit 1; }
|
|
376
|
+
[ "$YOLO_SET" -eq 0 ] || [ -n "$YOLO" ] || { echo "error: --yolo requires a non-empty value" >&2; exit 1; }
|
|
377
|
+
[ "$TRACEPARENT_SET" -eq 0 ] || [ -n "$TRACEPARENT_ARG" ] || { echo "error: --traceparent requires a non-empty value" >&2; exit 1; }
|
|
378
|
+
# A parent-delivered carrier replaces this home's own resolution, so it is
|
|
379
|
+
# refused unless it is a secondmate spawn carrying a strictly valid W3C value.
|
|
380
|
+
# Nothing else may reach the pane's TRACEPARENT export.
|
|
381
|
+
if [ "$TRACEPARENT_SET" -eq 1 ]; then
|
|
382
|
+
[ "$KIND" = secondmate ] || {
|
|
383
|
+
echo "error: --traceparent applies only to --secondmate spawns; every other spawn resolves its own carrier from this home's frozen trace-context decision" >&2
|
|
384
|
+
exit 1
|
|
385
|
+
}
|
|
386
|
+
fm_trace_context_valid "$TRACEPARENT_ARG" || {
|
|
387
|
+
echo "error: --traceparent is not a valid W3C traceparent" >&2
|
|
388
|
+
exit 1
|
|
389
|
+
}
|
|
390
|
+
fi
|
|
391
|
+
case "$EFFORT" in
|
|
392
|
+
''|low|medium|high|xhigh|max) ;;
|
|
393
|
+
*) echo "error: --effort must be one of low, medium, high, xhigh, max" >&2; exit 1 ;;
|
|
394
|
+
esac
|
|
395
|
+
|
|
396
|
+
# --relaunch reuses an existing task's endpoint, worktree, project, and kind,
|
|
397
|
+
# so every axis this block resolves for a fresh spawn instead comes from that
|
|
398
|
+
# task's own durable record below. Contradicting it on the command line is a
|
|
399
|
+
# refusal rather than a silently-ignored flag.
|
|
400
|
+
if [ "$RELAUNCH" -eq 1 ]; then
|
|
401
|
+
[ "$BACKEND_SET" -eq 0 ] || { echo "error: --relaunch reuses the task's recorded backend; --backend cannot override it" >&2; exit 1; }
|
|
402
|
+
[ "$KIND_SET" -eq 0 ] || { echo "error: --relaunch reuses the task's recorded kind; --scout/--secondmate cannot override it" >&2; exit 1; }
|
|
403
|
+
[ "$MODE_SET" -eq 0 ] || { echo "error: --relaunch reuses the task's recorded delivery mode; --mode cannot override it" >&2; exit 1; }
|
|
404
|
+
[ "$YOLO_SET" -eq 0 ] || { echo "error: --relaunch reuses the task's recorded yolo posture; --yolo cannot override it" >&2; exit 1; }
|
|
405
|
+
else
|
|
406
|
+
# Delivery contract (AGENTS.md section 7). A ship task's mode and yolo are
|
|
407
|
+
# firstmate's per-task decision, so they are required and closed-set validated
|
|
408
|
+
# here rather than resolved from the project registry. Scouts deliver a report
|
|
409
|
+
# and record no delivery posture; secondmate spawns hardcode theirs.
|
|
410
|
+
if [ "$KIND" = ship ]; then
|
|
411
|
+
[ "$MODE_SET" -eq 1 ] || {
|
|
412
|
+
echo "error: ship spawns require --mode <no-mistakes|direct-PR|local-only>; resolve it at intake from the captain's instruction and the project's registered posture in data/projects.md" >&2
|
|
413
|
+
exit 1
|
|
414
|
+
}
|
|
415
|
+
[ "$YOLO_SET" -eq 1 ] || {
|
|
416
|
+
echo "error: ship spawns require --yolo <on|off>; it is this task's merge authority, not a project lookup" >&2
|
|
417
|
+
exit 1
|
|
418
|
+
}
|
|
419
|
+
case "$MODE" in
|
|
420
|
+
no-mistakes|direct-PR|local-only) ;;
|
|
421
|
+
no-mistakes-prod-only)
|
|
422
|
+
echo "error: no-mistakes-prod-only is a registry policy, not a task mode; classify this task's surface and resolve it to no-mistakes or direct-PR at intake" >&2
|
|
423
|
+
exit 1 ;;
|
|
424
|
+
*) echo "error: --mode must be one of no-mistakes, direct-PR, local-only (got '$MODE')" >&2; exit 1 ;;
|
|
425
|
+
esac
|
|
426
|
+
case "$YOLO" in
|
|
427
|
+
on|off) ;;
|
|
428
|
+
*) echo "error: --yolo must be on or off (got '$YOLO')" >&2; exit 1 ;;
|
|
429
|
+
esac
|
|
430
|
+
else
|
|
431
|
+
[ "$MODE_SET" -eq 0 ] || {
|
|
432
|
+
echo "error: --mode applies only to ship spawns; a scout delivers a report and a secondmate records its own fixed posture" >&2
|
|
433
|
+
exit 1
|
|
434
|
+
}
|
|
435
|
+
[ "$YOLO_SET" -eq 0 ] || {
|
|
436
|
+
echo "error: --yolo applies only to ship spawns; a scout delivers a report and a secondmate records its own fixed posture" >&2
|
|
437
|
+
exit 1
|
|
438
|
+
}
|
|
439
|
+
fi
|
|
440
|
+
fi
|
|
441
|
+
|
|
442
|
+
spawn_remote_secondmate() {
|
|
443
|
+
local id=$1 remote host root home harness positional model effort backend out rc meta tmp
|
|
444
|
+
local remote_backend remote_target remote_harness remote_herdr_session registry_lock remote_lock remote_generation
|
|
445
|
+
local remote_traceparent remote_recorded_traceparent
|
|
446
|
+
local -a launch_args
|
|
447
|
+
id=${POS[0]:-}
|
|
448
|
+
fm_task_id_creation_valid "$id" || { echo "error: invalid task id" >&2; return 2; }
|
|
449
|
+
mkdir -p "$STATE" || { echo "error: could not create parent state directory" >&2; return 1; }
|
|
450
|
+
SPAWN_TASK_LOCK="$STATE/.spawn-$id.lock"
|
|
451
|
+
if ! fm_lock_try_acquire "$SPAWN_TASK_LOCK"; then
|
|
452
|
+
echo "error: another spawn is already creating task $id" >&2
|
|
453
|
+
return 1
|
|
454
|
+
fi
|
|
455
|
+
registry_lock=$(secondmate_registry_lock_path "$STATE")
|
|
456
|
+
if ! fm_lock_acquire_wait "$registry_lock"; then
|
|
457
|
+
fm_lock_release "$SPAWN_TASK_LOCK" || true
|
|
458
|
+
echo "error: secondmate registry could not be locked for remote spawn" >&2
|
|
459
|
+
return 1
|
|
460
|
+
fi
|
|
461
|
+
remote=$(secondmate_registry_field "$DATA/secondmates.md" "$id" remote 2>/dev/null || true)
|
|
462
|
+
if [ "$remote" != 1 ]; then
|
|
463
|
+
fm_lock_release "$registry_lock" || true
|
|
464
|
+
fm_lock_release "$SPAWN_TASK_LOCK" || true
|
|
465
|
+
return 3
|
|
466
|
+
fi
|
|
467
|
+
host=$(secondmate_registry_field "$DATA/secondmates.md" "$id" host)
|
|
468
|
+
root=$(secondmate_registry_field "$DATA/secondmates.md" "$id" root)
|
|
469
|
+
home=$(secondmate_registry_field "$DATA/secondmates.md" "$id" home)
|
|
470
|
+
positional=${POS[1]:-}
|
|
471
|
+
if [ "${#POS[@]}" -gt 2 ]; then
|
|
472
|
+
fm_lock_release "$registry_lock" || true
|
|
473
|
+
fm_lock_release "$SPAWN_TASK_LOCK" || true
|
|
474
|
+
echo "error: remote secondmate spawn accepts no local home positional argument" >&2
|
|
475
|
+
return 2
|
|
476
|
+
fi
|
|
477
|
+
if [ -n "$HARNESS_ARG" ]; then
|
|
478
|
+
harness=$HARNESS_ARG
|
|
479
|
+
elif [ -n "$positional" ]; then
|
|
480
|
+
harness=$positional
|
|
481
|
+
else
|
|
482
|
+
harness=$("$FM_ROOT/bin/fm-harness.sh" secondmate)
|
|
483
|
+
fi
|
|
484
|
+
case "$harness" in
|
|
485
|
+
claude|codex|opencode|pi|pi-signed|grok|kimi|cursor) ;;
|
|
486
|
+
*)
|
|
487
|
+
fm_lock_release "$registry_lock" || true
|
|
488
|
+
fm_lock_release "$SPAWN_TASK_LOCK" || true
|
|
489
|
+
echo "error: remote secondmate spawn requires a verified harness adapter, not a raw launch command: $harness" >&2
|
|
490
|
+
return 1
|
|
491
|
+
;;
|
|
492
|
+
esac
|
|
493
|
+
model=${MODEL:--}
|
|
494
|
+
effort=${EFFORT:--}
|
|
495
|
+
if [ -z "$HARNESS_ARG" ] && [ -z "$positional" ]; then
|
|
496
|
+
if [ "$MODEL_SET" -eq 0 ]; then
|
|
497
|
+
model=$("$SCRIPT_DIR/fm-harness.sh" secondmate-model)
|
|
498
|
+
[ -n "$model" ] || model=-
|
|
499
|
+
fi
|
|
500
|
+
if [ "$EFFORT_SET" -eq 0 ]; then
|
|
501
|
+
effort=$("$SCRIPT_DIR/fm-harness.sh" secondmate-effort)
|
|
502
|
+
[ -n "$effort" ] || effort=-
|
|
503
|
+
fi
|
|
504
|
+
fi
|
|
505
|
+
# A remote second mate always runs on Herdr: its server belongs to the host's
|
|
506
|
+
# own GUI login session, so the endpoint outlives every SSH connection that
|
|
507
|
+
# supervises it. bin/fm-remote-doctor.sh gates that host on the same
|
|
508
|
+
# requirement, and the remote home's config/backend never overrides it.
|
|
509
|
+
case "${BACKEND_ARG:--}" in
|
|
510
|
+
-|herdr) backend=herdr ;;
|
|
511
|
+
*)
|
|
512
|
+
fm_lock_release "$registry_lock" || true
|
|
513
|
+
fm_lock_release "$SPAWN_TASK_LOCK" || true
|
|
514
|
+
echo "error: a remote secondmate runs only on the herdr backend, not '$BACKEND_ARG'" >&2
|
|
515
|
+
return 1
|
|
516
|
+
;;
|
|
517
|
+
esac
|
|
518
|
+
case "$effort" in
|
|
519
|
+
-|low|medium|high|xhigh|max) ;;
|
|
520
|
+
*)
|
|
521
|
+
fm_lock_release "$registry_lock" || true
|
|
522
|
+
fm_lock_release "$SPAWN_TASK_LOCK" || true
|
|
523
|
+
echo "error: invalid configured remote secondmate effort: $effort" >&2
|
|
524
|
+
return 1
|
|
525
|
+
;;
|
|
526
|
+
esac
|
|
527
|
+
meta="$STATE/$id.meta"
|
|
528
|
+
if [ -e "$meta" ] || [ -L "$meta" ]; then
|
|
529
|
+
if ! fm_backlog_record_present "$meta" "task record" "$STATE" \
|
|
530
|
+
|| [ "$(fm_meta_get "$meta" kind)" != secondmate ] \
|
|
531
|
+
|| [ "$(fm_meta_get "$meta" remote_host)" != "$host" ] \
|
|
532
|
+
|| [ "$(fm_meta_get "$meta" remote_root)" != "$root" ] \
|
|
533
|
+
|| [ "$(fm_meta_get "$meta" home)" != "$home" ]; then
|
|
534
|
+
fm_lock_release "$registry_lock" || true
|
|
535
|
+
fm_lock_release "$SPAWN_TASK_LOCK" || true
|
|
536
|
+
echo "error: existing metadata for $id does not identify this remote secondmate route" >&2
|
|
537
|
+
return 1
|
|
538
|
+
fi
|
|
539
|
+
fi
|
|
540
|
+
# Gate the host before anything is published or transferred, so a host that
|
|
541
|
+
# cannot hold a durable Herdr endpoint refuses here rather than half-way
|
|
542
|
+
# through a launch. This is also the readiness gate every liveness relaunch
|
|
543
|
+
# passes through, because recovery respawns through this same route.
|
|
544
|
+
rc=0
|
|
545
|
+
fm_remote_readiness_ensure "$SCRIPT_DIR" "$id" || rc=$?
|
|
546
|
+
if [ "$rc" -ne 0 ]; then
|
|
547
|
+
fm_lock_release "$registry_lock" || true
|
|
548
|
+
fm_lock_release "$SPAWN_TASK_LOCK" || true
|
|
549
|
+
# Summary first, then the doctor's own text: a caller that reports only the
|
|
550
|
+
# first line, such as the startup liveness sweep, must still say something
|
|
551
|
+
# actionable.
|
|
552
|
+
if [ "$rc" -eq 255 ]; then
|
|
553
|
+
echo "error: remote secondmate $id readiness could not be confirmed; preserved route $host:$home" >&2
|
|
554
|
+
else
|
|
555
|
+
echo "error: remote secondmate $id host $host is not ready for a remote second mate; launch refused" >&2
|
|
556
|
+
fi
|
|
557
|
+
[ -z "$FM_REMOTE_READINESS_OUT" ] || printf '%s\n' "$FM_REMOTE_READINESS_OUT" >&2
|
|
558
|
+
[ "$rc" -ne 255 ] || return 255
|
|
559
|
+
return 1
|
|
560
|
+
fi
|
|
561
|
+
remote_lock=$(fm_remote_inherit_transaction_lock_path "$STATE" "$id")
|
|
562
|
+
if ! fm_lock_acquire_wait "$remote_lock"; then
|
|
563
|
+
fm_lock_release "$registry_lock" || true
|
|
564
|
+
fm_lock_release "$SPAWN_TASK_LOCK" || true
|
|
565
|
+
echo "error: remote secondmate $id inheritance transaction could not be locked" >&2
|
|
566
|
+
return 1
|
|
567
|
+
fi
|
|
568
|
+
remote_generation=$(fm_remote_inherit_generation_next "$STATE" "$id" 2>/dev/null || true)
|
|
569
|
+
if [ -z "$remote_generation" ]; then
|
|
570
|
+
fm_lock_release "$remote_lock" || true
|
|
571
|
+
fm_lock_release "$registry_lock" || true
|
|
572
|
+
fm_lock_release "$SPAWN_TASK_LOCK" || true
|
|
573
|
+
echo "error: remote secondmate $id inheritance generation could not be published" >&2
|
|
574
|
+
return 1
|
|
575
|
+
fi
|
|
576
|
+
if "$SCRIPT_DIR/fm-remote-inherit-push.sh" "$id" "$remote_generation" >/dev/null; then
|
|
577
|
+
:
|
|
578
|
+
else
|
|
579
|
+
rc=$?
|
|
580
|
+
fm_lock_release "$remote_lock" || true
|
|
581
|
+
fm_lock_release "$registry_lock" || true
|
|
582
|
+
fm_lock_release "$SPAWN_TASK_LOCK" || true
|
|
583
|
+
if [ "$rc" -eq 255 ]; then
|
|
584
|
+
echo "error: remote secondmate $id inheritance completion is unknown; launch refused and route preserved for reconciliation" >&2
|
|
585
|
+
else
|
|
586
|
+
echo "error: remote secondmate $id inheritance failed; launch refused" >&2
|
|
587
|
+
fi
|
|
588
|
+
return "$rc"
|
|
589
|
+
fi
|
|
590
|
+
# This parent home owns the remote secondmate's task identity because it holds
|
|
591
|
+
# the task metadata an observer reads, exactly as for a local spawn: the
|
|
592
|
+
# carrier is resolved against THIS task's own meta (reused verbatim on
|
|
593
|
+
# relaunch, freshly rooted otherwise, never adopting this process's ambient
|
|
594
|
+
# TRACEPARENT) under this home's frozen decision, then handed to the remote
|
|
595
|
+
# host to export into the agent's pane. Disabled resolves to empty and the
|
|
596
|
+
# remote launch call stays byte-identical to the untraced one.
|
|
597
|
+
remote_traceparent=
|
|
598
|
+
if [ "$(fm_trace_context_session_effective "$STATE/.trace-context-effective")" = on ]; then
|
|
599
|
+
remote_traceparent=$(FM_TRACE_CONTEXT=on fm_trace_context_resolve "$CONFIG" "$meta" || true)
|
|
600
|
+
fi
|
|
601
|
+
launch_args=("$id" "$harness" "$model" "$effort" "$backend")
|
|
602
|
+
[ -z "$remote_traceparent" ] || launch_args+=("$remote_traceparent")
|
|
603
|
+
if out=$("$SCRIPT_DIR/fm-on.sh" "$id" fm-remote-secondmate-control.sh launch \
|
|
604
|
+
"${launch_args[@]}" < /dev/null 2>&1); then
|
|
605
|
+
rc=0
|
|
606
|
+
else
|
|
607
|
+
rc=$?
|
|
608
|
+
fi
|
|
609
|
+
if [ "$rc" -ne 0 ]; then
|
|
610
|
+
fm_lock_release "$remote_lock" || true
|
|
611
|
+
fm_lock_release "$registry_lock" || true
|
|
612
|
+
fm_lock_release "$SPAWN_TASK_LOCK" || true
|
|
613
|
+
[ -z "$out" ] || printf '%s\n' "$out" >&2
|
|
614
|
+
if [ "$rc" -eq 255 ]; then
|
|
615
|
+
echo "error: remote secondmate $id is unavailable or launch completion is unknown; preserved route $host:$home" >&2
|
|
616
|
+
fi
|
|
617
|
+
return "$rc"
|
|
618
|
+
fi
|
|
619
|
+
remote_backend=$(printf '%s\n' "$out" | sed -n 's/^backend=//p' | tail -1)
|
|
620
|
+
remote_target=$(printf '%s\n' "$out" | sed -n 's/^target=//p' | tail -1)
|
|
621
|
+
remote_harness=$(printf '%s\n' "$out" | sed -n 's/^harness=//p' | tail -1)
|
|
622
|
+
remote_herdr_session=$(printf '%s\n' "$out" | sed -n 's/^herdr_session=//p' | tail -1)
|
|
623
|
+
if [ "$remote_backend" != herdr ]; then
|
|
624
|
+
fm_lock_release "$remote_lock" || true
|
|
625
|
+
fm_lock_release "$registry_lock" || true
|
|
626
|
+
fm_lock_release "$SPAWN_TASK_LOCK" || true
|
|
627
|
+
echo "error: remote launch returned backend '${remote_backend:-missing}', expected herdr; preserving the remote route for reconciliation" >&2
|
|
628
|
+
return 1
|
|
629
|
+
fi
|
|
630
|
+
[ -n "$remote_target" ] && [ "$remote_harness" = "$harness" ] || {
|
|
631
|
+
fm_lock_release "$remote_lock" || true
|
|
632
|
+
fm_lock_release "$registry_lock" || true
|
|
633
|
+
fm_lock_release "$SPAWN_TASK_LOCK" || true
|
|
634
|
+
echo "error: remote launch returned malformed route metadata; preserving the remote route for reconciliation" >&2
|
|
635
|
+
return 1
|
|
636
|
+
}
|
|
637
|
+
if [ "$remote_herdr_session" != fm-remote ] || [ "${remote_target%%:*}" != "$remote_herdr_session" ]; then
|
|
638
|
+
fm_lock_release "$remote_lock" || true
|
|
639
|
+
fm_lock_release "$registry_lock" || true
|
|
640
|
+
fm_lock_release "$SPAWN_TASK_LOCK" || true
|
|
641
|
+
echo "error: remote launch returned Herdr session '${remote_herdr_session:-missing}', expected 'fm-remote'; preserving the remote route for reconciliation" >&2
|
|
642
|
+
return 1
|
|
643
|
+
fi
|
|
644
|
+
# Record what the remote endpoint ACTUALLY carries, read back from its own
|
|
645
|
+
# launch, rather than what this side hoped to deliver. That keeps the #995
|
|
646
|
+
# guarantee that the recorded carrier is the identity the child received even
|
|
647
|
+
# when the remote host already had a live agent and reused its endpoint. An
|
|
648
|
+
# off decision delivers no carrier, but an endpoint already holding one still
|
|
649
|
+
# reports it here so the parent does not deny the agent's actual identity.
|
|
650
|
+
remote_recorded_traceparent=$(printf '%s\n' "$out" | sed -n 's/^traceparent=//p' | tail -1)
|
|
651
|
+
fm_trace_context_valid "$remote_recorded_traceparent" || remote_recorded_traceparent=
|
|
652
|
+
tmp="$meta.tmp.$$"
|
|
653
|
+
{
|
|
654
|
+
echo "window=remote:$id"
|
|
655
|
+
echo "endpoint_task_id=$id"
|
|
656
|
+
echo "worktree=$home"
|
|
657
|
+
echo "project=$root"
|
|
658
|
+
echo "harness=$harness"
|
|
659
|
+
echo "kind=secondmate"
|
|
660
|
+
echo "mode=secondmate"
|
|
661
|
+
echo "yolo=off"
|
|
662
|
+
echo "tasktmp="
|
|
663
|
+
echo "model=${model#-}"
|
|
664
|
+
echo "effort=${effort#-}"
|
|
665
|
+
echo "home=$home"
|
|
666
|
+
echo "projects=$(secondmate_registry_field "$DATA/secondmates.md" "$id" projects)"
|
|
667
|
+
echo "remote_host=$host"
|
|
668
|
+
echo "remote_root=$root"
|
|
669
|
+
echo "remote_backend=$remote_backend"
|
|
670
|
+
echo "remote_herdr_session=$remote_herdr_session"
|
|
671
|
+
echo "remote_target=$remote_target"
|
|
672
|
+
[ -z "$remote_recorded_traceparent" ] || echo "traceparent=$remote_recorded_traceparent"
|
|
673
|
+
} > "$tmp"
|
|
674
|
+
if ! fm_backlog_atomic_transition publish "$tmp" "$meta" "task record" "$STATE"; then
|
|
675
|
+
if [ "$SPAWN_TASK_SET_LOCK_HELD" = 1 ]; then
|
|
676
|
+
SPAWN_TASK_SET_LOCK_HELD=0
|
|
677
|
+
fm_lock_release "$SPAWN_TASK_SET_LOCK" || true
|
|
678
|
+
fi
|
|
679
|
+
fm_lock_release "$remote_lock" || true
|
|
680
|
+
fm_lock_release "$registry_lock" || true
|
|
681
|
+
fm_lock_release "$SPAWN_TASK_LOCK" || true
|
|
682
|
+
echo "error: remote secondmate $id launched, but its task record could not be published ($FM_BACKLOG_TRANSITION_ERROR)" >&2
|
|
683
|
+
return 1
|
|
684
|
+
fi
|
|
685
|
+
if [ "$SPAWN_TASK_SET_LOCK_HELD" = 1 ]; then
|
|
686
|
+
SPAWN_TASK_SET_LOCK_HELD=0
|
|
687
|
+
fm_lock_release "$SPAWN_TASK_SET_LOCK"
|
|
688
|
+
fi
|
|
689
|
+
fm_lock_release "$remote_lock" || true
|
|
690
|
+
fm_lock_release "$registry_lock" || true
|
|
691
|
+
fm_lock_release "$SPAWN_TASK_LOCK" || true
|
|
692
|
+
"$SCRIPT_DIR/fm-home-summary-refresh.sh" --best-effort || true
|
|
693
|
+
if ! "$SCRIPT_DIR/fm-procevent-remote-reply.sh" arm "$id" >/dev/null; then
|
|
694
|
+
echo "error: remote secondmate $id launched, but its reply source could not be armed; endpoint metadata is preserved" >&2
|
|
695
|
+
return 1
|
|
696
|
+
fi
|
|
697
|
+
echo "spawned $id harness=$harness kind=secondmate mode=secondmate yolo=off window=remote:$id worktree=$home remote=$host backend=$remote_backend"
|
|
698
|
+
return 0
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
BACKEND=
|
|
702
|
+
ORCA_ABORT_CLEANUP=0
|
|
703
|
+
ORCA_WORKTREE_ID=
|
|
704
|
+
ORCA_TERMINAL=
|
|
705
|
+
HERDR_PROJECTION_ABORT_CLEANUP=0
|
|
706
|
+
HERDR_PROJECTION_ABORT_SESSION=
|
|
707
|
+
HERDR_PROJECTION_ABORT_TASK_PANE=
|
|
708
|
+
HERDR_PROJECTION_ABORT_SEEDED_PANE=
|
|
709
|
+
HERDR_PRESENTATION_ORDER_LOCK=
|
|
710
|
+
HERDR_PRESENTATION_ORDER_LOCK_HELD=0
|
|
711
|
+
SPAWN_TASK_LOCK=
|
|
712
|
+
SPAWN_TASK_LOCK_HELD=0
|
|
713
|
+
SPAWN_CONTROL_LOCK=
|
|
714
|
+
SPAWN_CONTROL_LOCK_HELD=0
|
|
715
|
+
SPAWN_CONTROL_PARENT=0
|
|
716
|
+
SPAWN_META_TMP=
|
|
717
|
+
SPAWN_META_LOCK=
|
|
718
|
+
SPAWN_META_LOCK_HELD=0
|
|
719
|
+
SPAWN_META_PUBLISH_STARTED=0
|
|
720
|
+
SPAWN_FRESH_COMMIT_PENDING=0
|
|
721
|
+
SPAWN_TASK_SET_LOCK=
|
|
722
|
+
SPAWN_TASK_SET_LOCK_HELD=0
|
|
723
|
+
RELAUNCH_REPLACEMENT_PENDING=0
|
|
724
|
+
RELAUNCH_REPLACEMENT_BUSY_GEN=
|
|
725
|
+
RELAUNCH_REPLACEMENT_HARNESS=
|
|
726
|
+
RELAUNCH_REPLACEMENT_STATE=
|
|
727
|
+
RELAUNCH_REPLACEMENT_WT=
|
|
728
|
+
CONFIG_INHERIT_LOCK=
|
|
729
|
+
CONFIG_INHERIT_LOCK_HELD=0
|
|
730
|
+
|
|
731
|
+
spawn_fresh_commit_rollback() {
|
|
732
|
+
if fm_backlog_atomic_transition rollback "$STATE/$ID.meta" \
|
|
733
|
+
"$FM_ROOT/bin/fm-busy-event.sh" "$STATE" "$ID" "${BUSY_GEN:-}"; then
|
|
734
|
+
SPAWN_FRESH_COMMIT_PENDING=0
|
|
735
|
+
return 0
|
|
736
|
+
fi
|
|
737
|
+
echo "error: $FM_BACKLOG_TRANSITION_ERROR" >&2
|
|
738
|
+
return 1
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
parse_orca_worktree_result() {
|
|
742
|
+
local raw=$1 rest
|
|
743
|
+
ORCA_WORKTREE_ID=${raw%%$'\t'*}
|
|
744
|
+
if [ "$raw" = "$ORCA_WORKTREE_ID" ]; then
|
|
745
|
+
WT=
|
|
746
|
+
ORCA_TERMINAL=
|
|
747
|
+
return 1
|
|
748
|
+
fi
|
|
749
|
+
rest=${raw#*$'\t'}
|
|
750
|
+
WT=${rest%%$'\t'*}
|
|
751
|
+
if [ "$rest" != "$WT" ]; then
|
|
752
|
+
ORCA_TERMINAL=${rest#*$'\t'}
|
|
753
|
+
else
|
|
754
|
+
ORCA_TERMINAL=
|
|
755
|
+
fi
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
spawn_abort_cleanup() {
|
|
759
|
+
local status=$?
|
|
760
|
+
if [ "$RELAUNCH_REPLACEMENT_PENDING" = 1 ] \
|
|
761
|
+
&& [ "$SPAWN_META_PUBLISH_STARTED" = 1 ] \
|
|
762
|
+
&& [ -n "$SPAWN_META_TMP" ] \
|
|
763
|
+
&& [ ! -e "$SPAWN_META_TMP" ] \
|
|
764
|
+
&& [ ! -L "$SPAWN_META_TMP" ]; then
|
|
765
|
+
RELAUNCH_REPLACEMENT_PENDING=0
|
|
766
|
+
fi
|
|
767
|
+
if [ "$RELAUNCH_REPLACEMENT_PENDING" = 1 ]; then
|
|
768
|
+
RELAUNCH_REPLACEMENT_PENDING=0
|
|
769
|
+
if ! clear_relaunch_harness_wiring \
|
|
770
|
+
"$RELAUNCH_REPLACEMENT_HARNESS" \
|
|
771
|
+
"$RELAUNCH_REPLACEMENT_WT" \
|
|
772
|
+
"$RELAUNCH_REPLACEMENT_STATE" \
|
|
773
|
+
"$ID"; then
|
|
774
|
+
echo "warning: could not remove replacement wiring after aborted relaunch of $ID" >&2
|
|
775
|
+
fi
|
|
776
|
+
if [ -n "$RELAUNCH_REPLACEMENT_BUSY_GEN" ]; then
|
|
777
|
+
if ! "$FM_ROOT/bin/fm-busy-event.sh" retire \
|
|
778
|
+
"$RELAUNCH_REPLACEMENT_STATE" "$ID" \
|
|
779
|
+
--gen "$RELAUNCH_REPLACEMENT_BUSY_GEN"; then
|
|
780
|
+
echo "warning: could not retire replacement busy generation after aborted relaunch of $ID" >&2
|
|
781
|
+
fi
|
|
782
|
+
fi
|
|
783
|
+
fi
|
|
784
|
+
if [ "$HERDR_PROJECTION_ABORT_CLEANUP" = 1 ] \
|
|
785
|
+
&& [ "$HERDR_PRESENTATION_ORDER_LOCK_HELD" != 1 ]; then
|
|
786
|
+
if ! spawn_herdr_presentation_order_lock_acquire "${HERDR_PROJECTION_ABORT_SESSION:-}"; then
|
|
787
|
+
echo "warning: herdr presentation focus lock unavailable; retaining the projection journal and refusing concurrent abort cleanup" >&2
|
|
788
|
+
HERDR_PROJECTION_ABORT_CLEANUP=0
|
|
789
|
+
fi
|
|
790
|
+
fi
|
|
791
|
+
if [ "$HERDR_PROJECTION_ABORT_CLEANUP" = 1 ]; then
|
|
792
|
+
HERDR_PROJECTION_ABORT_CLEANUP=0
|
|
793
|
+
fm_backend_herdr_projection_cleanup_exact \
|
|
794
|
+
"$HERDR_PROJECTION_ABORT_SESSION" \
|
|
795
|
+
"$HERDR_PROJECTION_ABORT_TASK_PANE" \
|
|
796
|
+
"$HERDR_PROJECTION_ABORT_SEEDED_PANE" || true
|
|
797
|
+
fi
|
|
798
|
+
if [ "$HERDR_PRESENTATION_ORDER_LOCK_HELD" = 1 ]; then
|
|
799
|
+
HERDR_PRESENTATION_ORDER_LOCK_HELD=0
|
|
800
|
+
fm_lock_release "$HERDR_PRESENTATION_ORDER_LOCK" || true
|
|
801
|
+
fi
|
|
802
|
+
if [ "$ORCA_ABORT_CLEANUP" = 1 ]; then
|
|
803
|
+
ORCA_ABORT_CLEANUP=0
|
|
804
|
+
if [ -n "${ORCA_TERMINAL:-}" ]; then
|
|
805
|
+
fm_backend_kill orca "$ORCA_TERMINAL" 2>/dev/null || true
|
|
806
|
+
fi
|
|
807
|
+
if [ -n "${ORCA_WORKTREE_ID:-}" ]; then
|
|
808
|
+
if ! fm_backend_remove_worktree orca "$ORCA_WORKTREE_ID" 2>/dev/null; then
|
|
809
|
+
if [ "$SPAWN_FRESH_COMMIT_PENDING" = 1 ]; then
|
|
810
|
+
if ! spawn_fresh_commit_rollback; then
|
|
811
|
+
status=1
|
|
812
|
+
fi
|
|
813
|
+
SPAWN_FRESH_COMMIT_PENDING=0
|
|
814
|
+
fi
|
|
815
|
+
mkdir -p "$STATE" 2>/dev/null || true
|
|
816
|
+
if [ -d "$STATE" ]; then
|
|
817
|
+
SPAWN_META_TMP="$STATE/.$ID.meta.orca-recovery.${BASHPID:-$$}"
|
|
818
|
+
{
|
|
819
|
+
echo "window=$W"
|
|
820
|
+
echo "endpoint_task_id=$ID"
|
|
821
|
+
echo "cleanup_recovery=orca"
|
|
822
|
+
echo "worktree=${WT:-}"
|
|
823
|
+
echo "project=$PROJ_ABS"
|
|
824
|
+
echo "harness=$HARNESS"
|
|
825
|
+
echo "kind=$KIND"
|
|
826
|
+
[ -z "${MODE:-}" ] || echo "mode=$MODE"
|
|
827
|
+
[ -z "${YOLO:-}" ] || echo "yolo=$YOLO"
|
|
828
|
+
echo "tasktmp=${TASK_TMP:-}"
|
|
829
|
+
echo "model=${MODEL:-default}"
|
|
830
|
+
echo "effort=${EFFORT:-default}"
|
|
831
|
+
echo "backend=orca"
|
|
832
|
+
echo "orca_worktree_id=$ORCA_WORKTREE_ID"
|
|
833
|
+
[ -z "${ORCA_TERMINAL:-}" ] || echo "terminal=$ORCA_TERMINAL"
|
|
834
|
+
} > "$SPAWN_META_TMP" 2>/dev/null \
|
|
835
|
+
&& fm_backlog_atomic_transition publish "$SPAWN_META_TMP" "$STATE/$ID.meta" "task record" "$STATE" \
|
|
836
|
+
|| true
|
|
837
|
+
fi
|
|
838
|
+
fi
|
|
839
|
+
fi
|
|
840
|
+
fi
|
|
841
|
+
if [ "$SPAWN_TASK_LOCK_HELD" = 1 ]; then
|
|
842
|
+
SPAWN_TASK_LOCK_HELD=0
|
|
843
|
+
fm_lock_release "$SPAWN_TASK_LOCK" || true
|
|
844
|
+
fi
|
|
845
|
+
if [ "$SPAWN_FRESH_COMMIT_PENDING" = 1 ]; then
|
|
846
|
+
if ! spawn_fresh_commit_rollback; then
|
|
847
|
+
status=1
|
|
848
|
+
fi
|
|
849
|
+
fi
|
|
850
|
+
if [ "$SPAWN_META_LOCK_HELD" = 1 ]; then
|
|
851
|
+
SPAWN_META_LOCK_HELD=0
|
|
852
|
+
fm_lock_release "$SPAWN_META_LOCK" || true
|
|
853
|
+
fi
|
|
854
|
+
if [ "$SPAWN_TASK_SET_LOCK_HELD" = 1 ]; then
|
|
855
|
+
SPAWN_TASK_SET_LOCK_HELD=0
|
|
856
|
+
fm_lock_release "$SPAWN_TASK_SET_LOCK" || true
|
|
857
|
+
fi
|
|
858
|
+
if [ "$SPAWN_CONTROL_LOCK_HELD" = 1 ]; then
|
|
859
|
+
SPAWN_CONTROL_LOCK_HELD=0
|
|
860
|
+
fm_lock_release "$SPAWN_CONTROL_LOCK" || true
|
|
861
|
+
fi
|
|
862
|
+
[ -z "$SPAWN_META_TMP" ] || rm -f "$SPAWN_META_TMP" 2>/dev/null || true
|
|
863
|
+
if [ "$CONFIG_INHERIT_LOCK_HELD" = 1 ]; then
|
|
864
|
+
CONFIG_INHERIT_LOCK_HELD=0
|
|
865
|
+
fm_lock_release "$CONFIG_INHERIT_LOCK" || true
|
|
866
|
+
fi
|
|
867
|
+
return "$status"
|
|
868
|
+
}
|
|
869
|
+
trap spawn_abort_cleanup EXIT
|
|
870
|
+
|
|
871
|
+
# One bounded lock per live Herdr session/socket, shared across all homes.
|
|
872
|
+
# <session> is required so secondmate and primary spawns serialize against the
|
|
873
|
+
# same session without writing any other home's state directory.
|
|
874
|
+
spawn_herdr_presentation_order_lock_acquire() {
|
|
875
|
+
local session=${1:-} attempt lock_path
|
|
876
|
+
[ -n "$session" ] || session=$(fm_backend_herdr_session)
|
|
877
|
+
lock_path=$(fm_backend_herdr_presentation_session_lock_path "$session") || return 1
|
|
878
|
+
HERDR_PRESENTATION_ORDER_LOCK="$lock_path"
|
|
879
|
+
attempt=0
|
|
880
|
+
while [ "$attempt" -lt 50 ]; do
|
|
881
|
+
if fm_lock_try_acquire "$HERDR_PRESENTATION_ORDER_LOCK"; then
|
|
882
|
+
HERDR_PRESENTATION_ORDER_LOCK_HELD=1
|
|
883
|
+
return 0
|
|
884
|
+
fi
|
|
885
|
+
sleep 0.1
|
|
886
|
+
attempt=$((attempt + 1))
|
|
887
|
+
done
|
|
888
|
+
return 1
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
clear_relaunch_harness_wiring() {
|
|
892
|
+
local harness=$1 wt=$2 state=$3 id=$4 token_path token auth_path path
|
|
893
|
+
# The wiring arms above match on harness PREFIXES, because a task launched
|
|
894
|
+
# from a raw command records that command's basename rather than the exact
|
|
895
|
+
# adapter name. The retirement tables are keyed by the exact adapter, so the
|
|
896
|
+
# recorded value is resolved to its adapter first; otherwise a task recorded
|
|
897
|
+
# as, say, `grok-2` would have wiring armed and never retired. An
|
|
898
|
+
# unrecognized value resolves to no adapter, which is also the case in which
|
|
899
|
+
# no wiring was armed to begin with.
|
|
900
|
+
harness=$(fm_control_harness_family "$harness") || harness=
|
|
901
|
+
token_path=$(fm_control_harness_turnend_token_path "$harness" "$state" "$id") || return 1
|
|
902
|
+
token=
|
|
903
|
+
if [ -n "$token_path" ] && [ -f "$token_path" ]; then
|
|
904
|
+
IFS= read -r token < "$token_path" || [ -n "$token" ] || return 1
|
|
905
|
+
fi
|
|
906
|
+
auth_path=$(fm_control_harness_turnend_auth_path "$harness" "$token") || return 1
|
|
907
|
+
if [ -n "$auth_path" ]; then
|
|
908
|
+
rm -f -- "$auth_path" || return 1
|
|
909
|
+
fi
|
|
910
|
+
while IFS= read -r path; do
|
|
911
|
+
[ -n "$path" ] || continue
|
|
912
|
+
rm -f -- "$path" || return 1
|
|
913
|
+
done <<EOF
|
|
914
|
+
$(fm_control_harness_wiring_paths "$harness" "$wt" "$state" "$id")
|
|
915
|
+
EOF
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
spawn_herdr_presentation_order_lock_release() {
|
|
919
|
+
[ "$HERDR_PRESENTATION_ORDER_LOCK_HELD" = 1 ] || return 0
|
|
920
|
+
HERDR_PRESENTATION_ORDER_LOCK_HELD=0
|
|
921
|
+
fm_lock_release "$HERDR_PRESENTATION_ORDER_LOCK" || true
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
# Batch dispatch (see header): when the first positional is an `id=repo` pair, treat every
|
|
925
|
+
# positional as one and spawn each by re-execing this script in single-task mode. We use
|
|
926
|
+
# the FM_ROOT path (not $0) so it works whatever cwd or relative path invoked us, and reuse
|
|
927
|
+
# the single path verbatim. A failed pair is reported and skipped; the rest still launch;
|
|
928
|
+
# exit is non-zero if any pair failed. Single-task invocations never carry an '=' in arg
|
|
929
|
+
# one (task ids are bare slugs), so they fall straight through to the logic below.
|
|
930
|
+
idpart=${POS[0]:-}
|
|
931
|
+
idpart=${idpart%%=*}
|
|
932
|
+
if [ "$RELAUNCH" -eq 1 ] && [ "${#POS[@]}" -gt 0 ] && [ "${POS[0]}" != "$idpart" ]; then
|
|
933
|
+
echo "error: --relaunch is single-task only; relaunch each task explicitly" >&2
|
|
934
|
+
exit 1
|
|
935
|
+
fi
|
|
936
|
+
if [ "${#POS[@]}" -gt 0 ] && [ "${POS[0]}" != "$idpart" ] && case "$idpart" in */*) false ;; *) true ;; esac; then
|
|
937
|
+
if [ "$KIND" != secondmate ] && [ -z "$HARNESS_ARG" ] && [ -f "$CONFIG/crew-dispatch.json" ]; then
|
|
938
|
+
echo "error: config/crew-dispatch.json is active - pass an explicit harness resolved from the dispatch rules (the consultation backstop, so the rules are never silently skipped)." >&2
|
|
939
|
+
exit 1
|
|
940
|
+
fi
|
|
941
|
+
rc=0
|
|
942
|
+
shared_args=()
|
|
943
|
+
[ -z "$HARNESS_ARG" ] || shared_args+=(--harness "$HARNESS_ARG")
|
|
944
|
+
[ -z "$MODEL" ] || shared_args+=(--model "$MODEL")
|
|
945
|
+
[ -z "$EFFORT" ] || shared_args+=(--effort "$EFFORT")
|
|
946
|
+
[ -z "$BACKEND_ARG" ] || shared_args+=(--backend "$BACKEND_ARG")
|
|
947
|
+
# One delivery contract applies to every pair in a batch, exactly like the shared
|
|
948
|
+
# harness. Each pair still re-validates it against its own brief, so a batch
|
|
949
|
+
# spanning several modes is two invocations rather than a silent mixed dispatch.
|
|
950
|
+
[ "$MODE_SET" -eq 0 ] || shared_args+=(--mode "$MODE")
|
|
951
|
+
[ "$YOLO_SET" -eq 0 ] || shared_args+=(--yolo "$YOLO")
|
|
952
|
+
for pair in "${POS[@]}"; do
|
|
953
|
+
case "$pair" in
|
|
954
|
+
*=*) : ;;
|
|
955
|
+
*) echo "error: batch dispatch expects every argument as id=repo; got '$pair'" >&2; rc=2; continue ;;
|
|
956
|
+
esac
|
|
957
|
+
if [ "$KIND" = secondmate ]; then
|
|
958
|
+
echo "error: batch dispatch does not support --secondmate; spawn each secondmate explicitly" >&2
|
|
959
|
+
rc=2
|
|
960
|
+
continue
|
|
961
|
+
elif [ "$KIND" = scout ]; then
|
|
962
|
+
if FM_SPAWN_NO_GUARD=1 "$FM_ROOT/bin/fm-spawn.sh" "${pair%%=*}" "${pair#*=}" "${shared_args[@]+"${shared_args[@]}"}" --scout; then :; else echo "batch: FAILED to spawn ${pair%%=*} (${pair#*=})" >&2; rc=1; fi
|
|
963
|
+
else
|
|
964
|
+
if FM_SPAWN_NO_GUARD=1 "$FM_ROOT/bin/fm-spawn.sh" "${pair%%=*}" "${pair#*=}" "${shared_args[@]+"${shared_args[@]}"}"; then :; else echo "batch: FAILED to spawn ${pair%%=*} (${pair#*=})" >&2; rc=1; fi
|
|
965
|
+
fi
|
|
966
|
+
done
|
|
967
|
+
exit "$rc"
|
|
968
|
+
fi
|
|
969
|
+
ID=${POS[0]}
|
|
970
|
+
fm_task_id_creation_valid "$ID" || { echo "error: invalid task id" >&2; exit 2; }
|
|
971
|
+
if [ -e "$STATE" ] || [ -L "$STATE" ]; then
|
|
972
|
+
fm_backlog_directory_present "$STATE" "state directory" || {
|
|
973
|
+
echo "error: spawn refused: $FM_BACKLOG_TRANSITION_ERROR" >&2
|
|
974
|
+
exit 1
|
|
975
|
+
}
|
|
976
|
+
elif [ "$RELAUNCH" -eq 1 ]; then
|
|
977
|
+
echo "error: spawn refused: state directory does not exist at $STATE" >&2
|
|
978
|
+
exit 1
|
|
979
|
+
fi
|
|
980
|
+
# Role partition: spawning NEW work is MAIN-owned. A relaunch of an existing
|
|
981
|
+
# task is legitimate branch recovery (fm-control drives it through this same
|
|
982
|
+
# entrypoint), so only a fresh spawn refuses the branch actor (contract:
|
|
983
|
+
# bin/fm-lease-lib.sh; no-op in homes without a branch actor).
|
|
984
|
+
# shellcheck source=bin/fm-lease-lib.sh
|
|
985
|
+
. "$SCRIPT_DIR/fm-lease-lib.sh"
|
|
986
|
+
if [ "$RELAUNCH" -ne 1 ]; then
|
|
987
|
+
fm_lease_forbid_branch "new-task spawn (fm-spawn)"
|
|
988
|
+
fi
|
|
989
|
+
if [ "$RELAUNCH" -eq 1 ]; then
|
|
990
|
+
SPAWN_CONTROL_LOCK="$STATE/.control-$ID.lock"
|
|
991
|
+
control_owner=$(cat "$SPAWN_CONTROL_LOCK/pid" 2>/dev/null || true)
|
|
992
|
+
if [ "$control_owner" = "$PPID" ] && fm_pid_alive "$control_owner"; then
|
|
993
|
+
SPAWN_CONTROL_PARENT=1
|
|
994
|
+
elif [ "$(fm_lease_actor)" = branch ]; then
|
|
995
|
+
# Role partition refinement: branch recovery relaunches only through the
|
|
996
|
+
# fm-control transaction that owns the control lock, never by invoking
|
|
997
|
+
# this entrypoint directly (contract: bin/fm-lease-lib.sh).
|
|
998
|
+
echo "error: relaunch (fm-spawn) refused - the supervision branch must relaunch through fm-control" >&2
|
|
999
|
+
exit "$FM_LEASE_REFUSE_EXIT"
|
|
1000
|
+
elif fm_lock_try_acquire "$SPAWN_CONTROL_LOCK"; then
|
|
1001
|
+
SPAWN_CONTROL_LOCK_HELD=1
|
|
1002
|
+
else
|
|
1003
|
+
echo "error: another lifecycle action is already running for task $ID" >&2
|
|
1004
|
+
exit 1
|
|
1005
|
+
fi
|
|
1006
|
+
fi
|
|
1007
|
+
if [ "$RELAUNCH" -eq 0 ]; then
|
|
1008
|
+
mkdir -p "$STATE" || {
|
|
1009
|
+
echo "error: could not create parent state directory" >&2
|
|
1010
|
+
exit 1
|
|
1011
|
+
}
|
|
1012
|
+
fm_backlog_directory_present "$STATE" "state directory" || {
|
|
1013
|
+
echo "error: spawn refused: $FM_BACKLOG_TRANSITION_ERROR" >&2
|
|
1014
|
+
exit 1
|
|
1015
|
+
}
|
|
1016
|
+
# A FRESH spawn changes which tasks this home has, so it must not interleave
|
|
1017
|
+
# with a forced teardown that has already enumerated that set: a record
|
|
1018
|
+
# published inside the enumerate-then-remove window is invisible to the
|
|
1019
|
+
# teardown's per-task preflight but visible to its cleanup, and gets mutated
|
|
1020
|
+
# while never lifecycle-locked (bin/fm-wake-lib.sh's fm_task_set_lock_path
|
|
1021
|
+
# owns the evidence; bin/fm-teardown.sh holds the same lock from enumeration
|
|
1022
|
+
# through cleanup). Taken before this task's own locks, matching the
|
|
1023
|
+
# acquisition order documented there, and held through publication.
|
|
1024
|
+
#
|
|
1025
|
+
# A relaunch is exempt: it republishes a task that already exists, so it is
|
|
1026
|
+
# already covered by that task's control lock, which the teardown preflight
|
|
1027
|
+
# tests.
|
|
1028
|
+
#
|
|
1029
|
+
# Refusing rather than waiting is the fail-closed direction: the home may be
|
|
1030
|
+
# moments from removal, so there is nothing worth waiting for.
|
|
1031
|
+
SPAWN_TASK_SET_LOCK=$(fm_task_set_lock_path "$STATE") || {
|
|
1032
|
+
echo "error: could not resolve the task-set lock for $STATE" >&2
|
|
1033
|
+
exit 1
|
|
1034
|
+
}
|
|
1035
|
+
if ! fm_lock_try_acquire "$SPAWN_TASK_SET_LOCK"; then
|
|
1036
|
+
echo "error: this home's task set is locked by another operation (a forced teardown is enumerating or removing its tasks); refusing to create task $ID rather than racing it" >&2
|
|
1037
|
+
exit 1
|
|
1038
|
+
fi
|
|
1039
|
+
SPAWN_TASK_SET_LOCK_HELD=1
|
|
1040
|
+
fi
|
|
1041
|
+
if [ "$KIND" = secondmate ]; then
|
|
1042
|
+
if spawn_remote_secondmate "$ID"; then
|
|
1043
|
+
exit 0
|
|
1044
|
+
else
|
|
1045
|
+
remote_spawn_rc=$?
|
|
1046
|
+
fi
|
|
1047
|
+
[ "$remote_spawn_rc" -eq 3 ] || exit "$remote_spawn_rc"
|
|
1048
|
+
fi
|
|
1049
|
+
# Backend selection (data/fm-backend-design-d7): explicit --backend, else
|
|
1050
|
+
# FM_BACKEND env, else config/backend, else runtime auto-detection, else
|
|
1051
|
+
# default tmux (fm_backend_name). fm_backend_validate_spawn refuses unknown or
|
|
1052
|
+
# non-spawn-capable backends. The resolved value is
|
|
1053
|
+
# recorded in meta only when it is NOT tmux (fm-teardown.sh and fm-watch.sh's
|
|
1054
|
+
# window_backend/fm_backend_of_meta already treat an absent backend= as tmux),
|
|
1055
|
+
# so the default path's meta stays byte-identical.
|
|
1056
|
+
if [ "$RELAUNCH" -eq 0 ]; then
|
|
1057
|
+
if [ "$BACKEND_SET" -eq 1 ]; then
|
|
1058
|
+
BACKEND=$BACKEND_ARG
|
|
1059
|
+
else
|
|
1060
|
+
BACKEND=$(fm_backend_name)
|
|
1061
|
+
fi
|
|
1062
|
+
fm_backend_validate_spawn "$BACKEND" || exit 1
|
|
1063
|
+
fm_backend_source "$BACKEND" || exit 1
|
|
1064
|
+
if [ "$BACKEND" = orca ] && [ "$KIND" = secondmate ]; then
|
|
1065
|
+
echo "error: backend=orca does not support --secondmate spawns yet" >&2
|
|
1066
|
+
exit 1
|
|
1067
|
+
fi
|
|
1068
|
+
if [ "$BACKEND" = cmux ] && [ "$KIND" = secondmate ]; then
|
|
1069
|
+
echo "error: backend=cmux does not support --secondmate spawns yet" >&2
|
|
1070
|
+
exit 1
|
|
1071
|
+
fi
|
|
1072
|
+
if [ "$BACKEND" = orca ]; then
|
|
1073
|
+
fm_backend_orca_runtime_check || exit 1
|
|
1074
|
+
fi
|
|
1075
|
+
fi
|
|
1076
|
+
SPAWN_TASK_LOCK="$STATE/.spawn-$ID.lock"
|
|
1077
|
+
if ! fm_lock_try_acquire "$SPAWN_TASK_LOCK"; then
|
|
1078
|
+
echo "error: another spawn is already creating task $ID" >&2
|
|
1079
|
+
exit 1
|
|
1080
|
+
fi
|
|
1081
|
+
SPAWN_TASK_LOCK_HELD=1
|
|
1082
|
+
PROJ=
|
|
1083
|
+
ARG3=
|
|
1084
|
+
FIRSTMATE_HOME=
|
|
1085
|
+
|
|
1086
|
+
# --relaunch adoption: every identity axis comes from the task's own validated
|
|
1087
|
+
# durable record, never from the command line, so a relaunch can only ever
|
|
1088
|
+
# re-launch the task it names. The endpoint identity check is the same shared
|
|
1089
|
+
# validation teardown uses, so a malformed, ambiguous, or foreign record
|
|
1090
|
+
# refuses here exactly as it refuses there.
|
|
1091
|
+
RELAUNCH_PRIOR_HARNESS=
|
|
1092
|
+
if [ "$RELAUNCH" -eq 1 ]; then
|
|
1093
|
+
[ "${#POS[@]}" -eq 1 ] || {
|
|
1094
|
+
echo "error: --relaunch takes the task id only; its project or home comes from the task's own record" >&2
|
|
1095
|
+
exit 1
|
|
1096
|
+
}
|
|
1097
|
+
RELAUNCH_META="$STATE/$ID.meta"
|
|
1098
|
+
if [ ! -e "$RELAUNCH_META" ] && [ ! -L "$RELAUNCH_META" ]; then
|
|
1099
|
+
echo "error: --relaunch needs an existing task record; no $RELAUNCH_META" >&2
|
|
1100
|
+
exit 1
|
|
1101
|
+
fi
|
|
1102
|
+
fm_backlog_record_present "$RELAUNCH_META" "task record" "$STATE" || {
|
|
1103
|
+
echo "error: --relaunch refused: $FM_BACKLOG_TRANSITION_ERROR" >&2
|
|
1104
|
+
exit 1
|
|
1105
|
+
}
|
|
1106
|
+
SPAWN_META_LOCK=$(fm_meta_lock_path "$RELAUNCH_META") || exit 1
|
|
1107
|
+
fm_lock_acquire_wait "$SPAWN_META_LOCK"
|
|
1108
|
+
SPAWN_META_LOCK_HELD=1
|
|
1109
|
+
fm_backlog_record_present "$RELAUNCH_META" "task record" "$STATE" || {
|
|
1110
|
+
echo "error: --relaunch refused after locking: $FM_BACKLOG_TRANSITION_ERROR" >&2
|
|
1111
|
+
exit 1
|
|
1112
|
+
}
|
|
1113
|
+
fm_backend_validate_task_endpoint "$RELAUNCH_META" "$ID" || exit 1
|
|
1114
|
+
BACKEND=$FM_BACKEND_VALIDATED_BACKEND
|
|
1115
|
+
RELAUNCH_TARGET=$FM_BACKEND_VALIDATED_TARGET
|
|
1116
|
+
fm_backend_validate_spawn "$BACKEND" || exit 1
|
|
1117
|
+
fm_backend_source "$BACKEND" || exit 1
|
|
1118
|
+
# A relaunch must PROVE the previous agent is gone before it launches another
|
|
1119
|
+
# one into the same endpoint, and only tmux and herdr have a recovery-grade
|
|
1120
|
+
# classifier that can (bin/fm-control-lib.sh owns that capability table).
|
|
1121
|
+
fm_control_backend_state_verified "$BACKEND" || {
|
|
1122
|
+
echo "error: backend '$BACKEND' has no recovery-grade agent-state classifier, so a relaunch cannot prove the previous agent exited; refusing rather than risking two agents in one endpoint" >&2
|
|
1123
|
+
exit 1
|
|
1124
|
+
}
|
|
1125
|
+
RELAUNCH_STATE=$(fm_backend_agent_state "$BACKEND" "$RELAUNCH_TARGET")
|
|
1126
|
+
[ "$RELAUNCH_STATE" = dead ] || {
|
|
1127
|
+
echo "error: task $ID's endpoint reads '$RELAUNCH_STATE'; a relaunch requires a positively agent-free endpoint (stop the agent first with bin/fm-control.sh $ID exit)" >&2
|
|
1128
|
+
exit 1
|
|
1129
|
+
}
|
|
1130
|
+
RELAUNCH_PRIOR_HARNESS=$(fm_meta_get "$RELAUNCH_META" harness)
|
|
1131
|
+
KIND=$(fm_meta_get "$RELAUNCH_META" kind)
|
|
1132
|
+
[ -n "$KIND" ] || KIND=ship
|
|
1133
|
+
MODE=$(fm_meta_get "$RELAUNCH_META" mode)
|
|
1134
|
+
YOLO=$(fm_meta_get "$RELAUNCH_META" yolo)
|
|
1135
|
+
RELAUNCH_WT=$(fm_meta_get "$RELAUNCH_META" worktree)
|
|
1136
|
+
[ -n "$RELAUNCH_WT" ] && [ -d "$RELAUNCH_WT" ] || {
|
|
1137
|
+
echo "error: task $ID's recorded worktree '${RELAUNCH_WT:-none}' is missing; refusing to relaunch without the local copy its work lives in" >&2
|
|
1138
|
+
exit 1
|
|
1139
|
+
}
|
|
1140
|
+
if [ "$KIND" = secondmate ]; then
|
|
1141
|
+
FIRSTMATE_HOME=$(fm_meta_get "$RELAUNCH_META" home)
|
|
1142
|
+
[ -n "$FIRSTMATE_HOME" ] || FIRSTMATE_HOME=$RELAUNCH_WT
|
|
1143
|
+
else
|
|
1144
|
+
PROJ=$(fm_meta_get "$RELAUNCH_META" project)
|
|
1145
|
+
[ -n "$PROJ" ] || {
|
|
1146
|
+
echo "error: task $ID has no recorded project; refusing to relaunch" >&2
|
|
1147
|
+
exit 1
|
|
1148
|
+
}
|
|
1149
|
+
fi
|
|
1150
|
+
if [ "$BACKEND" = herdr ]; then
|
|
1151
|
+
HERDR_SES=$(fm_meta_get "$RELAUNCH_META" herdr_session)
|
|
1152
|
+
HERDR_WORKSPACE_ID=$(fm_meta_get "$RELAUNCH_META" herdr_workspace_id)
|
|
1153
|
+
HERDR_TAB_ID=$(fm_meta_get "$RELAUNCH_META" herdr_tab_id)
|
|
1154
|
+
HERDR_PANE_ID=$(fm_meta_get "$RELAUNCH_META" herdr_pane_id)
|
|
1155
|
+
fi
|
|
1156
|
+
# With no explicit harness, a relaunch reuses the harness already recorded
|
|
1157
|
+
# for this task. It must NOT fall through to the fresh-spawn config
|
|
1158
|
+
# resolution, which would silently move an existing task onto whatever the
|
|
1159
|
+
# crew or secondmate default currently says. Choosing a different harness is
|
|
1160
|
+
# the caller's explicit decision, made with --harness (bin/fm-control.sh
|
|
1161
|
+
# resolves that decision, including a secondmate's durable pin).
|
|
1162
|
+
ARG3=${HARNESS_ARG:-$RELAUNCH_PRIOR_HARNESS}
|
|
1163
|
+
[ -n "$ARG3" ] || {
|
|
1164
|
+
echo "error: task $ID has no recorded harness; pass --harness to relaunch it" >&2
|
|
1165
|
+
exit 1
|
|
1166
|
+
}
|
|
1167
|
+
elif [ "$KIND" = secondmate ]; then
|
|
1168
|
+
case "${POS[1]:-}" in
|
|
1169
|
+
''|claude|codex|opencode|pi|pi-signed|grok|kimi|cursor|muse)
|
|
1170
|
+
ARG3=${POS[1]:-}
|
|
1171
|
+
;;
|
|
1172
|
+
*' '*)
|
|
1173
|
+
if [ "${#POS[@]}" -gt 2 ] || [ -d "${POS[1]}" ]; then
|
|
1174
|
+
FIRSTMATE_HOME=${POS[1]}
|
|
1175
|
+
ARG3=${POS[2]:-}
|
|
1176
|
+
else
|
|
1177
|
+
ARG3=${POS[1]}
|
|
1178
|
+
fi
|
|
1179
|
+
;;
|
|
1180
|
+
*)
|
|
1181
|
+
FIRSTMATE_HOME=${POS[1]}
|
|
1182
|
+
ARG3=${POS[2]:-}
|
|
1183
|
+
;;
|
|
1184
|
+
esac
|
|
1185
|
+
else
|
|
1186
|
+
PROJ=${POS[1]}
|
|
1187
|
+
ARG3=${POS[2]:-}
|
|
1188
|
+
fi
|
|
1189
|
+
[ -z "$HARNESS_ARG" ] || ARG3=$HARNESS_ARG
|
|
1190
|
+
|
|
1191
|
+
shell_quote() {
|
|
1192
|
+
printf "'"
|
|
1193
|
+
printf '%s' "$1" | sed "s/'/'\\\\''/g"
|
|
1194
|
+
printf "'"
|
|
1195
|
+
}
|
|
1196
|
+
|
|
1197
|
+
resolve_pi_executable() {
|
|
1198
|
+
local candidate dir
|
|
1199
|
+
candidate=$(type -P -- "$1" 2>/dev/null) || return 1
|
|
1200
|
+
[ -x "$candidate" ] || return 1
|
|
1201
|
+
case "$candidate" in
|
|
1202
|
+
/*) printf '%s\n' "$candidate" ;;
|
|
1203
|
+
*)
|
|
1204
|
+
dir=$(cd "$(dirname "$candidate")" 2>/dev/null && pwd -P) || return 1
|
|
1205
|
+
printf '%s/%s\n' "$dir" "$(basename "$candidate")"
|
|
1206
|
+
;;
|
|
1207
|
+
esac
|
|
1208
|
+
}
|
|
1209
|
+
|
|
1210
|
+
# Pi's CLI surface is version-dependent, so probe the resolved executable's help
|
|
1211
|
+
# before composing the optional regular-TUI flag. An absent or inconclusive probe
|
|
1212
|
+
# omits the flag so older Pi versions can still spawn.
|
|
1213
|
+
pi_supports_tui_mode() {
|
|
1214
|
+
local executable=$1 help
|
|
1215
|
+
help=$("$executable" --help 2>&1) || return 1
|
|
1216
|
+
printf '%s\n' "$help" | grep -Eq -- '(^|[[:space:]])--tui-mode([[:space:]=]|$)'
|
|
1217
|
+
}
|
|
1218
|
+
|
|
1219
|
+
# The verified launch command per adapter. The knowledge half of each adapter
|
|
1220
|
+
# (busy-state source, exit command, dialogs, quirks) lives in the harness-adapters skill.
|
|
1221
|
+
launch_template() {
|
|
1222
|
+
local harness=$1 kind=${2:-ship}
|
|
1223
|
+
# shellcheck disable=SC2016 # single quotes are deliberate: $(cat ...) expands in the crewmate pane, not here
|
|
1224
|
+
case "$harness" in
|
|
1225
|
+
# CLAUDE_CODE_ENABLE_PROMPT_SUGGESTION=false disables claude's interactive
|
|
1226
|
+
# predicted-next-prompt ghost text, which renders as dim/faint text inside an
|
|
1227
|
+
# otherwise-empty composer and would otherwise read like real typed input when
|
|
1228
|
+
# firstmate captures the pane (see the harness-adapters skill). It is a per-launch env
|
|
1229
|
+
# prefix scoped to this firstmate-launched agent; it never touches the captain's
|
|
1230
|
+
# global config. The CLI's --prompt-suggestions flag is print/SDK-mode only and
|
|
1231
|
+
# does NOT suppress the interactive ghost text (verified empirically), so the env
|
|
1232
|
+
# var is the correct control. The dim-aware composer reader in fm-tmux-lib.sh is
|
|
1233
|
+
# the defense-in-depth backstop for any pane this flag cannot reach.
|
|
1234
|
+
claude) printf '%s' 'CLAUDE_CODE_ENABLE_PROMPT_SUGGESTION=false claude --dangerously-skip-permissions __MODELFLAG____EFFORTFLAG__"$(__OPINPUT__ encode launch-brief < __BRIEF__)"' ;;
|
|
1235
|
+
codex)
|
|
1236
|
+
if [ "$kind" = secondmate ]; then
|
|
1237
|
+
printf '%s' 'codex __MODELFLAG____EFFORTFLAG__--dangerously-bypass-approvals-and-sandbox "$(__OPINPUT__ encode launch-brief < __BRIEF__)"'
|
|
1238
|
+
else
|
|
1239
|
+
printf '%s' 'codex __MODELFLAG____EFFORTFLAG__--dangerously-bypass-approvals-and-sandbox -c "notify=[\"bash\",\"-c\",\"touch __TURNEND__\"]" "$(__OPINPUT__ encode launch-brief < __BRIEF__)"'
|
|
1240
|
+
fi
|
|
1241
|
+
;;
|
|
1242
|
+
opencode) printf '%s' 'OPENCODE_CONFIG_CONTENT='\''{"permission":{"*":"allow"}}'\'' opencode __MODELFLAG__--prompt "$(__OPINPUT__ encode launch-brief < __BRIEF__)"' ;;
|
|
1243
|
+
pi|pi-signed)
|
|
1244
|
+
printf '%s' '__PIBIN____PITUIMODE__'
|
|
1245
|
+
if [ "$kind" = secondmate ]; then
|
|
1246
|
+
printf '%s' ' __MODELFLAG____EFFORTFLAG__-e __PITURNEND__ -e __PIWATCH__ "$(__OPINPUT__ encode launch-brief < __BRIEF__)"'
|
|
1247
|
+
else
|
|
1248
|
+
printf '%s' ' __MODELFLAG____EFFORTFLAG__-e __PIEXT__ "$(__OPINPUT__ encode launch-brief < __BRIEF__)"'
|
|
1249
|
+
fi
|
|
1250
|
+
;;
|
|
1251
|
+
# grok (Grok Build TUI): a positional prompt starts the supervised interactive
|
|
1252
|
+
# session. --always-approve auto-approves every tool execution (verified: the
|
|
1253
|
+
# crewmate runs fully autonomously, no permission gate), which an unattended
|
|
1254
|
+
# crewmate needs; it is the targeted equivalent of claude's
|
|
1255
|
+
# --dangerously-skip-permissions. grok's turn-end signal does NOT ride the
|
|
1256
|
+
# launch command - it is a Stop-event hook installed below (global hook +
|
|
1257
|
+
# per-task pointer), so the template is identical for ship/scout/secondmate.
|
|
1258
|
+
grok) printf '%s' 'grok --always-approve __MODELFLAG____EFFORTFLAG__"$(__OPINPUT__ encode launch-brief < __BRIEF__)"' ;;
|
|
1259
|
+
# Cursor Agent CLI. --trust suppresses the workspace-trust prompt, which
|
|
1260
|
+
# --yolo does NOT cover and which would otherwise block every spawn, since
|
|
1261
|
+
# each task gets a fresh worktree path cursor has never seen. --yolo is the
|
|
1262
|
+
# --force alias whose TUI label is "Run Everything". --workspace pins the
|
|
1263
|
+
# exact worktree. -w/--worktree is deliberately never passed: it allocates a
|
|
1264
|
+
# SECOND worktree under ~/.cursor/worktrees and would break firstmate's
|
|
1265
|
+
# isolation contract. The binary is resolved rather than named because
|
|
1266
|
+
# `cursor` is not the CLI (the installed names are cursor-agent and the
|
|
1267
|
+
# legacy alias agent), and the foreign primary markers are cleared so an
|
|
1268
|
+
# inherited CLAUDECODE cannot outrank cursor's own marker in a process that
|
|
1269
|
+
# only reads the environment. Cursor exposes no effort flag, so the shared
|
|
1270
|
+
# effort axis is deliberately omitted and stays in task metadata only.
|
|
1271
|
+
cursor) printf '%s' 'env -u CLAUDECODE -u PI_CODING_AGENT -u GROK_AGENT -u FM_PI_HARNESS -u CURSOR_INVOKED_AS __CURSORBIN__ --trust --yolo __MODELFLAG__--workspace __WORKTREE__ "$(__OPINPUT__ encode launch-brief < __BRIEF__)"' ;;
|
|
1272
|
+
# Kimi Code rejects a positional prompt, so it launches bare and receives
|
|
1273
|
+
# only an absolute brief pointer after the TUI readiness gate below.
|
|
1274
|
+
# Its turn-end signal is a globally configured Stop hook plus a guarded
|
|
1275
|
+
# per-task worktree token, so no launch placeholder belongs here.
|
|
1276
|
+
kimi) printf '%s' '__KIMIBIN__ __MODELFLAG__--auto' ;;
|
|
1277
|
+
# muse (Muse Code): a positional prompt starts the supervised interactive
|
|
1278
|
+
# session. --yolo is the single flag that makes a crewmate pane viable: muse
|
|
1279
|
+
# ships approval prompts AND a filesystem/network sandbox ON by default
|
|
1280
|
+
# (--sandbox-network defaults to proxy-only, which refuses outright without a
|
|
1281
|
+
# managed proxy), and it gates a fresh workspace behind a trust dialog. One
|
|
1282
|
+
# --yolo disables approval, disables the sandbox so git and network work, and
|
|
1283
|
+
# trusts the workspace for the run, so no dialog appears on the fresh
|
|
1284
|
+
# per-task worktree (verified, muse 0.1.0-R708.1).
|
|
1285
|
+
# MUSE_EXPERIMENTAL_FOREIGN_PERSONAL_CONTEXT_KILL=on is the privacy control:
|
|
1286
|
+
# muse otherwise loads the OPERATOR's foreign personal rules from ~/.claude
|
|
1287
|
+
# into every run and ships them to Meta-hosted inference, even under an
|
|
1288
|
+
# isolated XDG_CONFIG_HOME. exec mode's --no-foreign-personal-context flag is
|
|
1289
|
+
# NOT accepted by the interactive TUI (it exits with "unexpected argument"),
|
|
1290
|
+
# so this env var is the only control that reaches a pane worker. Verified to
|
|
1291
|
+
# drop the foreign rules_file context block while KEEPING the project's own
|
|
1292
|
+
# AGENTS.md rules, which the crewmate contract depends on.
|
|
1293
|
+
# muse's turn-end signal rides neither the launch command nor a hook: its
|
|
1294
|
+
# plugin engine is off in the default build, so firstmate folds muse's own
|
|
1295
|
+
# session event log instead (bin/fm-busy-lib.sh), bound by the sidecar
|
|
1296
|
+
# written below. Nothing to place in the template for it.
|
|
1297
|
+
# codex, opencode, and kimi are also markerless and share this inherited-marker hazard; changing their verified launch boundaries belongs in follow-up work.
|
|
1298
|
+
muse) printf '%s' 'env -u CLAUDECODE -u PI_CODING_AGENT -u GROK_AGENT -u FM_PI_HARNESS XDG_CONFIG_HOME=__MUSECONFIG__ XDG_DATA_HOME=__MUSEDATA__ MUSE_EXPERIMENTAL_FOREIGN_PERSONAL_CONTEXT_KILL=on __MUSEBIN__ --yolo __MODELFLAG____EFFORTFLAG__"$(__OPINPUT__ encode launch-brief < __BRIEF__)"' ;;
|
|
1299
|
+
*) return 1 ;;
|
|
1300
|
+
esac
|
|
1301
|
+
}
|
|
1302
|
+
|
|
1303
|
+
case "$ARG3" in
|
|
1304
|
+
*' '*) # raw launch command (unverified-adapter escape hatch)
|
|
1305
|
+
LAUNCH=$ARG3
|
|
1306
|
+
HARNESS=""
|
|
1307
|
+
for word in $LAUNCH; do
|
|
1308
|
+
case "$word" in [A-Za-z_]*=*) continue ;; *) HARNESS=$(basename "$word"); break ;; esac
|
|
1309
|
+
done
|
|
1310
|
+
;;
|
|
1311
|
+
'')
|
|
1312
|
+
# No explicit harness: resolve from config. A secondmate AGENT launches on the
|
|
1313
|
+
# secondmate harness (config/secondmate-harness -> config/crew-harness -> own);
|
|
1314
|
+
# every other kind uses the crew harness only when no dispatch profile file is
|
|
1315
|
+
# active. Resolving here on every spawn is what makes the split DURABLE - a
|
|
1316
|
+
# respawn (recovery, /updatefirstmate, restart) re-resolves, so
|
|
1317
|
+
# config/secondmate-harness keeps governing secondmate launches across restarts.
|
|
1318
|
+
# The launch_template lookup below is the unverified-adapter guard for both
|
|
1319
|
+
# kinds: a harness with no template aborts the spawn.
|
|
1320
|
+
if [ "$KIND" = secondmate ]; then
|
|
1321
|
+
HARNESS=$("$FM_ROOT/bin/fm-harness.sh" secondmate)
|
|
1322
|
+
harness_src='config/secondmate-harness (falling back to config/crew-harness)'
|
|
1323
|
+
else
|
|
1324
|
+
if [ -f "$CONFIG/crew-dispatch.json" ]; then
|
|
1325
|
+
echo "error: config/crew-dispatch.json is active - pass an explicit harness resolved from the dispatch rules (the consultation backstop, so the rules are never silently skipped)." >&2
|
|
1326
|
+
exit 1
|
|
1327
|
+
fi
|
|
1328
|
+
HARNESS=$("$FM_ROOT/bin/fm-harness.sh" crew)
|
|
1329
|
+
harness_src='config/crew-harness'
|
|
1330
|
+
fi
|
|
1331
|
+
LAUNCH=$(launch_template "$HARNESS" "$KIND") || { echo "error: no launch template for harness '$HARNESS' (from $harness_src or detection); pass a raw launch command to use an unverified adapter" >&2; exit 1; }
|
|
1332
|
+
;;
|
|
1333
|
+
*)
|
|
1334
|
+
HARNESS=$ARG3
|
|
1335
|
+
LAUNCH=$(launch_template "$HARNESS" "$KIND") || { echo "error: unknown harness '$HARNESS'; pass a raw launch command to use an unverified adapter" >&2; exit 1; }
|
|
1336
|
+
;;
|
|
1337
|
+
esac
|
|
1338
|
+
|
|
1339
|
+
# muse is verified as a CREWMATE/SCOUT adapter only. A secondmate is a firstmate
|
|
1340
|
+
# instance, so it needs a primary supervision protocol; muse has none, and its
|
|
1341
|
+
# Claude-compatible hook dialect explicitly rejects the model-reawakening and
|
|
1342
|
+
# asyncRewake handlers that firstmate's primary turn-end supervision is built on
|
|
1343
|
+
# (muse 0.1.0-R708.1). Refusing here keeps that gap loud instead of standing up a
|
|
1344
|
+
# secondmate whose supervision cycle could never be armed.
|
|
1345
|
+
if [ "$KIND" = secondmate ] && [ "$HARNESS" = muse ]; then
|
|
1346
|
+
echo "error: muse is a verified crewmate/scout adapter only and cannot run a secondmate; it has no primary supervision protocol. Select a harness verified for secondmates." >&2
|
|
1347
|
+
exit 1
|
|
1348
|
+
fi
|
|
1349
|
+
|
|
1350
|
+
case "$HARNESS" in
|
|
1351
|
+
pi|pi-signed)
|
|
1352
|
+
PI_BIN=$(resolve_pi_executable "$HARNESS") || {
|
|
1353
|
+
echo "error: $HARNESS executable not found on PATH; install it or select a different verified harness" >&2
|
|
1354
|
+
exit 1
|
|
1355
|
+
}
|
|
1356
|
+
PI_TUI_MODE=
|
|
1357
|
+
if pi_supports_tui_mode "$PI_BIN"; then
|
|
1358
|
+
PI_TUI_MODE=' --tui-mode regular'
|
|
1359
|
+
fi
|
|
1360
|
+
LAUNCH=${LAUNCH//__PITUIMODE__/$PI_TUI_MODE}
|
|
1361
|
+
LAUNCH="FM_PI_HARNESS=$HARNESS $LAUNCH"
|
|
1362
|
+
;;
|
|
1363
|
+
cursor)
|
|
1364
|
+
# `cursor` is not the CLI name, and the legacy alias `agent` is far too
|
|
1365
|
+
# generic to launch on its name alone, so resolution runs through the
|
|
1366
|
+
# verified owner rather than a bare command lookup. Refusing here keeps a
|
|
1367
|
+
# missing install a loud spawn refusal instead of a pane that dies with a
|
|
1368
|
+
# command-not-found the supervisor would read as a wedged worker.
|
|
1369
|
+
CURSOR_BIN=$(fm_cursor_resolve_binary) || exit 1
|
|
1370
|
+
if [ -n "$MODEL" ] && [ "$MODEL" != default ]; then
|
|
1371
|
+
if CURSOR_MODELS=$(fm_cursor_list_models "$CURSOR_BIN"); then
|
|
1372
|
+
if ! printf '%s\n' "$CURSOR_MODELS" | fm_cursor_catalog_has_model "$MODEL"; then
|
|
1373
|
+
echo "error: Cursor model '$MODEL' is not available from '$CURSOR_BIN --list-models'; choose an id listed by that command or omit --model" >&2
|
|
1374
|
+
exit 1
|
|
1375
|
+
fi
|
|
1376
|
+
fi
|
|
1377
|
+
fi
|
|
1378
|
+
;;
|
|
1379
|
+
esac
|
|
1380
|
+
|
|
1381
|
+
# config/secondmate-harness may carry optional model/effort tokens alongside the
|
|
1382
|
+
# harness ("<harness> [<model>] [<effort>]"). They apply only when this is a
|
|
1383
|
+
# --secondmate spawn and no explicit per-spawn harness/raw launch was supplied, so
|
|
1384
|
+
# the harness itself came from the secondmate config fallback chain. Resolving
|
|
1385
|
+
# here on every spawn makes the pin durable across respawns. Precedence: explicit
|
|
1386
|
+
# --model/--effort flags still win over the file's tokens.
|
|
1387
|
+
if [ "$KIND" = secondmate ] && [ -z "$ARG3" ]; then
|
|
1388
|
+
if [ "$MODEL_SET" -eq 0 ]; then
|
|
1389
|
+
SM_MODEL=$("$SCRIPT_DIR/fm-harness.sh" secondmate-model)
|
|
1390
|
+
[ -z "$SM_MODEL" ] || MODEL=$SM_MODEL
|
|
1391
|
+
fi
|
|
1392
|
+
if [ "$EFFORT_SET" -eq 0 ]; then
|
|
1393
|
+
SM_EFFORT=$("$SCRIPT_DIR/fm-harness.sh" secondmate-effort)
|
|
1394
|
+
if [ -n "$SM_EFFORT" ]; then
|
|
1395
|
+
case "$SM_EFFORT" in
|
|
1396
|
+
low|medium|high|xhigh|max) EFFORT=$SM_EFFORT ;;
|
|
1397
|
+
*) echo "warning: config/secondmate-harness effort token '$SM_EFFORT' is not one of low, medium, high, xhigh, max; ignoring" >&2 ;;
|
|
1398
|
+
esac
|
|
1399
|
+
fi
|
|
1400
|
+
fi
|
|
1401
|
+
fi
|
|
1402
|
+
|
|
1403
|
+
secondmate_registry_value() {
|
|
1404
|
+
secondmate_registry_field "$DATA/secondmates.md" "$1" "$2"
|
|
1405
|
+
}
|
|
1406
|
+
|
|
1407
|
+
resolve_kimi_binary() {
|
|
1408
|
+
local candidate dir fallback
|
|
1409
|
+
candidate=$(command -v kimi 2>/dev/null || true)
|
|
1410
|
+
if [ -n "$candidate" ] && [ -x "$candidate" ]; then
|
|
1411
|
+
case "$candidate" in
|
|
1412
|
+
/*) printf '%s\n' "$candidate"; return 0 ;;
|
|
1413
|
+
*)
|
|
1414
|
+
dir=$(cd "$(dirname "$candidate")" 2>/dev/null && pwd -P) || dir=
|
|
1415
|
+
if [ -n "$dir" ]; then
|
|
1416
|
+
printf '%s/%s\n' "$dir" "$(basename "$candidate")"
|
|
1417
|
+
return 0
|
|
1418
|
+
fi
|
|
1419
|
+
;;
|
|
1420
|
+
esac
|
|
1421
|
+
fi
|
|
1422
|
+
fallback="${HOME:-}/.kimi-code/bin/kimi"
|
|
1423
|
+
if [ -n "${HOME:-}" ] && [ -x "$fallback" ]; then
|
|
1424
|
+
printf '%s\n' "$fallback"
|
|
1425
|
+
return 0
|
|
1426
|
+
fi
|
|
1427
|
+
echo "error: kimi executable not found; searched PATH for 'kimi' and fallback '$fallback'" >&2
|
|
1428
|
+
return 1
|
|
1429
|
+
}
|
|
1430
|
+
|
|
1431
|
+
resolve_muse_binary() {
|
|
1432
|
+
local candidate dir
|
|
1433
|
+
candidate=$(command -v muse 2>/dev/null || true)
|
|
1434
|
+
if [ -n "$candidate" ] && [ -x "$candidate" ]; then
|
|
1435
|
+
case "$candidate" in
|
|
1436
|
+
/*) printf '%s\n' "$candidate"; return 0 ;;
|
|
1437
|
+
*)
|
|
1438
|
+
dir=$(cd "$(dirname "$candidate")" 2>/dev/null && pwd -P) || dir=
|
|
1439
|
+
if [ -n "$dir" ]; then
|
|
1440
|
+
printf '%s/%s\n' "$dir" "$(basename "$candidate")"
|
|
1441
|
+
return 0
|
|
1442
|
+
fi
|
|
1443
|
+
;;
|
|
1444
|
+
esac
|
|
1445
|
+
fi
|
|
1446
|
+
echo "error: muse executable not found on PATH; install Muse Code or select a different verified harness" >&2
|
|
1447
|
+
return 1
|
|
1448
|
+
}
|
|
1449
|
+
|
|
1450
|
+
# muse_credential_present: 0 when a launched muse pane can reach its provider
|
|
1451
|
+
# without an interactive login. muse offers exactly two credential paths
|
|
1452
|
+
# (verified, muse 0.1.0-R708.1): the META_API_KEY environment variable, which
|
|
1453
|
+
# always takes priority, and a stored credential written by `muse auth set` or
|
|
1454
|
+
# `muse login` into <config>/muse/auth.json. This is a PREFLIGHT rather than a
|
|
1455
|
+
# rendered-screen check because an unauthenticated pane does not exit - it sits
|
|
1456
|
+
# on an OAuth device-code prompt ("Sign in at this page ... Waiting for
|
|
1457
|
+
# approval...") waiting for a human who is not there, which would look to
|
|
1458
|
+
# supervision like a wedged worker rather than a missing credential.
|
|
1459
|
+
muse_worker_meta_api_key_present() {
|
|
1460
|
+
local session worker_env
|
|
1461
|
+
[ "$BACKEND" = tmux ] || return 1
|
|
1462
|
+
if [ -n "${TMUX:-}" ]; then
|
|
1463
|
+
session=$(tmux display-message -p '#S' 2>/dev/null) || return 1
|
|
1464
|
+
else
|
|
1465
|
+
tmux has-session -t firstmate 2>/dev/null || return 1
|
|
1466
|
+
session=firstmate
|
|
1467
|
+
fi
|
|
1468
|
+
worker_env=$(tmux show-environment -t "$session" META_API_KEY 2>/dev/null) || return 1
|
|
1469
|
+
case "$worker_env" in
|
|
1470
|
+
META_API_KEY=?*) return 0 ;;
|
|
1471
|
+
esac
|
|
1472
|
+
return 1
|
|
1473
|
+
}
|
|
1474
|
+
|
|
1475
|
+
muse_credential_present() {
|
|
1476
|
+
local auth=$1
|
|
1477
|
+
[ -s "$auth" ] || muse_worker_meta_api_key_present
|
|
1478
|
+
}
|
|
1479
|
+
|
|
1480
|
+
model_flag_for_harness() {
|
|
1481
|
+
local harness=$1 model=$2
|
|
1482
|
+
[ -n "$model" ] && [ "$model" != default ] || return 0
|
|
1483
|
+
case "$harness" in
|
|
1484
|
+
claude|codex|opencode|pi|pi-signed|grok|kimi|cursor|muse)
|
|
1485
|
+
printf -- '--model %s ' "$(shell_quote "$model")"
|
|
1486
|
+
;;
|
|
1487
|
+
esac
|
|
1488
|
+
}
|
|
1489
|
+
|
|
1490
|
+
effort_flag_for_harness() {
|
|
1491
|
+
local harness=$1 effort=$2
|
|
1492
|
+
[ -n "$effort" ] && [ "$effort" != default ] || return 0
|
|
1493
|
+
case "$harness" in
|
|
1494
|
+
claude)
|
|
1495
|
+
case "$effort" in
|
|
1496
|
+
low|medium|high|xhigh|max) printf -- '--effort %s ' "$(shell_quote "$effort")" ;;
|
|
1497
|
+
esac
|
|
1498
|
+
;;
|
|
1499
|
+
codex)
|
|
1500
|
+
# The installed codex config schema uses model_reasoning_effort, and the
|
|
1501
|
+
# bundled model catalog advertises low|medium|high|xhigh. Omit max rather
|
|
1502
|
+
# than passing an unsupported value.
|
|
1503
|
+
case "$effort" in
|
|
1504
|
+
low|medium|high|xhigh) printf -- '-c %s ' "$(shell_quote "model_reasoning_effort=\"$effort\"")" ;;
|
|
1505
|
+
esac
|
|
1506
|
+
;;
|
|
1507
|
+
grok)
|
|
1508
|
+
# grok exposes both --effort and --reasoning-effort; firstmate's profile
|
|
1509
|
+
# axis is the reasoning knob. As of grok 0.2.99, --reasoning-effort accepts
|
|
1510
|
+
# only low|medium|high and rejects both xhigh and max, so omit those rather
|
|
1511
|
+
# than passing a known-bad value.
|
|
1512
|
+
case "$effort" in
|
|
1513
|
+
low|medium|high) printf -- '--reasoning-effort %s ' "$(shell_quote "$effort")" ;;
|
|
1514
|
+
esac
|
|
1515
|
+
;;
|
|
1516
|
+
pi|pi-signed)
|
|
1517
|
+
# Pi 0.80.6 accepts the full shared effort vocabulary, including max, through
|
|
1518
|
+
# its --thinking flag.
|
|
1519
|
+
case "$effort" in
|
|
1520
|
+
low|medium|high|xhigh|max) printf -- '--thinking %s ' "$(shell_quote "$effort")" ;;
|
|
1521
|
+
esac
|
|
1522
|
+
;;
|
|
1523
|
+
muse)
|
|
1524
|
+
# muse 0.1.0-R708.1 --reasoning-effort accepts none|minimal|low|medium|
|
|
1525
|
+
# high|xhigh|ultra and defaults to high, so low..xhigh map straight across.
|
|
1526
|
+
# ultra is muse's max-CLASS level, so firstmate's max maps onto it - but
|
|
1527
|
+
# only ever as an EXPLICIT captain choice, never as a fallback, because
|
|
1528
|
+
# AGENTS.md section 4 forbids selecting max without captain preference and
|
|
1529
|
+
# the omitted effort here leaves muse on its own high default. muse's extra
|
|
1530
|
+
# none/minimal levels sit below firstmate's shared vocabulary and are
|
|
1531
|
+
# deliberately unreachable rather than remapped onto low.
|
|
1532
|
+
case "$effort" in
|
|
1533
|
+
low|medium|high|xhigh) printf -- '--reasoning-effort %s ' "$(shell_quote "$effort")" ;;
|
|
1534
|
+
max) printf -- '--reasoning-effort %s ' "$(shell_quote ultra)" ;;
|
|
1535
|
+
esac
|
|
1536
|
+
;;
|
|
1537
|
+
# opencode's interactive `opencode --prompt` launch has a verified --model
|
|
1538
|
+
# flag but no verified effort flag. Its `opencode run --variant` flag belongs
|
|
1539
|
+
# to a different, non-interactive launch mode, so fm-spawn does not pass it.
|
|
1540
|
+
# kimi likewise has no reasoning-effort flag; the requested axis stays in
|
|
1541
|
+
# task metadata but never reaches the launch command. Cursor encodes effort
|
|
1542
|
+
# in model ids such as cursor-grok-4.5-high, so it also receives no separate
|
|
1543
|
+
# effort flag.
|
|
1544
|
+
esac
|
|
1545
|
+
}
|
|
1546
|
+
|
|
1547
|
+
case "$LAUNCH" in
|
|
1548
|
+
*__MUSEBIN__*)
|
|
1549
|
+
MUSE_BIN=$(resolve_muse_binary) || exit 1
|
|
1550
|
+
MUSE_CONFIG_HOME=$(resolve_directory_input XDG_CONFIG_HOME "${XDG_CONFIG_HOME:-${HOME:-}/.config}") || exit 1
|
|
1551
|
+
MUSE_DATA_HOME=$(resolve_directory_input XDG_DATA_HOME "${XDG_DATA_HOME:-${HOME:-}/.local/share}") || exit 1
|
|
1552
|
+
MUSE_AUTH_FILE="$MUSE_CONFIG_HOME/muse/auth.json"
|
|
1553
|
+
if ! muse_credential_present "$MUSE_AUTH_FILE"; then
|
|
1554
|
+
if [ -n "${META_API_KEY:-}" ]; then
|
|
1555
|
+
echo "error: muse has no worker-reachable credential; META_API_KEY is set for fm-spawn but cannot be proven present in the $BACKEND worker environment. Store the fleet credential at '$MUSE_AUTH_FILE' with 'muse login' or 'muse auth set --api-key-stdin'. The secret will not be copied into the launch command." >&2
|
|
1556
|
+
else
|
|
1557
|
+
echo "error: muse has no worker-reachable credential; META_API_KEY cannot be proven present in the $BACKEND worker environment and '$MUSE_AUTH_FILE' is absent or empty. Store the fleet credential with 'muse login' or 'muse auth set --api-key-stdin'." >&2
|
|
1558
|
+
fi
|
|
1559
|
+
exit 1
|
|
1560
|
+
fi
|
|
1561
|
+
LAUNCH=${LAUNCH//__MUSEBIN__/$(shell_quote "$MUSE_BIN")}
|
|
1562
|
+
LAUNCH=${LAUNCH//__MUSECONFIG__/$(shell_quote "$MUSE_CONFIG_HOME")}
|
|
1563
|
+
LAUNCH=${LAUNCH//__MUSEDATA__/$(shell_quote "$MUSE_DATA_HOME")}
|
|
1564
|
+
;;
|
|
1565
|
+
esac
|
|
1566
|
+
|
|
1567
|
+
case "$LAUNCH" in
|
|
1568
|
+
*__KIMIBIN__*)
|
|
1569
|
+
KIMI_BIN=$(resolve_kimi_binary) || exit 1
|
|
1570
|
+
LAUNCH=${LAUNCH//__KIMIBIN__/$(shell_quote "$KIMI_BIN")}
|
|
1571
|
+
if [ "$KIND" != secondmate ]; then
|
|
1572
|
+
"$FM_ROOT/bin/fm-kimi-turnend-hook.sh" install || {
|
|
1573
|
+
echo "error: refusing Kimi spawn because the global turn-end hook could not be installed safely" >&2
|
|
1574
|
+
exit 1
|
|
1575
|
+
}
|
|
1576
|
+
fi
|
|
1577
|
+
;;
|
|
1578
|
+
esac
|
|
1579
|
+
|
|
1580
|
+
json_escape() {
|
|
1581
|
+
printf '%s' "$1" | sed 's/\\/\\\\/g; s/"/\\"/g'
|
|
1582
|
+
}
|
|
1583
|
+
|
|
1584
|
+
resolved_existing_dir() {
|
|
1585
|
+
local path=$1
|
|
1586
|
+
[ -d "$path" ] || { echo "error: firstmate home does not exist or is not a directory: $path" >&2; return 1; }
|
|
1587
|
+
cd "$path" && pwd -P
|
|
1588
|
+
}
|
|
1589
|
+
|
|
1590
|
+
resolve_project_dir_arg() {
|
|
1591
|
+
local path=$1
|
|
1592
|
+
case "$path" in
|
|
1593
|
+
projects/*) printf '%s/%s\n' "$PROJECTS" "${path#projects/}" ;;
|
|
1594
|
+
*) printf '%s\n' "$path" ;;
|
|
1595
|
+
esac
|
|
1596
|
+
}
|
|
1597
|
+
|
|
1598
|
+
path_is_ancestor_of() {
|
|
1599
|
+
local ancestor=$1 path=$2
|
|
1600
|
+
[ -n "$ancestor" ] || return 1
|
|
1601
|
+
[ -n "$path" ] || return 1
|
|
1602
|
+
[ "$ancestor" != "$path" ] || return 1
|
|
1603
|
+
case "$path" in
|
|
1604
|
+
"$ancestor"/*) return 0 ;;
|
|
1605
|
+
esac
|
|
1606
|
+
return 1
|
|
1607
|
+
}
|
|
1608
|
+
|
|
1609
|
+
validate_firstmate_home_for_spawn() {
|
|
1610
|
+
local id=$1 home=$2 abs_home abs_active_home abs_root marker_id
|
|
1611
|
+
abs_home=$(resolved_existing_dir "$home") || return 1
|
|
1612
|
+
abs_active_home=$(resolved_existing_dir "$FM_HOME")
|
|
1613
|
+
abs_root=$(resolved_existing_dir "$FM_ROOT")
|
|
1614
|
+
if [ "$abs_home" = "/" ]; then
|
|
1615
|
+
echo "error: secondmate home cannot be the filesystem root: $home" >&2
|
|
1616
|
+
return 1
|
|
1617
|
+
fi
|
|
1618
|
+
if [ "$abs_home" = "$abs_active_home" ]; then
|
|
1619
|
+
echo "error: secondmate home cannot be the active firstmate home: $home" >&2
|
|
1620
|
+
return 1
|
|
1621
|
+
fi
|
|
1622
|
+
if [ "$abs_home" = "$abs_root" ]; then
|
|
1623
|
+
echo "error: secondmate home cannot be the firstmate repo: $home" >&2
|
|
1624
|
+
return 1
|
|
1625
|
+
fi
|
|
1626
|
+
if path_is_ancestor_of "$abs_active_home" "$abs_home"; then
|
|
1627
|
+
echo "error: secondmate home cannot be inside the active firstmate home: $home" >&2
|
|
1628
|
+
return 1
|
|
1629
|
+
fi
|
|
1630
|
+
if path_is_ancestor_of "$abs_root" "$abs_home"; then
|
|
1631
|
+
echo "error: secondmate home cannot be inside the firstmate repo: $home" >&2
|
|
1632
|
+
return 1
|
|
1633
|
+
fi
|
|
1634
|
+
if path_is_ancestor_of "$abs_home" "$abs_active_home"; then
|
|
1635
|
+
echo "error: secondmate home cannot be an ancestor of the active firstmate home: $home" >&2
|
|
1636
|
+
return 1
|
|
1637
|
+
fi
|
|
1638
|
+
if path_is_ancestor_of "$abs_home" "$abs_root"; then
|
|
1639
|
+
echo "error: secondmate home cannot be an ancestor of the firstmate repo: $home" >&2
|
|
1640
|
+
return 1
|
|
1641
|
+
fi
|
|
1642
|
+
validate_firstmate_operational_dirs "$abs_home" "$abs_active_home" "$abs_root" || return 1
|
|
1643
|
+
if [ ! -f "$abs_home/$SUB_HOME_MARKER" ]; then
|
|
1644
|
+
echo "error: firstmate home $home is not a seeded secondmate home" >&2
|
|
1645
|
+
return 1
|
|
1646
|
+
fi
|
|
1647
|
+
marker_id=$(cat "$abs_home/$SUB_HOME_MARKER" 2>/dev/null || true)
|
|
1648
|
+
if [ "$marker_id" != "$id" ]; then
|
|
1649
|
+
echo "error: firstmate home $home is marked for secondmate ${marker_id:-unknown}, expected $id" >&2
|
|
1650
|
+
return 1
|
|
1651
|
+
fi
|
|
1652
|
+
if [ ! -f "$abs_home/AGENTS.md" ]; then
|
|
1653
|
+
echo "error: $home is not a firstmate home (missing AGENTS.md)" >&2
|
|
1654
|
+
return 1
|
|
1655
|
+
fi
|
|
1656
|
+
if [ ! -d "$abs_home/bin" ]; then
|
|
1657
|
+
echo "error: $home is not a firstmate home (missing bin/)" >&2
|
|
1658
|
+
return 1
|
|
1659
|
+
fi
|
|
1660
|
+
printf '%s\n' "$abs_home"
|
|
1661
|
+
}
|
|
1662
|
+
|
|
1663
|
+
validate_firstmate_operational_dirs() {
|
|
1664
|
+
local abs_home=$1 abs_active_home=$2 abs_root=$3 name dir abs_dir
|
|
1665
|
+
for name in data state config projects; do
|
|
1666
|
+
dir="$abs_home/$name"
|
|
1667
|
+
if [ -L "$dir" ] && [ ! -e "$dir" ]; then
|
|
1668
|
+
echo "error: secondmate $name directory must resolve inside the secondmate home: $dir" >&2
|
|
1669
|
+
return 1
|
|
1670
|
+
fi
|
|
1671
|
+
if [ -d "$dir" ]; then
|
|
1672
|
+
abs_dir=$(cd "$dir" && pwd -P)
|
|
1673
|
+
elif [ -e "$dir" ]; then
|
|
1674
|
+
echo "error: secondmate $name path is not a directory: $dir" >&2
|
|
1675
|
+
return 1
|
|
1676
|
+
else
|
|
1677
|
+
abs_dir="$abs_home/$name"
|
|
1678
|
+
fi
|
|
1679
|
+
if ! path_is_ancestor_of "$abs_home" "$abs_dir"; then
|
|
1680
|
+
echo "error: secondmate $name directory must resolve inside the secondmate home: $dir" >&2
|
|
1681
|
+
return 1
|
|
1682
|
+
fi
|
|
1683
|
+
if [ "$abs_dir" = "$abs_active_home" ] || path_is_ancestor_of "$abs_active_home" "$abs_dir"; then
|
|
1684
|
+
echo "error: secondmate $name directory cannot be inside the active firstmate home: $dir" >&2
|
|
1685
|
+
return 1
|
|
1686
|
+
fi
|
|
1687
|
+
if [ "$abs_dir" = "$abs_root" ] || path_is_ancestor_of "$abs_root" "$abs_dir"; then
|
|
1688
|
+
echo "error: secondmate $name directory cannot be inside the firstmate repo: $dir" >&2
|
|
1689
|
+
return 1
|
|
1690
|
+
fi
|
|
1691
|
+
done
|
|
1692
|
+
}
|
|
1693
|
+
|
|
1694
|
+
if [ "$KIND" = secondmate ]; then
|
|
1695
|
+
if [ -z "$FIRSTMATE_HOME" ] && { [ -e "$STATE/$ID.meta" ] || [ -L "$STATE/$ID.meta" ]; }; then
|
|
1696
|
+
fm_backlog_record_present "$STATE/$ID.meta" "task record" "$STATE" || {
|
|
1697
|
+
echo "error: secondmate task record is unsafe: $FM_BACKLOG_TRANSITION_ERROR" >&2
|
|
1698
|
+
exit 1
|
|
1699
|
+
}
|
|
1700
|
+
FIRSTMATE_HOME=$(grep '^home=' "$STATE/$ID.meta" | cut -d= -f2- || true)
|
|
1701
|
+
fi
|
|
1702
|
+
if [ -z "$FIRSTMATE_HOME" ]; then
|
|
1703
|
+
FIRSTMATE_HOME=$(secondmate_registry_value "$ID" home || true)
|
|
1704
|
+
fi
|
|
1705
|
+
fi
|
|
1706
|
+
|
|
1707
|
+
if [ "$KIND" = secondmate ]; then
|
|
1708
|
+
[ -n "$FIRSTMATE_HOME" ] || { echo "error: no firstmate home supplied or registered for $ID" >&2; exit 1; }
|
|
1709
|
+
PROJ_ABS=$(validate_firstmate_home_for_spawn "$ID" "$FIRSTMATE_HOME")
|
|
1710
|
+
if [ -e "$DATA/secondmates.md" ] || [ -L "$DATA/secondmates.md" ]; then
|
|
1711
|
+
if ! secondmate_registry_validate_bindings "$DATA/secondmates.md" resolve_path "$ID" "$FIRSTMATE_HOME"; then
|
|
1712
|
+
echo "error: $SECONDMATE_REGISTRY_ERROR" >&2
|
|
1713
|
+
exit 1
|
|
1714
|
+
fi
|
|
1715
|
+
SECONDMATE_PROJECTS=$SECONDMATE_REGISTRY_MATCH_PROJECTS
|
|
1716
|
+
fi
|
|
1717
|
+
WT="$PROJ_ABS"
|
|
1718
|
+
# Local-HEAD sync: before launch, fast-forward this secondmate's worktree to the
|
|
1719
|
+
# PRIMARY checkout's current default-branch commit, so a freshly spawned or
|
|
1720
|
+
# recovery-respawned secondmate always runs the primary's version (AGENTS.md
|
|
1721
|
+
# spawn section). Purely local - no fetch: the home is a worktree of this same
|
|
1722
|
+
# repo and already holds the commit. ff-only and guarded; a dirty, diverged, or
|
|
1723
|
+
# wrong-branch home is left untouched and launches as-is. The agent re-reads
|
|
1724
|
+
# AGENTS.md fresh on launch, so no nudge is needed here.
|
|
1725
|
+
if sm_primary_head=$(primary_head_commit "$FM_ROOT"); then
|
|
1726
|
+
sm_ff_out=$(ff_target "$PROJ_ABS" "secondmate $ID" "$sm_primary_head" yes yes 2>&1 || true)
|
|
1727
|
+
case "$sm_ff_out" in
|
|
1728
|
+
*': skipped:'*)
|
|
1729
|
+
sm_ff_line=$(first_line "$sm_ff_out")
|
|
1730
|
+
sm_ff_prefix="secondmate $ID: skipped: "
|
|
1731
|
+
sm_ff_reason=${sm_ff_line#"$sm_ff_prefix"}
|
|
1732
|
+
echo "warning: secondmate $ID sync skipped before launch: $sm_ff_reason" >&2
|
|
1733
|
+
;;
|
|
1734
|
+
esac
|
|
1735
|
+
else
|
|
1736
|
+
echo "warning: secondmate $ID sync skipped before launch: primary default-branch commit cannot be resolved" >&2
|
|
1737
|
+
fi
|
|
1738
|
+
mkdir -p "$PROJ_ABS/state" || {
|
|
1739
|
+
echo "error: could not create secondmate state directory for $PROJ_ABS" >&2
|
|
1740
|
+
exit 1
|
|
1741
|
+
}
|
|
1742
|
+
if [ "${FM_SKIP_SECONDMATE_INHERIT:-0}" != 1 ]; then
|
|
1743
|
+
CONFIG_INHERIT_LOCK=$(fm_config_inherit_lock_path "$PROJ_ABS") || {
|
|
1744
|
+
echo "error: could not resolve secondmate inheritance lock for $PROJ_ABS" >&2
|
|
1745
|
+
exit 1
|
|
1746
|
+
}
|
|
1747
|
+
if ! fm_lock_acquire_wait "$CONFIG_INHERIT_LOCK"; then
|
|
1748
|
+
echo "error: could not acquire secondmate inheritance lock for $PROJ_ABS" >&2
|
|
1749
|
+
exit 1
|
|
1750
|
+
fi
|
|
1751
|
+
CONFIG_INHERIT_LOCK_HELD=1
|
|
1752
|
+
# Inheritance propagation: push the primary-authoritative live-safe local inheritance
|
|
1753
|
+
# surface into this secondmate home (fm-config-inherit-lib.sh).
|
|
1754
|
+
FM_CONFIG_INHERIT_LIVE=1 \
|
|
1755
|
+
propagate_secondmate_inheritance "$FM_HOME" "$PROJ_ABS" "$CONFIG" "$DATA" \
|
|
1756
|
+
|| echo "warning: secondmate $ID inheritance failed for $PROJ_ABS" >&2
|
|
1757
|
+
fi
|
|
1758
|
+
if [ -f "$PROJ_ABS/data/charter.md" ]; then
|
|
1759
|
+
BRIEF="$PROJ_ABS/data/charter.md"
|
|
1760
|
+
else
|
|
1761
|
+
BRIEF="$DATA/$ID/brief.md"
|
|
1762
|
+
fi
|
|
1763
|
+
else
|
|
1764
|
+
PROJ_ABS="$(cd "$(resolve_project_dir_arg "$PROJ")" && pwd)"
|
|
1765
|
+
WT=""
|
|
1766
|
+
BRIEF="$DATA/$ID/brief.md"
|
|
1767
|
+
fi
|
|
1768
|
+
[ -f "$BRIEF" ] || { echo "error: task $ID has no brief at inaccessible data path $BRIEF" >&2; exit 1; }
|
|
1769
|
+
|
|
1770
|
+
delivery_rigor_rank() { # <mode> -> 3 (most rigor) .. 1 (least); 0 = not a task mode
|
|
1771
|
+
case "$1" in
|
|
1772
|
+
no-mistakes) echo 3 ;;
|
|
1773
|
+
direct-PR) echo 2 ;;
|
|
1774
|
+
local-only) echo 1 ;;
|
|
1775
|
+
*) echo 0 ;;
|
|
1776
|
+
esac
|
|
1777
|
+
}
|
|
1778
|
+
|
|
1779
|
+
# Brief/spawn delivery agreement, checked before any endpoint exists.
|
|
1780
|
+
# fm-brief.sh records a ship brief's mode as a fixed "Delivery contract: mode=<mode>"
|
|
1781
|
+
# line. A spawn that disagrees would launch a worker whose instructions and whose
|
|
1782
|
+
# recorded task delivery differ, which is the exact drift this contract prevents.
|
|
1783
|
+
if [ "$KIND" = ship ]; then
|
|
1784
|
+
PROJ_NAME=$(basename "$PROJ_ABS")
|
|
1785
|
+
BRIEF_MODE=$(sed -n 's/^Delivery contract: mode=\([^ ]*\).*$/\1/p' "$BRIEF" | head -n 1)
|
|
1786
|
+
if [ -z "$BRIEF_MODE" ]; then
|
|
1787
|
+
echo "warning: $BRIEF records no delivery contract line (scaffolded before ship briefs recorded one); launching on the explicit --mode $MODE - confirm its definition of done matches" >&2
|
|
1788
|
+
elif [ "$BRIEF_MODE" != "$MODE" ]; then
|
|
1789
|
+
echo "error: delivery mismatch for $ID: the brief says mode=$BRIEF_MODE but this spawn passed --mode $MODE; correct the flag or re-scaffold the brief so the worker's instructions and the task record agree" >&2
|
|
1790
|
+
exit 1
|
|
1791
|
+
fi
|
|
1792
|
+
# The registry holds the captain's standing posture, so dropping below it is
|
|
1793
|
+
# allowed (a current explicit captain instruction wins) but never silent. An
|
|
1794
|
+
# unregistered project resolves to the same no-mistakes standing default, which
|
|
1795
|
+
# is why the notice names the standing posture rather than the registry line. A
|
|
1796
|
+
# conditional policy is excluded: both of its legs are legitimate classifications.
|
|
1797
|
+
STANDING_MODE=$("$FM_ROOT/bin/fm-project-mode.sh" --raw "$PROJ_NAME" 2>/dev/null | cut -d' ' -f1) || STANDING_MODE=
|
|
1798
|
+
if [ -n "$STANDING_MODE" ] && [ "$STANDING_MODE" != no-mistakes-prod-only ] \
|
|
1799
|
+
&& [ "$(delivery_rigor_rank "$MODE")" -lt "$(delivery_rigor_rank "$STANDING_MODE")" ]; then
|
|
1800
|
+
echo "notice: $ID ships mode=$MODE while the standing posture for $PROJ_NAME is $STANDING_MODE - less rigor than the captain's standing posture; proceed only on a current explicit captain instruction or an intake judgment you can state" >&2
|
|
1801
|
+
fi
|
|
1802
|
+
fi
|
|
1803
|
+
|
|
1804
|
+
BRIEF_DIR_REAL=$(cd "$(dirname "$BRIEF")" && pwd -P)
|
|
1805
|
+
BRIEF_REAL="$BRIEF_DIR_REAL/$(basename "$BRIEF")"
|
|
1806
|
+
|
|
1807
|
+
# PROJ_ABS can still carry a symlinked path component (e.g. macOS's /tmp ->
|
|
1808
|
+
# /private/tmp) when it came from the ship/scout branch's logical `pwd` above.
|
|
1809
|
+
# Every backend's own current-path read (tmux's pane_current_path, herdr's
|
|
1810
|
+
# foreground_cwd, zellij/cmux's active pwd probe against the live shell) can
|
|
1811
|
+
# report the OS-level, physically-resolved cwd, so comparing it against a
|
|
1812
|
+
# still-symlinked PROJ_ABS can misfire both ways: false-negative (the poll
|
|
1813
|
+
# below never notices the pane left the project) or false-positive (the
|
|
1814
|
+
# isolation guard refuses a spawn that never actually tangled). Canonicalize
|
|
1815
|
+
# once here so every downstream comparison uses the same physical form
|
|
1816
|
+
# (docs/herdr-backend.md "Known gaps").
|
|
1817
|
+
PROJ_ABS_REAL=$(cd "$PROJ_ABS" 2>/dev/null && pwd -P) || PROJ_ABS_REAL="$PROJ_ABS"
|
|
1818
|
+
|
|
1819
|
+
real_path_or_raw() { # <path>
|
|
1820
|
+
local path=$1 real
|
|
1821
|
+
if real=$(cd "$path" 2>/dev/null && pwd -P); then
|
|
1822
|
+
printf '%s\n' "$real"
|
|
1823
|
+
else
|
|
1824
|
+
printf '%s\n' "$path"
|
|
1825
|
+
fi
|
|
1826
|
+
}
|
|
1827
|
+
|
|
1828
|
+
# Session-provider container-ensure + task creation. tmux stays exactly as P1
|
|
1829
|
+
# left it (same session-name / new-window sequence, see bin/backends/tmux.sh);
|
|
1830
|
+
# a herdr spawn goes through the version-gated, workspace-per-HOME,
|
|
1831
|
+
# tab-per-task sequence in bin/backends/herdr.sh instead (D4/D5 as refined by
|
|
1832
|
+
# docs/herdr-backend.md's "workspace-per-home" pass, AGENTS.md task
|
|
1833
|
+
# herdr-sm-spaces-k4). Both branches converge on the same $T ("target") string
|
|
1834
|
+
# that every downstream operation (send/capture/kill) already treats as opaque
|
|
1835
|
+
# per-backend routing (fm_backend_resolve_selector).
|
|
1836
|
+
validate_spawn_worktree() { # <source> <inspect-target>
|
|
1837
|
+
local source=$1 inspect_target=$2 wt_real proj_real wt_top wt_top_real
|
|
1838
|
+
wt_real=
|
|
1839
|
+
if ! wt_real=$(cd "$WT" 2>/dev/null && pwd -P); then
|
|
1840
|
+
wt_real=
|
|
1841
|
+
fi
|
|
1842
|
+
proj_real=$PROJ_ABS_REAL
|
|
1843
|
+
wt_top=$(git -C "$WT" rev-parse --show-toplevel 2>/dev/null || true)
|
|
1844
|
+
wt_top_real=
|
|
1845
|
+
if ! wt_top_real=$(cd "$wt_top" 2>/dev/null && pwd -P); then
|
|
1846
|
+
wt_top_real=
|
|
1847
|
+
fi
|
|
1848
|
+
if [ -z "$wt_real" ] || [ -z "$wt_top_real" ] || [ "$wt_real" != "$wt_top_real" ] || [ "$wt_real" = "$proj_real" ]; then
|
|
1849
|
+
echo "error: $source did not yield an isolated worktree (resolved '$WT'; worktree root '${wt_top:-none}'; primary '$PROJ_ABS'); refusing to launch to avoid tangling the primary checkout. Inspect target $inspect_target" >&2
|
|
1850
|
+
exit 1
|
|
1851
|
+
fi
|
|
1852
|
+
}
|
|
1853
|
+
|
|
1854
|
+
# A pooled slot whose only deviation is a submodule gitlink is stale, not dirty:
|
|
1855
|
+
# an earlier refresh moved the superproject and left the submodule checkout on
|
|
1856
|
+
# the pin the previous base recorded. The refusal still stands and this gate
|
|
1857
|
+
# never touches the slot; it only names the cause, because "is not clean" while
|
|
1858
|
+
# the operator's own `git status` reads clean gives neither a cause nor a remedy.
|
|
1859
|
+
# A pin is only reported as stale when the commit the slot holds is already
|
|
1860
|
+
# contained in one of the submodule's remotes. Anything that cannot be proven
|
|
1861
|
+
# contained - an unpushed commit, a submodule with no remote, a git error - falls
|
|
1862
|
+
# through to the conservative uncommitted-work refusal, as does any entry that is
|
|
1863
|
+
# not exactly a clean submodule sitting on a different pin. The diagnosis is
|
|
1864
|
+
# buffered and only emitted once every entry qualifies, so it can never
|
|
1865
|
+
# contradict the verdict.
|
|
1866
|
+
#
|
|
1867
|
+
# No remedy command is printed, deliberately. That containment check reads local
|
|
1868
|
+
# refs only and never fetches, because this gate has to stay usable offline. A
|
|
1869
|
+
# remote-tracking ref that has gone stale - its upstream branch deleted or
|
|
1870
|
+
# force-pushed, and never pruned - therefore still reads as containment, so a
|
|
1871
|
+
# commit that is really unpushed can look contained. Naming the submodule and both
|
|
1872
|
+
# pins is what the operator actually needs; printing a checkout command on a
|
|
1873
|
+
# judgement that can be fooled could cost them that commit, so the remedy is left
|
|
1874
|
+
# to the operator, who can see the whole picture.
|
|
1875
|
+
describe_stale_submodule_pins() { # <worktree> <status>
|
|
1876
|
+
local worktree=$1 status=$2 line path want have unpushed lines=
|
|
1877
|
+
while IFS= read -r line; do
|
|
1878
|
+
[ -n "$line" ] || continue
|
|
1879
|
+
case $line in ' M '*) path=${line#' M '} ;; *) return 1 ;; esac
|
|
1880
|
+
[ "$(git -C "$worktree" ls-files --stage -- "$path" 2>/dev/null | cut -c1-6)" = 160000 ] || return 1
|
|
1881
|
+
[ -z "$(git -C "$worktree/$path" status --porcelain 2>/dev/null)" ] || return 1
|
|
1882
|
+
want=$(git -C "$worktree" rev-parse --verify --quiet "HEAD:$path" 2>/dev/null) || return 1
|
|
1883
|
+
have=$(git -C "$worktree/$path" rev-parse --verify --quiet HEAD 2>/dev/null) || return 1
|
|
1884
|
+
[ "$want" != "$have" ] || return 1
|
|
1885
|
+
unpushed=$(git -C "$worktree/$path" log --format=%H --max-count=1 "$have" --not --remotes -- 2>/dev/null) || return 1
|
|
1886
|
+
[ -z "$unpushed" ] || return 1
|
|
1887
|
+
lines+="error: submodule '$path' is checked out at $have, but this base records $want"$'\n'
|
|
1888
|
+
done <<EOF
|
|
1889
|
+
$status
|
|
1890
|
+
EOF
|
|
1891
|
+
[ -n "$lines" ] || return 1
|
|
1892
|
+
printf '%s' "$lines" >&2
|
|
1893
|
+
}
|
|
1894
|
+
|
|
1895
|
+
freshen_spawn_worktree_base() { # <worktree>
|
|
1896
|
+
local worktree=$1 default target expected actual status
|
|
1897
|
+
if ! git -C "$worktree" fetch --quiet origin; then
|
|
1898
|
+
echo "error: could not fetch origin for pooled worktree '$worktree'; refusing to launch from a potentially stale base" >&2
|
|
1899
|
+
return 1
|
|
1900
|
+
fi
|
|
1901
|
+
if ! git -C "$worktree" remote set-head origin --auto >/dev/null 2>&1; then
|
|
1902
|
+
echo "error: could not resolve origin's current default branch for pooled worktree '$worktree'; refusing to launch from a potentially stale base" >&2
|
|
1903
|
+
return 1
|
|
1904
|
+
fi
|
|
1905
|
+
default=$(default_branch "$worktree") || {
|
|
1906
|
+
echo "error: could not determine origin's default branch for pooled worktree '$worktree'; refusing to launch from a potentially stale base" >&2
|
|
1907
|
+
return 1
|
|
1908
|
+
}
|
|
1909
|
+
target="origin/$default"
|
|
1910
|
+
if ! git -C "$worktree" fetch --quiet origin "+refs/heads/$default:refs/remotes/origin/$default"; then
|
|
1911
|
+
echo "error: could not fetch '$target' for pooled worktree '$worktree'; refusing to launch from a potentially stale base" >&2
|
|
1912
|
+
return 1
|
|
1913
|
+
fi
|
|
1914
|
+
expected=$(git -C "$worktree" rev-parse --verify --quiet "$target^{commit}" 2>/dev/null) || {
|
|
1915
|
+
echo "error: '$target' is not a commit for pooled worktree '$worktree'; refusing to launch from a potentially stale base" >&2
|
|
1916
|
+
return 1
|
|
1917
|
+
}
|
|
1918
|
+
status=$(git -C "$worktree" -c core.quotePath=false status --porcelain) || {
|
|
1919
|
+
echo "error: could not inspect pooled worktree '$worktree' before refreshing its base" >&2
|
|
1920
|
+
return 1
|
|
1921
|
+
}
|
|
1922
|
+
if [ -n "$status" ]; then
|
|
1923
|
+
if describe_stale_submodule_pins "$worktree" "$status"; then
|
|
1924
|
+
echo "error: pooled worktree '$worktree' has a stale submodule checkout, not uncommitted work; refusing to launch and leaving it untouched" >&2
|
|
1925
|
+
else
|
|
1926
|
+
echo "error: pooled worktree '$worktree' is not clean; refusing to discard uncommitted work while refreshing its base" >&2
|
|
1927
|
+
fi
|
|
1928
|
+
return 1
|
|
1929
|
+
fi
|
|
1930
|
+
if ! git -C "$worktree" reset --hard "$target" >/dev/null; then
|
|
1931
|
+
echo "error: could not reset pooled worktree '$worktree' to '$target'; refusing to launch from a potentially stale base" >&2
|
|
1932
|
+
return 1
|
|
1933
|
+
fi
|
|
1934
|
+
actual=$(git -C "$worktree" rev-parse --verify --quiet HEAD 2>/dev/null || true)
|
|
1935
|
+
if [ "$actual" != "$expected" ]; then
|
|
1936
|
+
echo "error: pooled worktree '$worktree' is at '${actual:-unknown}', not current '$target' ('$expected'); refusing to launch" >&2
|
|
1937
|
+
return 1
|
|
1938
|
+
fi
|
|
1939
|
+
}
|
|
1940
|
+
|
|
1941
|
+
herdr_projection_meta_field_exact() { # <meta> <key>
|
|
1942
|
+
local meta=$1 key=$2 count
|
|
1943
|
+
[ -f "$meta" ] && [ ! -L "$meta" ] || return 1
|
|
1944
|
+
count=$(grep -c "^${key}=" "$meta" 2>/dev/null || true)
|
|
1945
|
+
[ "$count" = 1 ] || return 1
|
|
1946
|
+
grep "^${key}=" "$meta" 2>/dev/null | cut -d= -f2-
|
|
1947
|
+
}
|
|
1948
|
+
|
|
1949
|
+
# A stale presentation journal never grants launch authority.
|
|
1950
|
+
# Under the session lock, authoritative metadata must identify one positively
|
|
1951
|
+
# dead or agent-free endpoint before token inspection may allow flat fallback.
|
|
1952
|
+
# Exact Herdr fields are retained for the narrower version 2 reclaim path.
|
|
1953
|
+
herdr_projection_existing_meta_allows_flat() { # <meta>
|
|
1954
|
+
local meta=$1 old_backend old_target old_session old_pane old_state target_session target_pane
|
|
1955
|
+
HERDR_RECOVERY_BACKEND=""
|
|
1956
|
+
HERDR_RECOVERY_WORKSPACE_ID=""
|
|
1957
|
+
HERDR_RECOVERY_TAB_ID=""
|
|
1958
|
+
HERDR_RECOVERY_PANE_ID=""
|
|
1959
|
+
old_backend=$(fm_backend_of_meta "$meta")
|
|
1960
|
+
old_target=$(fm_backend_target_of_meta "$meta")
|
|
1961
|
+
[ -n "$old_target" ] || {
|
|
1962
|
+
echo "error: existing metadata for $ID has no endpoint; refusing duplicate launch while its herdr presentation journal is quarantined" >&2
|
|
1963
|
+
return 1
|
|
1964
|
+
}
|
|
1965
|
+
HERDR_RECOVERY_BACKEND=$old_backend
|
|
1966
|
+
if [ "$old_backend" = herdr ]; then
|
|
1967
|
+
fm_backend_herdr_parse_target "$old_target" || {
|
|
1968
|
+
echo "error: existing herdr endpoint for $ID is malformed; refusing duplicate launch" >&2
|
|
1969
|
+
return 1
|
|
1970
|
+
}
|
|
1971
|
+
target_session=$FM_BACKEND_HERDR_SESSION
|
|
1972
|
+
target_pane=$FM_BACKEND_HERDR_PANE
|
|
1973
|
+
old_session=$(herdr_projection_meta_field_exact "$meta" herdr_session) || {
|
|
1974
|
+
echo "error: existing herdr metadata for $ID has an ambiguous session; refusing duplicate launch" >&2
|
|
1975
|
+
return 1
|
|
1976
|
+
}
|
|
1977
|
+
HERDR_RECOVERY_WORKSPACE_ID=$(herdr_projection_meta_field_exact "$meta" herdr_workspace_id) || {
|
|
1978
|
+
echo "error: existing herdr metadata for $ID has an ambiguous workspace; refusing duplicate launch" >&2
|
|
1979
|
+
return 1
|
|
1980
|
+
}
|
|
1981
|
+
HERDR_RECOVERY_TAB_ID=$(herdr_projection_meta_field_exact "$meta" herdr_tab_id) || {
|
|
1982
|
+
echo "error: existing herdr metadata for $ID has an ambiguous tab; refusing duplicate launch" >&2
|
|
1983
|
+
return 1
|
|
1984
|
+
}
|
|
1985
|
+
old_pane=$(herdr_projection_meta_field_exact "$meta" herdr_pane_id) || {
|
|
1986
|
+
echo "error: existing herdr metadata for $ID has an ambiguous pane; refusing duplicate launch" >&2
|
|
1987
|
+
return 1
|
|
1988
|
+
}
|
|
1989
|
+
[ "$target_session" = "$old_session" ] && [ "$target_pane" = "$old_pane" ] || {
|
|
1990
|
+
echo "error: existing herdr metadata for $ID has inconsistent endpoint identities; refusing duplicate launch" >&2
|
|
1991
|
+
return 1
|
|
1992
|
+
}
|
|
1993
|
+
HERDR_RECOVERY_PANE_ID=$old_pane
|
|
1994
|
+
fm_backend_herdr_server_ensure "$old_session" || {
|
|
1995
|
+
echo "error: existing herdr endpoint for $ID could not be inspected; refusing duplicate launch" >&2
|
|
1996
|
+
return 1
|
|
1997
|
+
}
|
|
1998
|
+
old_state=$(fm_backend_herdr_pane_agent_state "$old_session" "$old_pane")
|
|
1999
|
+
case "$old_state" in
|
|
2000
|
+
dead|no-agent) return 0 ;;
|
|
2001
|
+
live|unknown)
|
|
2002
|
+
echo "error: existing herdr endpoint for $ID is $old_state; refusing duplicate launch" >&2
|
|
2003
|
+
return 1
|
|
2004
|
+
;;
|
|
2005
|
+
esac
|
|
2006
|
+
fi
|
|
2007
|
+
old_state=$(fm_backend_agent_alive "$old_backend" "$old_target")
|
|
2008
|
+
case "$old_state" in
|
|
2009
|
+
dead) return 0 ;;
|
|
2010
|
+
alive|unknown)
|
|
2011
|
+
echo "error: existing $old_backend endpoint for $ID is $old_state; refusing duplicate launch" >&2
|
|
2012
|
+
return 1
|
|
2013
|
+
;;
|
|
2014
|
+
esac
|
|
2015
|
+
}
|
|
2016
|
+
|
|
2017
|
+
# Backlog preflight (bin/fm-backlog-transition-lib.sh). This spawn is about to
|
|
2018
|
+
# become the sole owner of the row's In-flight transition, so prove the row is
|
|
2019
|
+
# transitionable BEFORE any endpoint, worktree, or record exists: a refusal here
|
|
2020
|
+
# costs nothing to unwind, while the same refusal after publication would strand
|
|
2021
|
+
# a live pane. The authoritative mutation still runs under the meta lock below.
|
|
2022
|
+
BACKLOG_TRANSITION=0
|
|
2023
|
+
BACKLOG_ROW_STATE=
|
|
2024
|
+
if fm_backlog_transition_applies "$CONFIG" "$DATA" "$KIND"; then
|
|
2025
|
+
BACKLOG_TRANSITION=1
|
|
2026
|
+
if fm_backlog_row_probe "$DATA" "$ID"; then
|
|
2027
|
+
BACKLOG_ROW_STATE=$FM_BACKLOG_ROW_STATE
|
|
2028
|
+
elif [ "$FM_BACKLOG_ROW_RESULT" = not_found ]; then
|
|
2029
|
+
echo "error: task $ID has no backlog item in this home, so dispatching it would leave a worker no record owns; add it first (tasks-axi add $ID '<title>' --kind $KIND) and re-run" >&2
|
|
2030
|
+
exit 1
|
|
2031
|
+
else
|
|
2032
|
+
echo "error: task $ID's backlog item could not be read before dispatch ($FM_BACKLOG_ROW_ERROR)" >&2
|
|
2033
|
+
exit 1
|
|
2034
|
+
fi
|
|
2035
|
+
if ! fm_backlog_row_dispatchable "$BACKLOG_ROW_STATE"; then
|
|
2036
|
+
echo "error: this home's backlog item $ID is not dispatchable in state $BACKLOG_ROW_STATE; refusing before creating its endpoint or local copy" >&2
|
|
2037
|
+
exit 1
|
|
2038
|
+
fi
|
|
2039
|
+
else
|
|
2040
|
+
BACKLOG_GATE_STATUS=$?
|
|
2041
|
+
if [ "$BACKLOG_GATE_STATUS" -eq 2 ]; then
|
|
2042
|
+
echo "error: task $ID cannot be dispatched because its backlog data directory is inaccessible: $DATA ($FM_BACKLOG_TRANSITION_ERROR)" >&2
|
|
2043
|
+
exit 1
|
|
2044
|
+
fi
|
|
2045
|
+
fi
|
|
2046
|
+
|
|
2047
|
+
if [ "$SPAWN_META_LOCK_HELD" != 1 ]; then
|
|
2048
|
+
SPAWN_META_LOCK=$(fm_meta_lock_path "$STATE/$ID.meta") || exit 1
|
|
2049
|
+
fm_lock_acquire_wait "$SPAWN_META_LOCK"
|
|
2050
|
+
SPAWN_META_LOCK_HELD=1
|
|
2051
|
+
fi
|
|
2052
|
+
if [ -e "$STATE/$ID.backlog-close" ] || [ -L "$STATE/$ID.backlog-close" ]; then
|
|
2053
|
+
echo "error: task $ID has a pending authoritative backlog close at $STATE/$ID.backlog-close; finish or repair that close before dispatching a new worker" >&2
|
|
2054
|
+
exit 1
|
|
2055
|
+
fi
|
|
2056
|
+
|
|
2057
|
+
W="fm-$ID"
|
|
2058
|
+
if [ "$RELAUNCH" -eq 1 ]; then
|
|
2059
|
+
# Adopt the recorded endpoint instead of creating one. This is what keeps a
|
|
2060
|
+
# relaunch a REPLACEMENT rather than a second copy of the task: no new
|
|
2061
|
+
# terminal, no second worktree, and every uncommitted change left exactly
|
|
2062
|
+
# where the previous agent left it.
|
|
2063
|
+
T=$RELAUNCH_TARGET
|
|
2064
|
+
# A secondmate's home already resolved WT above through the same validation a
|
|
2065
|
+
# fresh secondmate spawn uses; every other kind takes the recorded worktree.
|
|
2066
|
+
[ "$KIND" = secondmate ] || WT=$RELAUNCH_WT
|
|
2067
|
+
WT_TARGET=$T
|
|
2068
|
+
SES=${T%%:*}
|
|
2069
|
+
else
|
|
2070
|
+
case "$BACKEND" in
|
|
2071
|
+
tmux)
|
|
2072
|
+
SES=$(fm_backend_tmux_container_ensure)
|
|
2073
|
+
T="$SES:$W"
|
|
2074
|
+
# #134 robustness (tmux): fm_backend_tmux_create_task captures a stable window
|
|
2075
|
+
# id and pins the window name (automatic-rename/allow-rename off) so a captain's
|
|
2076
|
+
# non-default tmux config cannot rename the window away from fm-<id> once
|
|
2077
|
+
# treehouse cd's into the worktree. WT_TARGET carries that stable id for the
|
|
2078
|
+
# rename-critical worktree-detection steps below; the persisted window= handle
|
|
2079
|
+
# stays $T (the name form), which is safe now that rename is disabled.
|
|
2080
|
+
WID=$(fm_backend_tmux_create_task "$SES" "$W" "$PROJ_ABS") || exit 1
|
|
2081
|
+
WT_TARGET="$WID"
|
|
2082
|
+
;;
|
|
2083
|
+
herdr)
|
|
2084
|
+
# fm_backend_herdr_workspace_label resolves the target workspace from
|
|
2085
|
+
# FM_HOME. For every KIND except secondmate, this process's own FM_HOME is
|
|
2086
|
+
# already the right home (the primary spawning its own crewmate/scout, or
|
|
2087
|
+
# a secondmate spawning ITS OWN crewmate/scout from its own process's
|
|
2088
|
+
# FM_HOME - the latter needs no glue at all). A --secondmate spawn is the
|
|
2089
|
+
# one case that does: it is the PRIMARY's own fm-spawn.sh process
|
|
2090
|
+
# launching a DIFFERENT home (PROJ_ABS, already validated above as the
|
|
2091
|
+
# secondmate's home), so FM_HOME here still names the primary. Shadow it
|
|
2092
|
+
# to PROJ_ABS for just these two calls (bash restores it automatically
|
|
2093
|
+
# after each prefixed simple-command call) so the secondmate's tab lands
|
|
2094
|
+
# in the secondmate's own workspace, not the primary's "firstmate" one.
|
|
2095
|
+
#
|
|
2096
|
+
# Placement, separately from labeling: a crewmate/scout belongs in the
|
|
2097
|
+
# EXACT herdr workspace this launching process is itself running in, which
|
|
2098
|
+
# only its own herdr pane identity can name (a same-labeled sibling
|
|
2099
|
+
# workspace must never be adopted). A --secondmate launch is the exception -
|
|
2100
|
+
# it stands up a DIFFERENT home's own workspace by design - so it asks for
|
|
2101
|
+
# the per-home container instead of inheriting this launcher's.
|
|
2102
|
+
HERDR_LABEL_HOME=$FM_HOME
|
|
2103
|
+
HERDR_LAUNCHER_RELATIONSHIP=launcher-home
|
|
2104
|
+
if [ "$KIND" = secondmate ]; then
|
|
2105
|
+
HERDR_LABEL_HOME=$PROJ_ABS
|
|
2106
|
+
HERDR_LAUNCHER_RELATIONSHIP=other-home
|
|
2107
|
+
fi
|
|
2108
|
+
HERDR_PRESENTATION_JOURNAL=$(fm_backend_herdr_projection_journal_path "$STATE" "$ID")
|
|
2109
|
+
HERDR_PROJECTED=0
|
|
2110
|
+
if [ "$KIND" != secondmate ] && fm_backend_herdr_presentation_enabled "$CONFIG" "$STATE"; then
|
|
2111
|
+
HERDR_SES=$(fm_backend_herdr_session)
|
|
2112
|
+
HERDR_PARENT_LABEL=$(FM_HOME="$HERDR_LABEL_HOME" fm_backend_herdr_workspace_label)
|
|
2113
|
+
if [ -e "$HERDR_PRESENTATION_JOURNAL" ] || [ -L "$HERDR_PRESENTATION_JOURNAL" ]; then
|
|
2114
|
+
fm_backend_herdr_server_ensure "$HERDR_SES" || {
|
|
2115
|
+
echo "error: herdr presentation recovery could not ensure its exact named session" >&2
|
|
2116
|
+
exit 1
|
|
2117
|
+
}
|
|
2118
|
+
spawn_herdr_presentation_order_lock_acquire "$HERDR_SES" || {
|
|
2119
|
+
echo "error: herdr presentation recovery could not acquire its session lock; refusing a concurrent resume" >&2
|
|
2120
|
+
exit 1
|
|
2121
|
+
}
|
|
2122
|
+
if [ -e "$STATE/$ID.meta" ] || [ -L "$STATE/$ID.meta" ]; then
|
|
2123
|
+
herdr_projection_existing_meta_allows_flat "$STATE/$ID.meta" || exit 1
|
|
2124
|
+
fi
|
|
2125
|
+
fm_backend_herdr_projection_recovery_allows_flat \
|
|
2126
|
+
"$HERDR_SES" "$HERDR_PRESENTATION_JOURNAL" "$ID" || exit 1
|
|
2127
|
+
if [ "${HERDR_RECOVERY_BACKEND:-}" = herdr ]; then
|
|
2128
|
+
set +e
|
|
2129
|
+
FM_HOME="$HERDR_LABEL_HOME" fm_backend_herdr_projection_reclaim_task \
|
|
2130
|
+
"$HERDR_SES" "$HERDR_PRESENTATION_JOURNAL" "$ID" "$HERDR_LABEL_HOME" \
|
|
2131
|
+
"$HERDR_RECOVERY_WORKSPACE_ID" "$HERDR_RECOVERY_TAB_ID" "$HERDR_RECOVERY_PANE_ID" \
|
|
2132
|
+
"$HERDR_PARENT_LABEL" "$W" "$PROJ_ABS"
|
|
2133
|
+
HERDR_RECLAIM_STATUS=$?
|
|
2134
|
+
set -e
|
|
2135
|
+
case "$HERDR_RECLAIM_STATUS" in
|
|
2136
|
+
0)
|
|
2137
|
+
HERDR_PROJECTED=1
|
|
2138
|
+
HERDR_WORKSPACE_ID=$HERDR_RECOVERY_WORKSPACE_ID
|
|
2139
|
+
HERDR_SEEDED_DEFAULT_TAB_ID=""
|
|
2140
|
+
HERDR_TAB_ID=$FM_BACKEND_HERDR_PROJECTION_TAB_ID
|
|
2141
|
+
HERDR_PANE_ID=$FM_BACKEND_HERDR_PROJECTION_PANE_ID
|
|
2142
|
+
HERDR_PROJECTION_ABORT_CLEANUP=1
|
|
2143
|
+
HERDR_PROJECTION_ABORT_SESSION=$HERDR_SES
|
|
2144
|
+
HERDR_PROJECTION_ABORT_TASK_PANE=$HERDR_PANE_ID
|
|
2145
|
+
HERDR_PROJECTION_ABORT_SEEDED_PANE=""
|
|
2146
|
+
;;
|
|
2147
|
+
2)
|
|
2148
|
+
spawn_herdr_presentation_order_lock_release
|
|
2149
|
+
;;
|
|
2150
|
+
*) exit 1 ;;
|
|
2151
|
+
esac
|
|
2152
|
+
else
|
|
2153
|
+
spawn_herdr_presentation_order_lock_release
|
|
2154
|
+
fi
|
|
2155
|
+
elif [ ! -e "$STATE/$ID.meta" ] && [ ! -L "$STATE/$ID.meta" ]; then
|
|
2156
|
+
# Session lock path resolution and exact parent binding both need a
|
|
2157
|
+
# live named-session socket before journal publication.
|
|
2158
|
+
if ! fm_backend_herdr_server_ensure "$HERDR_SES"; then
|
|
2159
|
+
echo "warning: herdr presentation could not ensure its session server; using the ordinary flat layout without projection" >&2
|
|
2160
|
+
elif [ "${FM_BACKEND_HERDR_PRESENTATION_PREFERENCE:-default}" = default ] \
|
|
2161
|
+
&& ! fm_backend_herdr_presentation_default_supported "$STATE" "$HERDR_SES"; then
|
|
2162
|
+
:
|
|
2163
|
+
elif spawn_herdr_presentation_order_lock_acquire "$HERDR_SES"; then
|
|
2164
|
+
# The projected child is placed and bound UNDER this launcher's exact
|
|
2165
|
+
# parent workspace. Its own herdr pane identity names that workspace
|
|
2166
|
+
# directly; the label lookup is only the fallback for a launcher with
|
|
2167
|
+
# no herdr ancestry at all. A claimed-but-broken identity refuses here
|
|
2168
|
+
# rather than projecting under a guessed parent.
|
|
2169
|
+
set +e
|
|
2170
|
+
fm_backend_herdr_launcher_identity "$HERDR_SES"
|
|
2171
|
+
HERDR_LAUNCHER_STATUS=$?
|
|
2172
|
+
set -e
|
|
2173
|
+
case "$HERDR_LAUNCHER_STATUS" in
|
|
2174
|
+
0) HERDR_PARENT_WORKSPACE_ID=$FM_BACKEND_HERDR_LAUNCHER_WORKSPACE_ID ;;
|
|
2175
|
+
2) HERDR_PARENT_WORKSPACE_ID=$(fm_backend_herdr_projection_parent_workspace_exact \
|
|
2176
|
+
"$HERDR_SES" "$HERDR_PARENT_LABEL" 2>/dev/null || true) ;;
|
|
2177
|
+
*) spawn_herdr_presentation_order_lock_release; exit 1 ;;
|
|
2178
|
+
esac
|
|
2179
|
+
if [ -z "$HERDR_PARENT_WORKSPACE_ID" ]; then
|
|
2180
|
+
echo "warning: herdr presentation parent is absent or ambiguous; using the ordinary flat layout without projection" >&2
|
|
2181
|
+
spawn_herdr_presentation_order_lock_release
|
|
2182
|
+
else
|
|
2183
|
+
HERDR_PROJECTION_ID=$(fm_backend_herdr_projection_journal_create "$STATE" "$ID") || exit 1
|
|
2184
|
+
HERDR_PROJECTION_LABEL=$(fm_backend_herdr_projection_workspace_label "$ID" "$HERDR_PROJECTION_ID")
|
|
2185
|
+
if ! FM_HOME="$HERDR_LABEL_HOME" fm_backend_herdr_projection_create_task \
|
|
2186
|
+
"$PROJ_ABS" "$HERDR_PROJECTION_LABEL" "$W"; then
|
|
2187
|
+
if [ "${FM_BACKEND_HERDR_PROJECTION_CLEANUP_SAFE:-0}" = 1 ]; then
|
|
2188
|
+
HERDR_PROJECTION_ABORT_CLEANUP=1
|
|
2189
|
+
HERDR_PROJECTION_ABORT_SESSION=$FM_BACKEND_HERDR_PROJECTION_SESSION
|
|
2190
|
+
HERDR_PROJECTION_ABORT_TASK_PANE=$FM_BACKEND_HERDR_PROJECTION_PANE_ID
|
|
2191
|
+
HERDR_PROJECTION_ABORT_SEEDED_PANE=$FM_BACKEND_HERDR_PROJECTION_SEEDED_PANE_ID
|
|
2192
|
+
fi
|
|
2193
|
+
exit 1
|
|
2194
|
+
fi
|
|
2195
|
+
HERDR_PROJECTED=1
|
|
2196
|
+
HERDR_SES=$FM_BACKEND_HERDR_PROJECTION_SESSION
|
|
2197
|
+
HERDR_WORKSPACE_ID=$FM_BACKEND_HERDR_PROJECTION_WORKSPACE_ID
|
|
2198
|
+
HERDR_SEEDED_DEFAULT_TAB_ID=$FM_BACKEND_HERDR_PROJECTION_SEEDED_TAB_ID
|
|
2199
|
+
HERDR_TAB_ID=$FM_BACKEND_HERDR_PROJECTION_TAB_ID
|
|
2200
|
+
HERDR_PANE_ID=$FM_BACKEND_HERDR_PROJECTION_PANE_ID
|
|
2201
|
+
HERDR_PROJECTION_ABORT_CLEANUP=1
|
|
2202
|
+
HERDR_PROJECTION_ABORT_SESSION=$HERDR_SES
|
|
2203
|
+
HERDR_PROJECTION_ABORT_TASK_PANE=$HERDR_PANE_ID
|
|
2204
|
+
HERDR_PROJECTION_ABORT_SEEDED_PANE=$FM_BACKEND_HERDR_PROJECTION_SEEDED_PANE_ID
|
|
2205
|
+
fm_backend_herdr_projection_order_best_effort \
|
|
2206
|
+
"$HERDR_SES" "$HERDR_WORKSPACE_ID" "$HERDR_PARENT_LABEL" "$HERDR_PARENT_WORKSPACE_ID"
|
|
2207
|
+
HERDR_HOME_ID=$(fm_backend_herdr_projection_home_identity "$HERDR_LABEL_HOME" 2>/dev/null || true)
|
|
2208
|
+
if [ -n "$HERDR_HOME_ID" ] \
|
|
2209
|
+
&& fm_backend_herdr_projection_live_binding_matches \
|
|
2210
|
+
"$HERDR_SES" "$HERDR_PROJECTION_ID" "$HERDR_WORKSPACE_ID" \
|
|
2211
|
+
"$HERDR_TAB_ID" "$HERDR_PANE_ID" "$HERDR_PARENT_WORKSPACE_ID" \
|
|
2212
|
+
"$HERDR_PARENT_LABEL" "$HERDR_PROJECTION_LABEL" "$W" \
|
|
2213
|
+
&& fm_backend_herdr_projection_journal_bind \
|
|
2214
|
+
"$HERDR_PRESENTATION_JOURNAL" "$ID" "$HERDR_HOME_ID" "$HERDR_SES" \
|
|
2215
|
+
"$HERDR_WORKSPACE_ID" "$HERDR_TAB_ID" "$HERDR_PANE_ID" \
|
|
2216
|
+
"$HERDR_PARENT_WORKSPACE_ID" "$HERDR_PARENT_LABEL" "$HERDR_PROJECTION_LABEL" "$W"; then
|
|
2217
|
+
:
|
|
2218
|
+
else
|
|
2219
|
+
echo "warning: herdr presentation could not publish an exact restart binding; this task will use flat fallback after a restart" >&2
|
|
2220
|
+
fi
|
|
2221
|
+
fi
|
|
2222
|
+
else
|
|
2223
|
+
echo "warning: herdr presentation focus lock unavailable; using the ordinary flat layout without projection" >&2
|
|
2224
|
+
fi
|
|
2225
|
+
fi
|
|
2226
|
+
fi
|
|
2227
|
+
if [ "$HERDR_PROJECTED" -ne 1 ]; then
|
|
2228
|
+
HERDR_CONTAINER_RAW=$(FM_HOME="$HERDR_LABEL_HOME" fm_backend_herdr_container_ensure "$PROJ_ABS" "$HERDR_LAUNCHER_RELATIONSHIP") || exit 1
|
|
2229
|
+
# fm_backend_herdr_container_ensure echoes "<session>:<workspace_id>\t<seeded_default_tab_id>"
|
|
2230
|
+
# (the second field empty when this call ADOPTED a pre-existing workspace
|
|
2231
|
+
# rather than creating a fresh one). Split on the guaranteed single tab
|
|
2232
|
+
# character; the seeded tab id is threaded through to create_task
|
|
2233
|
+
# untouched, which is the only function permitted to prune it (never
|
|
2234
|
+
# re-derived from labels - see docs/herdr-backend.md "Default-tab prune").
|
|
2235
|
+
CONTAINER=${HERDR_CONTAINER_RAW%%$'\t'*}
|
|
2236
|
+
HERDR_SEEDED_DEFAULT_TAB_ID=${HERDR_CONTAINER_RAW#*$'\t'}
|
|
2237
|
+
HERDR_SES=${CONTAINER%%:*}
|
|
2238
|
+
HERDR_WORKSPACE_ID=${CONTAINER#*:}
|
|
2239
|
+
HERDR_TASK_IDS=$(FM_HOME="$HERDR_LABEL_HOME" fm_backend_herdr_create_task "$CONTAINER" "$W" "$PROJ_ABS" "$HERDR_SEEDED_DEFAULT_TAB_ID") || exit 1
|
|
2240
|
+
read -r HERDR_TAB_ID HERDR_PANE_ID <<EOF
|
|
2241
|
+
$HERDR_TASK_IDS
|
|
2242
|
+
EOF
|
|
2243
|
+
fi
|
|
2244
|
+
if [ -z "$HERDR_TAB_ID" ] || [ -z "$HERDR_PANE_ID" ]; then
|
|
2245
|
+
echo "error: herdr did not return a tab/pane id for $W" >&2
|
|
2246
|
+
exit 1
|
|
2247
|
+
fi
|
|
2248
|
+
T="$HERDR_SES:$HERDR_PANE_ID"
|
|
2249
|
+
;;
|
|
2250
|
+
zellij)
|
|
2251
|
+
ZELLIJ_SES=$(fm_backend_zellij_container_ensure) || exit 1
|
|
2252
|
+
ZELLIJ_TASK_IDS=$(fm_backend_zellij_create_task "$ZELLIJ_SES" "$W" "$PROJ_ABS") || exit 1
|
|
2253
|
+
read -r ZELLIJ_TAB_ID ZELLIJ_PANE_ID <<EOF
|
|
2254
|
+
$ZELLIJ_TASK_IDS
|
|
2255
|
+
EOF
|
|
2256
|
+
if [ -z "$ZELLIJ_TAB_ID" ] || [ -z "$ZELLIJ_PANE_ID" ]; then
|
|
2257
|
+
echo "error: zellij did not return a tab/pane id for $W" >&2
|
|
2258
|
+
exit 1
|
|
2259
|
+
fi
|
|
2260
|
+
T="$ZELLIJ_SES:$ZELLIJ_PANE_ID"
|
|
2261
|
+
;;
|
|
2262
|
+
cmux)
|
|
2263
|
+
fm_backend_cmux_container_ensure || exit 1
|
|
2264
|
+
CMUX_TASK_IDS=$(fm_backend_cmux_create_task "$W" "$PROJ_ABS") || exit 1
|
|
2265
|
+
read -r CMUX_WORKSPACE_ID CMUX_SURFACE_ID <<EOF
|
|
2266
|
+
$CMUX_TASK_IDS
|
|
2267
|
+
EOF
|
|
2268
|
+
if [ -z "$CMUX_WORKSPACE_ID" ] || [ -z "$CMUX_SURFACE_ID" ]; then
|
|
2269
|
+
echo "error: cmux did not return a workspace/surface id for $W" >&2
|
|
2270
|
+
exit 1
|
|
2271
|
+
fi
|
|
2272
|
+
T="$CMUX_WORKSPACE_ID:$CMUX_SURFACE_ID"
|
|
2273
|
+
;;
|
|
2274
|
+
orca)
|
|
2275
|
+
set +e
|
|
2276
|
+
ORCA_WT_RAW=$(fm_backend_orca_worktree_create "$PROJ_ABS" "$W")
|
|
2277
|
+
ORCA_WT_STATUS=$?
|
|
2278
|
+
set -e
|
|
2279
|
+
if [ "$ORCA_WT_STATUS" -ne 0 ]; then
|
|
2280
|
+
if [ "$ORCA_WT_STATUS" -eq 2 ] && [ -n "$ORCA_WT_RAW" ]; then
|
|
2281
|
+
if parse_orca_worktree_result "$ORCA_WT_RAW" && [ -n "$ORCA_WORKTREE_ID" ]; then
|
|
2282
|
+
ORCA_ABORT_CLEANUP=1
|
|
2283
|
+
fi
|
|
2284
|
+
fi
|
|
2285
|
+
exit 1
|
|
2286
|
+
fi
|
|
2287
|
+
parse_orca_worktree_result "$ORCA_WT_RAW" || true
|
|
2288
|
+
ORCA_ABORT_CLEANUP=1
|
|
2289
|
+
if [ -z "$ORCA_WORKTREE_ID" ] || [ -z "$WT" ]; then
|
|
2290
|
+
echo "error: orca did not return a worktree id/path for $W" >&2
|
|
2291
|
+
exit 1
|
|
2292
|
+
fi
|
|
2293
|
+
validate_spawn_worktree "orca worktree create" "$W"
|
|
2294
|
+
if [ -z "$ORCA_TERMINAL" ]; then
|
|
2295
|
+
ORCA_TERMINAL=$(fm_backend_orca_terminal_create "$ORCA_WORKTREE_ID" "$W") || exit 1
|
|
2296
|
+
fi
|
|
2297
|
+
T="$ORCA_TERMINAL"
|
|
2298
|
+
;;
|
|
2299
|
+
esac
|
|
2300
|
+
fi
|
|
2301
|
+
if [ "$KIND" = secondmate ]; then
|
|
2302
|
+
FM_INHERITABLE_CONFIG=trace-context \
|
|
2303
|
+
propagate_inheritable_config "$CONFIG" "$PROJ_ABS/config" \
|
|
2304
|
+
|| echo "warning: secondmate $ID trace-context inheritance failed for $PROJ_ABS" >&2
|
|
2305
|
+
fi
|
|
2306
|
+
# #134 robustness: only tmux needs a worktree-detection target distinct from $T -
|
|
2307
|
+
# its rename-safe stable window id, set as WT_TARGET=$WID in the tmux branch above.
|
|
2308
|
+
# Every other backend addresses its pane/surface by the id already in $T, so default
|
|
2309
|
+
# WT_TARGET to $T for them (and for any future backend) - the shared treehouse-get +
|
|
2310
|
+
# worktree-detection steps below must never reference an unbound WT_TARGET under set -u.
|
|
2311
|
+
: "${WT_TARGET:=$T}"
|
|
2312
|
+
spawn_send_text_line() { # <target> <text>
|
|
2313
|
+
case "$BACKEND" in
|
|
2314
|
+
tmux) fm_backend_tmux_send_text_line "$1" "$2" ;;
|
|
2315
|
+
herdr) fm_backend_herdr_send_text_line "$1" "$2" ;;
|
|
2316
|
+
zellij) fm_backend_zellij_send_text_line "$1" "$2" "$W" ;;
|
|
2317
|
+
orca) fm_backend_orca_send_text_line "$1" "$2" ;;
|
|
2318
|
+
cmux) fm_backend_cmux_send_text_line "$1" "$2" "$W" ;;
|
|
2319
|
+
esac
|
|
2320
|
+
}
|
|
2321
|
+
spawn_current_path() { # <target>
|
|
2322
|
+
case "$BACKEND" in
|
|
2323
|
+
tmux) fm_backend_tmux_current_path "$1" ;;
|
|
2324
|
+
herdr) fm_backend_herdr_current_path "$1" ;;
|
|
2325
|
+
zellij) fm_backend_zellij_current_path "$1" "$W" ;;
|
|
2326
|
+
cmux) fm_backend_cmux_current_path "$1" "$W" ;;
|
|
2327
|
+
esac
|
|
2328
|
+
}
|
|
2329
|
+
spawn_send_literal() { # <target> <text>
|
|
2330
|
+
case "$BACKEND" in
|
|
2331
|
+
tmux) fm_backend_tmux_send_literal "$1" "$2" ;;
|
|
2332
|
+
herdr) fm_backend_herdr_send_literal "$1" "$2" ;;
|
|
2333
|
+
zellij) fm_backend_zellij_send_literal "$1" "$2" "$W" ;;
|
|
2334
|
+
orca) fm_backend_orca_send_literal "$1" "$2" ;;
|
|
2335
|
+
cmux) fm_backend_cmux_send_literal "$1" "$2" "$W" ;;
|
|
2336
|
+
esac
|
|
2337
|
+
}
|
|
2338
|
+
spawn_send_key() { # <target> <key>
|
|
2339
|
+
case "$BACKEND" in
|
|
2340
|
+
tmux) fm_backend_tmux_send_key "$1" "$2" ;;
|
|
2341
|
+
herdr) fm_backend_herdr_send_key "$1" "$2" ;;
|
|
2342
|
+
zellij) fm_backend_zellij_send_key "$1" "$2" "$W" ;;
|
|
2343
|
+
orca) fm_backend_orca_send_key "$1" "$2" ;;
|
|
2344
|
+
cmux) fm_backend_cmux_send_key "$1" "$2" "$W" ;;
|
|
2345
|
+
esac
|
|
2346
|
+
}
|
|
2347
|
+
|
|
2348
|
+
kimi_capture() {
|
|
2349
|
+
fm_backend_capture "$BACKEND" "$T" 120 "$W" 2>/dev/null || true
|
|
2350
|
+
}
|
|
2351
|
+
|
|
2352
|
+
# Kimi launch-readiness and delivery route their composer-emptiness half
|
|
2353
|
+
# through the shared classifier (bin/fm-composer-lib.sh via
|
|
2354
|
+
# fm_backend_composer_state), the same owner every steer and injection guard
|
|
2355
|
+
# reads. This retired a fourth, spawn-local copy of composer shape knowledge -
|
|
2356
|
+
# a hardcoded bordered `│ > │` regex that would have silently broken kimi
|
|
2357
|
+
# spawn readiness fleet-wide the day kimi's TUI goes borderless the way
|
|
2358
|
+
# claude's did. The banner and brief-echo greps below are launch-progress
|
|
2359
|
+
# signals, not composer shapes, so they stay here.
|
|
2360
|
+
kimi_composer_is_empty() {
|
|
2361
|
+
[ "$(fm_backend_composer_state "$BACKEND" "$T" "$W" 2>/dev/null)" = empty ]
|
|
2362
|
+
}
|
|
2363
|
+
|
|
2364
|
+
kimi_wait_for_ready() {
|
|
2365
|
+
local pane i=0 max=${FM_KIMI_READY_POLLS:-60} interval=${FM_KIMI_POLL_INTERVAL:-0.5}
|
|
2366
|
+
while [ "$i" -lt "$max" ]; do
|
|
2367
|
+
pane=$(kimi_capture)
|
|
2368
|
+
if printf '%s\n' "$pane" | grep -Fq 'Welcome to Kimi Code!' \
|
|
2369
|
+
|| kimi_composer_is_empty; then
|
|
2370
|
+
return 0
|
|
2371
|
+
fi
|
|
2372
|
+
i=$((i + 1))
|
|
2373
|
+
[ "$i" -ge "$max" ] || sleep "$interval"
|
|
2374
|
+
done
|
|
2375
|
+
return 1
|
|
2376
|
+
}
|
|
2377
|
+
|
|
2378
|
+
kimi_delivery_is_confirmed() { # <plain-pane-capture>
|
|
2379
|
+
local pane=$1
|
|
2380
|
+
kimi_composer_is_empty || return 1
|
|
2381
|
+
if { printf '%s\n' "$pane" | grep -Fq '✨' \
|
|
2382
|
+
&& printf '%s\n' "$pane" | grep -Fq 'Read the brief at'; } \
|
|
2383
|
+
|| printf '%s\n' "$pane" \
|
|
2384
|
+
| grep -qiE 'context:[[:space:]]*(0\.[0-9]*[1-9][0-9]*|[1-9][0-9]*([.][0-9]+)?)[[:space:]]*%'; then
|
|
2385
|
+
return 0
|
|
2386
|
+
fi
|
|
2387
|
+
return 1
|
|
2388
|
+
}
|
|
2389
|
+
|
|
2390
|
+
kimi_wait_for_delivery() {
|
|
2391
|
+
local pane i=0 max=${FM_KIMI_DELIVERY_POLLS:-40} interval=${FM_KIMI_POLL_INTERVAL:-0.5}
|
|
2392
|
+
while [ "$i" -lt "$max" ]; do
|
|
2393
|
+
pane=$(kimi_capture)
|
|
2394
|
+
kimi_delivery_is_confirmed "$pane" && return 0
|
|
2395
|
+
i=$((i + 1))
|
|
2396
|
+
[ "$i" -ge "$max" ] || sleep "$interval"
|
|
2397
|
+
done
|
|
2398
|
+
return 1
|
|
2399
|
+
}
|
|
2400
|
+
|
|
2401
|
+
kimi_spawn_fail() { # <detail>
|
|
2402
|
+
printf 'failed: %s\n' "$1" >> "$STATE/$ID.status"
|
|
2403
|
+
echo "error: $1; inspect window $T" >&2
|
|
2404
|
+
}
|
|
2405
|
+
|
|
2406
|
+
if [ "$RELAUNCH" -eq 1 ]; then
|
|
2407
|
+
# No worktree is acquired: the recorded one is reused as-is. What must be
|
|
2408
|
+
# proven instead is that the adopted endpoint's shell is actually sitting in
|
|
2409
|
+
# that worktree, so the replacement agent starts where the work is rather
|
|
2410
|
+
# than wherever the pane happened to drift.
|
|
2411
|
+
relaunch_wt_real=$(real_path_or_raw "$WT")
|
|
2412
|
+
relaunch_seen=
|
|
2413
|
+
for _ in $(seq 1 10); do
|
|
2414
|
+
relaunch_seen=$(spawn_current_path "$WT_TARGET" || true)
|
|
2415
|
+
[ -z "$relaunch_seen" ] || [ "$(real_path_or_raw "$relaunch_seen")" != "$relaunch_wt_real" ] || break
|
|
2416
|
+
sleep 0.5
|
|
2417
|
+
done
|
|
2418
|
+
if [ -z "$relaunch_seen" ] || [ "$(real_path_or_raw "$relaunch_seen")" != "$relaunch_wt_real" ]; then
|
|
2419
|
+
echo "error: task $ID's endpoint is in '${relaunch_seen:-unknown}', not its recorded worktree '$WT'; refusing to relaunch an agent outside the copy holding its work" >&2
|
|
2420
|
+
exit 1
|
|
2421
|
+
fi
|
|
2422
|
+
[ "$KIND" = secondmate ] || validate_spawn_worktree "relaunch" "$T"
|
|
2423
|
+
elif [ "$KIND" != secondmate ] && [ "$BACKEND" != orca ]; then
|
|
2424
|
+
spawn_send_text_line "$WT_TARGET" 'treehouse get'
|
|
2425
|
+
|
|
2426
|
+
# Wait for the treehouse subshell: the pane's cwd moves from the project to the worktree.
|
|
2427
|
+
# Target the stable window id, not the name: if the name is ever lost (e.g. an
|
|
2428
|
+
# automatic-rename slips through), display-message -t <bad-name> falls back to the
|
|
2429
|
+
# active client's window, which would misread firstmate's OWN pane path as the
|
|
2430
|
+
# worktree and tangle a hook into the primary checkout. The window id never lies.
|
|
2431
|
+
# Compare against PROJ_ABS_REAL (physical), not PROJ_ABS: a symlinked project
|
|
2432
|
+
# prefix would otherwise make the pane's OS-level cwd read differ from
|
|
2433
|
+
# PROJ_ABS on the very first poll, before the pane has actually moved.
|
|
2434
|
+
#
|
|
2435
|
+
# A single read that already differs from PROJ_ABS_REAL is not proof the pane
|
|
2436
|
+
# settled there: on some tmux/WSL setups a brand-new window's pane_current_path
|
|
2437
|
+
# transiently reports an unrelated stale path (seen live as another real git
|
|
2438
|
+
# checkout entirely) before the shell catches up with treehouse get's cd. That
|
|
2439
|
+
# stale path still passes the PROJ_ABS_REAL comparison and validate_spawn_worktree
|
|
2440
|
+
# below (it resolves to a real, distinct worktree top-level too), so accepting it
|
|
2441
|
+
# on one read alone silently records the wrong worktree= in state/<id>.meta. Require
|
|
2442
|
+
# two consecutive reads to agree on the same non-project path before accepting it;
|
|
2443
|
+
# a mismatch just becomes the new candidate rather than resetting the wait, so a
|
|
2444
|
+
# pane that is already settled by the first real read only costs the one existing
|
|
2445
|
+
# inter-poll sleep as confirmation, not a whole extra cycle on top.
|
|
2446
|
+
candidate=""
|
|
2447
|
+
for _ in $(seq 1 60); do
|
|
2448
|
+
p=$(spawn_current_path "$WT_TARGET" || true)
|
|
2449
|
+
if [ -n "$p" ]; then
|
|
2450
|
+
p_real=$(real_path_or_raw "$p")
|
|
2451
|
+
if [ "$p_real" != "$PROJ_ABS_REAL" ]; then
|
|
2452
|
+
if [ -n "$candidate" ] && [ "$p_real" = "$candidate" ]; then
|
|
2453
|
+
WT="$p"
|
|
2454
|
+
break
|
|
2455
|
+
fi
|
|
2456
|
+
candidate="$p_real"
|
|
2457
|
+
else
|
|
2458
|
+
candidate=""
|
|
2459
|
+
fi
|
|
2460
|
+
else
|
|
2461
|
+
candidate=""
|
|
2462
|
+
fi
|
|
2463
|
+
sleep 1
|
|
2464
|
+
done
|
|
2465
|
+
if [ -z "$WT" ]; then
|
|
2466
|
+
echo "error: treehouse get did not enter a worktree within 60s; inspect window $T" >&2
|
|
2467
|
+
exit 1
|
|
2468
|
+
fi
|
|
2469
|
+
|
|
2470
|
+
validate_spawn_worktree "treehouse get" "$T"
|
|
2471
|
+
fi
|
|
2472
|
+
if [ "$RELAUNCH" -eq 0 ] && [ "$KIND" != secondmate ]; then
|
|
2473
|
+
freshen_spawn_worktree_base "$WT" || exit 1
|
|
2474
|
+
fi
|
|
2475
|
+
|
|
2476
|
+
# Per-task temp root: /tmp/fm-<id>/ with Go's build temp nested at gotmp/. Go won't
|
|
2477
|
+
# create GOTMPDIR, so mkdir before it is used; fm-teardown removes the whole root.
|
|
2478
|
+
# Nested (not a bare /tmp/fm-<id>/gotmp) so other per-task temp can live alongside
|
|
2479
|
+
# later, and teardown cleans one deterministic path. GOTMPDIR (not TMPDIR) is the
|
|
2480
|
+
# targeted knob: TMPDIR is too broad (affects every program's temp, not just Go's).
|
|
2481
|
+
TASK_TMP="/tmp/fm-$ID"
|
|
2482
|
+
mkdir -p "$TASK_TMP/gotmp"
|
|
2483
|
+
|
|
2484
|
+
# Per-harness turn-end hook where enabled: a file that touches
|
|
2485
|
+
# state/<id>.turn-ended when the agent finishes a turn. Worktree-resident hooks
|
|
2486
|
+
# and token pointers stay out of git's view so they never block teardown's dirty
|
|
2487
|
+
# check or leak into a commit.
|
|
2488
|
+
mkdir -p "$STATE"
|
|
2489
|
+
STATE_REAL=$(cd "$STATE" && pwd -P)
|
|
2490
|
+
TURNEND="$STATE_REAL/$ID.turn-ended"
|
|
2491
|
+
exclude_path() {
|
|
2492
|
+
local rel=$1 EXCL
|
|
2493
|
+
EXCL=$(git -C "$WT" rev-parse --git-path info/exclude 2>/dev/null || true)
|
|
2494
|
+
[ -n "$EXCL" ] || return 0
|
|
2495
|
+
mkdir -p "$(dirname "$EXCL")"
|
|
2496
|
+
grep -qxF "$rel" "$EXCL" 2>/dev/null || echo "$rel" >> "$EXCL"
|
|
2497
|
+
}
|
|
2498
|
+
if [ "$RELAUNCH" -eq 1 ]; then
|
|
2499
|
+
# Retire the previous incarnation's per-task harness wiring before arming the
|
|
2500
|
+
# new one. Without this, a harness switch would leave the old adapter's hook
|
|
2501
|
+
# files and turn-end token registry entries behind, and even a same-harness
|
|
2502
|
+
# relaunch would orphan the retired busy generation's token
|
|
2503
|
+
# (bin/fm-control-lib.sh owns where those artifacts live).
|
|
2504
|
+
clear_relaunch_harness_wiring "$RELAUNCH_PRIOR_HARNESS" "$WT" "$STATE_REAL" "$ID" || {
|
|
2505
|
+
echo "error: could not retire $RELAUNCH_PRIOR_HARNESS wiring for task $ID; refusing to arm the replacement" >&2
|
|
2506
|
+
exit 1
|
|
2507
|
+
}
|
|
2508
|
+
RELAUNCH_REPLACEMENT_PENDING=1
|
|
2509
|
+
RELAUNCH_REPLACEMENT_HARNESS=$HARNESS
|
|
2510
|
+
RELAUNCH_REPLACEMENT_STATE=$STATE_REAL
|
|
2511
|
+
RELAUNCH_REPLACEMENT_WT=$WT
|
|
2512
|
+
fi
|
|
2513
|
+
if [ "$KIND" != secondmate ]; then
|
|
2514
|
+
# Arm the semantic busy-state contract (bin/fm-busy-lib.sh) for every
|
|
2515
|
+
# adapter with a verified semantic source. The launch brief sent below IS a
|
|
2516
|
+
# submitted turn, so the seed record is busy/fm-spawn. The minted gen is
|
|
2517
|
+
# embedded into each adapter's wiring so an event from a superseded
|
|
2518
|
+
# incarnation is rejected as stale. Grok stays on its isolated rendered-tail
|
|
2519
|
+
# fallback and standalone Kimi stays unknown until fm_busy_kimi_verified
|
|
2520
|
+
# opens, so neither is armed here.
|
|
2521
|
+
BUSY_GEN=
|
|
2522
|
+
case "$HARNESS" in
|
|
2523
|
+
codex*)
|
|
2524
|
+
if fm_busy_codex_semantic_source; then
|
|
2525
|
+
echo "error: codex semantic busy-state wiring is not implemented; extend the probe only together with verified wiring" >&2
|
|
2526
|
+
exit 1
|
|
2527
|
+
fi
|
|
2528
|
+
;;
|
|
2529
|
+
esac
|
|
2530
|
+
case "$HARNESS" in
|
|
2531
|
+
claude*|opencode*|pi|pi-signed)
|
|
2532
|
+
BUSY_GEN=$("$FM_ROOT/bin/fm-busy-event.sh" arm "$STATE_REAL" "$ID") || {
|
|
2533
|
+
echo "error: failed to arm the busy-state contract for $ID" >&2
|
|
2534
|
+
exit 1
|
|
2535
|
+
}
|
|
2536
|
+
[ "$RELAUNCH" -ne 1 ] || RELAUNCH_REPLACEMENT_BUSY_GEN=$BUSY_GEN
|
|
2537
|
+
;;
|
|
2538
|
+
kimi*)
|
|
2539
|
+
# Standalone Kimi stays unknown until fm_busy_kimi_verified opens on a
|
|
2540
|
+
# live-verified installed version (bin/fm-busy-lib.sh owns the gate and
|
|
2541
|
+
# the required evidence). Arming without wiring would seed a busy record
|
|
2542
|
+
# nothing can ever clear, so the arm waits for the wiring.
|
|
2543
|
+
if fm_busy_kimi_verified; then
|
|
2544
|
+
echo "error: kimi semantic busy-state wiring is not implemented; open the gate only together with verified wiring" >&2
|
|
2545
|
+
exit 1
|
|
2546
|
+
fi
|
|
2547
|
+
;;
|
|
2548
|
+
esac
|
|
2549
|
+
case "$HARNESS" in
|
|
2550
|
+
claude*)
|
|
2551
|
+
# Semantic busy-state hooks (bin/fm-busy-lib.sh): UserPromptSubmit opens
|
|
2552
|
+
# a turn; Stop (normal completion), StopFailure (API-error turn end),
|
|
2553
|
+
# and SessionEnd (process shutdown) all close it, so an abnormal end can
|
|
2554
|
+
# never leave a stale busy record. Claude fires no hook for a manual
|
|
2555
|
+
# interrupt: fm-control preserves the adapter-owned state, while the
|
|
2556
|
+
# legacy fm-send --key Escape path records idle/fm-interrupt. Stop keeps
|
|
2557
|
+
# the turn-ended NOTIFICATION touch for the watcher. Every
|
|
2558
|
+
# hook command tolerates a refused event (|| true) so a stale-gen writer
|
|
2559
|
+
# can never break Claude's own lifecycle.
|
|
2560
|
+
mkdir -p "$WT/.claude"
|
|
2561
|
+
busy_cmd_prefix="$(shell_quote "$FM_ROOT/bin/fm-busy-event.sh") apply $(shell_quote "$STATE_REAL") $(shell_quote "$ID")"
|
|
2562
|
+
busy_suffix="--gen $(shell_quote "$BUSY_GEN") --source claude-hook"
|
|
2563
|
+
j_submit=$(json_escape "$busy_cmd_prefix busy $busy_suffix --event user-prompt-submit 2>/dev/null || true")
|
|
2564
|
+
j_stop=$(json_escape "touch $(shell_quote "$TURNEND"); $busy_cmd_prefix idle $busy_suffix --event stop 2>/dev/null || true")
|
|
2565
|
+
j_stopfail=$(json_escape "$busy_cmd_prefix idle $busy_suffix --event stop-failure 2>/dev/null || true")
|
|
2566
|
+
j_sessionend=$(json_escape "$busy_cmd_prefix idle $busy_suffix --event session-end 2>/dev/null || true")
|
|
2567
|
+
cat > "$WT/.claude/settings.local.json" <<EOF
|
|
2568
|
+
{"hooks":{"UserPromptSubmit":[{"hooks":[{"type":"command","command":"$j_submit"}]}],"Stop":[{"hooks":[{"type":"command","command":"$j_stop"}]}],"StopFailure":[{"hooks":[{"type":"command","command":"$j_stopfail"}]}],"SessionEnd":[{"hooks":[{"type":"command","command":"$j_sessionend"}]}]}}
|
|
2569
|
+
EOF
|
|
2570
|
+
exclude_path '.claude/settings.local.json'
|
|
2571
|
+
;;
|
|
2572
|
+
opencode*)
|
|
2573
|
+
mkdir -p "$WT/.opencode/plugins"
|
|
2574
|
+
cat > "$WT/.opencode/plugins/fm-busy-state.js" <<EOF
|
|
2575
|
+
// Firstmate semantic busy-state events + turn-end notification; written by
|
|
2576
|
+
// fm-spawn under the contract owned by bin/fm-busy-lib.sh.
|
|
2577
|
+
// Semantic state comes from OpenCode's session.status events: busy and retry
|
|
2578
|
+
// are active, idle is inactive. Scoping latches the first session that
|
|
2579
|
+
// reports activity (the worker's main session - a subagent child session can
|
|
2580
|
+
// only start while the main session is already busy) and ignores other
|
|
2581
|
+
// sessions' status until the latched session settles, so a child's idle can
|
|
2582
|
+
// never clear the worker's busy state. The session.idle touch stays the
|
|
2583
|
+
// watcher's wake NOTIFICATION, never current-state truth.
|
|
2584
|
+
import { execFile } from "node:child_process";
|
|
2585
|
+
const busyEvent = (state, event) =>
|
|
2586
|
+
new Promise((resolve) => {
|
|
2587
|
+
execFile("$FM_ROOT/bin/fm-busy-event.sh", [
|
|
2588
|
+
"apply", "$STATE_REAL", "$ID", state,
|
|
2589
|
+
"--gen", "$BUSY_GEN", "--source", "opencode-plugin", "--event", event,
|
|
2590
|
+
], () => resolve());
|
|
2591
|
+
});
|
|
2592
|
+
export const FmBusyState = async () => {
|
|
2593
|
+
let activeSession = null;
|
|
2594
|
+
return {
|
|
2595
|
+
event: async ({ event }) => {
|
|
2596
|
+
if (event.type === "session.status") {
|
|
2597
|
+
const sessionID = event.properties.sessionID;
|
|
2598
|
+
const statusType = event.properties.status && event.properties.status.type;
|
|
2599
|
+
if (statusType === "busy" || statusType === "retry") {
|
|
2600
|
+
if (activeSession === null) activeSession = sessionID;
|
|
2601
|
+
if (sessionID === activeSession) await busyEvent("busy", "session-" + statusType);
|
|
2602
|
+
return;
|
|
2603
|
+
}
|
|
2604
|
+
if (statusType === "idle" && sessionID === activeSession) {
|
|
2605
|
+
activeSession = null;
|
|
2606
|
+
await busyEvent("idle", "session-status-idle");
|
|
2607
|
+
}
|
|
2608
|
+
return;
|
|
2609
|
+
}
|
|
2610
|
+
if (event.type === "session.idle") {
|
|
2611
|
+
if (event.properties.sessionID === activeSession) {
|
|
2612
|
+
activeSession = null;
|
|
2613
|
+
await busyEvent("idle", "session-idle");
|
|
2614
|
+
}
|
|
2615
|
+
await new Promise((resolve) => {
|
|
2616
|
+
execFile("touch", ["$TURNEND"], () => resolve());
|
|
2617
|
+
});
|
|
2618
|
+
}
|
|
2619
|
+
},
|
|
2620
|
+
};
|
|
2621
|
+
};
|
|
2622
|
+
EOF
|
|
2623
|
+
exclude_path '.opencode/plugins/fm-busy-state.js'
|
|
2624
|
+
;;
|
|
2625
|
+
pi|pi-signed)
|
|
2626
|
+
# Written OUTSIDE the worktree: pi's project-trust gate fires on any extension
|
|
2627
|
+
# loaded from inside the project (verified live), but an explicit -e path
|
|
2628
|
+
# elsewhere loads without a dialog. Lives in state/, cleaned by teardown.
|
|
2629
|
+
cat > "$STATE/$ID.pi-ext.ts" <<EOF
|
|
2630
|
+
// Firstmate semantic busy-state events + turn-end notification; written by
|
|
2631
|
+
// fm-spawn under the contract owned by bin/fm-busy-lib.sh.
|
|
2632
|
+
// Semantic state: "agent_start" -> busy when a low-level agent run begins;
|
|
2633
|
+
// "agent_settled" -> idle only when ctx.isIdle() confirms Pi will not
|
|
2634
|
+
// continue automatically - auto-retries, auto-compaction retries, tool
|
|
2635
|
+
// loops, and queued continuations all keep the run un-settled, and a settle
|
|
2636
|
+
// that raced another extension's fresh run keeps state busy via isIdle().
|
|
2637
|
+
// "turn_end" fires at every inner turn boundary (one LLM response plus its
|
|
2638
|
+
// tool calls) and stays a wake NOTIFICATION touch for the watcher, never
|
|
2639
|
+
// current-state truth.
|
|
2640
|
+
import { execFile } from "node:child_process";
|
|
2641
|
+
const busyEvent = (state: string, event: string) =>
|
|
2642
|
+
new Promise<void>((resolve) => {
|
|
2643
|
+
execFile("$FM_ROOT/bin/fm-busy-event.sh", [
|
|
2644
|
+
"apply", "$STATE_REAL", "$ID", state,
|
|
2645
|
+
"--gen", "$BUSY_GEN", "--source", "pi-ext", "--event", event,
|
|
2646
|
+
], () => resolve());
|
|
2647
|
+
});
|
|
2648
|
+
export default function (pi: any) {
|
|
2649
|
+
pi.on("agent_start", () => busyEvent("busy", "agent-start"));
|
|
2650
|
+
pi.on("agent_settled", (_event: any, ctx: any) => {
|
|
2651
|
+
if (ctx && typeof ctx.isIdle === "function" && !ctx.isIdle()) return;
|
|
2652
|
+
return busyEvent("idle", "agent-settled");
|
|
2653
|
+
});
|
|
2654
|
+
pi.on("turn_end", () => execFile("touch", ["$TURNEND"]));
|
|
2655
|
+
}
|
|
2656
|
+
EOF
|
|
2657
|
+
;;
|
|
2658
|
+
codex*)
|
|
2659
|
+
# Semantic busy-state source negotiation (bin/fm-busy-lib.sh owns the
|
|
2660
|
+
# probes and the evidence). Neither Codex path is usable on the
|
|
2661
|
+
# installed binary: a pane worker's turns are not observable through
|
|
2662
|
+
# the app-server protocol, and its lifecycle hooks did not fire for a
|
|
2663
|
+
# firstmate-launched worker. Codex therefore classifies unknown with
|
|
2664
|
+
# an explicit reason rather than falling back to idle, and no busy
|
|
2665
|
+
# wiring is installed. The turn-end NOTIFICATION marker still rides
|
|
2666
|
+
# the launch command via -c notify=[...] and __TURNEND__.
|
|
2667
|
+
;;
|
|
2668
|
+
grok*)
|
|
2669
|
+
# grok fires a Stop hook at every turn boundary (verified, grok 0.2.73), the
|
|
2670
|
+
# clean equivalent of codex's notify= and pi's turn_end. But grok only loads
|
|
2671
|
+
# PROJECT hooks (<worktree>/.grok/hooks/, <worktree>/.claude/settings.local.json)
|
|
2672
|
+
# after the folder is granted hook-trust, which is not automatic and which
|
|
2673
|
+
# firstmate cannot establish at launch without editing grok's own managed
|
|
2674
|
+
# trust store (a high-blast-radius write). GLOBAL hooks in ~/.grok/hooks/ are
|
|
2675
|
+
# always trusted and load on first launch with no gate. So the turn-end hook
|
|
2676
|
+
# lives OUTSIDE the worktree as a single firstmate-owned global hook that is a
|
|
2677
|
+
# guarded no-op for every non-firstmate grok session: it fires only when the
|
|
2678
|
+
# current workspace holds a .fm-grok-turnend token pointer that matches the
|
|
2679
|
+
# firstmate-owned hook registry. firstmate then drops that per-task pointer
|
|
2680
|
+
# (gitignored, like the other harnesses' worktree hook files).
|
|
2681
|
+
# Result: the hook is outside the worktree, needs no trust grant, and never
|
|
2682
|
+
# touches grok's managed config - only firstmate-owned files.
|
|
2683
|
+
GROK_HOOKS_DIR="${GROK_HOME:-$HOME/.grok}/hooks"
|
|
2684
|
+
GROK_AUTH_DIR="$GROK_HOOKS_DIR/fm-turn-end.d"
|
|
2685
|
+
mkdir -p "$GROK_AUTH_DIR"
|
|
2686
|
+
old_umask=$(umask)
|
|
2687
|
+
umask 077
|
|
2688
|
+
auth_file=$(mktemp "$GROK_AUTH_DIR/fm.XXXXXXXXXXXX")
|
|
2689
|
+
umask "$old_umask"
|
|
2690
|
+
printf '%s\n' "$TURNEND" > "$auth_file"
|
|
2691
|
+
printf '%s\n' "${auth_file##*/}" > "$STATE/$ID.grok-turnend-token"
|
|
2692
|
+
sq_grok_auth_dir=$(shell_quote "$GROK_AUTH_DIR")
|
|
2693
|
+
cat > "$GROK_HOOKS_DIR/fm-turn-end.sh" <<EOF
|
|
2694
|
+
#!/usr/bin/env bash
|
|
2695
|
+
set -u
|
|
2696
|
+
auth_dir=$sq_grok_auth_dir
|
|
2697
|
+
workspace=\${GROK_WORKSPACE_ROOT:-}
|
|
2698
|
+
[ -n "\$workspace" ] || exit 0
|
|
2699
|
+
p="\$workspace/.fm-grok-turnend"
|
|
2700
|
+
[ -f "\$p" ] || exit 0
|
|
2701
|
+
first=
|
|
2702
|
+
IFS= read -r -n 256 first < "\$p" 2>/dev/null || [ -n "\$first" ] || exit 0
|
|
2703
|
+
case "\$first" in token=*) token=\${first#token=} ;; *) exit 0 ;; esac
|
|
2704
|
+
case "\$token" in fm.????????????) : ;; *) exit 0 ;; esac
|
|
2705
|
+
case "\$token" in *[!A-Za-z0-9._-]*) exit 0 ;; esac
|
|
2706
|
+
t=\$(cat "\$auth_dir/\$token" 2>/dev/null) || exit 0
|
|
2707
|
+
case "\$t" in /*.turn-ended) : ;; *) exit 0 ;; esac
|
|
2708
|
+
touch "\$t" 2>/dev/null || true
|
|
2709
|
+
exit 0
|
|
2710
|
+
EOF
|
|
2711
|
+
chmod +x "$GROK_HOOKS_DIR/fm-turn-end.sh"
|
|
2712
|
+
hook_command=$(json_escape "bash $(shell_quote "$GROK_HOOKS_DIR/fm-turn-end.sh")")
|
|
2713
|
+
printf '{"hooks":{"Stop":[{"hooks":[{"type":"command","command":"%s"}]}]}}\n' "$hook_command" > "$GROK_HOOKS_DIR/fm-turn-end.json"
|
|
2714
|
+
printf 'token=%s\n' "${auth_file##*/}" > "$WT/.fm-grok-turnend"
|
|
2715
|
+
exclude_path '.fm-grok-turnend'
|
|
2716
|
+
;;
|
|
2717
|
+
muse*)
|
|
2718
|
+
# muse's turn lifecycle is neither a hook nor a launch flag: its plugin
|
|
2719
|
+
# engine (the only hook surface) is disabled in the default build, so
|
|
2720
|
+
# firstmate reads muse's own durable session event log instead
|
|
2721
|
+
# (bin/fm-busy-lib.sh owns the fold). That is a PULL
|
|
2722
|
+
# source with no writer, so nothing is armed and no record is seeded -
|
|
2723
|
+
# exactly the reason standalone Kimi is not armed either.
|
|
2724
|
+
# This sidecar is the whole binding: it pins the sessions root, the
|
|
2725
|
+
# workspace root that muse records in each log's metadata, this pane's
|
|
2726
|
+
# binding identity, and every matching main log that predates this pane.
|
|
2727
|
+
# The classifier then accepts only one new matching log, so it never
|
|
2728
|
+
# guesses between pane incarnations. Recording the resolved root here
|
|
2729
|
+
# also means a later change to XDG_DATA_HOME cannot silently re-point an
|
|
2730
|
+
# already-running task at a different log tree.
|
|
2731
|
+
MUSE_SESSIONS_ROOT="${MUSE_DATA_HOME:-${XDG_DATA_HOME:-$HOME/.local/share}}/muse/sessions"
|
|
2732
|
+
MUSE_BINDING_ID="$$.$RANDOM.$(date +%s)"
|
|
2733
|
+
rm -f "$STATE/$ID.muse-session-current"
|
|
2734
|
+
{
|
|
2735
|
+
printf 'sessions_root=%s\n' "$MUSE_SESSIONS_ROOT"
|
|
2736
|
+
printf 'workspace_root=%s\n' "$WT"
|
|
2737
|
+
printf 'binding_id=%s\n' "$MUSE_BINDING_ID"
|
|
2738
|
+
while IFS= read -r MUSE_PRIOR_LOG; do
|
|
2739
|
+
[ -n "$MUSE_PRIOR_LOG" ] && printf 'prior_log=%s\n' "$MUSE_PRIOR_LOG"
|
|
2740
|
+
done <<EOF
|
|
2741
|
+
$(fm_busy_muse_matching_logs "$MUSE_SESSIONS_ROOT" "$WT" || true)
|
|
2742
|
+
EOF
|
|
2743
|
+
} > "$STATE/$ID.muse-session"
|
|
2744
|
+
;;
|
|
2745
|
+
cursor*)
|
|
2746
|
+
# Cursor's turn lifecycle is neither a hook nor a launch flag: it writes
|
|
2747
|
+
# its own durable per-conversation transcript and brackets every turn
|
|
2748
|
+
# there (bin/fm-busy-lib.sh owns the fold). Like muse that is a PULL
|
|
2749
|
+
# source with no writer, so nothing is armed and no record is seeded.
|
|
2750
|
+
# This sidecar is the whole binding. It pins the projects root and the
|
|
2751
|
+
# exact workspace path cursor records in each project's
|
|
2752
|
+
# .workspace-trusted, plus every conversation that already exists for
|
|
2753
|
+
# that workspace, so a relaunch into a reused worktree folds its OWN
|
|
2754
|
+
# conversation instead of its predecessor's. The classifier then accepts
|
|
2755
|
+
# only one remaining conversation and never guesses between incarnations.
|
|
2756
|
+
CURSOR_PROJECTS_ROOT="${CURSOR_PROJECTS_ROOT_OVERRIDE:-$HOME/.cursor/projects}"
|
|
2757
|
+
{
|
|
2758
|
+
printf 'projects_root=%s\n' "$CURSOR_PROJECTS_ROOT"
|
|
2759
|
+
printf 'workspace_root=%s\n' "$WT"
|
|
2760
|
+
if CURSOR_PRIOR_PROJECT=$(fm_busy_cursor_project_dir "$CURSOR_PROJECTS_ROOT" "$WT" 2>/dev/null); then
|
|
2761
|
+
for CURSOR_PRIOR_DIR in "$CURSOR_PRIOR_PROJECT"/agent-transcripts/*/; do
|
|
2762
|
+
[ -d "$CURSOR_PRIOR_DIR" ] || continue
|
|
2763
|
+
printf 'prior_conversation=%s\n' "$(basename -- "${CURSOR_PRIOR_DIR%/}")"
|
|
2764
|
+
done
|
|
2765
|
+
fi
|
|
2766
|
+
} > "$STATE/$ID.cursor-session"
|
|
2767
|
+
;;
|
|
2768
|
+
kimi*)
|
|
2769
|
+
# Kimi's Stop hook is global, but it is inert unless cwd contains this
|
|
2770
|
+
# task's token pointer and the token resolves through Firstmate's private
|
|
2771
|
+
# registry. The installer above owns the format-preserving config edit and
|
|
2772
|
+
# the always-zero, silent hook script.
|
|
2773
|
+
KIMI_AUTH_DIR="$HOME/.kimi-code/fm-turn-end.d"
|
|
2774
|
+
old_umask=$(umask)
|
|
2775
|
+
umask 077
|
|
2776
|
+
auth_file=$(mktemp "$KIMI_AUTH_DIR/fm.XXXXXXXXXXXX")
|
|
2777
|
+
umask "$old_umask"
|
|
2778
|
+
printf '%s\n' "$TURNEND" > "$auth_file"
|
|
2779
|
+
printf '%s\n' "${auth_file##*/}" > "$STATE/$ID.kimi-turnend-token"
|
|
2780
|
+
printf 'token=%s\n' "${auth_file##*/}" > "$WT/.fm-kimi-turnend"
|
|
2781
|
+
exclude_path '.fm-kimi-turnend'
|
|
2782
|
+
;;
|
|
2783
|
+
esac
|
|
2784
|
+
fi
|
|
2785
|
+
|
|
2786
|
+
# Delivery posture recorded in meta so fm-teardown's safety check and the
|
|
2787
|
+
# validate/merge stages can branch on it. A ship task carries the explicit
|
|
2788
|
+
# per-task decision validated above; a secondmate's posture is fixed; a scout
|
|
2789
|
+
# records none at all, because its deliverable is a report rather than a merge
|
|
2790
|
+
# (fm-teardown.sh defaults an absent mode to no-mistakes, and fm-promote.sh
|
|
2791
|
+
# requires an explicit mode when a scout is promoted to a ship task).
|
|
2792
|
+
if [ "$KIND" = secondmate ]; then
|
|
2793
|
+
MODE=secondmate
|
|
2794
|
+
YOLO=off
|
|
2795
|
+
: "${SECONDMATE_PROJECTS:=}"
|
|
2796
|
+
elif [ "$KIND" = scout ]; then
|
|
2797
|
+
MODE=
|
|
2798
|
+
YOLO=
|
|
2799
|
+
fi
|
|
2800
|
+
|
|
2801
|
+
# Resolve the optional default-off W3C trace context (bin/fm-trace-context-lib.sh,
|
|
2802
|
+
# docs/configuration.md): the one carrier both recorded in meta and injected into
|
|
2803
|
+
# the pane, so an observer reads exactly what the child receives. Empty only when
|
|
2804
|
+
# disabled or on entropy/validation failure. Reuses this task's already-recorded
|
|
2805
|
+
# value on relaunch; any other spawn roots a fresh trace, never adopting this
|
|
2806
|
+
# process's own ambient TRACEPARENT, so each routed task is its own trace
|
|
2807
|
+
# boundary even under a persistent supervisor. Never aborts the spawn and adds
|
|
2808
|
+
# only the cost of reading a few bytes of entropy.
|
|
2809
|
+
#
|
|
2810
|
+
# The session-start path owns input resolution. Spawn consumes only the frozen
|
|
2811
|
+
# home-session state and reuses it for the carrier and Secondmate launch prefix.
|
|
2812
|
+
#
|
|
2813
|
+
# A remote secondmate launch is the one case where this process is not the home
|
|
2814
|
+
# that owns the task's identity: the parent home resolved and will record the
|
|
2815
|
+
# carrier, and this host only delivers it. The validated --traceparent value
|
|
2816
|
+
# then IS the decision, so the enablement snapshot handed to the new Secondmate
|
|
2817
|
+
# agrees with the carrier it receives exactly as on the local path.
|
|
2818
|
+
if [ "$TRACEPARENT_SET" -eq 1 ]; then
|
|
2819
|
+
SPAWN_TRACE_EFFECTIVE=on
|
|
2820
|
+
SPAWN_TRACEPARENT=$TRACEPARENT_ARG
|
|
2821
|
+
else
|
|
2822
|
+
SPAWN_TRACE_EFFECTIVE=$(fm_trace_context_session_effective "$STATE/.trace-context-effective")
|
|
2823
|
+
if [ "$SPAWN_TRACE_EFFECTIVE" = on ]; then
|
|
2824
|
+
SPAWN_TRACEPARENT=$(FM_TRACE_CONTEXT=on fm_trace_context_resolve "$CONFIG" "$STATE/$ID.meta" || true)
|
|
2825
|
+
else
|
|
2826
|
+
SPAWN_TRACEPARENT=
|
|
2827
|
+
fi
|
|
2828
|
+
fi
|
|
2829
|
+
|
|
2830
|
+
META_WINDOW=$T
|
|
2831
|
+
[ "$BACKEND" = orca ] && META_WINDOW=$W
|
|
2832
|
+
SPAWN_GEN="s$(date +%s).${BASHPID:-$$}.$RANDOM"
|
|
2833
|
+
SPAWN_META_PATH="$STATE/$ID.meta"
|
|
2834
|
+
if [ "$SPAWN_META_LOCK_HELD" != 1 ]; then
|
|
2835
|
+
SPAWN_META_LOCK=$(fm_meta_lock_path "$STATE/$ID.meta") || exit 1
|
|
2836
|
+
fm_lock_acquire_wait "$SPAWN_META_LOCK"
|
|
2837
|
+
SPAWN_META_LOCK_HELD=1
|
|
2838
|
+
fi
|
|
2839
|
+
if [ "$RELAUNCH" -eq 1 ]; then
|
|
2840
|
+
SPAWN_META_TMP="$STATE/.$ID.meta.relaunch.${BASHPID:-$$}"
|
|
2841
|
+
else
|
|
2842
|
+
SPAWN_META_TMP="$STATE/.$ID.meta.spawn.${BASHPID:-$$}"
|
|
2843
|
+
SPAWN_FRESH_COMMIT_PENDING=1
|
|
2844
|
+
fi
|
|
2845
|
+
SPAWN_META_PATH=$SPAWN_META_TMP
|
|
2846
|
+
preserve_relaunch_meta() {
|
|
2847
|
+
awk -F= '
|
|
2848
|
+
BEGIN {
|
|
2849
|
+
split("window endpoint_task_id worktree project harness kind mode yolo tasktmp model effort busy_gen spawn_gen traceparent backend herdr_session herdr_workspace_id herdr_tab_id herdr_pane_id zellij_session zellij_tab_id zellij_pane_id orca_worktree_id terminal cmux_workspace_id cmux_surface_id home projects control_relaunch_tx", keys, " ")
|
|
2850
|
+
for (i in keys) owned[keys[i]] = 1
|
|
2851
|
+
}
|
|
2852
|
+
!($1 in owned)
|
|
2853
|
+
' "$RELAUNCH_META"
|
|
2854
|
+
}
|
|
2855
|
+
{
|
|
2856
|
+
echo "window=$META_WINDOW"
|
|
2857
|
+
echo "endpoint_task_id=$ID"
|
|
2858
|
+
echo "worktree=$WT"
|
|
2859
|
+
echo "project=$PROJ_ABS"
|
|
2860
|
+
echo "harness=$HARNESS"
|
|
2861
|
+
echo "kind=$KIND"
|
|
2862
|
+
[ -z "$MODE" ] || echo "mode=$MODE"
|
|
2863
|
+
[ -z "$YOLO" ] || echo "yolo=$YOLO"
|
|
2864
|
+
echo "tasktmp=$TASK_TMP"
|
|
2865
|
+
echo "model=${MODEL:-default}"
|
|
2866
|
+
echo "effort=${EFFORT:-default}"
|
|
2867
|
+
[ -z "${BUSY_GEN:-}" ] || echo "busy_gen=$BUSY_GEN"
|
|
2868
|
+
echo "spawn_gen=$SPAWN_GEN"
|
|
2869
|
+
# Default-off writes no traceparent= line.
|
|
2870
|
+
# backend= is written only for a non-default (non-tmux) backend, so the
|
|
2871
|
+
# default path's meta stays byte-identical (absent backend= means tmux;
|
|
2872
|
+
# data/fm-backend-design-d7's P1 compatibility contract).
|
|
2873
|
+
[ "$BACKEND" = tmux ] || echo "backend=$BACKEND"
|
|
2874
|
+
if [ "$BACKEND" = herdr ]; then
|
|
2875
|
+
echo "herdr_session=$HERDR_SES"
|
|
2876
|
+
echo "herdr_workspace_id=$HERDR_WORKSPACE_ID"
|
|
2877
|
+
echo "herdr_tab_id=$HERDR_TAB_ID"
|
|
2878
|
+
echo "herdr_pane_id=$HERDR_PANE_ID"
|
|
2879
|
+
fi
|
|
2880
|
+
if [ "$BACKEND" = zellij ]; then
|
|
2881
|
+
echo "zellij_session=$ZELLIJ_SES"
|
|
2882
|
+
echo "zellij_tab_id=$ZELLIJ_TAB_ID"
|
|
2883
|
+
echo "zellij_pane_id=$ZELLIJ_PANE_ID"
|
|
2884
|
+
fi
|
|
2885
|
+
if [ "$BACKEND" = orca ]; then
|
|
2886
|
+
echo "orca_worktree_id=$ORCA_WORKTREE_ID"
|
|
2887
|
+
echo "terminal=$ORCA_TERMINAL"
|
|
2888
|
+
fi
|
|
2889
|
+
if [ "$BACKEND" = cmux ]; then
|
|
2890
|
+
echo "cmux_workspace_id=$CMUX_WORKSPACE_ID"
|
|
2891
|
+
echo "cmux_surface_id=$CMUX_SURFACE_ID"
|
|
2892
|
+
fi
|
|
2893
|
+
if [ "$KIND" = secondmate ]; then
|
|
2894
|
+
echo "home=$PROJ_ABS"
|
|
2895
|
+
echo "projects=$SECONDMATE_PROJECTS"
|
|
2896
|
+
fi
|
|
2897
|
+
if [ "$RELAUNCH" -eq 1 ]; then
|
|
2898
|
+
preserve_relaunch_meta
|
|
2899
|
+
fi
|
|
2900
|
+
if [ "$SPAWN_CONTROL_PARENT" = 1 ] && [ -n "${FM_CONTROL_RELAUNCH_TX:-}" ]; then
|
|
2901
|
+
echo "control_relaunch_tx=$FM_CONTROL_RELAUNCH_TX"
|
|
2902
|
+
fi
|
|
2903
|
+
} > "$SPAWN_META_PATH" || {
|
|
2904
|
+
echo "error: task record for $ID could not be prepared at $SPAWN_META_PATH" >&2
|
|
2905
|
+
exit 1
|
|
2906
|
+
}
|
|
2907
|
+
if [ "$RELAUNCH" -eq 0 ]; then
|
|
2908
|
+
if ! fm_backlog_atomic_transition publish "$SPAWN_META_TMP" "$STATE/$ID.meta" "task record" "$STATE"; then
|
|
2909
|
+
echo "error: task record for $ID could not be published ($FM_BACKLOG_TRANSITION_ERROR)" >&2
|
|
2910
|
+
exit 1
|
|
2911
|
+
fi
|
|
2912
|
+
SPAWN_META_TMP=
|
|
2913
|
+
fi
|
|
2914
|
+
|
|
2915
|
+
# Fuse the backlog In-flight transition into the publication that just created
|
|
2916
|
+
# the record (bin/fm-backlog-transition-lib.sh owns the invariant). It runs under
|
|
2917
|
+
# this task's own meta lock, so a steer or teardown racing the same id stays
|
|
2918
|
+
# serialized exactly as before. The call itself is deferred to the final commit
|
|
2919
|
+
# point below so every earlier launch-delivery failure remains unwindable.
|
|
2920
|
+
spawn_commit_backlog_transition() {
|
|
2921
|
+
[ "$BACKLOG_TRANSITION" = 1 ] || return 0
|
|
2922
|
+
fm_backlog_atomic_transition dispatch "$STATE/$ID.meta" "$DATA" "$ID" "$STATE"
|
|
2923
|
+
}
|
|
2924
|
+
|
|
2925
|
+
if [ "$RELAUNCH" -eq 1 ]; then
|
|
2926
|
+
SPAWN_META_PUBLISH_STARTED=1
|
|
2927
|
+
if ! fm_backlog_atomic_transition publish "$SPAWN_META_TMP" "$STATE/$ID.meta" "task record" "$STATE"; then
|
|
2928
|
+
echo "error: replacement task record for $ID could not be published ($FM_BACKLOG_TRANSITION_ERROR)" >&2
|
|
2929
|
+
exit 1
|
|
2930
|
+
fi
|
|
2931
|
+
RELAUNCH_REPLACEMENT_PENDING=0
|
|
2932
|
+
SPAWN_META_PUBLISH_STARTED=0
|
|
2933
|
+
SPAWN_META_TMP=
|
|
2934
|
+
fi
|
|
2935
|
+
# A dispatch or relaunch keeps the per-task meta lock through launch delivery.
|
|
2936
|
+
# The backlog mutation is deliberately the final fallible commit below, so
|
|
2937
|
+
# teardown cannot remove a relaunched record while its replacement worker is
|
|
2938
|
+
# still being delivered, cannot observe or complete a fresh provisional record
|
|
2939
|
+
# between its state check and `tasks-axi start`, and a delivery failure cannot
|
|
2940
|
+
# follow a committed In-flight transition.
|
|
2941
|
+
if [ "$SPAWN_TASK_SET_LOCK_HELD" = 1 ]; then
|
|
2942
|
+
# The record is published, so this task is now part of the set a teardown
|
|
2943
|
+
# enumerates and locks per task. The set lock is only needed across that
|
|
2944
|
+
# publication.
|
|
2945
|
+
SPAWN_TASK_SET_LOCK_HELD=0
|
|
2946
|
+
fm_lock_release "$SPAWN_TASK_SET_LOCK"
|
|
2947
|
+
fi
|
|
2948
|
+
"$SCRIPT_DIR/fm-home-summary-refresh.sh" --best-effort || true
|
|
2949
|
+
[ "$BACKEND" = orca ] && ORCA_ABORT_CLEANUP=0
|
|
2950
|
+
|
|
2951
|
+
sq_brief=$(shell_quote "$BRIEF")
|
|
2952
|
+
sq_turnend=$(shell_quote "$TURNEND")
|
|
2953
|
+
sq_piext=$(shell_quote "$STATE/$ID.pi-ext.ts")
|
|
2954
|
+
sq_piturnend=$(shell_quote "$PROJ_ABS/.pi/extensions/fm-primary-turnend-guard.ts")
|
|
2955
|
+
sq_piwatch=$(shell_quote "$PROJ_ABS/.pi/extensions/fm-primary-pi-watch.ts")
|
|
2956
|
+
sq_opinput=$(shell_quote "$FM_ROOT/bin/fm-operational-input.sh")
|
|
2957
|
+
sq_worktree=$(shell_quote "$WT")
|
|
2958
|
+
MODELFLAG=$(model_flag_for_harness "$HARNESS" "$MODEL")
|
|
2959
|
+
EFFORTFLAG=$(effort_flag_for_harness "$HARNESS" "$EFFORT")
|
|
2960
|
+
LAUNCH=${LAUNCH//__MODELFLAG__/$MODELFLAG}
|
|
2961
|
+
LAUNCH=${LAUNCH//__EFFORTFLAG__/$EFFORTFLAG}
|
|
2962
|
+
LAUNCH=${LAUNCH//__BRIEF__/$sq_brief}
|
|
2963
|
+
LAUNCH=${LAUNCH//__TURNEND__/$sq_turnend}
|
|
2964
|
+
LAUNCH=${LAUNCH//__PIEXT__/$sq_piext}
|
|
2965
|
+
LAUNCH=${LAUNCH//__PITURNEND__/$sq_piturnend}
|
|
2966
|
+
LAUNCH=${LAUNCH//__PIWATCH__/$sq_piwatch}
|
|
2967
|
+
LAUNCH=${LAUNCH//__OPINPUT__/$sq_opinput}
|
|
2968
|
+
case "$HARNESS" in
|
|
2969
|
+
pi|pi-signed) LAUNCH=${LAUNCH//__PIBIN__/"$(shell_quote "$PI_BIN")"} ;;
|
|
2970
|
+
cursor) LAUNCH=${LAUNCH//__CURSORBIN__/"$(shell_quote "$CURSOR_BIN")"} ;;
|
|
2971
|
+
esac
|
|
2972
|
+
LAUNCH=${LAUNCH//__WORKTREE__/$sq_worktree}
|
|
2973
|
+
case "$HARNESS" in
|
|
2974
|
+
claude|codex|opencode|pi|pi-signed|grok|kimi|muse)
|
|
2975
|
+
LAUNCH="env -u CURSOR_AGENT -u CURSOR_INVOKED_AS $LAUNCH"
|
|
2976
|
+
;;
|
|
2977
|
+
esac
|
|
2978
|
+
# Crewmate panes are created by a long-lived tmux/herdr daemon that does not
|
|
2979
|
+
# inherit firstmate's current environment, so a bare `claude` in the pane falls
|
|
2980
|
+
# back to the default ~/.claude store even when firstmate itself runs under a
|
|
2981
|
+
# different CLAUDE_CONFIG_DIR (for example a work-vs-personal subscription split).
|
|
2982
|
+
# Forward firstmate's own resolved store onto the claude launch so the crewmate
|
|
2983
|
+
# uses the same credential/config firstmate is authenticated with. Only when set;
|
|
2984
|
+
# an unset value is the single-store default and needs no prefix.
|
|
2985
|
+
if [ "$HARNESS" = claude ] && [ -n "${CLAUDE_CONFIG_DIR:-}" ]; then
|
|
2986
|
+
LAUNCH="CLAUDE_CONFIG_DIR=$(shell_quote "$CLAUDE_CONFIG_DIR") $LAUNCH"
|
|
2987
|
+
fi
|
|
2988
|
+
if [ "$KIND" = secondmate ]; then
|
|
2989
|
+
sq_home=$(shell_quote "$PROJ_ABS")
|
|
2990
|
+
sq_primary_home=$(shell_quote "$FM_HOME")
|
|
2991
|
+
# Keep this in step with fm_supervision_model (bin/fm-wake-lib.sh): Claude's
|
|
2992
|
+
# Stop auto-arm and Cursor's stop-hook park both run the watcher only BETWEEN
|
|
2993
|
+
# turns, so a fresh beacon with no live watcher is their healthy mid-turn state.
|
|
2994
|
+
case "$HARNESS" in
|
|
2995
|
+
claude|cursor) supervision_model=autoarm ;;
|
|
2996
|
+
*) supervision_model=persistent ;;
|
|
2997
|
+
esac
|
|
2998
|
+
# Deliver the primary's EFFECTIVE trace-context decision as a normalized on/off
|
|
2999
|
+
# literal (never the raw FM_TRACE_CONTEXT string) so a FM_TRACE_CONTEXT override
|
|
3000
|
+
# on the primary reaches the secondmate's OWN workers, not just the copied
|
|
3001
|
+
# config/trace-context file: otherwise off would not disable them and on would
|
|
3002
|
+
# not enable them across the launch boundary (bin/fm-trace-context-lib.sh header).
|
|
3003
|
+
# Reuse the single frozen decision from the carrier resolution above so the
|
|
3004
|
+
# injected carrier and this on/off snapshot are guaranteed to agree.
|
|
3005
|
+
LAUNCH="FM_ROOT_OVERRIDE= FM_STATE_OVERRIDE= FM_DATA_OVERRIDE= FM_PROJECTS_OVERRIDE= FM_CONFIG_OVERRIDE= FM_PUBLIC_FOLLOWUP_PRIMARY_HOME=$sq_primary_home FM_HOME=$sq_home FM_TRACE_CONTEXT=$SPAWN_TRACE_EFFECTIVE FM_SUPERVISION_MODEL=$supervision_model $LAUNCH"
|
|
3006
|
+
fi
|
|
3007
|
+
if [ -z "$SPAWN_TRACEPARENT" ] && [ "$RELAUNCH" -eq 1 ]; then
|
|
3008
|
+
LAUNCH="unset TRACEPARENT; $LAUNCH"
|
|
3009
|
+
fi
|
|
3010
|
+
|
|
3011
|
+
spawn_record_traceparent() {
|
|
3012
|
+
local meta="$STATE/$ID.meta" status=0 acquired=0
|
|
3013
|
+
# Fresh publication still owns the lock. Relaunch deliberately uses a short
|
|
3014
|
+
# independent critical section so other metadata interfaces can serialize.
|
|
3015
|
+
if [ "$SPAWN_META_LOCK_HELD" != 1 ]; then
|
|
3016
|
+
SPAWN_META_LOCK=$(fm_meta_lock_path "$meta") || return 1
|
|
3017
|
+
fm_lock_acquire_wait "$SPAWN_META_LOCK"
|
|
3018
|
+
SPAWN_META_LOCK_HELD=1
|
|
3019
|
+
acquired=1
|
|
3020
|
+
fi
|
|
3021
|
+
SPAWN_META_TMP="$STATE/.$ID.meta.trace.${BASHPID:-$$}"
|
|
3022
|
+
if [ ! -f "$meta" ] || [ ! -w "$meta" ] \
|
|
3023
|
+
|| ! awk -F= '$1 != "traceparent"' "$meta" > "$SPAWN_META_TMP" \
|
|
3024
|
+
|| ! printf 'traceparent=%s\n' "$SPAWN_TRACEPARENT" >> "$SPAWN_META_TMP" \
|
|
3025
|
+
|| ! fm_backlog_atomic_transition publish "$SPAWN_META_TMP" "$meta" "task record" "$STATE"; then
|
|
3026
|
+
status=1
|
|
3027
|
+
rm -f "$SPAWN_META_TMP" 2>/dev/null || true
|
|
3028
|
+
fi
|
|
3029
|
+
SPAWN_META_TMP=
|
|
3030
|
+
if [ "$acquired" = 1 ]; then
|
|
3031
|
+
fm_lock_release "$SPAWN_META_LOCK" || status=1
|
|
3032
|
+
SPAWN_META_LOCK_HELD=0
|
|
3033
|
+
fi
|
|
3034
|
+
return "$status"
|
|
3035
|
+
}
|
|
3036
|
+
|
|
3037
|
+
# Export GOTMPDIR into the crewmate's pane shell so the agent and every child
|
|
3038
|
+
# process (go build, go test, ...) inherit it. Sent before the launch command so
|
|
3039
|
+
# the env is set when the agent starts; the brief sleep lets the export land.
|
|
3040
|
+
spawn_send_text_line "$T" "export GOTMPDIR=$TASK_TMP/gotmp"
|
|
3041
|
+
# Send through the exact channel that already ships GOTMPDIR, so every backend
|
|
3042
|
+
# and harness - ship, scout, and secondmate - gets it before launch. Skipped
|
|
3043
|
+
# entirely when trace context is off.
|
|
3044
|
+
if [ -n "$SPAWN_TRACEPARENT" ]; then
|
|
3045
|
+
if spawn_send_text_line "$T" "export TRACEPARENT=$SPAWN_TRACEPARENT"; then
|
|
3046
|
+
if ! spawn_record_traceparent; then
|
|
3047
|
+
LAUNCH="unset TRACEPARENT; $LAUNCH"
|
|
3048
|
+
fi
|
|
3049
|
+
else
|
|
3050
|
+
TRACE_SEND_STATUS=$?
|
|
3051
|
+
if [ "$TRACE_SEND_STATUS" -eq 2 ]; then
|
|
3052
|
+
echo "error: trace-context input could not be cleared for $W; refusing to append the launch command" >&2
|
|
3053
|
+
exit 1
|
|
3054
|
+
fi
|
|
3055
|
+
LAUNCH="unset TRACEPARENT; $LAUNCH"
|
|
3056
|
+
fi
|
|
3057
|
+
fi
|
|
3058
|
+
sleep 0.3
|
|
3059
|
+
spawn_send_literal "$T" "$LAUNCH"
|
|
3060
|
+
sleep 0.3
|
|
3061
|
+
if [ "${HERDR_PROJECTED:-0}" -eq 1 ]; then
|
|
3062
|
+
HERDR_PROJECTION_ABORT_CLEANUP=0
|
|
3063
|
+
spawn_herdr_presentation_order_lock_release
|
|
3064
|
+
fi
|
|
3065
|
+
spawn_send_key "$T" Enter
|
|
3066
|
+
if [ "$HARNESS" = kimi ]; then
|
|
3067
|
+
if ! kimi_wait_for_ready; then
|
|
3068
|
+
kimi_spawn_fail "kimi did not show a verified ready signal before brief delivery"
|
|
3069
|
+
exit 1
|
|
3070
|
+
fi
|
|
3071
|
+
KIMI_POINTER="Read the brief at $BRIEF_REAL and follow it exactly."
|
|
3072
|
+
KIMI_SUBMIT_RETRIES=${FM_KIMI_SUBMIT_RETRIES:-3}
|
|
3073
|
+
KIMI_SUBMIT_SLEEP=${FM_KIMI_SUBMIT_SLEEP:-${FM_KIMI_POLL_INTERVAL:-0.5}}
|
|
3074
|
+
KIMI_SUBMIT_SETTLE=${FM_KIMI_SUBMIT_SETTLE:-0}
|
|
3075
|
+
if ! KIMI_SUBMIT_VERDICT=$(fm_backend_send_text_submit \
|
|
3076
|
+
"$BACKEND" "$T" "$KIMI_POINTER" "$KIMI_SUBMIT_RETRIES" \
|
|
3077
|
+
"$KIMI_SUBMIT_SLEEP" "$KIMI_SUBMIT_SETTLE" "$W"); then
|
|
3078
|
+
kimi_spawn_fail "kimi brief pointer could not be submitted"
|
|
3079
|
+
exit 1
|
|
3080
|
+
fi
|
|
3081
|
+
if [ "$KIMI_SUBMIT_VERDICT" = send-failed ]; then
|
|
3082
|
+
kimi_spawn_fail "kimi brief pointer could not be submitted"
|
|
3083
|
+
exit 1
|
|
3084
|
+
fi
|
|
3085
|
+
if ! kimi_wait_for_delivery; then
|
|
3086
|
+
kimi_spawn_fail "kimi brief pointer delivery was not confirmed"
|
|
3087
|
+
exit 1
|
|
3088
|
+
fi
|
|
3089
|
+
fi
|
|
3090
|
+
if [ "$KIND" = secondmate ] && [ "${FM_SKIP_SECONDMATE_INHERIT:-0}" != 1 ]; then
|
|
3091
|
+
if ! fm_config_reread_discard_pending "$PROJ_ABS" "$ID" "$FM_HOME"; then
|
|
3092
|
+
if fm_config_reread_quarantine_pending "$PROJ_ABS" "$ID" "$FM_HOME"; then
|
|
3093
|
+
echo "CONFIG_REREAD: secondmate $ID: quarantined pre-relaunch generations after cleanup failure (destination=$PROJ_ABS/state/.fm-inherited-config-reread-quarantine source=$FM_HOME/state/.fm-inherited-config-reread-quarantine)" >&2
|
|
3094
|
+
else
|
|
3095
|
+
echo "CONFIG_REREAD: secondmate $ID: cleanup failed; pre-relaunch generations were force-cleared where possible (destination=$PROJ_ABS source=$FM_HOME)" >&2
|
|
3096
|
+
fi
|
|
3097
|
+
fi
|
|
3098
|
+
fi
|
|
3099
|
+
|
|
3100
|
+
# This is the commit point: all endpoint and harness delivery that can reject
|
|
3101
|
+
# the spawn has succeeded. Re-read and transition while holding the same
|
|
3102
|
+
# per-task lock as metadata publication, then and only then report success.
|
|
3103
|
+
if [ "$SPAWN_META_LOCK_HELD" != 1 ]; then
|
|
3104
|
+
SPAWN_META_LOCK=$(fm_meta_lock_path "$STATE/$ID.meta") || exit 1
|
|
3105
|
+
fm_lock_acquire_wait "$SPAWN_META_LOCK"
|
|
3106
|
+
SPAWN_META_LOCK_HELD=1
|
|
3107
|
+
fi
|
|
3108
|
+
SPAWN_DEFERRED_SIGNAL=
|
|
3109
|
+
if [ "$BACKLOG_TRANSITION" = 1 ]; then
|
|
3110
|
+
trap 'SPAWN_DEFERRED_SIGNAL=HUP' HUP
|
|
3111
|
+
trap 'SPAWN_DEFERRED_SIGNAL=INT' INT
|
|
3112
|
+
trap 'SPAWN_DEFERRED_SIGNAL=TERM' TERM
|
|
3113
|
+
fi
|
|
3114
|
+
SPAWN_BACKLOG_COMMIT_STATUS=0
|
|
3115
|
+
if spawn_commit_backlog_transition; then
|
|
3116
|
+
SPAWN_FRESH_COMMIT_PENDING=0
|
|
3117
|
+
else
|
|
3118
|
+
SPAWN_BACKLOG_COMMIT_STATUS=$?
|
|
3119
|
+
if spawn_commit_backlog_transition; then
|
|
3120
|
+
SPAWN_BACKLOG_COMMIT_STATUS=0
|
|
3121
|
+
SPAWN_FRESH_COMMIT_PENDING=0
|
|
3122
|
+
fi
|
|
3123
|
+
fi
|
|
3124
|
+
if [ "$SPAWN_BACKLOG_COMMIT_STATUS" -ne 0 ]; then
|
|
3125
|
+
if [ "$RELAUNCH" -eq 0 ]; then
|
|
3126
|
+
if spawn_fresh_commit_rollback; then
|
|
3127
|
+
echo "error: task $ID's backlog item could not be moved to In flight ($FM_BACKLOG_TRANSITION_ERROR); its record was removed so no worker is left that the backlog does not own - close out endpoint $T and local copy $WT by hand, then re-run the spawn" >&2
|
|
3128
|
+
else
|
|
3129
|
+
echo "error: task $ID's backlog item could not be moved to In flight ($FM_BACKLOG_TRANSITION_ERROR), and failed-dispatch cleanup is incomplete; the provisional record may remain at $STATE/$ID.meta - close out endpoint $T and local copy $WT by hand, then remove the record and busy state before retrying" >&2
|
|
3130
|
+
fi
|
|
3131
|
+
else
|
|
3132
|
+
echo "error: task $ID was republished but its backlog item could not be moved to In flight ($FM_BACKLOG_TRANSITION_ERROR); fix the backlog and re-run the relaunch" >&2
|
|
3133
|
+
fi
|
|
3134
|
+
fi
|
|
3135
|
+
trap - HUP INT TERM
|
|
3136
|
+
if [ "$SPAWN_BACKLOG_COMMIT_STATUS" -ne 0 ]; then
|
|
3137
|
+
exit "$SPAWN_BACKLOG_COMMIT_STATUS"
|
|
3138
|
+
fi
|
|
3139
|
+
fm_lock_release "$SPAWN_META_LOCK"
|
|
3140
|
+
SPAWN_META_LOCK_HELD=0
|
|
3141
|
+
if [ -n "$SPAWN_DEFERRED_SIGNAL" ]; then
|
|
3142
|
+
case "$SPAWN_DEFERRED_SIGNAL" in
|
|
3143
|
+
HUP) SPAWN_DEFERRED_SIGNAL_STATUS=129 ;;
|
|
3144
|
+
INT) SPAWN_DEFERRED_SIGNAL_STATUS=130 ;;
|
|
3145
|
+
TERM) SPAWN_DEFERRED_SIGNAL_STATUS=143 ;;
|
|
3146
|
+
esac
|
|
3147
|
+
echo "error: spawn of $ID was interrupted after launch delivery began; its paired task record and In-flight backlog state were preserved" >&2
|
|
3148
|
+
exit "$SPAWN_DEFERRED_SIGNAL_STATUS"
|
|
3149
|
+
fi
|
|
3150
|
+
|
|
3151
|
+
SPAWN_DELIVERY=
|
|
3152
|
+
[ -z "$MODE" ] || SPAWN_DELIVERY=" mode=$MODE yolo=$YOLO"
|
|
3153
|
+
echo "spawned $ID harness=$HARNESS kind=$KIND$SPAWN_DELIVERY window=$META_WINDOW worktree=$WT"
|