@zerwiz/ymir 0.1.3 → 0.1.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.agents/README.md +291 -0
- package/.agents/agents/bragi-marketer.md +111 -0
- package/.agents/agents/brokk.md +352 -0
- package/.agents/agents/forseti-reviewer.md +93 -0
- package/.agents/agents/frigg-consent.md +77 -0
- package/.agents/agents/groa-update.md +77 -0
- package/.agents/agents/hnoss-designer.md +69 -0
- package/.agents/agents/huginn-researcher.md +99 -0
- package/.agents/agents/jord-projects.md +78 -0
- package/.agents/agents/kvasir-scout.md +91 -0
- package/.agents/agents/mimir-planner.md +92 -0
- package/.agents/agents/muninn-stow.md +77 -0
- package/.agents/agents/saga-bearings.md +78 -0
- package/.agents/agents/sindri-developer.md +114 -0
- package/.agents/agents/snotra-documenter.md +105 -0
- package/.agents/agents/syn-recovery.md +78 -0
- package/.agents/agents/tyr-check.md +78 -0
- package/.agents/agents/urdh-hold.md +77 -0
- package/.agents/agents/volundr-smith.md +80 -0
- package/.agents/agents/vor-diagnostics.md +77 -0
- package/.agents/assets/agents/naming.md +50 -0
- package/.agents/assets/agents/registry.md +93 -0
- package/.agents/assets/agents/runtime.md +36 -0
- package/.agents/assets/agents/toon-tasks-cli.md +42 -0
- package/.agents/assets/templates/company_entity.template.md +37 -0
- package/.agents/backend/backends/cmux.sh +649 -0
- package/.agents/backend/backends/herdr-eventwait.py +157 -0
- package/.agents/backend/backends/herdr-workspace-move.py +114 -0
- package/.agents/backend/backends/herdr.sh +3386 -0
- package/.agents/backend/backends/orca.sh +290 -0
- package/.agents/backend/backends/tmux.sh +357 -0
- package/.agents/backend/backends/zellij.sh +700 -0
- package/.agents/backend/fm-afk-launch.sh +654 -0
- package/.agents/backend/fm-afk-return.sh +241 -0
- package/.agents/backend/fm-afk-start.sh +172 -0
- package/.agents/backend/fm-arm-command-policy.mjs +960 -0
- package/.agents/backend/fm-arm-pretool-check.sh +200 -0
- package/.agents/backend/fm-backend-hometag-lib.sh +52 -0
- package/.agents/backend/fm-backend.sh +981 -0
- package/.agents/backend/fm-backlog-handoff.sh +909 -0
- package/.agents/backend/fm-backlog-receive.sh +187 -0
- package/.agents/backend/fm-backlog-transition-lib.sh +779 -0
- package/.agents/backend/fm-bearings-board.sh +204 -0
- package/.agents/backend/fm-bearings-snapshot.sh +546 -0
- package/.agents/backend/fm-bootstrap.sh +1568 -0
- package/.agents/backend/fm-branch-outcome.sh +203 -0
- package/.agents/backend/fm-branch-prompt.sh +101 -0
- package/.agents/backend/fm-brief.sh +452 -0
- package/.agents/backend/fm-busy-event.sh +234 -0
- package/.agents/backend/fm-busy-lib.sh +986 -0
- package/.agents/backend/fm-captain-hold.sh +1001 -0
- package/.agents/backend/fm-cd-command-policy.mjs +153 -0
- package/.agents/backend/fm-cd-pretool-check.sh +190 -0
- package/.agents/backend/fm-check-lib.sh +74 -0
- package/.agents/backend/fm-check-register.sh +42 -0
- package/.agents/backend/fm-check-unregister.sh +52 -0
- package/.agents/backend/fm-classify-lib.sh +1770 -0
- package/.agents/backend/fm-claude-stop-autoarm.sh +334 -0
- package/.agents/backend/fm-composer-lib.sh +1416 -0
- package/.agents/backend/fm-config-inherit-lib.sh +1202 -0
- package/.agents/backend/fm-config-push.sh +254 -0
- package/.agents/backend/fm-control-lib.sh +251 -0
- package/.agents/backend/fm-control.sh +878 -0
- package/.agents/backend/fm-crew-state.sh +625 -0
- package/.agents/backend/fm-cursor-lib.sh +243 -0
- package/.agents/backend/fm-decision-hold.sh +232 -0
- package/.agents/backend/fm-doc-audience-check.sh +269 -0
- package/.agents/backend/fm-dod-lib.sh +67 -0
- package/.agents/backend/fm-ensure-agents-md.sh +253 -0
- package/.agents/backend/fm-extension-launch-barrier.mjs +129 -0
- package/.agents/backend/fm-extension.mjs +2577 -0
- package/.agents/backend/fm-extension.sh +16 -0
- package/.agents/backend/fm-ff-lib.sh +420 -0
- package/.agents/backend/fm-fleet-snapshot.sh +1463 -0
- package/.agents/backend/fm-fleet-sync.sh +458 -0
- package/.agents/backend/fm-fleet-view.sh +96 -0
- package/.agents/backend/fm-gate-refuse-lib.sh +102 -0
- package/.agents/backend/fm-guard.sh +242 -0
- package/.agents/backend/fm-harness.sh +195 -0
- package/.agents/backend/fm-herdr-ci-cleanup.sh +115 -0
- package/.agents/backend/fm-herdr-lab.sh +346 -0
- package/.agents/backend/fm-herdr-session-cleanup.sh +337 -0
- package/.agents/backend/fm-home-seed.sh +984 -0
- package/.agents/backend/fm-home-summary-refresh.sh +255 -0
- package/.agents/backend/fm-hook-host-lib.sh +36 -0
- package/.agents/backend/fm-inactive-reconcile.sh +531 -0
- package/.agents/backend/fm-inbox.sh +399 -0
- package/.agents/backend/fm-install-actionlint.sh +84 -0
- package/.agents/backend/fm-install-herdr.sh +96 -0
- package/.agents/backend/fm-install-shellcheck.sh +84 -0
- package/.agents/backend/fm-install-treehouse.sh +95 -0
- package/.agents/backend/fm-kimi-turnend-hook.sh +276 -0
- package/.agents/backend/fm-lease-lib.sh +218 -0
- package/.agents/backend/fm-lease.sh +189 -0
- package/.agents/backend/fm-line-cap-lib.sh +51 -0
- package/.agents/backend/fm-lint-workflows.sh +137 -0
- package/.agents/backend/fm-lint.sh +591 -0
- package/.agents/backend/fm-lock-lib.sh +104 -0
- package/.agents/backend/fm-lock.sh +107 -0
- package/.agents/backend/fm-marker-lib.sh +12 -0
- package/.agents/backend/fm-merge-local.sh +74 -0
- package/.agents/backend/fm-merge-outcome-lib.sh +138 -0
- package/.agents/backend/fm-nm-run-lib.sh +124 -0
- package/.agents/backend/fm-on.sh +125 -0
- package/.agents/backend/fm-operational-input.sh +253 -0
- package/.agents/backend/fm-peek.sh +46 -0
- package/.agents/backend/fm-pending-reply-lib.sh +1409 -0
- package/.agents/backend/fm-pr-check.sh +135 -0
- package/.agents/backend/fm-pr-lib.sh +1016 -0
- package/.agents/backend/fm-pr-merge.sh +707 -0
- package/.agents/backend/fm-pr-poll.sh +110 -0
- package/.agents/backend/fm-primary-scope-lib.sh +33 -0
- package/.agents/backend/fm-procevent-extension-capture.pl +259 -0
- package/.agents/backend/fm-procevent-lavish.sh +628 -0
- package/.agents/backend/fm-procevent-lib.sh +892 -0
- package/.agents/backend/fm-procevent-quota.sh +290 -0
- package/.agents/backend/fm-procevent-remote-reply.sh +577 -0
- package/.agents/backend/fm-procevent-when.sh +504 -0
- package/.agents/backend/fm-procevent.sh +1466 -0
- package/.agents/backend/fm-project-mode.sh +92 -0
- package/.agents/backend/fm-project-origin-lib.sh +180 -0
- package/.agents/backend/fm-promote.sh +282 -0
- package/.agents/backend/fm-public-followup-emit.sh +260 -0
- package/.agents/backend/fm-public-followup-lib.sh +398 -0
- package/.agents/backend/fm-public-followup.sh +1330 -0
- package/.agents/backend/fm-push-transition-lib.sh +168 -0
- package/.agents/backend/fm-quota-axi-lib.sh +93 -0
- package/.agents/backend/fm-quota-choose.sh +384 -0
- package/.agents/backend/fm-remote-delta-read.sh +189 -0
- package/.agents/backend/fm-remote-doctor.sh +800 -0
- package/.agents/backend/fm-remote-entrypoint.sh +193 -0
- package/.agents/backend/fm-remote-file.sh +234 -0
- package/.agents/backend/fm-remote-home-provision.sh +259 -0
- package/.agents/backend/fm-remote-home-seed.sh +259 -0
- package/.agents/backend/fm-remote-inherit-push.sh +90 -0
- package/.agents/backend/fm-remote-inherit.sh +179 -0
- package/.agents/backend/fm-remote-job-lib.sh +1241 -0
- package/.agents/backend/fm-remote-job-reap-orphans.sh +137 -0
- package/.agents/backend/fm-remote-job-worker.sh +1124 -0
- package/.agents/backend/fm-remote-readiness-lib.sh +44 -0
- package/.agents/backend/fm-remote-secondmate-control.sh +340 -0
- package/.agents/backend/fm-review-diff.sh +158 -0
- package/.agents/backend/fm-secondmate-charter-lib.sh +44 -0
- package/.agents/backend/fm-secondmate-nudge-lib.sh +68 -0
- package/.agents/backend/fm-secondmate-parent-lib.sh +70 -0
- package/.agents/backend/fm-secondmate-reconcile.sh +371 -0
- package/.agents/backend/fm-secondmate-registry-lib.sh +311 -0
- package/.agents/backend/fm-secondmate-report.sh +88 -0
- package/.agents/backend/fm-send.sh +1059 -0
- package/.agents/backend/fm-session-lock-lib.sh +176 -0
- package/.agents/backend/fm-session-start.sh +970 -0
- package/.agents/backend/fm-sessionstart-cursor.sh +40 -0
- package/.agents/backend/fm-sessionstart-nudge.sh +45 -0
- package/.agents/backend/fm-sessionstart-run.sh +145 -0
- package/.agents/backend/fm-spawn.sh +3153 -0
- package/.agents/backend/fm-startup-memory-budget-lib.sh +224 -0
- package/.agents/backend/fm-startup-memory-budget.sh +94 -0
- package/.agents/backend/fm-startup-network.sh +642 -0
- package/.agents/backend/fm-stow-cascade.sh +251 -0
- package/.agents/backend/fm-subagent-pretool-check.sh +207 -0
- package/.agents/backend/fm-supervise-daemon.sh +1732 -0
- package/.agents/backend/fm-supervision-instructions.sh +215 -0
- package/.agents/backend/fm-supervision-lib.sh +90 -0
- package/.agents/backend/fm-supervisor-target-lib.sh +78 -0
- package/.agents/backend/fm-tangle-lib.sh +53 -0
- package/.agents/backend/fm-task-inbox-lib.sh +402 -0
- package/.agents/backend/fm-tasks-axi-lib.sh +123 -0
- package/.agents/backend/fm-teardown.sh +2922 -0
- package/.agents/backend/fm-test-isolation-proof.sh +588 -0
- package/.agents/backend/fm-test-run.sh +2191 -0
- package/.agents/backend/fm-timeout-lib.sh +141 -0
- package/.agents/backend/fm-timing-lib.sh +179 -0
- package/.agents/backend/fm-tmux-lib.sh +291 -0
- package/.agents/backend/fm-tool-update-check.sh +898 -0
- package/.agents/backend/fm-trace-context-lib.sh +227 -0
- package/.agents/backend/fm-transition-lib.sh +103 -0
- package/.agents/backend/fm-turnend-guard-cursor.sh +391 -0
- package/.agents/backend/fm-turnend-guard-grok.sh +90 -0
- package/.agents/backend/fm-turnend-guard.sh +432 -0
- package/.agents/backend/fm-update.sh +110 -0
- package/.agents/backend/fm-vendor-auth-probe.sh +190 -0
- package/.agents/backend/fm-voice-client.py +1373 -0
- package/.agents/backend/fm-voice-relay.py +1256 -0
- package/.agents/backend/fm-wake-drain.sh +592 -0
- package/.agents/backend/fm-wake-grant.sh +127 -0
- package/.agents/backend/fm-wake-lib.sh +1849 -0
- package/.agents/backend/fm-watch-arm.sh +602 -0
- package/.agents/backend/fm-watch-checkpoint.sh +109 -0
- package/.agents/backend/fm-watch.sh +1962 -0
- package/.agents/backend/fm-x-dismiss.sh +116 -0
- package/.agents/backend/fm-x-followup.sh +286 -0
- package/.agents/backend/fm-x-lib.sh +1000 -0
- package/.agents/backend/fm-x-link.sh +234 -0
- package/.agents/backend/fm-x-poll.sh +185 -0
- package/.agents/backend/fm-x-reply.sh +385 -0
- package/.agents/backend/fm_voice_frame.py +166 -0
- package/.agents/backend/fm_voice_records.py +574 -0
- package/.agents/backend/model-bridge.py +290 -0
- package/.agents/backend/opencode-go-bridge.py +30 -0
- package/.agents/bus/README.md +12 -0
- package/.agents/bus/messages.json +1 -0
- package/.agents/config/agents.machine.example.yaml +16 -0
- package/.agents/config/agents.yaml +50 -0
- package/.agents/config/agents.yaml.example +72 -0
- package/.agents/config/crew-dispatch.json +22 -0
- package/.agents/config/cron.yaml +9 -0
- package/.agents/config/eindri-dispatch.json +67 -0
- package/.agents/config/eindri-harness +1 -0
- package/.agents/config/startup-memory-budget +1 -0
- package/.agents/config/tailscale-sync.yaml +27 -0
- package/.agents/filebrowser/.filebrowser.json +6 -0
- package/.agents/filebrowser/README.md +11 -0
- package/.agents/gateway/README.md +12 -0
- package/.agents/github/README.md +15 -0
- package/.agents/github/webhooks/.gitkeep +0 -0
- package/.agents/github/workflows/.gitkeep +0 -0
- package/.agents/harness/opencode/plugins/lib/rodd-operational-input.js +37 -0
- package/.agents/harness/opencode/plugins/package.json +4 -0
- package/.agents/harness/opencode/plugins/saga-sessionstart.js +77 -0
- package/.agents/harness/opencode/plugins/syn-cd-check.js +61 -0
- package/.agents/harness/opencode/plugins/syn-pretool-check.js +68 -0
- package/.agents/harness/opencode/plugins/syn-turnend-guard.js +105 -0
- package/.agents/harness/opencode/plugins/syn-watch-arm.js +521 -0
- package/.agents/memory/README.md +46 -0
- package/.agents/memory/kaia.engram +0 -0
- package/.agents/memory/kaia.engram-shm +0 -0
- package/.agents/memory/kaia.engram-wal +0 -0
- package/.agents/memory/runes_audit.md +5 -0
- package/.agents/memory/well/episodes.jsonl +367 -0
- package/.agents/memory/well/workspace.jsonl +27 -0
- package/.agents/migrations/0001-hodd-layout.sh +38 -0
- package/.agents/migrations/0002-a2a-mcp.sh +27 -0
- package/.agents/migrations/0003-private-data-separation.sh +111 -0
- package/.agents/migrations/0004-hoard-and-realms.sh +104 -0
- package/.agents/sandbox/Dockerfile.utgard +19 -0
- package/.agents/sandbox/README.md +14 -0
- package/.agents/sandbox/utgard.config.json +14 -0
- package/.agents/skills/README.md +59 -0
- package/.agents/skills/bragi-marketing/README.md +3 -0
- package/.agents/skills/bragi-marketing/SKILL.md +82 -0
- package/.agents/skills/eindri-homes/README.md +3 -0
- package/.agents/skills/eindri-homes/SKILL.md +291 -0
- package/.agents/skills/eindri-homes/assets/control-plane.md +122 -0
- package/.agents/skills/eindri-homes/assets/dispatch.md +80 -0
- package/.agents/skills/frigg-consent/README.md +3 -0
- package/.agents/skills/frigg-consent/SKILL.md +56 -0
- package/.agents/skills/galdr-ymirsystem/README.md +3 -0
- package/.agents/skills/galdr-ymirsystem/SKILL.md +158 -0
- package/.agents/skills/galdr-ymirsystem/assets/README.md +74 -0
- package/.agents/skills/galdr-ymirsystem/assets/brokk-distro-runtime.md +390 -0
- package/.agents/skills/galdr-ymirsystem/assets/build-method.md +113 -0
- package/.agents/skills/galdr-ymirsystem/assets/build-tool-categories.md +178 -0
- package/.agents/skills/galdr-ymirsystem/assets/eindri-orchestration.md +518 -0
- package/.agents/skills/galdr-ymirsystem/assets/eindri-profiles.md +136 -0
- package/.agents/skills/galdr-ymirsystem/assets/harness-integration/README.md +673 -0
- package/.agents/skills/galdr-ymirsystem/assets/harness-integration/claude-code.md +197 -0
- package/.agents/skills/galdr-ymirsystem/assets/harness-integration/codex.md +191 -0
- package/.agents/skills/galdr-ymirsystem/assets/harness-integration/cursor.md +193 -0
- package/.agents/skills/galdr-ymirsystem/assets/harness-integration/opencode.md +262 -0
- package/.agents/skills/galdr-ymirsystem/assets/harness-integration/pi.md +295 -0
- package/.agents/skills/galdr-ymirsystem/assets/hlidskjalf-ui.md +496 -0
- package/.agents/skills/galdr-ymirsystem/assets/installation.md +571 -0
- package/.agents/skills/galdr-ymirsystem/assets/local-models/HOST-DATA.example.md +72 -0
- package/.agents/skills/galdr-ymirsystem/assets/local-models.md +274 -0
- package/.agents/skills/galdr-ymirsystem/assets/memory-well.md +161 -0
- package/.agents/skills/galdr-ymirsystem/assets/nornir-jobs.md +377 -0
- package/.agents/skills/galdr-ymirsystem/assets/norse-naming.md +325 -0
- package/.agents/skills/galdr-ymirsystem/assets/odrerir-hall.md +129 -0
- package/.agents/skills/galdr-ymirsystem/assets/pi-boot/einherjar-spawn.schema.json +81 -0
- package/.agents/skills/galdr-ymirsystem/assets/pi-boot/herdr-profile.toml +75 -0
- package/.agents/skills/galdr-ymirsystem/assets/pi-boot/pi-profile.yml +96 -0
- package/.agents/skills/galdr-ymirsystem/assets/pi-boot/supervision-tree.yml +163 -0
- package/.agents/skills/galdr-ymirsystem/assets/pi-boot-guide.md +58 -0
- package/.agents/skills/galdr-ymirsystem/assets/porting-upstream-to-norse.md +301 -0
- package/.agents/skills/galdr-ymirsystem/assets/principles.md +385 -0
- package/.agents/skills/galdr-ymirsystem/assets/reference-adoption.md +99 -0
- package/.agents/skills/galdr-ymirsystem/assets/registry.md +379 -0
- package/.agents/skills/galdr-ymirsystem/assets/runtime-compliance.md +463 -0
- package/.agents/skills/galdr-ymirsystem/assets/runtime-components.md +553 -0
- package/.agents/skills/galdr-ymirsystem/assets/smidja.md +175 -0
- package/.agents/skills/galdr-ymirsystem/schemas/toon-schemas.md +190 -0
- package/.agents/skills/galdr-ymirsystem/scripts/README.md +45 -0
- package/.agents/skills/galdr-ymirsystem/scripts/bench-one.sh +248 -0
- package/.agents/skills/galdr-ymirsystem/scripts/compliance-check.sh +360 -0
- package/.agents/skills/galdr-ymirsystem/scripts/gpu-sample.sh +43 -0
- package/.agents/skills/galdr-ymirsystem/scripts/stop-all.sh +34 -0
- package/.agents/skills/galdr-ymirsystem/scripts/toon-check.py +188 -0
- package/.agents/skills/gjallarhorn-relay/README.md +3 -0
- package/.agents/skills/gjallarhorn-relay/SKILL.md +283 -0
- package/.agents/skills/groa-update/SKILL.md +70 -0
- package/.agents/skills/hamr-adapters/README.md +3 -0
- package/.agents/skills/hamr-adapters/SKILL.md +123 -0
- package/.agents/skills/hamr-adapters/references/common/control-and-recovery.md +37 -0
- package/.agents/skills/hamr-adapters/references/common/dispatch.md +32 -0
- package/.agents/skills/hamr-adapters/references/common/model-and-effort.md +42 -0
- package/.agents/skills/hamr-adapters/references/common/primary-hooks.md +40 -0
- package/.agents/skills/hamr-adapters/references/harness/claude.md +55 -0
- package/.agents/skills/hamr-adapters/references/harness/codex.md +43 -0
- package/.agents/skills/hamr-adapters/references/harness/cursor.md +75 -0
- package/.agents/skills/hamr-adapters/references/harness/grok.md +69 -0
- package/.agents/skills/hamr-adapters/references/harness/kimi.md +51 -0
- package/.agents/skills/hamr-adapters/references/harness/muse.md +70 -0
- package/.agents/skills/hamr-adapters/references/harness/opencode.md +42 -0
- package/.agents/skills/hamr-adapters/references/harness/pi.md +56 -0
- package/.agents/skills/herdr-panes/README.md +3 -0
- package/.agents/skills/herdr-panes/SKILL.md +92 -0
- package/.agents/skills/herdr-panes/assets/herdr-backend.md +346 -0
- package/.agents/skills/herdr-panes/assets/tmux-backend.md +115 -0
- package/.agents/skills/hnoss-design/DESIGN.md +19 -0
- package/.agents/skills/hnoss-design/README.md +3 -0
- package/.agents/skills/hnoss-design/SKILL.md +83 -0
- package/.agents/skills/hvild-afk/README.md +3 -0
- package/.agents/skills/hvild-afk/SKILL.md +241 -0
- package/.agents/skills/jord-projects/README.md +3 -0
- package/.agents/skills/jord-projects/SKILL.md +92 -0
- package/.agents/skills/lifecycle/SKILL.md +56 -0
- package/.agents/skills/lifecycle/smoke_test.sh +52 -0
- package/.agents/skills/lifecycle/start.sh +21 -0
- package/.agents/skills/lifecycle/status.sh +35 -0
- package/.agents/skills/lifecycle/stop.sh +16 -0
- package/.agents/skills/muninn-stow/README.md +3 -0
- package/.agents/skills/muninn-stow/SKILL.md +309 -0
- package/.agents/skills/no-mistakes/SKILL.md +379 -0
- package/.agents/skills/nornir-schedule/README.md +3 -0
- package/.agents/skills/nornir-schedule/SKILL.md +25 -0
- package/.agents/skills/nornir-schedule/assets/events.md +137 -0
- package/.agents/skills/nornir-schedule/assets/quota.md +131 -0
- package/.agents/skills/nsr-compliance/README.md +3 -0
- package/.agents/skills/nsr-compliance/SKILL.md +29 -0
- package/.agents/skills/nsr-compliance/assets/nsr/README.md +40 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/RULES/README.md +15 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/RULES/git-workflow.md +18 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/RULES/security.md +22 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/RULES/tech-stack.md +8 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/SKILL.md +71 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/config/core_four.yaml +16 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/gates/check_danger.sh +102 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/gates/check_wiring.sh +69 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/gates/validate_code.sh +29 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/gates/verify_docs.py +66 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/generate.sh +109 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/harness/envelopes/result_envelope.json +14 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/harness/envelopes/task_envelope.json +20 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/harness/runner.py +303 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/telemetry/logger.py +112 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/templates/compliance/README.md +18 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/templates/compliance/config/README.md +10 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/templates/compliance/gates/README.md +12 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/templates/compliance/gates/check_env.sh +23 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/templates/compliance/gates/check_paths.sh +20 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/templates/compliance/gates/check_platform.sh +23 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/templates/compliance/harness/README.md +11 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/templates/compliance/harness/envelopes/README.md +10 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/templates/compliance/installer/README.md +10 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/templates/compliance/installer/install.sh +125 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/templates/compliance/telemetry/README.md +11 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/README.md +20 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/features/README.md +36 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/features/SKILL-template.md +14 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/git_ops/README.md +25 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/git_ops/SKILL.md +13 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/git_ops/create_branch.sh +6 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/git_ops/safe_commit.sh +9 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/git_ops/sync_upstream.sh +6 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/lifecycle/README.md +27 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/lifecycle/SKILL.md +15 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/lifecycle/smoke_test.sh +12 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/lifecycle/start.sh +19 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/lifecycle/status.sh +12 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/lifecycle/stop.sh +15 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/README.md +18 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/config/README.md +10 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/config/core_four.yaml +13 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/gates/README.md +12 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/gates/check_danger.sh +102 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/gates/check_env.sh +23 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/gates/check_paths.sh +20 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/gates/check_platform.sh +23 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/gates/check_wiring.sh +69 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/harness/README.md +11 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/harness/envelopes/README.md +10 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/harness/envelopes/result_envelope.json +14 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/harness/envelopes/task_envelope.json +20 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/harness/runner.py +303 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/installer/README.md +10 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/installer/install.sh +125 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/telemetry/README.md +11 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/telemetry/logger.py +112 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/BEST_PRACTICES/README.md +14 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/BEST_PRACTICES/api.md +17 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/BEST_PRACTICES/compliance-wiring.md +155 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/BEST_PRACTICES/frontend.md +15 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/CI_CD/README.md +11 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/CI_CD/deployment/README.md +23 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/CI_CD/deployment/clients/README.md +16 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/CI_CD/deployment/clients/template-README.md +11 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/CI_CD/deployment/deploy.sh +49 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/CI_CD/deployment/envs/README.md +12 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/CI_CD/deployment/envs/development.env.example +17 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/CI_CD/deployment/envs/production.env.example +17 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/CI_CD/deployment/envs/staging.env.example +17 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/CI_CD/deployment/tenants/README.md +11 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/CI_CD/deployment/tenants/template.env.example +8 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/DEVELOPER_SETUP/README.md +26 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/DEVELOPER_SETUP/agent.md +7 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/DEVELOPER_SETUP/developers/dev-1.md +20 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/DEVELOPER_SETUP/linux.md +9 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/DEVELOPER_SETUP/macos.md +10 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/DEVELOPER_SETUP/shared.md +19 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/DEVELOPER_SETUP/windows.md +14 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/HOSTING/README.md +25 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/HOSTING/_template.md +41 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/HOSTING/hosting-1.md +41 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/README.md +21 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/RUNBOOK/README.md +15 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/RUNBOOK/deployment.md +19 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/RUNBOOK/disaster-recovery.md +15 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/RUNBOOK/ops-tasks.md +14 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/features/README.md +16 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/project-skills.md +146 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/research/README.md +11 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/research/software-compliance-research.md +269 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/working-with-agents.md +260 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/root/AGENTS.md +38 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/root/ARCHITECTURE.md +36 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/root/BEST_PRACTICES.md +27 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/root/CI_CD.md +23 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/root/FEATURES.md +15 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/root/README.md +22 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/root/RULES.md +37 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/root/STRUCTURE.md +45 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/root/TECH_STACK.md +28 -0
- package/.agents/skills/nsr-compliance/assets/nsr/scaffold-spec.md +63 -0
- package/.agents/skills/nsr-compliance/assets/nsr/scripts/scaffold.sh +103 -0
- package/.agents/skills/nsr-compliance/assets/nsr/scripts/validate.sh +52 -0
- package/.agents/skills/nsr-compliance/assets/nsrcompliance/SKILL.md +71 -0
- package/.agents/skills/nsr-compliance/assets/nsrcompliance/config/core_four.yaml +16 -0
- package/.agents/skills/nsr-compliance/assets/nsrcompliance/gates/check_danger.sh +102 -0
- package/.agents/skills/nsr-compliance/assets/nsrcompliance/gates/check_wiring.sh +69 -0
- package/.agents/skills/nsr-compliance/assets/nsrcompliance/gates/validate_code.sh +29 -0
- package/.agents/skills/nsr-compliance/assets/nsrcompliance/gates/verify_docs.py +66 -0
- package/.agents/skills/nsr-compliance/assets/nsrcompliance/generate.sh +109 -0
- package/.agents/skills/nsr-compliance/assets/nsrcompliance/harness/envelopes/result_envelope.json +14 -0
- package/.agents/skills/nsr-compliance/assets/nsrcompliance/harness/envelopes/task_envelope.json +20 -0
- package/.agents/skills/nsr-compliance/assets/nsrcompliance/harness/runner.py +303 -0
- package/.agents/skills/nsr-compliance/assets/nsrcompliance/telemetry/logger.py +112 -0
- package/.agents/skills/nsr-compliance/assets/nsrcompliance/templates/compliance/README.md +18 -0
- package/.agents/skills/nsr-compliance/assets/nsrcompliance/templates/compliance/config/README.md +10 -0
- package/.agents/skills/nsr-compliance/assets/nsrcompliance/templates/compliance/gates/README.md +12 -0
- package/.agents/skills/nsr-compliance/assets/nsrcompliance/templates/compliance/gates/check_env.sh +23 -0
- package/.agents/skills/nsr-compliance/assets/nsrcompliance/templates/compliance/gates/check_paths.sh +20 -0
- package/.agents/skills/nsr-compliance/assets/nsrcompliance/templates/compliance/gates/check_platform.sh +23 -0
- package/.agents/skills/nsr-compliance/assets/nsrcompliance/templates/compliance/harness/README.md +11 -0
- package/.agents/skills/nsr-compliance/assets/nsrcompliance/templates/compliance/harness/envelopes/README.md +10 -0
- package/.agents/skills/nsr-compliance/assets/nsrcompliance/templates/compliance/installer/README.md +10 -0
- package/.agents/skills/nsr-compliance/assets/nsrcompliance/templates/compliance/installer/install.sh +125 -0
- package/.agents/skills/nsr-compliance/assets/nsrcompliance/templates/compliance/telemetry/README.md +11 -0
- package/.agents/skills/pr-ops/README.md +3 -0
- package/.agents/skills/pr-ops/SKILL.md +109 -0
- package/.agents/skills/ratatoskr-a2a/README.md +3 -0
- package/.agents/skills/ratatoskr-a2a/SKILL.md +148 -0
- package/.agents/skills/rules-check-drift/SKILL.md +81 -0
- package/.agents/skills/saga-bearings/README.md +3 -0
- package/.agents/skills/saga-bearings/SKILL.md +26 -0
- package/.agents/skills/saga-bearings/assets/bearings.md +161 -0
- package/.agents/skills/saga-bearings/assets/board-template.html +731 -0
- package/.agents/skills/saga-bearings/assets/recap.md +56 -0
- package/.agents/skills/syn-recovery/README.md +3 -0
- package/.agents/skills/syn-recovery/SKILL.md +54 -0
- package/.agents/skills/tyr-check/README.md +3 -0
- package/.agents/skills/tyr-check/SKILL.md +218 -0
- package/.agents/skills/urdh-hold/README.md +3 -0
- package/.agents/skills/urdh-hold/SKILL.md +25 -0
- package/.agents/skills/urdh-hold/assets/decisions.md +15 -0
- package/.agents/skills/urdh-hold/assets/hold.md +54 -0
- package/.agents/skills/vor-diagnostics/README.md +3 -0
- package/.agents/skills/vor-diagnostics/SKILL.md +68 -0
- package/.agents/skills/ymir-host/README.md +3 -0
- package/.agents/skills/ymir-host/SKILL.md +34 -0
- package/.agents/skills/ymir-host/assets/desktop.md +36 -0
- package/.agents/skills/ymir-host/assets/omarchy.md +297 -0
- package/.agents/skills/ymir-host/assets/runbooks.md +15 -0
- package/.agents/skills/ymir-host/assets/thjazi.md +220 -0
- package/.agents/skills/ymir-host/assets/update.md +64 -0
- package/.agents/tests/assets/board-render-harness.mjs +117 -0
- package/.agents/tests/cmux-test-safety.sh +51 -0
- package/.agents/tests/fixtures.sh +291 -0
- package/.agents/tests/fm-afk-inject-e2e.test.sh +428 -0
- package/.agents/tests/fm-afk-inject-herdr-e2e.test.sh +536 -0
- package/.agents/tests/fm-afk-launch.test.sh +961 -0
- package/.agents/tests/fm-afk-pi-herdr-return-e2e.test.sh +297 -0
- package/.agents/tests/fm-afk-return.test.sh +283 -0
- package/.agents/tests/fm-arm-pretool-check.test.sh +482 -0
- package/.agents/tests/fm-ask-user-authority.test.sh +42 -0
- package/.agents/tests/fm-backend-autodetect-smoke.test.sh +176 -0
- package/.agents/tests/fm-backend-cmux-smoke.test.sh +188 -0
- package/.agents/tests/fm-backend-cmux.test.sh +1166 -0
- package/.agents/tests/fm-backend-herdr-eventwait-smoke.test.sh +136 -0
- package/.agents/tests/fm-backend-herdr-eventwait.test.py +106 -0
- package/.agents/tests/fm-backend-herdr-focus-flash-e2e.test.sh +409 -0
- package/.agents/tests/fm-backend-herdr-launcher-workspace-e2e.test.sh +437 -0
- package/.agents/tests/fm-backend-herdr-presentation-e2e.test.sh +1424 -0
- package/.agents/tests/fm-backend-herdr-prune-safety-e2e.test.sh +180 -0
- package/.agents/tests/fm-backend-herdr-respawn-idem-e2e.test.sh +182 -0
- package/.agents/tests/fm-backend-herdr-smoke.test.sh +355 -0
- package/.agents/tests/fm-backend-herdr-workspace-per-home-e2e.test.sh +251 -0
- package/.agents/tests/fm-backend-herdr.test.sh +4606 -0
- package/.agents/tests/fm-backend-orca.test.sh +1357 -0
- package/.agents/tests/fm-backend-tmux-smoke.test.sh +173 -0
- package/.agents/tests/fm-backend-zellij-smoke.test.sh +205 -0
- package/.agents/tests/fm-backend-zellij.test.sh +1360 -0
- package/.agents/tests/fm-backend.test.sh +1142 -0
- package/.agents/tests/fm-backlog-atomicity.test.sh +2302 -0
- package/.agents/tests/fm-backlog-handoff.test.sh +1360 -0
- package/.agents/tests/fm-bearings-board-render.test.sh +135 -0
- package/.agents/tests/fm-bearings-board.test.sh +414 -0
- package/.agents/tests/fm-bearings-snapshot.test.sh +1990 -0
- package/.agents/tests/fm-bootstrap-network-parallel.test.sh +316 -0
- package/.agents/tests/fm-bootstrap.test.sh +1178 -0
- package/.agents/tests/fm-branch-supervision.test.sh +552 -0
- package/.agents/tests/fm-brief.test.sh +785 -0
- package/.agents/tests/fm-busy-adapter-wiring.test.sh +324 -0
- package/.agents/tests/fm-busy-state.test.sh +465 -0
- package/.agents/tests/fm-calm-pi-extension.test.sh +3979 -0
- package/.agents/tests/fm-captain-hold-lifecycle.test.sh +1187 -0
- package/.agents/tests/fm-cd-pretool-check.test.sh +400 -0
- package/.agents/tests/fm-check-unregister.test.sh +198 -0
- package/.agents/tests/fm-classify-corr-token.test.sh +543 -0
- package/.agents/tests/fm-classify-decision-key.test.sh +340 -0
- package/.agents/tests/fm-claude-stop-autoarm-live-e2e.test.sh +164 -0
- package/.agents/tests/fm-claude-stop-autoarm.test.sh +1190 -0
- package/.agents/tests/fm-cmux-claude-composer-live-e2e.test.sh +110 -0
- package/.agents/tests/fm-codex-continuity-live-e2e.test.sh +55 -0
- package/.agents/tests/fm-composer-ghost.test.sh +716 -0
- package/.agents/tests/fm-composer-lib.test.sh +666 -0
- package/.agents/tests/fm-composer-matrix-live-e2e.test.sh +220 -0
- package/.agents/tests/fm-control-herdr-smoke.test.sh +149 -0
- package/.agents/tests/fm-control-relaunch.test.sh +1530 -0
- package/.agents/tests/fm-control.test.sh +910 -0
- package/.agents/tests/fm-crew-state.test.sh +1610 -0
- package/.agents/tests/fm-cursor-harness.test.sh +404 -0
- package/.agents/tests/fm-cursor-primary-live-e2e.test.sh +217 -0
- package/.agents/tests/fm-cursor-primary.test.sh +708 -0
- package/.agents/tests/fm-daemon.test.sh +2740 -0
- package/.agents/tests/fm-documentation-audiences.test.sh +141 -0
- package/.agents/tests/fm-ensure-agents-md.test.sh +371 -0
- package/.agents/tests/fm-extension-binding.test.sh +2187 -0
- package/.agents/tests/fm-fleet-snapshot-view.test.sh +816 -0
- package/.agents/tests/fm-fleet-sync.test.sh +721 -0
- package/.agents/tests/fm-gate-refuse.test.sh +356 -0
- package/.agents/tests/fm-gitignore-config.test.sh +89 -0
- package/.agents/tests/fm-gotmp.test.sh +230 -0
- package/.agents/tests/fm-grok-continuity-live-e2e.test.sh +112 -0
- package/.agents/tests/fm-grok-harness.test.sh +117 -0
- package/.agents/tests/fm-grok-stop-live-e2e.test.sh +219 -0
- package/.agents/tests/fm-guard-stale-banner.test.sh +709 -0
- package/.agents/tests/fm-harness-adapter-instructions-live-e2e.test.sh +134 -0
- package/.agents/tests/fm-harness-adapter-references.test.sh +30 -0
- package/.agents/tests/fm-harness-liveness-drift-live-e2e.test.sh +151 -0
- package/.agents/tests/fm-herdr-lab.test.sh +243 -0
- package/.agents/tests/fm-herdr-session-cleanup-e2e.test.sh +146 -0
- package/.agents/tests/fm-herdr-session-cleanup.test.sh +329 -0
- package/.agents/tests/fm-herdr-submit-confirm-live-e2e.test.sh +125 -0
- package/.agents/tests/fm-herdr-version-floor-live-e2e.test.sh +144 -0
- package/.agents/tests/fm-home-summary-refresh.test.sh +971 -0
- package/.agents/tests/fm-inactive-reconcile.test.sh +485 -0
- package/.agents/tests/fm-kimi-harness.test.sh +687 -0
- package/.agents/tests/fm-lint-workflows.test.sh +532 -0
- package/.agents/tests/fm-lint.test.sh +1023 -0
- package/.agents/tests/fm-muse-harness.test.sh +932 -0
- package/.agents/tests/fm-muse-signals-live-e2e.test.sh +205 -0
- package/.agents/tests/fm-no-mistakes-required.test.sh +72 -0
- package/.agents/tests/fm-on.test.sh +507 -0
- package/.agents/tests/fm-opencode-primary-live-e2e.test.sh +357 -0
- package/.agents/tests/fm-operational-input.test.sh +160 -0
- package/.agents/tests/fm-peek-remote.test.sh +110 -0
- package/.agents/tests/fm-pending-reply.test.sh +1288 -0
- package/.agents/tests/fm-pi-branch-extension.test.sh +3182 -0
- package/.agents/tests/fm-pi-branch-live-e2e.test.sh +527 -0
- package/.agents/tests/fm-pi-primary-live-e2e.test.sh +345 -0
- package/.agents/tests/fm-pi-primary-types.test.sh +68 -0
- package/.agents/tests/fm-pi-watch-extension.test.sh +2843 -0
- package/.agents/tests/fm-pr-check-security.test.sh +2140 -0
- package/.agents/tests/fm-pr-merge.test.sh +2135 -0
- package/.agents/tests/fm-procevent-quota.test.sh +225 -0
- package/.agents/tests/fm-procevent-when.test.sh +406 -0
- package/.agents/tests/fm-procevent.test.sh +1745 -0
- package/.agents/tests/fm-project-origin.test.sh +110 -0
- package/.agents/tests/fm-public-followup.test.sh +2272 -0
- package/.agents/tests/fm-quota-array-dispatch-live-e2e.test.sh +526 -0
- package/.agents/tests/fm-quota-choose.test.sh +626 -0
- package/.agents/tests/fm-remote-backlog-handoff.test.sh +453 -0
- package/.agents/tests/fm-remote-doctor.test.sh +625 -0
- package/.agents/tests/fm-remote-entrypoint.test.sh +59 -0
- package/.agents/tests/fm-remote-job-orphan-reap.test.sh +205 -0
- package/.agents/tests/fm-remote-job.test.sh +768 -0
- package/.agents/tests/fm-remote-reply.test.sh +520 -0
- package/.agents/tests/fm-remote-secondmate-lifecycle-e2e.test.sh +1256 -0
- package/.agents/tests/fm-remote-secondmate-parent-binding.test.sh +326 -0
- package/.agents/tests/fm-remote-secondmate-trace-context.test.sh +310 -0
- package/.agents/tests/fm-remote-transport-lanes.test.sh +431 -0
- package/.agents/tests/fm-review-diff.test.sh +176 -0
- package/.agents/tests/fm-secondmate-harness.test.sh +2600 -0
- package/.agents/tests/fm-secondmate-lifecycle-e2e.test.sh +259 -0
- package/.agents/tests/fm-secondmate-liveness.test.sh +559 -0
- package/.agents/tests/fm-secondmate-reconcile.test.sh +732 -0
- package/.agents/tests/fm-secondmate-safety.test.sh +2972 -0
- package/.agents/tests/fm-secondmate-sync.test.sh +913 -0
- package/.agents/tests/fm-send-inbox-doorbell-live-e2e.test.sh +206 -0
- package/.agents/tests/fm-send-inbox.test.sh +352 -0
- package/.agents/tests/fm-send-popup-settle.test.sh +167 -0
- package/.agents/tests/fm-send-remote-delivery.test.sh +796 -0
- package/.agents/tests/fm-send-resolve-key.test.sh +551 -0
- package/.agents/tests/fm-send-secondmate-marker-herdr-e2e.test.sh +187 -0
- package/.agents/tests/fm-send-secondmate-marker.test.sh +278 -0
- package/.agents/tests/fm-send-settle.test.sh +147 -0
- package/.agents/tests/fm-send-strict.test.sh +241 -0
- package/.agents/tests/fm-session-lock-ancestry.test.sh +365 -0
- package/.agents/tests/fm-session-start.test.sh +2515 -0
- package/.agents/tests/fm-sessionstart-hook-live-e2e.test.sh +631 -0
- package/.agents/tests/fm-sessionstart-instruction-refresh-live-e2e.test.sh +230 -0
- package/.agents/tests/fm-sessionstart-nudge.test.sh +1039 -0
- package/.agents/tests/fm-shared-captain-inheritance.test.sh +404 -0
- package/.agents/tests/fm-spawn-batch.test.sh +148 -0
- package/.agents/tests/fm-spawn-dispatch-profile.test.sh +827 -0
- package/.agents/tests/fm-spawn-pool-base-freshen.test.sh +440 -0
- package/.agents/tests/fm-spawn-worktree-settle.test.sh +147 -0
- package/.agents/tests/fm-startup-memory-budget.test.sh +333 -0
- package/.agents/tests/fm-startup-network.test.sh +712 -0
- package/.agents/tests/fm-stow-cascade.test.sh +370 -0
- package/.agents/tests/fm-subagent-pretool-check.test.sh +291 -0
- package/.agents/tests/fm-supervision-events.test.sh +156 -0
- package/.agents/tests/fm-supervision-instructions.test.sh +189 -0
- package/.agents/tests/fm-tangle-guard.test.sh +277 -0
- package/.agents/tests/fm-task-delivery.test.sh +420 -0
- package/.agents/tests/fm-task-inbox.test.sh +513 -0
- package/.agents/tests/fm-teardown-endpoint-safety.test.sh +374 -0
- package/.agents/tests/fm-teardown.test.sh +2665 -0
- package/.agents/tests/fm-test-fixture-cleanup.test.sh +172 -0
- package/.agents/tests/fm-test-fixtures.test.sh +132 -0
- package/.agents/tests/fm-test-isolation-proof.test.sh +278 -0
- package/.agents/tests/fm-test-run.test.sh +1196 -0
- package/.agents/tests/fm-tmux-agent-liveness.test.sh +354 -0
- package/.agents/tests/fm-tmux-submit-busy.test.sh +340 -0
- package/.agents/tests/fm-tool-update-check.test.sh +1044 -0
- package/.agents/tests/fm-trace-context-lib.test.sh +253 -0
- package/.agents/tests/fm-trace-context-spawn.test.sh +581 -0
- package/.agents/tests/fm-transition-lib.test.sh +54 -0
- package/.agents/tests/fm-turnend-guard.test.sh +1822 -0
- package/.agents/tests/fm-update.test.sh +304 -0
- package/.agents/tests/fm-vendor-auth-probe.test.sh +395 -0
- package/.agents/tests/fm-voice-relay.test.sh +4741 -0
- package/.agents/tests/fm-wake-daemon-lifecycle-e2e.test.sh +171 -0
- package/.agents/tests/fm-wake-drain-open-decisions-cursor.test.sh +356 -0
- package/.agents/tests/fm-wake-drain-open-decisions.test.sh +226 -0
- package/.agents/tests/fm-wake-drain-unread-status.test.sh +379 -0
- package/.agents/tests/fm-wake-queue.test.sh +1224 -0
- package/.agents/tests/fm-watch-arm.test.sh +821 -0
- package/.agents/tests/fm-watch-checkpoint.test.sh +87 -0
- package/.agents/tests/fm-watch-recovery-loop.test.sh +223 -0
- package/.agents/tests/fm-watch-triage.test.sh +3889 -0
- package/.agents/tests/fm-watcher-lock.test.sh +1133 -0
- package/.agents/tests/fm-x-mode.test.sh +3042 -0
- package/.agents/tests/gleipnir-guard-pretool-check.test.sh +51 -0
- package/.agents/tests/gleipnir-machine-lock.test.sh +130 -0
- package/.agents/tests/herdr-test-safety.sh +42 -0
- package/.agents/tests/lib.sh +315 -0
- package/.agents/tests/remote-herdr-fixture.sh +125 -0
- package/.agents/tests/secondmate-helpers.sh +193 -0
- package/.agents/tests/smoke.test.sh +93 -0
- package/.agents/tests/wake-helpers.sh +338 -0
- package/.agents/tests/zellij-test-safety.sh +60 -0
- package/.agents/tools/README.md +10 -0
- package/.agents/tools/bin/tasks-cli +267 -0
- package/.agents/tools/tasks-cli.ts +216 -0
- package/AGENTS.md +454 -0
- package/CHANGELOG.md +881 -0
- package/LICENSE +1 -1
- package/NOTICE +63 -0
- package/README.md +113 -39
- package/RULES/01-domains.md +51 -0
- package/RULES/02-agents.md +78 -0
- package/RULES/03-houses.md +61 -0
- package/RULES/04-hoard.md +65 -0
- package/RULES/05-platforms.md +67 -0
- package/RULES/06-append-only.md +58 -0
- package/RULES/07-config.md +45 -0
- package/RULES/08-delivery-gate.md +75 -0
- package/RULES/README.md +20 -0
- package/bin/README.md +107 -0
- package/bin/a2a-mcp.sh +98 -0
- package/bin/a2a-serve.py +103 -0
- package/bin/a2a-serve.sh +8 -0
- package/bin/a2a-talk.sh +50 -0
- package/bin/a2abridge-ensure.sh +108 -0
- package/bin/agent-run.sh +43 -0
- package/bin/agents-config.sh +233 -0
- package/bin/apodex-smoke-test.sh +92 -0
- package/bin/bifrost-bridge.sh +112 -0
- package/bin/bifrost-ingress.sh +77 -0
- package/bin/bootstrap-macos.sh +107 -0
- package/bin/bootstrap-windows.ps1 +112 -0
- package/bin/branch-guard.sh +64 -0
- package/bin/brokk +73 -0
- package/bin/brokk-classify-lib.sh +1770 -0
- package/bin/brokk-lease-lib.sh +222 -0
- package/bin/brokk-lease.sh +193 -0
- package/bin/brokk-lint.sh +65 -0
- package/bin/brokk-send.sh +38 -0
- package/bin/brokk-update.sh +9 -0
- package/bin/brokk-wake-grant.sh +127 -0
- package/bin/brokk-wake-lib.sh +1849 -0
- package/bin/changelog-guard.sh +165 -0
- package/bin/crash-sense.sh +160 -0
- package/bin/desktop-place.sh +224 -0
- package/bin/docs-guard.sh +39 -0
- package/bin/editor-place.sh +207 -0
- package/bin/eindri-acclaim.sh +34 -0
- package/bin/eindri-control.sh +28 -0
- package/bin/eindri-role.sh +83 -0
- package/bin/eindri-seen.sh +41 -0
- package/bin/eindri-send.sh +15 -0
- package/bin/eindri-start.sh +85 -0
- package/bin/eindri-watch.sh +57 -0
- package/bin/einherjar-spawn.sh +576 -0
- package/bin/eir-doctor.sh +112 -0
- package/bin/erindi-brief.sh +271 -0
- package/bin/fleet-apply.sh +68 -0
- package/bin/github-deploy.sh +81 -0
- package/bin/gjallarhorn-expose.sh +102 -0
- package/bin/gjallarhorn-notify.sh +58 -0
- package/bin/gjallarhorn-purge.sh +54 -0
- package/bin/gjallarhorn-tunnel.sh +72 -0
- package/bin/gleipnir-lock-lib.sh +284 -0
- package/bin/groa-update.sh +122 -0
- package/bin/hall-snapshot.sh +92 -0
- package/bin/hamr-harness.sh +246 -0
- package/bin/heimdall-ssh-keys.sh +374 -0
- package/bin/herdr-agents.py +44 -0
- package/bin/herdr-ensure.sh +104 -0
- package/bin/herdr-run.sh +431 -0
- package/bin/hermes-ensure.sh +88 -0
- package/bin/hoard-lib.sh +25 -0
- package/bin/hodd.sh +78 -0
- package/bin/host-sense.sh +199 -0
- package/bin/huginn-research-worker.sh +108 -0
- package/bin/local-model-lock.sh +51 -0
- package/bin/mimir-bridge.py +226 -0
- package/bin/mimir-bridge.sh +87 -0
- package/bin/mimir-ingest.sh +131 -0
- package/bin/mimir.sh +113 -0
- package/bin/mjollnir-webhook.sh +59 -0
- package/bin/mjollnir.sh +85 -0
- package/bin/model-hardware.sh +82 -0
- package/bin/model-resolve.sh +147 -0
- package/bin/models-detect.sh +178 -0
- package/bin/nornir-cron-start.sh +162 -0
- package/bin/nornir-job-daily-briefing.sh +191 -0
- package/bin/nornir-job-git-sync.sh +152 -0
- package/bin/nornir-job-memory-housekeeping.sh +114 -0
- package/bin/nornir-job-observer.sh +166 -0
- package/bin/nornir-job-skillopt-sleep.sh +76 -0
- package/bin/omarchy-hook-install.sh +60 -0
- package/bin/omarchy-install.sh +148 -0
- package/bin/omarchy-plugins.sh +174 -0
- package/bin/omarchy-sense.sh +149 -0
- package/bin/perm-guard.sh +30 -0
- package/bin/pi-agent.sh +68 -0
- package/bin/pi-ensure.sh +77 -0
- package/bin/pi-local.sh +37 -0
- package/bin/pi-seat.sh +63 -0
- package/bin/prereq-ensure.sh +166 -0
- package/bin/private-guard.sh +38 -0
- package/bin/project-git.sh +55 -0
- package/bin/public-guard.sh +70 -0
- package/bin/ratatoskr.sh +49 -0
- package/bin/realm-lib.sh +32 -0
- package/bin/repo-scrub.sh +62 -0
- package/bin/rodd-operational-input.sh +250 -0
- package/bin/runes-append.sh +198 -0
- package/bin/saga-session-start.sh +172 -0
- package/bin/saga-sessionstart-run.sh +79 -0
- package/bin/saga-wake-drain.sh +41 -0
- package/bin/secret-guard.sh +63 -0
- package/bin/sessrumnir-ensure.sh +100 -0
- package/bin/sessrumnir-sync.sh +237 -0
- package/bin/sessrumnir.sh +91 -0
- package/bin/skillopt-setup.sh +76 -0
- package/bin/skuld-branch-outcome.sh +203 -0
- package/bin/skuld-branch-prompt.sh +101 -0
- package/bin/smidja-bootstrap.sh +90 -0
- package/bin/smidja-observe.sh +86 -0
- package/bin/syn-arm-pretool-check.sh +57 -0
- package/bin/syn-asset-pretool-check.sh +76 -0
- package/bin/syn-cd-pretool-check.sh +28 -0
- package/bin/syn-guard-pretool-check.sh +83 -0
- package/bin/syn-turnend-guard.sh +36 -0
- package/bin/syn-watch-arm.sh +99 -0
- package/bin/tailscale-sync.sh +190 -0
- package/bin/telegram-bot.sh +69 -0
- package/bin/toolchain.sh +74 -0
- package/bin/utgard.sh +111 -0
- package/bin/valhalla.sh +115 -0
- package/bin/valknut-load.sh +260 -0
- package/bin/vor-crew-state.sh +124 -0
- package/bin/wedge-notify.sh +47 -0
- package/bin/workspace-provision.sh +80 -0
- package/bin/workspace-rag.sh +150 -0
- package/bin/wyrd-db.sh +47 -0
- package/bin/yggdrasil.sh +152 -0
- package/bin/ymir-edit.sh +145 -0
- package/bin/ymir-install.sh +641 -0
- package/bin/ymir-invite.sh +93 -0
- package/bin/ymir-isolation.sh +88 -0
- package/bin/ymir-migrate.sh +61 -0
- package/bin/ymir-platform.sh +348 -0
- package/bin/ymir-say.sh +97 -0
- package/bin/ymir-setup-auth.sh +108 -0
- package/bin/ymir-validate.sh +173 -0
- package/bin/ymir.js +74 -16
- package/docs/Architecture.md +339 -0
- package/docs/design.md +351 -0
- package/docs/installations/README.md +63 -0
- package/docs/installations/ymir-home.md +93 -0
- package/docs/integration.md +105 -0
- package/docs/lore.md +1056 -0
- package/docs/research/agent-harnesses.md +290 -0
- package/docs/runbooks/README.md +19 -0
- package/docs/runbooks/a2a.md +91 -0
- package/docs/runbooks/agent-permissions.md +99 -0
- package/docs/runbooks/agents.md +57 -0
- package/docs/runbooks/models.md +69 -0
- package/docs/runbooks/realm-onboarding.md +60 -0
- package/docs/runbooks/secrets-and-hoard.md +60 -0
- package/docs/runbooks/updates-and-migrations.md +110 -0
- package/docs/runbooks/worktrees-isolation.md +72 -0
- package/docs/session-start.md +105 -0
- package/docs/skillopt-integration.md +321 -0
- package/docs/sync-system.md +66 -0
- package/docs/workspaces.md +104 -0
- package/docs/ymir-rut.md +260 -0
- package/package.json +44 -14
- package/packaging/build.sh +74 -0
- package/packaging/macos/Ymir Installer.command +43 -0
- package/packaging/windows/ymir-setup.nsi +74 -0
- package/scripts/README.md +22 -0
- package/scripts/electron.sh +210 -0
- package/scripts/start.sh +167 -0
- package/scripts/stop.sh +113 -0
- package/install.sh +0 -35
|
@@ -0,0 +1,2972 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# tests/fm-secondmate-safety.test.sh - secondmate home safety invariants:
|
|
3
|
+
# the path-boundary matrices (seed/spawn/teardown), registry/charter/origin
|
|
4
|
+
# validation, treehouse lease handling, no-mistakes initialization of new
|
|
5
|
+
# clones, child-worktree protection, and backlog-handoff safety. The happy-path
|
|
6
|
+
# operator flow lives in fm-secondmate-lifecycle-e2e.test.sh; this file keeps the
|
|
7
|
+
# destructive-invariant coverage that an e2e run cannot deterministically reach.
|
|
8
|
+
set -u
|
|
9
|
+
|
|
10
|
+
# shellcheck source=tests/secondmate-helpers.sh disable=SC1091
|
|
11
|
+
. "$(dirname "${BASH_SOURCE[0]}")/secondmate-helpers.sh"
|
|
12
|
+
|
|
13
|
+
TMP_ROOT=$(fm_test_tmproot fm-secondmate-safety)
|
|
14
|
+
export FM_BACKEND=tmux
|
|
15
|
+
|
|
16
|
+
file_mode() {
|
|
17
|
+
if [ "$(uname)" = Darwin ]; then
|
|
18
|
+
stat -f %Lp "$1"
|
|
19
|
+
else
|
|
20
|
+
stat -c %a "$1"
|
|
21
|
+
fi
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
install_fake_process_event_sweep() {
|
|
25
|
+
local home=$1 log=$2
|
|
26
|
+
mkdir -p "$home/bin"
|
|
27
|
+
cat > "$home/bin/fm-procevent.sh" <<'SH'
|
|
28
|
+
#!/usr/bin/env bash
|
|
29
|
+
set -eu
|
|
30
|
+
case "${1:-}" in
|
|
31
|
+
sweep-home)
|
|
32
|
+
if [ "${2:-}" = --preflight ]; then
|
|
33
|
+
exit 0
|
|
34
|
+
fi
|
|
35
|
+
[ "$#" -eq 1 ] || exit 2
|
|
36
|
+
printf '%s\n' "$FM_HOME" >> "$FM_FAKE_PROCEVENT_SWEEP_LOG"
|
|
37
|
+
rm -f -- "$FM_HOME"/state/procevent/*.source "$FM_HOME"/state/procevent/*.runner
|
|
38
|
+
;;
|
|
39
|
+
reconcile)
|
|
40
|
+
printf '%s\n' "$FM_HOME" >> "$FM_FAKE_PROCEVENT_REARM_LOG"
|
|
41
|
+
[ -z "${FM_FAKE_PROCEVENT_REARM_FAIL:-}" ] || exit 1
|
|
42
|
+
;;
|
|
43
|
+
*) exit 2 ;;
|
|
44
|
+
esac
|
|
45
|
+
SH
|
|
46
|
+
chmod +x "$home/bin/fm-procevent.sh"
|
|
47
|
+
: > "$log"
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
test_fm_home_parameterization() {
|
|
51
|
+
local brief home_one home_two out
|
|
52
|
+
home_one="$TMP_ROOT/home one"
|
|
53
|
+
home_two="$TMP_ROOT/home-two"
|
|
54
|
+
mkdir -p "$home_one/data" "$home_one/state" "$home_two/data" "$home_two/state"
|
|
55
|
+
printf '%s\n' '- app [local-only +yolo] - test app (added 2026-06-22)' > "$home_one/data/projects.md"
|
|
56
|
+
|
|
57
|
+
out=$(FM_HOME="$home_one" "$ROOT/bin/fm-project-mode.sh" app)
|
|
58
|
+
[ "$out" = "local-only on" ] || fail "fm-project-mode did not read projects.md from FM_HOME"
|
|
59
|
+
out=$(FM_HOME="$home_two" "$ROOT/bin/fm-project-mode.sh" app 2>/dev/null)
|
|
60
|
+
[ "$out" = "no-mistakes off" ] || fail "fm-project-mode did not isolate missing registry by home"
|
|
61
|
+
|
|
62
|
+
FM_HOME="$home_one" "$ROOT/bin/fm-brief.sh" task-a app --mode no-mistakes >/dev/null || fail "brief scaffold failed under FM_HOME"
|
|
63
|
+
brief="$home_one/data/task-a/brief.md"
|
|
64
|
+
[ -f "$brief" ] || fail "brief was not written under FM_HOME/data"
|
|
65
|
+
grep -F ">> '$home_one/state/task-a.status'" "$brief" >/dev/null || fail "brief did not shell-quote FM_HOME state path"
|
|
66
|
+
|
|
67
|
+
FM_HOME="$home_one" "$ROOT/bin/fm-brief.sh" task-b app --scout >/dev/null || fail "scout brief scaffold failed under FM_HOME"
|
|
68
|
+
brief="$home_one/data/task-b/brief.md"
|
|
69
|
+
grep -F ">> '$home_one/state/task-b.status'" "$brief" >/dev/null || fail "scout brief did not shell-quote FM_HOME state path"
|
|
70
|
+
|
|
71
|
+
FM_HOME="$home_one" FM_SECONDMATE_CHARTER='ops domain' "$ROOT/bin/fm-brief.sh" task-c --secondmate app >/dev/null \
|
|
72
|
+
|| fail "secondmate brief scaffold failed under FM_HOME"
|
|
73
|
+
brief="$home_one/data/task-c/brief.md"
|
|
74
|
+
grep -F ">> '$home_one/state/task-c.status'" "$brief" >/dev/null || fail "secondmate brief did not shell-quote FM_HOME state path"
|
|
75
|
+
|
|
76
|
+
printf 'project=x\n' > "$home_one/state/task-a.meta"
|
|
77
|
+
FM_HOME="$home_one" FM_GUARD_GRACE=999999 "$ROOT/bin/fm-pr-check.sh" task-a https://github.com/example/repo/pull/1 >/dev/null 2>/dev/null \
|
|
78
|
+
|| fail "fm-pr-check failed under FM_HOME"
|
|
79
|
+
[ -f "$home_one/state/task-a.check.sh" ] || fail "pr check was not written under FM_HOME/state"
|
|
80
|
+
[ ! -e "$home_two/state/task-a.check.sh" ] || fail "pr check leaked into another home"
|
|
81
|
+
pass "FM_HOME parameterizes data and state paths"
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
test_lock_status_is_per_home() {
|
|
85
|
+
local home_one home_two out
|
|
86
|
+
home_one="$TMP_ROOT/lock-one"
|
|
87
|
+
home_two="$TMP_ROOT/lock-two"
|
|
88
|
+
mkdir -p "$home_one/state" "$home_two/state"
|
|
89
|
+
printf '999999\n' > "$home_one/state/.lock"
|
|
90
|
+
out=$(FM_HOME="$home_one" "$ROOT/bin/fm-lock.sh" status)
|
|
91
|
+
printf '%s\n' "$out" | grep -F 'lock: stale' >/dev/null || fail "home one lock status did not read its own lock"
|
|
92
|
+
out=$(FM_HOME="$home_two" "$ROOT/bin/fm-lock.sh" status)
|
|
93
|
+
[ "$out" = "lock: free" ] || fail "home two lock status was affected by home one"
|
|
94
|
+
pass "fm-lock status is scoped per home"
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
test_seed_allows_overlapping_clones_and_drops_owner() {
|
|
98
|
+
# A project may appear in several secondmates' (non-exclusive) clone lists; the
|
|
99
|
+
# registry never uses the legacy owns: field, and the removed `owner` subcommand
|
|
100
|
+
# stays gone. The full happy seed - charter copied, clones+origins, no-mistakes
|
|
101
|
+
# init, modes preserved - is asserted by fm-secondmate-lifecycle-e2e.
|
|
102
|
+
local home design other
|
|
103
|
+
home="$TMP_ROOT/overlap-main"
|
|
104
|
+
design="$TMP_ROOT/overlap-design"
|
|
105
|
+
other="$TMP_ROOT/overlap-other"
|
|
106
|
+
mkdir -p "$home/projects" "$home/data" "$home/state"
|
|
107
|
+
fm_git_init_commit "$home/projects/alpha"
|
|
108
|
+
fm_git_init_commit "$home/projects/beta"
|
|
109
|
+
fm_git_add_origin "$home/projects/alpha" "$TMP_ROOT/remotes/seed-overlap-alpha.git"
|
|
110
|
+
fm_git_add_origin "$home/projects/beta" "$TMP_ROOT/remotes/seed-overlap-beta.git"
|
|
111
|
+
cat > "$home/data/projects.md" <<EOF
|
|
112
|
+
- alpha [direct-PR] - alpha project (added 2026-06-22)
|
|
113
|
+
- beta [direct-PR] - beta project (added 2026-06-22)
|
|
114
|
+
EOF
|
|
115
|
+
|
|
116
|
+
FM_HOME="$home" FM_SECONDMATE_CHARTER='feature design for alpha beta' \
|
|
117
|
+
FM_SECONDMATE_SCOPE='feature design for alpha beta' \
|
|
118
|
+
"$ROOT/bin/fm-home-seed.sh" design "$design" alpha beta >/dev/null \
|
|
119
|
+
|| fail "initial seed failed"
|
|
120
|
+
assert_grep '- design - feature design for alpha beta' "$home/data/secondmates.md" "design registry line missing"
|
|
121
|
+
assert_grep 'projects: alpha, beta' "$home/data/secondmates.md" "design project clone list missing"
|
|
122
|
+
assert_no_grep 'owns:' "$home/data/secondmates.md" "registry used the legacy owns field"
|
|
123
|
+
|
|
124
|
+
# beta is shared with a second secondmate of a different scope (overlap allowed).
|
|
125
|
+
FM_HOME="$home" FM_SECONDMATE_CHARTER='issue triage for beta' \
|
|
126
|
+
FM_SECONDMATE_SCOPE='issue triage for beta' \
|
|
127
|
+
"$ROOT/bin/fm-home-seed.sh" other "$other" beta >/dev/null 2>&1 \
|
|
128
|
+
|| fail "seed refused overlapping project clones across different scopes"
|
|
129
|
+
assert_grep '- other - issue triage for beta' "$home/data/secondmates.md" "overlapping registry line missing"
|
|
130
|
+
FM_HOME="$home" "$ROOT/bin/fm-home-seed.sh" validate >/dev/null || fail "registry validation rejected overlapping clones"
|
|
131
|
+
|
|
132
|
+
if FM_HOME="$home" "$ROOT/bin/fm-home-seed.sh" owner alpha >/dev/null 2>&1; then
|
|
133
|
+
fail "owner subcommand still succeeded after routing moved to scopes"
|
|
134
|
+
fi
|
|
135
|
+
pass "seed allows overlapping project clone lists and drops the owns/owner routing"
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
test_home_seed_validate_rejects_unparseable_registry_entry() {
|
|
139
|
+
local home err
|
|
140
|
+
home="$TMP_ROOT/unparseable-registry-home"
|
|
141
|
+
err="$TMP_ROOT/unparseable-registry.err"
|
|
142
|
+
mkdir -p "$home/data"
|
|
143
|
+
printf '%s\n' '- broken - prose (home: /tmp/child; scope: missing projects and date)' > "$home/data/secondmates.md"
|
|
144
|
+
if FM_HOME="$home" "$ROOT/bin/fm-home-seed.sh" validate >/dev/null 2>"$err"; then
|
|
145
|
+
fail "home-seed validation accepted an operationally unparseable registry record"
|
|
146
|
+
fi
|
|
147
|
+
grep -F 'malformed secondmate registry entry' "$err" >/dev/null \
|
|
148
|
+
|| fail "home-seed validation did not explain the malformed registry record"
|
|
149
|
+
pass "home-seed validation rejects registry records no operational parser can consume"
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
test_home_seed_refuses_broken_registry_symlink() {
|
|
153
|
+
local home sub err target
|
|
154
|
+
home="$TMP_ROOT/broken-registry-symlink-home"
|
|
155
|
+
sub="$TMP_ROOT/broken-registry-symlink-subhome"
|
|
156
|
+
err="$TMP_ROOT/broken-registry-symlink.err"
|
|
157
|
+
target="$home/data/missing-secondmates.md"
|
|
158
|
+
mkdir -p "$home/data" "$home/state" "$home/projects"
|
|
159
|
+
ln -s "$target" "$home/data/secondmates.md"
|
|
160
|
+
if FM_HOME="$home" "$ROOT/bin/fm-home-seed.sh" validate >/dev/null 2>"$err"; then
|
|
161
|
+
fail "home-seed validation accepted a broken registry symlink"
|
|
162
|
+
fi
|
|
163
|
+
grep -F 'secondmate registry is unavailable or unsafe' "$err" >/dev/null \
|
|
164
|
+
|| fail "home-seed validation did not explain the broken registry symlink"
|
|
165
|
+
if FM_HOME="$home" FM_SECONDMATE_CHARTER='design domain' \
|
|
166
|
+
"$ROOT/bin/fm-home-seed.sh" design "$sub" alpha >/dev/null 2>"$err"; then
|
|
167
|
+
fail "home seeding accepted a broken registry symlink"
|
|
168
|
+
fi
|
|
169
|
+
[ -L "$home/data/secondmates.md" ] || fail "home seeding replaced the broken registry symlink"
|
|
170
|
+
[ ! -e "$target" ] || fail "home seeding wrote through the broken registry symlink"
|
|
171
|
+
[ ! -e "$sub" ] || fail "home seeding provisioned a home before broken registry refusal"
|
|
172
|
+
[ ! -e "$home/data/design" ] || fail "home seeding created a brief before broken registry refusal"
|
|
173
|
+
pass "home seeding refuses broken registry symlinks before provisioning"
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
test_home_seed_refuses_unreadable_registry() {
|
|
177
|
+
local home sub err registry
|
|
178
|
+
home="$TMP_ROOT/unreadable-registry-home"
|
|
179
|
+
sub="$TMP_ROOT/unreadable-registry-subhome"
|
|
180
|
+
err="$TMP_ROOT/unreadable-registry.err"
|
|
181
|
+
registry="$home/data/secondmates.md"
|
|
182
|
+
mkdir -p "$home/data" "$home/state" "$home/projects"
|
|
183
|
+
printf '%s\n' '- design - design domain (home: /tmp/design; scope: design; projects: alpha; added 2026-07-30)' > "$registry"
|
|
184
|
+
chmod 000 "$registry"
|
|
185
|
+
if FM_HOME="$home" "$ROOT/bin/fm-home-seed.sh" validate >/dev/null 2>"$err"; then
|
|
186
|
+
chmod 600 "$registry"
|
|
187
|
+
fail "home-seed validation accepted an unreadable registry"
|
|
188
|
+
fi
|
|
189
|
+
grep -F 'secondmate registry is unavailable or unsafe' "$err" >/dev/null || {
|
|
190
|
+
chmod 600 "$registry"
|
|
191
|
+
fail "home-seed validation did not explain the unreadable registry"
|
|
192
|
+
}
|
|
193
|
+
if FM_HOME="$home" FM_SECONDMATE_CHARTER='design domain' \
|
|
194
|
+
"$ROOT/bin/fm-home-seed.sh" design "$sub" alpha >/dev/null 2>"$err"; then
|
|
195
|
+
chmod 600 "$registry"
|
|
196
|
+
fail "home seeding accepted an unreadable registry"
|
|
197
|
+
fi
|
|
198
|
+
chmod 600 "$registry"
|
|
199
|
+
[ ! -e "$sub" ] || fail "home seeding provisioned a home before unreadable registry refusal"
|
|
200
|
+
[ ! -e "$home/data/design" ] || fail "home seeding created a brief before unreadable registry refusal"
|
|
201
|
+
pass "home seeding refuses unreadable registries before provisioning"
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
test_home_seed_validate_rejects_duplicate_homes() {
|
|
205
|
+
local home subhome subhome_abs err
|
|
206
|
+
home="$TMP_ROOT/duplicate-home"
|
|
207
|
+
subhome="$TMP_ROOT/duplicate-subhome"
|
|
208
|
+
err="$TMP_ROOT/duplicate-home.err"
|
|
209
|
+
mkdir -p "$home/data" "$subhome"
|
|
210
|
+
subhome_abs=$(cd "$subhome" && pwd -P)
|
|
211
|
+
cat > "$home/data/secondmates.md" <<EOF
|
|
212
|
+
- design - design domain mentions home: $TMP_ROOT/ignored-summary-home (home: $subhome_abs; scope: design work mentions home: $TMP_ROOT/ignored-scope-home; projects: alpha; added 2026-06-22)
|
|
213
|
+
- triage - triage domain (home: $subhome_abs; scope: issue triage; projects: beta; added 2026-06-22)
|
|
214
|
+
EOF
|
|
215
|
+
|
|
216
|
+
if FM_HOME="$home" "$ROOT/bin/fm-home-seed.sh" validate >/dev/null 2>"$err"; then
|
|
217
|
+
fail "registry validation accepted two secondmates with the same home"
|
|
218
|
+
fi
|
|
219
|
+
grep -F 'duplicate secondmate home assignment' "$err" >/dev/null \
|
|
220
|
+
|| fail "registry validation did not explain duplicate home assignment"
|
|
221
|
+
pass "home seed validation rejects duplicate home routes"
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
test_home_seed_validate_rejects_duplicate_ids() {
|
|
225
|
+
local home first second first_abs second_abs err
|
|
226
|
+
home="$TMP_ROOT/duplicate-id-home"
|
|
227
|
+
first="$TMP_ROOT/duplicate-id-first"
|
|
228
|
+
second="$TMP_ROOT/duplicate-id-second"
|
|
229
|
+
err="$TMP_ROOT/duplicate-id.err"
|
|
230
|
+
mkdir -p "$home/data" "$first" "$second"
|
|
231
|
+
first_abs=$(cd "$first" && pwd -P)
|
|
232
|
+
second_abs=$(cd "$second" && pwd -P)
|
|
233
|
+
cat > "$home/data/secondmates.md" <<EOF
|
|
234
|
+
- design - design domain (home: $first_abs; scope: design work; projects: alpha; added 2026-06-22)
|
|
235
|
+
- design - design domain (home: $second_abs; scope: design work; projects: beta; added 2026-06-22)
|
|
236
|
+
EOF
|
|
237
|
+
|
|
238
|
+
if FM_HOME="$home" "$ROOT/bin/fm-home-seed.sh" validate >/dev/null 2>"$err"; then
|
|
239
|
+
fail "registry validation accepted two homes for the same secondmate id"
|
|
240
|
+
fi
|
|
241
|
+
grep -F 'duplicate secondmate id assignment' "$err" >/dev/null \
|
|
242
|
+
|| fail "registry validation did not explain duplicate id assignment"
|
|
243
|
+
pass "home seed validation rejects duplicate id routes"
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
test_home_seed_validate_rejects_nested_homes() {
|
|
247
|
+
local home ancestor descendant ancestor_abs descendant_abs err
|
|
248
|
+
home="$TMP_ROOT/nested-home"
|
|
249
|
+
ancestor="$TMP_ROOT/nested-domain-a"
|
|
250
|
+
descendant="$ancestor/domain-b"
|
|
251
|
+
err="$TMP_ROOT/nested-home.err"
|
|
252
|
+
mkdir -p "$home/data" "$ancestor" "$descendant"
|
|
253
|
+
ancestor_abs=$(cd "$ancestor" && pwd -P)
|
|
254
|
+
descendant_abs=$(cd "$descendant" && pwd -P)
|
|
255
|
+
cat > "$home/data/secondmates.md" <<EOF
|
|
256
|
+
- design - design domain (home: $ancestor_abs; scope: design work; projects: alpha; added 2026-06-22)
|
|
257
|
+
- triage - triage domain (home: $descendant_abs; scope: issue triage; projects: beta; added 2026-06-22)
|
|
258
|
+
EOF
|
|
259
|
+
|
|
260
|
+
if FM_HOME="$home" "$ROOT/bin/fm-home-seed.sh" validate >/dev/null 2>"$err"; then
|
|
261
|
+
fail "registry validation accepted nested secondmate homes"
|
|
262
|
+
fi
|
|
263
|
+
grep -F 'overlapping secondmate home assignment' "$err" >/dev/null \
|
|
264
|
+
|| fail "registry validation did not explain nested home assignment"
|
|
265
|
+
pass "home seed validation rejects nested home routes"
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
test_home_seed_uses_treehouse_acquired_home() {
|
|
269
|
+
local home acquired acquired_abs fakebin log lease out
|
|
270
|
+
home="$TMP_ROOT/dash-home"
|
|
271
|
+
acquired="$TMP_ROOT/dash-acquired-home"
|
|
272
|
+
mkdir -p "$home/projects" "$home/data" "$home/state"
|
|
273
|
+
fm_git_init_commit "$home/projects/alpha"
|
|
274
|
+
fm_git_add_origin "$home/projects/alpha" "$TMP_ROOT/remotes/dash-alpha.git"
|
|
275
|
+
printf '%s\n' '- alpha [direct-PR] - alpha project (added 2026-06-22)' > "$home/data/projects.md"
|
|
276
|
+
git clone --quiet "$ROOT" "$acquired"
|
|
277
|
+
fakebin=$(make_fake_tmux "$TMP_ROOT/dash-fake")
|
|
278
|
+
log="$TMP_ROOT/dash-fake/tmux.log"
|
|
279
|
+
lease="$TMP_ROOT/dash-fake/lease"
|
|
280
|
+
|
|
281
|
+
out=$(PATH="$fakebin:$PATH" FM_HOME="$home" FM_FAKE_TREEHOUSE_HOME="$acquired" FM_FAKE_TMUX_LOG="$log" \
|
|
282
|
+
FM_FAKE_TREEHOUSE_LEASE_FILE="$lease" \
|
|
283
|
+
FM_SECONDMATE_CHARTER='dash acquired scope' FM_SECONDMATE_SCOPE='dash acquired scope' \
|
|
284
|
+
"$ROOT/bin/fm-home-seed.sh" dash - alpha) \
|
|
285
|
+
|| fail "seed failed for a treehouse-acquired home"
|
|
286
|
+
acquired_abs=$(cd "$acquired" && pwd -P)
|
|
287
|
+
printf '%s\n' "$out" | grep -F "home=$acquired_abs" >/dev/null || fail "seed did not report acquired home"
|
|
288
|
+
grep -F 'treehouse get --lease --lease-holder dash' "$log" >/dev/null || fail "seed did not durably lease a home under the secondmate id"
|
|
289
|
+
[ -f "$lease" ] || fail "seed did not record a treehouse lease"
|
|
290
|
+
[ "$(cat "$lease")" = dash ] || fail "seed did not set the lease holder to the secondmate id"
|
|
291
|
+
[ -f "$acquired/.fm-secondmate-home" ] || fail "seed did not mark acquired home"
|
|
292
|
+
[ "$(cat "$acquired/.fm-secondmate-home")" = dash ] || fail "seed wrote wrong acquired-home marker"
|
|
293
|
+
[ -d "$acquired/projects/alpha/.git" ] || fail "seed did not clone project into acquired home"
|
|
294
|
+
grep -F "home: $acquired_abs" "$home/data/secondmates.md" >/dev/null || fail "registry did not record acquired home"
|
|
295
|
+
pass "home seeding durably leases treehouse-acquired dash homes under the secondmate id"
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
test_home_seed_returns_treehouse_acquired_home_on_assignment_failure() {
|
|
299
|
+
local home acquired acquired_abs fakebin log err
|
|
300
|
+
home="$TMP_ROOT/dash-fail-home"
|
|
301
|
+
acquired="$TMP_ROOT/dash-fail-acquired-home"
|
|
302
|
+
err="$TMP_ROOT/dash-fail.err"
|
|
303
|
+
mkdir -p "$home/projects" "$home/data" "$home/state"
|
|
304
|
+
fm_git_init_commit "$home/projects/alpha"
|
|
305
|
+
fm_git_add_origin "$home/projects/alpha" "$TMP_ROOT/remotes/dash-fail-alpha.git"
|
|
306
|
+
printf '%s\n' '- alpha [direct-PR] - alpha project (added 2026-06-22)' > "$home/data/projects.md"
|
|
307
|
+
git clone --quiet "$ROOT" "$acquired"
|
|
308
|
+
acquired_abs=$(cd "$acquired" && pwd -P)
|
|
309
|
+
printf 'other\n' > "$acquired/.fm-secondmate-home"
|
|
310
|
+
fakebin=$(make_fake_tmux "$TMP_ROOT/dash-fail-fake")
|
|
311
|
+
log="$TMP_ROOT/dash-fail-fake/tmux.log"
|
|
312
|
+
|
|
313
|
+
if PATH="$fakebin:$PATH" FM_HOME="$home" FM_FAKE_TREEHOUSE_HOME="$acquired" FM_FAKE_TMUX_LOG="$log" \
|
|
314
|
+
FM_SECONDMATE_CHARTER='dash acquired scope' FM_SECONDMATE_SCOPE='dash acquired scope' \
|
|
315
|
+
"$ROOT/bin/fm-home-seed.sh" dash - alpha >/dev/null 2>"$err"; then
|
|
316
|
+
fail "seed reused an acquired home marked for another secondmate"
|
|
317
|
+
fi
|
|
318
|
+
grep -F 'already marked for other' "$err" >/dev/null || fail "seed did not explain acquired marked-home rejection"
|
|
319
|
+
grep -F "treehouse return --force $acquired_abs" "$log" >/dev/null \
|
|
320
|
+
|| fail "failed acquired seed did not return the home through treehouse"
|
|
321
|
+
if [ -f "$home/data/secondmates.md" ] && grep -F -- '- dash ' "$home/data/secondmates.md" >/dev/null; then
|
|
322
|
+
fail "failed acquired seed left a registry route"
|
|
323
|
+
fi
|
|
324
|
+
pass "home seeding returns rejected acquired homes through treehouse"
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
test_home_seed_warns_when_acquired_home_return_fails() {
|
|
328
|
+
local home acquired acquired_abs fakebin log err lease
|
|
329
|
+
home="$TMP_ROOT/dash-return-fail-home"
|
|
330
|
+
acquired="$TMP_ROOT/dash-return-fail-acquired-home"
|
|
331
|
+
err="$TMP_ROOT/dash-return-fail.err"
|
|
332
|
+
mkdir -p "$home/projects" "$home/data" "$home/state"
|
|
333
|
+
fm_git_init_commit "$home/projects/alpha"
|
|
334
|
+
fm_git_add_origin "$home/projects/alpha" "$TMP_ROOT/remotes/dash-return-fail-alpha.git"
|
|
335
|
+
printf '%s\n' '- alpha [direct-PR] - alpha project (added 2026-06-22)' > "$home/data/projects.md"
|
|
336
|
+
git clone --quiet "$ROOT" "$acquired"
|
|
337
|
+
acquired_abs=$(cd "$acquired" && pwd -P)
|
|
338
|
+
printf 'other\n' > "$acquired/.fm-secondmate-home"
|
|
339
|
+
fakebin=$(make_fake_tmux "$TMP_ROOT/dash-return-fail-fake")
|
|
340
|
+
log="$TMP_ROOT/dash-return-fail-fake/tmux.log"
|
|
341
|
+
lease="$TMP_ROOT/dash-return-fail-fake/lease"
|
|
342
|
+
|
|
343
|
+
if PATH="$fakebin:$PATH" FM_HOME="$home" FM_FAKE_TREEHOUSE_HOME="$acquired" FM_FAKE_TMUX_LOG="$log" \
|
|
344
|
+
FM_FAKE_TREEHOUSE_LEASE_FILE="$lease" FM_FAKE_TREEHOUSE_RETURN_FAIL=1 \
|
|
345
|
+
FM_SECONDMATE_CHARTER='dash acquired scope' FM_SECONDMATE_SCOPE='dash acquired scope' \
|
|
346
|
+
"$ROOT/bin/fm-home-seed.sh" dash - alpha >/dev/null 2>"$err"; then
|
|
347
|
+
fail "seed reused an acquired home after return failure setup"
|
|
348
|
+
fi
|
|
349
|
+
grep -F 'already marked for other' "$err" >/dev/null || fail "seed did not report original acquired-home rejection"
|
|
350
|
+
grep -F "warning: failed to return treehouse-acquired home $acquired_abs during seed rollback" "$err" >/dev/null \
|
|
351
|
+
|| fail "seed rollback did not warn when treehouse return failed"
|
|
352
|
+
[ -f "$lease" ] || fail "failed rollback return did not preserve lease evidence"
|
|
353
|
+
grep -F "treehouse return --force $acquired_abs" "$log" >/dev/null \
|
|
354
|
+
|| fail "failed rollback did not attempt to return the acquired home"
|
|
355
|
+
pass "home seed rollback warns when treehouse-acquired return fails"
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
test_home_seed_does_not_return_unsafe_acquired_home() {
|
|
359
|
+
local home descendant fakebin log err
|
|
360
|
+
home="$TMP_ROOT/dash-active-home"
|
|
361
|
+
descendant="$home/data/dash-descendant-home"
|
|
362
|
+
err="$TMP_ROOT/dash-active.err"
|
|
363
|
+
mkdir -p "$home/projects" "$home/data" "$home/state"
|
|
364
|
+
fm_git_init_commit "$home/projects/alpha"
|
|
365
|
+
fm_git_add_origin "$home/projects/alpha" "$TMP_ROOT/remotes/dash-active-alpha.git"
|
|
366
|
+
printf '%s\n' '- alpha [direct-PR] - alpha project (added 2026-06-22)' > "$home/data/projects.md"
|
|
367
|
+
fakebin=$(make_fake_tmux "$TMP_ROOT/dash-active-fake")
|
|
368
|
+
log="$TMP_ROOT/dash-active-fake/tmux.log"
|
|
369
|
+
|
|
370
|
+
if PATH="$fakebin:$PATH" FM_HOME="$home" FM_FAKE_TREEHOUSE_HOME="$home" FM_FAKE_TMUX_LOG="$log" \
|
|
371
|
+
"$ROOT/bin/fm-home-seed.sh" dash - alpha >/dev/null 2>"$err"; then
|
|
372
|
+
fail "seed accepted an acquired home matching the active firstmate home"
|
|
373
|
+
fi
|
|
374
|
+
grep -F 'secondmate home cannot be the active firstmate home' "$err" >/dev/null \
|
|
375
|
+
|| fail "seed did not explain active acquired-home rejection"
|
|
376
|
+
grep -F "treehouse return --force" "$log" >/dev/null \
|
|
377
|
+
&& fail "seed returned an unsafe acquired active home through treehouse"
|
|
378
|
+
[ -d "$home/projects/alpha" ] || fail "unsafe acquired-home rollback removed the active home"
|
|
379
|
+
|
|
380
|
+
: > "$log"
|
|
381
|
+
if PATH="$fakebin:$PATH" FM_HOME="$home" FM_FAKE_TREEHOUSE_HOME="$descendant" FM_FAKE_TMUX_LOG="$log" \
|
|
382
|
+
"$ROOT/bin/fm-home-seed.sh" dash - alpha >/dev/null 2>"$err"; then
|
|
383
|
+
fail "seed accepted an acquired home inside the active firstmate home"
|
|
384
|
+
fi
|
|
385
|
+
grep -F 'secondmate home cannot be inside the active firstmate home' "$err" >/dev/null \
|
|
386
|
+
|| fail "seed did not explain active descendant acquired-home rejection"
|
|
387
|
+
grep -F "treehouse return --force" "$log" >/dev/null \
|
|
388
|
+
&& fail "seed returned an unsafe acquired active descendant through treehouse"
|
|
389
|
+
[ -d "$descendant" ] || fail "unsafe acquired-home rollback removed the active descendant"
|
|
390
|
+
pass "home seeding leaves unsafe acquired active homes untouched"
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
test_home_seed_rolls_back_failed_clone() {
|
|
394
|
+
local home subhome err missing_remote
|
|
395
|
+
home="$TMP_ROOT/rollback-home"
|
|
396
|
+
subhome="$TMP_ROOT/rollback-subhome"
|
|
397
|
+
err="$TMP_ROOT/rollback-home.err"
|
|
398
|
+
missing_remote="$TMP_ROOT/remotes/missing-beta.git"
|
|
399
|
+
mkdir -p "$home/projects" "$home/data" "$home/state"
|
|
400
|
+
fm_git_init_commit "$home/projects/alpha"
|
|
401
|
+
fm_git_init_commit "$home/projects/beta"
|
|
402
|
+
fm_git_add_origin "$home/projects/alpha" "$TMP_ROOT/remotes/rollback-alpha.git"
|
|
403
|
+
git -C "$home/projects/beta" remote add origin "file://$missing_remote"
|
|
404
|
+
cat > "$home/data/projects.md" <<EOF
|
|
405
|
+
- alpha [direct-PR] - alpha project (added 2026-06-22)
|
|
406
|
+
- beta [direct-PR] - beta project (added 2026-06-22)
|
|
407
|
+
EOF
|
|
408
|
+
|
|
409
|
+
if FM_HOME="$home" FM_SECONDMATE_CHARTER='rollback scope' FM_SECONDMATE_SCOPE='rollback scope' \
|
|
410
|
+
"$ROOT/bin/fm-home-seed.sh" rollback "$subhome" alpha beta >/dev/null 2>"$err"; then
|
|
411
|
+
fail "seed succeeded even though the second project clone failed"
|
|
412
|
+
fi
|
|
413
|
+
grep -F 'does not appear to be a git repository' "$err" >/dev/null \
|
|
414
|
+
|| grep -F 'repository' "$err" >/dev/null \
|
|
415
|
+
|| fail "seed failure did not include the clone error"
|
|
416
|
+
[ ! -e "$subhome" ] || fail "failed seed left the newly created secondmate home behind"
|
|
417
|
+
[ ! -e "$subhome/.fm-secondmate-home" ] || fail "failed seed left a subhome marker"
|
|
418
|
+
[ ! -e "$subhome/projects/alpha" ] || fail "failed seed left a previously cloned project"
|
|
419
|
+
[ ! -e "$home/data/rollback/brief.md" ] || fail "failed seed left a generated charter brief"
|
|
420
|
+
if [ -f "$home/data/secondmates.md" ] && grep -F -- '- rollback ' "$home/data/secondmates.md" >/dev/null; then
|
|
421
|
+
fail "failed seed left a registry route"
|
|
422
|
+
fi
|
|
423
|
+
pass "home seeding rolls back failed clone attempts without residue"
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
test_home_seed_refuses_missing_filled_charter() {
|
|
427
|
+
local home subhome err
|
|
428
|
+
home="$TMP_ROOT/missing-charter-home"
|
|
429
|
+
subhome="$TMP_ROOT/missing-charter-subhome"
|
|
430
|
+
err="$TMP_ROOT/missing-charter.err"
|
|
431
|
+
mkdir -p "$home/projects" "$home/data" "$home/state"
|
|
432
|
+
fm_git_init_commit "$home/projects/alpha"
|
|
433
|
+
fm_git_add_origin "$home/projects/alpha" "$TMP_ROOT/remotes/missing-charter-alpha.git"
|
|
434
|
+
printf '%s\n' '- alpha [direct-PR] - alpha project (added 2026-06-22)' > "$home/data/projects.md"
|
|
435
|
+
|
|
436
|
+
if FM_HOME="$home" "$ROOT/bin/fm-home-seed.sh" design "$subhome" alpha >/dev/null 2>"$err"; then
|
|
437
|
+
fail "seed accepted a direct seed without a filled charter"
|
|
438
|
+
fi
|
|
439
|
+
grep -F 'no filled secondmate charter brief' "$err" >/dev/null \
|
|
440
|
+
|| fail "seed did not explain missing filled charter refusal"
|
|
441
|
+
[ ! -e "$subhome" ] || fail "missing charter seed left a generated subhome"
|
|
442
|
+
[ ! -e "$home/data/design/brief.md" ] || fail "missing charter seed generated a placeholder charter"
|
|
443
|
+
pass "home seeding refuses direct seed without filled charter text"
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
test_home_seed_refuses_placeholder_charter() {
|
|
447
|
+
local home subhome err
|
|
448
|
+
home="$TMP_ROOT/placeholder-charter-home"
|
|
449
|
+
subhome="$TMP_ROOT/placeholder-charter-subhome"
|
|
450
|
+
err="$TMP_ROOT/placeholder-charter.err"
|
|
451
|
+
mkdir -p "$home/projects" "$home/data" "$home/state"
|
|
452
|
+
fm_git_init_commit "$home/projects/alpha"
|
|
453
|
+
fm_git_add_origin "$home/projects/alpha" "$TMP_ROOT/remotes/placeholder-charter-alpha.git"
|
|
454
|
+
printf '%s\n' '- alpha [direct-PR] - alpha project (added 2026-06-22)' > "$home/data/projects.md"
|
|
455
|
+
FM_HOME="$home" "$ROOT/bin/fm-brief.sh" design --secondmate alpha >/dev/null \
|
|
456
|
+
|| fail "placeholder charter scaffold failed"
|
|
457
|
+
|
|
458
|
+
if FM_HOME="$home" "$ROOT/bin/fm-home-seed.sh" design "$subhome" alpha >/dev/null 2>"$err"; then
|
|
459
|
+
fail "seed accepted an unfilled placeholder charter"
|
|
460
|
+
fi
|
|
461
|
+
grep -F 'still contains {TASK}' "$err" >/dev/null \
|
|
462
|
+
|| fail "seed did not explain placeholder charter refusal"
|
|
463
|
+
[ ! -e "$subhome" ] || fail "placeholder charter seed left a generated subhome"
|
|
464
|
+
[ ! -e "$subhome/projects/alpha" ] || fail "placeholder charter seed cloned before refusing"
|
|
465
|
+
pass "home seeding refuses unfilled placeholder charters"
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
test_home_seed_refuses_empty_charter_fields() {
|
|
469
|
+
local home subhome err
|
|
470
|
+
home="$TMP_ROOT/empty-charter-home"
|
|
471
|
+
subhome="$TMP_ROOT/empty-charter-subhome"
|
|
472
|
+
err="$TMP_ROOT/empty-charter.err"
|
|
473
|
+
mkdir -p "$home/projects" "$home/data" "$home/state"
|
|
474
|
+
fm_git_init_commit "$home/projects/alpha"
|
|
475
|
+
fm_git_add_origin "$home/projects/alpha" "$TMP_ROOT/remotes/empty-charter-alpha.git"
|
|
476
|
+
printf '%s\n' '- alpha [direct-PR] - alpha project (added 2026-06-22)' > "$home/data/projects.md"
|
|
477
|
+
|
|
478
|
+
if FM_HOME="$home" FM_SECONDMATE_CHARTER=' ' "$ROOT/bin/fm-home-seed.sh" design "$subhome" alpha >/dev/null 2>"$err"; then
|
|
479
|
+
fail "seed accepted a whitespace-only charter"
|
|
480
|
+
fi
|
|
481
|
+
grep -F 'empty Charter section' "$err" >/dev/null \
|
|
482
|
+
|| fail "seed did not explain empty charter refusal"
|
|
483
|
+
[ ! -e "$subhome" ] || fail "empty charter seed left a generated subhome"
|
|
484
|
+
|
|
485
|
+
rm -rf "$home/data/design" "$subhome" "$err"
|
|
486
|
+
FM_SECONDMATE_SCOPE=' ' scaffold_secondmate_charter "$home" design 'filled charter' alpha \
|
|
487
|
+
|| fail "empty scope fixture scaffold failed"
|
|
488
|
+
if FM_HOME="$home" "$ROOT/bin/fm-home-seed.sh" design "$subhome" alpha >/dev/null 2>"$err"; then
|
|
489
|
+
fail "seed accepted an empty routing scope"
|
|
490
|
+
fi
|
|
491
|
+
grep -F 'empty Routing scope section' "$err" >/dev/null \
|
|
492
|
+
|| fail "seed did not explain empty routing scope refusal"
|
|
493
|
+
[ ! -e "$subhome" ] || fail "empty routing scope seed left a generated subhome"
|
|
494
|
+
pass "home seeding refuses empty normalized charter fields"
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
test_home_seed_no_projects_end_to_end() {
|
|
498
|
+
# A domain whose subject is the firstmate repo itself needs no project clones:
|
|
499
|
+
# the deliberate --no-projects signal scaffolds, seeds, registers, and spawns a
|
|
500
|
+
# project-less home end to end with no placeholder clone.
|
|
501
|
+
local home sub sub_abs fakebin log meta proj_val out
|
|
502
|
+
home="$TMP_ROOT/no-projects-seed-home"
|
|
503
|
+
sub="$TMP_ROOT/no-projects-seed-subhome"
|
|
504
|
+
mkdir -p "$home/projects" "$home/data" "$home/state"
|
|
505
|
+
fakebin=$(make_fake_tmux "$TMP_ROOT/no-projects-fake")
|
|
506
|
+
log="$TMP_ROOT/no-projects-fake/tmux.log"
|
|
507
|
+
|
|
508
|
+
out=$(FM_HOME="$home" FM_SECONDMATE_CHARTER='firstmate self-development' \
|
|
509
|
+
FM_SECONDMATE_SCOPE='firstmate repo work' \
|
|
510
|
+
"$ROOT/bin/fm-home-seed.sh" fdev "$sub" --no-projects) \
|
|
511
|
+
|| fail "project-less seed failed"
|
|
512
|
+
sub_abs=$(cd "$sub" && pwd -P)
|
|
513
|
+
printf '%s\n' "$out" | grep -F "home=$sub_abs" >/dev/null || fail "seed did not report the project-less subhome"
|
|
514
|
+
|
|
515
|
+
# Registered with an empty projects field, marked, charter copied, no clones.
|
|
516
|
+
assert_grep '- fdev - firstmate self-development' "$home/data/secondmates.md" "project-less registry line missing"
|
|
517
|
+
assert_grep 'scope: firstmate repo work' "$home/data/secondmates.md" "project-less registry scope missing"
|
|
518
|
+
assert_grep 'projects: ;' "$home/data/secondmates.md" "project-less registry did not render an empty projects field"
|
|
519
|
+
[ "$(cat "$sub/.fm-secondmate-home")" = fdev ] || fail "project-less seed did not mark the subhome"
|
|
520
|
+
assert_present "$sub/data/charter.md" "project-less seed did not copy the charter"
|
|
521
|
+
[ -z "$(ls -A "$sub/projects" 2>/dev/null)" ] || fail "project-less seed cloned a project"
|
|
522
|
+
FM_HOME="$home" "$ROOT/bin/fm-home-seed.sh" validate >/dev/null || fail "registry validation failed after project-less seed"
|
|
523
|
+
|
|
524
|
+
# Spawn tolerates the empty projects field: the home resolves from the registry
|
|
525
|
+
# and the projects meta is recorded empty rather than breaking the launch.
|
|
526
|
+
: > "$log"
|
|
527
|
+
PATH="$fakebin:$PATH" FM_HOME="$home" FM_FAKE_TMUX_LOG="$log" \
|
|
528
|
+
FM_FAKE_TMUX_CAPTURE="$TMP_ROOT/no-projects-fake/pane.txt" \
|
|
529
|
+
"$ROOT/bin/fm-spawn.sh" fdev "$sub" codex --secondmate >/dev/null 2>&1 \
|
|
530
|
+
|| fail "project-less secondmate spawn failed"
|
|
531
|
+
meta="$home/state/fdev.meta"
|
|
532
|
+
assert_grep 'kind=secondmate' "$meta" "project-less spawn meta lost kind=secondmate"
|
|
533
|
+
assert_grep "home=$sub_abs" "$meta" "project-less spawn meta lost the subhome"
|
|
534
|
+
proj_val=$(grep '^projects=' "$meta" | head -1 | cut -d= -f2-)
|
|
535
|
+
[ -z "$proj_val" ] || fail "project-less spawn recorded a non-empty projects meta: '$proj_val'"
|
|
536
|
+
pass "home seeding scaffolds, registers, and spawns a project-less home end to end"
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
test_secondmate_spawn_resolves_punctuated_registry_projects() {
|
|
540
|
+
local home sub sub_abs fakebin log meta projects
|
|
541
|
+
home="$TMP_ROOT/punctuated-spawn-home"
|
|
542
|
+
sub="$TMP_ROOT/punctuated-spawn-subhome"
|
|
543
|
+
mkdir -p "$home/data" "$home/state" "$home/config" "$home/projects"
|
|
544
|
+
mkdir -p "$sub/data" "$sub/state" "$sub/config" "$sub/projects"
|
|
545
|
+
mark_firstmate_home "$sub"
|
|
546
|
+
printf 'punctuated\n' > "$sub/.fm-secondmate-home"
|
|
547
|
+
printf '# Charter\n\nHandled work.\n' > "$sub/data/charter.md"
|
|
548
|
+
sub_abs=$(cd "$sub" && pwd -P)
|
|
549
|
+
printf -- '- punctuated - launch notes (parenthetical) (home: %s; scope: launch (child); semicolon is valid; projects: alpha, beta; added 2026-07-30)' \
|
|
550
|
+
"$sub_abs" > "$home/data/secondmates.md"
|
|
551
|
+
FM_HOME="$home" "$ROOT/bin/fm-home-seed.sh" validate >/dev/null \
|
|
552
|
+
|| fail "home-seed validation rejected punctuated registry fields before spawn"
|
|
553
|
+
fakebin=$(make_fake_tmux "$TMP_ROOT/punctuated-spawn-fake")
|
|
554
|
+
log="$TMP_ROOT/punctuated-spawn-fake/tmux.log"
|
|
555
|
+
PATH="$fakebin:$PATH" FM_HOME="$home" FM_FAKE_TMUX_LOG="$log" \
|
|
556
|
+
FM_FAKE_TMUX_CAPTURE="$TMP_ROOT/punctuated-spawn-fake/pane.txt" \
|
|
557
|
+
"$ROOT/bin/fm-spawn.sh" punctuated codex --secondmate >/dev/null 2>&1 \
|
|
558
|
+
|| fail "secondmate spawn failed for punctuated registry fields"
|
|
559
|
+
meta="$home/state/punctuated.meta"
|
|
560
|
+
projects=$(grep '^projects=' "$meta" | cut -d= -f2-)
|
|
561
|
+
[ "$projects" = 'alpha, beta' ] \
|
|
562
|
+
|| fail "secondmate spawn resolved the wrong projects field: '$projects'"
|
|
563
|
+
pass "secondmate spawn resolves home validation and projects from punctuated registry fields"
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
test_secondmate_spawn_refuses_ambiguous_and_mismatched_registry_bindings() {
|
|
567
|
+
local row case_name home sub other fakebin log err meta_before
|
|
568
|
+
for row in duplicate-id unterminated-duplicate-id duplicate-home supplied-mismatch metadata-mismatch; do
|
|
569
|
+
case_name=${row%%|*}
|
|
570
|
+
home="$TMP_ROOT/spawn-binding-$case_name-home"
|
|
571
|
+
sub="$TMP_ROOT/spawn-binding-$case_name-sub"
|
|
572
|
+
other="$TMP_ROOT/spawn-binding-$case_name-other"
|
|
573
|
+
mkdir -p "$home/data" "$home/state" "$home/config" "$home/projects"
|
|
574
|
+
mark_firstmate_home "$sub"
|
|
575
|
+
mark_firstmate_home "$other"
|
|
576
|
+
printf 'domain\n' > "$sub/.fm-secondmate-home"
|
|
577
|
+
printf 'domain\n' > "$other/.fm-secondmate-home"
|
|
578
|
+
case "$case_name" in
|
|
579
|
+
duplicate-id)
|
|
580
|
+
cat > "$home/data/secondmates.md" <<EOF
|
|
581
|
+
- domain - primary route (home: $sub; scope: valid (scope); punctuation; projects: alpha; added 2026-07-30)
|
|
582
|
+
- domain - duplicate route (home: $other; scope: duplicate; projects: beta; added 2026-07-30)
|
|
583
|
+
EOF
|
|
584
|
+
;;
|
|
585
|
+
unterminated-duplicate-id)
|
|
586
|
+
printf -- '- domain - primary route (home: %s; scope: valid (scope); punctuation; projects: alpha; added 2026-07-30)\n- domain - duplicate route (home: %s; scope: duplicate; projects: beta; added 2026-07-30)' \
|
|
587
|
+
"$sub" "$other" > "$home/data/secondmates.md"
|
|
588
|
+
;;
|
|
589
|
+
duplicate-home)
|
|
590
|
+
cat > "$home/data/secondmates.md" <<EOF
|
|
591
|
+
- domain - primary route (home: $sub; scope: valid (scope); punctuation; projects: alpha; added 2026-07-30)
|
|
592
|
+
- other - duplicate home route (home: $sub; scope: duplicate; projects: beta; added 2026-07-30)
|
|
593
|
+
EOF
|
|
594
|
+
;;
|
|
595
|
+
supplied-mismatch|metadata-mismatch)
|
|
596
|
+
printf -- '- domain - mismatched route (home: %s; scope: valid (scope); punctuation; projects: alpha; added 2026-07-30)\n' \
|
|
597
|
+
"$other" > "$home/data/secondmates.md"
|
|
598
|
+
;;
|
|
599
|
+
esac
|
|
600
|
+
fakebin=$(make_fake_tmux "$TMP_ROOT/spawn-binding-$case_name-fake")
|
|
601
|
+
log="$TMP_ROOT/spawn-binding-$case_name-fake/tmux.log"
|
|
602
|
+
err="$TMP_ROOT/spawn-binding-$case_name.err"
|
|
603
|
+
if [ "$case_name" = metadata-mismatch ]; then
|
|
604
|
+
fm_write_secondmate_meta "$home/state/domain.meta" "$sub"
|
|
605
|
+
meta_before="$TMP_ROOT/spawn-binding-$case_name.meta.before"
|
|
606
|
+
cp "$home/state/domain.meta" "$meta_before"
|
|
607
|
+
if PATH="$fakebin:$PATH" FM_HOME="$home" FM_FAKE_TMUX_LOG="$log" \
|
|
608
|
+
FM_FAKE_TMUX_CAPTURE="$TMP_ROOT/spawn-binding-$case_name-fake/pane.txt" \
|
|
609
|
+
"$ROOT/bin/fm-spawn.sh" domain codex --secondmate >/dev/null 2>"$err"; then
|
|
610
|
+
fail "secondmate spawn accepted $case_name registry binding"
|
|
611
|
+
fi
|
|
612
|
+
cmp -s "$meta_before" "$home/state/domain.meta" || fail "secondmate spawn changed metadata after $case_name refusal"
|
|
613
|
+
else
|
|
614
|
+
if PATH="$fakebin:$PATH" FM_HOME="$home" FM_FAKE_TMUX_LOG="$log" \
|
|
615
|
+
FM_FAKE_TMUX_CAPTURE="$TMP_ROOT/spawn-binding-$case_name-fake/pane.txt" \
|
|
616
|
+
"$ROOT/bin/fm-spawn.sh" domain "$sub" codex --secondmate >/dev/null 2>"$err"; then
|
|
617
|
+
fail "secondmate spawn accepted $case_name registry binding"
|
|
618
|
+
fi
|
|
619
|
+
[ ! -e "$home/state/domain.meta" ] || fail "secondmate spawn wrote metadata after $case_name refusal"
|
|
620
|
+
fi
|
|
621
|
+
[ ! -e "$home/state/.spawn-domain.lock" ] || fail "secondmate spawn left a lock after $case_name refusal"
|
|
622
|
+
grep -F 'new-window' "$log" >/dev/null && fail "secondmate spawn created an endpoint before $case_name refusal"
|
|
623
|
+
done
|
|
624
|
+
pass "secondmate spawn refuses ambiguous, supplied-home, and metadata-home registry bindings"
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
test_home_seed_refuses_projectful_reused_charter_for_projectless_home() {
|
|
628
|
+
local home reusable_sub stale_sub stale_brief stale_brief_before err
|
|
629
|
+
home="$TMP_ROOT/no-projects-reused-charter-home"
|
|
630
|
+
reusable_sub="$TMP_ROOT/no-projects-reused-charter-valid-subhome"
|
|
631
|
+
stale_sub="$TMP_ROOT/no-projects-reused-charter-stale-subhome"
|
|
632
|
+
stale_brief="$home/data/stale/brief.md"
|
|
633
|
+
stale_brief_before="$TMP_ROOT/no-projects-reused-charter.before"
|
|
634
|
+
err="$TMP_ROOT/no-projects-reused-charter.err"
|
|
635
|
+
mkdir -p "$home/data" "$home/state" "$reusable_sub/data" "$stale_sub/data"
|
|
636
|
+
mark_firstmate_home "$reusable_sub"
|
|
637
|
+
mark_firstmate_home "$stale_sub"
|
|
638
|
+
|
|
639
|
+
scaffold_secondmate_charter "$home" reusable 'firstmate self-development' --no-projects \
|
|
640
|
+
|| fail "project-less charter scaffold failed"
|
|
641
|
+
printf '\n# Custom note\nThe projects above are local clones for work you supervise.\n' >> "$home/data/reusable/brief.md"
|
|
642
|
+
FM_HOME="$home" "$ROOT/bin/fm-home-seed.sh" reusable "$reusable_sub" --no-projects >/dev/null \
|
|
643
|
+
|| fail "project-less seed rejected a reused project-less charter"
|
|
644
|
+
assert_grep 'None. This is a project-less domain' "$reusable_sub/data/charter.md" \
|
|
645
|
+
"reused project-less charter was not copied"
|
|
646
|
+
|
|
647
|
+
scaffold_secondmate_charter "$home" stale 'firstmate self-development. None. This is a project-less domain.' alpha \
|
|
648
|
+
|| fail "projectful charter scaffold failed"
|
|
649
|
+
sed 's/The projects above are local clones for work you supervise; they are not an exclusive ownership claim./Project clone details are customized for this domain./' \
|
|
650
|
+
"$stale_brief" > "$stale_brief_before"
|
|
651
|
+
mv "$stale_brief_before" "$stale_brief"
|
|
652
|
+
cp "$stale_brief" "$stale_brief_before"
|
|
653
|
+
if FM_HOME="$home" "$ROOT/bin/fm-home-seed.sh" stale "$stale_sub" --no-projects >/dev/null 2>"$err"; then
|
|
654
|
+
fail "project-less seed accepted a reused charter with project clones"
|
|
655
|
+
fi
|
|
656
|
+
grep -F 'existing charter brief' "$err" >/dev/null \
|
|
657
|
+
|| fail "project-less charter refusal did not name the stale charter conflict"
|
|
658
|
+
grep -F 'fm-brief.sh stale --secondmate --no-projects' "$err" >/dev/null \
|
|
659
|
+
|| fail "project-less charter refusal did not explain how to re-scaffold"
|
|
660
|
+
cmp -s "$stale_brief_before" "$stale_brief" \
|
|
661
|
+
|| fail "project-less charter refusal changed the reused charter"
|
|
662
|
+
assert_absent "$stale_sub/.fm-secondmate-home" "project-less charter refusal wrote a home marker"
|
|
663
|
+
assert_absent "$stale_sub/data/charter.md" "project-less charter refusal copied a charter"
|
|
664
|
+
assert_absent "$stale_sub/projects" "project-less charter refusal created a projects directory"
|
|
665
|
+
if grep -F -- '- stale ' "$home/data/secondmates.md" >/dev/null; then
|
|
666
|
+
fail "project-less charter refusal wrote a parent registry route"
|
|
667
|
+
fi
|
|
668
|
+
pass "home seeding validates reused project-less charters before mutation"
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
test_home_seed_refuses_projectless_conversion_of_populated_home() {
|
|
672
|
+
local home sub err registry_before
|
|
673
|
+
home="$TMP_ROOT/no-projects-conversion-home"
|
|
674
|
+
sub="$TMP_ROOT/no-projects-conversion-subhome"
|
|
675
|
+
err="$TMP_ROOT/no-projects-conversion.err"
|
|
676
|
+
mkdir -p "$home/data" "$home/state" "$sub/data" "$sub/projects/existing-clone"
|
|
677
|
+
mark_firstmate_home "$sub"
|
|
678
|
+
fm_git_init_commit "$sub/projects/existing-clone"
|
|
679
|
+
cat > "$sub/data/projects.md" <<EOF
|
|
680
|
+
- registry-only [direct-PR] - retained project entry (added 2026-06-22)
|
|
681
|
+
EOF
|
|
682
|
+
registry_before=$(cat "$sub/data/projects.md")
|
|
683
|
+
|
|
684
|
+
if FM_HOME="$home" FM_SECONDMATE_CHARTER='firstmate self-development' \
|
|
685
|
+
FM_SECONDMATE_SCOPE='firstmate repo work' \
|
|
686
|
+
"$ROOT/bin/fm-home-seed.sh" fdev "$sub" --no-projects >/dev/null 2>"$err"; then
|
|
687
|
+
fail "project-less seed converted a populated secondmate home"
|
|
688
|
+
fi
|
|
689
|
+
grep -F 'existing-clone' "$err" >/dev/null \
|
|
690
|
+
|| fail "project-less conversion refusal did not name the existing clone"
|
|
691
|
+
grep -F 'registry-only' "$err" >/dev/null \
|
|
692
|
+
|| fail "project-less conversion refusal did not name the registry entry"
|
|
693
|
+
grep -F 'retire or clean this home first' "$err" >/dev/null \
|
|
694
|
+
|| fail "project-less conversion refusal did not explain the required cleanup"
|
|
695
|
+
assert_present "$sub/projects/existing-clone/.git" "project-less conversion refusal removed the existing clone"
|
|
696
|
+
[ "$registry_before" = "$(cat "$sub/data/projects.md")" ] \
|
|
697
|
+
|| fail "project-less conversion refusal changed the project registry"
|
|
698
|
+
assert_absent "$sub/.fm-secondmate-home" "project-less conversion refusal wrote a home marker"
|
|
699
|
+
assert_absent "$sub/data/charter.md" "project-less conversion refusal copied a charter"
|
|
700
|
+
assert_absent "$sub/state" "project-less conversion refusal left an operational directory"
|
|
701
|
+
if [ -f "$home/data/secondmates.md" ] && grep -F -- '- fdev ' "$home/data/secondmates.md" >/dev/null; then
|
|
702
|
+
fail "project-less conversion refusal wrote a parent registry route"
|
|
703
|
+
fi
|
|
704
|
+
pass "home seeding refuses project-less conversion of a populated home"
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
test_home_seed_refuses_projectless_home_with_uninspectable_projects() {
|
|
708
|
+
local home sub err
|
|
709
|
+
home="$TMP_ROOT/no-projects-uninspectable-home"
|
|
710
|
+
sub="$TMP_ROOT/no-projects-uninspectable-subhome"
|
|
711
|
+
err="$TMP_ROOT/no-projects-uninspectable.err"
|
|
712
|
+
mkdir -p "$home/data" "$home/state" "$sub/data" "$sub/projects/hidden-clone"
|
|
713
|
+
mark_firstmate_home "$sub"
|
|
714
|
+
fm_git_init_commit "$sub/projects/hidden-clone"
|
|
715
|
+
chmod 311 "$sub/projects"
|
|
716
|
+
|
|
717
|
+
if FM_HOME="$home" FM_SECONDMATE_CHARTER='firstmate self-development' \
|
|
718
|
+
FM_SECONDMATE_SCOPE='firstmate repo work' \
|
|
719
|
+
"$ROOT/bin/fm-home-seed.sh" fdev "$sub" --no-projects >/dev/null 2>"$err"; then
|
|
720
|
+
chmod 700 "$sub/projects"
|
|
721
|
+
fail "project-less seed accepted a home whose projects directory could not be inspected"
|
|
722
|
+
fi
|
|
723
|
+
chmod 700 "$sub/projects"
|
|
724
|
+
grep -F 'cannot inspect existing projects directory' "$err" >/dev/null \
|
|
725
|
+
|| fail "project-less seed did not explain the projects inspection failure"
|
|
726
|
+
grep -F 'resolve its access permissions or retire or clean this home' "$err" >/dev/null \
|
|
727
|
+
|| fail "project-less seed did not explain how to resolve the inspection failure"
|
|
728
|
+
assert_present "$sub/projects/hidden-clone/.git" "project-less inspection refusal removed the existing clone"
|
|
729
|
+
assert_absent "$sub/.fm-secondmate-home" "project-less inspection refusal wrote a home marker"
|
|
730
|
+
assert_absent "$sub/data/charter.md" "project-less inspection refusal copied a charter"
|
|
731
|
+
assert_absent "$sub/state" "project-less inspection refusal left an operational directory"
|
|
732
|
+
if [ -f "$home/data/secondmates.md" ] && grep -F -- '- fdev ' "$home/data/secondmates.md" >/dev/null; then
|
|
733
|
+
fail "project-less inspection refusal wrote a parent registry route"
|
|
734
|
+
fi
|
|
735
|
+
pass "home seeding refuses project-less homes whose projects directory cannot be inspected"
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
test_home_seed_refuses_projectless_home_with_symlinked_projects() {
|
|
739
|
+
local home sub target err
|
|
740
|
+
home="$TMP_ROOT/no-projects-symlinked-projects-home"
|
|
741
|
+
sub="$TMP_ROOT/no-projects-symlinked-projects-subhome"
|
|
742
|
+
target="$sub/retained-projects"
|
|
743
|
+
err="$TMP_ROOT/no-projects-symlinked-projects.err"
|
|
744
|
+
mkdir -p "$home/data" "$home/state" "$sub/data" "$target/hidden-clone"
|
|
745
|
+
mark_firstmate_home "$sub"
|
|
746
|
+
fm_git_init_commit "$target/hidden-clone"
|
|
747
|
+
ln -s "$target" "$sub/projects"
|
|
748
|
+
chmod 311 "$target"
|
|
749
|
+
|
|
750
|
+
if FM_HOME="$home" FM_SECONDMATE_CHARTER='firstmate self-development' \
|
|
751
|
+
FM_SECONDMATE_SCOPE='firstmate repo work' \
|
|
752
|
+
"$ROOT/bin/fm-home-seed.sh" fdev "$sub" --no-projects >/dev/null 2>"$err"; then
|
|
753
|
+
chmod 700 "$target"
|
|
754
|
+
fail "project-less seed accepted a home whose projects directory is a symlink"
|
|
755
|
+
fi
|
|
756
|
+
chmod 700 "$target"
|
|
757
|
+
grep -F 'projects directory' "$err" >/dev/null \
|
|
758
|
+
|| fail "project-less seed did not identify the symlinked projects directory"
|
|
759
|
+
grep -F 'it is a symlink' "$err" >/dev/null \
|
|
760
|
+
|| fail "project-less seed did not explain the symlinked projects directory refusal"
|
|
761
|
+
assert_present "$target/hidden-clone/.git" "project-less symlink refusal removed the target clone"
|
|
762
|
+
[ -L "$sub/projects" ] || fail "project-less symlink refusal changed the projects symlink"
|
|
763
|
+
[ "$(readlink "$sub/projects")" = "$target" ] \
|
|
764
|
+
|| fail "project-less symlink refusal retargeted the projects symlink"
|
|
765
|
+
assert_absent "$sub/.fm-secondmate-home" "project-less symlink refusal wrote a home marker"
|
|
766
|
+
assert_absent "$sub/data/charter.md" "project-less symlink refusal copied a charter"
|
|
767
|
+
assert_absent "$sub/state" "project-less symlink refusal left an operational directory"
|
|
768
|
+
if [ -f "$home/data/secondmates.md" ] && grep -F -- '- fdev ' "$home/data/secondmates.md" >/dev/null; then
|
|
769
|
+
fail "project-less symlink refusal wrote a parent registry route"
|
|
770
|
+
fi
|
|
771
|
+
pass "home seeding refuses project-less homes with symlinked projects directories"
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
test_home_seed_refuses_projectless_home_with_non_directory_projects() {
|
|
775
|
+
local home sub err projects_before
|
|
776
|
+
home="$TMP_ROOT/no-projects-nondirectory-projects-home"
|
|
777
|
+
sub="$TMP_ROOT/no-projects-nondirectory-projects-subhome"
|
|
778
|
+
err="$TMP_ROOT/no-projects-nondirectory-projects.err"
|
|
779
|
+
mkdir -p "$home/data" "$home/state" "$sub/data"
|
|
780
|
+
mark_firstmate_home "$sub"
|
|
781
|
+
printf '%s\n' 'retained project path' > "$sub/projects"
|
|
782
|
+
projects_before=$(cat "$sub/projects")
|
|
783
|
+
|
|
784
|
+
if FM_HOME="$home" FM_SECONDMATE_CHARTER='firstmate self-development' \
|
|
785
|
+
FM_SECONDMATE_SCOPE='firstmate repo work' \
|
|
786
|
+
"$ROOT/bin/fm-home-seed.sh" fdev "$sub" --no-projects >/dev/null 2>"$err"; then
|
|
787
|
+
fail "project-less seed accepted a home whose projects path is not a directory"
|
|
788
|
+
fi
|
|
789
|
+
grep -F 'projects directory' "$err" >/dev/null \
|
|
790
|
+
|| fail "project-less seed did not identify the non-directory projects path"
|
|
791
|
+
grep -F 'it is not a directory' "$err" >/dev/null \
|
|
792
|
+
|| fail "project-less seed did not explain the non-directory projects path refusal"
|
|
793
|
+
[ "$projects_before" = "$(cat "$sub/projects")" ] \
|
|
794
|
+
|| fail "project-less non-directory refusal changed the projects path"
|
|
795
|
+
assert_absent "$sub/.fm-secondmate-home" "project-less non-directory refusal wrote a home marker"
|
|
796
|
+
assert_absent "$sub/data/charter.md" "project-less non-directory refusal copied a charter"
|
|
797
|
+
assert_absent "$sub/state" "project-less non-directory refusal left an operational directory"
|
|
798
|
+
if [ -f "$home/data/secondmates.md" ] && grep -F -- '- fdev ' "$home/data/secondmates.md" >/dev/null; then
|
|
799
|
+
fail "project-less non-directory refusal wrote a parent registry route"
|
|
800
|
+
fi
|
|
801
|
+
pass "home seeding refuses project-less homes with non-directory projects paths"
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
test_home_seed_refuses_projectless_home_with_uninspectable_registry() {
|
|
805
|
+
local home sub err registry_before
|
|
806
|
+
home="$TMP_ROOT/no-projects-uninspectable-registry-home"
|
|
807
|
+
sub="$TMP_ROOT/no-projects-uninspectable-registry-subhome"
|
|
808
|
+
err="$TMP_ROOT/no-projects-uninspectable-registry.err"
|
|
809
|
+
mkdir -p "$home/data" "$home/state" "$sub/data"
|
|
810
|
+
mark_firstmate_home "$sub"
|
|
811
|
+
printf '%s\n' '- hidden-registry [direct-PR] - retained project entry (added 2026-06-22)' > "$sub/data/projects.md"
|
|
812
|
+
registry_before=$(cat "$sub/data/projects.md")
|
|
813
|
+
chmod 000 "$sub/data/projects.md"
|
|
814
|
+
|
|
815
|
+
if FM_HOME="$home" FM_SECONDMATE_CHARTER='firstmate self-development' \
|
|
816
|
+
FM_SECONDMATE_SCOPE='firstmate repo work' \
|
|
817
|
+
"$ROOT/bin/fm-home-seed.sh" fdev "$sub" --no-projects >/dev/null 2>"$err"; then
|
|
818
|
+
chmod 600 "$sub/data/projects.md"
|
|
819
|
+
fail "project-less seed accepted a home whose project registry could not be inspected"
|
|
820
|
+
fi
|
|
821
|
+
chmod 600 "$sub/data/projects.md"
|
|
822
|
+
grep -F 'cannot inspect existing project registry' "$err" >/dev/null \
|
|
823
|
+
|| fail "project-less seed did not explain the project registry inspection failure"
|
|
824
|
+
grep -F 'resolve its access permissions or retire or clean this home' "$err" >/dev/null \
|
|
825
|
+
|| fail "project-less seed did not explain how to resolve the project registry inspection failure"
|
|
826
|
+
[ "$registry_before" = "$(cat "$sub/data/projects.md")" ] \
|
|
827
|
+
|| fail "project-less inspection refusal changed the project registry"
|
|
828
|
+
assert_absent "$sub/.fm-secondmate-home" "project-less registry inspection refusal wrote a home marker"
|
|
829
|
+
assert_absent "$sub/data/charter.md" "project-less registry inspection refusal copied a charter"
|
|
830
|
+
assert_absent "$sub/state" "project-less registry inspection refusal left an operational directory"
|
|
831
|
+
assert_absent "$sub/projects" "project-less registry inspection refusal created a projects directory"
|
|
832
|
+
if [ -f "$home/data/secondmates.md" ] && grep -F -- '- fdev ' "$home/data/secondmates.md" >/dev/null; then
|
|
833
|
+
fail "project-less registry inspection refusal wrote a parent registry route"
|
|
834
|
+
fi
|
|
835
|
+
pass "home seeding refuses project-less homes whose project registry cannot be inspected"
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
test_home_seed_refuses_missing_projects_without_signal() {
|
|
839
|
+
# Accidental omission of the project list, with no deliberate --no-projects
|
|
840
|
+
# signal, must fail loudly and leave nothing behind, so a forgotten argument is
|
|
841
|
+
# never mistaken for an intentional project-less seed.
|
|
842
|
+
local home sub err
|
|
843
|
+
home="$TMP_ROOT/missing-projects-home"
|
|
844
|
+
sub="$TMP_ROOT/missing-projects-subhome"
|
|
845
|
+
err="$TMP_ROOT/missing-projects.err"
|
|
846
|
+
mkdir -p "$home/projects" "$home/data" "$home/state"
|
|
847
|
+
|
|
848
|
+
if FM_HOME="$home" FM_SECONDMATE_CHARTER='some scope' \
|
|
849
|
+
"$ROOT/bin/fm-home-seed.sh" fdev "$sub" >/dev/null 2>"$err"; then
|
|
850
|
+
fail "seed accepted a project-less home without the deliberate --no-projects signal"
|
|
851
|
+
fi
|
|
852
|
+
assert_absent "$sub" "loud-failure seed created a subhome"
|
|
853
|
+
if [ -f "$home/data/secondmates.md" ] && grep -F -- '- fdev ' "$home/data/secondmates.md" >/dev/null; then
|
|
854
|
+
fail "loud-failure seed left a registry route"
|
|
855
|
+
fi
|
|
856
|
+
|
|
857
|
+
# The deliberate signal is mutually exclusive with a project list.
|
|
858
|
+
if FM_HOME="$home" FM_SECONDMATE_CHARTER='some scope' \
|
|
859
|
+
"$ROOT/bin/fm-home-seed.sh" fdev "$sub" --no-projects alpha >/dev/null 2>"$err"; then
|
|
860
|
+
fail "seed accepted --no-projects combined with a project list"
|
|
861
|
+
fi
|
|
862
|
+
grep -F 'cannot be combined with a project list' "$err" >/dev/null \
|
|
863
|
+
|| fail "seed did not explain the --no-projects mutual-exclusion rejection"
|
|
864
|
+
pass "home seeding fails loudly on accidental project omission and rejects mixed --no-projects"
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
test_home_seed_refuses_local_only_project() {
|
|
868
|
+
local home subhome err
|
|
869
|
+
home="$TMP_ROOT/local-only-seed-home"
|
|
870
|
+
subhome="$TMP_ROOT/local-only-seed-subhome"
|
|
871
|
+
err="$TMP_ROOT/local-only-seed.err"
|
|
872
|
+
mkdir -p "$home/projects" "$home/data" "$home/state"
|
|
873
|
+
fm_git_init_commit "$home/projects/alpha"
|
|
874
|
+
printf '%s\n' '- alpha [local-only] - alpha project (added 2026-06-22)' > "$home/data/projects.md"
|
|
875
|
+
|
|
876
|
+
if FM_HOME="$home" "$ROOT/bin/fm-home-seed.sh" design "$subhome" alpha >/dev/null 2>"$err"; then
|
|
877
|
+
fail "seed allowed a local-only project into a secondmate home"
|
|
878
|
+
fi
|
|
879
|
+
grep -F 'project alpha is local-only; secondmate routes support only no-mistakes and direct-PR projects' "$err" >/dev/null \
|
|
880
|
+
|| fail "seed did not explain local-only project rejection"
|
|
881
|
+
[ ! -e "$subhome" ] || fail "seed created a subhome before rejecting a local-only project"
|
|
882
|
+
pass "home seeding refuses local-only projects"
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
test_home_seed_refuses_registry_delimiter_home() {
|
|
886
|
+
local home subhome err
|
|
887
|
+
home="$TMP_ROOT/delimiter-home"
|
|
888
|
+
subhome="$TMP_ROOT/delimiter)subhome"
|
|
889
|
+
err="$TMP_ROOT/delimiter-home.err"
|
|
890
|
+
mkdir -p "$home/projects" "$home/data" "$home/state"
|
|
891
|
+
fm_git_init_commit "$home/projects/alpha"
|
|
892
|
+
fm_git_add_origin "$home/projects/alpha" "$TMP_ROOT/remotes/delimiter-alpha.git"
|
|
893
|
+
printf '%s\n' '- alpha [direct-PR] - alpha project (added 2026-06-22)' > "$home/data/projects.md"
|
|
894
|
+
|
|
895
|
+
if FM_HOME="$home" FM_SECONDMATE_CHARTER='delimiter charter' "$ROOT/bin/fm-home-seed.sh" design "$subhome" alpha >/dev/null 2>"$err"; then
|
|
896
|
+
fail "seed accepted a home path with registry delimiters"
|
|
897
|
+
fi
|
|
898
|
+
grep -F 'secondmate home path contains registry delimiters' "$err" >/dev/null \
|
|
899
|
+
|| fail "seed did not explain delimiter home refusal"
|
|
900
|
+
[ ! -e "$subhome/.fm-secondmate-home" ] || fail "delimiter home seed wrote a marker"
|
|
901
|
+
if [ -f "$home/data/secondmates.md" ] && grep -F -- '- design ' "$home/data/secondmates.md" >/dev/null; then
|
|
902
|
+
fail "delimiter home seed wrote a registry route"
|
|
903
|
+
fi
|
|
904
|
+
pass "home seeding refuses registry delimiter home paths"
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
test_home_seed_refuses_active_home_and_root() {
|
|
908
|
+
local home err active_ancestor active_descendant root_clone root_descendant root_ancestor root_inside
|
|
909
|
+
active_ancestor="$TMP_ROOT/active-seed-ancestor"
|
|
910
|
+
home="$active_ancestor/main-home"
|
|
911
|
+
err="$TMP_ROOT/active-seed.err"
|
|
912
|
+
active_descendant="$home/nested/design-home"
|
|
913
|
+
root_clone="$TMP_ROOT/active-seed-root"
|
|
914
|
+
root_descendant="$root_clone/tmp/design-home"
|
|
915
|
+
root_ancestor="$TMP_ROOT/active-seed-root-ancestor"
|
|
916
|
+
root_inside="$root_ancestor/nested-root"
|
|
917
|
+
git clone --quiet "$ROOT" "$active_ancestor"
|
|
918
|
+
mkdir -p "$home/projects" "$home/data" "$home/state"
|
|
919
|
+
fm_git_init_commit "$home/projects/alpha"
|
|
920
|
+
fm_git_add_origin "$home/projects/alpha" "$TMP_ROOT/remotes/active-alpha.git"
|
|
921
|
+
printf '%s\n' '- alpha [direct-PR] - alpha project (added 2026-06-22)' > "$home/data/projects.md"
|
|
922
|
+
scaffold_secondmate_charter "$home" design 'design domain' alpha || fail "charter scaffold failed for active-home seed test"
|
|
923
|
+
|
|
924
|
+
if FM_HOME="$home" "$ROOT/bin/fm-home-seed.sh" design "$home" alpha >/dev/null 2>"$err"; then
|
|
925
|
+
fail "seed allowed secondmate home to reuse active FM_HOME"
|
|
926
|
+
fi
|
|
927
|
+
grep -F 'secondmate home cannot be the active firstmate home' "$err" >/dev/null \
|
|
928
|
+
|| fail "seed did not explain active FM_HOME rejection"
|
|
929
|
+
|
|
930
|
+
if FM_HOME="$home" "$ROOT/bin/fm-home-seed.sh" design "$active_descendant" alpha >/dev/null 2>"$err"; then
|
|
931
|
+
fail "seed allowed secondmate home inside active FM_HOME"
|
|
932
|
+
fi
|
|
933
|
+
grep -F 'secondmate home cannot be inside the active firstmate home' "$err" >/dev/null \
|
|
934
|
+
|| fail "seed did not explain active FM_HOME descendant rejection"
|
|
935
|
+
[ ! -e "$home/nested" ] || fail "seed created a directory inside active FM_HOME before descendant rejection"
|
|
936
|
+
|
|
937
|
+
if FM_HOME="$home" "$ROOT/bin/fm-home-seed.sh" design "$active_ancestor" alpha >/dev/null 2>"$err"; then
|
|
938
|
+
fail "seed allowed secondmate home to contain active FM_HOME"
|
|
939
|
+
fi
|
|
940
|
+
grep -F 'secondmate home cannot be an ancestor of the active firstmate home' "$err" >/dev/null \
|
|
941
|
+
|| fail "seed did not explain active FM_HOME ancestor rejection"
|
|
942
|
+
[ ! -f "$active_ancestor/.fm-secondmate-home" ] || fail "seed marked an ancestor of active FM_HOME"
|
|
943
|
+
|
|
944
|
+
if FM_HOME="$home" "$ROOT/bin/fm-home-seed.sh" design "$ROOT" alpha >/dev/null 2>"$err"; then
|
|
945
|
+
fail "seed allowed secondmate home to reuse FM_ROOT"
|
|
946
|
+
fi
|
|
947
|
+
grep -F 'secondmate home cannot be the firstmate repo' "$err" >/dev/null \
|
|
948
|
+
|| fail "seed did not explain FM_ROOT rejection"
|
|
949
|
+
|
|
950
|
+
git clone --quiet "$ROOT" "$root_clone"
|
|
951
|
+
if FM_HOME="$home" FM_ROOT_OVERRIDE="$root_clone" "$ROOT/bin/fm-home-seed.sh" design "$root_descendant" alpha >/dev/null 2>"$err"; then
|
|
952
|
+
fail "seed allowed secondmate home inside FM_ROOT"
|
|
953
|
+
fi
|
|
954
|
+
grep -F 'secondmate home cannot be inside the firstmate repo' "$err" >/dev/null \
|
|
955
|
+
|| fail "seed did not explain FM_ROOT descendant rejection"
|
|
956
|
+
[ ! -e "$root_clone/tmp" ] || fail "seed created a directory inside FM_ROOT before descendant rejection"
|
|
957
|
+
|
|
958
|
+
git clone --quiet "$ROOT" "$root_ancestor"
|
|
959
|
+
git clone --quiet "$ROOT" "$root_inside"
|
|
960
|
+
if FM_HOME="$home" FM_ROOT_OVERRIDE="$root_inside" "$ROOT/bin/fm-home-seed.sh" design "$root_ancestor" alpha >/dev/null 2>"$err"; then
|
|
961
|
+
fail "seed allowed secondmate home to contain FM_ROOT"
|
|
962
|
+
fi
|
|
963
|
+
grep -F 'secondmate home cannot be an ancestor of the firstmate repo' "$err" >/dev/null \
|
|
964
|
+
|| fail "seed did not explain FM_ROOT ancestor rejection"
|
|
965
|
+
[ ! -f "$root_ancestor/.fm-secondmate-home" ] || fail "seed marked an ancestor of FM_ROOT"
|
|
966
|
+
pass "home seeding refuses active home and repo root"
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
test_home_seed_refuses_home_marked_for_another_id() {
|
|
970
|
+
local home subhome err
|
|
971
|
+
home="$TMP_ROOT/marked-seed-home"
|
|
972
|
+
subhome="$TMP_ROOT/marked-seed-subhome"
|
|
973
|
+
err="$TMP_ROOT/marked-seed.err"
|
|
974
|
+
mkdir -p "$home/projects" "$home/data" "$home/state"
|
|
975
|
+
fm_git_init_commit "$home/projects/alpha"
|
|
976
|
+
fm_git_add_origin "$home/projects/alpha" "$TMP_ROOT/remotes/marked-alpha.git"
|
|
977
|
+
git clone --quiet "$ROOT" "$subhome"
|
|
978
|
+
printf 'other\n' > "$subhome/.fm-secondmate-home"
|
|
979
|
+
printf '%s\n' '- alpha [direct-PR] - alpha project (added 2026-06-22)' > "$home/data/projects.md"
|
|
980
|
+
scaffold_secondmate_charter "$home" design 'design domain' alpha || fail "charter scaffold failed for marked-home seed test"
|
|
981
|
+
|
|
982
|
+
if FM_HOME="$home" "$ROOT/bin/fm-home-seed.sh" design "$subhome" alpha >/dev/null 2>"$err"; then
|
|
983
|
+
fail "seed reused a home marked for another secondmate"
|
|
984
|
+
fi
|
|
985
|
+
grep -F 'already marked for other' "$err" >/dev/null || fail "seed did not explain marked-home rejection"
|
|
986
|
+
[ "$(cat "$subhome/.fm-secondmate-home")" = "other" ] || fail "seed overwrote another secondmate marker"
|
|
987
|
+
pass "home seeding refuses homes marked for another id"
|
|
988
|
+
}
|
|
989
|
+
|
|
990
|
+
test_home_seed_refuses_home_registered_to_another_id() {
|
|
991
|
+
local home subhome subhome_abs err
|
|
992
|
+
home="$TMP_ROOT/registered-seed-home"
|
|
993
|
+
subhome="$TMP_ROOT/registered-seed-subhome"
|
|
994
|
+
err="$TMP_ROOT/registered-seed.err"
|
|
995
|
+
mkdir -p "$home/projects" "$home/data" "$home/state"
|
|
996
|
+
fm_git_init_commit "$home/projects/alpha"
|
|
997
|
+
fm_git_add_origin "$home/projects/alpha" "$TMP_ROOT/remotes/registered-alpha.git"
|
|
998
|
+
git clone --quiet "$ROOT" "$subhome"
|
|
999
|
+
subhome_abs=$(cd "$subhome" && pwd -P)
|
|
1000
|
+
printf '%s\n' '- alpha [direct-PR] - alpha project (added 2026-06-22)' > "$home/data/projects.md"
|
|
1001
|
+
printf '%s\n' '- other - other domain (home: '"$subhome_abs"'; scope: other domain; projects: beta; added 2026-06-22)' > "$home/data/secondmates.md"
|
|
1002
|
+
scaffold_secondmate_charter "$home" design 'design domain' alpha || fail "charter scaffold failed for registered-home seed test"
|
|
1003
|
+
|
|
1004
|
+
if FM_HOME="$home" "$ROOT/bin/fm-home-seed.sh" design "$subhome" alpha >/dev/null 2>"$err"; then
|
|
1005
|
+
fail "seed reused a home registered to another secondmate"
|
|
1006
|
+
fi
|
|
1007
|
+
grep -F 'already registered to other' "$err" >/dev/null || fail "seed did not explain registered-home rejection"
|
|
1008
|
+
[ ! -e "$subhome/.fm-secondmate-home" ] || fail "seed wrote a marker before rejecting a registered home"
|
|
1009
|
+
pass "home seeding refuses homes registered to another id"
|
|
1010
|
+
}
|
|
1011
|
+
|
|
1012
|
+
test_home_seed_refuses_reassigning_existing_id_to_different_home() {
|
|
1013
|
+
local home first second first_abs second_abs err
|
|
1014
|
+
home="$TMP_ROOT/reassign-id-home"
|
|
1015
|
+
first="$TMP_ROOT/reassign-id-first"
|
|
1016
|
+
second="$TMP_ROOT/reassign-id-second"
|
|
1017
|
+
err="$TMP_ROOT/reassign-id.err"
|
|
1018
|
+
mkdir -p "$home/projects" "$home/data" "$home/state"
|
|
1019
|
+
fm_git_init_commit "$home/projects/alpha"
|
|
1020
|
+
fm_git_add_origin "$home/projects/alpha" "$TMP_ROOT/remotes/reassign-alpha.git"
|
|
1021
|
+
printf '%s\n' '- alpha [direct-PR] - alpha project (added 2026-06-22)' > "$home/data/projects.md"
|
|
1022
|
+
|
|
1023
|
+
FM_HOME="$home" FM_SECONDMATE_CHARTER='design domain' FM_SECONDMATE_SCOPE='design domain' \
|
|
1024
|
+
"$ROOT/bin/fm-home-seed.sh" design "$first" alpha >/dev/null \
|
|
1025
|
+
|| fail "initial seed failed for reassigning-id test"
|
|
1026
|
+
first_abs=$(cd "$first" && pwd -P)
|
|
1027
|
+
|
|
1028
|
+
if FM_HOME="$home" FM_SECONDMATE_CHARTER='design domain' FM_SECONDMATE_SCOPE='design domain' \
|
|
1029
|
+
"$ROOT/bin/fm-home-seed.sh" design "$second" alpha >/dev/null 2>"$err"; then
|
|
1030
|
+
fail "seed reassigned an existing secondmate id to a different home"
|
|
1031
|
+
fi
|
|
1032
|
+
grep -F "secondmate id design is already registered to home $first_abs" "$err" >/dev/null \
|
|
1033
|
+
|| fail "seed did not explain same-id different-home rejection"
|
|
1034
|
+
[ ! -e "$second" ] || fail "failed id reassignment created the new subhome"
|
|
1035
|
+
[ "$(cat "$first/.fm-secondmate-home")" = design ] || fail "failed id reassignment changed the original marker"
|
|
1036
|
+
grep -F "home: $first_abs" "$home/data/secondmates.md" >/dev/null \
|
|
1037
|
+
|| fail "failed id reassignment did not preserve the original registry route"
|
|
1038
|
+
second_abs=$(cd "$(dirname "$second")" && printf '%s/%s\n' "$(pwd -P)" "$(basename "$second")")
|
|
1039
|
+
grep -F "home: $second_abs" "$home/data/secondmates.md" >/dev/null \
|
|
1040
|
+
&& fail "failed id reassignment recorded the rejected home"
|
|
1041
|
+
pass "home seeding refuses same-id reassignment to a different home"
|
|
1042
|
+
}
|
|
1043
|
+
|
|
1044
|
+
test_home_seed_refuses_home_overlapping_registered_home() {
|
|
1045
|
+
local home registered_parent registered_child nested parent err
|
|
1046
|
+
home="$TMP_ROOT/overlap-seed-home"
|
|
1047
|
+
registered_parent="$TMP_ROOT/overlap-registered-parent"
|
|
1048
|
+
registered_child="$TMP_ROOT/overlap-registered-child-parent/child"
|
|
1049
|
+
nested="$registered_parent/nested"
|
|
1050
|
+
parent="$TMP_ROOT/overlap-registered-child-parent"
|
|
1051
|
+
err="$TMP_ROOT/overlap-seed.err"
|
|
1052
|
+
mkdir -p "$home/projects" "$home/data" "$home/state"
|
|
1053
|
+
fm_git_init_commit "$home/projects/alpha"
|
|
1054
|
+
fm_git_add_origin "$home/projects/alpha" "$TMP_ROOT/remotes/overlap-alpha.git"
|
|
1055
|
+
git clone --quiet "$ROOT" "$registered_parent"
|
|
1056
|
+
git clone --quiet "$ROOT" "$registered_child"
|
|
1057
|
+
printf '%s\n' '- alpha [direct-PR] - alpha project (added 2026-06-22)' > "$home/data/projects.md"
|
|
1058
|
+
cat > "$home/data/secondmates.md" <<EOF
|
|
1059
|
+
- parent - parent domain (home: $registered_parent; scope: parent domain; projects: beta; added 2026-06-22)
|
|
1060
|
+
- child - child domain (home: $registered_child; scope: child domain; projects: gamma; added 2026-06-22)
|
|
1061
|
+
EOF
|
|
1062
|
+
|
|
1063
|
+
if FM_HOME="$home" "$ROOT/bin/fm-home-seed.sh" design "$nested" alpha >/dev/null 2>"$err"; then
|
|
1064
|
+
fail "seed accepted a home inside a registered secondmate home"
|
|
1065
|
+
fi
|
|
1066
|
+
grep -F 'overlaps registered secondmate home' "$err" >/dev/null \
|
|
1067
|
+
|| fail "seed did not explain registered ancestor overlap"
|
|
1068
|
+
[ ! -e "$nested" ] || fail "seed created a nested home inside a registered home"
|
|
1069
|
+
|
|
1070
|
+
if FM_HOME="$home" "$ROOT/bin/fm-home-seed.sh" design "$parent" alpha >/dev/null 2>"$err"; then
|
|
1071
|
+
fail "seed accepted a home containing a registered secondmate home"
|
|
1072
|
+
fi
|
|
1073
|
+
grep -F 'overlaps registered secondmate home' "$err" >/dev/null \
|
|
1074
|
+
|| fail "seed did not explain registered descendant overlap"
|
|
1075
|
+
[ ! -f "$parent/.fm-secondmate-home" ] || fail "seed marked a home containing a registered home"
|
|
1076
|
+
pass "home seeding refuses registered home overlaps"
|
|
1077
|
+
}
|
|
1078
|
+
|
|
1079
|
+
test_home_seed_refuses_remote_backed_project_without_origin() {
|
|
1080
|
+
local home subhome err
|
|
1081
|
+
home="$TMP_ROOT/no-origin-home"
|
|
1082
|
+
subhome="$TMP_ROOT/no-origin-subhome"
|
|
1083
|
+
err="$TMP_ROOT/no-origin.err"
|
|
1084
|
+
mkdir -p "$home/projects" "$home/data" "$home/state"
|
|
1085
|
+
fm_git_init_commit "$home/projects/alpha"
|
|
1086
|
+
printf '%s\n' '- alpha [direct-PR] - alpha project (added 2026-06-22)' > "$home/data/projects.md"
|
|
1087
|
+
scaffold_secondmate_charter "$home" design 'design domain' alpha || fail "charter scaffold failed for no-origin seed test"
|
|
1088
|
+
|
|
1089
|
+
if FM_HOME="$home" "$ROOT/bin/fm-home-seed.sh" design "$subhome" alpha >/dev/null 2>"$err"; then
|
|
1090
|
+
fail "seed allowed remote-backed project without origin"
|
|
1091
|
+
fi
|
|
1092
|
+
grep -F 'project alpha is direct-PR but has no origin remote' "$err" >/dev/null || fail "seed did not explain missing origin for remote-backed project"
|
|
1093
|
+
pass "remote-backed subhome seeding requires a source origin"
|
|
1094
|
+
}
|
|
1095
|
+
|
|
1096
|
+
test_home_seed_refuses_existing_remote_backed_project_with_wrong_origin() {
|
|
1097
|
+
local home subhome subhome_abs err expected
|
|
1098
|
+
home="$TMP_ROOT/wrong-origin-home"
|
|
1099
|
+
subhome="$TMP_ROOT/wrong-origin-subhome"
|
|
1100
|
+
err="$TMP_ROOT/wrong-origin.err"
|
|
1101
|
+
mkdir -p "$home/projects" "$home/data" "$home/state"
|
|
1102
|
+
fm_git_init_commit "$home/projects/alpha"
|
|
1103
|
+
fm_git_add_origin "$home/projects/alpha" "$TMP_ROOT/remotes/wrong-alpha.git"
|
|
1104
|
+
git clone --quiet "$ROOT" "$subhome"
|
|
1105
|
+
subhome_abs=$(cd "$subhome" && pwd -P)
|
|
1106
|
+
mkdir -p "$subhome/projects"
|
|
1107
|
+
git clone --quiet "$home/projects/alpha" "$subhome/projects/alpha"
|
|
1108
|
+
printf '%s\n' '- alpha [direct-PR] - alpha project (added 2026-06-22)' > "$home/data/projects.md"
|
|
1109
|
+
scaffold_secondmate_charter "$home" design 'design domain' alpha || fail "charter scaffold failed for wrong-origin seed test"
|
|
1110
|
+
|
|
1111
|
+
if FM_HOME="$home" "$ROOT/bin/fm-home-seed.sh" design "$subhome" alpha >/dev/null 2>"$err"; then
|
|
1112
|
+
fail "seed accepted existing remote-backed project with wrong origin"
|
|
1113
|
+
fi
|
|
1114
|
+
expected=$(git -C "$home/projects/alpha" remote get-url origin)
|
|
1115
|
+
grep -F "seeded project alpha at $subhome_abs/projects/alpha has origin" "$err" >/dev/null \
|
|
1116
|
+
|| fail "seed did not identify wrong origin for existing remote-backed project"
|
|
1117
|
+
grep -F "expected $expected" "$err" >/dev/null \
|
|
1118
|
+
|| fail "seed did not report expected origin for existing remote-backed project"
|
|
1119
|
+
pass "remote-backed subhome seeding validates existing destination origins"
|
|
1120
|
+
}
|
|
1121
|
+
|
|
1122
|
+
test_home_seed_resolves_relative_source_origins() {
|
|
1123
|
+
local home subhome subhome_abs expected out actual
|
|
1124
|
+
home="$TMP_ROOT/relative-origin-home"
|
|
1125
|
+
subhome="$TMP_ROOT/relative-origin-subhome"
|
|
1126
|
+
mkdir -p "$home/projects" "$home/data" "$home/state" "$home/remotes"
|
|
1127
|
+
fm_git_init_commit "$home/projects/alpha"
|
|
1128
|
+
git clone --quiet --bare "$home/projects/alpha" "$home/remotes/relative-alpha.git"
|
|
1129
|
+
git -C "$home/projects/alpha" remote add origin ../../remotes/relative-alpha.git
|
|
1130
|
+
printf '%s\n' '- alpha [direct-PR] - alpha project (added 2026-06-22)' > "$home/data/projects.md"
|
|
1131
|
+
scaffold_secondmate_charter "$home" design 'design domain' alpha || fail "charter scaffold failed for relative origin seed test"
|
|
1132
|
+
|
|
1133
|
+
out=$(FM_HOME="$home" "$ROOT/bin/fm-home-seed.sh" design "$subhome" alpha)
|
|
1134
|
+
subhome_abs=$(cd "$subhome" && pwd -P)
|
|
1135
|
+
expected=$(cd "$home/remotes/relative-alpha.git" && pwd -P)
|
|
1136
|
+
printf '%s\n' "$out" | grep -F "home=$subhome_abs" >/dev/null || fail "seed did not report relative-origin subhome"
|
|
1137
|
+
[ -d "$subhome/projects/alpha/.git" ] || fail "relative source origin was not cloned"
|
|
1138
|
+
actual=$(git -C "$subhome/projects/alpha" remote get-url origin)
|
|
1139
|
+
[ "$actual" = "$expected" ] || fail "relative source origin was not cloned through the resolved path"
|
|
1140
|
+
FM_HOME="$home" "$ROOT/bin/fm-home-seed.sh" design "$subhome" alpha >/dev/null \
|
|
1141
|
+
|| fail "relative source origin did not compare equal on reseed"
|
|
1142
|
+
pass "home seeding resolves relative source origins against the source project"
|
|
1143
|
+
}
|
|
1144
|
+
|
|
1145
|
+
test_home_seed_skips_initialized_existing_no_mistakes_projects() {
|
|
1146
|
+
local home subhome err fakebin log origin
|
|
1147
|
+
home="$TMP_ROOT/existing-initialized-home"
|
|
1148
|
+
subhome="$TMP_ROOT/existing-initialized-subhome"
|
|
1149
|
+
err="$TMP_ROOT/existing-initialized.err"
|
|
1150
|
+
log="$TMP_ROOT/existing-initialized-no-mistakes.log"
|
|
1151
|
+
mkdir -p "$home/projects" "$home/data" "$home/state"
|
|
1152
|
+
fm_git_init_commit "$home/projects/alpha"
|
|
1153
|
+
fm_git_init_commit "$home/projects/beta"
|
|
1154
|
+
fm_git_add_origin "$home/projects/alpha" "$TMP_ROOT/remotes/existing-alpha.git"
|
|
1155
|
+
fm_git_add_origin "$home/projects/beta" "$TMP_ROOT/remotes/existing-beta.git"
|
|
1156
|
+
git clone --quiet "$ROOT" "$subhome"
|
|
1157
|
+
mkdir -p "$subhome/projects"
|
|
1158
|
+
origin=$(git -C "$home/projects/alpha" remote get-url origin)
|
|
1159
|
+
git clone --quiet "$origin" "$subhome/projects/alpha"
|
|
1160
|
+
git -C "$subhome/projects/alpha" remote add no-mistakes "$TMP_ROOT/no-mistakes-alpha.git"
|
|
1161
|
+
printf '%s\n' '- alpha - alpha project (added 2026-06-22)' '- beta - beta project (added 2026-06-22)' > "$home/data/projects.md"
|
|
1162
|
+
fakebin=$(make_recording_no_mistakes "$TMP_ROOT/existing-initialized-fake")
|
|
1163
|
+
: > "$log"
|
|
1164
|
+
|
|
1165
|
+
if PATH="$fakebin:$PATH" FM_FAKE_NO_MISTAKES_LOG="$log" FM_FAKE_NO_MISTAKES_FAIL_PROJECT=beta \
|
|
1166
|
+
FM_HOME="$home" FM_SECONDMATE_CHARTER='existing init rollback scope' FM_SECONDMATE_SCOPE='existing init rollback scope' \
|
|
1167
|
+
"$ROOT/bin/fm-home-seed.sh" design "$subhome" alpha beta >/dev/null 2>"$err"; then
|
|
1168
|
+
fail "seed succeeded even though later no-mistakes initialization failed"
|
|
1169
|
+
fi
|
|
1170
|
+
grep -F 'failed to initialize no-mistakes for beta' "$err" >/dev/null \
|
|
1171
|
+
|| fail "seed did not explain later no-mistakes initialization failure"
|
|
1172
|
+
grep -F "$subhome/projects/alpha" "$log" >/dev/null \
|
|
1173
|
+
&& fail "seed ran no-mistakes against an initialized existing clone"
|
|
1174
|
+
[ ! -f "$subhome/projects/alpha/.no-mistakes-init" ] || fail "seed mutated initialized existing clone with no-mistakes init"
|
|
1175
|
+
[ ! -f "$subhome/projects/alpha/.no-mistakes-doctor" ] || fail "seed mutated initialized existing clone with no-mistakes doctor"
|
|
1176
|
+
[ ! -e "$subhome/projects/beta" ] || fail "failed seed left a newly cloned project after no-mistakes failure"
|
|
1177
|
+
pass "home seeding skips initialized existing no-mistakes clones"
|
|
1178
|
+
}
|
|
1179
|
+
|
|
1180
|
+
test_home_seed_refuses_uninitialized_existing_no_mistakes_project() {
|
|
1181
|
+
local home subhome err fakebin log origin
|
|
1182
|
+
home="$TMP_ROOT/existing-uninitialized-home"
|
|
1183
|
+
subhome="$TMP_ROOT/existing-uninitialized-subhome"
|
|
1184
|
+
err="$TMP_ROOT/existing-uninitialized.err"
|
|
1185
|
+
log="$TMP_ROOT/existing-uninitialized-no-mistakes.log"
|
|
1186
|
+
mkdir -p "$home/projects" "$home/data" "$home/state"
|
|
1187
|
+
fm_git_init_commit "$home/projects/alpha"
|
|
1188
|
+
fm_git_add_origin "$home/projects/alpha" "$TMP_ROOT/remotes/uninitialized-alpha.git"
|
|
1189
|
+
git clone --quiet "$ROOT" "$subhome"
|
|
1190
|
+
mkdir -p "$subhome/projects"
|
|
1191
|
+
origin=$(git -C "$home/projects/alpha" remote get-url origin)
|
|
1192
|
+
git clone --quiet "$origin" "$subhome/projects/alpha"
|
|
1193
|
+
printf '%s\n' '- alpha - alpha project (added 2026-06-22)' > "$home/data/projects.md"
|
|
1194
|
+
fakebin=$(make_recording_no_mistakes "$TMP_ROOT/existing-uninitialized-fake")
|
|
1195
|
+
: > "$log"
|
|
1196
|
+
|
|
1197
|
+
if PATH="$fakebin:$PATH" FM_FAKE_NO_MISTAKES_LOG="$log" \
|
|
1198
|
+
FM_HOME="$home" FM_SECONDMATE_CHARTER='existing uninitialized scope' \
|
|
1199
|
+
"$ROOT/bin/fm-home-seed.sh" design "$subhome" alpha >/dev/null 2>"$err"; then
|
|
1200
|
+
fail "seed initialized a preexisting no-mistakes clone"
|
|
1201
|
+
fi
|
|
1202
|
+
grep -F 'refusing to mutate preexisting clone' "$err" >/dev/null \
|
|
1203
|
+
|| fail "seed did not explain uninitialized existing no-mistakes clone refusal"
|
|
1204
|
+
[ ! -s "$log" ] || fail "seed ran no-mistakes before refusing an uninitialized existing clone"
|
|
1205
|
+
[ ! -f "$subhome/projects/alpha/.no-mistakes-init" ] || fail "seed mutated uninitialized existing clone"
|
|
1206
|
+
pass "home seeding refuses uninitialized existing no-mistakes clones"
|
|
1207
|
+
}
|
|
1208
|
+
|
|
1209
|
+
test_home_seed_refuses_project_destinations_outside_subhome() {
|
|
1210
|
+
local home subhome sink err
|
|
1211
|
+
home="$TMP_ROOT/symlink-project-home"
|
|
1212
|
+
subhome="$TMP_ROOT/symlink-project-subhome"
|
|
1213
|
+
sink="$home/data/symlink-projects"
|
|
1214
|
+
err="$TMP_ROOT/symlink-project.err"
|
|
1215
|
+
mkdir -p "$home/projects" "$home/data" "$home/state" "$sink"
|
|
1216
|
+
fm_git_init_commit "$home/projects/alpha"
|
|
1217
|
+
fm_git_add_origin "$home/projects/alpha" "$TMP_ROOT/remotes/symlink-alpha.git"
|
|
1218
|
+
git clone --quiet "$ROOT" "$subhome"
|
|
1219
|
+
rm -rf "$subhome/projects"
|
|
1220
|
+
ln -s "$sink" "$subhome/projects"
|
|
1221
|
+
printf '%s\n' '- alpha [direct-PR] - alpha project (added 2026-06-22)' > "$home/data/projects.md"
|
|
1222
|
+
scaffold_secondmate_charter "$home" design 'design domain' alpha || fail "charter scaffold failed for symlink destination seed test"
|
|
1223
|
+
|
|
1224
|
+
if FM_HOME="$home" "$ROOT/bin/fm-home-seed.sh" design "$subhome" alpha >/dev/null 2>"$err"; then
|
|
1225
|
+
fail "seed followed a subhome projects symlink outside the subhome"
|
|
1226
|
+
fi
|
|
1227
|
+
grep -F 'secondmate projects directory must resolve inside the secondmate home' "$err" >/dev/null \
|
|
1228
|
+
|| fail "seed did not explain unsafe project destination rejection"
|
|
1229
|
+
[ ! -e "$sink/alpha" ] || fail "seed cloned a project through an unsafe projects symlink"
|
|
1230
|
+
[ ! -f "$subhome/.fm-secondmate-home" ] || fail "seed marked subhome after unsafe project destination rejection"
|
|
1231
|
+
pass "home seeding refuses project destinations outside the subhome"
|
|
1232
|
+
}
|
|
1233
|
+
|
|
1234
|
+
test_home_seed_refuses_operational_dirs_outside_subhome() {
|
|
1235
|
+
local home subhome sink err opdir
|
|
1236
|
+
home="$TMP_ROOT/symlink-opdir-home"
|
|
1237
|
+
err="$TMP_ROOT/symlink-opdir.err"
|
|
1238
|
+
mkdir -p "$home/projects" "$home/data" "$home/state"
|
|
1239
|
+
fm_git_init_commit "$home/projects/alpha"
|
|
1240
|
+
fm_git_add_origin "$home/projects/alpha" "$TMP_ROOT/remotes/symlink-opdir-alpha.git"
|
|
1241
|
+
printf '%s\n' '- alpha [direct-PR] - alpha project (added 2026-06-22)' > "$home/data/projects.md"
|
|
1242
|
+
scaffold_secondmate_charter "$home" design 'design domain' alpha || fail "charter scaffold failed for symlink operational dir seed test"
|
|
1243
|
+
|
|
1244
|
+
for opdir in data state config; do
|
|
1245
|
+
subhome="$TMP_ROOT/symlink-opdir-subhome-$opdir"
|
|
1246
|
+
sink="$home/data/symlink-opdir-$opdir"
|
|
1247
|
+
rm -rf "$subhome" "$sink"
|
|
1248
|
+
git clone --quiet "$ROOT" "$subhome"
|
|
1249
|
+
mkdir -p "$sink"
|
|
1250
|
+
rm -rf "${subhome:?}/${opdir:?}"
|
|
1251
|
+
ln -s "$sink" "$subhome/$opdir"
|
|
1252
|
+
if FM_HOME="$home" "$ROOT/bin/fm-home-seed.sh" design "$subhome" alpha >/dev/null 2>"$err"; then
|
|
1253
|
+
fail "seed accepted a subhome with $opdir symlinked outside the subhome"
|
|
1254
|
+
fi
|
|
1255
|
+
grep -F "secondmate $opdir directory must resolve inside the secondmate home" "$err" >/dev/null \
|
|
1256
|
+
|| fail "seed did not explain unsafe $opdir directory rejection"
|
|
1257
|
+
[ ! -f "$subhome/.fm-secondmate-home" ] || fail "seed marked subhome after unsafe $opdir directory rejection"
|
|
1258
|
+
done
|
|
1259
|
+
pass "home seeding refuses operational directories outside the subhome"
|
|
1260
|
+
}
|
|
1261
|
+
|
|
1262
|
+
test_home_seed_refuses_unsafe_leaf_files() {
|
|
1263
|
+
local home subhome sink err leaf target expected
|
|
1264
|
+
home="$TMP_ROOT/symlink-leaf-home"
|
|
1265
|
+
err="$TMP_ROOT/symlink-leaf.err"
|
|
1266
|
+
mkdir -p "$home/projects" "$home/data" "$home/state"
|
|
1267
|
+
fm_git_init_commit "$home/projects/alpha"
|
|
1268
|
+
fm_git_add_origin "$home/projects/alpha" "$TMP_ROOT/remotes/symlink-leaf-alpha.git"
|
|
1269
|
+
printf '%s\n' '- alpha [direct-PR] - alpha project (added 2026-06-22)' > "$home/data/projects.md"
|
|
1270
|
+
scaffold_secondmate_charter "$home" design 'design domain' alpha || fail "charter scaffold failed for symlink leaf seed test"
|
|
1271
|
+
|
|
1272
|
+
for leaf in data/projects.md data/charter.md .fm-secondmate-home .fm-secondmate-parent; do
|
|
1273
|
+
subhome="$TMP_ROOT/symlink-leaf-subhome-${leaf//\//-}"
|
|
1274
|
+
sink="$home/data/symlink-leaf-${leaf//\//-}"
|
|
1275
|
+
rm -rf "$subhome" "$sink"
|
|
1276
|
+
git clone --quiet "$ROOT" "$subhome"
|
|
1277
|
+
mkdir -p "$(dirname "$subhome/$leaf")" "$(dirname "$sink")"
|
|
1278
|
+
expected=outside
|
|
1279
|
+
if [ "$leaf" = ".fm-secondmate-home" ]; then
|
|
1280
|
+
expected=design
|
|
1281
|
+
fi
|
|
1282
|
+
printf '%s\n' "$expected" > "$sink"
|
|
1283
|
+
ln -s "$sink" "$subhome/$leaf"
|
|
1284
|
+
if FM_HOME="$home" "$ROOT/bin/fm-home-seed.sh" design "$subhome" alpha >/dev/null 2>"$err"; then
|
|
1285
|
+
fail "seed accepted symlinked leaf file $leaf"
|
|
1286
|
+
fi
|
|
1287
|
+
grep -F 'secondmate leaf file must not be a symlink:' "$err" >/dev/null \
|
|
1288
|
+
|| fail "seed did not explain symlinked leaf refusal for $leaf"
|
|
1289
|
+
target=$(cat "$sink")
|
|
1290
|
+
[ "$target" = "$expected" ] || fail "seed overwrote outside symlink target for $leaf"
|
|
1291
|
+
[ ! -f "$subhome/.fm-secondmate-home" ] || [ "$leaf" = ".fm-secondmate-home" ] || fail "seed marked subhome after symlinked leaf refusal"
|
|
1292
|
+
done
|
|
1293
|
+
for leaf in data/projects.md data/charter.md .fm-secondmate-home .fm-secondmate-parent; do
|
|
1294
|
+
subhome="$TMP_ROOT/directory-leaf-subhome-${leaf//\//-}"
|
|
1295
|
+
rm -rf "$subhome"
|
|
1296
|
+
git clone --quiet "$ROOT" "$subhome"
|
|
1297
|
+
mkdir -p "$subhome/$leaf"
|
|
1298
|
+
if FM_HOME="$home" "$ROOT/bin/fm-home-seed.sh" design "$subhome" alpha >/dev/null 2>"$err"; then
|
|
1299
|
+
fail "seed accepted directory leaf $leaf"
|
|
1300
|
+
fi
|
|
1301
|
+
grep -F 'secondmate leaf file must be a regular file:' "$err" >/dev/null \
|
|
1302
|
+
|| fail "seed did not explain directory leaf refusal for $leaf"
|
|
1303
|
+
[ -d "$subhome/$leaf" ] || fail "seed changed directory leaf $leaf"
|
|
1304
|
+
[ ! -f "$subhome/.fm-secondmate-home" ] \
|
|
1305
|
+
|| fail "seed published an identity marker after directory leaf refusal for $leaf"
|
|
1306
|
+
done
|
|
1307
|
+
pass "home seeding refuses symlinked and non-regular leaf files"
|
|
1308
|
+
}
|
|
1309
|
+
|
|
1310
|
+
test_home_seed_preserves_existing_parent_binding() {
|
|
1311
|
+
local parent_a parent_b child child_abs before err out parent_a_abs parent_b_abs leaf
|
|
1312
|
+
parent_a="$TMP_ROOT/reseed-parent-a"
|
|
1313
|
+
parent_b="$TMP_ROOT/reseed-parent-b"
|
|
1314
|
+
child="$TMP_ROOT/reseed-parent-child"
|
|
1315
|
+
before="$TMP_ROOT/reseed-parent-before"
|
|
1316
|
+
err="$TMP_ROOT/reseed-parent.err"
|
|
1317
|
+
mkdir -p "$parent_a/data" "$parent_a/state" "$parent_a/projects" \
|
|
1318
|
+
"$parent_b/data" "$parent_b/state" "$parent_b/projects" "$before/data"
|
|
1319
|
+
|
|
1320
|
+
FM_HOME="$parent_a" FM_SECONDMATE_CHARTER='Durable parent reseed charter.' \
|
|
1321
|
+
FM_SECONDMATE_SCOPE='durable parent reseed scope' \
|
|
1322
|
+
"$ROOT/bin/fm-home-seed.sh" mate "$child" --no-projects >/dev/null \
|
|
1323
|
+
|| fail "initial durable-parent seed failed"
|
|
1324
|
+
parent_a_abs=$(cd "$parent_a" && pwd -P)
|
|
1325
|
+
parent_b_abs=$(cd "$parent_b" && pwd -P)
|
|
1326
|
+
child_abs=$(cd "$child" && pwd -P)
|
|
1327
|
+
for leaf in data/projects.md data/charter.md .fm-secondmate-home .fm-secondmate-parent; do
|
|
1328
|
+
mkdir -p "$before/$(dirname "$leaf")"
|
|
1329
|
+
cp "$child/$leaf" "$before/$leaf"
|
|
1330
|
+
done
|
|
1331
|
+
|
|
1332
|
+
if FM_HOME="$parent_b" FM_SECONDMATE_CHARTER='Replacement parent charter.' \
|
|
1333
|
+
FM_SECONDMATE_SCOPE='replacement parent scope' \
|
|
1334
|
+
"$ROOT/bin/fm-home-seed.sh" mate "$child" --no-projects > /dev/null 2>"$err"; then
|
|
1335
|
+
fail "reseed replaced a valid durable parent binding"
|
|
1336
|
+
fi
|
|
1337
|
+
grep -F "bound to parent $parent_a_abs, not requested parent $parent_b_abs" "$err" >/dev/null \
|
|
1338
|
+
|| fail "mismatched-parent reseed did not name both parent identities"
|
|
1339
|
+
for leaf in data/projects.md data/charter.md .fm-secondmate-home .fm-secondmate-parent; do
|
|
1340
|
+
cmp -s "$before/$leaf" "$child/$leaf" \
|
|
1341
|
+
|| fail "mismatched-parent reseed changed $leaf"
|
|
1342
|
+
done
|
|
1343
|
+
[ ! -e "$parent_b/data/mate/brief.md" ] \
|
|
1344
|
+
|| fail "mismatched-parent reseed created a replacement parent brief"
|
|
1345
|
+
[ ! -e "$parent_b/data/secondmates.md" ] \
|
|
1346
|
+
|| fail "mismatched-parent reseed registered the child to the replacement parent"
|
|
1347
|
+
|
|
1348
|
+
out=$(FM_HOME="$parent_a" "$ROOT/bin/fm-home-seed.sh" mate "$child" --no-projects) \
|
|
1349
|
+
|| fail "matching-parent reseed failed"
|
|
1350
|
+
printf '%s\n' "$out" | grep -F "home=$child_abs" >/dev/null \
|
|
1351
|
+
|| fail "matching-parent reseed did not report success"
|
|
1352
|
+
cmp -s "$before/.fm-secondmate-parent" "$child/.fm-secondmate-parent" \
|
|
1353
|
+
|| fail "matching-parent reseed changed the durable parent binding"
|
|
1354
|
+
pass "home reseeding preserves and enforces the durable parent binding"
|
|
1355
|
+
}
|
|
1356
|
+
|
|
1357
|
+
test_secondmate_spawn_requires_seeded_matching_home() {
|
|
1358
|
+
local home subhome wronghome marker_only active_descendant active_ancestor ancestor_active_home fakeroot root_descendant root_ancestor root_inside fakebin log err
|
|
1359
|
+
home="$TMP_ROOT/spawn-validate-home"
|
|
1360
|
+
subhome="$TMP_ROOT/spawn-validate-subhome"
|
|
1361
|
+
wronghome="$TMP_ROOT/spawn-validate-wronghome"
|
|
1362
|
+
marker_only="$TMP_ROOT/spawn-validate-marker-only"
|
|
1363
|
+
active_descendant="$home/data/spawn-descendant-home"
|
|
1364
|
+
active_ancestor="$TMP_ROOT/spawn-active-ancestor"
|
|
1365
|
+
ancestor_active_home="$active_ancestor/main-home"
|
|
1366
|
+
fakeroot="$TMP_ROOT/spawn-validate-root"
|
|
1367
|
+
root_descendant="$fakeroot/tmp/spawn-descendant-home"
|
|
1368
|
+
root_ancestor="$TMP_ROOT/spawn-root-ancestor"
|
|
1369
|
+
root_inside="$root_ancestor/repo"
|
|
1370
|
+
mkdir -p "$home/data" "$home/state" "$subhome/data" "$wronghome/data" "$marker_only/data" "$active_descendant/data" "$root_descendant/data" "$fakeroot/bin"
|
|
1371
|
+
cat > "$fakeroot/bin/fm-guard.sh" <<'SH'
|
|
1372
|
+
#!/usr/bin/env bash
|
|
1373
|
+
exit 0
|
|
1374
|
+
SH
|
|
1375
|
+
chmod +x "$fakeroot/bin/fm-guard.sh"
|
|
1376
|
+
mkdir -p "$ancestor_active_home/data" "$ancestor_active_home/state" "$active_ancestor/data" "$root_ancestor/data" "$root_inside/bin"
|
|
1377
|
+
cat > "$root_inside/bin/fm-guard.sh" <<'SH'
|
|
1378
|
+
#!/usr/bin/env bash
|
|
1379
|
+
exit 0
|
|
1380
|
+
SH
|
|
1381
|
+
chmod +x "$root_inside/bin/fm-guard.sh"
|
|
1382
|
+
fakebin=$(make_fake_tmux "$TMP_ROOT/spawn-validate-fake")
|
|
1383
|
+
log="$TMP_ROOT/spawn-validate-fake/tmux.log"
|
|
1384
|
+
err="$TMP_ROOT/spawn-validate.err"
|
|
1385
|
+
|
|
1386
|
+
if PATH="$fakebin:$PATH" FM_HOME="$home" FM_FAKE_TMUX_LOG="$log" FM_FAKE_TMUX_CAPTURE="$TMP_ROOT/spawn-validate-fake/pane.txt" \
|
|
1387
|
+
"$ROOT/bin/fm-spawn.sh" domain "$subhome" codex --secondmate >/dev/null 2>"$err"; then
|
|
1388
|
+
fail "secondmate spawn accepted an unseeded home"
|
|
1389
|
+
fi
|
|
1390
|
+
grep -F 'not a seeded secondmate home' "$err" >/dev/null || fail "spawn did not explain missing seed marker"
|
|
1391
|
+
# Canonical ordering proof: validation runs before any tmux side-effect. Every rejection
|
|
1392
|
+
# reason below shares this one linear pre-launch path, so they each assert only their own
|
|
1393
|
+
# refusal message rather than re-proving "no window created before validation" each time.
|
|
1394
|
+
grep -F 'new-window' "$log" >/dev/null && fail "spawn created a window before validation"
|
|
1395
|
+
|
|
1396
|
+
printf 'other\n' > "$wronghome/.fm-secondmate-home"
|
|
1397
|
+
if PATH="$fakebin:$PATH" FM_HOME="$home" FM_FAKE_TMUX_LOG="$log" FM_FAKE_TMUX_CAPTURE="$TMP_ROOT/spawn-validate-fake/pane.txt" \
|
|
1398
|
+
"$ROOT/bin/fm-spawn.sh" domain "$wronghome" codex --secondmate >/dev/null 2>"$err"; then
|
|
1399
|
+
fail "secondmate spawn accepted a home marked for another secondmate"
|
|
1400
|
+
fi
|
|
1401
|
+
grep -F 'marked for secondmate other, expected domain' "$err" >/dev/null || fail "spawn did not explain marker mismatch"
|
|
1402
|
+
|
|
1403
|
+
printf 'domain\n' > "$marker_only/.fm-secondmate-home"
|
|
1404
|
+
printf 'charter\n' > "$marker_only/data/charter.md"
|
|
1405
|
+
if PATH="$fakebin:$PATH" FM_HOME="$home" FM_FAKE_TMUX_LOG="$log" FM_FAKE_TMUX_CAPTURE="$TMP_ROOT/spawn-validate-fake/pane.txt" \
|
|
1406
|
+
"$ROOT/bin/fm-spawn.sh" domain "$marker_only" codex --secondmate >/dev/null 2>"$err"; then
|
|
1407
|
+
fail "secondmate spawn accepted a marked home missing AGENTS.md"
|
|
1408
|
+
fi
|
|
1409
|
+
grep -F 'not a firstmate home (missing AGENTS.md)' "$err" >/dev/null || fail "spawn did not explain missing AGENTS.md"
|
|
1410
|
+
|
|
1411
|
+
printf '# Firstmate\n' > "$marker_only/AGENTS.md"
|
|
1412
|
+
if PATH="$fakebin:$PATH" FM_HOME="$home" FM_FAKE_TMUX_LOG="$log" FM_FAKE_TMUX_CAPTURE="$TMP_ROOT/spawn-validate-fake/pane.txt" \
|
|
1413
|
+
"$ROOT/bin/fm-spawn.sh" domain "$marker_only" codex --secondmate >/dev/null 2>"$err"; then
|
|
1414
|
+
fail "secondmate spawn accepted a marked home missing bin"
|
|
1415
|
+
fi
|
|
1416
|
+
grep -F 'not a firstmate home (missing bin/)' "$err" >/dev/null || fail "spawn did not explain missing bin"
|
|
1417
|
+
|
|
1418
|
+
printf 'domain\n' > "$home/.fm-secondmate-home"
|
|
1419
|
+
if PATH="$fakebin:$PATH" FM_HOME="$home" FM_FAKE_TMUX_LOG="$log" FM_FAKE_TMUX_CAPTURE="$TMP_ROOT/spawn-validate-fake/pane.txt" \
|
|
1420
|
+
"$ROOT/bin/fm-spawn.sh" domain "$home" codex --secondmate >/dev/null 2>"$err"; then
|
|
1421
|
+
fail "secondmate spawn accepted the active home"
|
|
1422
|
+
fi
|
|
1423
|
+
grep -F 'secondmate home cannot be the active firstmate home' "$err" >/dev/null || fail "spawn did not reject active home"
|
|
1424
|
+
|
|
1425
|
+
if PATH="$fakebin:$PATH" FM_HOME="$home" FM_FAKE_TMUX_LOG="$log" FM_FAKE_TMUX_CAPTURE="$TMP_ROOT/spawn-validate-fake/pane.txt" \
|
|
1426
|
+
"$ROOT/bin/fm-spawn.sh" domain "$ROOT" codex --secondmate >/dev/null 2>"$err"; then
|
|
1427
|
+
fail "secondmate spawn accepted the firstmate repo root"
|
|
1428
|
+
fi
|
|
1429
|
+
grep -F 'secondmate home cannot be the firstmate repo' "$err" >/dev/null || fail "spawn did not reject firstmate repo root"
|
|
1430
|
+
|
|
1431
|
+
printf 'domain\n' > "$active_descendant/.fm-secondmate-home"
|
|
1432
|
+
printf 'charter\n' > "$active_descendant/data/charter.md"
|
|
1433
|
+
if PATH="$fakebin:$PATH" FM_HOME="$home" FM_FAKE_TMUX_LOG="$log" FM_FAKE_TMUX_CAPTURE="$TMP_ROOT/spawn-validate-fake/pane.txt" \
|
|
1434
|
+
"$ROOT/bin/fm-spawn.sh" domain "$active_descendant" codex --secondmate >/dev/null 2>"$err"; then
|
|
1435
|
+
fail "secondmate spawn accepted a home inside the active firstmate home"
|
|
1436
|
+
fi
|
|
1437
|
+
grep -F 'secondmate home cannot be inside the active firstmate home' "$err" >/dev/null || fail "spawn did not reject active home descendant"
|
|
1438
|
+
|
|
1439
|
+
printf 'domain\n' > "$active_ancestor/.fm-secondmate-home"
|
|
1440
|
+
printf 'charter\n' > "$active_ancestor/data/charter.md"
|
|
1441
|
+
if PATH="$fakebin:$PATH" FM_HOME="$ancestor_active_home" FM_FAKE_TMUX_LOG="$log" FM_FAKE_TMUX_CAPTURE="$TMP_ROOT/spawn-validate-fake/pane.txt" \
|
|
1442
|
+
"$ROOT/bin/fm-spawn.sh" domain "$active_ancestor" codex --secondmate >/dev/null 2>"$err"; then
|
|
1443
|
+
fail "secondmate spawn accepted a home containing the active firstmate home"
|
|
1444
|
+
fi
|
|
1445
|
+
grep -F 'secondmate home cannot be an ancestor of the active firstmate home' "$err" >/dev/null || fail "spawn did not reject active home ancestor"
|
|
1446
|
+
|
|
1447
|
+
printf 'domain\n' > "$root_descendant/.fm-secondmate-home"
|
|
1448
|
+
printf 'charter\n' > "$root_descendant/data/charter.md"
|
|
1449
|
+
if PATH="$fakebin:$PATH" FM_ROOT_OVERRIDE="$fakeroot" FM_HOME="$home" FM_FAKE_TMUX_LOG="$log" FM_FAKE_TMUX_CAPTURE="$TMP_ROOT/spawn-validate-fake/pane.txt" \
|
|
1450
|
+
"$ROOT/bin/fm-spawn.sh" domain "$root_descendant" codex --secondmate >/dev/null 2>"$err"; then
|
|
1451
|
+
fail "secondmate spawn accepted a home inside the firstmate repo"
|
|
1452
|
+
fi
|
|
1453
|
+
grep -F 'secondmate home cannot be inside the firstmate repo' "$err" >/dev/null || fail "spawn did not reject repo root descendant"
|
|
1454
|
+
|
|
1455
|
+
printf 'domain\n' > "$root_ancestor/.fm-secondmate-home"
|
|
1456
|
+
printf 'charter\n' > "$root_ancestor/data/charter.md"
|
|
1457
|
+
if PATH="$fakebin:$PATH" FM_ROOT_OVERRIDE="$root_inside" FM_HOME="$home" FM_FAKE_TMUX_LOG="$log" FM_FAKE_TMUX_CAPTURE="$TMP_ROOT/spawn-validate-fake/pane.txt" \
|
|
1458
|
+
"$ROOT/bin/fm-spawn.sh" domain "$root_ancestor" codex --secondmate >/dev/null 2>"$err"; then
|
|
1459
|
+
fail "secondmate spawn accepted a home containing the firstmate repo"
|
|
1460
|
+
fi
|
|
1461
|
+
grep -F 'secondmate home cannot be an ancestor of the firstmate repo' "$err" >/dev/null || fail "spawn did not reject repo ancestor"
|
|
1462
|
+
|
|
1463
|
+
pass "secondmate spawn validates homes before launch"
|
|
1464
|
+
}
|
|
1465
|
+
|
|
1466
|
+
test_secondmate_spawn_refuses_operational_dirs_outside_subhome() {
|
|
1467
|
+
local home subhome sink fakebin log err opdir
|
|
1468
|
+
home="$TMP_ROOT/spawn-opdir-home"
|
|
1469
|
+
fakebin=$(make_fake_tmux "$TMP_ROOT/spawn-opdir-fake")
|
|
1470
|
+
log="$TMP_ROOT/spawn-opdir-fake/tmux.log"
|
|
1471
|
+
err="$TMP_ROOT/spawn-opdir.err"
|
|
1472
|
+
mkdir -p "$home/data" "$home/state"
|
|
1473
|
+
|
|
1474
|
+
for opdir in data state config projects; do
|
|
1475
|
+
subhome="$TMP_ROOT/spawn-opdir-subhome-$opdir"
|
|
1476
|
+
sink="$home/data/spawn-opdir-$opdir"
|
|
1477
|
+
rm -rf "$subhome" "$sink"
|
|
1478
|
+
mkdir -p "$subhome/data" "$subhome/state" "$subhome/config" "$subhome/projects" "$sink"
|
|
1479
|
+
printf 'domain\n' > "$subhome/.fm-secondmate-home"
|
|
1480
|
+
printf 'charter\n' > "$subhome/data/charter.md"
|
|
1481
|
+
rm -rf "${subhome:?}/${opdir:?}"
|
|
1482
|
+
ln -s "$sink" "$subhome/$opdir"
|
|
1483
|
+
if [ "$opdir" = data ]; then
|
|
1484
|
+
printf 'charter\n' > "$sink/charter.md"
|
|
1485
|
+
fi
|
|
1486
|
+
: > "$log"
|
|
1487
|
+
if PATH="$fakebin:$PATH" FM_HOME="$home" FM_FAKE_TMUX_LOG="$log" FM_FAKE_TMUX_CAPTURE="$TMP_ROOT/spawn-opdir-fake/pane.txt" \
|
|
1488
|
+
"$ROOT/bin/fm-spawn.sh" domain "$subhome" codex --secondmate >/dev/null 2>"$err"; then
|
|
1489
|
+
fail "secondmate spawn accepted a subhome with $opdir symlinked outside the subhome"
|
|
1490
|
+
fi
|
|
1491
|
+
grep -F "secondmate $opdir directory must resolve inside the secondmate home" "$err" >/dev/null \
|
|
1492
|
+
|| fail "spawn did not explain unsafe $opdir directory rejection"
|
|
1493
|
+
grep -F 'new-window' "$log" >/dev/null && fail "spawn created a window before unsafe $opdir directory validation"
|
|
1494
|
+
done
|
|
1495
|
+
pass "secondmate spawn refuses operational directories outside the subhome"
|
|
1496
|
+
}
|
|
1497
|
+
|
|
1498
|
+
test_fm_send_refuses_bare_window_without_home_meta() {
|
|
1499
|
+
# The happy path (a bare fm-<id> resolves the window recorded in THIS home's
|
|
1500
|
+
# meta and never a foreign same-named window) is asserted in the lifecycle e2e.
|
|
1501
|
+
# Here: with NO meta for the id, send must refuse rather than fall back to a
|
|
1502
|
+
# foreign same-named window that list-windows happens to return.
|
|
1503
|
+
local home fakebin log err
|
|
1504
|
+
home="$TMP_ROOT/send-home"
|
|
1505
|
+
mkdir -p "$home/state"
|
|
1506
|
+
touch "$home/state/.last-watcher-beat"
|
|
1507
|
+
fakebin=$(make_fake_tmux "$TMP_ROOT/send-fake")
|
|
1508
|
+
log="$TMP_ROOT/send-fake/tmux.log"
|
|
1509
|
+
err="$TMP_ROOT/send-fake/send.err"
|
|
1510
|
+
|
|
1511
|
+
if PATH="$fakebin:$PATH" FM_HOME="$home" FM_FAKE_TMUX_WINDOW="other-session:fm-missing" FM_FAKE_TMUX_LOG="$log" FM_FAKE_TMUX_CAPTURE="$TMP_ROOT/send-fake/pane.txt" \
|
|
1512
|
+
"$ROOT/bin/fm-send.sh" fm-missing 'wrong home' >/dev/null 2>"$err"; then
|
|
1513
|
+
fail "fm-send sent to a bare firstmate window without home metadata"
|
|
1514
|
+
fi
|
|
1515
|
+
grep -F "no metadata for fm-missing in $home/state" "$err" >/dev/null \
|
|
1516
|
+
|| fail "fm-send did not explain missing home metadata"
|
|
1517
|
+
grep -F 'send-keys -t other-session:fm-missing' "$log" >/dev/null \
|
|
1518
|
+
&& fail "fm-send fell back to a foreign same-name window"
|
|
1519
|
+
pass "fm-send refuses a bare firstmate window with no metadata in this home"
|
|
1520
|
+
}
|
|
1521
|
+
|
|
1522
|
+
test_secondmate_teardown_retires_empty_home() {
|
|
1523
|
+
local home subhome subhome_abs fakebin log lease fmroot
|
|
1524
|
+
home="$TMP_ROOT/teardown-home"
|
|
1525
|
+
subhome="$TMP_ROOT/teardown-subhome"
|
|
1526
|
+
fmroot="$TMP_ROOT/teardown-fmroot"
|
|
1527
|
+
make_firstmate_git_root "$fmroot"
|
|
1528
|
+
git -C "$fmroot" worktree add --quiet --detach "$subhome" HEAD
|
|
1529
|
+
mkdir -p "$home/state" "$home/data" "$subhome/state"
|
|
1530
|
+
printf 'domain\n' > "$subhome/.fm-secondmate-home"
|
|
1531
|
+
subhome_abs=$(cd "$subhome" && pwd -P)
|
|
1532
|
+
cat > "$home/state/domain.meta" <<EOF
|
|
1533
|
+
window=firstmate:fm-domain
|
|
1534
|
+
worktree=$subhome
|
|
1535
|
+
project=$subhome
|
|
1536
|
+
harness=echo
|
|
1537
|
+
kind=secondmate
|
|
1538
|
+
mode=secondmate
|
|
1539
|
+
yolo=off
|
|
1540
|
+
home=$subhome
|
|
1541
|
+
projects=alpha
|
|
1542
|
+
EOF
|
|
1543
|
+
printf '%s\n' '- domain - design domain (home: '"$subhome"'; scope: design domain; projects: alpha; added 2026-06-22)' > "$home/data/secondmates.md"
|
|
1544
|
+
fakebin=$(make_fake_tmux "$TMP_ROOT/teardown-fake")
|
|
1545
|
+
log="$TMP_ROOT/teardown-fake/tmux.log"
|
|
1546
|
+
lease="$TMP_ROOT/teardown-fake/lease"
|
|
1547
|
+
printf 'domain\n' > "$lease"
|
|
1548
|
+
PATH="$fakebin:$PATH" FM_ROOT_OVERRIDE="$fmroot" FM_HOME="$home" FM_FAKE_TMUX_LOG="$log" FM_FAKE_TMUX_CAPTURE="$TMP_ROOT/teardown-fake/pane.txt" \
|
|
1549
|
+
FM_FAKE_TREEHOUSE_LEASE_FILE="$lease" \
|
|
1550
|
+
"$ROOT/bin/fm-teardown.sh" domain >/dev/null 2>/dev/null \
|
|
1551
|
+
|| fail "teardown failed for empty secondmate home"
|
|
1552
|
+
grep -F "treehouse return --force $subhome_abs" "$log" >/dev/null || fail "teardown did not release the secondmate home lease via treehouse return"
|
|
1553
|
+
[ ! -e "$lease" ] || fail "teardown left the secondmate home lease held after retirement"
|
|
1554
|
+
[ ! -d "$subhome" ] || fail "teardown did not remove the retired secondmate home"
|
|
1555
|
+
[ ! -e "$home/state/domain.meta" ] || fail "teardown did not clear parent meta"
|
|
1556
|
+
grep -F -- '- domain ' "$home/data/secondmates.md" >/dev/null && fail "teardown did not remove secondmate registry route"
|
|
1557
|
+
pass "secondmate teardown retires empty homes and releases routing"
|
|
1558
|
+
}
|
|
1559
|
+
|
|
1560
|
+
test_secondmate_teardown_refuses_ambiguous_and_mismatched_registry_bindings() {
|
|
1561
|
+
local case_name home sub other fakebin log err meta_before registry_before
|
|
1562
|
+
for case_name in duplicate-id duplicate-home home-mismatch; do
|
|
1563
|
+
home="$TMP_ROOT/teardown-binding-$case_name-home"
|
|
1564
|
+
sub="$TMP_ROOT/teardown-binding-$case_name-sub"
|
|
1565
|
+
other="$TMP_ROOT/teardown-binding-$case_name-other"
|
|
1566
|
+
mkdir -p "$home/state" "$home/data" "$sub/state" "$sub/data" "$sub/config" "$sub/projects" "$other"
|
|
1567
|
+
printf 'domain\n' > "$sub/.fm-secondmate-home"
|
|
1568
|
+
fm_write_secondmate_meta "$home/state/domain.meta" "$sub"
|
|
1569
|
+
case "$case_name" in
|
|
1570
|
+
duplicate-id)
|
|
1571
|
+
cat > "$home/data/secondmates.md" <<EOF
|
|
1572
|
+
- domain - primary route (home: $sub; scope: valid (scope); punctuation; projects: alpha; added 2026-07-30)
|
|
1573
|
+
- domain - duplicate route (home: $other; scope: duplicate; projects: beta; added 2026-07-30)
|
|
1574
|
+
EOF
|
|
1575
|
+
;;
|
|
1576
|
+
duplicate-home)
|
|
1577
|
+
cat > "$home/data/secondmates.md" <<EOF
|
|
1578
|
+
- domain - primary route (home: $sub; scope: valid (scope); punctuation; projects: alpha; added 2026-07-30)
|
|
1579
|
+
- other - duplicate home route (home: $sub; scope: duplicate; projects: beta; added 2026-07-30)
|
|
1580
|
+
EOF
|
|
1581
|
+
;;
|
|
1582
|
+
home-mismatch)
|
|
1583
|
+
printf -- '- domain - mismatched route (home: %s; scope: valid (scope); punctuation; projects: alpha; added 2026-07-30)\n' \
|
|
1584
|
+
"$other" > "$home/data/secondmates.md"
|
|
1585
|
+
;;
|
|
1586
|
+
esac
|
|
1587
|
+
meta_before="$TMP_ROOT/teardown-binding-$case_name.meta.before"
|
|
1588
|
+
registry_before="$TMP_ROOT/teardown-binding-$case_name.registry.before"
|
|
1589
|
+
cp "$home/state/domain.meta" "$meta_before"
|
|
1590
|
+
cp "$home/data/secondmates.md" "$registry_before"
|
|
1591
|
+
fakebin=$(make_fake_tmux "$TMP_ROOT/teardown-binding-$case_name-fake")
|
|
1592
|
+
log="$TMP_ROOT/teardown-binding-$case_name-fake/tmux.log"
|
|
1593
|
+
err="$TMP_ROOT/teardown-binding-$case_name.err"
|
|
1594
|
+
if PATH="$fakebin:$PATH" FM_HOME="$home" FM_FAKE_TMUX_LOG="$log" \
|
|
1595
|
+
FM_FAKE_TMUX_CAPTURE="$TMP_ROOT/teardown-binding-$case_name-fake/pane.txt" \
|
|
1596
|
+
"$ROOT/bin/fm-teardown.sh" domain --force >/dev/null 2>"$err"; then
|
|
1597
|
+
fail "secondmate teardown accepted $case_name registry binding"
|
|
1598
|
+
fi
|
|
1599
|
+
[ -d "$sub" ] || fail "secondmate teardown removed the home after $case_name refusal"
|
|
1600
|
+
cmp -s "$meta_before" "$home/state/domain.meta" || fail "secondmate teardown changed metadata after $case_name refusal"
|
|
1601
|
+
cmp -s "$registry_before" "$home/data/secondmates.md" || fail "secondmate teardown changed registry after $case_name refusal"
|
|
1602
|
+
grep -F 'kill-window' "$log" >/dev/null && fail "secondmate teardown killed an endpoint before $case_name refusal"
|
|
1603
|
+
done
|
|
1604
|
+
pass "secondmate teardown refuses ambiguous and identity-mismatched registry bindings"
|
|
1605
|
+
}
|
|
1606
|
+
|
|
1607
|
+
test_secondmate_teardown_sweeps_process_events_before_removal() {
|
|
1608
|
+
local home subhome subhome_abs fakebin log sweep_log
|
|
1609
|
+
home="$TMP_ROOT/procevent-teardown-home"
|
|
1610
|
+
subhome="$TMP_ROOT/procevent-teardown-subhome"
|
|
1611
|
+
sweep_log="$TMP_ROOT/procevent-teardown-sweep.log"
|
|
1612
|
+
mkdir -p "$home/state" "$home/data" "$subhome/state/procevent"
|
|
1613
|
+
mark_firstmate_home "$subhome"
|
|
1614
|
+
printf 'domain\n' > "$subhome/.fm-secondmate-home"
|
|
1615
|
+
printf 'adapter=lavish\n' > "$subhome/state/procevent/source.source"
|
|
1616
|
+
printf 'runner\n' > "$subhome/state/procevent/source.runner"
|
|
1617
|
+
install_fake_process_event_sweep "$subhome" "$sweep_log"
|
|
1618
|
+
subhome_abs=$(cd "$subhome" && pwd -P)
|
|
1619
|
+
fm_write_secondmate_meta "$home/state/domain.meta" "$subhome"
|
|
1620
|
+
printf '%s\n' '- domain - design domain (home: '"$subhome"'; scope: design domain; projects: alpha; added 2026-06-22)' > "$home/data/secondmates.md"
|
|
1621
|
+
fakebin=$(make_fake_tmux "$TMP_ROOT/procevent-teardown-fake")
|
|
1622
|
+
log="$TMP_ROOT/procevent-teardown-fake/tmux.log"
|
|
1623
|
+
|
|
1624
|
+
PATH="$fakebin:$PATH" FM_HOME="$home" FM_FAKE_TMUX_LOG="$log" \
|
|
1625
|
+
FM_FAKE_TMUX_CAPTURE="$TMP_ROOT/procevent-teardown-fake/pane.txt" \
|
|
1626
|
+
FM_FAKE_PROCEVENT_SWEEP_LOG="$sweep_log" \
|
|
1627
|
+
"$ROOT/bin/fm-teardown.sh" domain >/dev/null 2>/dev/null \
|
|
1628
|
+
|| fail "normal secondmate teardown failed after process-event sweep"
|
|
1629
|
+
grep -Fx "$subhome_abs" "$sweep_log" >/dev/null || fail "normal secondmate teardown did not invoke the child home's sweep"
|
|
1630
|
+
[ ! -d "$subhome" ] || fail "normal secondmate teardown retained a successfully swept home"
|
|
1631
|
+
[ ! -e "$home/state/domain.meta" ] || fail "normal swept teardown retained parent evidence"
|
|
1632
|
+
pass "normal secondmate teardown sweeps process events before removal"
|
|
1633
|
+
}
|
|
1634
|
+
|
|
1635
|
+
test_secondmate_teardown_refuses_process_events_without_sweep_script() {
|
|
1636
|
+
local home subhome fakebin log err claim_root
|
|
1637
|
+
home="$TMP_ROOT/procevent-refusal-home"
|
|
1638
|
+
subhome="$TMP_ROOT/procevent-refusal-subhome"
|
|
1639
|
+
err="$TMP_ROOT/procevent-refusal.err"
|
|
1640
|
+
claim_root="$TMP_ROOT/procevent-refusal-claims"
|
|
1641
|
+
mkdir -p "$home/state" "$home/data" "$subhome/state/procevent" "$claim_root"
|
|
1642
|
+
mark_firstmate_home "$subhome"
|
|
1643
|
+
printf 'domain\n' > "$subhome/.fm-secondmate-home"
|
|
1644
|
+
printf 'adapter=lavish\n' > "$subhome/state/procevent/source.source"
|
|
1645
|
+
printf '%s\n999999\ntoken\nidentity\n' "$subhome" > "$claim_root/source.claim"
|
|
1646
|
+
fm_write_secondmate_meta "$home/state/domain.meta" "$subhome"
|
|
1647
|
+
printf '%s\n' '- domain - design domain (home: '"$subhome"'; scope: design domain; projects: alpha; added 2026-06-22)' > "$home/data/secondmates.md"
|
|
1648
|
+
fakebin=$(make_fake_tmux "$TMP_ROOT/procevent-refusal-fake")
|
|
1649
|
+
log="$TMP_ROOT/procevent-refusal-fake/tmux.log"
|
|
1650
|
+
|
|
1651
|
+
if PATH="$fakebin:$PATH" FM_HOME="$home" FM_PROCEVENT_CLAIM_ROOT="$claim_root" \
|
|
1652
|
+
FM_FAKE_TMUX_LOG="$log" FM_FAKE_TMUX_CAPTURE="$TMP_ROOT/procevent-refusal-fake/pane.txt" \
|
|
1653
|
+
"$ROOT/bin/fm-teardown.sh" domain --force >/dev/null 2>"$err"; then
|
|
1654
|
+
fail "force teardown removed process-event state without a sweep-capable child script"
|
|
1655
|
+
fi
|
|
1656
|
+
grep -F 'no sweep-capable bin/fm-procevent.sh' "$err" >/dev/null || fail "missing sweep capability refusal was not explained"
|
|
1657
|
+
[ -d "$subhome" ] || fail "missing sweep capability refusal removed the home"
|
|
1658
|
+
[ -e "$home/state/domain.meta" ] || fail "missing sweep capability refusal removed parent evidence"
|
|
1659
|
+
grep -F -- '- domain ' "$home/data/secondmates.md" >/dev/null || fail "missing sweep capability refusal removed the route"
|
|
1660
|
+
[ -e "$subhome/state/procevent/source.source" ] || fail "missing sweep capability refusal removed the registration"
|
|
1661
|
+
[ -e "$claim_root/source.claim" ] || fail "missing sweep capability refusal removed the claim"
|
|
1662
|
+
grep -F 'kill-window' "$log" >/dev/null && fail "missing sweep capability refusal killed a runtime endpoint"
|
|
1663
|
+
pass "secondmate teardown preserves state when process-event sweeping is unavailable"
|
|
1664
|
+
}
|
|
1665
|
+
|
|
1666
|
+
test_secondmate_teardown_preserves_process_events_on_later_refusal() {
|
|
1667
|
+
local home subhome fakebin log sweep_log err
|
|
1668
|
+
home="$TMP_ROOT/procevent-later-refusal-home"
|
|
1669
|
+
subhome="$TMP_ROOT/procevent-later-refusal-subhome"
|
|
1670
|
+
sweep_log="$TMP_ROOT/procevent-later-refusal-sweep.log"
|
|
1671
|
+
err="$TMP_ROOT/procevent-later-refusal.err"
|
|
1672
|
+
mkdir -p "$home/state/public-followup/registry" "$home/data" "$subhome/state/procevent"
|
|
1673
|
+
mark_firstmate_home "$subhome"
|
|
1674
|
+
printf 'domain\n' > "$subhome/.fm-secondmate-home"
|
|
1675
|
+
printf 'adapter=lavish\n' > "$subhome/state/procevent/source.source"
|
|
1676
|
+
install_fake_process_event_sweep "$subhome" "$sweep_log"
|
|
1677
|
+
printf 'FMX_PAIRING_TOKEN=test-token\n' > "$home/.env"
|
|
1678
|
+
printf 'work_home=secondmate:domain\nwork_id=domain\n' > "$home/state/public-followup/registry/obligation"
|
|
1679
|
+
fm_write_secondmate_meta "$home/state/domain.meta" "$subhome"
|
|
1680
|
+
printf '%s\n' '- domain - design domain (home: '"$subhome"'; scope: design domain; projects: alpha; added 2026-06-22)' > "$home/data/secondmates.md"
|
|
1681
|
+
fakebin=$(make_fake_tmux "$TMP_ROOT/procevent-later-refusal-fake")
|
|
1682
|
+
log="$TMP_ROOT/procevent-later-refusal-fake/tmux.log"
|
|
1683
|
+
cat > "$fakebin/tasks-axi" <<'SH'
|
|
1684
|
+
#!/usr/bin/env bash
|
|
1685
|
+
exit 1
|
|
1686
|
+
SH
|
|
1687
|
+
chmod +x "$fakebin/tasks-axi"
|
|
1688
|
+
|
|
1689
|
+
if PATH="$fakebin:$PATH" FM_HOME="$home" FM_FAKE_TMUX_LOG="$log" \
|
|
1690
|
+
FM_FAKE_TMUX_CAPTURE="$TMP_ROOT/procevent-later-refusal-fake/pane.txt" \
|
|
1691
|
+
FM_FAKE_PROCEVENT_SWEEP_LOG="$sweep_log" \
|
|
1692
|
+
"$ROOT/bin/fm-teardown.sh" domain >/dev/null 2>"$err"; then
|
|
1693
|
+
fail "teardown bypassed a later public-followup refusal"
|
|
1694
|
+
fi
|
|
1695
|
+
grep -F 'still owes a public reply' "$err" >/dev/null || fail "later public-followup refusal was not reached"
|
|
1696
|
+
[ ! -s "$sweep_log" ] || fail "later refusal retired process-event sources before teardown was authorized"
|
|
1697
|
+
[ -e "$subhome/state/procevent/source.source" ] || fail "later refusal removed the process-event registration"
|
|
1698
|
+
[ -d "$subhome" ] || fail "later refusal removed the secondmate home"
|
|
1699
|
+
[ -e "$home/state/domain.meta" ] || fail "later refusal removed parent evidence"
|
|
1700
|
+
pass "later teardown refusals preserve active process-event sources"
|
|
1701
|
+
}
|
|
1702
|
+
|
|
1703
|
+
test_secondmate_force_teardown_sweeps_nested_homes() {
|
|
1704
|
+
local home subhome childhome subhome_abs childhome_abs fakebin log sweep_log
|
|
1705
|
+
home="$TMP_ROOT/procevent-force-home"
|
|
1706
|
+
subhome="$TMP_ROOT/procevent-force-subhome"
|
|
1707
|
+
childhome="$TMP_ROOT/procevent-force-childhome"
|
|
1708
|
+
sweep_log="$TMP_ROOT/procevent-force-sweep.log"
|
|
1709
|
+
mkdir -p "$home/state" "$home/data" "$subhome/state/procevent" "$childhome/state/procevent"
|
|
1710
|
+
mark_firstmate_home "$subhome"
|
|
1711
|
+
mark_firstmate_home "$childhome"
|
|
1712
|
+
printf 'domain\n' > "$subhome/.fm-secondmate-home"
|
|
1713
|
+
printf 'nested\n' > "$childhome/.fm-secondmate-home"
|
|
1714
|
+
printf 'adapter=lavish\n' > "$subhome/state/procevent/parent-source.source"
|
|
1715
|
+
printf 'adapter=lavish\n' > "$childhome/state/procevent/child-source.source"
|
|
1716
|
+
install_fake_process_event_sweep "$subhome" "$sweep_log"
|
|
1717
|
+
install_fake_process_event_sweep "$childhome" "$sweep_log"
|
|
1718
|
+
subhome_abs=$(cd "$subhome" && pwd -P)
|
|
1719
|
+
childhome_abs=$(cd "$childhome" && pwd -P)
|
|
1720
|
+
fm_write_secondmate_meta "$home/state/domain.meta" "$subhome"
|
|
1721
|
+
fm_write_secondmate_meta "$subhome/state/nested.meta" "$childhome"
|
|
1722
|
+
cat > "$home/data/secondmates.md" <<EOF
|
|
1723
|
+
- domain - design domain (home: $subhome; scope: design domain; projects: alpha; added 2026-06-22)
|
|
1724
|
+
- nested - nested domain (home: $childhome; scope: nested domain; projects: beta; added 2026-06-22)
|
|
1725
|
+
EOF
|
|
1726
|
+
fakebin=$(make_fake_tmux "$TMP_ROOT/procevent-force-fake")
|
|
1727
|
+
log="$TMP_ROOT/procevent-force-fake/tmux.log"
|
|
1728
|
+
|
|
1729
|
+
PATH="$fakebin:$PATH" FM_HOME="$home" FM_FAKE_TMUX_LOG="$log" \
|
|
1730
|
+
FM_FAKE_TMUX_CAPTURE="$TMP_ROOT/procevent-force-fake/pane.txt" \
|
|
1731
|
+
FM_FAKE_PROCEVENT_SWEEP_LOG="$sweep_log" \
|
|
1732
|
+
"$ROOT/bin/fm-teardown.sh" domain --force >/dev/null 2>/dev/null \
|
|
1733
|
+
|| fail "force teardown failed after recursively sweeping process events"
|
|
1734
|
+
grep -Fx "$subhome_abs" "$sweep_log" >/dev/null || fail "force teardown did not sweep the parent secondmate home"
|
|
1735
|
+
grep -Fx "$childhome_abs" "$sweep_log" >/dev/null || fail "force teardown did not sweep the nested secondmate home"
|
|
1736
|
+
[ ! -d "$subhome" ] || fail "force teardown retained the swept parent home"
|
|
1737
|
+
[ ! -d "$childhome" ] || fail "force teardown retained the swept nested home"
|
|
1738
|
+
pass "force teardown sweeps nested secondmate homes before deletion"
|
|
1739
|
+
}
|
|
1740
|
+
|
|
1741
|
+
test_secondmate_force_teardown_preserves_nested_restore_status() {
|
|
1742
|
+
local home subhome childhome grandchildhome fmroot fakebin log sweep_log rearm_log err rc backup
|
|
1743
|
+
home="$TMP_ROOT/procevent-nested-fail-home"
|
|
1744
|
+
subhome="$TMP_ROOT/procevent-nested-fail-subhome"
|
|
1745
|
+
childhome="$TMP_ROOT/procevent-nested-fail-childhome"
|
|
1746
|
+
grandchildhome="$TMP_ROOT/procevent-nested-fail-grandchildhome"
|
|
1747
|
+
fmroot="$TMP_ROOT/procevent-nested-fail-fmroot"
|
|
1748
|
+
sweep_log="$TMP_ROOT/procevent-nested-fail-sweep.log"
|
|
1749
|
+
rearm_log="$TMP_ROOT/procevent-nested-fail-rearm.log"
|
|
1750
|
+
err="$TMP_ROOT/procevent-nested-fail.err"
|
|
1751
|
+
make_firstmate_git_root "$fmroot"
|
|
1752
|
+
git -C "$fmroot" worktree add --quiet --detach "$grandchildhome" HEAD
|
|
1753
|
+
mkdir -p "$home/state" "$home/data" "$subhome/state" "$childhome/state" "$grandchildhome/state/procevent"
|
|
1754
|
+
mark_firstmate_home "$subhome"
|
|
1755
|
+
mark_firstmate_home "$childhome"
|
|
1756
|
+
printf 'domain\n' > "$subhome/.fm-secondmate-home"
|
|
1757
|
+
printf 'nested\n' > "$childhome/.fm-secondmate-home"
|
|
1758
|
+
printf 'leaf\n' > "$grandchildhome/.fm-secondmate-home"
|
|
1759
|
+
printf 'adapter=lavish\n' > "$grandchildhome/state/procevent/leaf-source.source"
|
|
1760
|
+
install_fake_process_event_sweep "$grandchildhome" "$sweep_log"
|
|
1761
|
+
: > "$rearm_log"
|
|
1762
|
+
fm_write_secondmate_meta "$home/state/domain.meta" "$subhome"
|
|
1763
|
+
fm_write_secondmate_meta "$subhome/state/nested.meta" "$childhome"
|
|
1764
|
+
fm_write_secondmate_meta "$childhome/state/leaf.meta" "$grandchildhome"
|
|
1765
|
+
cat > "$home/data/secondmates.md" <<EOF
|
|
1766
|
+
- domain - design domain (home: $subhome; scope: design domain; projects: alpha; added 2026-06-22)
|
|
1767
|
+
- nested - nested domain (home: $childhome; scope: nested domain; projects: beta; added 2026-06-22)
|
|
1768
|
+
- leaf - leaf domain (home: $grandchildhome; scope: leaf domain; projects: gamma; added 2026-06-22)
|
|
1769
|
+
EOF
|
|
1770
|
+
fakebin=$(make_fake_tmux "$TMP_ROOT/procevent-nested-fail-fake")
|
|
1771
|
+
log="$TMP_ROOT/procevent-nested-fail-fake/tmux.log"
|
|
1772
|
+
|
|
1773
|
+
set +e
|
|
1774
|
+
PATH="$fakebin:$PATH" FM_ROOT_OVERRIDE="$fmroot" FM_HOME="$home" FM_FAKE_TMUX_LOG="$log" \
|
|
1775
|
+
FM_FAKE_TMUX_CAPTURE="$TMP_ROOT/procevent-nested-fail-fake/pane.txt" \
|
|
1776
|
+
FM_FAKE_PROCEVENT_SWEEP_LOG="$sweep_log" FM_FAKE_PROCEVENT_REARM_LOG="$rearm_log" \
|
|
1777
|
+
FM_FAKE_TREEHOUSE_RETURN_FAIL=1 FM_FAKE_PROCEVENT_REARM_FAIL=1 \
|
|
1778
|
+
"$ROOT/bin/fm-teardown.sh" domain --force >/dev/null 2>"$err"
|
|
1779
|
+
rc=$?
|
|
1780
|
+
set -e
|
|
1781
|
+
|
|
1782
|
+
[ "$rc" -eq 4 ] || fail "nested process-event restoration failure was collapsed at a recursive teardown boundary"
|
|
1783
|
+
grep -F 'active waits may remain retired; recover registrations from ' "$err" >/dev/null || fail "nested restoration failure did not report its recovery backup"
|
|
1784
|
+
backup=$(find "$TMP_ROOT" -maxdepth 1 -type d -name '.fm-procevent-restore.*' \
|
|
1785
|
+
-exec test -e '{}/leaf-source.source' \; -print -quit)
|
|
1786
|
+
[ -n "$backup" ] && [ -e "$backup/leaf-source.source" ] || fail "nested restoration failure did not retain its registration backup"
|
|
1787
|
+
[ -e "$childhome/state/leaf.meta" ] || fail "nested restoration failure removed its parent identity record"
|
|
1788
|
+
[ -e "$subhome/state/nested.meta" ] || fail "nested restoration failure removed its ancestor identity record"
|
|
1789
|
+
[ -e "$home/state/domain.meta" ] || fail "nested restoration failure removed its top-level identity record"
|
|
1790
|
+
pass "force teardown preserves nested process-event restoration status and recovery state"
|
|
1791
|
+
}
|
|
1792
|
+
|
|
1793
|
+
test_secondmate_teardown_refuses_failed_leased_home_return() {
|
|
1794
|
+
local home subhome subhome_abs fakebin log fmroot err rc sweep_log rearm_log backup
|
|
1795
|
+
home="$TMP_ROOT/teardown-return-fail-home"
|
|
1796
|
+
subhome="$TMP_ROOT/teardown-return-fail-subhome"
|
|
1797
|
+
fmroot="$TMP_ROOT/teardown-return-fail-fmroot"
|
|
1798
|
+
err="$TMP_ROOT/teardown-return-fail.err"
|
|
1799
|
+
sweep_log="$TMP_ROOT/teardown-return-fail-sweep.log"
|
|
1800
|
+
rearm_log="$TMP_ROOT/teardown-return-fail-rearm.log"
|
|
1801
|
+
make_firstmate_git_root "$fmroot"
|
|
1802
|
+
git -C "$fmroot" worktree add --quiet --detach "$subhome" HEAD
|
|
1803
|
+
mkdir -p "$home/state" "$home/data" "$subhome/state/procevent"
|
|
1804
|
+
printf 'domain\n' > "$subhome/.fm-secondmate-home"
|
|
1805
|
+
printf 'adapter=lavish\nargc=1\nargv:\n/bin/true\n' > "$subhome/state/procevent/source.source"
|
|
1806
|
+
install_fake_process_event_sweep "$subhome" "$sweep_log"
|
|
1807
|
+
: > "$rearm_log"
|
|
1808
|
+
subhome_abs=$(cd "$subhome" && pwd -P)
|
|
1809
|
+
cat > "$home/state/domain.meta" <<EOF
|
|
1810
|
+
window=firstmate:fm-domain
|
|
1811
|
+
worktree=$subhome
|
|
1812
|
+
project=$subhome
|
|
1813
|
+
harness=echo
|
|
1814
|
+
kind=secondmate
|
|
1815
|
+
mode=secondmate
|
|
1816
|
+
yolo=off
|
|
1817
|
+
home=$subhome
|
|
1818
|
+
projects=alpha
|
|
1819
|
+
EOF
|
|
1820
|
+
printf '%s\n' '- domain - design domain (home: '"$subhome"'; scope: design domain; projects: alpha; added 2026-06-22)' > "$home/data/secondmates.md"
|
|
1821
|
+
fakebin=$(make_fake_tmux "$TMP_ROOT/teardown-return-fail-fake")
|
|
1822
|
+
log="$TMP_ROOT/teardown-return-fail-fake/tmux.log"
|
|
1823
|
+
|
|
1824
|
+
set +e
|
|
1825
|
+
PATH="$fakebin:$PATH" FM_ROOT_OVERRIDE="$fmroot" FM_HOME="$home" FM_FAKE_TMUX_LOG="$log" FM_FAKE_TMUX_CAPTURE="$TMP_ROOT/teardown-return-fail-fake/pane.txt" \
|
|
1826
|
+
FM_FAKE_PROCEVENT_SWEEP_LOG="$sweep_log" FM_FAKE_PROCEVENT_REARM_LOG="$rearm_log" \
|
|
1827
|
+
FM_FAKE_TREEHOUSE_RETURN_FAIL=1 \
|
|
1828
|
+
"$ROOT/bin/fm-teardown.sh" domain >/dev/null 2>"$err"
|
|
1829
|
+
rc=$?
|
|
1830
|
+
set -e
|
|
1831
|
+
|
|
1832
|
+
[ "$rc" -ne 0 ] || fail "teardown succeeded despite failed treehouse return"
|
|
1833
|
+
grep -F "treehouse return --force $subhome_abs" "$log" >/dev/null || fail "teardown did not try to return the leased home"
|
|
1834
|
+
grep -F 'treehouse return failed for secondmate home' "$err" >/dev/null || fail "teardown did not report failed leased home return"
|
|
1835
|
+
[ -d "$subhome" ] || fail "teardown removed a leased home after return failed"
|
|
1836
|
+
[ -e "$subhome/state/procevent/source.source" ] || fail "failed leased-home return did not restore the source registration"
|
|
1837
|
+
grep -Fx "$subhome_abs" "$rearm_log" >/dev/null || fail "failed leased-home return did not rearm restored process-event sources"
|
|
1838
|
+
[ -e "$home/state/domain.meta" ] || fail "teardown cleared meta after leased home return failed"
|
|
1839
|
+
grep -F -- '- domain ' "$home/data/secondmates.md" >/dev/null || fail "teardown removed registry route after leased home return failed"
|
|
1840
|
+
|
|
1841
|
+
set +e
|
|
1842
|
+
PATH="$fakebin:$PATH" FM_ROOT_OVERRIDE="$fmroot" FM_HOME="$home" FM_FAKE_TMUX_LOG="$log" FM_FAKE_TMUX_CAPTURE="$TMP_ROOT/teardown-return-fail-fake/pane.txt" \
|
|
1843
|
+
FM_FAKE_PROCEVENT_SWEEP_LOG="$sweep_log" FM_FAKE_PROCEVENT_REARM_LOG="$rearm_log" \
|
|
1844
|
+
FM_FAKE_TREEHOUSE_RETURN_FAIL=1 FM_FAKE_PROCEVENT_REARM_FAIL=1 \
|
|
1845
|
+
"$ROOT/bin/fm-teardown.sh" domain >/dev/null 2>"$err"
|
|
1846
|
+
rc=$?
|
|
1847
|
+
set -e
|
|
1848
|
+
|
|
1849
|
+
[ "$rc" -eq 4 ] || fail "failed process-event restoration did not return its distinct recoverable status"
|
|
1850
|
+
grep -F 'active waits may remain retired; recover registrations from ' "$err" >/dev/null || fail "failed process-event restoration did not report its recovery backup"
|
|
1851
|
+
backup=$(find "$TMP_ROOT" -maxdepth 1 -type d -name '.fm-procevent-restore.*' \
|
|
1852
|
+
-exec test -e '{}/source.source' \; -print -quit)
|
|
1853
|
+
[ -n "$backup" ] && [ -e "$backup/source.source" ] || fail "failed process-event restoration did not retain its registration backup"
|
|
1854
|
+
pass "secondmate teardown refuses to hide failed leased-home return"
|
|
1855
|
+
}
|
|
1856
|
+
|
|
1857
|
+
test_secondmate_teardown_removes_plain_clone_home_without_treehouse_return() {
|
|
1858
|
+
local home subhome subhome_abs fakebin log
|
|
1859
|
+
home="$TMP_ROOT/plain-clone-teardown-home"
|
|
1860
|
+
subhome="$TMP_ROOT/plain-clone-teardown-subhome"
|
|
1861
|
+
mkdir -p "$home/state" "$home/data" "$subhome/state"
|
|
1862
|
+
mark_firstmate_home "$subhome"
|
|
1863
|
+
printf 'domain\n' > "$subhome/.fm-secondmate-home"
|
|
1864
|
+
subhome_abs=$(cd "$subhome" && pwd -P)
|
|
1865
|
+
cat > "$home/state/domain.meta" <<EOF
|
|
1866
|
+
window=firstmate:fm-domain
|
|
1867
|
+
worktree=$subhome
|
|
1868
|
+
project=$subhome
|
|
1869
|
+
harness=echo
|
|
1870
|
+
kind=secondmate
|
|
1871
|
+
mode=secondmate
|
|
1872
|
+
yolo=off
|
|
1873
|
+
home=$subhome
|
|
1874
|
+
projects=alpha
|
|
1875
|
+
EOF
|
|
1876
|
+
printf '%s\n' '- domain - design domain (home: '"$subhome"'; scope: design domain; projects: alpha; added 2026-06-22)' > "$home/data/secondmates.md"
|
|
1877
|
+
fakebin=$(make_fake_tmux "$TMP_ROOT/plain-clone-teardown-fake")
|
|
1878
|
+
log="$TMP_ROOT/plain-clone-teardown-fake/tmux.log"
|
|
1879
|
+
|
|
1880
|
+
PATH="$fakebin:$PATH" FM_HOME="$home" FM_FAKE_TMUX_LOG="$log" FM_FAKE_TMUX_CAPTURE="$TMP_ROOT/plain-clone-teardown-fake/pane.txt" \
|
|
1881
|
+
FM_FAKE_TREEHOUSE_RETURN_FAIL=1 \
|
|
1882
|
+
"$ROOT/bin/fm-teardown.sh" domain >/dev/null 2>/dev/null \
|
|
1883
|
+
|| fail "teardown failed for plain-clone secondmate home"
|
|
1884
|
+
grep -F "treehouse return --force $subhome_abs" "$log" >/dev/null && fail "teardown tried to return a plain-clone home through treehouse"
|
|
1885
|
+
[ ! -d "$subhome" ] || fail "teardown did not remove the plain-clone secondmate home"
|
|
1886
|
+
[ ! -e "$home/state/domain.meta" ] || fail "teardown did not clear parent meta for plain-clone home"
|
|
1887
|
+
grep -F -- '- domain ' "$home/data/secondmates.md" >/dev/null && fail "teardown did not remove plain-clone registry route"
|
|
1888
|
+
pass "secondmate teardown raw-removes plain-clone homes"
|
|
1889
|
+
}
|
|
1890
|
+
|
|
1891
|
+
test_secondmate_force_teardown_discards_child_work() {
|
|
1892
|
+
local home subhome childproj childwt fakebin log
|
|
1893
|
+
home="$TMP_ROOT/force-teardown-home"
|
|
1894
|
+
subhome="$TMP_ROOT/force-teardown-subhome"
|
|
1895
|
+
childproj="$subhome/projects/alpha"
|
|
1896
|
+
childwt="$TMP_ROOT/force-child-worktree"
|
|
1897
|
+
mkdir -p "$home/state" "$home/data" "$subhome/state"
|
|
1898
|
+
fm_git_worktree "$childproj" "$childwt" force-child
|
|
1899
|
+
printf 'domain\n' > "$subhome/.fm-secondmate-home"
|
|
1900
|
+
cat > "$home/state/domain.meta" <<EOF
|
|
1901
|
+
window=firstmate:fm-domain
|
|
1902
|
+
worktree=$subhome
|
|
1903
|
+
project=$subhome
|
|
1904
|
+
harness=echo
|
|
1905
|
+
kind=secondmate
|
|
1906
|
+
mode=secondmate
|
|
1907
|
+
yolo=off
|
|
1908
|
+
home=$subhome
|
|
1909
|
+
projects=alpha
|
|
1910
|
+
EOF
|
|
1911
|
+
printf '%s\n' '- domain - design domain (home: '"$subhome"'; scope: design domain; projects: alpha; added 2026-06-22)' > "$home/data/secondmates.md"
|
|
1912
|
+
cat > "$subhome/state/child.meta" <<EOF
|
|
1913
|
+
window=firstmate:fm-child
|
|
1914
|
+
worktree=$childwt
|
|
1915
|
+
project=$childproj
|
|
1916
|
+
harness=echo
|
|
1917
|
+
kind=ship
|
|
1918
|
+
mode=no-mistakes
|
|
1919
|
+
yolo=off
|
|
1920
|
+
EOF
|
|
1921
|
+
fakebin=$(make_fake_tmux "$TMP_ROOT/force-teardown-fake")
|
|
1922
|
+
log="$TMP_ROOT/force-teardown-fake/tmux.log"
|
|
1923
|
+
if PATH="$fakebin:$PATH" FM_HOME="$home" FM_FAKE_TMUX_LOG="$log" FM_FAKE_TMUX_CAPTURE="$TMP_ROOT/force-teardown-fake/pane.txt" \
|
|
1924
|
+
"$ROOT/bin/fm-teardown.sh" domain >/dev/null 2>&1; then
|
|
1925
|
+
fail "teardown allowed a secondmate with in-flight child work"
|
|
1926
|
+
fi
|
|
1927
|
+
PATH="$fakebin:$PATH" FM_HOME="$home" FM_FAKE_TMUX_LOG="$log" FM_FAKE_TMUX_CAPTURE="$TMP_ROOT/force-teardown-fake/pane.txt" \
|
|
1928
|
+
"$ROOT/bin/fm-teardown.sh" domain --force >/dev/null 2>/dev/null \
|
|
1929
|
+
|| fail "force teardown failed to discard child work"
|
|
1930
|
+
[ ! -d "$subhome" ] || fail "force teardown did not remove the retired secondmate home"
|
|
1931
|
+
[ ! -d "$childwt" ] || fail "force teardown did not remove child worktree"
|
|
1932
|
+
[ ! -e "$home/state/domain.meta" ] || fail "teardown did not clear parent meta"
|
|
1933
|
+
grep -F -- '- domain ' "$home/data/secondmates.md" >/dev/null && fail "force teardown did not remove secondmate registry route"
|
|
1934
|
+
grep -F 'kill-window -t =firstmate:=fm-child' "$log" >/dev/null || fail "force teardown did not kill child window"
|
|
1935
|
+
grep -F 'kill-window -t =firstmate:=fm-domain' "$log" >/dev/null || fail "force teardown did not kill parent window"
|
|
1936
|
+
pass "secondmate force teardown discards child work"
|
|
1937
|
+
}
|
|
1938
|
+
|
|
1939
|
+
test_secondmate_force_teardown_preserves_child_on_unproven_lock() {
|
|
1940
|
+
local home subhome childproj childwt fakebin log err rc lock
|
|
1941
|
+
home="$TMP_ROOT/force-lock-home"
|
|
1942
|
+
subhome="$TMP_ROOT/force-lock-subhome"
|
|
1943
|
+
childproj="$subhome/projects/alpha"
|
|
1944
|
+
childwt="$TMP_ROOT/force-lock-child-worktree"
|
|
1945
|
+
err="$TMP_ROOT/force-lock-child.err"
|
|
1946
|
+
mkdir -p "$home/state" "$home/data" "$subhome/state"
|
|
1947
|
+
fm_git_worktree "$childproj" "$childwt" force-child-lock
|
|
1948
|
+
printf 'domain\n' > "$subhome/.fm-secondmate-home"
|
|
1949
|
+
cat > "$home/state/domain.meta" <<EOF
|
|
1950
|
+
window=firstmate:fm-domain
|
|
1951
|
+
worktree=$subhome
|
|
1952
|
+
project=$subhome
|
|
1953
|
+
harness=echo
|
|
1954
|
+
kind=secondmate
|
|
1955
|
+
mode=secondmate
|
|
1956
|
+
yolo=off
|
|
1957
|
+
home=$subhome
|
|
1958
|
+
projects=alpha
|
|
1959
|
+
EOF
|
|
1960
|
+
printf '%s\n' '- domain - design domain (home: '"$subhome"'; scope: design domain; projects: alpha; added 2026-06-22)' > "$home/data/secondmates.md"
|
|
1961
|
+
cat > "$subhome/state/child.meta" <<EOF
|
|
1962
|
+
window=firstmate:fm-child
|
|
1963
|
+
worktree=$childwt
|
|
1964
|
+
project=$childproj
|
|
1965
|
+
harness=echo
|
|
1966
|
+
kind=ship
|
|
1967
|
+
mode=no-mistakes
|
|
1968
|
+
yolo=off
|
|
1969
|
+
EOF
|
|
1970
|
+
fakebin=$(make_fake_tmux "$TMP_ROOT/force-lock-child-fake")
|
|
1971
|
+
log="$TMP_ROOT/force-lock-child-fake/tmux.log"
|
|
1972
|
+
cat > "$fakebin/treehouse" <<'SH'
|
|
1973
|
+
#!/usr/bin/env bash
|
|
1974
|
+
set -u
|
|
1975
|
+
printf 'treehouse %s\n' "$*" >> "${FM_FAKE_TMUX_LOG:-/dev/null}"
|
|
1976
|
+
case "${1:-}" in
|
|
1977
|
+
return)
|
|
1978
|
+
shift
|
|
1979
|
+
target=
|
|
1980
|
+
while [ $# -gt 0 ]; do
|
|
1981
|
+
case "$1" in
|
|
1982
|
+
--force) ;;
|
|
1983
|
+
*) target=$1 ;;
|
|
1984
|
+
esac
|
|
1985
|
+
shift
|
|
1986
|
+
done
|
|
1987
|
+
lock=$(git -C "$target" rev-parse --git-path index.lock 2>/dev/null || true)
|
|
1988
|
+
if [ -n "$lock" ] && [ -e "$lock" ]; then
|
|
1989
|
+
echo "fatal: Unable to create '$lock': File exists." >&2
|
|
1990
|
+
exit 128
|
|
1991
|
+
fi
|
|
1992
|
+
[ -n "$target" ] && rm -rf -- "$target"
|
|
1993
|
+
exit 0
|
|
1994
|
+
;;
|
|
1995
|
+
esac
|
|
1996
|
+
exit 0
|
|
1997
|
+
SH
|
|
1998
|
+
cat > "$fakebin/lsof" <<'SH'
|
|
1999
|
+
#!/usr/bin/env bash
|
|
2000
|
+
exit 0
|
|
2001
|
+
SH
|
|
2002
|
+
chmod +x "$fakebin/treehouse" "$fakebin/lsof"
|
|
2003
|
+
lock=$(git -C "$childwt" rev-parse --git-path index.lock)
|
|
2004
|
+
mkdir -p "$(dirname "$lock")"
|
|
2005
|
+
: > "$lock"
|
|
2006
|
+
touch -t 200001010000 "$lock"
|
|
2007
|
+
|
|
2008
|
+
set +e
|
|
2009
|
+
PATH="$fakebin:$PATH" FM_HOME="$home" FM_FAKE_TMUX_LOG="$log" FM_FAKE_TMUX_CAPTURE="$TMP_ROOT/force-lock-child-fake/pane.txt" \
|
|
2010
|
+
FM_STALE_WORKTREE_LOCK_RETRY_WAIT_SECS=0 FM_STALE_WORKTREE_LOCK_AGE_SECS=1 \
|
|
2011
|
+
"$ROOT/bin/fm-teardown.sh" domain --force >/dev/null 2>"$err"
|
|
2012
|
+
rc=$?
|
|
2013
|
+
set -e
|
|
2014
|
+
|
|
2015
|
+
[ "$rc" -ne 0 ] || fail "force teardown succeeded after child treehouse refused an unproven lock"
|
|
2016
|
+
[ -d "$childwt" ] || fail "force teardown raw-removed child worktree after unproven lock refusal"
|
|
2017
|
+
[ -e "$lock" ] || fail "force teardown removed unproven child index.lock"
|
|
2018
|
+
[ -d "$subhome" ] || fail "force teardown removed subhome after child lock refusal"
|
|
2019
|
+
[ -e "$subhome/state/child.meta" ] || fail "force teardown cleared child meta after child lock refusal"
|
|
2020
|
+
grep -F 'not provably stale' "$err" >/dev/null || fail "force teardown did not explain unproven child lock refusal"
|
|
2021
|
+
pass "secondmate force teardown preserves child worktree after unproven lock refusal"
|
|
2022
|
+
}
|
|
2023
|
+
|
|
2024
|
+
test_secondmate_force_teardown_allows_non_state_operational_dir_symlinks_inside_home() {
|
|
2025
|
+
local opdir home subhome target fakebin err log
|
|
2026
|
+
for opdir in data config projects; do
|
|
2027
|
+
home="$TMP_ROOT/symlink-inside-teardown-home-$opdir"
|
|
2028
|
+
subhome="$TMP_ROOT/symlink-inside-teardown-subhome-$opdir"
|
|
2029
|
+
target="$subhome/internal-$opdir"
|
|
2030
|
+
err="$TMP_ROOT/symlink-inside-teardown-$opdir.err"
|
|
2031
|
+
rm -rf "$home" "$subhome"
|
|
2032
|
+
mkdir -p "$home/state" "$home/data" "$subhome" "$target"
|
|
2033
|
+
printf 'domain\n' > "$subhome/.fm-secondmate-home"
|
|
2034
|
+
ln -s "$target" "$subhome/$opdir"
|
|
2035
|
+
cat > "$home/state/domain.meta" <<EOF
|
|
2036
|
+
window=firstmate:fm-domain
|
|
2037
|
+
worktree=$subhome
|
|
2038
|
+
project=$subhome
|
|
2039
|
+
harness=echo
|
|
2040
|
+
kind=secondmate
|
|
2041
|
+
mode=secondmate
|
|
2042
|
+
yolo=off
|
|
2043
|
+
home=$subhome
|
|
2044
|
+
projects=alpha
|
|
2045
|
+
EOF
|
|
2046
|
+
printf '%s\n' '- domain - design domain (home: '"$subhome"'; scope: design domain; projects: alpha; added 2026-06-22)' > "$home/data/secondmates.md"
|
|
2047
|
+
fakebin=$(make_fake_tmux "$TMP_ROOT/symlink-inside-teardown-fake-$opdir")
|
|
2048
|
+
log="$TMP_ROOT/symlink-inside-teardown-fake-$opdir/tmux.log"
|
|
2049
|
+
PATH="$fakebin:$PATH" FM_HOME="$home" FM_FAKE_TMUX_LOG="$log" FM_FAKE_TMUX_CAPTURE="$TMP_ROOT/symlink-inside-teardown-fake-$opdir/pane.txt" \
|
|
2050
|
+
"$ROOT/bin/fm-teardown.sh" domain --force >/dev/null 2>"$err" \
|
|
2051
|
+
|| fail "force teardown refused $opdir symlinked inside the secondmate home"
|
|
2052
|
+
[ ! -e "$subhome" ] || fail "force teardown did not remove subhome with inside $opdir symlink"
|
|
2053
|
+
[ ! -e "$home/state/domain.meta" ] || fail "force teardown did not clear parent meta for inside $opdir symlink"
|
|
2054
|
+
grep -F 'kill-window -t =firstmate:=fm-domain' "$log" >/dev/null || fail "force teardown did not kill parent window for inside $opdir symlink"
|
|
2055
|
+
done
|
|
2056
|
+
pass "force teardown allows non-state operational directory symlinks inside the subhome"
|
|
2057
|
+
}
|
|
2058
|
+
|
|
2059
|
+
test_secondmate_force_teardown_refuses_operational_dir_symlink_outside_home() {
|
|
2060
|
+
local home subhome external_state fakebin err log
|
|
2061
|
+
home="$TMP_ROOT/symlink-state-teardown-home"
|
|
2062
|
+
subhome="$TMP_ROOT/symlink-state-teardown-subhome"
|
|
2063
|
+
external_state="$home/data/external-state"
|
|
2064
|
+
err="$TMP_ROOT/symlink-state-teardown.err"
|
|
2065
|
+
mkdir -p "$home/state" "$home/data" "$subhome" "$external_state"
|
|
2066
|
+
printf 'domain\n' > "$subhome/.fm-secondmate-home"
|
|
2067
|
+
ln -s "$external_state" "$subhome/state"
|
|
2068
|
+
cat > "$home/state/domain.meta" <<EOF
|
|
2069
|
+
window=firstmate:fm-domain
|
|
2070
|
+
worktree=$subhome
|
|
2071
|
+
project=$subhome
|
|
2072
|
+
harness=echo
|
|
2073
|
+
kind=secondmate
|
|
2074
|
+
mode=secondmate
|
|
2075
|
+
yolo=off
|
|
2076
|
+
home=$subhome
|
|
2077
|
+
projects=alpha
|
|
2078
|
+
EOF
|
|
2079
|
+
printf '%s\n' '- domain - design domain (home: '"$subhome"'; scope: design domain; projects: alpha; added 2026-06-22)' > "$home/data/secondmates.md"
|
|
2080
|
+
fakebin=$(make_fake_tmux "$TMP_ROOT/symlink-state-teardown-fake")
|
|
2081
|
+
log="$TMP_ROOT/symlink-state-teardown-fake/tmux.log"
|
|
2082
|
+
if PATH="$fakebin:$PATH" FM_HOME="$home" FM_FAKE_TMUX_LOG="$log" FM_FAKE_TMUX_CAPTURE="$TMP_ROOT/symlink-state-teardown-fake/pane.txt" \
|
|
2083
|
+
"$ROOT/bin/fm-teardown.sh" domain --force >/dev/null 2>"$err"; then
|
|
2084
|
+
fail "force teardown accepted a symlinked secondmate state directory"
|
|
2085
|
+
fi
|
|
2086
|
+
[ -d "$subhome" ] || fail "force teardown removed subhome after symlinked state refusal"
|
|
2087
|
+
[ -d "$external_state" ] || fail "force teardown removed external symlink target"
|
|
2088
|
+
grep -F 'state directory' "$err" >/dev/null || fail "teardown did not explain symlinked state refusal"
|
|
2089
|
+
grep -F 'resolves outside the secondmate home' "$err" >/dev/null || fail "teardown did not identify unsafe state symlink"
|
|
2090
|
+
grep -F 'kill-window' "$log" >/dev/null && fail "teardown killed a window before symlinked state refusal"
|
|
2091
|
+
pass "force teardown refuses operational directory symlinks outside the subhome"
|
|
2092
|
+
}
|
|
2093
|
+
|
|
2094
|
+
test_secondmate_teardown_path_boundary_matrix() {
|
|
2095
|
+
# The teardown path-boundary matrix: a secondmate home is refused (and left
|
|
2096
|
+
# fully intact, with no window killed before validation) when it is unmarked,
|
|
2097
|
+
# an ancestor of the active firstmate home, inside the active firstmate home,
|
|
2098
|
+
# or inside the firstmate repo. One row per hazard, one shared assertion block.
|
|
2099
|
+
local row base home subhome fmroot fakebin log err expect tid
|
|
2100
|
+
while IFS='|' read -r row expect; do
|
|
2101
|
+
[ -n "$row" ] || continue
|
|
2102
|
+
base="$TMP_ROOT/td-pb-$row"
|
|
2103
|
+
fmroot="$ROOT" # real firstmate repo unless a row overrides it
|
|
2104
|
+
tid=domain
|
|
2105
|
+
case "$row" in
|
|
2106
|
+
unmarked)
|
|
2107
|
+
home="$base/main"; subhome="$base/sub"
|
|
2108
|
+
mkdir -p "$home/state" "$home/data" "$subhome/state"
|
|
2109
|
+
# No .fm-secondmate-home marker on purpose.
|
|
2110
|
+
;;
|
|
2111
|
+
ancestor)
|
|
2112
|
+
# The home being torn down is an ANCESTOR of the active firstmate home.
|
|
2113
|
+
subhome="$base/anc"; home="$subhome/main-home"
|
|
2114
|
+
mkdir -p "$home/state" "$home/data" "$subhome/state"
|
|
2115
|
+
printf 'domain\n' > "$subhome/.fm-secondmate-home"
|
|
2116
|
+
;;
|
|
2117
|
+
active-descendant)
|
|
2118
|
+
home="$base/desc"; subhome="$home/data/domain-home"
|
|
2119
|
+
mkdir -p "$home/state" "$home/data" "$subhome/state"
|
|
2120
|
+
printf 'domain\n' > "$subhome/.fm-secondmate-home"
|
|
2121
|
+
;;
|
|
2122
|
+
repo-descendant)
|
|
2123
|
+
home="$base/home"; fmroot="$base/root"; subhome="$fmroot/tmp/domain-home"; tid='repo-domain'
|
|
2124
|
+
mkdir -p "$home/state" "$home/data" "$subhome/state" "$fmroot/bin"
|
|
2125
|
+
cat > "$fmroot/bin/fm-guard.sh" <<'SH'
|
|
2126
|
+
#!/usr/bin/env bash
|
|
2127
|
+
exit 0
|
|
2128
|
+
SH
|
|
2129
|
+
chmod +x "$fmroot/bin/fm-guard.sh"
|
|
2130
|
+
printf 'repo-domain\n' > "$subhome/.fm-secondmate-home"
|
|
2131
|
+
;;
|
|
2132
|
+
esac
|
|
2133
|
+
fm_write_secondmate_meta "$home/state/$tid.meta" "$subhome"
|
|
2134
|
+
printf -- '- %s - design domain (home: %s; scope: design domain; projects: alpha; added 2026-06-22)\n' \
|
|
2135
|
+
"$tid" "$subhome" > "$home/data/secondmates.md"
|
|
2136
|
+
fakebin=$(make_fake_tmux "$base/fake")
|
|
2137
|
+
log="$base/fake/tmux.log"
|
|
2138
|
+
err="$base/teardown.err"
|
|
2139
|
+
if PATH="$fakebin:$PATH" FM_ROOT_OVERRIDE="$fmroot" FM_HOME="$home" \
|
|
2140
|
+
FM_FAKE_TMUX_LOG="$log" FM_FAKE_TMUX_CAPTURE="$base/fake/pane.txt" \
|
|
2141
|
+
"$ROOT/bin/fm-teardown.sh" "$tid" >/dev/null 2>"$err"; then
|
|
2142
|
+
fail "teardown ($row) accepted a hazardous secondmate home"
|
|
2143
|
+
fi
|
|
2144
|
+
grep -F "$expect" "$err" >/dev/null || fail "teardown ($row) did not explain the refusal (expected '$expect'): $(cat "$err")"
|
|
2145
|
+
[ -d "$subhome" ] || fail "teardown ($row) removed the protected home after refusal"
|
|
2146
|
+
[ -e "$home/state/$tid.meta" ] || fail "teardown ($row) cleared the parent meta after refusal"
|
|
2147
|
+
grep -F -- "- $tid " "$home/data/secondmates.md" >/dev/null || fail "teardown ($row) removed the registry route after refusal"
|
|
2148
|
+
grep -F 'kill-window' "$log" >/dev/null && fail "teardown ($row) killed a window before validation"
|
|
2149
|
+
done <<'ROWS'
|
|
2150
|
+
unmarked|not a seeded secondmate home
|
|
2151
|
+
ancestor|ancestor of the active firstmate home
|
|
2152
|
+
active-descendant|inside the active firstmate home
|
|
2153
|
+
repo-descendant|inside the firstmate repo
|
|
2154
|
+
ROWS
|
|
2155
|
+
pass "secondmate teardown path-boundary matrix refuses unmarked/ancestor/active-descendant/repo-descendant homes"
|
|
2156
|
+
}
|
|
2157
|
+
|
|
2158
|
+
test_secondmate_teardown_refuses_registered_nested_home() {
|
|
2159
|
+
local home subhome nested fakebin err log
|
|
2160
|
+
home="$TMP_ROOT/nested-teardown-home"
|
|
2161
|
+
subhome="$TMP_ROOT/nested-teardown-subhome"
|
|
2162
|
+
nested="$subhome/nested-domain"
|
|
2163
|
+
err="$TMP_ROOT/nested-teardown.err"
|
|
2164
|
+
mkdir -p "$home/state" "$home/data" "$subhome/state" "$nested/state"
|
|
2165
|
+
printf 'domain\n' > "$subhome/.fm-secondmate-home"
|
|
2166
|
+
printf 'nested\n' > "$nested/.fm-secondmate-home"
|
|
2167
|
+
cat > "$home/state/domain.meta" <<EOF
|
|
2168
|
+
window=firstmate:fm-domain
|
|
2169
|
+
worktree=$subhome
|
|
2170
|
+
project=$subhome
|
|
2171
|
+
harness=echo
|
|
2172
|
+
kind=secondmate
|
|
2173
|
+
mode=secondmate
|
|
2174
|
+
yolo=off
|
|
2175
|
+
home=$subhome
|
|
2176
|
+
projects=alpha
|
|
2177
|
+
EOF
|
|
2178
|
+
cat > "$home/state/nested.meta" <<EOF
|
|
2179
|
+
window=firstmate:fm-nested
|
|
2180
|
+
worktree=$nested
|
|
2181
|
+
project=$nested
|
|
2182
|
+
harness=echo
|
|
2183
|
+
kind=secondmate
|
|
2184
|
+
mode=secondmate
|
|
2185
|
+
yolo=off
|
|
2186
|
+
home=$nested
|
|
2187
|
+
projects=beta
|
|
2188
|
+
EOF
|
|
2189
|
+
cat > "$home/data/secondmates.md" <<EOF
|
|
2190
|
+
- domain - design domain (home: $subhome; scope: design domain; projects: alpha; added 2026-06-22)
|
|
2191
|
+
- nested - nested domain mentions home: $TMP_ROOT/ignored-summary-home (home: $nested; scope: nested domain mentions home: $TMP_ROOT/ignored-scope-home; projects: beta; added 2026-06-22)
|
|
2192
|
+
EOF
|
|
2193
|
+
fakebin=$(make_fake_tmux "$TMP_ROOT/nested-teardown-fake")
|
|
2194
|
+
log="$TMP_ROOT/nested-teardown-fake/tmux.log"
|
|
2195
|
+
if PATH="$fakebin:$PATH" FM_HOME="$home" FM_FAKE_TMUX_LOG="$log" FM_FAKE_TMUX_CAPTURE="$TMP_ROOT/nested-teardown-fake/pane.txt" \
|
|
2196
|
+
"$ROOT/bin/fm-teardown.sh" domain >/dev/null 2>"$err"; then
|
|
2197
|
+
fail "teardown removed a home containing another registered secondmate home"
|
|
2198
|
+
fi
|
|
2199
|
+
[ -d "$subhome" ] || fail "teardown removed registered ancestor home after refusal"
|
|
2200
|
+
[ -d "$nested" ] || fail "teardown removed registered nested home after refusal"
|
|
2201
|
+
[ -e "$home/state/domain.meta" ] || fail "teardown cleared ancestor meta after nested-home refusal"
|
|
2202
|
+
[ -e "$home/state/nested.meta" ] || fail "teardown cleared nested meta after nested-home refusal"
|
|
2203
|
+
grep -F 'kill-window' "$log" >/dev/null && fail "teardown killed a window before nested-home refusal"
|
|
2204
|
+
grep -F 'contains registered secondmate home' "$err" >/dev/null || fail "teardown did not explain registered nested-home refusal"
|
|
2205
|
+
pass "secondmate teardown refuses homes containing registered nested homes"
|
|
2206
|
+
}
|
|
2207
|
+
|
|
2208
|
+
test_secondmate_teardown_refuses_child_registry_nested_home() {
|
|
2209
|
+
local home subhome nested fakebin err log
|
|
2210
|
+
home="$TMP_ROOT/child-registry-teardown-home"
|
|
2211
|
+
subhome="$TMP_ROOT/child-registry-teardown-subhome"
|
|
2212
|
+
nested="$subhome/nested-domain"
|
|
2213
|
+
err="$TMP_ROOT/child-registry-teardown.err"
|
|
2214
|
+
mkdir -p "$home/state" "$home/data" "$subhome/state" "$subhome/data" "$nested/state"
|
|
2215
|
+
printf 'domain\n' > "$subhome/.fm-secondmate-home"
|
|
2216
|
+
printf 'nested\n' > "$nested/.fm-secondmate-home"
|
|
2217
|
+
cat > "$home/state/domain.meta" <<EOF
|
|
2218
|
+
window=firstmate:fm-domain
|
|
2219
|
+
worktree=$subhome
|
|
2220
|
+
project=$subhome
|
|
2221
|
+
harness=echo
|
|
2222
|
+
kind=secondmate
|
|
2223
|
+
mode=secondmate
|
|
2224
|
+
yolo=off
|
|
2225
|
+
home=$subhome
|
|
2226
|
+
projects=alpha
|
|
2227
|
+
EOF
|
|
2228
|
+
printf '%s\n' '- domain - design domain (home: '"$subhome"'; scope: design domain; projects: alpha; added 2026-06-22)' > "$home/data/secondmates.md"
|
|
2229
|
+
printf '%s\n' '- nested - nested domain (home: '"$nested"'; scope: nested domain; projects: beta; added 2026-06-22)' > "$subhome/data/secondmates.md"
|
|
2230
|
+
fakebin=$(make_fake_tmux "$TMP_ROOT/child-registry-teardown-fake")
|
|
2231
|
+
log="$TMP_ROOT/child-registry-teardown-fake/tmux.log"
|
|
2232
|
+
if PATH="$fakebin:$PATH" FM_HOME="$home" FM_FAKE_TMUX_LOG="$log" FM_FAKE_TMUX_CAPTURE="$TMP_ROOT/child-registry-teardown-fake/pane.txt" \
|
|
2233
|
+
"$ROOT/bin/fm-teardown.sh" domain >/dev/null 2>"$err"; then
|
|
2234
|
+
fail "teardown removed a home containing a child-registry secondmate home"
|
|
2235
|
+
fi
|
|
2236
|
+
[ -d "$subhome" ] || fail "teardown removed ancestor home after child-registry refusal"
|
|
2237
|
+
[ -d "$nested" ] || fail "teardown removed child-registry nested home after refusal"
|
|
2238
|
+
[ -e "$home/state/domain.meta" ] || fail "teardown cleared parent meta after child-registry refusal"
|
|
2239
|
+
grep -F 'kill-window' "$log" >/dev/null && fail "teardown killed a window before child-registry refusal"
|
|
2240
|
+
grep -F 'contains registered secondmate home' "$err" >/dev/null || fail "teardown did not explain child-registry nested-home refusal"
|
|
2241
|
+
pass "secondmate teardown refuses nested homes from the child registry"
|
|
2242
|
+
}
|
|
2243
|
+
|
|
2244
|
+
test_secondmate_force_teardown_prevalidates_before_child_cleanup() {
|
|
2245
|
+
local home subhome childproj childwt fakebin err log
|
|
2246
|
+
home="$TMP_ROOT/prevalidate-teardown-home"
|
|
2247
|
+
subhome="$TMP_ROOT/prevalidate-teardown-subhome"
|
|
2248
|
+
childproj="$subhome/projects/alpha"
|
|
2249
|
+
childwt="$TMP_ROOT/prevalidate-child-worktree"
|
|
2250
|
+
err="$TMP_ROOT/prevalidate-teardown.err"
|
|
2251
|
+
mkdir -p "$home/state" "$home/data" "$subhome/state" "$childproj" "$childwt"
|
|
2252
|
+
cat > "$home/state/domain.meta" <<EOF
|
|
2253
|
+
window=firstmate:fm-domain
|
|
2254
|
+
worktree=$subhome
|
|
2255
|
+
project=$subhome
|
|
2256
|
+
harness=echo
|
|
2257
|
+
kind=secondmate
|
|
2258
|
+
mode=secondmate
|
|
2259
|
+
yolo=off
|
|
2260
|
+
home=$subhome
|
|
2261
|
+
projects=alpha
|
|
2262
|
+
EOF
|
|
2263
|
+
printf '%s\n' '- domain - design domain (home: '"$subhome"'; scope: design domain; projects: alpha; added 2026-06-22)' > "$home/data/secondmates.md"
|
|
2264
|
+
cat > "$subhome/state/child.meta" <<EOF
|
|
2265
|
+
window=firstmate:fm-child
|
|
2266
|
+
worktree=$childwt
|
|
2267
|
+
project=$childproj
|
|
2268
|
+
harness=echo
|
|
2269
|
+
kind=ship
|
|
2270
|
+
mode=no-mistakes
|
|
2271
|
+
yolo=off
|
|
2272
|
+
EOF
|
|
2273
|
+
fakebin=$(make_fake_tmux "$TMP_ROOT/prevalidate-teardown-fake")
|
|
2274
|
+
log="$TMP_ROOT/prevalidate-teardown-fake/tmux.log"
|
|
2275
|
+
if PATH="$fakebin:$PATH" FM_HOME="$home" FM_FAKE_TMUX_LOG="$log" FM_FAKE_TMUX_CAPTURE="$TMP_ROOT/prevalidate-teardown-fake/pane.txt" \
|
|
2276
|
+
"$ROOT/bin/fm-teardown.sh" domain --force >/dev/null 2>"$err"; then
|
|
2277
|
+
fail "force teardown discarded child work before validating subhome"
|
|
2278
|
+
fi
|
|
2279
|
+
[ -d "$subhome" ] || fail "force teardown removed unmarked subhome after refusal"
|
|
2280
|
+
[ -d "$childwt" ] || fail "force teardown removed child worktree before validation"
|
|
2281
|
+
[ -e "$home/state/domain.meta" ] || fail "force teardown cleared parent meta before validation"
|
|
2282
|
+
[ -e "$subhome/state/child.meta" ] || fail "force teardown cleared child meta before validation"
|
|
2283
|
+
grep -F 'kill-window' "$log" >/dev/null && fail "force teardown killed windows before subhome validation"
|
|
2284
|
+
grep -F 'not a seeded secondmate home' "$err" >/dev/null || fail "force teardown did not explain missing seed marker"
|
|
2285
|
+
pass "force teardown validates subhome before child cleanup"
|
|
2286
|
+
}
|
|
2287
|
+
|
|
2288
|
+
# A per-task lock cannot protect a task that does not exist yet. Forced teardown
|
|
2289
|
+
# enumerates a home's task set, locks what it found, then re-enumerates while
|
|
2290
|
+
# removing - so a fresh spawn publishing inside that window was destructively
|
|
2291
|
+
# processed while never lifecycle-locked (reproduced with real agents). The
|
|
2292
|
+
# per-home task-set lock serializes the two. Both directions are asserted here,
|
|
2293
|
+
# by HOLDING the lock rather than racing on timing, so neither case can go
|
|
2294
|
+
# quietly vacuous.
|
|
2295
|
+
seed_task_set_lock_home() { # <tag> -> echoes "<home>|<subhome>"
|
|
2296
|
+
local tag=$1 home subhome childproj childwt
|
|
2297
|
+
home="$TMP_ROOT/$tag-home"
|
|
2298
|
+
subhome="$TMP_ROOT/$tag-subhome"
|
|
2299
|
+
childproj="$subhome/projects/alpha"
|
|
2300
|
+
childwt="$TMP_ROOT/$tag-child-worktree"
|
|
2301
|
+
mkdir -p "$home/state" "$home/data" "$subhome/state" "$subhome/data"
|
|
2302
|
+
# A real worktree pair: child-removal validation runs before the task-set
|
|
2303
|
+
# preflight and refuses a child whose worktree is not a genuine worktree of
|
|
2304
|
+
# its project, which would mask the refusal under test.
|
|
2305
|
+
fm_git_worktree "$childproj" "$childwt" "child-$tag"
|
|
2306
|
+
printf '%s\n' domain > "$subhome/.fm-secondmate-home"
|
|
2307
|
+
cat > "$home/state/domain.meta" <<EOF
|
|
2308
|
+
window=firstmate:fm-domain
|
|
2309
|
+
worktree=$subhome
|
|
2310
|
+
project=$subhome
|
|
2311
|
+
harness=echo
|
|
2312
|
+
kind=secondmate
|
|
2313
|
+
mode=secondmate
|
|
2314
|
+
yolo=off
|
|
2315
|
+
home=$subhome
|
|
2316
|
+
projects=alpha
|
|
2317
|
+
EOF
|
|
2318
|
+
printf '%s\n' '- domain - design domain (home: '"$subhome"'; scope: design domain; projects: alpha; added 2026-06-22)' > "$home/data/secondmates.md"
|
|
2319
|
+
cat > "$subhome/state/child.meta" <<EOF
|
|
2320
|
+
window=firstmate:fm-child
|
|
2321
|
+
worktree=$childwt
|
|
2322
|
+
project=$childproj
|
|
2323
|
+
harness=echo
|
|
2324
|
+
kind=ship
|
|
2325
|
+
mode=no-mistakes
|
|
2326
|
+
yolo=off
|
|
2327
|
+
EOF
|
|
2328
|
+
printf '%s|%s\n' "$home" "$subhome"
|
|
2329
|
+
}
|
|
2330
|
+
|
|
2331
|
+
task_set_lock_path() { # <state-dir>
|
|
2332
|
+
local state=$1
|
|
2333
|
+
( . "$ROOT/bin/fm-wake-lib.sh"; fm_task_set_lock_path "$state" )
|
|
2334
|
+
}
|
|
2335
|
+
|
|
2336
|
+
# The holder must stay ALIVE: fm_lock_try_acquire reclaims a lock whose owning
|
|
2337
|
+
# pid is gone, so a lock taken in a subshell that then exits would be stolen and
|
|
2338
|
+
# the contention under test would never happen.
|
|
2339
|
+
hold_task_set_lock() { # <state-dir> -> echoes "<holder-pid> <lock-path>"
|
|
2340
|
+
local state=$1 lock holder i=0
|
|
2341
|
+
lock=$(task_set_lock_path "$state") || return 1
|
|
2342
|
+
[ -n "$lock" ] || return 1
|
|
2343
|
+
# stdout/stderr are redirected so the long-lived holder does not inherit this
|
|
2344
|
+
# function's command-substitution pipe; leaving it open would block the caller
|
|
2345
|
+
# until the holder exited, by which point the lock would be stale and the
|
|
2346
|
+
# contention under test could not happen.
|
|
2347
|
+
(
|
|
2348
|
+
# shellcheck source=/dev/null
|
|
2349
|
+
. "$ROOT/bin/fm-wake-lib.sh"
|
|
2350
|
+
fm_lock_try_acquire "$lock" || exit 1
|
|
2351
|
+
sleep 30
|
|
2352
|
+
) >/dev/null 2>&1 &
|
|
2353
|
+
holder=$!
|
|
2354
|
+
while [ ! -e "$lock" ] && [ "$i" -lt 100 ]; do
|
|
2355
|
+
sleep 0.1
|
|
2356
|
+
i=$((i + 1))
|
|
2357
|
+
done
|
|
2358
|
+
[ -e "$lock" ] || {
|
|
2359
|
+
kill "$holder" 2>/dev/null || true
|
|
2360
|
+
wait "$holder" 2>/dev/null || true
|
|
2361
|
+
return 1
|
|
2362
|
+
}
|
|
2363
|
+
printf '%s %s\n' "$holder" "$lock"
|
|
2364
|
+
}
|
|
2365
|
+
|
|
2366
|
+
seed_empty_task_set_home() { # <tag> -> echoes "<home>|<subhome>"
|
|
2367
|
+
local tag=$1 home subhome
|
|
2368
|
+
home="$TMP_ROOT/$tag-home"
|
|
2369
|
+
subhome="$TMP_ROOT/$tag-subhome"
|
|
2370
|
+
mkdir -p "$home/state" "$home/data" "$subhome/data"
|
|
2371
|
+
printf '%s\n' domain > "$subhome/.fm-secondmate-home"
|
|
2372
|
+
cat > "$home/state/domain.meta" <<EOF
|
|
2373
|
+
window=firstmate:fm-domain
|
|
2374
|
+
worktree=$subhome
|
|
2375
|
+
project=$subhome
|
|
2376
|
+
harness=echo
|
|
2377
|
+
kind=secondmate
|
|
2378
|
+
mode=secondmate
|
|
2379
|
+
yolo=off
|
|
2380
|
+
home=$subhome
|
|
2381
|
+
projects=alpha
|
|
2382
|
+
EOF
|
|
2383
|
+
printf '%s\n' '- domain - design domain (home: '"$subhome"'; scope: design domain; projects: alpha; added 2026-06-22)' > "$home/data/secondmates.md"
|
|
2384
|
+
printf '%s|%s\n' "$home" "$subhome"
|
|
2385
|
+
}
|
|
2386
|
+
|
|
2387
|
+
test_force_teardown_refuses_non_directory_descendant_state() {
|
|
2388
|
+
local home subhome fakebin err log rec
|
|
2389
|
+
rec=$(seed_empty_task_set_home taskset-state-file)
|
|
2390
|
+
IFS='|' read -r home subhome <<EOF
|
|
2391
|
+
$rec
|
|
2392
|
+
EOF
|
|
2393
|
+
printf '%s\n' 'not a state directory' > "$subhome/state"
|
|
2394
|
+
err="$TMP_ROOT/taskset-state-file.err"
|
|
2395
|
+
fakebin=$(make_fake_tmux "$TMP_ROOT/taskset-state-file-fake")
|
|
2396
|
+
log="$TMP_ROOT/taskset-state-file-fake/tmux.log"
|
|
2397
|
+
if PATH="$fakebin:$PATH" FM_HOME="$home" FM_FAKE_TMUX_LOG="$log" \
|
|
2398
|
+
FM_FAKE_TMUX_CAPTURE="$TMP_ROOT/taskset-state-file-fake/pane.txt" \
|
|
2399
|
+
"$ROOT/bin/fm-teardown.sh" domain --force >/dev/null 2>"$err"; then
|
|
2400
|
+
fail "forced teardown accepted a non-directory descendant state path"
|
|
2401
|
+
fi
|
|
2402
|
+
[ -d "$subhome" ] || fail "state-path refusal removed the descendant home"
|
|
2403
|
+
[ -f "$subhome/state" ] || fail "state-path refusal changed the non-directory state path"
|
|
2404
|
+
[ -e "$home/state/domain.meta" ] || fail "state-path refusal removed parent metadata"
|
|
2405
|
+
grep -F 'kill-window' "$log" >/dev/null && fail "state-path refusal killed a window"
|
|
2406
|
+
grep -F "$(basename "$subhome")" "$err" >/dev/null || fail "state-path refusal did not name the descendant home: $(cat "$err")"
|
|
2407
|
+
grep -F 'not a directory' "$err" >/dev/null || fail "state-path refusal did not explain the concrete problem: $(cat "$err")"
|
|
2408
|
+
pass "forced teardown refuses a non-directory descendant state path"
|
|
2409
|
+
}
|
|
2410
|
+
|
|
2411
|
+
test_force_teardown_refuses_symlinked_descendant_state() {
|
|
2412
|
+
local home subhome fakebin err log rec
|
|
2413
|
+
rec=$(seed_empty_task_set_home taskset-state-symlink)
|
|
2414
|
+
IFS='|' read -r home subhome <<EOF
|
|
2415
|
+
$rec
|
|
2416
|
+
EOF
|
|
2417
|
+
mkdir -p "$subhome/state-target"
|
|
2418
|
+
ln -s state-target "$subhome/state"
|
|
2419
|
+
err="$TMP_ROOT/taskset-state-symlink.err"
|
|
2420
|
+
fakebin=$(make_fake_tmux "$TMP_ROOT/taskset-state-symlink-fake")
|
|
2421
|
+
log="$TMP_ROOT/taskset-state-symlink-fake/tmux.log"
|
|
2422
|
+
if PATH="$fakebin:$PATH" FM_HOME="$home" FM_FAKE_TMUX_LOG="$log" \
|
|
2423
|
+
FM_FAKE_TMUX_CAPTURE="$TMP_ROOT/taskset-state-symlink-fake/pane.txt" \
|
|
2424
|
+
"$ROOT/bin/fm-teardown.sh" domain --force >/dev/null 2>"$err"; then
|
|
2425
|
+
fail "forced teardown accepted a symlinked descendant state path"
|
|
2426
|
+
fi
|
|
2427
|
+
[ -d "$subhome" ] || fail "symlinked state-path refusal removed the descendant home"
|
|
2428
|
+
[ -L "$subhome/state" ] || fail "symlinked state-path refusal changed the state symlink"
|
|
2429
|
+
[ -d "$subhome/state-target" ] || fail "symlinked state-path refusal changed the state target"
|
|
2430
|
+
[ -e "$home/state/domain.meta" ] || fail "symlinked state-path refusal removed parent metadata"
|
|
2431
|
+
grep -F 'kill-window' "$log" >/dev/null && fail "symlinked state-path refusal killed a window"
|
|
2432
|
+
grep -F "$(basename "$subhome")" "$err" >/dev/null || fail "symlinked state-path refusal did not name the descendant home: $(cat "$err")"
|
|
2433
|
+
grep -F 'symbolic-link state path' "$err" >/dev/null || fail "symlinked state-path refusal did not explain the concrete problem: $(cat "$err")"
|
|
2434
|
+
pass "forced teardown refuses a symlinked descendant state path"
|
|
2435
|
+
}
|
|
2436
|
+
|
|
2437
|
+
test_force_teardown_locks_descendant_with_absent_state() {
|
|
2438
|
+
local home subhome fakebin err log rec claim_root ready release lock pid i=0
|
|
2439
|
+
rec=$(seed_empty_task_set_home taskset-state-absent)
|
|
2440
|
+
IFS='|' read -r home subhome <<EOF
|
|
2441
|
+
$rec
|
|
2442
|
+
EOF
|
|
2443
|
+
claim_root="$TMP_ROOT/taskset-state-absent-xdg/firstmate/procevent-claims"
|
|
2444
|
+
ready="$TMP_ROOT/taskset-state-absent.ready"
|
|
2445
|
+
release="$TMP_ROOT/taskset-state-absent.release"
|
|
2446
|
+
mkdir -p "$claim_root" "$subhome/bin"
|
|
2447
|
+
printf '%s\n' "$subhome" > "$claim_root/held.claim"
|
|
2448
|
+
cat > "$subhome/bin/fm-procevent.sh" <<'SH'
|
|
2449
|
+
#!/usr/bin/env bash
|
|
2450
|
+
set -u
|
|
2451
|
+
if [ "${1:-}" = sweep-home ] && [ "${2:-}" = --preflight ]; then
|
|
2452
|
+
: > "$FM_TASK_SET_TEST_READY"
|
|
2453
|
+
while [ ! -e "$FM_TASK_SET_TEST_RELEASE" ]; do sleep 0.05; done
|
|
2454
|
+
exit 1
|
|
2455
|
+
fi
|
|
2456
|
+
exit 1
|
|
2457
|
+
SH
|
|
2458
|
+
chmod +x "$subhome/bin/fm-procevent.sh"
|
|
2459
|
+
err="$TMP_ROOT/taskset-state-absent.err"
|
|
2460
|
+
fakebin=$(make_fake_tmux "$TMP_ROOT/taskset-state-absent-fake")
|
|
2461
|
+
log="$TMP_ROOT/taskset-state-absent-fake/tmux.log"
|
|
2462
|
+
PATH="$fakebin:$PATH" FM_HOME="$home" FM_FAKE_TMUX_LOG="$log" \
|
|
2463
|
+
FM_FAKE_TMUX_CAPTURE="$TMP_ROOT/taskset-state-absent-fake/pane.txt" \
|
|
2464
|
+
XDG_STATE_HOME="$TMP_ROOT/taskset-state-absent-xdg" \
|
|
2465
|
+
FM_TASK_SET_TEST_READY="$ready" FM_TASK_SET_TEST_RELEASE="$release" \
|
|
2466
|
+
"$ROOT/bin/fm-teardown.sh" domain --force >/dev/null 2>"$err" &
|
|
2467
|
+
pid=$!
|
|
2468
|
+
while [ ! -e "$ready" ] && kill -0 "$pid" 2>/dev/null && [ "$i" -lt 200 ]; do
|
|
2469
|
+
sleep 0.05
|
|
2470
|
+
i=$((i + 1))
|
|
2471
|
+
done
|
|
2472
|
+
[ -e "$ready" ] || {
|
|
2473
|
+
: > "$release"
|
|
2474
|
+
wait "$pid" 2>/dev/null || true
|
|
2475
|
+
fail "forced teardown did not reach the post-lock preflight: $(cat "$err")"
|
|
2476
|
+
}
|
|
2477
|
+
lock=$(task_set_lock_path "$subhome/state") \
|
|
2478
|
+
|| fail "could not resolve the established descendant task-set lock"
|
|
2479
|
+
[ -d "$subhome/state" ] || fail "forced teardown did not establish the absent state directory"
|
|
2480
|
+
[ -e "$lock" ] || fail "forced teardown did not own the descendant task-set lock during preflight"
|
|
2481
|
+
kill -0 "$pid" 2>/dev/null || fail "forced teardown exited before task-set ownership was observed"
|
|
2482
|
+
: > "$release"
|
|
2483
|
+
if wait "$pid"; then
|
|
2484
|
+
fail "forced teardown ignored the staged process-event preflight refusal"
|
|
2485
|
+
fi
|
|
2486
|
+
[ -d "$subhome" ] || fail "post-lock refusal removed the descendant home"
|
|
2487
|
+
[ -e "$home/state/domain.meta" ] || fail "post-lock refusal removed parent metadata"
|
|
2488
|
+
[ ! -e "$lock" ] || fail "refused teardown left the descendant task-set lock behind"
|
|
2489
|
+
grep -F 'kill-window' "$log" >/dev/null && fail "post-lock refusal killed a window"
|
|
2490
|
+
pass "forced teardown locks a descendant whose state directory was absent"
|
|
2491
|
+
}
|
|
2492
|
+
|
|
2493
|
+
test_force_teardown_refuses_while_a_task_is_being_published() {
|
|
2494
|
+
local home subhome fakebin err log lock rec held holder
|
|
2495
|
+
rec=$(seed_task_set_lock_home taskset-teardown)
|
|
2496
|
+
IFS='|' read -r home subhome <<EOF
|
|
2497
|
+
$rec
|
|
2498
|
+
EOF
|
|
2499
|
+
err="$TMP_ROOT/taskset-teardown.err"
|
|
2500
|
+
# Stand in for a fresh spawn that is mid-publication in the secondmate's home.
|
|
2501
|
+
held=$(hold_task_set_lock "$subhome/state") \
|
|
2502
|
+
|| fail "could not stage a held task-set lock"
|
|
2503
|
+
holder=${held%% *}
|
|
2504
|
+
lock=${held#* }
|
|
2505
|
+
fakebin=$(make_fake_tmux "$TMP_ROOT/taskset-teardown-fake")
|
|
2506
|
+
log="$TMP_ROOT/taskset-teardown-fake/tmux.log"
|
|
2507
|
+
if PATH="$fakebin:$PATH" FM_HOME="$home" FM_FAKE_TMUX_LOG="$log" \
|
|
2508
|
+
FM_FAKE_TMUX_CAPTURE="$TMP_ROOT/taskset-teardown-fake/pane.txt" \
|
|
2509
|
+
"$ROOT/bin/fm-teardown.sh" domain --force >/dev/null 2>"$err"; then
|
|
2510
|
+
fail "forced teardown proceeded while a task was being published"
|
|
2511
|
+
fi
|
|
2512
|
+
[ -d "$subhome" ] || fail "forced teardown removed the home despite refusing"
|
|
2513
|
+
[ -e "$subhome/state/child.meta" ] || fail "forced teardown removed child metadata despite refusing"
|
|
2514
|
+
[ -e "$home/state/domain.meta" ] || fail "forced teardown cleared parent metadata despite refusing"
|
|
2515
|
+
grep -F 'kill-window' "$log" >/dev/null && fail "forced teardown killed a window despite refusing"
|
|
2516
|
+
[ -e "$lock" ] || fail "forced teardown removed the publisher's task-set lock"
|
|
2517
|
+
grep -F 'task-set lock is held' "$err" >/dev/null \
|
|
2518
|
+
|| fail "the refusal did not name the task-set contention: $(cat "$err")"
|
|
2519
|
+
kill "$holder" 2>/dev/null || true
|
|
2520
|
+
wait "$holder" 2>/dev/null || true
|
|
2521
|
+
pass "forced teardown refuses while a fresh task is being published in the home"
|
|
2522
|
+
}
|
|
2523
|
+
|
|
2524
|
+
test_fresh_spawn_refuses_while_a_forced_teardown_owns_the_task_set() {
|
|
2525
|
+
local home subhome err rec held holder lock
|
|
2526
|
+
rec=$(seed_task_set_lock_home taskset-spawn)
|
|
2527
|
+
IFS='|' read -r home subhome <<EOF
|
|
2528
|
+
$rec
|
|
2529
|
+
EOF
|
|
2530
|
+
err="$TMP_ROOT/taskset-spawn.err"
|
|
2531
|
+
# Stand in for a forced teardown that already enumerated this home's task set.
|
|
2532
|
+
held=$(hold_task_set_lock "$subhome/state") \
|
|
2533
|
+
|| fail "could not stage a held task-set lock"
|
|
2534
|
+
holder=${held%% *}
|
|
2535
|
+
lock=${held#* }
|
|
2536
|
+
if FM_HOME="$subhome" FM_SPAWN_NO_GUARD=1 \
|
|
2537
|
+
"$ROOT/bin/fm-spawn.sh" newtask "$subhome/projects/alpha" --scout >/dev/null 2>"$err"; then
|
|
2538
|
+
kill "$holder" 2>/dev/null || true
|
|
2539
|
+
fail "a fresh spawn published a task while a forced teardown owned the set"
|
|
2540
|
+
fi
|
|
2541
|
+
[ ! -e "$subhome/state/newtask.meta" ] \
|
|
2542
|
+
|| fail "a refused spawn still published a durable record"
|
|
2543
|
+
[ -e "$lock" ] || fail "a refused spawn removed the teardown's task-set lock"
|
|
2544
|
+
grep -F "task set is locked" "$err" >/dev/null \
|
|
2545
|
+
|| fail "the spawn refusal did not name the task-set contention: $(cat "$err")"
|
|
2546
|
+
[ ! -e "$subhome/state/.spawn-newtask.lock" ] \
|
|
2547
|
+
|| fail "a refused spawn left its own task lock behind"
|
|
2548
|
+
kill "$holder" 2>/dev/null || true
|
|
2549
|
+
wait "$holder" 2>/dev/null || true
|
|
2550
|
+
pass "a fresh spawn refuses to publish while a forced teardown owns the task set"
|
|
2551
|
+
}
|
|
2552
|
+
|
|
2553
|
+
test_fresh_remote_secondmate_spawn_refuses_while_task_set_is_owned() {
|
|
2554
|
+
local home err held holder lock
|
|
2555
|
+
home="$TMP_ROOT/taskset-remote-spawn-home"
|
|
2556
|
+
err="$TMP_ROOT/taskset-remote-spawn.err"
|
|
2557
|
+
mkdir -p "$home/state" "$home/data"
|
|
2558
|
+
printf '%s\n' '- remote-new - remote domain (host: remote-mac; root: /remote/root; home: /remote/home; scope: remote work; projects: alpha; added 2026-08-02)' \
|
|
2559
|
+
> "$home/data/secondmates.md"
|
|
2560
|
+
held=$(hold_task_set_lock "$home/state") \
|
|
2561
|
+
|| fail "could not stage a held remote-spawn task-set lock"
|
|
2562
|
+
holder=${held%% *}
|
|
2563
|
+
lock=${held#* }
|
|
2564
|
+
if FM_HOME="$home" FM_SPAWN_NO_GUARD=1 \
|
|
2565
|
+
"$ROOT/bin/fm-spawn.sh" remote-new --secondmate >/dev/null 2>"$err"; then
|
|
2566
|
+
kill "$holder" 2>/dev/null || true
|
|
2567
|
+
fail "a fresh remote secondmate spawn published while the task set was owned"
|
|
2568
|
+
fi
|
|
2569
|
+
[ ! -e "$home/state/remote-new.meta" ] \
|
|
2570
|
+
|| fail "a refused remote secondmate spawn still published a durable record"
|
|
2571
|
+
[ -e "$lock" ] || fail "a refused remote secondmate spawn removed the owner's task-set lock"
|
|
2572
|
+
grep -F "task set is locked" "$err" >/dev/null \
|
|
2573
|
+
|| fail "the remote spawn refusal did not name task-set contention: $(cat "$err")"
|
|
2574
|
+
[ ! -e "$home/state/.spawn-remote-new.lock" ] \
|
|
2575
|
+
|| fail "a refused remote secondmate spawn left its own task lock behind"
|
|
2576
|
+
kill "$holder" 2>/dev/null || true
|
|
2577
|
+
wait "$holder" 2>/dev/null || true
|
|
2578
|
+
pass "a fresh remote secondmate spawn refuses while the task set is owned"
|
|
2579
|
+
}
|
|
2580
|
+
|
|
2581
|
+
test_secondmate_force_teardown_refuses_child_active_home_descendant() {
|
|
2582
|
+
local home subhome childproj childwt fakebin err log
|
|
2583
|
+
home="$TMP_ROOT/child-active-descendant-home"
|
|
2584
|
+
subhome="$TMP_ROOT/child-active-descendant-subhome"
|
|
2585
|
+
childproj="$subhome/projects/alpha"
|
|
2586
|
+
childwt="$home/data"
|
|
2587
|
+
err="$TMP_ROOT/child-active-descendant.err"
|
|
2588
|
+
mkdir -p "$home/state" "$home/data" "$subhome/state" "$childproj"
|
|
2589
|
+
printf 'domain\n' > "$subhome/.fm-secondmate-home"
|
|
2590
|
+
cat > "$home/state/domain.meta" <<EOF
|
|
2591
|
+
window=firstmate:fm-domain
|
|
2592
|
+
worktree=$subhome
|
|
2593
|
+
project=$subhome
|
|
2594
|
+
harness=echo
|
|
2595
|
+
kind=secondmate
|
|
2596
|
+
mode=secondmate
|
|
2597
|
+
yolo=off
|
|
2598
|
+
home=$subhome
|
|
2599
|
+
projects=alpha
|
|
2600
|
+
EOF
|
|
2601
|
+
printf '%s\n' '- domain - design domain (home: '"$subhome"'; scope: design domain; projects: alpha; added 2026-06-22)' > "$home/data/secondmates.md"
|
|
2602
|
+
cat > "$subhome/state/child.meta" <<EOF
|
|
2603
|
+
window=firstmate:fm-child
|
|
2604
|
+
worktree=$childwt
|
|
2605
|
+
project=$childproj
|
|
2606
|
+
harness=echo
|
|
2607
|
+
kind=ship
|
|
2608
|
+
mode=no-mistakes
|
|
2609
|
+
yolo=off
|
|
2610
|
+
EOF
|
|
2611
|
+
fakebin=$(make_fake_tmux "$TMP_ROOT/child-active-descendant-fake")
|
|
2612
|
+
log="$TMP_ROOT/child-active-descendant-fake/tmux.log"
|
|
2613
|
+
if PATH="$fakebin:$PATH" FM_HOME="$home" FM_FAKE_TMUX_LOG="$log" FM_FAKE_TMUX_CAPTURE="$TMP_ROOT/child-active-descendant-fake/pane.txt" \
|
|
2614
|
+
"$ROOT/bin/fm-teardown.sh" domain --force >/dev/null 2>"$err"; then
|
|
2615
|
+
fail "force teardown removed a child worktree inside active FM_HOME"
|
|
2616
|
+
fi
|
|
2617
|
+
[ -d "$home/data" ] || fail "force teardown removed active home data"
|
|
2618
|
+
[ -d "$subhome" ] || fail "force teardown removed subhome after child validation refusal"
|
|
2619
|
+
[ -e "$home/state/domain.meta" ] || fail "force teardown cleared parent meta after child validation refusal"
|
|
2620
|
+
[ -e "$subhome/state/child.meta" ] || fail "force teardown cleared child meta after child validation refusal"
|
|
2621
|
+
grep -F 'kill-window' "$log" >/dev/null && fail "force teardown killed windows before child validation refusal"
|
|
2622
|
+
grep -F 'inside the active firstmate home' "$err" >/dev/null || fail "force teardown did not explain active home descendant rejection"
|
|
2623
|
+
pass "force teardown refuses child worktrees inside the active home"
|
|
2624
|
+
}
|
|
2625
|
+
|
|
2626
|
+
test_secondmate_force_teardown_refuses_child_repo_descendant() {
|
|
2627
|
+
local home subhome childproj childwt fakeroot fakebin err log
|
|
2628
|
+
home="$TMP_ROOT/child-repo-descendant-home"
|
|
2629
|
+
subhome="$TMP_ROOT/child-repo-descendant-subhome"
|
|
2630
|
+
childproj="$subhome/projects/alpha"
|
|
2631
|
+
fakeroot="$TMP_ROOT/child-repo-descendant-root"
|
|
2632
|
+
childwt="$fakeroot/data"
|
|
2633
|
+
err="$TMP_ROOT/child-repo-descendant.err"
|
|
2634
|
+
mkdir -p "$home/state" "$home/data" "$subhome/state" "$childproj" "$childwt" "$fakeroot/bin"
|
|
2635
|
+
cat > "$fakeroot/bin/fm-guard.sh" <<'SH'
|
|
2636
|
+
#!/usr/bin/env bash
|
|
2637
|
+
exit 0
|
|
2638
|
+
SH
|
|
2639
|
+
chmod +x "$fakeroot/bin/fm-guard.sh"
|
|
2640
|
+
printf 'domain\n' > "$subhome/.fm-secondmate-home"
|
|
2641
|
+
cat > "$home/state/domain.meta" <<EOF
|
|
2642
|
+
window=firstmate:fm-domain
|
|
2643
|
+
worktree=$subhome
|
|
2644
|
+
project=$subhome
|
|
2645
|
+
harness=echo
|
|
2646
|
+
kind=secondmate
|
|
2647
|
+
mode=secondmate
|
|
2648
|
+
yolo=off
|
|
2649
|
+
home=$subhome
|
|
2650
|
+
projects=alpha
|
|
2651
|
+
EOF
|
|
2652
|
+
printf '%s\n' '- domain - design domain (home: '"$subhome"'; scope: design domain; projects: alpha; added 2026-06-22)' > "$home/data/secondmates.md"
|
|
2653
|
+
cat > "$subhome/state/child.meta" <<EOF
|
|
2654
|
+
window=firstmate:fm-child
|
|
2655
|
+
worktree=$childwt
|
|
2656
|
+
project=$childproj
|
|
2657
|
+
harness=echo
|
|
2658
|
+
kind=ship
|
|
2659
|
+
mode=no-mistakes
|
|
2660
|
+
yolo=off
|
|
2661
|
+
EOF
|
|
2662
|
+
fakebin=$(make_fake_tmux "$TMP_ROOT/child-repo-descendant-fake")
|
|
2663
|
+
log="$TMP_ROOT/child-repo-descendant-fake/tmux.log"
|
|
2664
|
+
if PATH="$fakebin:$PATH" FM_ROOT_OVERRIDE="$fakeroot" FM_HOME="$home" FM_FAKE_TMUX_LOG="$log" FM_FAKE_TMUX_CAPTURE="$TMP_ROOT/child-repo-descendant-fake/pane.txt" \
|
|
2665
|
+
"$ROOT/bin/fm-teardown.sh" domain --force >/dev/null 2>"$err"; then
|
|
2666
|
+
fail "force teardown removed a child worktree inside FM_ROOT"
|
|
2667
|
+
fi
|
|
2668
|
+
[ -d "$childwt" ] || fail "force teardown removed repo descendant worktree"
|
|
2669
|
+
[ -d "$subhome" ] || fail "force teardown removed subhome after repo child validation refusal"
|
|
2670
|
+
[ -e "$home/state/domain.meta" ] || fail "force teardown cleared parent meta after repo child validation refusal"
|
|
2671
|
+
[ -e "$subhome/state/child.meta" ] || fail "force teardown cleared child meta after repo child validation refusal"
|
|
2672
|
+
grep -F 'kill-window' "$log" >/dev/null && fail "force teardown killed windows before repo child validation refusal"
|
|
2673
|
+
grep -F 'inside the firstmate repo' "$err" >/dev/null || fail "force teardown did not explain repo descendant rejection"
|
|
2674
|
+
pass "force teardown refuses child worktrees inside the firstmate repo"
|
|
2675
|
+
}
|
|
2676
|
+
|
|
2677
|
+
test_secondmate_force_teardown_refuses_unregistered_child_worktree() {
|
|
2678
|
+
local home subhome childproj childwt fakebin err log
|
|
2679
|
+
home="$TMP_ROOT/unregistered-child-home"
|
|
2680
|
+
subhome="$TMP_ROOT/unregistered-child-subhome"
|
|
2681
|
+
childproj="$subhome/projects/alpha"
|
|
2682
|
+
childwt="$TMP_ROOT/unregistered-child-worktree"
|
|
2683
|
+
err="$TMP_ROOT/unregistered-child.err"
|
|
2684
|
+
mkdir -p "$home/state" "$home/data" "$subhome/state" "$childproj" "$childwt"
|
|
2685
|
+
printf 'domain\n' > "$subhome/.fm-secondmate-home"
|
|
2686
|
+
cat > "$home/state/domain.meta" <<EOF
|
|
2687
|
+
window=firstmate:fm-domain
|
|
2688
|
+
worktree=$subhome
|
|
2689
|
+
project=$subhome
|
|
2690
|
+
harness=echo
|
|
2691
|
+
kind=secondmate
|
|
2692
|
+
mode=secondmate
|
|
2693
|
+
yolo=off
|
|
2694
|
+
home=$subhome
|
|
2695
|
+
projects=alpha
|
|
2696
|
+
EOF
|
|
2697
|
+
printf '%s\n' '- domain - design domain (home: '"$subhome"'; scope: design domain; projects: alpha; added 2026-06-22)' > "$home/data/secondmates.md"
|
|
2698
|
+
cat > "$subhome/state/child.meta" <<EOF
|
|
2699
|
+
window=firstmate:fm-child
|
|
2700
|
+
worktree=$childwt
|
|
2701
|
+
project=$childproj
|
|
2702
|
+
harness=echo
|
|
2703
|
+
kind=ship
|
|
2704
|
+
mode=no-mistakes
|
|
2705
|
+
yolo=off
|
|
2706
|
+
EOF
|
|
2707
|
+
fakebin=$(make_fake_tmux "$TMP_ROOT/unregistered-child-fake")
|
|
2708
|
+
log="$TMP_ROOT/unregistered-child-fake/tmux.log"
|
|
2709
|
+
if PATH="$fakebin:$PATH" FM_HOME="$home" FM_FAKE_TMUX_LOG="$log" FM_FAKE_TMUX_CAPTURE="$TMP_ROOT/unregistered-child-fake/pane.txt" \
|
|
2710
|
+
"$ROOT/bin/fm-teardown.sh" domain --force >/dev/null 2>"$err"; then
|
|
2711
|
+
fail "force teardown removed an unregistered child worktree"
|
|
2712
|
+
fi
|
|
2713
|
+
[ -d "$childwt" ] || fail "force teardown removed unregistered child worktree"
|
|
2714
|
+
[ -d "$subhome" ] || fail "force teardown removed subhome after unregistered child refusal"
|
|
2715
|
+
[ -e "$home/state/domain.meta" ] || fail "force teardown cleared parent meta after unregistered child refusal"
|
|
2716
|
+
[ -e "$subhome/state/child.meta" ] || fail "force teardown cleared child meta after unregistered child refusal"
|
|
2717
|
+
grep -F 'kill-window' "$log" >/dev/null && fail "force teardown killed windows before unregistered child refusal"
|
|
2718
|
+
grep -F 'is not a git worktree for' "$err" >/dev/null || fail "force teardown did not explain unregistered child rejection"
|
|
2719
|
+
pass "force teardown refuses unregistered child worktree paths"
|
|
2720
|
+
}
|
|
2721
|
+
|
|
2722
|
+
test_secondmate_idle_pane_is_not_stale() {
|
|
2723
|
+
local home fakebin out pid window
|
|
2724
|
+
home="$TMP_ROOT/watch-home"
|
|
2725
|
+
mkdir -p "$home/state"
|
|
2726
|
+
window="firstmate:fm-domain"
|
|
2727
|
+
cat > "$home/state/domain.meta" <<EOF
|
|
2728
|
+
window=$window
|
|
2729
|
+
worktree=$TMP_ROOT/watch-subhome
|
|
2730
|
+
project=$TMP_ROOT/watch-subhome
|
|
2731
|
+
harness=echo
|
|
2732
|
+
kind=secondmate
|
|
2733
|
+
home=$TMP_ROOT/watch-subhome
|
|
2734
|
+
projects=alpha
|
|
2735
|
+
EOF
|
|
2736
|
+
fakebin=$(make_fake_tmux "$TMP_ROOT/watch-fake")
|
|
2737
|
+
out="$TMP_ROOT/watch-fake/watch.out"
|
|
2738
|
+
PATH="$fakebin:$PATH" FM_HOME="$home" FM_FAKE_TMUX_WINDOW="$window" FM_FAKE_TMUX_LOG="$TMP_ROOT/watch-fake/tmux.log" FM_FAKE_TMUX_CAPTURE="$TMP_ROOT/watch-fake/pane.txt" \
|
|
2739
|
+
FM_POLL=1 FM_SIGNAL_GRACE=1 FM_CHECK_INTERVAL=999999 FM_HEARTBEAT=999999 "$ROOT/bin/fm-watch.sh" > "$out" &
|
|
2740
|
+
pid=$!
|
|
2741
|
+
if ! wait_live "$pid" 25; then
|
|
2742
|
+
wait "$pid" || true
|
|
2743
|
+
grep -F "stale: $window" "$out" >/dev/null && fail "idle secondmate pane triggered stale wake"
|
|
2744
|
+
fail "watcher exited unexpectedly while supervising idle secondmate"
|
|
2745
|
+
fi
|
|
2746
|
+
kill "$pid" 2>/dev/null || true
|
|
2747
|
+
wait "$pid" 2>/dev/null || true
|
|
2748
|
+
grep -F "stale: $window" "$out" >/dev/null && fail "idle secondmate pane triggered stale wake"
|
|
2749
|
+
pass "idle kind=secondmate pane is healthy and not stale"
|
|
2750
|
+
}
|
|
2751
|
+
|
|
2752
|
+
test_secondmate_charter_brief_is_idle_by_default() {
|
|
2753
|
+
local home brief
|
|
2754
|
+
home="$TMP_ROOT/idle-charter-home"
|
|
2755
|
+
mkdir -p "$home/data" "$home/state"
|
|
2756
|
+
scaffold_secondmate_charter "$home" idle-sm 'feature work for alpha' alpha
|
|
2757
|
+
brief="$home/data/idle-sm/brief.md"
|
|
2758
|
+
[ -f "$brief" ] || fail "secondmate charter brief was not scaffolded"
|
|
2759
|
+
# Idle contract: waits for routed work, never self-initiates.
|
|
2760
|
+
grep -F 'go idle and wait silently for the main firstmate' "$brief" >/dev/null \
|
|
2761
|
+
|| fail "charter brief does not tell the secondmate to go idle and wait for routed work"
|
|
2762
|
+
grep -F 'Act only on tasks the main firstmate routes to you' "$brief" >/dev/null \
|
|
2763
|
+
|| fail "charter brief does not restrict work to routed tasks"
|
|
2764
|
+
grep -F 'never spawn a survey, audit, or any self-directed' "$brief" >/dev/null \
|
|
2765
|
+
|| fail "charter brief does not forbid self-initiated survey/audit work"
|
|
2766
|
+
# Reconcile-on-startup must remain: bootstrap and recovery still run, scoped to own work.
|
|
2767
|
+
grep -F 'run normal firstmate bootstrap and recovery' "$brief" >/dev/null \
|
|
2768
|
+
|| fail "charter brief dropped the bootstrap/recovery reconciliation step"
|
|
2769
|
+
grep -F 'only to RECONCILE work that is already yours' "$brief" >/dev/null \
|
|
2770
|
+
|| fail "charter brief does not scope startup work to reconciling existing work"
|
|
2771
|
+
# Regression guard: the over-broad phrasing that got misread as "go find work" is gone.
|
|
2772
|
+
if grep -F 'then supervise work that matches your scope' "$brief" >/dev/null; then
|
|
2773
|
+
fail "charter brief still uses the over-broad 'supervise work that matches your scope' phrasing"
|
|
2774
|
+
fi
|
|
2775
|
+
pass "secondmate charter brief is idle by default and does not self-initiate work"
|
|
2776
|
+
}
|
|
2777
|
+
|
|
2778
|
+
test_backlog_handoff_aborts_safely() {
|
|
2779
|
+
# The happy move (verbatim into the Queued section, out-of-scope left alone,
|
|
2780
|
+
# idempotent re-run) is asserted in the lifecycle e2e. Here: every refusal path
|
|
2781
|
+
# aborts atomically and mutates neither backlog.
|
|
2782
|
+
local home subhome subhome_abs before
|
|
2783
|
+
home="$TMP_ROOT/handoff-main"
|
|
2784
|
+
subhome="$TMP_ROOT/handoff-sub"
|
|
2785
|
+
mkdir -p "$home/data" "$home/state"
|
|
2786
|
+
seed_secondmate_home_marker "$subhome" design
|
|
2787
|
+
subhome_abs=$(cd "$subhome" && pwd -P)
|
|
2788
|
+
printf -- '- design - feature work (home: %s; scope: feature work; projects: alpha; added 2026-06-22)\n' "$subhome_abs" > "$home/data/secondmates.md"
|
|
2789
|
+
cat > "$home/data/backlog.md" <<'EOF'
|
|
2790
|
+
## In flight
|
|
2791
|
+
- [ ] live-task - active work (repo: alpha, since 2026-06-20)
|
|
2792
|
+
|
|
2793
|
+
## Queued
|
|
2794
|
+
- [ ] bug-z - fix bug z (repo: gamma)
|
|
2795
|
+
|
|
2796
|
+
## Done
|
|
2797
|
+
- [x] old-task - shipped thing - local main (merged 2026-06-19)
|
|
2798
|
+
EOF
|
|
2799
|
+
|
|
2800
|
+
# A key matching neither backlog aborts atomically: nothing moves.
|
|
2801
|
+
before=$(cat "$home/data/backlog.md")
|
|
2802
|
+
if FM_HOME="$home" "$ROOT/bin/fm-backlog-handoff.sh" design bug-z no-such-key >/dev/null 2>&1; then
|
|
2803
|
+
fail "handoff succeeded despite an unmatched key"
|
|
2804
|
+
fi
|
|
2805
|
+
[ "$before" = "$(cat "$home/data/backlog.md")" ] || fail "handoff with an unmatched key still mutated the main backlog"
|
|
2806
|
+
grep -F 'bug-z' "$home/data/backlog.md" >/dev/null || fail "atomic abort lost the valid bug-z item"
|
|
2807
|
+
|
|
2808
|
+
# An in-flight item is refused (active ownership lives in tmux + state too).
|
|
2809
|
+
before=$(cat "$home/data/backlog.md")
|
|
2810
|
+
if FM_HOME="$home" "$ROOT/bin/fm-backlog-handoff.sh" design live-task >/dev/null 2>&1; then
|
|
2811
|
+
fail "handoff accepted an in-flight backlog item"
|
|
2812
|
+
fi
|
|
2813
|
+
[ "$before" = "$(cat "$home/data/backlog.md")" ] || fail "handoff with an in-flight key mutated the main backlog"
|
|
2814
|
+
grep -F 'live-task' "$home/data/backlog.md" >/dev/null || fail "in-flight refusal lost the live task"
|
|
2815
|
+
[ ! -e "$subhome/data/backlog.md" ] || ! grep -F 'live-task' "$subhome/data/backlog.md" >/dev/null || fail "in-flight refusal copied the live task into the secondmate backlog"
|
|
2816
|
+
|
|
2817
|
+
# An unregistered secondmate id is refused.
|
|
2818
|
+
if FM_HOME="$home" "$ROOT/bin/fm-backlog-handoff.sh" ghost bug-z >/dev/null 2>&1; then
|
|
2819
|
+
fail "handoff accepted an unregistered secondmate id"
|
|
2820
|
+
fi
|
|
2821
|
+
pass "fm-backlog-handoff aborts atomically on unmatched, in-flight, and unregistered targets"
|
|
2822
|
+
}
|
|
2823
|
+
|
|
2824
|
+
test_backlog_handoff_refuses_done_items_and_non_secondmate_homes() {
|
|
2825
|
+
local home subhome subhome_abs projhome projhome_abs markerhome markerhome_abs symlinkhome symlinkhome_abs outside before_main before_sub out
|
|
2826
|
+
home="$TMP_ROOT/handoff-safety-main"
|
|
2827
|
+
subhome="$TMP_ROOT/handoff-safety-sub"
|
|
2828
|
+
projhome="$TMP_ROOT/handoff-safety-proj"
|
|
2829
|
+
markerhome="$TMP_ROOT/handoff-safety-marker"
|
|
2830
|
+
symlinkhome="$TMP_ROOT/handoff-safety-symlink"
|
|
2831
|
+
outside="$TMP_ROOT/handoff-safety-outside"
|
|
2832
|
+
mkdir -p "$home/data" "$home/state"
|
|
2833
|
+
|
|
2834
|
+
seed_secondmate_home_marker "$subhome" archive
|
|
2835
|
+
subhome_abs=$(cd "$subhome" && pwd -P)
|
|
2836
|
+
printf '## Queued\n- [ ] keep-me - stays (repo: alpha)\n' > "$subhome/data/backlog.md"
|
|
2837
|
+
printf -- '- archive - archival (home: %s; scope: archival; projects: alpha; added 2026-06-22)\n' "$subhome_abs" > "$home/data/secondmates.md"
|
|
2838
|
+
printf '##\tDone\n- [x] shipped-task - shipped thing - local main (merged 2026-06-19)\n' > "$home/data/backlog.md"
|
|
2839
|
+
before_main="$TMP_ROOT/handoff-safety-main.before"
|
|
2840
|
+
before_sub="$TMP_ROOT/handoff-safety-sub.before"
|
|
2841
|
+
cp "$home/data/backlog.md" "$before_main"
|
|
2842
|
+
cp "$subhome/data/backlog.md" "$before_sub"
|
|
2843
|
+
if out=$(FM_HOME="$home" "$ROOT/bin/fm-backlog-handoff.sh" archive shipped-task 2>&1); then
|
|
2844
|
+
fail "handoff accepted a Done backlog item"
|
|
2845
|
+
fi
|
|
2846
|
+
printf '%s\n' "$out" | grep -F 'shipped-task' >/dev/null \
|
|
2847
|
+
|| fail "Done-item refusal did not name the selected item"
|
|
2848
|
+
printf '%s\n' "$out" | grep -F 'queued work only' >/dev/null \
|
|
2849
|
+
|| fail "Done-item refusal did not state the queued-only contract"
|
|
2850
|
+
cmp -s "$before_main" "$home/data/backlog.md" \
|
|
2851
|
+
|| fail "Done-item refusal mutated the main backlog"
|
|
2852
|
+
cmp -s "$before_sub" "$subhome/data/backlog.md" \
|
|
2853
|
+
|| fail "Done-item refusal mutated the secondmate backlog"
|
|
2854
|
+
|
|
2855
|
+
# A registered home that is not a seeded secondmate home (e.g. a project clone)
|
|
2856
|
+
# is refused, and nothing is written into it.
|
|
2857
|
+
fm_git_init_commit "$projhome"
|
|
2858
|
+
projhome_abs=$(cd "$projhome" && pwd -P)
|
|
2859
|
+
printf -- '- proj-sm - bogus (home: %s; scope: bogus; projects: alpha; added 2026-06-22)\n' "$projhome_abs" >> "$home/data/secondmates.md"
|
|
2860
|
+
if FM_HOME="$home" "$ROOT/bin/fm-backlog-handoff.sh" proj-sm shipped-task >/dev/null 2>&1; then
|
|
2861
|
+
fail "handoff wrote into a destination that is not a seeded secondmate home"
|
|
2862
|
+
fi
|
|
2863
|
+
[ ! -e "$projhome/data/backlog.md" ] || fail "handoff created a backlog inside a non-secondmate home"
|
|
2864
|
+
|
|
2865
|
+
mkdir -p "$markerhome/data"
|
|
2866
|
+
markerhome_abs=$(cd "$markerhome" && pwd -P)
|
|
2867
|
+
printf 'marker-sm\n' > "$markerhome/.fm-secondmate-home"
|
|
2868
|
+
printf -- '- marker-sm - bogus (home: %s; scope: bogus; projects: alpha; added 2026-06-22)\n' "$markerhome_abs" >> "$home/data/secondmates.md"
|
|
2869
|
+
cat > "$home/data/backlog.md" <<'EOF'
|
|
2870
|
+
## Queued
|
|
2871
|
+
- [ ] marker-task - should not move (repo: alpha)
|
|
2872
|
+
EOF
|
|
2873
|
+
if FM_HOME="$home" "$ROOT/bin/fm-backlog-handoff.sh" marker-sm marker-task >/dev/null 2>&1; then
|
|
2874
|
+
fail "handoff accepted a marker-only directory as a secondmate home"
|
|
2875
|
+
fi
|
|
2876
|
+
[ ! -e "$markerhome/data/backlog.md" ] || fail "handoff wrote into a marker-only directory"
|
|
2877
|
+
grep -F 'marker-task' "$home/data/backlog.md" >/dev/null || fail "marker-only refusal lost the main backlog item"
|
|
2878
|
+
|
|
2879
|
+
seed_secondmate_home_marker "$symlinkhome" symlink-sm
|
|
2880
|
+
symlinkhome_abs=$(cd "$symlinkhome" && pwd -P)
|
|
2881
|
+
mkdir -p "$outside"
|
|
2882
|
+
rm -rf "$symlinkhome/data"
|
|
2883
|
+
ln -s "$outside" "$symlinkhome/data"
|
|
2884
|
+
printf -- '- symlink-sm - bogus (home: %s; scope: bogus; projects: alpha; added 2026-06-22)\n' "$symlinkhome_abs" >> "$home/data/secondmates.md"
|
|
2885
|
+
cat > "$home/data/backlog.md" <<'EOF'
|
|
2886
|
+
## Queued
|
|
2887
|
+
- [ ] symlink-task - should not move (repo: alpha)
|
|
2888
|
+
EOF
|
|
2889
|
+
if FM_HOME="$home" "$ROOT/bin/fm-backlog-handoff.sh" symlink-sm symlink-task >/dev/null 2>&1; then
|
|
2890
|
+
fail "handoff accepted a secondmate home with data outside the home"
|
|
2891
|
+
fi
|
|
2892
|
+
[ ! -e "$outside/backlog.md" ] || fail "handoff wrote through a symlinked secondmate data directory"
|
|
2893
|
+
grep -F 'symlink-task' "$home/data/backlog.md" >/dev/null || fail "symlink refusal lost the main backlog item"
|
|
2894
|
+
pass "fm-backlog-handoff refuses Done items under whitespace section headings and unsafe homes"
|
|
2895
|
+
}
|
|
2896
|
+
|
|
2897
|
+
test_fm_home_parameterization
|
|
2898
|
+
test_lock_status_is_per_home
|
|
2899
|
+
test_seed_allows_overlapping_clones_and_drops_owner
|
|
2900
|
+
test_home_seed_validate_rejects_unparseable_registry_entry
|
|
2901
|
+
test_home_seed_refuses_broken_registry_symlink
|
|
2902
|
+
test_home_seed_refuses_unreadable_registry
|
|
2903
|
+
test_home_seed_validate_rejects_duplicate_homes
|
|
2904
|
+
test_home_seed_validate_rejects_duplicate_ids
|
|
2905
|
+
test_home_seed_validate_rejects_nested_homes
|
|
2906
|
+
test_home_seed_uses_treehouse_acquired_home
|
|
2907
|
+
test_home_seed_returns_treehouse_acquired_home_on_assignment_failure
|
|
2908
|
+
test_home_seed_warns_when_acquired_home_return_fails
|
|
2909
|
+
test_home_seed_does_not_return_unsafe_acquired_home
|
|
2910
|
+
test_home_seed_rolls_back_failed_clone
|
|
2911
|
+
test_home_seed_refuses_missing_filled_charter
|
|
2912
|
+
test_home_seed_refuses_placeholder_charter
|
|
2913
|
+
test_home_seed_refuses_empty_charter_fields
|
|
2914
|
+
test_home_seed_no_projects_end_to_end
|
|
2915
|
+
test_secondmate_spawn_resolves_punctuated_registry_projects
|
|
2916
|
+
test_secondmate_spawn_refuses_ambiguous_and_mismatched_registry_bindings
|
|
2917
|
+
test_home_seed_refuses_projectful_reused_charter_for_projectless_home
|
|
2918
|
+
test_home_seed_refuses_projectless_conversion_of_populated_home
|
|
2919
|
+
test_home_seed_refuses_projectless_home_with_uninspectable_projects
|
|
2920
|
+
test_home_seed_refuses_projectless_home_with_symlinked_projects
|
|
2921
|
+
test_home_seed_refuses_projectless_home_with_non_directory_projects
|
|
2922
|
+
test_home_seed_refuses_projectless_home_with_uninspectable_registry
|
|
2923
|
+
test_home_seed_refuses_missing_projects_without_signal
|
|
2924
|
+
test_home_seed_refuses_local_only_project
|
|
2925
|
+
test_home_seed_refuses_registry_delimiter_home
|
|
2926
|
+
test_home_seed_refuses_active_home_and_root
|
|
2927
|
+
test_home_seed_refuses_home_marked_for_another_id
|
|
2928
|
+
test_home_seed_refuses_home_registered_to_another_id
|
|
2929
|
+
test_home_seed_refuses_reassigning_existing_id_to_different_home
|
|
2930
|
+
test_home_seed_refuses_home_overlapping_registered_home
|
|
2931
|
+
test_home_seed_refuses_remote_backed_project_without_origin
|
|
2932
|
+
test_home_seed_refuses_existing_remote_backed_project_with_wrong_origin
|
|
2933
|
+
test_home_seed_resolves_relative_source_origins
|
|
2934
|
+
test_home_seed_skips_initialized_existing_no_mistakes_projects
|
|
2935
|
+
test_home_seed_refuses_uninitialized_existing_no_mistakes_project
|
|
2936
|
+
test_home_seed_refuses_project_destinations_outside_subhome
|
|
2937
|
+
test_home_seed_refuses_operational_dirs_outside_subhome
|
|
2938
|
+
test_home_seed_refuses_unsafe_leaf_files
|
|
2939
|
+
test_home_seed_preserves_existing_parent_binding
|
|
2940
|
+
test_secondmate_spawn_requires_seeded_matching_home
|
|
2941
|
+
test_secondmate_spawn_refuses_operational_dirs_outside_subhome
|
|
2942
|
+
test_fm_send_refuses_bare_window_without_home_meta
|
|
2943
|
+
test_secondmate_teardown_retires_empty_home
|
|
2944
|
+
test_secondmate_teardown_refuses_ambiguous_and_mismatched_registry_bindings
|
|
2945
|
+
test_secondmate_teardown_sweeps_process_events_before_removal
|
|
2946
|
+
test_secondmate_teardown_refuses_process_events_without_sweep_script
|
|
2947
|
+
test_secondmate_teardown_preserves_process_events_on_later_refusal
|
|
2948
|
+
test_secondmate_force_teardown_sweeps_nested_homes
|
|
2949
|
+
test_secondmate_force_teardown_preserves_nested_restore_status
|
|
2950
|
+
test_secondmate_teardown_refuses_failed_leased_home_return
|
|
2951
|
+
test_secondmate_teardown_removes_plain_clone_home_without_treehouse_return
|
|
2952
|
+
test_secondmate_force_teardown_discards_child_work
|
|
2953
|
+
test_secondmate_force_teardown_preserves_child_on_unproven_lock
|
|
2954
|
+
test_secondmate_force_teardown_allows_non_state_operational_dir_symlinks_inside_home
|
|
2955
|
+
test_secondmate_force_teardown_refuses_operational_dir_symlink_outside_home
|
|
2956
|
+
test_secondmate_teardown_refuses_registered_nested_home
|
|
2957
|
+
test_secondmate_teardown_refuses_child_registry_nested_home
|
|
2958
|
+
test_secondmate_force_teardown_prevalidates_before_child_cleanup
|
|
2959
|
+
test_force_teardown_refuses_non_directory_descendant_state
|
|
2960
|
+
test_force_teardown_refuses_symlinked_descendant_state
|
|
2961
|
+
test_force_teardown_locks_descendant_with_absent_state
|
|
2962
|
+
test_force_teardown_refuses_while_a_task_is_being_published
|
|
2963
|
+
test_fresh_spawn_refuses_while_a_forced_teardown_owns_the_task_set
|
|
2964
|
+
test_fresh_remote_secondmate_spawn_refuses_while_task_set_is_owned
|
|
2965
|
+
test_secondmate_force_teardown_refuses_child_active_home_descendant
|
|
2966
|
+
test_secondmate_force_teardown_refuses_child_repo_descendant
|
|
2967
|
+
test_secondmate_force_teardown_refuses_unregistered_child_worktree
|
|
2968
|
+
test_secondmate_teardown_path_boundary_matrix
|
|
2969
|
+
test_secondmate_idle_pane_is_not_stale
|
|
2970
|
+
test_secondmate_charter_brief_is_idle_by_default
|
|
2971
|
+
test_backlog_handoff_aborts_safely
|
|
2972
|
+
test_backlog_handoff_refuses_done_items_and_non_secondmate_homes
|