@zerwiz/ymir 0.1.4 → 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 -59
- 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,2922 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Tear down a finished task: return the treehouse worktree, release the Orca
|
|
3
|
+
# worktree, or retire a secondmate home; kill the recorded runtime endpoint,
|
|
4
|
+
# clear volatile state, and CLOSE this home's backlog item for ship and scout
|
|
5
|
+
# tasks before reporting success (a secondmate teardown closes none, since
|
|
6
|
+
# secondmates are not backlog items), then refresh/prune the project's clone for
|
|
7
|
+
# PR-based ship tasks.
|
|
8
|
+
# Removing state/<id>.meta and closing the backlog item are one step, not two:
|
|
9
|
+
# bin/fm-backlog-transition-lib.sh owns that invariant, and both halves run under
|
|
10
|
+
# the task's own meta lock before this script reports success. Because the
|
|
11
|
+
# completion links (the PR, the report path, a local-main note) live only in the
|
|
12
|
+
# record being removed, the intended close is recorded in
|
|
13
|
+
# state/<id>.backlog-close first, so a process killed between the halves leaves
|
|
14
|
+
# the next session start enough to finish it; a landed close removes that record.
|
|
15
|
+
# A close that fails is fatal and loud, preserves its pending-close record, and
|
|
16
|
+
# is retried by the next session start. The transition is skipped on a
|
|
17
|
+
# config/backlog-backend=manual home and in a home that keeps no
|
|
18
|
+
# data/backlog.md; those cases print the manual follow-up. An automatic-backend
|
|
19
|
+
# home with a backlog but no compatible tasks-axi refuses before cleanup.
|
|
20
|
+
# None of this loosens the landed-work gates below: the transition runs only on
|
|
21
|
+
# the paths that already proceed to remove the record.
|
|
22
|
+
# REFUSES if the worktree holds work that has not LANDED, because cleanup
|
|
23
|
+
# hard-resets/removes the worktree and kills its processes. Work has landed when it is
|
|
24
|
+
# reachable from any remote-tracking branch (a fork counts as a remote, so
|
|
25
|
+
# upstream-contribution PRs pushed to a fork satisfy this in any mode), OR - for a
|
|
26
|
+
# normal ship task whose commits are not so reachable - when its PR is merged and
|
|
27
|
+
# GitHub reports a PR head that contains the current local work, or its content is
|
|
28
|
+
# already present in the up-to-date default branch. This recognizes the common
|
|
29
|
+
# squash-merge-then-delete-branch flow, where the branch's own commits live nowhere
|
|
30
|
+
# on a remote yet the change is fully in main.
|
|
31
|
+
# The PR itself is resolved from the task's recorded pr= when present, or - when
|
|
32
|
+
# no pr= was ever recorded (e.g. a yolo-authorized merge on a repo with no PR CI,
|
|
33
|
+
# where the usual "checks green" fm-pr-check.sh trigger never fires) - by looking
|
|
34
|
+
# up a merged PR whose head branch matches the worktree's branch, fetching its head
|
|
35
|
+
# via refs/pull/<n>/head when the branch itself was deleted. So a missing pr= never
|
|
36
|
+
# by itself causes a false refusal of landed work.
|
|
37
|
+
# A gh lookup error falls back to the content check; if that is also inconclusive,
|
|
38
|
+
# teardown refuses rather than risk discarding unlanded work.
|
|
39
|
+
# Uncommitted changes are never landed.
|
|
40
|
+
# local-only projects additionally accept work merged into the local default
|
|
41
|
+
# branch (firstmate performs that merge after configured approval) as a fallback
|
|
42
|
+
# for the common case where there is no remote at all.
|
|
43
|
+
# Scout tasks (kind=scout in meta) carve out of that check: their worktree is
|
|
44
|
+
# declared scratch and the report at data/<task-id>/report.md is the work
|
|
45
|
+
# product. Teardown proceeds only once the report exists and the shared
|
|
46
|
+
# unresolved-decision completion gate verifies its captain-held inventory.
|
|
47
|
+
# Before destructive cleanup, teardown validates task check artifacts as
|
|
48
|
+
# ordinary single-link files on the state device. It refuses and preserves
|
|
49
|
+
# task state when that proof fails; otherwise it removes the task's check,
|
|
50
|
+
# trust record, PR sidecar, and publication record with the rest of the
|
|
51
|
+
# volatile state.
|
|
52
|
+
# Orca tasks use the same safety checks, then close the recorded terminal and
|
|
53
|
+
# remove the recorded worktree through `orca worktree rm`; teardown never guesses
|
|
54
|
+
# an Orca target from ambient CLI state.
|
|
55
|
+
# A Herdr presentation journal never authorizes cleanup. Teardown still closes
|
|
56
|
+
# only the exact task pane from ordinary endpoint metadata and never calls
|
|
57
|
+
# `workspace close`. It retires the non-authoritative journal only when a
|
|
58
|
+
# read-only token correlation agrees with that endpoint and pane closure is
|
|
59
|
+
# confirmed. Otherwise the journal stays quarantined for manual inspection.
|
|
60
|
+
# Projected closes share the presentation-order lock, refuse to close the
|
|
61
|
+
# captain's active tab, and restore the exact response-derived pre-close tab
|
|
62
|
+
# if Herdr's last-pane cleanup focuses an unrelated neighboring workspace.
|
|
63
|
+
# Secondmates (kind=secondmate in meta) are retired explicitly. Normal
|
|
64
|
+
# teardown refuses while their home has in-flight crewmate meta files; --force
|
|
65
|
+
# is the approved discard path that prevalidates child removal targets, locks each
|
|
66
|
+
# descendant home's task set before enumeration, and holds those locks through
|
|
67
|
+
# child cleanup. Contention refuses the complete forced teardown before child
|
|
68
|
+
# mutation. Local and remote retirement serialize their destructive phase with
|
|
69
|
+
# that mate's backlog-handoff lock under the registry lock. Pending handoff wake
|
|
70
|
+
# state is retired with the home, and local removal failure restores that state
|
|
71
|
+
# before preserving the route for retry. Teardown then discards child work, kills
|
|
72
|
+
# child runtime endpoints, and removes the retired home. Removing a leased home
|
|
73
|
+
# releases its durable treehouse lease so the pool slot is freed,
|
|
74
|
+
# never left leased forever. If the treehouse return fails, teardown leaves the
|
|
75
|
+
# leased home and state in place instead of hiding a still-held lease.
|
|
76
|
+
# Usage: fm-teardown.sh <task-id> [--force]
|
|
77
|
+
# --force skips ordinary-task dirty and landed-work checks, skips scout report
|
|
78
|
+
# checks, and discards secondmate child work for kind=secondmate. Only use it
|
|
79
|
+
# when the captain has explicitly said to discard the work.
|
|
80
|
+
#
|
|
81
|
+
# Transient / stale worktree git lock recovery (teardown-lock-race): a crew process
|
|
82
|
+
# killed mid-git-operation can leave a .git/worktrees/<wt>/index.lock (or, for a
|
|
83
|
+
# non-linked worktree, .git/index.lock) that makes `treehouse return --force` fail
|
|
84
|
+
# with Unable to create '...index.lock': File exists. That lock is usually transient
|
|
85
|
+
# (the dying process finishes or exits within seconds) and must never be force-deleted
|
|
86
|
+
# while a live git process might still own it - the fix is patience, not rm.
|
|
87
|
+
#
|
|
88
|
+
# On that failure signature only, teardown_treehouse_return:
|
|
89
|
+
# 1. Retries up to FM_TREEHOUSE_RETURN_LOCK_RETRIES times (default 3), waiting
|
|
90
|
+
# FM_TREEHOUSE_RETURN_LOCK_RETRY_WAIT_SECS (default 1s; falls back to the older
|
|
91
|
+
# FM_STALE_WORKTREE_LOCK_RETRY_WAIT_SECS name when the new one is unset) between
|
|
92
|
+
# attempts. Retries key off the error text, not whether the lock file still
|
|
93
|
+
# exists after the failed attempt - a lock that self-clears mid-check still
|
|
94
|
+
# deserves a retry of the return.
|
|
95
|
+
# 2. Other treehouse return failures still abort immediately and loudly (no retry).
|
|
96
|
+
# 3. If every retry still hits the lock signature and the lock remains, it is removed
|
|
97
|
+
# and the return tried once more ONLY when the lock is provably stale per
|
|
98
|
+
# bin/fm-lock-lib.sh's fm_lock_is_provably_stale, passing the worktree dir as the
|
|
99
|
+
# companion directory and FM_STALE_WORKTREE_LOCK_AGE_SECS (default 30s) as the age
|
|
100
|
+
# threshold. That shared proof owns the exact lsof-holder, mtime-age, and fail-safe
|
|
101
|
+
# rules.
|
|
102
|
+
# 4. If retries exhaust and the lock is not provably stale, teardown fails as loudly
|
|
103
|
+
# as a normal return failure and notes that the lock persisted across the retry
|
|
104
|
+
# window. A missing `lsof`, or a lock that fails any stale check, is treated as
|
|
105
|
+
# NOT provably stale (fail safe): the lock is left untouched.
|
|
106
|
+
# The same proof is used when non-force safety inspection cannot run because the lock
|
|
107
|
+
# is present; teardown clears only a provably stale lock, then re-runs the safety
|
|
108
|
+
# checks before any destructive return. Teardown output notes every wait, retry, and
|
|
109
|
+
# removal so the operator can see what happened.
|
|
110
|
+
#
|
|
111
|
+
# Pre-teardown cleanup sequence (runs once every landed/discard-work safety
|
|
112
|
+
# refusal above has already passed, and BEFORE any worktree return, branch
|
|
113
|
+
# delete, or backend kill below - a still-active run or a leaked process may
|
|
114
|
+
# own live work in that worktree):
|
|
115
|
+
# Fix 1 - conclude the task's own no-mistakes run. A ship task's worktree can
|
|
116
|
+
# be torn down while its no-mistakes pipeline run is still PARKED at a gate
|
|
117
|
+
# (awaiting_approval/fix_review/any awaiting_agent field), with no worker
|
|
118
|
+
# left to ever answer it - the run then sits there holding a fleet slot
|
|
119
|
+
# indefinitely (observed 2026-08-03: runs parked 7h39m and parked at a
|
|
120
|
+
# post-CI approval gate after the worker was already cleaned up). A run
|
|
121
|
+
# with an autonomous step still under way (running/fixing/ci) is left
|
|
122
|
+
# alone: no-mistakes drives those against its own gate-repo clone, not the
|
|
123
|
+
# crew's worktree, so they are not orphaned by removing the worktree.
|
|
124
|
+
# conclude_task_no_mistakes_run attributes the active-or-most-recent run to
|
|
125
|
+
# THIS task only when its branch AND code identity (bin/fm-nm-run-lib.sh's
|
|
126
|
+
# strict fm_nm_head_matches_worktree rule) both match this worktree, then
|
|
127
|
+
# runs `no-mistakes axi abort --run <id>` for
|
|
128
|
+
# that verified run instance. A run already terminal
|
|
129
|
+
# (an outcome is set) or not parked at a gate is left untouched. Idempotent:
|
|
130
|
+
# an already-aborted run reads back terminal and is skipped on retry.
|
|
131
|
+
# Fix 2 - reap leaked descendant processes. A backgrounded/disowned process
|
|
132
|
+
# started under the worktree (or its per-task tasktmp) does not receive the
|
|
133
|
+
# SIGHUP/SIGTERM that closing the backend pane sends to its own foreground
|
|
134
|
+
# process group, so it survives reparented to init (observed 2026-08-03:
|
|
135
|
+
# two `go test` binaries, deadlines blown past by ~100x, pinning CPU for
|
|
136
|
+
# hours with no live task meta to attribute them to once teardown had
|
|
137
|
+
# already removed it). reap_task_worktree_processes finds every process
|
|
138
|
+
# whose CURRENT WORKING DIRECTORY is this task's own worktree or tasktmp
|
|
139
|
+
# root via `lsof -a -d cwd` (cheap: bounded by process count, not by
|
|
140
|
+
# walking the worktree's file tree) and sends TERM, then KILL after a short
|
|
141
|
+
# grace period to any survivor whose process identity still matches. Both
|
|
142
|
+
# roots are unique per task and never
|
|
143
|
+
# shared, so this can never reach another task's or the primary's
|
|
144
|
+
# processes. Idempotent: nothing left to find is a silent no-op.
|
|
145
|
+
# Fix 3 - sweep abandoned remote job workers. A remote job worker started
|
|
146
|
+
# from a worktree's own bin/ outlives that worktree's removal without
|
|
147
|
+
# being reachable by Fix 2, because its working directory is wherever it
|
|
148
|
+
# was launched rather than the task worktree (observed 2026-08-07: 29
|
|
149
|
+
# workers at ppid 1, 1-2 days old, each still polling and appending to a
|
|
150
|
+
# log in a pruned no-mistakes gate worktree). bin/fm-remote-job-reap-orphans.sh
|
|
151
|
+
# owns that sweep and its safety rule; it never touches a worker whose code
|
|
152
|
+
# root still exists, so the account's healthy LaunchAgent worker and every
|
|
153
|
+
# live remote secondmate worker are out of scope. Best effort: a sweep
|
|
154
|
+
# failure never blocks this teardown.
|
|
155
|
+
set -eu
|
|
156
|
+
|
|
157
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
158
|
+
FM_ROOT="${FM_ROOT_OVERRIDE:-$(cd "$SCRIPT_DIR/.." && pwd)}"
|
|
159
|
+
FM_HOME="${FM_HOME:-${FM_ROOT_OVERRIDE:-$FM_ROOT}}"
|
|
160
|
+
STATE="${FM_STATE_OVERRIDE:-$FM_HOME/state}"
|
|
161
|
+
DATA="${FM_DATA_OVERRIDE:-$FM_HOME/data}"
|
|
162
|
+
CONFIG="${FM_CONFIG_OVERRIDE:-$FM_HOME/config}"
|
|
163
|
+
SECONDMATE_REG="$DATA/secondmates.md"
|
|
164
|
+
SUB_HOME_MARKER=".fm-secondmate-home"
|
|
165
|
+
SUB_HOME_PARENT_MARKER=".fm-secondmate-parent"
|
|
166
|
+
# shellcheck source=bin/fm-tasks-axi-lib.sh
|
|
167
|
+
. "$SCRIPT_DIR/fm-tasks-axi-lib.sh"
|
|
168
|
+
# shellcheck source=bin/fm-backlog-transition-lib.sh
|
|
169
|
+
. "$SCRIPT_DIR/fm-backlog-transition-lib.sh"
|
|
170
|
+
# shellcheck source=bin/fm-backend.sh
|
|
171
|
+
. "$SCRIPT_DIR/fm-backend.sh"
|
|
172
|
+
# shellcheck source=bin/fm-control-lib.sh
|
|
173
|
+
. "$SCRIPT_DIR/fm-control-lib.sh"
|
|
174
|
+
# shellcheck source=bin/fm-lock-lib.sh
|
|
175
|
+
. "$SCRIPT_DIR/fm-lock-lib.sh"
|
|
176
|
+
# shellcheck source=bin/fm-classify-lib.sh
|
|
177
|
+
. "$SCRIPT_DIR/fm-classify-lib.sh"
|
|
178
|
+
# shellcheck source=bin/fm-gate-refuse-lib.sh
|
|
179
|
+
. "$SCRIPT_DIR/fm-gate-refuse-lib.sh"
|
|
180
|
+
# shellcheck source=bin/fm-pr-lib.sh
|
|
181
|
+
. "$SCRIPT_DIR/fm-pr-lib.sh"
|
|
182
|
+
# shellcheck source=bin/fm-public-followup-lib.sh
|
|
183
|
+
. "$SCRIPT_DIR/fm-public-followup-lib.sh"
|
|
184
|
+
# shellcheck source=bin/fm-secondmate-registry-lib.sh
|
|
185
|
+
. "$SCRIPT_DIR/fm-secondmate-registry-lib.sh"
|
|
186
|
+
# shellcheck source=bin/fm-secondmate-parent-lib.sh
|
|
187
|
+
. "$SCRIPT_DIR/fm-secondmate-parent-lib.sh"
|
|
188
|
+
# shellcheck source=bin/fm-pending-reply-lib.sh
|
|
189
|
+
. "$SCRIPT_DIR/fm-pending-reply-lib.sh"
|
|
190
|
+
# shellcheck source=bin/fm-nm-run-lib.sh
|
|
191
|
+
. "$SCRIPT_DIR/fm-nm-run-lib.sh"
|
|
192
|
+
if [ "$#" -lt 1 ] || ! fm_task_id_path_safe "$1"; then
|
|
193
|
+
echo "error: invalid teardown request" >&2
|
|
194
|
+
exit 2
|
|
195
|
+
fi
|
|
196
|
+
ID=$1
|
|
197
|
+
FORCE=${2:-}
|
|
198
|
+
fm_backlog_directory_present "$STATE" "state directory" || {
|
|
199
|
+
echo "error: teardown refused: $FM_BACKLOG_TRANSITION_ERROR" >&2
|
|
200
|
+
exit 1
|
|
201
|
+
}
|
|
202
|
+
# shellcheck source=bin/fm-wake-lib.sh
|
|
203
|
+
. "$SCRIPT_DIR/fm-wake-lib.sh"
|
|
204
|
+
# Supervision lease guard: post-landing cleanup is overlap territory between
|
|
205
|
+
# the two Pi supervision actors; refuse while the OTHER actor holds this
|
|
206
|
+
# task's live lease (contract: bin/fm-lease-lib.sh; no-op in homes without
|
|
207
|
+
# leases).
|
|
208
|
+
# shellcheck source=bin/fm-lease-lib.sh
|
|
209
|
+
. "$SCRIPT_DIR/fm-lease-lib.sh"
|
|
210
|
+
# Role partition: forced teardown discards work, and the supervision branch
|
|
211
|
+
# never discards anything - only an ordinary landed-work teardown is branch
|
|
212
|
+
# territory (contract: bin/fm-lease-lib.sh).
|
|
213
|
+
if [ "$FORCE" = --force ] && [ "$(fm_lease_actor)" = branch ]; then
|
|
214
|
+
echo "error: forced teardown refused - the supervision branch cannot discard work" >&2
|
|
215
|
+
exit "$FM_LEASE_REFUSE_EXIT"
|
|
216
|
+
fi
|
|
217
|
+
fm_lease_guard "$ID" "teardown (fm-teardown)"
|
|
218
|
+
CONTROL_LOCK="$STATE/.control-$ID.lock"
|
|
219
|
+
CONTROL_LOCK_HELD=0
|
|
220
|
+
META_LOCK=
|
|
221
|
+
META_LOCK_HELD=0
|
|
222
|
+
DESCENDANT_LOCK_PATHS=()
|
|
223
|
+
DESCENDANT_TASK_STATES=()
|
|
224
|
+
DESCENDANT_TASK_IDS=()
|
|
225
|
+
DESCENDANT_TASK_KINDS=()
|
|
226
|
+
DESCENDANT_TASK_HOMES=()
|
|
227
|
+
teardown_release_locks() {
|
|
228
|
+
local status=$? i
|
|
229
|
+
if declare -F teardown_release_herdr_locks >/dev/null 2>&1; then
|
|
230
|
+
teardown_release_herdr_locks || true
|
|
231
|
+
fi
|
|
232
|
+
for ((i=${#DESCENDANT_LOCK_PATHS[@]} - 1; i >= 0; i--)); do
|
|
233
|
+
fm_lock_release "${DESCENDANT_LOCK_PATHS[$i]}" || true
|
|
234
|
+
done
|
|
235
|
+
DESCENDANT_LOCK_PATHS=()
|
|
236
|
+
if [ -n "${HANDOFF_WAKE_RETIRE_LOCK:-}" ]; then
|
|
237
|
+
fm_lock_release "$HANDOFF_WAKE_RETIRE_LOCK" || true
|
|
238
|
+
HANDOFF_WAKE_RETIRE_LOCK=
|
|
239
|
+
fi
|
|
240
|
+
if [ -n "${LOCAL_HANDOFF_LOCK:-}" ]; then
|
|
241
|
+
fm_lock_release "$LOCAL_HANDOFF_LOCK" || true
|
|
242
|
+
LOCAL_HANDOFF_LOCK=
|
|
243
|
+
fi
|
|
244
|
+
if [ -n "${LOCAL_REGISTRY_LOCK:-}" ]; then
|
|
245
|
+
fm_lock_release "$LOCAL_REGISTRY_LOCK" || true
|
|
246
|
+
LOCAL_REGISTRY_LOCK=
|
|
247
|
+
fi
|
|
248
|
+
if [ "$META_LOCK_HELD" = 1 ]; then
|
|
249
|
+
fm_lock_release "$META_LOCK" || true
|
|
250
|
+
META_LOCK_HELD=0
|
|
251
|
+
fi
|
|
252
|
+
if [ "$CONTROL_LOCK_HELD" = 1 ]; then
|
|
253
|
+
fm_lock_release "$CONTROL_LOCK" || true
|
|
254
|
+
CONTROL_LOCK_HELD=0
|
|
255
|
+
fi
|
|
256
|
+
fm_lease_guard_release || true
|
|
257
|
+
return "$status"
|
|
258
|
+
}
|
|
259
|
+
trap teardown_release_locks EXIT
|
|
260
|
+
fm_lock_try_acquire "$CONTROL_LOCK" || {
|
|
261
|
+
echo "error: another lifecycle action is already running for task $ID; nothing was changed" >&2
|
|
262
|
+
exit 1
|
|
263
|
+
}
|
|
264
|
+
CONTROL_LOCK_HELD=1
|
|
265
|
+
# Fail closed before any fleet mutation: a no-mistakes gate agent must never tear
|
|
266
|
+
# down a worktree (see bin/fm-gate-refuse-lib.sh).
|
|
267
|
+
fm_refuse_if_gate_agent
|
|
268
|
+
FM_LOCK_LOG_PREFIX=teardown
|
|
269
|
+
|
|
270
|
+
META="$STATE/$ID.meta"
|
|
271
|
+
fm_backlog_record_present "$META" "task record" "$STATE" || {
|
|
272
|
+
echo "error: teardown refused: $FM_BACKLOG_TRANSITION_ERROR" >&2
|
|
273
|
+
exit 1
|
|
274
|
+
}
|
|
275
|
+
META_LOCK=$(fm_meta_lock_path "$META") || exit 1
|
|
276
|
+
fm_lock_acquire_wait "$META_LOCK"
|
|
277
|
+
META_LOCK_HELD=1
|
|
278
|
+
fm_backlog_record_present "$META" "task record" "$STATE" || {
|
|
279
|
+
echo "error: teardown refused after locking: $FM_BACKLOG_TRANSITION_ERROR" >&2
|
|
280
|
+
exit 1
|
|
281
|
+
}
|
|
282
|
+
TEARDOWN_META_KIND=$(fm_meta_get "$META" kind)
|
|
283
|
+
[ -n "$TEARDOWN_META_KIND" ] || TEARDOWN_META_KIND=ship
|
|
284
|
+
TEARDOWN_CLEANUP_RECOVERY=$(fm_meta_get "$META" cleanup_recovery)
|
|
285
|
+
TEARDOWN_META_SPAWN_GEN=
|
|
286
|
+
TEARDOWN_BACKLOG_APPLIES=0
|
|
287
|
+
TEARDOWN_BACKLOG_SKIP_REASON=
|
|
288
|
+
if [ "$TEARDOWN_CLEANUP_RECOVERY" != orca ]; then
|
|
289
|
+
if fm_backlog_transition_applies "$CONFIG" "$DATA" "$TEARDOWN_META_KIND"; then
|
|
290
|
+
TEARDOWN_BACKLOG_APPLIES=1
|
|
291
|
+
else
|
|
292
|
+
TEARDOWN_BACKLOG_GATE_STATUS=$?
|
|
293
|
+
if [ "$TEARDOWN_BACKLOG_GATE_STATUS" -eq 2 ]; then
|
|
294
|
+
echo "error: task $ID cannot be torn down because its backlog data directory is inaccessible: $DATA ($FM_BACKLOG_TRANSITION_ERROR)" >&2
|
|
295
|
+
exit 1
|
|
296
|
+
fi
|
|
297
|
+
TEARDOWN_BACKLOG_SKIP_REASON=$FM_BACKLOG_TRANSITION_SKIP
|
|
298
|
+
fi
|
|
299
|
+
fi
|
|
300
|
+
if [ "$TEARDOWN_BACKLOG_APPLIES" = 1 ]; then
|
|
301
|
+
if ! fm_backlog_meta_spawn_gen "$META" "$STATE"; then
|
|
302
|
+
echo "error: task $ID's record has no spawn_gen that identifies one exact incarnation ($FM_BACKLOG_TRANSITION_ERROR); refusing automatic teardown - relaunch the task to publish an unambiguous incarnation, then retry teardown" >&2
|
|
303
|
+
exit 1
|
|
304
|
+
fi
|
|
305
|
+
TEARDOWN_META_SPAWN_GEN=$FM_BACKLOG_META_SPAWN_GEN
|
|
306
|
+
fi
|
|
307
|
+
|
|
308
|
+
REMOTE_HANDOFF_DIR_PRESENT=0
|
|
309
|
+
REMOTE_HANDOFF_DIR_REAL=
|
|
310
|
+
REMOTE_OUTBOX_PRESENT=0
|
|
311
|
+
REMOTE_PENDING_DIR_PRESENT=0
|
|
312
|
+
REMOTE_PENDING_DIR_REAL=
|
|
313
|
+
REMOTE_HANDOFF_LOCK=
|
|
314
|
+
REMOTE_REGISTRY_LOCK=
|
|
315
|
+
REMOTE_REPLY_LIFECYCLE_LOCK=
|
|
316
|
+
LOCAL_HANDOFF_LOCK=
|
|
317
|
+
LOCAL_REGISTRY_LOCK=
|
|
318
|
+
HANDOFF_WAKE_RETIRE_MARKER=
|
|
319
|
+
HANDOFF_WAKE_RETIRE_VALUE=
|
|
320
|
+
HANDOFF_WAKE_RETIRE_CORR=
|
|
321
|
+
HANDOFF_WAKE_RETIRE_LOCK=
|
|
322
|
+
HANDOFF_WAKE_RETIRE_STAGE=
|
|
323
|
+
|
|
324
|
+
handoff_wake_retire_validate() {
|
|
325
|
+
local marker="$STATE/.backlog-handoff-$ID.wake-pending" value corr rec confirmation
|
|
326
|
+
HANDOFF_WAKE_RETIRE_MARKER=
|
|
327
|
+
HANDOFF_WAKE_RETIRE_VALUE=
|
|
328
|
+
HANDOFF_WAKE_RETIRE_CORR=
|
|
329
|
+
[ -e "$marker" ] || [ -L "$marker" ] || return 0
|
|
330
|
+
[ -f "$marker" ] && [ ! -L "$marker" ] || {
|
|
331
|
+
echo "REFUSED: receiver wake state for secondmate $ID is unsafe" >&2
|
|
332
|
+
return 1
|
|
333
|
+
}
|
|
334
|
+
value=$(cat "$marker" 2>/dev/null || true)
|
|
335
|
+
case "$value" in
|
|
336
|
+
pending|confirmed) ;;
|
|
337
|
+
prepared:*)
|
|
338
|
+
corr=${value#prepared:}
|
|
339
|
+
corr=${corr%%:*}
|
|
340
|
+
printf '%s' "$value" | grep -Eq '^prepared:[a-f0-9]{16}:[a-f0-9]{16}$' || {
|
|
341
|
+
echo "REFUSED: receiver wake state for secondmate $ID is invalid" >&2
|
|
342
|
+
return 1
|
|
343
|
+
}
|
|
344
|
+
;;
|
|
345
|
+
pending:*|confirmed:*)
|
|
346
|
+
corr=${value#*:}
|
|
347
|
+
printf '%s' "$corr" | grep -Eq '^[a-f0-9]{16}$' || {
|
|
348
|
+
echo "REFUSED: receiver wake state for secondmate $ID is invalid" >&2
|
|
349
|
+
return 1
|
|
350
|
+
}
|
|
351
|
+
;;
|
|
352
|
+
*)
|
|
353
|
+
echo "REFUSED: receiver wake state for secondmate $ID is invalid" >&2
|
|
354
|
+
return 1
|
|
355
|
+
;;
|
|
356
|
+
esac
|
|
357
|
+
if [ -n "$corr" ]; then
|
|
358
|
+
rec=$(fm_pending_reply_path "$STATE" "$corr")
|
|
359
|
+
if [ -e "$rec" ] || [ -L "$rec" ]; then
|
|
360
|
+
[ -f "$rec" ] && [ ! -L "$rec" ] \
|
|
361
|
+
&& [ "$(fm_pending_reply_get "$rec" task_id)" = "$ID" ] || {
|
|
362
|
+
echo "REFUSED: receiver wake correlation for secondmate $ID is unsafe or belongs to another task" >&2
|
|
363
|
+
return 1
|
|
364
|
+
}
|
|
365
|
+
fi
|
|
366
|
+
confirmation=$(fm_pending_reply_delivery_confirmation_path "$STATE" "$corr")
|
|
367
|
+
if [ -e "$confirmation" ] || [ -L "$confirmation" ]; then
|
|
368
|
+
[ -f "$confirmation" ] && [ ! -L "$confirmation" ] || {
|
|
369
|
+
echo "REFUSED: receiver wake delivery state for secondmate $ID is unsafe" >&2
|
|
370
|
+
return 1
|
|
371
|
+
}
|
|
372
|
+
fi
|
|
373
|
+
HANDOFF_WAKE_RETIRE_CORR=$corr
|
|
374
|
+
fi
|
|
375
|
+
HANDOFF_WAKE_RETIRE_MARKER=$marker
|
|
376
|
+
HANDOFF_WAKE_RETIRE_VALUE=$value
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
handoff_wake_retire() {
|
|
380
|
+
local marker=$HANDOFF_WAKE_RETIRE_MARKER corr=$HANDOFF_WAKE_RETIRE_CORR lock rec confirmation rc=0
|
|
381
|
+
[ -n "$marker" ] || return 0
|
|
382
|
+
[ -f "$marker" ] && [ ! -L "$marker" ] \
|
|
383
|
+
&& [ "$(cat "$marker" 2>/dev/null || true)" = "$HANDOFF_WAKE_RETIRE_VALUE" ] || return 1
|
|
384
|
+
if [ -n "$corr" ]; then
|
|
385
|
+
lock="$STATE/.pending-reply-$corr.lock"
|
|
386
|
+
fm_lock_acquire_wait "$lock" || return 1
|
|
387
|
+
rec=$(fm_pending_reply_path "$STATE" "$corr")
|
|
388
|
+
confirmation=$(fm_pending_reply_delivery_confirmation_path "$STATE" "$corr")
|
|
389
|
+
if { [ ! -e "$rec" ] && [ ! -L "$rec" ]; } \
|
|
390
|
+
|| { [ -f "$rec" ] && [ ! -L "$rec" ] \
|
|
391
|
+
&& [ "$(fm_pending_reply_get "$rec" task_id)" = "$ID" ]; }; then
|
|
392
|
+
rm -f -- "$confirmation" "$rec" "$marker" || rc=$?
|
|
393
|
+
else
|
|
394
|
+
rc=1
|
|
395
|
+
fi
|
|
396
|
+
fm_lock_release "$lock"
|
|
397
|
+
return "$rc"
|
|
398
|
+
fi
|
|
399
|
+
rm -f -- "$marker"
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
handoff_wake_retire_stage_restore() {
|
|
403
|
+
local stage=$HANDOFF_WAKE_RETIRE_STAGE marker rec confirmation name destination
|
|
404
|
+
[ -n "$stage" ] || return 0
|
|
405
|
+
marker="$STATE/.backlog-handoff-$ID.wake-pending"
|
|
406
|
+
rec=
|
|
407
|
+
confirmation=
|
|
408
|
+
if [ -n "$HANDOFF_WAKE_RETIRE_CORR" ]; then
|
|
409
|
+
rec=$(fm_pending_reply_path "$STATE" "$HANDOFF_WAKE_RETIRE_CORR")
|
|
410
|
+
confirmation=$(fm_pending_reply_delivery_confirmation_path "$STATE" "$HANDOFF_WAKE_RETIRE_CORR")
|
|
411
|
+
fi
|
|
412
|
+
for name in record confirmation marker; do
|
|
413
|
+
[ -e "$stage/$name" ] || continue
|
|
414
|
+
case "$name" in
|
|
415
|
+
record) destination=$rec ;;
|
|
416
|
+
confirmation) destination=$confirmation ;;
|
|
417
|
+
marker) destination=$marker ;;
|
|
418
|
+
esac
|
|
419
|
+
[ -n "$destination" ] && [ ! -e "$destination" ] && [ ! -L "$destination" ] \
|
|
420
|
+
&& mv -- "$stage/$name" "$destination" || return 1
|
|
421
|
+
done
|
|
422
|
+
rm -f -- "$stage/corr" || return 1
|
|
423
|
+
rmdir -- "$stage" || return 1
|
|
424
|
+
if [ -n "$HANDOFF_WAKE_RETIRE_LOCK" ]; then
|
|
425
|
+
fm_lock_release "$HANDOFF_WAKE_RETIRE_LOCK" || return 1
|
|
426
|
+
HANDOFF_WAKE_RETIRE_LOCK=
|
|
427
|
+
fi
|
|
428
|
+
HANDOFF_WAKE_RETIRE_STAGE=
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
handoff_wake_retire_stage_commit() {
|
|
432
|
+
local stage=$HANDOFF_WAKE_RETIRE_STAGE retired
|
|
433
|
+
[ -n "$stage" ] || return 0
|
|
434
|
+
retired="$stage.retired.$$"
|
|
435
|
+
[ ! -e "$retired" ] && [ ! -L "$retired" ] || return 1
|
|
436
|
+
mv -- "$stage" "$retired" || return 1
|
|
437
|
+
HANDOFF_WAKE_RETIRE_STAGE=
|
|
438
|
+
if [ -n "$HANDOFF_WAKE_RETIRE_LOCK" ]; then
|
|
439
|
+
fm_lock_release "$HANDOFF_WAKE_RETIRE_LOCK" || return 1
|
|
440
|
+
HANDOFF_WAKE_RETIRE_LOCK=
|
|
441
|
+
fi
|
|
442
|
+
rm -rf -- "$retired" || echo "warning: retired receiver wake state remains at $retired" >&2
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
handoff_wake_retire_stage_recover() {
|
|
446
|
+
local home=$1 stage="$STATE/.backlog-handoff-$ID.wake-retiring" corr
|
|
447
|
+
[ -e "$stage" ] || [ -L "$stage" ] || return 0
|
|
448
|
+
[ -d "$stage" ] && [ ! -L "$stage" ] || {
|
|
449
|
+
echo "REFUSED: receiver wake retirement state for secondmate $ID is unsafe" >&2
|
|
450
|
+
return 1
|
|
451
|
+
}
|
|
452
|
+
if [ ! -e "$stage/corr" ] && [ ! -L "$stage/corr" ]; then
|
|
453
|
+
rmdir -- "$stage" 2>/dev/null && return 0
|
|
454
|
+
echo "REFUSED: receiver wake retirement state for secondmate $ID is incomplete" >&2
|
|
455
|
+
return 1
|
|
456
|
+
fi
|
|
457
|
+
[ -f "$stage/corr" ] && [ ! -L "$stage/corr" ] || {
|
|
458
|
+
echo "REFUSED: receiver wake retirement state for secondmate $ID is unsafe" >&2
|
|
459
|
+
return 1
|
|
460
|
+
}
|
|
461
|
+
corr=$(cat "$stage/corr" 2>/dev/null || true)
|
|
462
|
+
[ -z "$corr" ] || printf '%s' "$corr" | grep -Eq '^[a-f0-9]{16}$' || {
|
|
463
|
+
echo "REFUSED: receiver wake retirement correlation for secondmate $ID is invalid" >&2
|
|
464
|
+
return 1
|
|
465
|
+
}
|
|
466
|
+
local staged
|
|
467
|
+
for staged in "$stage/marker" "$stage/record" "$stage/confirmation"; do
|
|
468
|
+
[ ! -e "$staged" ] && [ ! -L "$staged" ] && continue
|
|
469
|
+
[ -f "$staged" ] && [ ! -L "$staged" ] || {
|
|
470
|
+
echo "REFUSED: receiver wake retirement state for secondmate $ID is unsafe" >&2
|
|
471
|
+
return 1
|
|
472
|
+
}
|
|
473
|
+
done
|
|
474
|
+
HANDOFF_WAKE_RETIRE_CORR=$corr
|
|
475
|
+
HANDOFF_WAKE_RETIRE_STAGE=$stage
|
|
476
|
+
if [ -n "$corr" ]; then
|
|
477
|
+
HANDOFF_WAKE_RETIRE_LOCK="$STATE/.pending-reply-$corr.lock"
|
|
478
|
+
fm_lock_acquire_wait "$HANDOFF_WAKE_RETIRE_LOCK" || return 1
|
|
479
|
+
fi
|
|
480
|
+
if [ -e "$home" ] || [ -L "$home" ]; then
|
|
481
|
+
handoff_wake_retire_stage_restore
|
|
482
|
+
else
|
|
483
|
+
handoff_wake_retire_stage_commit
|
|
484
|
+
fi
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
handoff_wake_retire_stage() {
|
|
488
|
+
local stage="$STATE/.backlog-handoff-$ID.wake-retiring" marker=$HANDOFF_WAKE_RETIRE_MARKER
|
|
489
|
+
local corr=$HANDOFF_WAKE_RETIRE_CORR rec confirmation
|
|
490
|
+
[ -n "$marker" ] || return 0
|
|
491
|
+
[ ! -e "$stage" ] && [ ! -L "$stage" ] || return 1
|
|
492
|
+
(umask 077; mkdir -- "$stage") || return 1
|
|
493
|
+
HANDOFF_WAKE_RETIRE_STAGE=$stage
|
|
494
|
+
printf '%s\n' "$corr" > "$stage/corr" || { handoff_wake_retire_stage_restore || true; return 1; }
|
|
495
|
+
if [ -n "$corr" ]; then
|
|
496
|
+
HANDOFF_WAKE_RETIRE_LOCK="$STATE/.pending-reply-$corr.lock"
|
|
497
|
+
fm_lock_acquire_wait "$HANDOFF_WAKE_RETIRE_LOCK" || {
|
|
498
|
+
HANDOFF_WAKE_RETIRE_LOCK=
|
|
499
|
+
handoff_wake_retire_stage_restore || true
|
|
500
|
+
return 1
|
|
501
|
+
}
|
|
502
|
+
rec=$(fm_pending_reply_path "$STATE" "$corr")
|
|
503
|
+
confirmation=$(fm_pending_reply_delivery_confirmation_path "$STATE" "$corr")
|
|
504
|
+
if [ -e "$rec" ] && ! mv -- "$rec" "$stage/record"; then
|
|
505
|
+
handoff_wake_retire_stage_restore || true
|
|
506
|
+
return 1
|
|
507
|
+
fi
|
|
508
|
+
if [ -e "$confirmation" ] && ! mv -- "$confirmation" "$stage/confirmation"; then
|
|
509
|
+
handoff_wake_retire_stage_restore || true
|
|
510
|
+
return 1
|
|
511
|
+
fi
|
|
512
|
+
fi
|
|
513
|
+
if ! mv -- "$marker" "$stage/marker"; then
|
|
514
|
+
handoff_wake_retire_stage_restore || true
|
|
515
|
+
return 1
|
|
516
|
+
fi
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
remote_teardown_locks_release() {
|
|
520
|
+
if [ -n "$REMOTE_REPLY_LIFECYCLE_LOCK" ]; then
|
|
521
|
+
fm_lock_release "$REMOTE_REPLY_LIFECYCLE_LOCK"
|
|
522
|
+
REMOTE_REPLY_LIFECYCLE_LOCK=
|
|
523
|
+
fi
|
|
524
|
+
if [ -n "$REMOTE_HANDOFF_LOCK" ]; then
|
|
525
|
+
fm_lock_release "$REMOTE_HANDOFF_LOCK"
|
|
526
|
+
REMOTE_HANDOFF_LOCK=
|
|
527
|
+
fi
|
|
528
|
+
if [ -n "$REMOTE_REGISTRY_LOCK" ]; then
|
|
529
|
+
fm_lock_release "$REMOTE_REGISTRY_LOCK"
|
|
530
|
+
REMOTE_REGISTRY_LOCK=
|
|
531
|
+
fi
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
remote_recovery_paths_validate() {
|
|
535
|
+
local mode=${1:-initial} handoff_dir outbox pending_dir real rec
|
|
536
|
+
handoff_dir="$DATA/handoff"
|
|
537
|
+
outbox="$handoff_dir/$ID.outbox.md"
|
|
538
|
+
pending_dir="$STATE/pending-replies"
|
|
539
|
+
if [ -e "$handoff_dir" ] || [ -L "$handoff_dir" ]; then
|
|
540
|
+
[ -d "$handoff_dir" ] && [ ! -L "$handoff_dir" ] \
|
|
541
|
+
|| { echo "REFUSED: remote handoff recovery directory is unsafe" >&2; return 1; }
|
|
542
|
+
real=$(CDPATH='' cd -- "$handoff_dir" 2>/dev/null && pwd -P) || return 1
|
|
543
|
+
if [ "$mode" = initial ]; then
|
|
544
|
+
REMOTE_HANDOFF_DIR_PRESENT=1
|
|
545
|
+
REMOTE_HANDOFF_DIR_REAL=$real
|
|
546
|
+
elif [ "$REMOTE_HANDOFF_DIR_PRESENT" -ne 1 ] || [ "$REMOTE_HANDOFF_DIR_REAL" != "$real" ]; then
|
|
547
|
+
echo "REFUSED: remote handoff recovery directory changed during retirement" >&2
|
|
548
|
+
return 1
|
|
549
|
+
fi
|
|
550
|
+
elif [ "$mode" != initial ] && [ "$REMOTE_HANDOFF_DIR_PRESENT" -ne 0 ]; then
|
|
551
|
+
echo "REFUSED: remote handoff recovery directory changed during retirement" >&2
|
|
552
|
+
return 1
|
|
553
|
+
fi
|
|
554
|
+
if [ -e "$outbox" ] || [ -L "$outbox" ]; then
|
|
555
|
+
[ -f "$outbox" ] && [ ! -L "$outbox" ] \
|
|
556
|
+
|| { echo "REFUSED: remote backlog outbox is unsafe" >&2; return 1; }
|
|
557
|
+
if [ "$mode" = initial ]; then
|
|
558
|
+
REMOTE_OUTBOX_PRESENT=1
|
|
559
|
+
elif [ "$REMOTE_OUTBOX_PRESENT" -ne 1 ]; then
|
|
560
|
+
echo "REFUSED: remote backlog outbox changed during retirement" >&2
|
|
561
|
+
return 1
|
|
562
|
+
fi
|
|
563
|
+
elif [ "$mode" != initial ] && [ "$REMOTE_OUTBOX_PRESENT" -ne 0 ]; then
|
|
564
|
+
echo "REFUSED: remote backlog outbox changed during retirement" >&2
|
|
565
|
+
return 1
|
|
566
|
+
fi
|
|
567
|
+
if [ -e "$pending_dir" ] || [ -L "$pending_dir" ]; then
|
|
568
|
+
[ -d "$pending_dir" ] && [ ! -L "$pending_dir" ] \
|
|
569
|
+
|| { echo "REFUSED: pending-replies recovery directory is unsafe" >&2; return 1; }
|
|
570
|
+
real=$(CDPATH='' cd -- "$pending_dir" 2>/dev/null && pwd -P) || return 1
|
|
571
|
+
if [ "$mode" = initial ]; then
|
|
572
|
+
REMOTE_PENDING_DIR_PRESENT=1
|
|
573
|
+
REMOTE_PENDING_DIR_REAL=$real
|
|
574
|
+
elif [ "$REMOTE_PENDING_DIR_PRESENT" -ne 1 ] || [ "$REMOTE_PENDING_DIR_REAL" != "$real" ]; then
|
|
575
|
+
echo "REFUSED: pending-replies recovery directory changed during retirement" >&2
|
|
576
|
+
return 1
|
|
577
|
+
fi
|
|
578
|
+
for rec in "$pending_dir"/*; do
|
|
579
|
+
[ -e "$rec" ] || [ -L "$rec" ] || continue
|
|
580
|
+
[ -f "$rec" ] && [ ! -L "$rec" ] \
|
|
581
|
+
|| { echo "REFUSED: pending-replies contains an unsafe recovery entry" >&2; return 1; }
|
|
582
|
+
done
|
|
583
|
+
elif [ "$mode" != initial ] && [ "$REMOTE_PENDING_DIR_PRESENT" -ne 0 ]; then
|
|
584
|
+
echo "REFUSED: pending-replies recovery directory changed during retirement" >&2
|
|
585
|
+
return 1
|
|
586
|
+
fi
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
remote_pending_replies_cleanup() {
|
|
590
|
+
local rec
|
|
591
|
+
[ "$REMOTE_PENDING_DIR_PRESENT" -eq 1 ] || return 0
|
|
592
|
+
(
|
|
593
|
+
CDPATH='' cd -- "$STATE/pending-replies" 2>/dev/null || exit 1
|
|
594
|
+
[ "$(pwd -P)" = "$REMOTE_PENDING_DIR_REAL" ] || exit 1
|
|
595
|
+
for rec in ./*; do
|
|
596
|
+
[ -e "$rec" ] || [ -L "$rec" ] || continue
|
|
597
|
+
[ -f "$rec" ] && [ ! -L "$rec" ] || exit 1
|
|
598
|
+
[ "$(fm_meta_get "$rec" task_id)" = "$ID" ] && rm -f -- "$rec"
|
|
599
|
+
done
|
|
600
|
+
)
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
remote_outbox_cleanup() {
|
|
604
|
+
[ "$REMOTE_OUTBOX_PRESENT" -eq 1 ] || return 0
|
|
605
|
+
(
|
|
606
|
+
CDPATH='' cd -- "$DATA/handoff" 2>/dev/null || exit 1
|
|
607
|
+
[ "$(pwd -P)" = "$REMOTE_HANDOFF_DIR_REAL" ] || exit 1
|
|
608
|
+
[ -f "$ID.outbox.md" ] && [ ! -L "$ID.outbox.md" ] || exit 1
|
|
609
|
+
rm -f -- "$ID.outbox.md"
|
|
610
|
+
)
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
remote_secondmate_teardown() {
|
|
614
|
+
local remote_host remote_root remote_home kind route_host route_root route_home out rc tmp rec phase task_id
|
|
615
|
+
remote_host=$(fm_meta_get "$META" remote_host)
|
|
616
|
+
[ -n "$remote_host" ] || return 3
|
|
617
|
+
kind=$(fm_meta_get "$META" kind)
|
|
618
|
+
[ "$kind" = secondmate ] || { echo "REFUSED: remote placement metadata is valid only for a secondmate" >&2; return 1; }
|
|
619
|
+
remote_root=$(fm_meta_get "$META" remote_root)
|
|
620
|
+
remote_home=$(fm_meta_get "$META" home)
|
|
621
|
+
[ -n "$remote_root" ] && [ -n "$remote_home" ] || { echo "REFUSED: remote secondmate metadata is incomplete" >&2; return 1; }
|
|
622
|
+
secondmate_registry_line_for_id "$SECONDMATE_REG" "$ID" || { echo "REFUSED: remote secondmate route is missing or ambiguous" >&2; return 1; }
|
|
623
|
+
[ "$SECONDMATE_REGISTRY_REMOTE" -eq 1 ] || { echo "REFUSED: secondmate registry route is not remote" >&2; return 1; }
|
|
624
|
+
route_host=$SECONDMATE_REGISTRY_HOST
|
|
625
|
+
route_root=$SECONDMATE_REGISTRY_ROOT
|
|
626
|
+
route_home=$SECONDMATE_REGISTRY_HOME
|
|
627
|
+
[ "$route_host" = "$remote_host" ] && [ "$route_root" = "$remote_root" ] && [ "$route_home" = "$remote_home" ] \
|
|
628
|
+
|| { echo "REFUSED: remote secondmate metadata does not match its registry route" >&2; return 1; }
|
|
629
|
+
[ -z "$FORCE" ] || [ "$FORCE" = --force ] || { echo "error: invalid teardown option: $FORCE" >&2; return 2; }
|
|
630
|
+
handoff_wake_retire_validate || return 1
|
|
631
|
+
remote_recovery_paths_validate initial || return 1
|
|
632
|
+
if [ "$FORCE" != --force ] && [ "$REMOTE_OUTBOX_PRESENT" -eq 1 ]; then
|
|
633
|
+
echo "REFUSED: remote secondmate $ID still has a pending backlog outbox; deliver it or explicitly discard with --force" >&2
|
|
634
|
+
return 1
|
|
635
|
+
fi
|
|
636
|
+
if [ "$FORCE" != --force ] && [ -d "$STATE/pending-replies" ]; then
|
|
637
|
+
for rec in "$STATE/pending-replies"/*; do
|
|
638
|
+
[ -f "$rec" ] || continue
|
|
639
|
+
task_id=$(fm_meta_get "$rec" task_id)
|
|
640
|
+
[ "$task_id" = "$ID" ] || continue
|
|
641
|
+
phase=$(fm_meta_get "$rec" phase)
|
|
642
|
+
[ "$phase" = resolved ] || {
|
|
643
|
+
echo "REFUSED: remote secondmate $ID still has an unresolved routed reply" >&2
|
|
644
|
+
return 1
|
|
645
|
+
}
|
|
646
|
+
done
|
|
647
|
+
fi
|
|
648
|
+
"$SCRIPT_DIR/fm-procevent-remote-reply.sh" retire-quiesce-locked "$ID" "$FORCE" >/dev/null 2>&1 || {
|
|
649
|
+
echo "REFUSED: remote secondmate $ID still has an unhandled captured reply" >&2
|
|
650
|
+
return 1
|
|
651
|
+
}
|
|
652
|
+
"$FM_ROOT/bin/fm-guard.sh" || true
|
|
653
|
+
if [ "$FORCE" = --force ]; then
|
|
654
|
+
if out=$("$SCRIPT_DIR/fm-on.sh" "$ID" fm-remote-secondmate-control.sh retire "$ID" --force < /dev/null 2>&1); then rc=0; else rc=$?; fi
|
|
655
|
+
else
|
|
656
|
+
if out=$("$SCRIPT_DIR/fm-on.sh" "$ID" fm-remote-secondmate-control.sh retire "$ID" < /dev/null 2>&1); then rc=0; else rc=$?; fi
|
|
657
|
+
fi
|
|
658
|
+
if [ "$rc" -ne 0 ]; then
|
|
659
|
+
[ -z "$out" ] || printf '%s\n' "$out" >&2
|
|
660
|
+
if [ "$rc" -eq 255 ]; then
|
|
661
|
+
echo "error: remote retirement completion is unknown; preserving the route and local records for same-host reconciliation" >&2
|
|
662
|
+
elif ! "$SCRIPT_DIR/fm-procevent-remote-reply.sh" arm-locked "$ID" >/dev/null 2>&1; then
|
|
663
|
+
echo "error: remote retirement failed and the reply source could not be re-armed" >&2
|
|
664
|
+
fi
|
|
665
|
+
return "$rc"
|
|
666
|
+
fi
|
|
667
|
+
remote_recovery_paths_validate recheck || {
|
|
668
|
+
echo "error: remote home retired but local recovery paths changed; preserving the local route for retry" >&2
|
|
669
|
+
return 1
|
|
670
|
+
}
|
|
671
|
+
"$SCRIPT_DIR/fm-procevent-remote-reply.sh" retire-finalize-locked "$ID" "$FORCE" >/dev/null 2>&1 || {
|
|
672
|
+
echo "error: remote home retired but reply-source cleanup is incomplete; preserving the local route for retry" >&2
|
|
673
|
+
return 1
|
|
674
|
+
}
|
|
675
|
+
if [ "$FORCE" = --force ]; then
|
|
676
|
+
remote_outbox_cleanup || { echo "error: remote outbox cleanup failed; preserving the local route for retry" >&2; return 1; }
|
|
677
|
+
fi
|
|
678
|
+
remote_pending_replies_cleanup \
|
|
679
|
+
|| { echo "error: remote pending-reply cleanup failed; preserving the local route for retry" >&2; return 1; }
|
|
680
|
+
handoff_wake_retire \
|
|
681
|
+
|| { echo "error: remote receiver wake cleanup failed; preserving the local route for retry" >&2; return 1; }
|
|
682
|
+
tmp="$SECONDMATE_REG.tmp.$$"
|
|
683
|
+
grep -vE "^- $ID( |$)" "$SECONDMATE_REG" > "$tmp" || true
|
|
684
|
+
mv -f -- "$tmp" "$SECONDMATE_REG"
|
|
685
|
+
status_retire_presentation_task "$STATE" "$ID" || return 1
|
|
686
|
+
fm_backlog_atomic_transition remove "$STATE/$ID.meta" "task record" "$STATE" || return 1
|
|
687
|
+
rm -f -- "$STATE/$ID.turn-ended"
|
|
688
|
+
printf 'teardown %s complete (remote %s:%s)\n' "$ID" "$remote_host" "$remote_home"
|
|
689
|
+
return 0
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
remote_secondmate_teardown_locked() {
|
|
693
|
+
local rc
|
|
694
|
+
[ -n "$(fm_meta_get "$META" remote_host)" ] || return 3
|
|
695
|
+
REMOTE_REGISTRY_LOCK=$(secondmate_registry_lock_path "$STATE")
|
|
696
|
+
fm_lock_acquire_wait "$REMOTE_REGISTRY_LOCK" || return 1
|
|
697
|
+
REMOTE_HANDOFF_LOCK="$STATE/.backlog-handoff-$ID.lock"
|
|
698
|
+
fm_lock_acquire_wait "$REMOTE_HANDOFF_LOCK" || {
|
|
699
|
+
remote_teardown_locks_release
|
|
700
|
+
return 1
|
|
701
|
+
}
|
|
702
|
+
REMOTE_REPLY_LIFECYCLE_LOCK=$(secondmate_reply_lifecycle_lock_path "$STATE" "$ID")
|
|
703
|
+
fm_lock_acquire_wait "$REMOTE_REPLY_LIFECYCLE_LOCK" || {
|
|
704
|
+
remote_teardown_locks_release
|
|
705
|
+
return 1
|
|
706
|
+
}
|
|
707
|
+
if remote_secondmate_teardown; then rc=0; else rc=$?; fi
|
|
708
|
+
remote_teardown_locks_release
|
|
709
|
+
return "$rc"
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
if remote_secondmate_teardown_locked; then
|
|
713
|
+
"$SCRIPT_DIR/fm-home-summary-refresh.sh" --best-effort || true
|
|
714
|
+
exit 0
|
|
715
|
+
else
|
|
716
|
+
remote_teardown_rc=$?
|
|
717
|
+
fi
|
|
718
|
+
[ "$remote_teardown_rc" -eq 3 ] || exit "$remote_teardown_rc"
|
|
719
|
+
|
|
720
|
+
# This is the first cleanup authorization check. It is metadata-only and must
|
|
721
|
+
# complete before fm-guard, a backend command, file removal, branch deletion,
|
|
722
|
+
# worktree return, registry change, or process termination can run.
|
|
723
|
+
fm_backend_validate_task_endpoint "$META" "$ID" || exit 1
|
|
724
|
+
BACKEND=$FM_BACKEND_VALIDATED_BACKEND
|
|
725
|
+
T=$FM_BACKEND_VALIDATED_TARGET
|
|
726
|
+
WT=$(fm_meta_get "$META" worktree)
|
|
727
|
+
PROJ=$(fm_meta_get "$META" project)
|
|
728
|
+
T_ORCA=
|
|
729
|
+
[ "$BACKEND" != orca ] || T_ORCA=$T
|
|
730
|
+
if [ "${FM_TEARDOWN_GUARD_DONE:-0}" != 1 ]; then
|
|
731
|
+
"$FM_ROOT/bin/fm-guard.sh" || true
|
|
732
|
+
fi
|
|
733
|
+
HOME_PATH=$(grep '^home=' "$META" | cut -d= -f2- || true)
|
|
734
|
+
PR_URL=$(grep '^pr=' "$META" | tail -1 | cut -d= -f2- || true)
|
|
735
|
+
# tasktmp is recorded by fm-spawn for tasks that set up a per-task temp root
|
|
736
|
+
# (/tmp/fm-<id>/); absent for tasks spawned before that change, so tolerate empty.
|
|
737
|
+
TASK_TMP=$(grep '^tasktmp=' "$META" | cut -d= -f2- || true)
|
|
738
|
+
BUSY_GEN=$(fm_meta_get "$META" busy_gen)
|
|
739
|
+
if [ -z "$BUSY_GEN" ]; then
|
|
740
|
+
BUSY_GEN=$(cat "$STATE/$ID.busy-gen" 2>/dev/null || true)
|
|
741
|
+
fi
|
|
742
|
+
ORCA_WORKTREE_ID=$(fm_meta_get "$META" orca_worktree_id)
|
|
743
|
+
ORCA_PATH_MATCH_VERIFIED=0
|
|
744
|
+
CLEANUP_RECOVERY=$TEARDOWN_CLEANUP_RECOVERY
|
|
745
|
+
|
|
746
|
+
KIND=$TEARDOWN_META_KIND
|
|
747
|
+
MODE=$(grep '^mode=' "$META" | cut -d= -f2- || true)
|
|
748
|
+
[ -n "$MODE" ] || MODE=no-mistakes
|
|
749
|
+
PUBLIC_FOLLOWUP_HOME=$FM_HOME
|
|
750
|
+
PUBLIC_FOLLOWUP_STATE=$STATE
|
|
751
|
+
PUBLIC_FOLLOWUP_WORK_HOME=main
|
|
752
|
+
PUBLIC_FOLLOWUP_PARENT_UNRESOLVED=0
|
|
753
|
+
PUBLIC_FOLLOWUP_PARENT_RELAY_ACTIVE=0
|
|
754
|
+
PUBLIC_FOLLOWUP_RELAY_ACTIVE=0
|
|
755
|
+
public_followup_canonical_home() {
|
|
756
|
+
local home=$1
|
|
757
|
+
case "$home" in /*) ;; *) return 1 ;; esac
|
|
758
|
+
CDPATH='' cd -- "$home" 2>/dev/null && pwd -P
|
|
759
|
+
}
|
|
760
|
+
public_followup_resolve_primary_home() {
|
|
761
|
+
local parent=$1 child=$2 id=$3 parent_meta registry meta_home
|
|
762
|
+
fm_pf_home_id_valid "secondmate:$id" || return 1
|
|
763
|
+
parent=$(public_followup_canonical_home "$parent") || return 1
|
|
764
|
+
child=$(public_followup_canonical_home "$child") || return 1
|
|
765
|
+
[ "$parent" != "$child" ] || return 1
|
|
766
|
+
parent_meta="$parent/state/$id.meta"
|
|
767
|
+
[ -f "$parent_meta" ] && [ ! -L "$parent_meta" ] || return 1
|
|
768
|
+
[ "$(fm_meta_get "$parent_meta" kind)" = secondmate ] || return 1
|
|
769
|
+
meta_home=$(fm_meta_get "$parent_meta" home)
|
|
770
|
+
meta_home=$(CDPATH='' cd -- "$meta_home" 2>/dev/null && pwd -P) || return 1
|
|
771
|
+
[ "$meta_home" = "$child" ] || return 1
|
|
772
|
+
registry="$parent/data/secondmates.md"
|
|
773
|
+
secondmate_registry_validate_bindings "$registry" secondmate_registry_path_key "$id" "$child" || return 1
|
|
774
|
+
printf '%s\n' "$parent"
|
|
775
|
+
}
|
|
776
|
+
if [ -f "$FM_HOME/$SUB_HOME_MARKER" ]; then
|
|
777
|
+
SECOND_MATE_ID=$(sed -n '1p' "$FM_HOME/$SUB_HOME_MARKER")
|
|
778
|
+
# The durable parent record (written once at seeding, next to the identity
|
|
779
|
+
# marker) names this home's route to its parent: "local" when they share a
|
|
780
|
+
# filesystem, "remote" when the parent lives on another machine. Absent for
|
|
781
|
+
# a home seeded before this record existed, which preserves today's exact
|
|
782
|
+
# env-var-only behavior for that legacy home rather than guessing its route.
|
|
783
|
+
PARENT_ROUTE_FILE="$FM_HOME/$SUB_HOME_PARENT_MARKER"
|
|
784
|
+
PARENT_ROUTE_RECORD=absent
|
|
785
|
+
PARENT_ROUTE=
|
|
786
|
+
PARENT_ROUTE_HOME=
|
|
787
|
+
if [ -e "$PARENT_ROUTE_FILE" ] || [ -L "$PARENT_ROUTE_FILE" ]; then
|
|
788
|
+
PARENT_ROUTE_RECORD=invalid
|
|
789
|
+
if fm_secondmate_parent_record_parse "$PARENT_ROUTE_FILE"; then
|
|
790
|
+
PARENT_ROUTE=$FM_SECONDMATE_PARENT_ROUTE
|
|
791
|
+
PARENT_ROUTE_HOME=$FM_SECONDMATE_PARENT_HOME
|
|
792
|
+
PARENT_ROUTE_RECORD=valid
|
|
793
|
+
fi
|
|
794
|
+
fi
|
|
795
|
+
if [ "$PARENT_ROUTE_RECORD" = invalid ]; then
|
|
796
|
+
PUBLIC_FOLLOWUP_PARENT_UNRESOLVED=1
|
|
797
|
+
elif [ "$PARENT_ROUTE" = remote ]; then
|
|
798
|
+
# The entire promised-public-reply subsystem is same-filesystem by
|
|
799
|
+
# construction (bin/fm-public-followup-emit.sh header): a parent recorded
|
|
800
|
+
# on another machine can never hold a delegated promise for this child, so
|
|
801
|
+
# the delegated-parent path is out of scope and never refuses cleanup on
|
|
802
|
+
# its own. A token committed directly to THIS home's own .env is still a
|
|
803
|
+
# real, same-filesystem signal, so it is still checked - but read only
|
|
804
|
+
# from the file, never from the process environment, so an unrelated
|
|
805
|
+
# export in the remote host's own login shell cannot trigger it the way
|
|
806
|
+
# fm_pf_relay_active's environment-wins rule would.
|
|
807
|
+
if [ -f "$FM_HOME/.env" ]; then
|
|
808
|
+
HOME_ENV_TOKEN=$(fmx_env_get FMX_PAIRING_TOKEN "$FM_HOME/.env")
|
|
809
|
+
[ -z "$HOME_ENV_TOKEN" ] || PUBLIC_FOLLOWUP_PARENT_RELAY_ACTIVE=1
|
|
810
|
+
fi
|
|
811
|
+
if [ "$PUBLIC_FOLLOWUP_PARENT_RELAY_ACTIVE" = 1 ]; then
|
|
812
|
+
PUBLIC_FOLLOWUP_PARENT_UNRESOLVED=1
|
|
813
|
+
else
|
|
814
|
+
PUBLIC_FOLLOWUP_HOME=
|
|
815
|
+
PUBLIC_FOLLOWUP_STATE=
|
|
816
|
+
fi
|
|
817
|
+
elif [ "$PARENT_ROUTE" = local ]; then
|
|
818
|
+
PUBLIC_FOLLOWUP_PARENT_UNRESOLVED=1
|
|
819
|
+
PRIMARY_HOME_CANDIDATE=${FM_PUBLIC_FOLLOWUP_PRIMARY_HOME:-$PARENT_ROUTE_HOME}
|
|
820
|
+
PARENT_BINDINGS_MATCH=1
|
|
821
|
+
if [ -n "${FM_PUBLIC_FOLLOWUP_PRIMARY_HOME:-}" ]; then
|
|
822
|
+
LIVE_PARENT_HOME=$(public_followup_canonical_home \
|
|
823
|
+
"$FM_PUBLIC_FOLLOWUP_PRIMARY_HOME") || PARENT_BINDINGS_MATCH=0
|
|
824
|
+
DURABLE_PARENT_HOME=$(public_followup_canonical_home \
|
|
825
|
+
"$PARENT_ROUTE_HOME") || PARENT_BINDINGS_MATCH=0
|
|
826
|
+
if [ "$PARENT_BINDINGS_MATCH" = 1 ] \
|
|
827
|
+
&& [ "$LIVE_PARENT_HOME" != "$DURABLE_PARENT_HOME" ]; then
|
|
828
|
+
PARENT_BINDINGS_MATCH=0
|
|
829
|
+
fi
|
|
830
|
+
fi
|
|
831
|
+
if [ "$PARENT_BINDINGS_MATCH" = 1 ] \
|
|
832
|
+
&& fm_pf_home_id_valid "secondmate:$SECOND_MATE_ID"; then
|
|
833
|
+
PUBLIC_FOLLOWUP_WORK_HOME="secondmate:$SECOND_MATE_ID"
|
|
834
|
+
if PUBLIC_FOLLOWUP_HOME=$(public_followup_resolve_primary_home \
|
|
835
|
+
"$PRIMARY_HOME_CANDIDATE" "$FM_HOME" "$SECOND_MATE_ID"); then
|
|
836
|
+
PUBLIC_FOLLOWUP_STATE="$PUBLIC_FOLLOWUP_HOME/state"
|
|
837
|
+
PUBLIC_FOLLOWUP_PARENT_UNRESOLVED=0
|
|
838
|
+
if [ "$FORCE" != "--force" ] \
|
|
839
|
+
&& fm_pf_relay_active "$PUBLIC_FOLLOWUP_HOME"; then
|
|
840
|
+
PUBLIC_FOLLOWUP_RELAY_ACTIVE=1
|
|
841
|
+
fi
|
|
842
|
+
else
|
|
843
|
+
PUBLIC_FOLLOWUP_HOME=
|
|
844
|
+
PUBLIC_FOLLOWUP_STATE=
|
|
845
|
+
fi
|
|
846
|
+
fi
|
|
847
|
+
else
|
|
848
|
+
# A home seeded before the durable record existed retains the legacy
|
|
849
|
+
# launch-time binding behavior unchanged.
|
|
850
|
+
PRIMARY_HOME_CANDIDATE=${FM_PUBLIC_FOLLOWUP_PRIMARY_HOME:-}
|
|
851
|
+
if [ -n "$PRIMARY_HOME_CANDIDATE" ]; then
|
|
852
|
+
if fm_pf_relay_active "$PRIMARY_HOME_CANDIDATE"; then
|
|
853
|
+
PUBLIC_FOLLOWUP_PARENT_RELAY_ACTIVE=1
|
|
854
|
+
fi
|
|
855
|
+
elif fm_pf_relay_active "$FM_HOME"; then
|
|
856
|
+
PUBLIC_FOLLOWUP_PARENT_RELAY_ACTIVE=1
|
|
857
|
+
fi
|
|
858
|
+
if [ "$PUBLIC_FOLLOWUP_PARENT_RELAY_ACTIVE" = 1 ]; then
|
|
859
|
+
PUBLIC_FOLLOWUP_PARENT_UNRESOLVED=1
|
|
860
|
+
if fm_pf_home_id_valid "secondmate:$SECOND_MATE_ID"; then
|
|
861
|
+
PUBLIC_FOLLOWUP_WORK_HOME="secondmate:$SECOND_MATE_ID"
|
|
862
|
+
if PUBLIC_FOLLOWUP_HOME=$(public_followup_resolve_primary_home \
|
|
863
|
+
"$PRIMARY_HOME_CANDIDATE" "$FM_HOME" "$SECOND_MATE_ID"); then
|
|
864
|
+
PUBLIC_FOLLOWUP_STATE="$PUBLIC_FOLLOWUP_HOME/state"
|
|
865
|
+
PUBLIC_FOLLOWUP_PARENT_UNRESOLVED=0
|
|
866
|
+
if [ "$FORCE" != "--force" ] \
|
|
867
|
+
&& fm_pf_relay_active "$PUBLIC_FOLLOWUP_HOME"; then
|
|
868
|
+
PUBLIC_FOLLOWUP_RELAY_ACTIVE=1
|
|
869
|
+
fi
|
|
870
|
+
else
|
|
871
|
+
PUBLIC_FOLLOWUP_HOME=
|
|
872
|
+
PUBLIC_FOLLOWUP_STATE=
|
|
873
|
+
fi
|
|
874
|
+
fi
|
|
875
|
+
else
|
|
876
|
+
PUBLIC_FOLLOWUP_HOME=
|
|
877
|
+
PUBLIC_FOLLOWUP_STATE=
|
|
878
|
+
fi
|
|
879
|
+
fi
|
|
880
|
+
elif [ "$KIND" = secondmate ]; then
|
|
881
|
+
PUBLIC_FOLLOWUP_WORK_HOME="secondmate:$ID"
|
|
882
|
+
if [ "$FORCE" != "--force" ] && fm_pf_relay_active "$FM_HOME"; then
|
|
883
|
+
PUBLIC_FOLLOWUP_RELAY_ACTIVE=1
|
|
884
|
+
fi
|
|
885
|
+
elif [ "$FORCE" != "--force" ] && fm_pf_relay_active "$FM_HOME"; then
|
|
886
|
+
PUBLIC_FOLLOWUP_RELAY_ACTIVE=1
|
|
887
|
+
fi
|
|
888
|
+
|
|
889
|
+
default_branch() {
|
|
890
|
+
local ref branch
|
|
891
|
+
ref=$(git -C "$PROJ" symbolic-ref --quiet --short refs/remotes/origin/HEAD 2>/dev/null || true)
|
|
892
|
+
if [ -n "$ref" ]; then
|
|
893
|
+
echo "${ref#origin/}"
|
|
894
|
+
return 0
|
|
895
|
+
fi
|
|
896
|
+
for branch in main master; do
|
|
897
|
+
if git -C "$PROJ" show-ref --verify --quiet "refs/heads/$branch"; then
|
|
898
|
+
echo "$branch"
|
|
899
|
+
return 0
|
|
900
|
+
fi
|
|
901
|
+
done
|
|
902
|
+
return 1
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
meta_value() {
|
|
906
|
+
local meta=$1 key=$2
|
|
907
|
+
fm_meta_get "$meta" "$key"
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
require_orca_worktree_id() {
|
|
911
|
+
local meta=$1 id
|
|
912
|
+
id=$(meta_value "$meta" orca_worktree_id)
|
|
913
|
+
if [ -z "$id" ]; then
|
|
914
|
+
echo "error: missing orca_worktree_id in $meta; cannot remove Orca worktree" >&2
|
|
915
|
+
return 1
|
|
916
|
+
fi
|
|
917
|
+
printf '%s\n' "$id"
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
require_orca_terminal() {
|
|
921
|
+
local meta=$1 terminal
|
|
922
|
+
terminal=$(meta_value "$meta" terminal)
|
|
923
|
+
if [ -z "$terminal" ]; then
|
|
924
|
+
echo "error: missing terminal in $meta; cannot close Orca terminal" >&2
|
|
925
|
+
return 1
|
|
926
|
+
fi
|
|
927
|
+
printf '%s\n' "$terminal"
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
if [ "$BACKEND" = orca ] && [ "$KIND" != secondmate ]; then
|
|
931
|
+
ORCA_WORKTREE_ID=$(require_orca_worktree_id "$META") || exit 1
|
|
932
|
+
T_ORCA=$(meta_value "$META" terminal)
|
|
933
|
+
[ -z "$T_ORCA" ] || T=$T_ORCA
|
|
934
|
+
fi
|
|
935
|
+
|
|
936
|
+
# Where a harness's firstmate-owned global turn-end registry entry lives is
|
|
937
|
+
# owned by bin/fm-control-lib.sh, so teardown and the control plane's relaunch
|
|
938
|
+
# retire the same artifact rather than each carrying its own copy of the path.
|
|
939
|
+
remove_grok_turnend_auth() {
|
|
940
|
+
local state_dir=$1 id=$2 token_path token='' path
|
|
941
|
+
token_path=$(fm_control_harness_turnend_token_path grok "$state_dir" "$id") || return 1
|
|
942
|
+
if [ -n "$token_path" ] && [ -f "$token_path" ]; then
|
|
943
|
+
IFS= read -r token < "$token_path" || [ -n "$token" ] || return 1
|
|
944
|
+
fi
|
|
945
|
+
path=$(fm_control_harness_turnend_auth_path grok "$token") || return 1
|
|
946
|
+
[ -n "$path" ] || return 0
|
|
947
|
+
rm -f -- "$path"
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
remove_kimi_turnend_auth() {
|
|
951
|
+
local state_dir=$1 id=$2 token_path token='' path
|
|
952
|
+
token_path=$(fm_control_harness_turnend_token_path kimi "$state_dir" "$id") || return 1
|
|
953
|
+
if [ -n "$token_path" ] && [ -f "$token_path" ]; then
|
|
954
|
+
IFS= read -r token < "$token_path" || [ -n "$token" ] || return 1
|
|
955
|
+
fi
|
|
956
|
+
path=$(fm_control_harness_turnend_auth_path kimi "$token") || return 1
|
|
957
|
+
[ -n "$path" ] || return 0
|
|
958
|
+
rm -f -- "$path"
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
retire_busy_state() {
|
|
962
|
+
local state_dir=$1 id=$2 gen=${3:-}
|
|
963
|
+
if [ -n "$gen" ]; then
|
|
964
|
+
"$SCRIPT_DIR/fm-busy-event.sh" retire "$state_dir" "$id" --gen "$gen"
|
|
965
|
+
elif [ -f "$state_dir/$id.busy-gen" ]; then
|
|
966
|
+
"$SCRIPT_DIR/fm-busy-event.sh" retire "$state_dir" "$id" --current-gen
|
|
967
|
+
fi
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
validate_pr_poll_cleanup() {
|
|
971
|
+
local state_dir=$1 id=$2 state_device artifact has_artifact=0
|
|
972
|
+
fm_task_id_path_safe "$id" || return 0
|
|
973
|
+
for artifact in "$state_dir/$id.check.sh" "$state_dir/$id.pr-poll" \
|
|
974
|
+
"$state_dir/$id.pr-poll-registration" "$state_dir/$id.pr-poll-retirement" \
|
|
975
|
+
"$state_dir/$id.check-trust"; do
|
|
976
|
+
[ -e "$artifact" ] || [ -L "$artifact" ] || continue
|
|
977
|
+
has_artifact=1
|
|
978
|
+
done
|
|
979
|
+
[ "$has_artifact" -eq 1 ] || return 0
|
|
980
|
+
[ -d "$state_dir" ] && [ ! -L "$state_dir" ] || return 1
|
|
981
|
+
state_device=$(fm_pr_file_device "$state_dir") || return 1
|
|
982
|
+
for artifact in "$state_dir/$id.check.sh" "$state_dir/$id.pr-poll" \
|
|
983
|
+
"$state_dir/$id.pr-poll-registration" "$state_dir/$id.pr-poll-retirement" \
|
|
984
|
+
"$state_dir/$id.check-trust"; do
|
|
985
|
+
[ -e "$artifact" ] || [ -L "$artifact" ] || continue
|
|
986
|
+
if [ ! -f "$artifact" ] || [ -L "$artifact" ] \
|
|
987
|
+
|| [ "$(fm_pr_file_device "$artifact")" != "$state_device" ] \
|
|
988
|
+
|| [ "$(fm_pr_file_link_count "$artifact")" != 1 ]; then
|
|
989
|
+
echo "REFUSED: unsafe task PR-check artifact; preserving task state." >&2
|
|
990
|
+
return 1
|
|
991
|
+
fi
|
|
992
|
+
done
|
|
993
|
+
if [ -e "$state_dir/$id.pr-poll-retirement" ] \
|
|
994
|
+
|| [ -L "$state_dir/$id.pr-poll-retirement" ]; then
|
|
995
|
+
fm_pr_poll_retirement_state_valid "$state_dir" "$id" || {
|
|
996
|
+
echo "REFUSED: invalid PR-poll retirement receipt; preserving task state." >&2
|
|
997
|
+
return 1
|
|
998
|
+
}
|
|
999
|
+
fi
|
|
1000
|
+
}
|
|
1001
|
+
|
|
1002
|
+
remove_pr_poll_artifacts() {
|
|
1003
|
+
local state_dir=$1 id=$2
|
|
1004
|
+
validate_pr_poll_cleanup "$state_dir" "$id" || return 1
|
|
1005
|
+
fm_pr_poll_retirement_recover_one "$state_dir" "$id" "$SCRIPT_DIR/fm-pr-poll.sh" || return 1
|
|
1006
|
+
fm_pr_poll_merge_notified_remove "$state_dir" "$id" || return 1
|
|
1007
|
+
rm -f "$state_dir/$id.check.sh" "$state_dir/$id.pr-poll" \
|
|
1008
|
+
"$state_dir/$id.pr-poll-registration" "$state_dir/$id.pr-poll-retirement" \
|
|
1009
|
+
"$state_dir/$id.check-trust" || return 1
|
|
1010
|
+
}
|
|
1011
|
+
|
|
1012
|
+
# Resolve the PR number for a worktree branch via gh-axi. Echoes the number on a
|
|
1013
|
+
# single match and returns 0; returns non-zero on no match or any lookup failure,
|
|
1014
|
+
# so the caller treats it as "no PR found" (fail-safe).
|
|
1015
|
+
pr_number_from_branch() {
|
|
1016
|
+
local branch=$1 out n
|
|
1017
|
+
[ -n "$branch" ] && [ "$branch" != HEAD ] || return 1
|
|
1018
|
+
out=$( cd "$WT" && gh-axi pr list --state all --head "$branch" --limit 1 2>/dev/null ) || return 1
|
|
1019
|
+
n=$(printf '%s\n' "$out" | sed -n 's/^[[:space:]]*\([0-9][0-9]*\),.*/\1/p' | head -1)
|
|
1020
|
+
[ -n "$n" ] || return 1
|
|
1021
|
+
printf '%s' "$n"
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
pr_number_from_target() {
|
|
1025
|
+
local target=$1 n
|
|
1026
|
+
case "$target" in
|
|
1027
|
+
'' ) return 1 ;;
|
|
1028
|
+
*"/pull/"*)
|
|
1029
|
+
n=${target##*/pull/}
|
|
1030
|
+
n=${n%%[!0-9]*}
|
|
1031
|
+
;;
|
|
1032
|
+
[0-9]*)
|
|
1033
|
+
n=${target%%[!0-9]*}
|
|
1034
|
+
;;
|
|
1035
|
+
*) return 1 ;;
|
|
1036
|
+
esac
|
|
1037
|
+
[ -n "$n" ] || return 1
|
|
1038
|
+
printf '%s' "$n"
|
|
1039
|
+
}
|
|
1040
|
+
|
|
1041
|
+
ensure_commit_object() {
|
|
1042
|
+
local target=$1 commit=$2 n
|
|
1043
|
+
git -C "$WT" cat-file -e "$commit^{commit}" 2>/dev/null && return 0
|
|
1044
|
+
n=$(pr_number_from_target "$target") || return 1
|
|
1045
|
+
git -C "$WT" remote get-url origin >/dev/null 2>&1 || return 1
|
|
1046
|
+
git -C "$WT" fetch --quiet origin "refs/pull/$n/head" >/dev/null 2>&1 || return 1
|
|
1047
|
+
git -C "$WT" cat-file -e "$commit^{commit}" 2>/dev/null
|
|
1048
|
+
}
|
|
1049
|
+
|
|
1050
|
+
patch_id_for_commit() {
|
|
1051
|
+
local commit=$1
|
|
1052
|
+
git -C "$WT" show --pretty=medium --no-ext-diff "$commit" 2>/dev/null \
|
|
1053
|
+
| git patch-id --stable 2>/dev/null \
|
|
1054
|
+
| awk 'NR == 1 { print $1 }'
|
|
1055
|
+
}
|
|
1056
|
+
|
|
1057
|
+
unpushed_patches_are_in_pr_head() {
|
|
1058
|
+
local pr_head=$1 current base pr_patch_ids commit patch_id unpushed
|
|
1059
|
+
current=$(git -C "$WT" rev-parse --verify HEAD 2>/dev/null) || return 1
|
|
1060
|
+
base=$(git -C "$WT" merge-base "$current" "$pr_head" 2>/dev/null) || return 1
|
|
1061
|
+
pr_patch_ids=$(
|
|
1062
|
+
git -C "$WT" log --format=%H "$base..$pr_head" -- 2>/dev/null \
|
|
1063
|
+
| while IFS= read -r commit; do
|
|
1064
|
+
patch_id_for_commit "$commit"
|
|
1065
|
+
done \
|
|
1066
|
+
| sed '/^$/d' \
|
|
1067
|
+
| sort -u
|
|
1068
|
+
) || return 1
|
|
1069
|
+
[ -n "$pr_patch_ids" ] || return 1
|
|
1070
|
+
unpushed=$(git -C "$WT" log --format=%H HEAD --not --remotes -- 2>/dev/null) || return 1
|
|
1071
|
+
[ -n "$unpushed" ] || return 1
|
|
1072
|
+
while IFS= read -r commit; do
|
|
1073
|
+
[ -n "$commit" ] || continue
|
|
1074
|
+
patch_id=$(patch_id_for_commit "$commit") || return 1
|
|
1075
|
+
[ -n "$patch_id" ] || return 1
|
|
1076
|
+
printf '%s\n' "$pr_patch_ids" | grep -qxF "$patch_id" || return 1
|
|
1077
|
+
done <<EOF
|
|
1078
|
+
$unpushed
|
|
1079
|
+
EOF
|
|
1080
|
+
}
|
|
1081
|
+
|
|
1082
|
+
# Is the worktree's PR merged for local work contained in that PR? Resolves the
|
|
1083
|
+
# PR from the recorded pr= URL first, then from the branch name, and asks GitHub
|
|
1084
|
+
# for both the PR state and head. Returns non-zero when the PR is not merged, the
|
|
1085
|
+
# current work is not contained in the PR head, no PR is found, or any gh error
|
|
1086
|
+
# occurs - the caller then falls back to the content check.
|
|
1087
|
+
pr_is_merged() {
|
|
1088
|
+
local branch=$1 target view state remainder head resolved_url current landed=0
|
|
1089
|
+
if [ -n "$PR_URL" ]; then
|
|
1090
|
+
target=$PR_URL
|
|
1091
|
+
else
|
|
1092
|
+
target=$(pr_number_from_branch "$branch") || return 1
|
|
1093
|
+
fi
|
|
1094
|
+
[ -n "$target" ] || return 1
|
|
1095
|
+
view=$(cd "$WT" && gh pr view "$target" --json state,headRefOid,url -q '.state + "\t" + .headRefOid + "\t" + .url' 2>/dev/null) || return 1
|
|
1096
|
+
state=${view%%$'\t'*}
|
|
1097
|
+
remainder=${view#*$'\t'}
|
|
1098
|
+
[ "$state" != "$view" ] || return 1
|
|
1099
|
+
head=${remainder%%$'\t'*}
|
|
1100
|
+
resolved_url=${remainder#*$'\t'}
|
|
1101
|
+
[ "$head" != "$remainder" ] || return 1
|
|
1102
|
+
case "$state" in
|
|
1103
|
+
MERGED|merged) ;;
|
|
1104
|
+
*) return 1 ;;
|
|
1105
|
+
esac
|
|
1106
|
+
[ -n "$head" ] || return 1
|
|
1107
|
+
ensure_commit_object "$target" "$head" || return 1
|
|
1108
|
+
current=$(git -C "$WT" rev-parse --verify HEAD 2>/dev/null) || return 1
|
|
1109
|
+
if git -C "$WT" merge-base --is-ancestor "$current" "$head" 2>/dev/null; then
|
|
1110
|
+
landed=1
|
|
1111
|
+
elif unpushed_patches_are_in_pr_head "$head"; then
|
|
1112
|
+
landed=1
|
|
1113
|
+
fi
|
|
1114
|
+
[ "$landed" = 1 ] || return 1
|
|
1115
|
+
if [ -z "$PR_URL" ]; then
|
|
1116
|
+
[ -n "$resolved_url" ] || return 1
|
|
1117
|
+
PR_URL=$resolved_url
|
|
1118
|
+
fi
|
|
1119
|
+
return 0
|
|
1120
|
+
}
|
|
1121
|
+
|
|
1122
|
+
# Is the branch's content already present in the up-to-date default branch? Fetches
|
|
1123
|
+
# first, then 3-way merges the default branch with HEAD: when HEAD introduces nothing
|
|
1124
|
+
# the default branch does not already contain (e.g. its change landed via squash) the
|
|
1125
|
+
# merged tree equals the default branch's tree. This isolates branch-only changes, so
|
|
1126
|
+
# unrelated commits the default branch gained past the merge-base do not count as
|
|
1127
|
+
# "added". Returns non-zero when inconclusive (no default ref, or a merge conflict),
|
|
1128
|
+
# so the caller refuses rather than guesses.
|
|
1129
|
+
content_in_default() {
|
|
1130
|
+
local name ref default_tree merged_tree
|
|
1131
|
+
name=$(default_branch) || return 1
|
|
1132
|
+
if git -C "$WT" remote get-url origin >/dev/null 2>&1; then
|
|
1133
|
+
git -C "$WT" fetch --quiet origin "+refs/heads/$name:refs/remotes/origin/$name" >/dev/null 2>&1 || return 1
|
|
1134
|
+
ref="refs/remotes/origin/$name"
|
|
1135
|
+
elif git -C "$WT" rev-parse --quiet --verify "refs/heads/$name" >/dev/null 2>&1; then
|
|
1136
|
+
ref="refs/heads/$name"
|
|
1137
|
+
else
|
|
1138
|
+
return 1
|
|
1139
|
+
fi
|
|
1140
|
+
default_tree=$(git -C "$WT" rev-parse --quiet --verify "$ref^{tree}" 2>/dev/null) || return 1
|
|
1141
|
+
[ -n "$default_tree" ] || return 1
|
|
1142
|
+
merged_tree=$(git -C "$WT" merge-tree --write-tree "$ref" HEAD 2>/dev/null) || return 1
|
|
1143
|
+
merged_tree=$(printf '%s\n' "$merged_tree" | head -1)
|
|
1144
|
+
[ "$merged_tree" = "$default_tree" ]
|
|
1145
|
+
}
|
|
1146
|
+
|
|
1147
|
+
# Has the worktree's committed work actually LANDED, though its commits are not
|
|
1148
|
+
# reachable from any remote-tracking branch? True when a merged PR proves the
|
|
1149
|
+
# current local work is contained in the PR head, OR the content is already in the
|
|
1150
|
+
# default branch (fallback, which also covers the no-PR and gh-error paths). False
|
|
1151
|
+
# only for genuinely unlanded work.
|
|
1152
|
+
work_is_landed() {
|
|
1153
|
+
local branch=$1
|
|
1154
|
+
pr_is_merged "$branch" && return 0
|
|
1155
|
+
content_in_default
|
|
1156
|
+
}
|
|
1157
|
+
|
|
1158
|
+
# The completion links this teardown already holds locally. A scout's
|
|
1159
|
+
# deliverable is its report, a local-only ship lands on local main, and every
|
|
1160
|
+
# other ship carries the PR recorded on its own record.
|
|
1161
|
+
BACKLOG_DONE_ARGS=()
|
|
1162
|
+
backlog_done_args() {
|
|
1163
|
+
local data_relative
|
|
1164
|
+
BACKLOG_DONE_ARGS=()
|
|
1165
|
+
case "$KIND" in
|
|
1166
|
+
scout)
|
|
1167
|
+
data_relative=$(fm_backlog_data_relative "$DATA") || return 1
|
|
1168
|
+
BACKLOG_DONE_ARGS=(--report "$data_relative/$ID/report.md")
|
|
1169
|
+
;;
|
|
1170
|
+
*)
|
|
1171
|
+
if [ "$MODE" = local-only ]; then
|
|
1172
|
+
BACKLOG_DONE_ARGS=(--note "local main")
|
|
1173
|
+
elif [ -n "$PR_URL" ]; then
|
|
1174
|
+
BACKLOG_DONE_ARGS=(--pr "$PR_URL")
|
|
1175
|
+
fi
|
|
1176
|
+
;;
|
|
1177
|
+
esac
|
|
1178
|
+
}
|
|
1179
|
+
|
|
1180
|
+
# Closing the backlog item is this script's own last act on the record, not a
|
|
1181
|
+
# printed instruction for a later turn (bin/fm-backlog-transition-lib.sh owns the
|
|
1182
|
+
# invariant). This prints what already happened, so the follow-up wording stays
|
|
1183
|
+
# only where a human still owes the edit.
|
|
1184
|
+
backlog_refresh_reminder() {
|
|
1185
|
+
local backlog_display
|
|
1186
|
+
[ "$KIND" = secondmate ] && return 0
|
|
1187
|
+
[ "$CLEANUP_RECOVERY" = orca ] && return 0
|
|
1188
|
+
if backlog_display=$(fm_backlog_file "$DATA"); then
|
|
1189
|
+
:
|
|
1190
|
+
else
|
|
1191
|
+
backlog_display="${DATA%/}/backlog.md"
|
|
1192
|
+
fi
|
|
1193
|
+
if [ "$BACKLOG_CLOSED" = 1 ]; then
|
|
1194
|
+
printf '%s\n' "Backlog: $ID is closed in $backlog_display. Run tasks-axi ready for dependency-cleared candidates, check date gates, and dispatch only work whose blockers are gone and date is due."
|
|
1195
|
+
else
|
|
1196
|
+
printf '%s\n' "Backlog: $ID just finished ($BACKLOG_SKIP_REASON). Update $backlog_display - move $ID to Done, keep Done to the 10 most recent, then re-scan Queued and dispatch only work whose blockers are gone and date is due."
|
|
1197
|
+
fi
|
|
1198
|
+
}
|
|
1199
|
+
|
|
1200
|
+
path_is_ancestor_of() {
|
|
1201
|
+
local ancestor=$1 path=$2
|
|
1202
|
+
[ -n "$ancestor" ] || return 1
|
|
1203
|
+
[ -n "$path" ] || return 1
|
|
1204
|
+
[ "$ancestor" != "$path" ] || return 1
|
|
1205
|
+
case "$path" in
|
|
1206
|
+
"$ancestor"/*) return 0 ;;
|
|
1207
|
+
esac
|
|
1208
|
+
return 1
|
|
1209
|
+
}
|
|
1210
|
+
|
|
1211
|
+
removal_target_abs_path() {
|
|
1212
|
+
local target=$1
|
|
1213
|
+
if [ -d "$target" ]; then
|
|
1214
|
+
cd "$target" && pwd -P
|
|
1215
|
+
else
|
|
1216
|
+
cd "$(dirname "$target")" && printf '%s/%s\n' "$(pwd -P)" "$(basename "$target")"
|
|
1217
|
+
fi
|
|
1218
|
+
}
|
|
1219
|
+
|
|
1220
|
+
worktree_registered_for_project() {
|
|
1221
|
+
local project=$1 target=$2 abs_target listed line listed_abs
|
|
1222
|
+
[ -n "$project" ] || return 1
|
|
1223
|
+
[ -d "$project" ] || return 1
|
|
1224
|
+
git -C "$project" rev-parse --git-dir >/dev/null 2>&1 || return 1
|
|
1225
|
+
abs_target=$(removal_target_abs_path "$target")
|
|
1226
|
+
listed=$(git -C "$project" -c core.quotePath=false worktree list --porcelain 2>/dev/null) || return 1
|
|
1227
|
+
while IFS= read -r line; do
|
|
1228
|
+
case "$line" in
|
|
1229
|
+
worktree\ *)
|
|
1230
|
+
listed_abs=$(removal_target_abs_path "${line#worktree }" 2>/dev/null || true)
|
|
1231
|
+
[ "$listed_abs" = "$abs_target" ] && return 0
|
|
1232
|
+
;;
|
|
1233
|
+
esac
|
|
1234
|
+
done <<EOF
|
|
1235
|
+
$listed
|
|
1236
|
+
EOF
|
|
1237
|
+
return 1
|
|
1238
|
+
}
|
|
1239
|
+
|
|
1240
|
+
inspectable_git_worktree() {
|
|
1241
|
+
local target=$1 top
|
|
1242
|
+
[ -n "$target" ] || return 1
|
|
1243
|
+
[ -d "$target" ] || return 1
|
|
1244
|
+
top=$(git -C "$target" rev-parse --show-toplevel 2>/dev/null) || return 1
|
|
1245
|
+
[ -n "$top" ] || return 1
|
|
1246
|
+
[ -d "$top" ] || return 1
|
|
1247
|
+
git -C "$top" rev-parse --git-dir >/dev/null 2>&1
|
|
1248
|
+
}
|
|
1249
|
+
|
|
1250
|
+
canonical_existing_dir() {
|
|
1251
|
+
local target=$1
|
|
1252
|
+
[ -n "$target" ] || return 1
|
|
1253
|
+
[ -d "$target" ] || return 1
|
|
1254
|
+
( cd "$target" && pwd -P )
|
|
1255
|
+
}
|
|
1256
|
+
|
|
1257
|
+
retry_wait_secs_is_valid() {
|
|
1258
|
+
[[ "$1" =~ ^([0-9]+([.][0-9]*)?|[.][0-9]+)$ ]]
|
|
1259
|
+
}
|
|
1260
|
+
|
|
1261
|
+
STALE_WORKTREE_LOCK_AGE_SECS=${FM_STALE_WORKTREE_LOCK_AGE_SECS:-30}
|
|
1262
|
+
# Bounded patience window for transient index.lock after killing a crew process.
|
|
1263
|
+
# New knobs are preferred; FM_STALE_WORKTREE_LOCK_RETRY_WAIT_SECS remains an alias
|
|
1264
|
+
# for the per-attempt wait so existing tests and operators keep working.
|
|
1265
|
+
TREEHOUSE_RETURN_LOCK_RETRIES=${FM_TREEHOUSE_RETURN_LOCK_RETRIES:-3}
|
|
1266
|
+
TREEHOUSE_RETURN_LOCK_RETRY_WAIT_SECS=${FM_TREEHOUSE_RETURN_LOCK_RETRY_WAIT_SECS:-${FM_STALE_WORKTREE_LOCK_RETRY_WAIT_SECS:-1}}
|
|
1267
|
+
if ! retry_wait_secs_is_valid "$TREEHOUSE_RETURN_LOCK_RETRY_WAIT_SECS"; then
|
|
1268
|
+
echo "teardown: invalid treehouse return lock retry wait '$TREEHOUSE_RETURN_LOCK_RETRY_WAIT_SECS'; using 1s" >&2
|
|
1269
|
+
TREEHOUSE_RETURN_LOCK_RETRY_WAIT_SECS=1
|
|
1270
|
+
fi
|
|
1271
|
+
# Compatibility alias used by the safety-check wait path and older call sites.
|
|
1272
|
+
STALE_WORKTREE_LOCK_RETRY_WAIT_SECS=$TREEHOUSE_RETURN_LOCK_RETRY_WAIT_SECS
|
|
1273
|
+
TEARDOWN_TREEHOUSE_LOCK_REFUSED=2
|
|
1274
|
+
TEARDOWN_WORKTREE_SAFETY_LOCK_BLOCKED=3
|
|
1275
|
+
TEARDOWN_PROCEVENT_RESTORE_FAILED=4
|
|
1276
|
+
|
|
1277
|
+
# True when treehouse/git stderr shows the transient index.lock "File exists" race.
|
|
1278
|
+
# Other return failures must not enter the retry path.
|
|
1279
|
+
treehouse_return_is_index_lock_error() {
|
|
1280
|
+
local text=$1
|
|
1281
|
+
printf '%s\n' "$text" | grep -Eq "Unable to create ['\"].*index\\.lock['\"]: File exists"
|
|
1282
|
+
}
|
|
1283
|
+
|
|
1284
|
+
# Absolute path to the git index lock for a worktree/repo dir, or empty when it
|
|
1285
|
+
# cannot be resolved (dir missing or not a git worktree at all).
|
|
1286
|
+
worktree_git_lock_path() {
|
|
1287
|
+
local dir=$1 lock abs_dir
|
|
1288
|
+
[ -n "$dir" ] && [ -d "$dir" ] || return 1
|
|
1289
|
+
lock=$(git -C "$dir" rev-parse --git-path index.lock 2>/dev/null) || return 1
|
|
1290
|
+
[ -n "$lock" ] || return 1
|
|
1291
|
+
case "$lock" in
|
|
1292
|
+
/*) printf '%s\n' "$lock" ;;
|
|
1293
|
+
*)
|
|
1294
|
+
abs_dir=$(canonical_existing_dir "$dir") || return 1
|
|
1295
|
+
printf '%s/%s\n' "$abs_dir" "$lock"
|
|
1296
|
+
;;
|
|
1297
|
+
esac
|
|
1298
|
+
}
|
|
1299
|
+
|
|
1300
|
+
# The lock-staleness proof (lsof holder check, mtime age, fail-safe defaults)
|
|
1301
|
+
# is owned by bin/fm-lock-lib.sh's fm_lock_is_provably_stale, sourced above.
|
|
1302
|
+
# Teardown passes the worktree dir as the companion directory and its own
|
|
1303
|
+
# STALE_WORKTREE_LOCK_AGE_SECS threshold.
|
|
1304
|
+
|
|
1305
|
+
worktree_safety_blocked_by_lock() {
|
|
1306
|
+
local reason=$1 lock
|
|
1307
|
+
lock=$(worktree_git_lock_path "$WT") || lock=""
|
|
1308
|
+
[ -n "$lock" ] && [ -e "$lock" ] || return 1
|
|
1309
|
+
echo "teardown: cannot inspect worktree $WT for $reason while git lock $lock is present; checking whether the lock is stale" >&2
|
|
1310
|
+
return 0
|
|
1311
|
+
}
|
|
1312
|
+
|
|
1313
|
+
cleanup_stale_lock_for_safety_check() {
|
|
1314
|
+
local dir=$1 lock
|
|
1315
|
+
lock=$(worktree_git_lock_path "$dir") || lock=""
|
|
1316
|
+
[ -n "$lock" ] && [ -e "$lock" ] || return 0
|
|
1317
|
+
|
|
1318
|
+
echo "teardown: worktree safety check blocked by git lock $lock; waiting ${STALE_WORKTREE_LOCK_RETRY_WAIT_SECS}s and retrying (owning process may be exiting)" >&2
|
|
1319
|
+
sleep "$STALE_WORKTREE_LOCK_RETRY_WAIT_SECS"
|
|
1320
|
+
|
|
1321
|
+
if [ ! -e "$lock" ]; then
|
|
1322
|
+
echo "teardown: worktree safety check lock cleared on its own; retrying safety checks" >&2
|
|
1323
|
+
return 0
|
|
1324
|
+
fi
|
|
1325
|
+
|
|
1326
|
+
if fm_lock_is_provably_stale "$lock" "$dir" "$STALE_WORKTREE_LOCK_AGE_SECS"; then
|
|
1327
|
+
rm -f "$lock"
|
|
1328
|
+
echo "teardown: removed provably-stale git lock $lock (age >= ${STALE_WORKTREE_LOCK_AGE_SECS}s, no live holder) and retrying worktree safety checks" >&2
|
|
1329
|
+
return 0
|
|
1330
|
+
fi
|
|
1331
|
+
|
|
1332
|
+
echo "teardown: worktree safety check blocked by git lock $lock that is not provably stale (may belong to a live process); leaving it in place" >&2
|
|
1333
|
+
return "$TEARDOWN_TREEHOUSE_LOCK_REFUSED"
|
|
1334
|
+
}
|
|
1335
|
+
|
|
1336
|
+
# Return a worktree/home via `treehouse return --force`, tolerating a transient or
|
|
1337
|
+
# stale git index.lock left by a killed crew process. See the script header.
|
|
1338
|
+
teardown_treehouse_return() {
|
|
1339
|
+
local dir=$1 cd_dir=$2 label=$3 post_cleanup_check=${4:-}
|
|
1340
|
+
local out lock attempt=0 max_retries lock_desc
|
|
1341
|
+
|
|
1342
|
+
# Capture stdout+stderr so non-lock failures stay visible and lock failures can
|
|
1343
|
+
# be matched by signature even when the lock file is already gone mid-check.
|
|
1344
|
+
if out=$( ( cd "$cd_dir" && treehouse return --force "$dir" ) 2>&1 ); then
|
|
1345
|
+
[ -n "$out" ] && printf '%s\n' "$out"
|
|
1346
|
+
return 0
|
|
1347
|
+
fi
|
|
1348
|
+
[ -n "$out" ] && printf '%s\n' "$out" >&2
|
|
1349
|
+
|
|
1350
|
+
if ! treehouse_return_is_index_lock_error "$out"; then
|
|
1351
|
+
return 1
|
|
1352
|
+
fi
|
|
1353
|
+
|
|
1354
|
+
lock=$(worktree_git_lock_path "$dir") || lock=""
|
|
1355
|
+
if [ -n "$lock" ]; then
|
|
1356
|
+
lock_desc=$lock
|
|
1357
|
+
else
|
|
1358
|
+
lock_desc="index.lock"
|
|
1359
|
+
fi
|
|
1360
|
+
|
|
1361
|
+
max_retries=$TREEHOUSE_RETURN_LOCK_RETRIES
|
|
1362
|
+
case "$max_retries" in ''|*[!0-9]*) max_retries=3 ;; esac
|
|
1363
|
+
|
|
1364
|
+
while [ "$attempt" -lt "$max_retries" ]; do
|
|
1365
|
+
attempt=$(( attempt + 1 ))
|
|
1366
|
+
echo "teardown: $label return failed with transient git lock ($lock_desc); waiting ${TREEHOUSE_RETURN_LOCK_RETRY_WAIT_SECS}s and retrying ($attempt/${max_retries})" >&2
|
|
1367
|
+
sleep "$TREEHOUSE_RETURN_LOCK_RETRY_WAIT_SECS"
|
|
1368
|
+
|
|
1369
|
+
if out=$( ( cd "$cd_dir" && treehouse return --force "$dir" ) 2>&1 ); then
|
|
1370
|
+
[ -n "$out" ] && printf '%s\n' "$out"
|
|
1371
|
+
echo "teardown: $label return succeeded on retry; lock cleared on its own" >&2
|
|
1372
|
+
return 0
|
|
1373
|
+
fi
|
|
1374
|
+
[ -n "$out" ] && printf '%s\n' "$out" >&2
|
|
1375
|
+
|
|
1376
|
+
if ! treehouse_return_is_index_lock_error "$out"; then
|
|
1377
|
+
echo "teardown: $label return failed with a non-lock error after retry; aborting" >&2
|
|
1378
|
+
return 1
|
|
1379
|
+
fi
|
|
1380
|
+
done
|
|
1381
|
+
|
|
1382
|
+
# Refresh lock path after the patience window; it may have appeared, moved, or
|
|
1383
|
+
# cleared while we waited.
|
|
1384
|
+
lock=$(worktree_git_lock_path "$dir") || lock=""
|
|
1385
|
+
if [ -n "$lock" ] && [ -e "$lock" ]; then
|
|
1386
|
+
lock_desc=$lock
|
|
1387
|
+
if fm_lock_is_provably_stale "$lock" "$dir" "$STALE_WORKTREE_LOCK_AGE_SECS"; then
|
|
1388
|
+
rm -f "$lock"
|
|
1389
|
+
echo "teardown: removed provably-stale git lock $lock (age >= ${STALE_WORKTREE_LOCK_AGE_SECS}s, no live holder) and retrying $label return" >&2
|
|
1390
|
+
if [ -n "$post_cleanup_check" ]; then
|
|
1391
|
+
if ! "$post_cleanup_check"; then
|
|
1392
|
+
echo "teardown: $label return aborted after stale-lock cleanup because safety checks failed" >&2
|
|
1393
|
+
return 1
|
|
1394
|
+
fi
|
|
1395
|
+
fi
|
|
1396
|
+
if out=$( ( cd "$cd_dir" && treehouse return --force "$dir" ) 2>&1 ); then
|
|
1397
|
+
[ -n "$out" ] && printf '%s\n' "$out"
|
|
1398
|
+
echo "teardown: $label return succeeded after stale-lock cleanup" >&2
|
|
1399
|
+
return 0
|
|
1400
|
+
fi
|
|
1401
|
+
[ -n "$out" ] && printf '%s\n' "$out" >&2
|
|
1402
|
+
echo "teardown: $label return still failing after stale-lock cleanup" >&2
|
|
1403
|
+
return 1
|
|
1404
|
+
fi
|
|
1405
|
+
|
|
1406
|
+
echo "teardown: $label return failed: git lock $lock_desc persisted across ${max_retries} retries (waiting ${TREEHOUSE_RETURN_LOCK_RETRY_WAIT_SECS}s each) and is not provably stale (may belong to a live process); leaving it in place" >&2
|
|
1407
|
+
return "$TEARDOWN_TREEHOUSE_LOCK_REFUSED"
|
|
1408
|
+
fi
|
|
1409
|
+
|
|
1410
|
+
echo "teardown: $label return failed: git index.lock signature persisted across ${max_retries} retries (waiting ${TREEHOUSE_RETURN_LOCK_RETRY_WAIT_SECS}s each) even after the lock file disappeared" >&2
|
|
1411
|
+
return 1
|
|
1412
|
+
}
|
|
1413
|
+
|
|
1414
|
+
validate_worktree_teardown_safety() {
|
|
1415
|
+
local dirty_raw dirty unpushed_raw unpushed DEFAULT unmerged_raw unmerged branch
|
|
1416
|
+
[ -d "$WT" ] || return 0
|
|
1417
|
+
[ "$FORCE" != "--force" ] || return 0
|
|
1418
|
+
case "$KIND" in
|
|
1419
|
+
secondmate|scout) return 0 ;;
|
|
1420
|
+
esac
|
|
1421
|
+
|
|
1422
|
+
if ! dirty_raw=$(git -C "$WT" status --porcelain 2>/dev/null); then
|
|
1423
|
+
if worktree_safety_blocked_by_lock "uncommitted changes"; then
|
|
1424
|
+
return "$TEARDOWN_WORKTREE_SAFETY_LOCK_BLOCKED"
|
|
1425
|
+
fi
|
|
1426
|
+
echo "REFUSED: cannot inspect worktree $WT for uncommitted changes." >&2
|
|
1427
|
+
echo "Restore the git index state, or get the captain's explicit OK to discard, then --force." >&2
|
|
1428
|
+
return 1
|
|
1429
|
+
fi
|
|
1430
|
+
dirty=$(printf '%s\n' "$dirty_raw" | grep -vE '^\?\? (\.claude/|\.fm-(grok|kimi)-turnend$)' | head -1 || true)
|
|
1431
|
+
|
|
1432
|
+
if ! unpushed_raw=$(git -C "$WT" log --oneline HEAD --not --remotes -- 2>/dev/null); then
|
|
1433
|
+
if worktree_safety_blocked_by_lock "commits not on a remote"; then
|
|
1434
|
+
return "$TEARDOWN_WORKTREE_SAFETY_LOCK_BLOCKED"
|
|
1435
|
+
fi
|
|
1436
|
+
echo "REFUSED: cannot inspect worktree $WT for commits not on a remote." >&2
|
|
1437
|
+
echo "Restore the git index state, or get the captain's explicit OK to discard, then --force." >&2
|
|
1438
|
+
return 1
|
|
1439
|
+
fi
|
|
1440
|
+
unpushed=$(printf '%s\n' "$unpushed_raw" | head -5)
|
|
1441
|
+
|
|
1442
|
+
if [ -n "$unpushed" ] && [ "$MODE" = local-only ]; then
|
|
1443
|
+
DEFAULT=$(default_branch) || { echo "REFUSED: cannot determine default branch for $PROJ; expected origin/HEAD, main, or master." >&2; return 1; }
|
|
1444
|
+
if ! unmerged_raw=$(git -C "$WT" log --oneline HEAD --not "$DEFAULT" -- 2>/dev/null); then
|
|
1445
|
+
if worktree_safety_blocked_by_lock "commits not on $DEFAULT"; then
|
|
1446
|
+
return "$TEARDOWN_WORKTREE_SAFETY_LOCK_BLOCKED"
|
|
1447
|
+
fi
|
|
1448
|
+
echo "REFUSED: cannot inspect worktree $WT for commits not on $DEFAULT." >&2
|
|
1449
|
+
echo "Restore the git index state, or get the captain's explicit OK to discard, then --force." >&2
|
|
1450
|
+
return 1
|
|
1451
|
+
fi
|
|
1452
|
+
unmerged=$(printf '%s\n' "$unmerged_raw" | head -5)
|
|
1453
|
+
if [ -n "$dirty" ] || [ -n "$unmerged" ]; then
|
|
1454
|
+
echo "REFUSED: local-only worktree $WT has work not yet merged into $DEFAULT and not on any remote." >&2
|
|
1455
|
+
[ -n "$dirty" ] && echo "uncommitted changes present" >&2
|
|
1456
|
+
[ -n "$unmerged" ] && printf 'commits not yet on %s:\n%s\n' "$DEFAULT" "$unmerged" >&2
|
|
1457
|
+
echo "Merge the branch into local $DEFAULT first (bin/fm-merge-local.sh after the captain approves), or push to a fork/remote, or get the captain's explicit OK to discard, then --force." >&2
|
|
1458
|
+
return 1
|
|
1459
|
+
fi
|
|
1460
|
+
elif [ -n "$dirty" ]; then
|
|
1461
|
+
echo "REFUSED: worktree $WT has uncommitted changes." >&2
|
|
1462
|
+
echo "uncommitted changes present" >&2
|
|
1463
|
+
echo "Commit them (or get the captain's explicit OK to discard, then --force)." >&2
|
|
1464
|
+
return 1
|
|
1465
|
+
elif [ -n "$unpushed" ]; then
|
|
1466
|
+
branch=${TEARDOWN_WORKTREE_BRANCH_FOR_SAFETY:-}
|
|
1467
|
+
if [ -z "$branch" ]; then
|
|
1468
|
+
branch=$(git -C "$WT" rev-parse --abbrev-ref HEAD 2>/dev/null || echo HEAD)
|
|
1469
|
+
TEARDOWN_WORKTREE_BRANCH_FOR_SAFETY=$branch
|
|
1470
|
+
fi
|
|
1471
|
+
if ! work_is_landed "$branch"; then
|
|
1472
|
+
echo "REFUSED: worktree $WT has work not on any remote and not landed." >&2
|
|
1473
|
+
printf 'unpushed commits:\n%s\n' "$unpushed" >&2
|
|
1474
|
+
echo "Push the branch, land its PR, or get the captain's explicit OK to discard, then --force." >&2
|
|
1475
|
+
return 1
|
|
1476
|
+
fi
|
|
1477
|
+
fi
|
|
1478
|
+
}
|
|
1479
|
+
|
|
1480
|
+
# Fix 1 (see script header): does the active-or-most-recent no-mistakes run in
|
|
1481
|
+
# worktree $1 belong to THIS task, and is it parked at a gate awaiting an agent
|
|
1482
|
+
# that is about to be removed? Prints nothing; returns 0 only on a genuine
|
|
1483
|
+
# match so the caller knows it is safe to abort - never a guess.
|
|
1484
|
+
NM_TEARDOWN_TIMEOUT=${FM_TEARDOWN_NM_TIMEOUT:-10}
|
|
1485
|
+
case "$NM_TEARDOWN_TIMEOUT" in ''|*[!0-9]*) NM_TEARDOWN_TIMEOUT=10 ;; esac
|
|
1486
|
+
TASK_RUN_ID=
|
|
1487
|
+
task_status_is_own_parked_run() { # <worktree> <axi-status-output>
|
|
1488
|
+
local wt=$1 out=$2 branch run_id run_branch run_head status outcome awaiting has_gate
|
|
1489
|
+
TASK_RUN_ID=
|
|
1490
|
+
branch=$(git -C "$wt" symbolic-ref --quiet --short HEAD 2>/dev/null) || return 1
|
|
1491
|
+
[ -n "$branch" ] || return 1
|
|
1492
|
+
[ -n "$out" ] || return 1
|
|
1493
|
+
run_id=$(fm_nm_strip_quotes "$(fm_nm_field "$out" id)")
|
|
1494
|
+
[ -n "$run_id" ] || return 1
|
|
1495
|
+
run_branch=$(fm_nm_strip_quotes "$(fm_nm_field "$out" branch)")
|
|
1496
|
+
[ -n "$run_branch" ] && [ "$run_branch" = "$branch" ] || return 1
|
|
1497
|
+
run_head=$(fm_nm_strip_quotes "$(fm_nm_field "$out" head)")
|
|
1498
|
+
fm_nm_head_matches_worktree "$wt" "$run_head" || return 1
|
|
1499
|
+
outcome=$(fm_nm_strip_quotes "$(fm_nm_field "$out" outcome)")
|
|
1500
|
+
[ -z "$outcome" ] || return 1
|
|
1501
|
+
status=$(fm_nm_strip_quotes "$(fm_nm_field "$out" status)")
|
|
1502
|
+
awaiting=$(printf '%s\n' "$out" | grep -E '^[[:space:]]*awaiting_agent:' | head -1 || true)
|
|
1503
|
+
has_gate=$(printf '%s\n' "$out" | grep -Eq '^[[:space:]]*gate:[[:space:]]*' && echo 1 || echo 0)
|
|
1504
|
+
case "$status" in
|
|
1505
|
+
awaiting_approval|fix_review) TASK_RUN_ID=$run_id; return 0 ;;
|
|
1506
|
+
esac
|
|
1507
|
+
if [ -n "$awaiting" ] || [ "$has_gate" = 1 ]; then
|
|
1508
|
+
TASK_RUN_ID=$run_id
|
|
1509
|
+
return 0
|
|
1510
|
+
fi
|
|
1511
|
+
return 1
|
|
1512
|
+
}
|
|
1513
|
+
|
|
1514
|
+
task_run_is_own_parked_run() { # <worktree>
|
|
1515
|
+
local wt=$1 out
|
|
1516
|
+
# Accepted best-effort residual: query failures stay fail-open because making
|
|
1517
|
+
# no-mistakes availability a prerequisite would block ship tasks with no run.
|
|
1518
|
+
out=$(fm_nm_run "$wt" "$NM_TEARDOWN_TIMEOUT" axi status)
|
|
1519
|
+
task_status_is_own_parked_run "$wt" "$out"
|
|
1520
|
+
}
|
|
1521
|
+
|
|
1522
|
+
task_status_is_terminal_run() { # <axi-status-output> <run-id>
|
|
1523
|
+
local out=$1 expected_id=$2 run_id outcome
|
|
1524
|
+
run_id=$(fm_nm_strip_quotes "$(fm_nm_field "$out" id)")
|
|
1525
|
+
[ "$run_id" = "$expected_id" ] || return 1
|
|
1526
|
+
outcome=$(fm_nm_strip_quotes "$(fm_nm_field "$out" outcome)")
|
|
1527
|
+
case "$outcome" in
|
|
1528
|
+
cancelled|failed|passed|checks-passed) return 0 ;;
|
|
1529
|
+
esac
|
|
1530
|
+
return 1
|
|
1531
|
+
}
|
|
1532
|
+
|
|
1533
|
+
task_status_is_run_not_found() { # <status-error> <run-id>
|
|
1534
|
+
local actual expected
|
|
1535
|
+
actual=$(fm_nm_trim "$1")
|
|
1536
|
+
expected=$(printf 'error: "run \\"%s\\" not found"' "$2")
|
|
1537
|
+
[ "$actual" = "$expected" ]
|
|
1538
|
+
}
|
|
1539
|
+
|
|
1540
|
+
# Abort THIS task's own parked no-mistakes run before the worker that would
|
|
1541
|
+
# have answered its gate is removed, so no run is left orphaned holding a
|
|
1542
|
+
# fleet slot. Only KIND=ship drives a no-mistakes validation of its own
|
|
1543
|
+
# worktree (scouts and secondmates never do, mirroring bin/fm-crew-state.sh);
|
|
1544
|
+
# a run not attributed to this exact branch+head is left completely alone.
|
|
1545
|
+
conclude_task_no_mistakes_run() { # <worktree>
|
|
1546
|
+
local wt=$1 out run_id
|
|
1547
|
+
[ "$KIND" = ship ] || return 0
|
|
1548
|
+
[ -d "$wt" ] || return 0
|
|
1549
|
+
command -v no-mistakes >/dev/null 2>&1 || return 0
|
|
1550
|
+
task_run_is_own_parked_run "$wt" || return 0
|
|
1551
|
+
run_id=$TASK_RUN_ID
|
|
1552
|
+
echo "teardown: no-mistakes run for $ID is parked at a gate; aborting before the worker is removed" >&2
|
|
1553
|
+
# Accepted best-effort residual: abort supports run-id targeting but no atomic
|
|
1554
|
+
# live-state condition; fully closing the resume race needs upstream compare-and-cancel.
|
|
1555
|
+
fm_nm_run_checked "$wt" "$NM_TEARDOWN_TIMEOUT" axi abort --run "$run_id" >/dev/null 2>&1 || true
|
|
1556
|
+
if out=$(fm_nm_run_bounded "$wt" "$NM_TEARDOWN_TIMEOUT" axi status --run "$run_id" 2>&1); then
|
|
1557
|
+
task_status_is_terminal_run "$out" "$run_id" && return 0
|
|
1558
|
+
elif task_status_is_run_not_found "$out" "$run_id"; then
|
|
1559
|
+
return 0
|
|
1560
|
+
fi
|
|
1561
|
+
echo "REFUSED: no-mistakes run for $ID is still parked after axi abort; confirm it stopped (no-mistakes axi status) or abort it manually (no-mistakes axi abort --run <id>) before retrying teardown." >&2
|
|
1562
|
+
return 1
|
|
1563
|
+
}
|
|
1564
|
+
|
|
1565
|
+
# Fix 2 (see script header): pids of every process whose CURRENT WORKING
|
|
1566
|
+
# DIRECTORY is exactly $1 or under it, from one bounded system-wide `lsof -a
|
|
1567
|
+
# -d cwd` scan (never the recursive +D file-tree walk, which lsof itself
|
|
1568
|
+
# documents as slow). Never $$ (this script's own pid). Empty output when
|
|
1569
|
+
# nothing matches; failure means the scan could not establish a safe result.
|
|
1570
|
+
pids_with_cwd_under() { # <dir>
|
|
1571
|
+
local dir=$1 out pid path line
|
|
1572
|
+
[ -n "$dir" ] && [ -d "$dir" ] || return 0
|
|
1573
|
+
dir=$(cd "$dir" && pwd -P) || return 1
|
|
1574
|
+
out=$(lsof -a -d cwd -Fpn 2>/dev/null) || return 1
|
|
1575
|
+
[ -n "$out" ] || return 0
|
|
1576
|
+
pid=
|
|
1577
|
+
while IFS= read -r line; do
|
|
1578
|
+
case "$line" in
|
|
1579
|
+
p*)
|
|
1580
|
+
pid=${line#p}
|
|
1581
|
+
case "$pid" in ''|*[!0-9]*) return 1 ;; esac
|
|
1582
|
+
;;
|
|
1583
|
+
fcwd) [ -n "$pid" ] || return 1 ;;
|
|
1584
|
+
n*)
|
|
1585
|
+
[ -n "$pid" ] || return 1
|
|
1586
|
+
path=${line#n}
|
|
1587
|
+
case "$path" in
|
|
1588
|
+
"$dir"|"$dir"/*)
|
|
1589
|
+
[ -n "$pid" ] && [ "$pid" != "$$" ] && printf '%s\n' "$pid"
|
|
1590
|
+
;;
|
|
1591
|
+
esac
|
|
1592
|
+
;;
|
|
1593
|
+
'') ;;
|
|
1594
|
+
*) return 1 ;;
|
|
1595
|
+
esac
|
|
1596
|
+
done <<EOF
|
|
1597
|
+
$out
|
|
1598
|
+
EOF
|
|
1599
|
+
}
|
|
1600
|
+
|
|
1601
|
+
task_process_identity() { # <pid>
|
|
1602
|
+
local pid=$1 proc_root stat_line starttime value
|
|
1603
|
+
local -a stat_fields
|
|
1604
|
+
proc_root=${FM_PROC_ROOT_OVERRIDE:-/proc}
|
|
1605
|
+
if [ -r "$proc_root/$pid/stat" ]; then
|
|
1606
|
+
stat_line=$(cat "$proc_root/$pid/stat" 2>/dev/null) || return 1
|
|
1607
|
+
read -r -a stat_fields <<< "${stat_line##*)}"
|
|
1608
|
+
[ "${#stat_fields[@]}" -ge 20 ] || return 1
|
|
1609
|
+
starttime=${stat_fields[19]}
|
|
1610
|
+
case "$starttime" in ''|*[!0-9]*) return 1 ;; esac
|
|
1611
|
+
printf 'starttime=%s\n' "$starttime"
|
|
1612
|
+
return 0
|
|
1613
|
+
fi
|
|
1614
|
+
value=$(LC_ALL=C ps -p "$pid" -o lstart= 2>/dev/null) || return 1
|
|
1615
|
+
value=$(fm_nm_trim "$value")
|
|
1616
|
+
[ -n "$value" ] || return 1
|
|
1617
|
+
case "$value" in *$'\n'*|*$'\r'*) return 1 ;; esac
|
|
1618
|
+
printf 'lstart=%s\n' "$value"
|
|
1619
|
+
}
|
|
1620
|
+
|
|
1621
|
+
task_process_identity_matches() { # <pid> <identity>
|
|
1622
|
+
local current
|
|
1623
|
+
current=$(task_process_identity "$1") || return 1
|
|
1624
|
+
[ "$current" = "$2" ]
|
|
1625
|
+
}
|
|
1626
|
+
|
|
1627
|
+
task_pid_list_contains() { # <pid-list> <pid>
|
|
1628
|
+
printf '%s\n' "$1" | grep -Fxq "$2"
|
|
1629
|
+
}
|
|
1630
|
+
|
|
1631
|
+
task_pids_under_roots() { # <dir>...
|
|
1632
|
+
TASK_PIDS=
|
|
1633
|
+
TASK_PIDS_FAILED_DIR=
|
|
1634
|
+
local dir dir_pids pids=""
|
|
1635
|
+
for dir in "$@"; do
|
|
1636
|
+
[ -n "$dir" ] || continue
|
|
1637
|
+
if ! dir_pids=$(pids_with_cwd_under "$dir"); then
|
|
1638
|
+
TASK_PIDS_FAILED_DIR=$dir
|
|
1639
|
+
return 1
|
|
1640
|
+
fi
|
|
1641
|
+
pids="$pids
|
|
1642
|
+
$dir_pids"
|
|
1643
|
+
done
|
|
1644
|
+
TASK_PIDS=$(printf '%s\n' "$pids" | grep -E '^[0-9]+$' | sort -un || true)
|
|
1645
|
+
}
|
|
1646
|
+
|
|
1647
|
+
reap_task_backend_process_group() { # <label>
|
|
1648
|
+
local label=$1 leader leader_start pgid current_pgid own_pgid
|
|
1649
|
+
if [ "$BACKEND" != tmux ]; then
|
|
1650
|
+
echo "warning: lsof is unavailable; cannot resolve a process-group fallback for $BACKEND task $ID" >&2
|
|
1651
|
+
return 0
|
|
1652
|
+
fi
|
|
1653
|
+
leader=$(tmux display-message -p -t "$T" '#{pane_pid}' 2>/dev/null) || leader=""
|
|
1654
|
+
case "$leader" in ''|*[!0-9]*)
|
|
1655
|
+
echo "warning: lsof is unavailable; cannot resolve the tmux pane process group for $ID" >&2
|
|
1656
|
+
return 0
|
|
1657
|
+
;;
|
|
1658
|
+
esac
|
|
1659
|
+
leader_start=$(task_process_identity "$leader") || {
|
|
1660
|
+
echo "warning: lsof is unavailable; cannot identify the tmux pane process group for $ID" >&2
|
|
1661
|
+
return 0
|
|
1662
|
+
}
|
|
1663
|
+
pgid=$(ps -o pgid= -p "$leader" 2>/dev/null) || pgid=""
|
|
1664
|
+
pgid=$(printf '%s' "$pgid" | tr -d '[:space:]')
|
|
1665
|
+
case "$pgid" in ''|*[!0-9]*|0|1)
|
|
1666
|
+
echo "warning: lsof is unavailable; cannot resolve the tmux pane process group for $ID" >&2
|
|
1667
|
+
return 0
|
|
1668
|
+
;;
|
|
1669
|
+
esac
|
|
1670
|
+
own_pgid=$(ps -o pgid= -p "$$" 2>/dev/null) || own_pgid=""
|
|
1671
|
+
own_pgid=$(printf '%s' "$own_pgid" | tr -d '[:space:]')
|
|
1672
|
+
if [ "$pgid" = "$own_pgid" ]; then
|
|
1673
|
+
echo "warning: lsof is unavailable; refusing to signal teardown's own process group for $ID" >&2
|
|
1674
|
+
return 0
|
|
1675
|
+
fi
|
|
1676
|
+
task_process_identity_matches "$leader" "$leader_start" || return 0
|
|
1677
|
+
current_pgid=$(ps -o pgid= -p "$leader" 2>/dev/null) || current_pgid=""
|
|
1678
|
+
current_pgid=$(printf '%s' "$current_pgid" | tr -d '[:space:]')
|
|
1679
|
+
[ "$current_pgid" = "$pgid" ] || return 0
|
|
1680
|
+
echo "teardown: reaping leaked $label process group for $ID: $pgid" >&2
|
|
1681
|
+
kill -TERM -- "-$pgid" 2>/dev/null || true
|
|
1682
|
+
sleep 1
|
|
1683
|
+
if task_process_identity_matches "$leader" "$leader_start" \
|
|
1684
|
+
&& [ "$(ps -o pgid= -p "$leader" 2>/dev/null | tr -d '[:space:]')" = "$pgid" ] \
|
|
1685
|
+
&& kill -0 -- "-$pgid" 2>/dev/null; then
|
|
1686
|
+
echo "teardown: force-killing leaked $label process group for $ID: $pgid" >&2
|
|
1687
|
+
kill -KILL -- "-$pgid" 2>/dev/null || true
|
|
1688
|
+
fi
|
|
1689
|
+
}
|
|
1690
|
+
|
|
1691
|
+
# Reap every process rooted (by cwd) under this task's own worktree or tasktmp
|
|
1692
|
+
# - both unique per task and never shared - before either is removed. TERM
|
|
1693
|
+
# first, then KILL after a short grace period for anything still alive; a
|
|
1694
|
+
# process that exits on its own between the two passes is simply absent from
|
|
1695
|
+
# the recheck. A missing lsof uses the backend process-group fallback; an lsof
|
|
1696
|
+
# scan error refuses before destructive teardown.
|
|
1697
|
+
reap_task_worktree_processes() { # <label> <dir>...
|
|
1698
|
+
local label=$1 pids pid identity current_pids i pass=1 max_passes=3
|
|
1699
|
+
local -a tracked_pids tracked_identities remaining_pids remaining_identities
|
|
1700
|
+
shift
|
|
1701
|
+
if ! command -v lsof >/dev/null 2>&1; then
|
|
1702
|
+
reap_task_backend_process_group "$label"
|
|
1703
|
+
return 0
|
|
1704
|
+
fi
|
|
1705
|
+
while [ "$pass" -le "$max_passes" ]; do
|
|
1706
|
+
if ! task_pids_under_roots "$@"; then
|
|
1707
|
+
echo "REFUSED: cannot determine leaked processes under ${TASK_PIDS_FAILED_DIR:-<missing>} for $ID (lsof failed); preserving the worktree/tasktmp for manual inspection or retry." >&2
|
|
1708
|
+
return 1
|
|
1709
|
+
fi
|
|
1710
|
+
pids=$TASK_PIDS
|
|
1711
|
+
[ -n "$pids" ] || return 0
|
|
1712
|
+
tracked_pids=()
|
|
1713
|
+
tracked_identities=()
|
|
1714
|
+
while IFS= read -r pid; do
|
|
1715
|
+
[ -n "$pid" ] || continue
|
|
1716
|
+
if ! identity=$(task_process_identity "$pid"); then
|
|
1717
|
+
if ! task_pids_under_roots "$@"; then
|
|
1718
|
+
echo "REFUSED: cannot determine leaked processes under ${TASK_PIDS_FAILED_DIR:-<missing>} for $ID (lsof failed); preserving the worktree/tasktmp for manual inspection or retry." >&2
|
|
1719
|
+
return 1
|
|
1720
|
+
fi
|
|
1721
|
+
if task_pid_list_contains "$TASK_PIDS" "$pid"; then
|
|
1722
|
+
echo "REFUSED: cannot verify leaked process $pid identity for $ID; preserving the worktree/tasktmp for manual inspection or retry." >&2
|
|
1723
|
+
return 1
|
|
1724
|
+
fi
|
|
1725
|
+
continue
|
|
1726
|
+
fi
|
|
1727
|
+
tracked_pids+=("$pid")
|
|
1728
|
+
tracked_identities+=("$identity")
|
|
1729
|
+
done <<EOF
|
|
1730
|
+
$pids
|
|
1731
|
+
EOF
|
|
1732
|
+
if [ "${#tracked_pids[@]}" -eq 0 ]; then
|
|
1733
|
+
pass=$((pass + 1))
|
|
1734
|
+
continue
|
|
1735
|
+
fi
|
|
1736
|
+
if ! task_pids_under_roots "$@"; then
|
|
1737
|
+
echo "REFUSED: cannot determine leaked processes under ${TASK_PIDS_FAILED_DIR:-<missing>} for $ID (lsof failed); preserving the worktree/tasktmp for manual inspection or retry." >&2
|
|
1738
|
+
return 1
|
|
1739
|
+
fi
|
|
1740
|
+
current_pids=$TASK_PIDS
|
|
1741
|
+
echo "teardown: reaping leaked $label process(es) for $ID: $(printf '%s' "$pids" | tr '\n' ' ')" >&2
|
|
1742
|
+
for i in "${!tracked_pids[@]}"; do
|
|
1743
|
+
pid=${tracked_pids[$i]}
|
|
1744
|
+
identity=${tracked_identities[$i]}
|
|
1745
|
+
if task_pid_list_contains "$current_pids" "$pid" \
|
|
1746
|
+
&& task_process_identity_matches "$pid" "$identity"; then
|
|
1747
|
+
kill -TERM "$pid" 2>/dev/null || true
|
|
1748
|
+
fi
|
|
1749
|
+
done
|
|
1750
|
+
sleep 1
|
|
1751
|
+
if ! task_pids_under_roots "$@"; then
|
|
1752
|
+
echo "REFUSED: cannot determine leaked processes under ${TASK_PIDS_FAILED_DIR:-<missing>} for $ID (lsof failed); preserving the worktree/tasktmp for manual inspection or retry." >&2
|
|
1753
|
+
return 1
|
|
1754
|
+
fi
|
|
1755
|
+
current_pids=$TASK_PIDS
|
|
1756
|
+
remaining_pids=()
|
|
1757
|
+
remaining_identities=()
|
|
1758
|
+
for i in "${!tracked_pids[@]}"; do
|
|
1759
|
+
pid=${tracked_pids[$i]}
|
|
1760
|
+
identity=${tracked_identities[$i]}
|
|
1761
|
+
if task_pid_list_contains "$current_pids" "$pid" \
|
|
1762
|
+
&& task_process_identity_matches "$pid" "$identity"; then
|
|
1763
|
+
remaining_pids+=("$pid")
|
|
1764
|
+
remaining_identities+=("$identity")
|
|
1765
|
+
fi
|
|
1766
|
+
done
|
|
1767
|
+
if [ "${#remaining_pids[@]}" -gt 0 ]; then
|
|
1768
|
+
echo "teardown: force-killing leaked $label process(es) for $ID: ${remaining_pids[*]}" >&2
|
|
1769
|
+
if ! task_pids_under_roots "$@"; then
|
|
1770
|
+
echo "REFUSED: cannot determine leaked processes under ${TASK_PIDS_FAILED_DIR:-<missing>} for $ID (lsof failed); preserving the worktree/tasktmp for manual inspection or retry." >&2
|
|
1771
|
+
return 1
|
|
1772
|
+
fi
|
|
1773
|
+
current_pids=$TASK_PIDS
|
|
1774
|
+
for i in "${!remaining_pids[@]}"; do
|
|
1775
|
+
pid=${remaining_pids[$i]}
|
|
1776
|
+
identity=${remaining_identities[$i]}
|
|
1777
|
+
if task_pid_list_contains "$current_pids" "$pid" \
|
|
1778
|
+
&& task_process_identity_matches "$pid" "$identity"; then
|
|
1779
|
+
kill -KILL "$pid" 2>/dev/null || true
|
|
1780
|
+
fi
|
|
1781
|
+
done
|
|
1782
|
+
fi
|
|
1783
|
+
pass=$((pass + 1))
|
|
1784
|
+
done
|
|
1785
|
+
if ! task_pids_under_roots "$@"; then
|
|
1786
|
+
echo "REFUSED: cannot determine leaked processes under ${TASK_PIDS_FAILED_DIR:-<missing>} for $ID (lsof failed); preserving the worktree/tasktmp for manual inspection or retry." >&2
|
|
1787
|
+
return 1
|
|
1788
|
+
fi
|
|
1789
|
+
[ -z "$TASK_PIDS" ] && return 0
|
|
1790
|
+
echo "REFUSED: leaked $label processes for $ID remain after $max_passes reap attempts; preserving the worktree/tasktmp for manual inspection or retry." >&2
|
|
1791
|
+
return 1
|
|
1792
|
+
}
|
|
1793
|
+
|
|
1794
|
+
require_orca_worktree_path_match() {
|
|
1795
|
+
local worktree_id=$1 inspected=$2 resolved inspected_abs resolved_abs
|
|
1796
|
+
resolved=$(fm_backend_worktree_path orca "$worktree_id") || {
|
|
1797
|
+
echo "REFUSED: cannot resolve Orca worktree id $worktree_id to a path; preserving metadata." >&2
|
|
1798
|
+
return 1
|
|
1799
|
+
}
|
|
1800
|
+
inspected_abs=$(canonical_existing_dir "$inspected") || {
|
|
1801
|
+
echo "REFUSED: cannot canonicalize inspected worktree ${inspected:-<missing>}; preserving metadata." >&2
|
|
1802
|
+
return 1
|
|
1803
|
+
}
|
|
1804
|
+
resolved_abs=$(canonical_existing_dir "$resolved") || {
|
|
1805
|
+
echo "REFUSED: Orca worktree id $worktree_id resolved to uninspectable path ${resolved:-<missing>}; preserving metadata." >&2
|
|
1806
|
+
return 1
|
|
1807
|
+
}
|
|
1808
|
+
if [ "$resolved_abs" != "$inspected_abs" ]; then
|
|
1809
|
+
echo "REFUSED: Orca worktree id $worktree_id resolves to $resolved_abs, not inspected worktree $inspected_abs." >&2
|
|
1810
|
+
echo "Cannot verify dirty or unlanded work for the worktree Orca would remove; preserving metadata." >&2
|
|
1811
|
+
return 1
|
|
1812
|
+
fi
|
|
1813
|
+
}
|
|
1814
|
+
|
|
1815
|
+
require_orca_worktree_path_match_if_present() {
|
|
1816
|
+
local worktree_id=$1 inspected=$2
|
|
1817
|
+
[ -n "$inspected" ] && [ -e "$inspected" ] || return 0
|
|
1818
|
+
require_orca_worktree_path_match "$worktree_id" "$inspected"
|
|
1819
|
+
}
|
|
1820
|
+
|
|
1821
|
+
firstmate_home_has_treehouse_slot() {
|
|
1822
|
+
local home=$1
|
|
1823
|
+
worktree_registered_for_project "$FM_ROOT" "$home"
|
|
1824
|
+
}
|
|
1825
|
+
|
|
1826
|
+
validate_removal_target() {
|
|
1827
|
+
local target=$1 label=$2 abs_target abs_home abs_root
|
|
1828
|
+
[ -n "$target" ] || return 0
|
|
1829
|
+
[ -e "$target" ] || return 0
|
|
1830
|
+
abs_target=$(removal_target_abs_path "$target")
|
|
1831
|
+
if abs_home=$(cd "$FM_HOME" 2>/dev/null && pwd -P); then
|
|
1832
|
+
:
|
|
1833
|
+
else
|
|
1834
|
+
abs_home=
|
|
1835
|
+
fi
|
|
1836
|
+
abs_root=$(cd "$FM_ROOT" && pwd -P)
|
|
1837
|
+
case "$abs_target" in
|
|
1838
|
+
''|/) echo "REFUSED: unsafe $label removal target $target" >&2; return 1 ;;
|
|
1839
|
+
esac
|
|
1840
|
+
if [ -n "$abs_home" ] && [ "$abs_target" = "$abs_home" ]; then
|
|
1841
|
+
echo "REFUSED: unsafe $label removal target $target is the active firstmate home" >&2
|
|
1842
|
+
return 1
|
|
1843
|
+
fi
|
|
1844
|
+
if [ "$abs_target" = "$abs_root" ]; then
|
|
1845
|
+
echo "REFUSED: unsafe $label removal target $target is the firstmate repo" >&2
|
|
1846
|
+
return 1
|
|
1847
|
+
fi
|
|
1848
|
+
if [ -n "$abs_home" ] && path_is_ancestor_of "$abs_target" "$abs_home"; then
|
|
1849
|
+
echo "REFUSED: unsafe $label removal target $target is an ancestor of the active firstmate home" >&2
|
|
1850
|
+
return 1
|
|
1851
|
+
fi
|
|
1852
|
+
if path_is_ancestor_of "$abs_target" "$abs_root"; then
|
|
1853
|
+
echo "REFUSED: unsafe $label removal target $target is an ancestor of the firstmate repo" >&2
|
|
1854
|
+
return 1
|
|
1855
|
+
fi
|
|
1856
|
+
if [ -n "$abs_home" ] && path_is_ancestor_of "$abs_home" "$abs_target"; then
|
|
1857
|
+
echo "REFUSED: unsafe $label removal target $target is inside the active firstmate home" >&2
|
|
1858
|
+
return 1
|
|
1859
|
+
fi
|
|
1860
|
+
if path_is_ancestor_of "$abs_root" "$abs_target"; then
|
|
1861
|
+
echo "REFUSED: unsafe $label removal target $target is inside the firstmate repo" >&2
|
|
1862
|
+
return 1
|
|
1863
|
+
fi
|
|
1864
|
+
printf '%s\n' "$abs_target"
|
|
1865
|
+
}
|
|
1866
|
+
|
|
1867
|
+
registered_descendant_home_for_removal() {
|
|
1868
|
+
local reg=$1 target=$2 line id registered_home registered_abs
|
|
1869
|
+
[ -f "$reg" ] || return 1
|
|
1870
|
+
if ! secondmate_registry_validate_bindings "$reg" secondmate_registry_path_key; then
|
|
1871
|
+
echo "REFUSED: $SECONDMATE_REGISTRY_ERROR" >&2
|
|
1872
|
+
return 2
|
|
1873
|
+
fi
|
|
1874
|
+
while IFS= read -r line || [ -n "$line" ]; do
|
|
1875
|
+
case "$line" in
|
|
1876
|
+
"- "*)
|
|
1877
|
+
secondmate_registry_parse_line "$line" || {
|
|
1878
|
+
echo "REFUSED: malformed secondmate registry entry: $line" >&2
|
|
1879
|
+
return 2
|
|
1880
|
+
}
|
|
1881
|
+
id=$SECONDMATE_REGISTRY_ID
|
|
1882
|
+
registered_home=$SECONDMATE_REGISTRY_HOME
|
|
1883
|
+
registered_abs=$(removal_target_abs_path "$registered_home" 2>/dev/null || true)
|
|
1884
|
+
[ -n "$registered_abs" ] || continue
|
|
1885
|
+
[ "$registered_abs" = "$target" ] && continue
|
|
1886
|
+
if path_is_ancestor_of "$target" "$registered_abs"; then
|
|
1887
|
+
printf '%s\t%s\n' "$id" "$registered_abs"
|
|
1888
|
+
return 0
|
|
1889
|
+
fi
|
|
1890
|
+
;;
|
|
1891
|
+
esac
|
|
1892
|
+
done < "$reg"
|
|
1893
|
+
return 1
|
|
1894
|
+
}
|
|
1895
|
+
|
|
1896
|
+
validate_firstmate_operational_dirs_for_removal() {
|
|
1897
|
+
local home=$1 label=$2 name dir abs_home abs_dir
|
|
1898
|
+
abs_home=$(removal_target_abs_path "$home")
|
|
1899
|
+
for name in data state config projects; do
|
|
1900
|
+
dir="$home/$name"
|
|
1901
|
+
[ -e "$dir" ] || [ -L "$dir" ] || continue
|
|
1902
|
+
if [ -L "$dir" ] && [ ! -e "$dir" ]; then
|
|
1903
|
+
echo "REFUSED: unsafe $label $name directory $dir resolves outside the secondmate home" >&2
|
|
1904
|
+
return 1
|
|
1905
|
+
fi
|
|
1906
|
+
if [ -d "$dir" ]; then
|
|
1907
|
+
abs_dir=$(cd "$dir" && pwd -P)
|
|
1908
|
+
elif [ -e "$dir" ]; then
|
|
1909
|
+
echo "REFUSED: unsafe $label $name path $dir is not a directory" >&2
|
|
1910
|
+
return 1
|
|
1911
|
+
else
|
|
1912
|
+
abs_dir=
|
|
1913
|
+
fi
|
|
1914
|
+
if [ -z "$abs_dir" ] || ! path_is_ancestor_of "$abs_home" "$abs_dir"; then
|
|
1915
|
+
echo "REFUSED: unsafe $label $name directory $dir resolves outside the secondmate home" >&2
|
|
1916
|
+
return 1
|
|
1917
|
+
fi
|
|
1918
|
+
done
|
|
1919
|
+
}
|
|
1920
|
+
|
|
1921
|
+
validate_child_worktree_for_removal() {
|
|
1922
|
+
local target=$1 project=$2 abs_target abs_home abs_root
|
|
1923
|
+
[ -n "$target" ] || return 0
|
|
1924
|
+
[ -e "$target" ] || return 0
|
|
1925
|
+
abs_target=$(validate_removal_target "$target" "child worktree") || return 1
|
|
1926
|
+
if abs_home=$(cd "$FM_HOME" 2>/dev/null && pwd -P); then
|
|
1927
|
+
if path_is_ancestor_of "$abs_home" "$abs_target"; then
|
|
1928
|
+
echo "REFUSED: unsafe child worktree removal target $target is inside the active firstmate home" >&2
|
|
1929
|
+
return 1
|
|
1930
|
+
fi
|
|
1931
|
+
fi
|
|
1932
|
+
abs_root=$(cd "$FM_ROOT" && pwd -P)
|
|
1933
|
+
if path_is_ancestor_of "$abs_root" "$abs_target"; then
|
|
1934
|
+
echo "REFUSED: unsafe child worktree removal target $target is inside the firstmate repo" >&2
|
|
1935
|
+
return 1
|
|
1936
|
+
fi
|
|
1937
|
+
if ! worktree_registered_for_project "$project" "$target"; then
|
|
1938
|
+
echo "REFUSED: unsafe child worktree removal target $target is not a git worktree for ${project:-the recorded project}" >&2
|
|
1939
|
+
return 1
|
|
1940
|
+
fi
|
|
1941
|
+
printf '%s\n' "$abs_target"
|
|
1942
|
+
}
|
|
1943
|
+
|
|
1944
|
+
safe_rm_rf() {
|
|
1945
|
+
local target=$1 label=$2
|
|
1946
|
+
validate_removal_target "$target" "$label" >/dev/null || return 1
|
|
1947
|
+
rm -rf -- "$target"
|
|
1948
|
+
}
|
|
1949
|
+
|
|
1950
|
+
safe_rm_rf_child_worktree() {
|
|
1951
|
+
local target=$1 project=$2
|
|
1952
|
+
validate_child_worktree_for_removal "$target" "$project" >/dev/null || return 1
|
|
1953
|
+
rm -rf -- "$target"
|
|
1954
|
+
}
|
|
1955
|
+
|
|
1956
|
+
validate_firstmate_home_for_removal() {
|
|
1957
|
+
local home=$1 label=$2 expected_id=${3:-} abs_home_path marker_id conflict child_id child_home
|
|
1958
|
+
[ -n "$home" ] || return 0
|
|
1959
|
+
[ -e "$home" ] || return 0
|
|
1960
|
+
abs_home_path=$(validate_removal_target "$home" "$label") || return 1
|
|
1961
|
+
if [ ! -f "$abs_home_path/$SUB_HOME_MARKER" ]; then
|
|
1962
|
+
echo "REFUSED: unsafe $label removal target $home is not a seeded secondmate home" >&2
|
|
1963
|
+
return 1
|
|
1964
|
+
fi
|
|
1965
|
+
if [ -n "$expected_id" ]; then
|
|
1966
|
+
marker_id=$(cat "$abs_home_path/$SUB_HOME_MARKER" 2>/dev/null || true)
|
|
1967
|
+
if [ "$marker_id" != "$expected_id" ]; then
|
|
1968
|
+
echo "REFUSED: unsafe $label removal target $home is marked for secondmate ${marker_id:-unknown}, expected $expected_id" >&2
|
|
1969
|
+
return 1
|
|
1970
|
+
fi
|
|
1971
|
+
if [ -e "$SECONDMATE_REG" ] || [ -L "$SECONDMATE_REG" ]; then
|
|
1972
|
+
if ! secondmate_registry_validate_bindings "$SECONDMATE_REG" secondmate_registry_path_key "$expected_id" "$abs_home_path"; then
|
|
1973
|
+
case "$SECONDMATE_REGISTRY_ERROR" in
|
|
1974
|
+
overlapping\ secondmate\ home\ assignment:*)
|
|
1975
|
+
echo "REFUSED: unsafe $label removal target $home contains registered secondmate home; $SECONDMATE_REGISTRY_ERROR" >&2
|
|
1976
|
+
;;
|
|
1977
|
+
*) echo "REFUSED: $SECONDMATE_REGISTRY_ERROR" >&2 ;;
|
|
1978
|
+
esac
|
|
1979
|
+
return 1
|
|
1980
|
+
fi
|
|
1981
|
+
fi
|
|
1982
|
+
fi
|
|
1983
|
+
validate_firstmate_operational_dirs_for_removal "$abs_home_path" "$label" || return 1
|
|
1984
|
+
conflict=
|
|
1985
|
+
if conflict=$(registered_descendant_home_for_removal "$SECONDMATE_REG" "$abs_home_path"); then
|
|
1986
|
+
:
|
|
1987
|
+
else
|
|
1988
|
+
conflict_rc=$?
|
|
1989
|
+
[ "$conflict_rc" -eq 1 ] || return 1
|
|
1990
|
+
fi
|
|
1991
|
+
if [ -z "$conflict" ]; then
|
|
1992
|
+
if conflict=$(registered_descendant_home_for_removal "$abs_home_path/data/secondmates.md" "$abs_home_path"); then
|
|
1993
|
+
:
|
|
1994
|
+
else
|
|
1995
|
+
conflict_rc=$?
|
|
1996
|
+
[ "$conflict_rc" -eq 1 ] || return 1
|
|
1997
|
+
fi
|
|
1998
|
+
fi
|
|
1999
|
+
if [ -n "$conflict" ]; then
|
|
2000
|
+
IFS=$'\t' read -r child_id child_home <<EOF
|
|
2001
|
+
$conflict
|
|
2002
|
+
EOF
|
|
2003
|
+
echo "REFUSED: unsafe $label removal target $home contains registered secondmate home $child_home for $child_id" >&2
|
|
2004
|
+
return 1
|
|
2005
|
+
fi
|
|
2006
|
+
printf '%s\n' "$abs_home_path"
|
|
2007
|
+
}
|
|
2008
|
+
|
|
2009
|
+
remove_firstmate_home() {
|
|
2010
|
+
local home=$1 label=$2 expected_id=${3:-} abs_home_path process_event_backup
|
|
2011
|
+
[ -n "$home" ] || return 0
|
|
2012
|
+
[ -e "$home" ] || return 0
|
|
2013
|
+
abs_home_path=$(validate_firstmate_home_for_removal "$home" "$label" "$expected_id") || return 1
|
|
2014
|
+
[ -n "$abs_home_path" ] || return 0
|
|
2015
|
+
process_event_backup=$(snapshot_firstmate_home_process_events "$abs_home_path" "$label") || return 1
|
|
2016
|
+
if ! cleanup_firstmate_home_process_events "$abs_home_path" "$label"; then
|
|
2017
|
+
restore_firstmate_home_process_events "$abs_home_path" "$label" "$process_event_backup" || return $?
|
|
2018
|
+
return 1
|
|
2019
|
+
fi
|
|
2020
|
+
if firstmate_home_has_treehouse_slot "$abs_home_path"; then
|
|
2021
|
+
command -v treehouse >/dev/null 2>&1 || {
|
|
2022
|
+
echo "error: treehouse command not found; cannot return $label $abs_home_path" >&2
|
|
2023
|
+
restore_firstmate_home_process_events "$abs_home_path" "$label" "$process_event_backup" || return $?
|
|
2024
|
+
return 1
|
|
2025
|
+
}
|
|
2026
|
+
teardown_treehouse_return "$abs_home_path" "$FM_ROOT" "$label" || {
|
|
2027
|
+
echo "error: treehouse return failed for $label $abs_home_path; lease may still be held" >&2
|
|
2028
|
+
restore_firstmate_home_process_events "$abs_home_path" "$label" "$process_event_backup" || return $?
|
|
2029
|
+
return 1
|
|
2030
|
+
}
|
|
2031
|
+
[ -z "$process_event_backup" ] || rm -rf -- "$process_event_backup"
|
|
2032
|
+
return 0
|
|
2033
|
+
fi
|
|
2034
|
+
if safe_rm_rf "$abs_home_path" "$label"; then
|
|
2035
|
+
[ -z "$process_event_backup" ] || rm -rf -- "$process_event_backup"
|
|
2036
|
+
return 0
|
|
2037
|
+
fi
|
|
2038
|
+
restore_firstmate_home_process_events "$abs_home_path" "$label" "$process_event_backup" || return $?
|
|
2039
|
+
return 1
|
|
2040
|
+
}
|
|
2041
|
+
|
|
2042
|
+
firstmate_home_has_process_events() {
|
|
2043
|
+
local home=$1 path owner claim_root
|
|
2044
|
+
for path in "$home/state/procevent"/*.source "$home/state/procevent"/*.runner; do
|
|
2045
|
+
if [ -e "$path" ] || [ -L "$path" ]; then
|
|
2046
|
+
return 0
|
|
2047
|
+
fi
|
|
2048
|
+
done
|
|
2049
|
+
claim_root=${FM_PROCEVENT_CLAIM_ROOT:-${XDG_STATE_HOME:-$HOME/.local/state}/firstmate/procevent-claims}
|
|
2050
|
+
for path in "$claim_root"/*.claim; do
|
|
2051
|
+
[ -f "$path" ] && [ ! -L "$path" ] || continue
|
|
2052
|
+
IFS= read -r owner < "$path" 2>/dev/null || continue
|
|
2053
|
+
[ "$owner" = "$home" ] && return 0
|
|
2054
|
+
done
|
|
2055
|
+
return 1
|
|
2056
|
+
}
|
|
2057
|
+
|
|
2058
|
+
snapshot_firstmate_home_process_events() {
|
|
2059
|
+
local home=$1 label=$2 backup path
|
|
2060
|
+
if ! firstmate_home_has_process_events "$home"; then
|
|
2061
|
+
printf '\n'
|
|
2062
|
+
return 0
|
|
2063
|
+
fi
|
|
2064
|
+
backup=$(umask 077; mktemp -d "${home%/*}/.fm-procevent-restore.XXXXXX") || {
|
|
2065
|
+
echo "REFUSED: cannot stage recoverable process-event state for $label $home" >&2
|
|
2066
|
+
return 1
|
|
2067
|
+
}
|
|
2068
|
+
for path in "$home/state/procevent"/*.source; do
|
|
2069
|
+
[ -e "$path" ] || continue
|
|
2070
|
+
if [ ! -f "$path" ] || [ -L "$path" ] || ! cp -p -- "$path" "$backup/"; then
|
|
2071
|
+
rm -rf -- "$backup"
|
|
2072
|
+
echo "REFUSED: cannot preserve process-event registrations for $label $home" >&2
|
|
2073
|
+
return 1
|
|
2074
|
+
fi
|
|
2075
|
+
done
|
|
2076
|
+
printf '%s\n' "$backup"
|
|
2077
|
+
}
|
|
2078
|
+
|
|
2079
|
+
restore_firstmate_home_process_events() {
|
|
2080
|
+
local home=$1 label=$2 backup=$3 reg source tmp runner
|
|
2081
|
+
[ -n "$backup" ] || return 0
|
|
2082
|
+
[ -d "$backup" ] && [ ! -L "$backup" ] || {
|
|
2083
|
+
echo "error: process-event restoration failed for $label $home; recovery backup is unavailable at $backup" >&2
|
|
2084
|
+
return "$TEARDOWN_PROCEVENT_RESTORE_FAILED"
|
|
2085
|
+
}
|
|
2086
|
+
reg="$home/state/procevent"
|
|
2087
|
+
(umask 077; mkdir -p "$reg") || {
|
|
2088
|
+
echo "error: process-event restoration failed for $label $home; recover registrations from $backup" >&2
|
|
2089
|
+
return "$TEARDOWN_PROCEVENT_RESTORE_FAILED"
|
|
2090
|
+
}
|
|
2091
|
+
[ -d "$reg" ] && [ ! -L "$reg" ] || {
|
|
2092
|
+
echo "error: process-event restoration failed for $label $home; recover registrations from $backup" >&2
|
|
2093
|
+
return "$TEARDOWN_PROCEVENT_RESTORE_FAILED"
|
|
2094
|
+
}
|
|
2095
|
+
for source in "$backup"/*.source; do
|
|
2096
|
+
[ -e "$source" ] || continue
|
|
2097
|
+
[ -f "$source" ] && [ ! -L "$source" ] || {
|
|
2098
|
+
echo "error: process-event restoration failed for $label $home; recover registrations from $backup" >&2
|
|
2099
|
+
return "$TEARDOWN_PROCEVENT_RESTORE_FAILED"
|
|
2100
|
+
}
|
|
2101
|
+
tmp=$(umask 077; mktemp "$reg/.restore.XXXXXX") || {
|
|
2102
|
+
echo "error: process-event restoration failed for $label $home; recover registrations from $backup" >&2
|
|
2103
|
+
return "$TEARDOWN_PROCEVENT_RESTORE_FAILED"
|
|
2104
|
+
}
|
|
2105
|
+
if ! cp -- "$source" "$tmp" || ! chmod 0600 "$tmp" || ! mv -f -- "$tmp" "$reg/${source##*/}"; then
|
|
2106
|
+
rm -f -- "$tmp"
|
|
2107
|
+
echo "error: process-event restoration failed for $label $home; recover registrations from $backup" >&2
|
|
2108
|
+
return "$TEARDOWN_PROCEVENT_RESTORE_FAILED"
|
|
2109
|
+
fi
|
|
2110
|
+
done
|
|
2111
|
+
runner="$home/bin/fm-procevent.sh"
|
|
2112
|
+
if [ ! -f "$runner" ] || [ -L "$runner" ] || [ ! -x "$runner" ]; then
|
|
2113
|
+
runner="$SCRIPT_DIR/fm-procevent.sh"
|
|
2114
|
+
fi
|
|
2115
|
+
if ! FM_HOME="$home" FM_ROOT_OVERRIDE="$FM_ROOT" "$runner" reconcile >/dev/null; then
|
|
2116
|
+
echo "error: process-event restoration could not rearm $label $home; active waits may remain retired; recover registrations from $backup" >&2
|
|
2117
|
+
return "$TEARDOWN_PROCEVENT_RESTORE_FAILED"
|
|
2118
|
+
fi
|
|
2119
|
+
rm -rf -- "$backup"
|
|
2120
|
+
}
|
|
2121
|
+
|
|
2122
|
+
cleanup_firstmate_home_process_events() {
|
|
2123
|
+
local home=$1 label=$2 runner="$1/bin/fm-procevent.sh"
|
|
2124
|
+
firstmate_home_has_process_events "$home" || return 0
|
|
2125
|
+
if [ ! -f "$runner" ] || [ -L "$runner" ] || [ ! -x "$runner" ]; then
|
|
2126
|
+
echo "REFUSED: $label $home has process-event state but no sweep-capable bin/fm-procevent.sh; restore the home script and rerun teardown" >&2
|
|
2127
|
+
return 1
|
|
2128
|
+
fi
|
|
2129
|
+
if ! FM_HOME="$home" FM_ROOT_OVERRIDE="$home" "$runner" sweep-home; then
|
|
2130
|
+
echo "REFUSED: process-event cleanup is incomplete for $label $home; preserving the home, lease, and retirement records for retry" >&2
|
|
2131
|
+
return 1
|
|
2132
|
+
fi
|
|
2133
|
+
if firstmate_home_has_process_events "$home"; then
|
|
2134
|
+
echo "REFUSED: process-event state remains for $label $home after its bounded sweep; preserving the home, lease, and retirement records for retry" >&2
|
|
2135
|
+
return 1
|
|
2136
|
+
fi
|
|
2137
|
+
}
|
|
2138
|
+
|
|
2139
|
+
preflight_firstmate_home_process_events() {
|
|
2140
|
+
local home=$1 label=$2 runner="$1/bin/fm-procevent.sh"
|
|
2141
|
+
firstmate_home_has_process_events "$home" || return 0
|
|
2142
|
+
if [ ! -f "$runner" ] || [ -L "$runner" ] || [ ! -x "$runner" ]; then
|
|
2143
|
+
echo "REFUSED: $label $home has process-event state but no sweep-capable bin/fm-procevent.sh; restore the home script and rerun teardown" >&2
|
|
2144
|
+
return 1
|
|
2145
|
+
fi
|
|
2146
|
+
if ! FM_HOME="$home" FM_ROOT_OVERRIDE="$home" "$runner" sweep-home --preflight >/dev/null; then
|
|
2147
|
+
echo "REFUSED: process-event cleanup cannot safely proceed for $label $home; preserving the home, lease, and retirement records for retry" >&2
|
|
2148
|
+
return 1
|
|
2149
|
+
fi
|
|
2150
|
+
}
|
|
2151
|
+
|
|
2152
|
+
preflight_firstmate_home_process_event_tree() {
|
|
2153
|
+
local home=$1 label=$2 sub_state child_meta child_kind child_home child_wt child_id
|
|
2154
|
+
sub_state="$home/state"
|
|
2155
|
+
if [ -d "$sub_state" ]; then
|
|
2156
|
+
for child_meta in "$sub_state"/*.meta; do
|
|
2157
|
+
[ -e "$child_meta" ] || continue
|
|
2158
|
+
child_kind=$(meta_value "$child_meta" kind)
|
|
2159
|
+
[ "$child_kind" = secondmate ] || continue
|
|
2160
|
+
child_id=$(basename "$child_meta" .meta)
|
|
2161
|
+
child_wt=$(meta_value "$child_meta" worktree)
|
|
2162
|
+
child_home=$(meta_value "$child_meta" home)
|
|
2163
|
+
[ -n "$child_home" ] || child_home=$child_wt
|
|
2164
|
+
preflight_firstmate_home_process_event_tree "$child_home" "child firstmate home for $child_id" || return 1
|
|
2165
|
+
done
|
|
2166
|
+
fi
|
|
2167
|
+
preflight_firstmate_home_process_events "$home" "$label"
|
|
2168
|
+
}
|
|
2169
|
+
|
|
2170
|
+
collect_descendant_task_locks() {
|
|
2171
|
+
local home=$1 sub_state child_meta child_id child_kind child_wt child_home task_set_lock
|
|
2172
|
+
local -a child_ids
|
|
2173
|
+
sub_state="$home/state"
|
|
2174
|
+
if [ -L "$sub_state" ]; then
|
|
2175
|
+
echo "REFUSED: secondmate home $home has a symbolic-link state path at $sub_state; forced teardown changed nothing" >&2
|
|
2176
|
+
return 1
|
|
2177
|
+
fi
|
|
2178
|
+
if [ -e "$sub_state" ] && [ ! -d "$sub_state" ]; then
|
|
2179
|
+
echo "REFUSED: secondmate home $home has a non-directory state path at $sub_state; forced teardown changed nothing" >&2
|
|
2180
|
+
return 1
|
|
2181
|
+
fi
|
|
2182
|
+
if ! mkdir -p -- "$sub_state"; then
|
|
2183
|
+
echo "REFUSED: secondmate home $home state directory could not be established at $sub_state; forced teardown changed nothing" >&2
|
|
2184
|
+
return 1
|
|
2185
|
+
fi
|
|
2186
|
+
if [ -L "$sub_state" ] || [ ! -d "$sub_state" ]; then
|
|
2187
|
+
echo "REFUSED: secondmate home $home state path is not a safe directory at $sub_state; forced teardown changed nothing" >&2
|
|
2188
|
+
return 1
|
|
2189
|
+
fi
|
|
2190
|
+
# Freeze this home's task SET before reading it. Everything below locks the
|
|
2191
|
+
# tasks that exist right now, but the later cleanup re-enumerates, so without
|
|
2192
|
+
# this a fresh spawn could publish a record into the gap and be mutated
|
|
2193
|
+
# without ever having been lifecycle-locked (bin/fm-wake-lib.sh's
|
|
2194
|
+
# fm_task_set_lock_path owns why). Taken per home, parent before child, and
|
|
2195
|
+
# held until this teardown exits.
|
|
2196
|
+
task_set_lock=$(fm_task_set_lock_path "$sub_state") || {
|
|
2197
|
+
echo "REFUSED: secondmate home $home has an invalid task-set lock path; forced teardown changed nothing" >&2
|
|
2198
|
+
return 1
|
|
2199
|
+
}
|
|
2200
|
+
if ! fm_lock_try_acquire "$task_set_lock"; then
|
|
2201
|
+
echo "REFUSED: secondmate home $home is publishing a task right now (task-set lock is held); forced teardown changed nothing" >&2
|
|
2202
|
+
return 1
|
|
2203
|
+
fi
|
|
2204
|
+
DESCENDANT_LOCK_PATHS+=("$task_set_lock")
|
|
2205
|
+
child_ids=()
|
|
2206
|
+
for child_meta in "$sub_state"/*.meta; do
|
|
2207
|
+
[ -e "$child_meta" ] || continue
|
|
2208
|
+
child_ids+=("$(basename "$child_meta" .meta)")
|
|
2209
|
+
done
|
|
2210
|
+
[ "${#child_ids[@]}" -gt 0 ] || return 0
|
|
2211
|
+
while IFS= read -r child_id; do
|
|
2212
|
+
child_meta="$sub_state/$child_id.meta"
|
|
2213
|
+
child_kind=$(meta_value "$child_meta" kind)
|
|
2214
|
+
[ -n "$child_kind" ] || child_kind=ship
|
|
2215
|
+
child_home=
|
|
2216
|
+
if [ "$child_kind" = secondmate ]; then
|
|
2217
|
+
child_wt=$(meta_value "$child_meta" worktree)
|
|
2218
|
+
child_home=$(meta_value "$child_meta" home)
|
|
2219
|
+
[ -n "$child_home" ] || child_home=$child_wt
|
|
2220
|
+
fi
|
|
2221
|
+
DESCENDANT_TASK_STATES+=("$sub_state")
|
|
2222
|
+
DESCENDANT_TASK_IDS+=("$child_id")
|
|
2223
|
+
DESCENDANT_TASK_KINDS+=("$child_kind")
|
|
2224
|
+
DESCENDANT_TASK_HOMES+=("$child_home")
|
|
2225
|
+
[ "$child_kind" != secondmate ] \
|
|
2226
|
+
|| collect_descendant_task_locks "$child_home" \
|
|
2227
|
+
|| return 1
|
|
2228
|
+
done < <(printf '%s\n' "${child_ids[@]}" | LC_ALL=C sort)
|
|
2229
|
+
}
|
|
2230
|
+
|
|
2231
|
+
preflight_descendant_task_locks() {
|
|
2232
|
+
local home=$1 i state task_id meta control_lock meta_lock kind child_wt child_home
|
|
2233
|
+
DESCENDANT_TASK_STATES=()
|
|
2234
|
+
DESCENDANT_TASK_IDS=()
|
|
2235
|
+
DESCENDANT_TASK_KINDS=()
|
|
2236
|
+
DESCENDANT_TASK_HOMES=()
|
|
2237
|
+
collect_descendant_task_locks "$home" || return 1
|
|
2238
|
+
# Acquisition order, which every other holder of these locks must match so
|
|
2239
|
+
# they cannot cycle: each home's task-set lock first (parent home before child
|
|
2240
|
+
# home, during collection above), then per-task locks in that same
|
|
2241
|
+
# parent-before-child preorder, sorted by id within each home, each control
|
|
2242
|
+
# lock before its matching metadata lock. No child lock holder ever reaches
|
|
2243
|
+
# back for a parent lock. bin/fm-spawn.sh takes the same task-set lock before
|
|
2244
|
+
# its own per-task locks when it publishes a fresh record.
|
|
2245
|
+
for ((i=0; i < ${#DESCENDANT_TASK_IDS[@]}; i++)); do
|
|
2246
|
+
state=${DESCENDANT_TASK_STATES[$i]}
|
|
2247
|
+
task_id=${DESCENDANT_TASK_IDS[$i]}
|
|
2248
|
+
meta="$state/$task_id.meta"
|
|
2249
|
+
control_lock="$state/.control-$task_id.lock"
|
|
2250
|
+
meta_lock=$(fm_meta_lock_path "$meta") || {
|
|
2251
|
+
echo "REFUSED: descendant task $task_id has an invalid metadata lock path; forced teardown changed nothing" >&2
|
|
2252
|
+
return 1
|
|
2253
|
+
}
|
|
2254
|
+
if ! fm_lock_try_acquire "$control_lock"; then
|
|
2255
|
+
echo "REFUSED: descendant task $task_id has a lifecycle action in flight (control lock is held); forced teardown changed nothing" >&2
|
|
2256
|
+
return 1
|
|
2257
|
+
fi
|
|
2258
|
+
DESCENDANT_LOCK_PATHS+=("$control_lock")
|
|
2259
|
+
if ! fm_lock_try_acquire "$meta_lock"; then
|
|
2260
|
+
echo "REFUSED: descendant task $task_id has a metadata update in flight (metadata lock is held); forced teardown changed nothing" >&2
|
|
2261
|
+
return 1
|
|
2262
|
+
fi
|
|
2263
|
+
DESCENDANT_LOCK_PATHS+=("$meta_lock")
|
|
2264
|
+
[ -f "$meta" ] || {
|
|
2265
|
+
echo "REFUSED: descendant task $task_id changed while forced teardown acquired its locks; forced teardown changed nothing" >&2
|
|
2266
|
+
return 1
|
|
2267
|
+
}
|
|
2268
|
+
kind=$(meta_value "$meta" kind)
|
|
2269
|
+
[ -n "$kind" ] || kind=ship
|
|
2270
|
+
[ "$kind" = "${DESCENDANT_TASK_KINDS[$i]}" ] || {
|
|
2271
|
+
echo "REFUSED: descendant task $task_id changed kind while forced teardown acquired its locks; forced teardown changed nothing" >&2
|
|
2272
|
+
return 1
|
|
2273
|
+
}
|
|
2274
|
+
if [ "$kind" = secondmate ]; then
|
|
2275
|
+
child_wt=$(meta_value "$meta" worktree)
|
|
2276
|
+
child_home=$(meta_value "$meta" home)
|
|
2277
|
+
[ -n "$child_home" ] || child_home=$child_wt
|
|
2278
|
+
[ "$child_home" = "${DESCENDANT_TASK_HOMES[$i]}" ] || {
|
|
2279
|
+
echo "REFUSED: descendant task $task_id changed home while forced teardown acquired its locks; forced teardown changed nothing" >&2
|
|
2280
|
+
return 1
|
|
2281
|
+
}
|
|
2282
|
+
fi
|
|
2283
|
+
done
|
|
2284
|
+
}
|
|
2285
|
+
|
|
2286
|
+
validate_firstmate_home_children_removal() {
|
|
2287
|
+
local home=$1 sub_state child_meta child_id child_wt child_proj child_kind child_home child_backend child_orca_worktree_id
|
|
2288
|
+
sub_state="$home/state"
|
|
2289
|
+
[ -d "$sub_state" ] || return 0
|
|
2290
|
+
for child_meta in "$sub_state"/*.meta; do
|
|
2291
|
+
[ -e "$child_meta" ] || continue
|
|
2292
|
+
child_id=$(basename "$child_meta" .meta)
|
|
2293
|
+
fm_backend_validate_task_endpoint "$child_meta" "$child_id" || return 1
|
|
2294
|
+
validate_pr_poll_cleanup "$sub_state" "$child_id" || return 1
|
|
2295
|
+
child_wt=$(meta_value "$child_meta" worktree)
|
|
2296
|
+
child_kind=$(meta_value "$child_meta" kind)
|
|
2297
|
+
[ -n "$child_kind" ] || child_kind=ship
|
|
2298
|
+
child_backend=$(fm_backend_of_meta "$child_meta")
|
|
2299
|
+
if [ "$child_kind" = secondmate ]; then
|
|
2300
|
+
child_home=$(meta_value "$child_meta" home)
|
|
2301
|
+
[ -n "$child_home" ] || child_home=$child_wt
|
|
2302
|
+
validate_firstmate_home_for_removal "$child_home" "child firstmate home" "$child_id" >/dev/null || return 1
|
|
2303
|
+
validate_firstmate_home_children_removal "$child_home" || return 1
|
|
2304
|
+
elif [ "$child_backend" = orca ]; then
|
|
2305
|
+
child_orca_worktree_id=$(require_orca_worktree_id "$child_meta") || return 1
|
|
2306
|
+
if [ -n "$child_wt" ] && [ -e "$child_wt" ]; then
|
|
2307
|
+
child_proj=$(meta_value "$child_meta" project)
|
|
2308
|
+
validate_child_worktree_for_removal "$child_wt" "$child_proj" >/dev/null || return 1
|
|
2309
|
+
require_orca_worktree_path_match "$child_orca_worktree_id" "$child_wt" || return 1
|
|
2310
|
+
fi
|
|
2311
|
+
elif [ -n "$child_wt" ] && [ -e "$child_wt" ]; then
|
|
2312
|
+
child_proj=$(meta_value "$child_meta" project)
|
|
2313
|
+
validate_child_worktree_for_removal "$child_wt" "$child_proj" >/dev/null || return 1
|
|
2314
|
+
fi
|
|
2315
|
+
done
|
|
2316
|
+
}
|
|
2317
|
+
|
|
2318
|
+
TEARDOWN_HERDR_LOCK_RECORDS=
|
|
2319
|
+
teardown_release_herdr_locks() {
|
|
2320
|
+
local lock_session lock_path
|
|
2321
|
+
[ -n "$TEARDOWN_HERDR_LOCK_RECORDS" ] || return 0
|
|
2322
|
+
while IFS=$'\t' read -r lock_session lock_path; do
|
|
2323
|
+
[ -n "$lock_path" ] || continue
|
|
2324
|
+
fm_lock_release "$lock_path" || true
|
|
2325
|
+
done <<FMEOF
|
|
2326
|
+
$TEARDOWN_HERDR_LOCK_RECORDS
|
|
2327
|
+
FMEOF
|
|
2328
|
+
TEARDOWN_HERDR_LOCK_RECORDS=
|
|
2329
|
+
}
|
|
2330
|
+
|
|
2331
|
+
teardown_herdr_session_lock_held() { # <session>
|
|
2332
|
+
local session=$1 lock_session lock_path
|
|
2333
|
+
[ -n "$TEARDOWN_HERDR_LOCK_RECORDS" ] || return 1
|
|
2334
|
+
while IFS=$'\t' read -r lock_session lock_path; do
|
|
2335
|
+
[ "$lock_session" != "$session" ] || return 0
|
|
2336
|
+
done <<FMEOF
|
|
2337
|
+
$TEARDOWN_HERDR_LOCK_RECORDS
|
|
2338
|
+
FMEOF
|
|
2339
|
+
return 1
|
|
2340
|
+
}
|
|
2341
|
+
|
|
2342
|
+
teardown_herdr_require_prerequisites() { # <task-id>
|
|
2343
|
+
local task_id=$1 prerequisite
|
|
2344
|
+
if ! fm_backend_source herdr; then
|
|
2345
|
+
echo "error: herdr teardown prerequisites are unavailable for $task_id; nothing was changed - restore the adapter and rerun teardown" >&2
|
|
2346
|
+
return 1
|
|
2347
|
+
fi
|
|
2348
|
+
for prerequisite in \
|
|
2349
|
+
fm_backend_herdr_parse_target \
|
|
2350
|
+
fm_backend_herdr_pane_presence_state \
|
|
2351
|
+
fm_backend_herdr_workspace_presence_state \
|
|
2352
|
+
fm_backend_herdr_endpoint_confirmed_gone \
|
|
2353
|
+
fm_backend_herdr_explicit_close_pane_confirmed \
|
|
2354
|
+
fm_backend_herdr_presentation_session_lock_path; do
|
|
2355
|
+
if ! declare -F "$prerequisite" >/dev/null 2>&1; then
|
|
2356
|
+
echo "error: herdr teardown prerequisites are unavailable for $task_id; nothing was changed - restore the adapter and rerun teardown" >&2
|
|
2357
|
+
return 1
|
|
2358
|
+
fi
|
|
2359
|
+
done
|
|
2360
|
+
if ! declare -F fm_lock_try_acquire >/dev/null 2>&1; then
|
|
2361
|
+
# shellcheck source=bin/fm-wake-lib.sh
|
|
2362
|
+
. "$SCRIPT_DIR/fm-wake-lib.sh"
|
|
2363
|
+
fi
|
|
2364
|
+
if ! declare -F fm_lock_try_acquire >/dev/null 2>&1 \
|
|
2365
|
+
|| ! declare -F fm_lock_release >/dev/null 2>&1; then
|
|
2366
|
+
echo "error: herdr teardown lock machinery is unavailable for $task_id; nothing was changed - restore the lock support and rerun teardown" >&2
|
|
2367
|
+
return 1
|
|
2368
|
+
fi
|
|
2369
|
+
}
|
|
2370
|
+
|
|
2371
|
+
teardown_herdr_preflight_target() { # <target> <task-id>
|
|
2372
|
+
local target=$1 task_id=$2 session pane presence lock_path verified_lock_path lock_session held_path attempt
|
|
2373
|
+
teardown_herdr_require_prerequisites "$task_id" || return 1
|
|
2374
|
+
if ! fm_backend_herdr_parse_target "$target"; then
|
|
2375
|
+
echo "error: herdr endpoint $target for $task_id could not be parsed exactly; nothing was changed - repair the endpoint metadata and rerun teardown" >&2
|
|
2376
|
+
return 1
|
|
2377
|
+
fi
|
|
2378
|
+
session=$FM_BACKEND_HERDR_SESSION
|
|
2379
|
+
pane=$FM_BACKEND_HERDR_PANE
|
|
2380
|
+
presence=$(fm_backend_herdr_pane_presence_state "$session" "$pane")
|
|
2381
|
+
case "$presence" in
|
|
2382
|
+
dead|present) ;;
|
|
2383
|
+
*)
|
|
2384
|
+
echo "error: herdr endpoint $target for $task_id has ambiguous structured presence; nothing was changed - restore reliable endpoint inspection and rerun teardown" >&2
|
|
2385
|
+
return 1
|
|
2386
|
+
;;
|
|
2387
|
+
esac
|
|
2388
|
+
if ! lock_path=$(fm_backend_herdr_presentation_session_lock_path "$session"); then
|
|
2389
|
+
echo "error: herdr session presentation lock could not be resolved for $task_id; nothing was changed - rerun teardown once the session is reachable and unambiguous" >&2
|
|
2390
|
+
return 1
|
|
2391
|
+
fi
|
|
2392
|
+
if [ -n "$TEARDOWN_HERDR_LOCK_RECORDS" ]; then
|
|
2393
|
+
while IFS=$'\t' read -r lock_session held_path; do
|
|
2394
|
+
if [ "$lock_session" = "$session" ]; then
|
|
2395
|
+
if [ "$held_path" != "$lock_path" ]; then
|
|
2396
|
+
echo "error: herdr session presentation lock changed during preflight for $task_id; nothing was changed - rerun teardown once session identity is stable" >&2
|
|
2397
|
+
return 1
|
|
2398
|
+
fi
|
|
2399
|
+
return 0
|
|
2400
|
+
fi
|
|
2401
|
+
done <<FMEOF
|
|
2402
|
+
$TEARDOWN_HERDR_LOCK_RECORDS
|
|
2403
|
+
FMEOF
|
|
2404
|
+
fi
|
|
2405
|
+
attempt=0
|
|
2406
|
+
while [ "$attempt" -lt 50 ]; do
|
|
2407
|
+
if fm_lock_try_acquire "$lock_path"; then
|
|
2408
|
+
if ! verified_lock_path=$(fm_backend_herdr_presentation_session_lock_path "$session") \
|
|
2409
|
+
|| [ "$verified_lock_path" != "$lock_path" ]; then
|
|
2410
|
+
fm_lock_release "$lock_path" || true
|
|
2411
|
+
echo "error: herdr session presentation lock changed during preflight for $task_id; nothing was changed - rerun teardown once session identity is stable" >&2
|
|
2412
|
+
return 1
|
|
2413
|
+
fi
|
|
2414
|
+
if [ -n "$TEARDOWN_HERDR_LOCK_RECORDS" ]; then
|
|
2415
|
+
TEARDOWN_HERDR_LOCK_RECORDS="$TEARDOWN_HERDR_LOCK_RECORDS
|
|
2416
|
+
$session $lock_path"
|
|
2417
|
+
else
|
|
2418
|
+
TEARDOWN_HERDR_LOCK_RECORDS="$session $lock_path"
|
|
2419
|
+
fi
|
|
2420
|
+
return 0
|
|
2421
|
+
fi
|
|
2422
|
+
sleep 0.1
|
|
2423
|
+
attempt=$((attempt + 1))
|
|
2424
|
+
done
|
|
2425
|
+
echo "error: herdr session presentation lock is contended for $task_id; nothing was changed - rerun teardown once the contention clears" >&2
|
|
2426
|
+
return 1
|
|
2427
|
+
}
|
|
2428
|
+
|
|
2429
|
+
preflight_firstmate_home_herdr_children() { # <home>
|
|
2430
|
+
local home=$1 sub_state child_meta child_id child_backend child_target child_kind child_home child_wt
|
|
2431
|
+
sub_state="$home/state"
|
|
2432
|
+
[ -d "$sub_state" ] || return 0
|
|
2433
|
+
for child_meta in "$sub_state"/*.meta; do
|
|
2434
|
+
[ -e "$child_meta" ] || continue
|
|
2435
|
+
child_id=$(basename "$child_meta" .meta)
|
|
2436
|
+
fm_backend_validate_task_endpoint "$child_meta" "$child_id" || return 1
|
|
2437
|
+
child_backend=$FM_BACKEND_VALIDATED_BACKEND
|
|
2438
|
+
child_target=$FM_BACKEND_VALIDATED_TARGET
|
|
2439
|
+
if [ "$child_backend" = herdr ]; then
|
|
2440
|
+
teardown_herdr_preflight_target "$child_target" "$child_id" || return 1
|
|
2441
|
+
fi
|
|
2442
|
+
child_kind=$(meta_value "$child_meta" kind)
|
|
2443
|
+
[ -n "$child_kind" ] || child_kind=ship
|
|
2444
|
+
if [ "$child_kind" = secondmate ]; then
|
|
2445
|
+
child_wt=$(meta_value "$child_meta" worktree)
|
|
2446
|
+
child_home=$(meta_value "$child_meta" home)
|
|
2447
|
+
[ -n "$child_home" ] || child_home=$child_wt
|
|
2448
|
+
preflight_firstmate_home_herdr_children "$child_home" || return 1
|
|
2449
|
+
fi
|
|
2450
|
+
done
|
|
2451
|
+
}
|
|
2452
|
+
|
|
2453
|
+
cleanup_firstmate_home_children() {
|
|
2454
|
+
local home=$1 sub_state child_meta child_id child_t child_wt child_proj child_kind child_home child_backend child_orca_worktree_id child_return_rc child_busy_gen
|
|
2455
|
+
sub_state="$home/state"
|
|
2456
|
+
[ -d "$sub_state" ] || return 0
|
|
2457
|
+
for child_meta in "$sub_state"/*.meta; do
|
|
2458
|
+
[ -e "$child_meta" ] || continue
|
|
2459
|
+
child_id=$(basename "$child_meta" .meta)
|
|
2460
|
+
child_wt=$(meta_value "$child_meta" worktree)
|
|
2461
|
+
child_proj=$(meta_value "$child_meta" project)
|
|
2462
|
+
child_kind=$(meta_value "$child_meta" kind)
|
|
2463
|
+
[ -n "$child_kind" ] || child_kind=ship
|
|
2464
|
+
child_backend=$(fm_backend_of_meta "$child_meta")
|
|
2465
|
+
if [ "$child_backend" = orca ]; then
|
|
2466
|
+
child_t=$(meta_value "$child_meta" terminal)
|
|
2467
|
+
else
|
|
2468
|
+
child_t=$(fm_backend_target_of_meta "$child_meta")
|
|
2469
|
+
fi
|
|
2470
|
+
if [ "$child_backend" = orca ] && [ "$child_kind" != secondmate ]; then
|
|
2471
|
+
child_orca_worktree_id=$(require_orca_worktree_id "$child_meta") || return 1
|
|
2472
|
+
if [ -n "$child_wt" ] && [ -e "$child_wt" ]; then
|
|
2473
|
+
validate_child_worktree_for_removal "$child_wt" "$child_proj" >/dev/null || return 1
|
|
2474
|
+
fi
|
|
2475
|
+
fi
|
|
2476
|
+
if [ -n "$child_t" ]; then
|
|
2477
|
+
if [ "$child_backend" = herdr ]; then
|
|
2478
|
+
fm_backend_herdr_parse_target "$child_t" || return 1
|
|
2479
|
+
if ! teardown_herdr_session_lock_held "$FM_BACKEND_HERDR_SESSION"; then
|
|
2480
|
+
echo "error: herdr session presentation lock is not held for child $child_id; retaining that child's durable identity records and stopping forced cleanup" >&2
|
|
2481
|
+
return 1
|
|
2482
|
+
fi
|
|
2483
|
+
fm_backend_herdr_kill_serialized "$FM_BACKEND_HERDR_SESSION" "$FM_BACKEND_HERDR_PANE" 2>/dev/null || true
|
|
2484
|
+
if ! fm_backend_herdr_endpoint_confirmed_gone "$child_t"; then
|
|
2485
|
+
echo "error: herdr pane $child_t for child $child_id is not confirmed gone; retaining that child's durable identity records and stopping forced cleanup" >&2
|
|
2486
|
+
return 1
|
|
2487
|
+
fi
|
|
2488
|
+
elif [ "$child_backend" = zellij ]; then
|
|
2489
|
+
# Zellij titles are scoped by the owning home tag, so forced secondmate
|
|
2490
|
+
# cleanup must verify child tabs as that child home, not the parent.
|
|
2491
|
+
( unset FM_ROOT_OVERRIDE; FM_HOME=$home FM_ROOT=$home fm_backend_kill "$child_backend" "$child_t" "$(meta_value "$child_meta" zellij_tab_id)" "fm-$child_id" ) 2>/dev/null || true
|
|
2492
|
+
else
|
|
2493
|
+
fm_backend_kill "$child_backend" "$child_t" "$(meta_value "$child_meta" zellij_tab_id)" "fm-$child_id" 2>/dev/null || true
|
|
2494
|
+
fi
|
|
2495
|
+
fi
|
|
2496
|
+
if [ "$child_kind" = secondmate ]; then
|
|
2497
|
+
child_home=$(meta_value "$child_meta" home)
|
|
2498
|
+
[ -n "$child_home" ] || child_home=$child_wt
|
|
2499
|
+
if [ -n "$child_home" ] && [ -d "$child_home" ]; then
|
|
2500
|
+
cleanup_firstmate_home_children "$child_home" || return $?
|
|
2501
|
+
remove_firstmate_home "$child_home" "child firstmate home" "$child_id" || return $?
|
|
2502
|
+
fi
|
|
2503
|
+
elif [ "$child_backend" = orca ]; then
|
|
2504
|
+
if [ -n "$child_wt" ] && [ -d "$child_wt" ]; then
|
|
2505
|
+
validate_child_worktree_for_removal "$child_wt" "$child_proj" >/dev/null || return 1
|
|
2506
|
+
rm -f "$child_wt/.claude/settings.local.json" "$child_wt/.opencode/plugins/fm-turn-end.js" \
|
|
2507
|
+
"$child_wt/.fm-grok-turnend" "$child_wt/.fm-kimi-turnend"
|
|
2508
|
+
fi
|
|
2509
|
+
fm_backend_remove_worktree "$child_backend" "$child_orca_worktree_id" || return 1
|
|
2510
|
+
elif [ -n "$child_wt" ] && [ -d "$child_wt" ]; then
|
|
2511
|
+
validate_child_worktree_for_removal "$child_wt" "$child_proj" >/dev/null || return 1
|
|
2512
|
+
rm -f "$child_wt/.claude/settings.local.json" "$child_wt/.opencode/plugins/fm-turn-end.js" \
|
|
2513
|
+
"$child_wt/.opencode/plugins/fm-busy-state.js" \
|
|
2514
|
+
"$child_wt/.fm-grok-turnend" "$child_wt/.fm-kimi-turnend"
|
|
2515
|
+
if [ -n "$child_proj" ] && [ -d "$child_proj" ] && command -v treehouse >/dev/null 2>&1; then
|
|
2516
|
+
if teardown_treehouse_return "$child_wt" "$child_proj" "child worktree"; then
|
|
2517
|
+
:
|
|
2518
|
+
else
|
|
2519
|
+
child_return_rc=$?
|
|
2520
|
+
if [ "$child_return_rc" -eq "$TEARDOWN_TREEHOUSE_LOCK_REFUSED" ]; then
|
|
2521
|
+
return "$child_return_rc"
|
|
2522
|
+
fi
|
|
2523
|
+
safe_rm_rf_child_worktree "$child_wt" "$child_proj"
|
|
2524
|
+
fi
|
|
2525
|
+
else
|
|
2526
|
+
safe_rm_rf_child_worktree "$child_wt" "$child_proj"
|
|
2527
|
+
fi
|
|
2528
|
+
fi
|
|
2529
|
+
remove_grok_turnend_auth "$sub_state" "$child_id" || return 1
|
|
2530
|
+
remove_kimi_turnend_auth "$sub_state" "$child_id" || return 1
|
|
2531
|
+
remove_pr_poll_artifacts "$sub_state" "$child_id" || return 1
|
|
2532
|
+
child_busy_gen=$(meta_value "$child_meta" busy_gen)
|
|
2533
|
+
if [ -z "$child_busy_gen" ]; then
|
|
2534
|
+
child_busy_gen=$(cat "$sub_state/$child_id.busy-gen" 2>/dev/null || true)
|
|
2535
|
+
fi
|
|
2536
|
+
retire_busy_state "$sub_state" "$child_id" "$child_busy_gen" || return 1
|
|
2537
|
+
status_retire_presentation_task "$sub_state" "$child_id" || return 1
|
|
2538
|
+
fm_backlog_atomic_transition remove "$sub_state/$child_id.meta" "task record" "$sub_state" || return 1
|
|
2539
|
+
rm -f "$sub_state/$child_id.turn-ended" \
|
|
2540
|
+
"$sub_state/$child_id.pi-ext.ts" \
|
|
2541
|
+
"$sub_state/$child_id.grok-turnend-token" "$sub_state/$child_id.kimi-turnend-token" \
|
|
2542
|
+
"$sub_state/$child_id.muse-session" "$sub_state/$child_id.muse-session-current" \
|
|
2543
|
+
"$sub_state/$child_id.cursor-session" "$sub_state/$child_id.reconcile-nudged"
|
|
2544
|
+
done
|
|
2545
|
+
}
|
|
2546
|
+
|
|
2547
|
+
remove_secondmate_registry_entry() {
|
|
2548
|
+
local id=$1 tmp lock rc=0 acquired=0
|
|
2549
|
+
[ -f "$SECONDMATE_REG" ] || return 0
|
|
2550
|
+
lock=$(secondmate_registry_lock_path "$STATE")
|
|
2551
|
+
if [ "$LOCAL_REGISTRY_LOCK" != "$lock" ]; then
|
|
2552
|
+
fm_lock_acquire_wait "$lock" || return 1
|
|
2553
|
+
acquired=1
|
|
2554
|
+
fi
|
|
2555
|
+
tmp="$SECONDMATE_REG.tmp.$$"
|
|
2556
|
+
grep -vE "^- $id( |$)" "$SECONDMATE_REG" > "$tmp" || true
|
|
2557
|
+
mv "$tmp" "$SECONDMATE_REG" || rc=$?
|
|
2558
|
+
[ "$acquired" -eq 0 ] || fm_lock_release "$lock"
|
|
2559
|
+
return "$rc"
|
|
2560
|
+
}
|
|
2561
|
+
|
|
2562
|
+
validate_pr_poll_cleanup "$STATE" "$ID" || exit 1
|
|
2563
|
+
|
|
2564
|
+
if [ "$KIND" = secondmate ]; then
|
|
2565
|
+
LOCAL_REGISTRY_LOCK=$(secondmate_registry_lock_path "$STATE")
|
|
2566
|
+
fm_lock_acquire_wait "$LOCAL_REGISTRY_LOCK" || exit 1
|
|
2567
|
+
LOCAL_HANDOFF_LOCK="$STATE/.backlog-handoff-$ID.lock"
|
|
2568
|
+
fm_lock_acquire_wait "$LOCAL_HANDOFF_LOCK" || exit 1
|
|
2569
|
+
[ -n "$HOME_PATH" ] || HOME_PATH=$WT
|
|
2570
|
+
handoff_wake_retire_stage_recover "$HOME_PATH" || exit 1
|
|
2571
|
+
handoff_wake_retire_validate || exit 1
|
|
2572
|
+
validate_firstmate_home_for_removal "$HOME_PATH" "secondmate home" "$ID" >/dev/null || exit 1
|
|
2573
|
+
if [ "$FORCE" = "--force" ]; then
|
|
2574
|
+
validate_firstmate_home_children_removal "$HOME_PATH" || exit 1
|
|
2575
|
+
preflight_descendant_task_locks "$HOME_PATH" || exit 1
|
|
2576
|
+
validate_firstmate_home_children_removal "$HOME_PATH" || exit 1
|
|
2577
|
+
if [ "$BACKEND" = herdr ]; then
|
|
2578
|
+
teardown_herdr_preflight_target "$T" "$ID" || exit 1
|
|
2579
|
+
fi
|
|
2580
|
+
preflight_firstmate_home_herdr_children "$HOME_PATH" || exit 1
|
|
2581
|
+
fi
|
|
2582
|
+
fi
|
|
2583
|
+
|
|
2584
|
+
if [ "$KIND" = secondmate ] && [ "$FORCE" != "--force" ]; then
|
|
2585
|
+
SUB_STATE="$HOME_PATH/state"
|
|
2586
|
+
if [ -d "$SUB_STATE" ]; then
|
|
2587
|
+
for child_meta in "$SUB_STATE"/*.meta; do
|
|
2588
|
+
[ -e "$child_meta" ] || continue
|
|
2589
|
+
echo "REFUSED: secondmate $ID still has in-flight work in $SUB_STATE." >&2
|
|
2590
|
+
echo "Found $(basename "$child_meta"). Let that home finish or explicitly discard with --force." >&2
|
|
2591
|
+
exit 1
|
|
2592
|
+
done
|
|
2593
|
+
fi
|
|
2594
|
+
fi
|
|
2595
|
+
|
|
2596
|
+
if [ "$KIND" = secondmate ]; then
|
|
2597
|
+
preflight_firstmate_home_process_event_tree "$HOME_PATH" "secondmate home" || exit 1
|
|
2598
|
+
fi
|
|
2599
|
+
|
|
2600
|
+
if [ "$KIND" = secondmate ] && [ "$FORCE" = "--force" ]; then
|
|
2601
|
+
cleanup_firstmate_home_children "$HOME_PATH" || exit $?
|
|
2602
|
+
fi
|
|
2603
|
+
|
|
2604
|
+
if [ "$KIND" = scout ] && [ "$FORCE" != "--force" ]; then
|
|
2605
|
+
REPORT="$DATA/$ID/report.md"
|
|
2606
|
+
if [ ! -f "$REPORT" ]; then
|
|
2607
|
+
echo "REFUSED: scout task $ID has no report at $REPORT." >&2
|
|
2608
|
+
echo "The report is the work product. Have the crewmate write it, or use --force after explicit discard approval." >&2
|
|
2609
|
+
exit 1
|
|
2610
|
+
fi
|
|
2611
|
+
if ! FM_HOME="$FM_HOME" FM_STATE_OVERRIDE="$STATE" FM_DATA_OVERRIDE="$DATA" \
|
|
2612
|
+
FM_CONFIG_OVERRIDE="$CONFIG" "$SCRIPT_DIR/fm-captain-hold.sh" verify "$ID" >/dev/null; then
|
|
2613
|
+
echo "REFUSED: scout task $ID has not passed the captain-call completion gate." >&2
|
|
2614
|
+
echo "Inventory its report and any visual review through bin/fm-captain-hold.sh before teardown." >&2
|
|
2615
|
+
exit 1
|
|
2616
|
+
fi
|
|
2617
|
+
fi
|
|
2618
|
+
|
|
2619
|
+
# A public commitment is not kept until its final reply lands in the ORIGINAL
|
|
2620
|
+
# thread, and this cleanup removes the task records that make the promise
|
|
2621
|
+
# reconcilable. Refuse while this home still owes a public reply for exactly this
|
|
2622
|
+
# work. Both gates live in bin/fm-public-followup-lib.sh, so a home that never
|
|
2623
|
+
# opted into the myfirstmate relay runs one [ -f ] test and nothing else here.
|
|
2624
|
+
if [ "$FORCE" != "--force" ] && [ "$PUBLIC_FOLLOWUP_PARENT_UNRESOLVED" = 1 ]; then
|
|
2625
|
+
echo "REFUSED: cannot resolve the primary home for marked secondmate $SECOND_MATE_ID; refusing cleanup without its durable parent binding." >&2
|
|
2626
|
+
exit 1
|
|
2627
|
+
fi
|
|
2628
|
+
if [ "$FORCE" != "--force" ] \
|
|
2629
|
+
&& [ -n "$PUBLIC_FOLLOWUP_STATE" ] \
|
|
2630
|
+
&& [ "$PUBLIC_FOLLOWUP_RELAY_ACTIVE" = 1 ] \
|
|
2631
|
+
&& fm_pf_has_registrations "$PUBLIC_FOLLOWUP_STATE"; then
|
|
2632
|
+
if ! PUBLIC_FOLLOWUP_BLOCKING=$(FM_HOME="$PUBLIC_FOLLOWUP_HOME" FM_STATE_OVERRIDE="$PUBLIC_FOLLOWUP_STATE" \
|
|
2633
|
+
"$SCRIPT_DIR/fm-public-followup.sh" guard-work "$PUBLIC_FOLLOWUP_WORK_HOME" "$ID" 2>/dev/null); then
|
|
2634
|
+
echo "REFUSED: task $ID still owes a public reply through the myfirstmate relay." >&2
|
|
2635
|
+
printf '%s\n' "$PUBLIC_FOLLOWUP_BLOCKING" >&2
|
|
2636
|
+
echo "Deliver it with bin/fm-public-followup.sh deliver <obligation-id>, waive it with tasks-axi public-followup waive, or use --force after explicit discard approval." >&2
|
|
2637
|
+
exit 1
|
|
2638
|
+
fi
|
|
2639
|
+
fi
|
|
2640
|
+
|
|
2641
|
+
# Non-blocking: a delivered public loop is not a teardown refusal (guard-work
|
|
2642
|
+
# already passed), but tearing down a ship whose PR merged while a loop is still
|
|
2643
|
+
# open with nothing owed is the moment the drop is detectable.
|
|
2644
|
+
if [ "$KIND" = ship ] && [ -n "$PR_URL" ] \
|
|
2645
|
+
&& [ -n "$PUBLIC_FOLLOWUP_STATE" ] \
|
|
2646
|
+
&& [ "${PUBLIC_FOLLOWUP_RELAY_ACTIVE:-0}" = 1 ] \
|
|
2647
|
+
&& fm_pf_has_delivered_open_loops "$PUBLIC_FOLLOWUP_STATE"; then
|
|
2648
|
+
echo "warning: an open public loop with nothing owed is still recorded in the consent-holding home while cleaning up ship task $ID. Hand it on with bin/fm-public-followup.sh rechain or close it with retire --reason." >&2
|
|
2649
|
+
fi
|
|
2650
|
+
|
|
2651
|
+
# Non-blocking: the legacy Relay link is not guarded as a refusal.
|
|
2652
|
+
X_REQUEST=$(grep '^x_request=' "$META" 2>/dev/null | tail -1 | cut -d= -f2- || true)
|
|
2653
|
+
if [ -n "$X_REQUEST" ]; then
|
|
2654
|
+
echo "warning: task $ID still carries an unreconciled Relay request link ($X_REQUEST) on its task record." >&2
|
|
2655
|
+
fi
|
|
2656
|
+
|
|
2657
|
+
if [ "$BACKEND" = orca ] && [ "$KIND" != scout ] && [ "$KIND" != secondmate ] && [ "$FORCE" != "--force" ]; then
|
|
2658
|
+
if ! inspectable_git_worktree "$WT"; then
|
|
2659
|
+
echo "REFUSED: Orca ship task $ID has no inspectable git worktree at ${WT:-<missing>}." >&2
|
|
2660
|
+
echo "Cannot verify dirty or unlanded work; restore the worktree path or get explicit OK to discard, then --force." >&2
|
|
2661
|
+
exit 1
|
|
2662
|
+
fi
|
|
2663
|
+
require_orca_worktree_path_match "$ORCA_WORKTREE_ID" "$WT" || exit 1
|
|
2664
|
+
ORCA_PATH_MATCH_VERIFIED=1
|
|
2665
|
+
fi
|
|
2666
|
+
|
|
2667
|
+
if [ -d "$WT" ] && [ "$FORCE" != "--force" ]; then
|
|
2668
|
+
if validate_worktree_teardown_safety; then
|
|
2669
|
+
:
|
|
2670
|
+
else
|
|
2671
|
+
safety_rc=$?
|
|
2672
|
+
if [ "$safety_rc" -eq "$TEARDOWN_WORKTREE_SAFETY_LOCK_BLOCKED" ]; then
|
|
2673
|
+
cleanup_stale_lock_for_safety_check "$WT" || exit 1
|
|
2674
|
+
validate_worktree_teardown_safety || exit 1
|
|
2675
|
+
else
|
|
2676
|
+
exit 1
|
|
2677
|
+
fi
|
|
2678
|
+
fi
|
|
2679
|
+
fi
|
|
2680
|
+
|
|
2681
|
+
# A Herdr close may reposition shared workspace order, so the whole
|
|
2682
|
+
# destructive sequence below (worktree return, pane close, record removal)
|
|
2683
|
+
# runs under the named-session presentation lock, acquired BEFORE anything is
|
|
2684
|
+
# returned or erased: a contended lock refuses here while the isolated copy,
|
|
2685
|
+
# every durable record, and the endpoint are all still intact for a plain
|
|
2686
|
+
# rerun. An unresolvable lock path (for example an unreachable server) also
|
|
2687
|
+
# refuses before any destructive step.
|
|
2688
|
+
TEARDOWN_HERDR_SESSION=
|
|
2689
|
+
TEARDOWN_HERDR_PANE=
|
|
2690
|
+
if [ "$BACKEND" = herdr ]; then
|
|
2691
|
+
teardown_herdr_preflight_target "$T" "$ID" || exit 1
|
|
2692
|
+
fm_backend_herdr_parse_target "$T" || exit 1
|
|
2693
|
+
TEARDOWN_HERDR_SESSION=$FM_BACKEND_HERDR_SESSION
|
|
2694
|
+
TEARDOWN_HERDR_PANE=$FM_BACKEND_HERDR_PANE
|
|
2695
|
+
fi
|
|
2696
|
+
|
|
2697
|
+
BACKLOG_CLOSED=0
|
|
2698
|
+
BACKLOG_SKIP_REASON=
|
|
2699
|
+
if [ "$TEARDOWN_BACKLOG_APPLIES" = 1 ]; then
|
|
2700
|
+
backlog_done_args || {
|
|
2701
|
+
echo "error: the pending backlog close for $ID is not replayable; refusing destructive teardown" >&2
|
|
2702
|
+
exit 1
|
|
2703
|
+
}
|
|
2704
|
+
BACKLOG_CLOSED=1
|
|
2705
|
+
META_SPAWN_GEN=$TEARDOWN_META_SPAWN_GEN
|
|
2706
|
+
fm_backlog_close_marker_write "$STATE" "$ID" "$DATA" "$META_SPAWN_GEN" \
|
|
2707
|
+
"${BACKLOG_DONE_ARGS[@]+"${BACKLOG_DONE_ARGS[@]}"}" \
|
|
2708
|
+
|| { echo "error: the pending backlog close for $ID could not be recorded ($FM_BACKLOG_TRANSITION_ERROR); retaining every durable task record" >&2; exit 1; }
|
|
2709
|
+
else
|
|
2710
|
+
if [ "$CLEANUP_RECOVERY" = orca ]; then
|
|
2711
|
+
BACKLOG_SKIP_REASON="Orca cleanup recovery is not a launched backlog worker"
|
|
2712
|
+
else
|
|
2713
|
+
BACKLOG_SKIP_REASON=$TEARDOWN_BACKLOG_SKIP_REASON
|
|
2714
|
+
fi
|
|
2715
|
+
fi
|
|
2716
|
+
|
|
2717
|
+
# Every landed/discard-work refusal above has now passed (or --force skipped
|
|
2718
|
+
# them). Fix 1 and Fix 2 (see script header) run here, unconditionally on
|
|
2719
|
+
# --force, and before ANY destructive step below - a still-parked run or a
|
|
2720
|
+
# leaked process can own live work in this exact worktree. Not for
|
|
2721
|
+
# kind=secondmate: a secondmate home's own runtime lifecycle is owned by the
|
|
2722
|
+
# dedicated process-event and firstmate-home removal machinery further below,
|
|
2723
|
+
# not by task-worktree cleanup.
|
|
2724
|
+
if [ "$KIND" != secondmate ]; then
|
|
2725
|
+
conclude_task_no_mistakes_run "$WT"
|
|
2726
|
+
reap_task_worktree_processes worktree "$WT" "$TASK_TMP"
|
|
2727
|
+
fi
|
|
2728
|
+
|
|
2729
|
+
# Fix 3 (see script header): sweep remote job workers abandoned by an already
|
|
2730
|
+
# pruned code root. Best effort - a sweep failure never blocks this teardown.
|
|
2731
|
+
"$SCRIPT_DIR/fm-remote-job-reap-orphans.sh" >&2 || true
|
|
2732
|
+
|
|
2733
|
+
# Best-effort: drop the local task branch so the shared repo does not accumulate refs.
|
|
2734
|
+
if [ "$BACKEND" = orca ] && [ "$KIND" != secondmate ]; then
|
|
2735
|
+
if [ "$ORCA_PATH_MATCH_VERIFIED" != 1 ]; then
|
|
2736
|
+
require_orca_worktree_path_match_if_present "$ORCA_WORKTREE_ID" "$WT" || exit 1
|
|
2737
|
+
ORCA_PATH_MATCH_VERIFIED=1
|
|
2738
|
+
fi
|
|
2739
|
+
if [ -d "$WT" ]; then
|
|
2740
|
+
branch=$(git -C "$WT" rev-parse --abbrev-ref HEAD 2>/dev/null || echo HEAD)
|
|
2741
|
+
if [ "$branch" != "HEAD" ]; then
|
|
2742
|
+
if git -C "$WT" checkout --detach -q 2>/dev/null; then
|
|
2743
|
+
git -C "$WT" branch -D "$branch" >/dev/null 2>&1 || true
|
|
2744
|
+
fi
|
|
2745
|
+
fi
|
|
2746
|
+
rm -f "$WT/.claude/settings.local.json" "$WT/.opencode/plugins/fm-turn-end.js" \
|
|
2747
|
+
"$WT/.opencode/plugins/fm-busy-state.js" \
|
|
2748
|
+
"$WT/.fm-grok-turnend" "$WT/.fm-kimi-turnend"
|
|
2749
|
+
fi
|
|
2750
|
+
[ -z "$T_ORCA" ] || fm_backend_kill "$BACKEND" "$T" "$(meta_value "$META" zellij_tab_id)" "fm-$ID" 2>/dev/null || true
|
|
2751
|
+
fm_backend_remove_worktree "$BACKEND" "$ORCA_WORKTREE_ID"
|
|
2752
|
+
elif [ -d "$WT" ] && [ "$KIND" != secondmate ]; then
|
|
2753
|
+
branch=$(git -C "$WT" rev-parse --abbrev-ref HEAD 2>/dev/null || echo HEAD)
|
|
2754
|
+
if [ "$branch" != "HEAD" ]; then
|
|
2755
|
+
if git -C "$WT" checkout --detach -q 2>/dev/null; then
|
|
2756
|
+
git -C "$WT" branch -D "$branch" >/dev/null 2>&1 || true
|
|
2757
|
+
fi
|
|
2758
|
+
fi
|
|
2759
|
+
# Remove our hook file so a reused pool worktree cannot fire signals for a dead task.
|
|
2760
|
+
rm -f "$WT/.claude/settings.local.json" "$WT/.opencode/plugins/fm-turn-end.js" \
|
|
2761
|
+
"$WT/.fm-grok-turnend" "$WT/.fm-kimi-turnend"
|
|
2762
|
+
# Kills remaining processes in the worktree (including the agent), resets, returns
|
|
2763
|
+
# to pool. treehouse resolves the pool from the working directory, so run it from
|
|
2764
|
+
# the project. teardown_treehouse_return tolerates transient and stale git locks
|
|
2765
|
+
# left by a killed crew process; see the script header for retry and stale-lock proof.
|
|
2766
|
+
post_lock_cleanup_check=
|
|
2767
|
+
if [ "$FORCE" != "--force" ] && [ "$KIND" != scout ] && [ "$KIND" != secondmate ]; then
|
|
2768
|
+
post_lock_cleanup_check=validate_worktree_teardown_safety
|
|
2769
|
+
fi
|
|
2770
|
+
teardown_treehouse_return "$WT" "$PROJ" "worktree" "$post_lock_cleanup_check" || {
|
|
2771
|
+
echo "error: treehouse return failed for worktree $WT; teardown aborted" >&2
|
|
2772
|
+
exit 1
|
|
2773
|
+
}
|
|
2774
|
+
fi
|
|
2775
|
+
|
|
2776
|
+
HERDR_PRESENTATION_JOURNAL="$STATE/$ID.herdr-presentation"
|
|
2777
|
+
HERDR_PRESENTATION_RETIRE_CANDIDATE=0
|
|
2778
|
+
HERDR_PRESENTATION_SESSION=
|
|
2779
|
+
HERDR_PRESENTATION_PANE=
|
|
2780
|
+
if [ "$BACKEND" = herdr ] \
|
|
2781
|
+
&& { [ -e "$HERDR_PRESENTATION_JOURNAL" ] || [ -L "$HERDR_PRESENTATION_JOURNAL" ]; }; then
|
|
2782
|
+
fm_backend_source herdr || true
|
|
2783
|
+
HERDR_PRESENTATION_SESSION=$(meta_value "$META" herdr_session)
|
|
2784
|
+
HERDR_PRESENTATION_WORKSPACE=$(meta_value "$META" herdr_workspace_id)
|
|
2785
|
+
HERDR_PRESENTATION_PANE=$(meta_value "$META" herdr_pane_id)
|
|
2786
|
+
if [ -n "$HERDR_PRESENTATION_SESSION" ] \
|
|
2787
|
+
&& [ -n "$HERDR_PRESENTATION_WORKSPACE" ] \
|
|
2788
|
+
&& [ -n "$HERDR_PRESENTATION_PANE" ] \
|
|
2789
|
+
&& [ "$T" = "$HERDR_PRESENTATION_SESSION:$HERDR_PRESENTATION_PANE" ] \
|
|
2790
|
+
&& fm_backend_herdr_projection_endpoint_matches_journal \
|
|
2791
|
+
"$HERDR_PRESENTATION_SESSION" "$HERDR_PRESENTATION_WORKSPACE" \
|
|
2792
|
+
"$HERDR_PRESENTATION_JOURNAL" "$ID"; then
|
|
2793
|
+
HERDR_PRESENTATION_RETIRE_CANDIDATE=1
|
|
2794
|
+
fi
|
|
2795
|
+
fi
|
|
2796
|
+
|
|
2797
|
+
if [ "$HERDR_PRESENTATION_RETIRE_CANDIDATE" = 1 ]; then
|
|
2798
|
+
# The presentation lock was acquired before the worktree return above; a
|
|
2799
|
+
# contended lock already refused this teardown while everything was intact.
|
|
2800
|
+
if teardown_herdr_session_lock_held "$HERDR_PRESENTATION_SESSION"; then
|
|
2801
|
+
# stderr is deliberately NOT discarded here. This is the highest-frequency
|
|
2802
|
+
# projected-close call site, and the helper's only stderr output is a real
|
|
2803
|
+
# warning - unverifiable workspace.move support, a refused focus-unsafe
|
|
2804
|
+
# close, an unconfirmed repositioned-workspace removal, or a failed exact
|
|
2805
|
+
# restore.
|
|
2806
|
+
# Swallowing them left a wrong active workspace with no operator-visible
|
|
2807
|
+
# signal at all. The close stays non-fatal exactly as before: the presence
|
|
2808
|
+
# gate below is what decides whether any durable record may be removed.
|
|
2809
|
+
fm_backend_herdr_projection_close_pane_focus_preserving \
|
|
2810
|
+
"$HERDR_PRESENTATION_SESSION" "$HERDR_PRESENTATION_PANE" || true
|
|
2811
|
+
else
|
|
2812
|
+
echo "warning: herdr presentation focus lock unavailable; refusing a concurrent focus-unsafe pane close" >&2
|
|
2813
|
+
fi
|
|
2814
|
+
elif [ "$BACKEND" = herdr ]; then
|
|
2815
|
+
if teardown_herdr_session_lock_held "$TEARDOWN_HERDR_SESSION"; then
|
|
2816
|
+
fm_backend_herdr_kill_serialized "$TEARDOWN_HERDR_SESSION" "$TEARDOWN_HERDR_PANE" 2>/dev/null || true
|
|
2817
|
+
else
|
|
2818
|
+
echo "warning: herdr session presentation lock path is unavailable; skipping the pane close rather than closing unlocked" >&2
|
|
2819
|
+
fi
|
|
2820
|
+
elif [ "$BACKEND" != orca ]; then
|
|
2821
|
+
fm_backend_kill "$BACKEND" "$T" "$(meta_value "$META" zellij_tab_id)" "fm-$ID" 2>/dev/null || true
|
|
2822
|
+
fi
|
|
2823
|
+
if [ "$HERDR_PRESENTATION_RETIRE_CANDIDATE" = 1 ]; then
|
|
2824
|
+
if [ "$(fm_backend_herdr_pane_agent_state "$HERDR_PRESENTATION_SESSION" "$HERDR_PRESENTATION_PANE")" = dead ]; then
|
|
2825
|
+
rm -f "$HERDR_PRESENTATION_JOURNAL"
|
|
2826
|
+
else
|
|
2827
|
+
echo "warning: exact herdr task-pane close could not be confirmed for $ID; retaining the presentation journal and attempting no workspace cleanup" >&2
|
|
2828
|
+
fi
|
|
2829
|
+
elif [ "$BACKEND" = herdr ] \
|
|
2830
|
+
&& { [ -e "$HERDR_PRESENTATION_JOURNAL" ] || [ -L "$HERDR_PRESENTATION_JOURNAL" ]; }; then
|
|
2831
|
+
echo "warning: herdr presentation journal for $ID remains quarantined; no workspace cleanup was attempted" >&2
|
|
2832
|
+
fi
|
|
2833
|
+
# A refused, skipped, or failed Herdr close must never erase a live task's
|
|
2834
|
+
# durable endpoint identity: unless the exact pane is confirmed gone, retain
|
|
2835
|
+
# every record and stop before any removal below so a later rerun can retry
|
|
2836
|
+
# the locked close. Only a structured not-found proves the pane gone; unknown
|
|
2837
|
+
# presence, missing or malformed endpoint identity, and missing confirmation
|
|
2838
|
+
# machinery all refuse.
|
|
2839
|
+
if [ "$BACKEND" = herdr ]; then
|
|
2840
|
+
fm_backend_source herdr || true
|
|
2841
|
+
if ! declare -F fm_backend_herdr_endpoint_confirmed_gone >/dev/null 2>&1; then
|
|
2842
|
+
echo "error: herdr endpoint confirmation is unavailable for $ID; retaining every durable task record" >&2
|
|
2843
|
+
exit 1
|
|
2844
|
+
fi
|
|
2845
|
+
if ! fm_backend_herdr_endpoint_confirmed_gone "$T"; then
|
|
2846
|
+
echo "error: herdr pane $T for $ID is not confirmed gone after its close was refused, skipped, or failed; retaining every durable task record - rerun teardown once the close can run under the session lock" >&2
|
|
2847
|
+
exit 1
|
|
2848
|
+
fi
|
|
2849
|
+
fi
|
|
2850
|
+
if [ "$KIND" = secondmate ]; then
|
|
2851
|
+
[ -n "$HOME_PATH" ] || HOME_PATH=$WT
|
|
2852
|
+
handoff_wake_retire_stage \
|
|
2853
|
+
|| { echo "error: receiver wake cleanup could not be staged; preserving the secondmate home and route" >&2; exit 1; }
|
|
2854
|
+
if remove_firstmate_home "$HOME_PATH" "secondmate home" "$ID"; then
|
|
2855
|
+
:
|
|
2856
|
+
else
|
|
2857
|
+
rc=$?
|
|
2858
|
+
handoff_wake_retire_stage_restore \
|
|
2859
|
+
|| echo "error: receiver wake restoration failed; recovery state remains at $HANDOFF_WAKE_RETIRE_STAGE" >&2
|
|
2860
|
+
exit "$rc"
|
|
2861
|
+
fi
|
|
2862
|
+
handoff_wake_retire_stage_commit \
|
|
2863
|
+
|| { echo "error: receiver wake cleanup failed; preserving the secondmate route for retry" >&2; exit 1; }
|
|
2864
|
+
remove_secondmate_registry_entry "$ID"
|
|
2865
|
+
fi
|
|
2866
|
+
remove_grok_turnend_auth "$STATE" "$ID" || exit 1
|
|
2867
|
+
remove_kimi_turnend_auth "$STATE" "$ID" || exit 1
|
|
2868
|
+
fm_backend_clear_transition "$BACKEND" "$STATE" "$T" || true
|
|
2869
|
+
# Remove the per-task temp root (/tmp/fm-<id>/, incl. its gotmp/) recorded by spawn.
|
|
2870
|
+
# Read before the state-file rm below; empty (pre-fix tasks without tasktmp=) is a no-op.
|
|
2871
|
+
[ -n "$TASK_TMP" ] && rm -rf "$TASK_TMP"
|
|
2872
|
+
remove_pr_poll_artifacts "$STATE" "$ID" || exit 1
|
|
2873
|
+
retire_busy_state "$STATE" "$ID" "$BUSY_GEN" || exit 1
|
|
2874
|
+
status_retire_presentation_task "$STATE" "$ID" || exit 1
|
|
2875
|
+
rm -f "$STATE/$ID.turn-ended" \
|
|
2876
|
+
"$STATE/$ID.pi-ext.ts" "$STATE/$ID.grok-turnend-token" \
|
|
2877
|
+
"$STATE/$ID.kimi-turnend-token" "$STATE/$ID.muse-session" \
|
|
2878
|
+
"$STATE/$ID.muse-session-current" "$STATE/$ID.cursor-session" \
|
|
2879
|
+
"$STATE/$ID.control-relaunch" "$STATE/$ID.control-relaunch.meta-prior" \
|
|
2880
|
+
"$STATE/$ID.control-relaunch.brief-prior" "$STATE/$ID.control-relaunch.note" \
|
|
2881
|
+
"$STATE/$ID.reconcile-nudged"
|
|
2882
|
+
# The steering inbox (bin/fm-task-inbox-lib.sh) is runtime state for the
|
|
2883
|
+
# retired endpoint; teardown only runs after landing is confirmed, so any
|
|
2884
|
+
# leftover unhandled steer here is moot rather than unlanded work.
|
|
2885
|
+
rm -rf "$STATE/$ID.inbox"
|
|
2886
|
+
# The record is gone, so the backlog must not still show this task in flight
|
|
2887
|
+
# when teardown reports success. Still under this task's meta lock, so a steer
|
|
2888
|
+
# racing the same id stays serialized exactly as it was before.
|
|
2889
|
+
if [ "$BACKLOG_CLOSED" = 1 ]; then
|
|
2890
|
+
BACKLOG_CLOSE_MARKER=$(fm_backlog_close_marker_path "$STATE" "$ID") || exit 1
|
|
2891
|
+
if ! fm_backlog_atomic_transition close "$STATE/$ID.meta" "$BACKLOG_CLOSE_MARKER" \
|
|
2892
|
+
"$DATA" "$ID" "$STATE" "${BACKLOG_DONE_ARGS[@]+"${BACKLOG_DONE_ARGS[@]}"}"; then
|
|
2893
|
+
fm_lock_release "$META_LOCK"
|
|
2894
|
+
META_LOCK_HELD=0
|
|
2895
|
+
echo "error: $ID's endpoint and local copy are cleaned up, but its backlog item could not be closed atomically ($FM_BACKLOG_TRANSITION_ERROR); the pending close is recorded and the next session start retries it" >&2
|
|
2896
|
+
exit 1
|
|
2897
|
+
fi
|
|
2898
|
+
elif [ "$KIND" = secondmate ] && [ ! -e "$STATE" ] && [ ! -L "$STATE" ]; then
|
|
2899
|
+
# A nested remote retirement can keep its route record inside the home being
|
|
2900
|
+
# removed. remove_firstmate_home above already performed that physical
|
|
2901
|
+
# deletion; do not turn its confirmed absence into a false cleanup failure.
|
|
2902
|
+
:
|
|
2903
|
+
else
|
|
2904
|
+
if ! fm_backlog_atomic_transition remove "$STATE/$ID.meta" "task record" "$STATE"; then
|
|
2905
|
+
fm_lock_release "$META_LOCK"
|
|
2906
|
+
META_LOCK_HELD=0
|
|
2907
|
+
echo "error: $ID's endpoint and local copy are cleaned up, but its task record could not be removed ($FM_BACKLOG_TRANSITION_ERROR)" >&2
|
|
2908
|
+
exit 1
|
|
2909
|
+
fi
|
|
2910
|
+
fi
|
|
2911
|
+
fm_lock_release "$META_LOCK"
|
|
2912
|
+
META_LOCK_HELD=0
|
|
2913
|
+
if [ "$KIND" != scout ] && [ "$KIND" != secondmate ] && [ "$MODE" != local-only ]; then
|
|
2914
|
+
"$FM_ROOT/bin/fm-fleet-sync.sh" "$PROJ" || true
|
|
2915
|
+
fi
|
|
2916
|
+
# A secondmate retirement may remove the home containing an overridden control
|
|
2917
|
+
# state directory. Do not let the side-band refresh recreate that retired home.
|
|
2918
|
+
if [ -d "$STATE" ]; then
|
|
2919
|
+
"$SCRIPT_DIR/fm-home-summary-refresh.sh" --best-effort || true
|
|
2920
|
+
fi
|
|
2921
|
+
echo "teardown $ID complete (window $T, worktree $WT)"
|
|
2922
|
+
backlog_refresh_reminder
|