@zerwiz/ymir 0.1.4 → 0.1.7
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/README.md +74 -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 +360 -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.example +72 -0
- package/.agents/config/crew-dispatch.json +22 -0
- package/.agents/config/cron.yaml +11 -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/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/runes_audit.md +5 -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 +121 -0
- package/.agents/migrations/0004-hoard-and-realms.sh +104 -0
- package/.agents/migrations/0005-roots-out-of-tree.sh +158 -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 +159 -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 +736 -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 +539 -0
- package/.agents/skills/galdr-ymirsystem/assets/installation.md +763 -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 +163 -0
- package/.agents/skills/galdr-ymirsystem/assets/nornir-jobs.md +415 -0
- package/.agents/skills/galdr-ymirsystem/assets/norse-naming.md +325 -0
- package/.agents/skills/galdr-ymirsystem/assets/odrerir-hall.md +133 -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 +101 -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 +180 -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 +503 -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 +90 -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 +57 -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 +35 -0
- package/.agents/skills/ymir-host/assets/desktop.md +36 -0
- package/.agents/skills/ymir-host/assets/install.md +69 -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 +498 -0
- package/CHANGELOG.md +1913 -0
- package/LICENSE +1 -1
- package/NOTICE +63 -0
- package/README.md +114 -60
- 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 +97 -0
- package/RULES/05-platforms.md +67 -0
- package/RULES/06-append-only.md +108 -0
- package/RULES/07-config.md +101 -0
- package/RULES/08-delivery-gate.md +75 -0
- package/RULES/09-electron.md +58 -0
- package/RULES/README.md +22 -0
- package/bin/README.md +107 -0
- package/bin/a2a-mcp.sh +133 -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 +256 -0
- package/bin/apodex-smoke-test.sh +105 -0
- package/bin/bifrost-bridge.sh +131 -0
- package/bin/bifrost-ingress.sh +77 -0
- package/bin/bootstrap-macos.sh +112 -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 +51 -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-assemble.sh +154 -0
- package/bin/changelog-guard.sh +197 -0
- package/bin/crash-sense.sh +173 -0
- package/bin/design-check.sh +80 -0
- package/bin/design-icon.sh +153 -0
- package/bin/desktop-place.sh +224 -0
- package/bin/docs-guard.sh +39 -0
- package/bin/editor-place.sh +225 -0
- package/bin/eindri-acclaim.sh +47 -0
- package/bin/eindri-control.sh +28 -0
- package/bin/eindri-role.sh +83 -0
- package/bin/eindri-seat.sh +83 -0
- package/bin/eindri-seen.sh +54 -0
- package/bin/eindri-send.sh +15 -0
- package/bin/eindri-start.sh +98 -0
- package/bin/eindri-watch.sh +57 -0
- package/bin/einherjar-spawn.sh +576 -0
- package/bin/eir-doctor.sh +158 -0
- package/bin/erindi-brief.sh +271 -0
- package/bin/feature-inventory.sh +73 -0
- package/bin/fleet-apply.sh +81 -0
- package/bin/github-deploy.sh +94 -0
- package/bin/gjallarhorn-expose.sh +136 -0
- package/bin/gjallarhorn-notify.sh +71 -0
- package/bin/gjallarhorn-purge.sh +67 -0
- package/bin/gjallarhorn-tunnel.sh +85 -0
- package/bin/gleipnir-lock-lib.sh +284 -0
- package/bin/groa-update.sh +130 -0
- package/bin/hall-snapshot.sh +111 -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 +452 -0
- package/bin/hermes-ensure.sh +88 -0
- package/bin/hlidskjalf-agents.sh +172 -0
- package/bin/hlidskjalf-usage.sh +196 -0
- package/bin/hoard-guard.sh +135 -0
- package/bin/hoard-lib.sh +105 -0
- package/bin/hodd.sh +102 -0
- package/bin/host-sense.sh +199 -0
- package/bin/huginn-research-worker.sh +121 -0
- package/bin/local-model-lock.sh +70 -0
- package/bin/mimir-bridge.py +259 -0
- package/bin/mimir-bridge.sh +100 -0
- package/bin/mimir-ingest.sh +131 -0
- package/bin/mimir-reflect.sh +27 -0
- package/bin/mimir.sh +113 -0
- package/bin/mjollnir-webhook.sh +72 -0
- package/bin/mjollnir.sh +104 -0
- package/bin/model-hardware.sh +96 -0
- package/bin/model-resolve.sh +160 -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-hall-snapshot.sh +28 -0
- package/bin/nornir-job-memory-housekeeping.sh +114 -0
- package/bin/nornir-job-nsr-compliance.sh +54 -0
- package/bin/nornir-job-observer.sh +166 -0
- package/bin/nornir-job-skillopt-sleep.sh +76 -0
- package/bin/npm-publish.sh +136 -0
- package/bin/omarchy-hook-install.sh +60 -0
- package/bin/omarchy-install.sh +161 -0
- package/bin/omarchy-plugins.sh +187 -0
- package/bin/omarchy-sense.sh +162 -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-model-agnostic.sh +71 -0
- package/bin/pi-seat.sh +63 -0
- package/bin/prereq-ensure.sh +166 -0
- package/bin/private-guard.sh +58 -0
- package/bin/project-git.sh +69 -0
- package/bin/public-guard.sh +71 -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 +104 -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 +94 -0
- package/bin/smidja-observe.sh +86 -0
- package/bin/syn-arm-pretool-check.sh +57 -0
- package/bin/syn-asset-pretool-check.sh +89 -0
- package/bin/syn-cd-pretool-check.sh +28 -0
- package/bin/syn-guard-pretool-check.sh +83 -0
- package/bin/syn-spawn-pretool-check.sh +58 -0
- package/bin/syn-turnend-guard.sh +36 -0
- package/bin/syn-watch-arm.sh +99 -0
- package/bin/tailscale-sync.sh +203 -0
- package/bin/telegram-bot.sh +82 -0
- package/bin/toolchain.sh +74 -0
- package/bin/ui-truth-check.sh +75 -0
- package/bin/utgard.sh +111 -0
- package/bin/valhalla.sh +115 -0
- package/bin/valknut-load.sh +298 -0
- package/bin/vor-crew-state.sh +124 -0
- package/bin/wedge-notify.sh +60 -0
- package/bin/workspace-provision.sh +80 -0
- package/bin/workspace-rag.sh +163 -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 +806 -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-plan.sh +356 -0
- package/bin/ymir-platform.sh +348 -0
- package/bin/ymir-say.sh +110 -0
- package/bin/ymir-setup-auth.sh +119 -0
- package/bin/ymir-validate.sh +193 -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 +1060 -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 +54 -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 +269 -0
- package/scripts/lower.sh +34 -0
- package/scripts/raise.sh +42 -0
- package/scripts/start.sh +217 -0
- package/scripts/stop.sh +113 -0
- package/install.sh +0 -35
|
@@ -0,0 +1,2600 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Tests for the secondmate-vs-crewmate harness split, the optional model/effort
|
|
3
|
+
# tokens config/secondmate-harness carries alongside the harness, and the
|
|
4
|
+
# primary->secondmate inherited local-material propagation.
|
|
5
|
+
#
|
|
6
|
+
# Three capabilities are under test:
|
|
7
|
+
# A) Harness split. config/secondmate-harness sets the harness the PRIMARY uses
|
|
8
|
+
# to launch SECONDMATE agents, independent of config/crew-harness (the
|
|
9
|
+
# crewmate harness). fm-harness.sh secondmate resolves the fallback chain
|
|
10
|
+
# config/secondmate-harness -> config/crew-harness -> own; an absent or
|
|
11
|
+
# "default" secondmate-harness behaves exactly as the crew harness did before
|
|
12
|
+
# this knob existed (full backward-compat). fm-spawn.sh resolves a secondmate
|
|
13
|
+
# launch through that mode, durably (every respawn re-resolves), while an
|
|
14
|
+
# explicit per-spawn harness arg still wins.
|
|
15
|
+
# B) Inheritance. The primary pushes a declared, extensible set of LOCAL
|
|
16
|
+
# (gitignored) config items - config/crew-dispatch.json, config/crew-harness,
|
|
17
|
+
# config/backlog-backend, config/backend, config/herdr-presentation-spaces,
|
|
18
|
+
# config/startup-memory-budget, and config/trace-context -
|
|
19
|
+
# down into each secondmate home's config/, so the secondmate's OWN crewmates,
|
|
20
|
+
# dispatch profiles, backlog backend, runtime-backend default, Herdr
|
|
21
|
+
# presentation choice, startup-memory budget, and trace context inherit the
|
|
22
|
+
# primary's settings. For config/herdr-presentation-spaces, an absent
|
|
23
|
+
# primary file and an absent destination file both mean the same
|
|
24
|
+
# unconfigured default, so the generic absence mirror converges that item
|
|
25
|
+
# without deciding its release-dependent floor.
|
|
26
|
+
# It is primary-authoritative
|
|
27
|
+
# (re-pushed at secondmate spawn, on the bootstrap secondmate sweep, and by
|
|
28
|
+
# config push).
|
|
29
|
+
# config/secondmate-harness is deliberately NOT inherited (secondmates do
|
|
30
|
+
# not spawn secondmates). After a successful push that changes allowlisted
|
|
31
|
+
# config under an already-running home, a literal-content reread instruction
|
|
32
|
+
# is written to the secondmate home and only its pointer is sent via the
|
|
33
|
+
# routed secondmate path (exact destination bytes, no summaries); unchanged
|
|
34
|
+
# config sends nothing unless a previous send failure is pending.
|
|
35
|
+
|
|
36
|
+
# C) Model/effort pin. config/secondmate-harness may carry optional model and
|
|
37
|
+
# effort tokens after the harness ("<harness> [<model>] [<effort>]"), read by
|
|
38
|
+
# fm-harness.sh secondmate-model / secondmate-effort. A bare harness-only
|
|
39
|
+
# line (today's format) yields empty model/effort - full backward-compat.
|
|
40
|
+
# fm-spawn.sh populates MODEL/EFFORT from those tokens for a --secondmate
|
|
41
|
+
# spawn only when the harness also resolves from that file, so the pin is
|
|
42
|
+
# durable across every respawn while explicit per-spawn harness/model/effort
|
|
43
|
+
# flags still win.
|
|
44
|
+
set -u
|
|
45
|
+
|
|
46
|
+
# shellcheck source=tests/lib.sh
|
|
47
|
+
. "$(dirname "${BASH_SOURCE[0]}")/lib.sh"
|
|
48
|
+
# shellcheck source=/dev/null
|
|
49
|
+
. "$ROOT/bin/fm-ff-lib.sh"
|
|
50
|
+
# shellcheck source=/dev/null
|
|
51
|
+
. "$ROOT/bin/fm-config-inherit-lib.sh"
|
|
52
|
+
|
|
53
|
+
# The harness-detection cases below fake `ps` so process ancestry is fully
|
|
54
|
+
# controlled, but bin/fm-harness.sh checks verified ENV markers before ancestry.
|
|
55
|
+
# A suite run from inside one of those harnesses inherits its marker, and the
|
|
56
|
+
# highest-precedence one wins over everything these cases set up: with an
|
|
57
|
+
# ambient CLAUDECODE=1, the pi-signed ancestry case resolves "claude". Drop the
|
|
58
|
+
# ambient markers so what this suite asserts does not depend on which harness it
|
|
59
|
+
# was launched from; every case states the marker it means to test.
|
|
60
|
+
unset CLAUDECODE PI_CODING_AGENT FM_PI_HARNESS GROK_AGENT CURSOR_AGENT CURSOR_INVOKED_AS
|
|
61
|
+
|
|
62
|
+
BASE_PATH=${FM_TEST_BASE_PATH:-/usr/bin:/bin:/usr/sbin:/sbin}
|
|
63
|
+
fm_git_identity fmtest fmtest@example.com
|
|
64
|
+
TMP_ROOT=$(fm_test_tmproot fm-secondmate-harness)
|
|
65
|
+
export FM_BACKEND=tmux
|
|
66
|
+
|
|
67
|
+
# ===========================================================================
|
|
68
|
+
# A) fm-harness.sh secondmate resolution + fallback (deterministic detect_own)
|
|
69
|
+
# ===========================================================================
|
|
70
|
+
# detect_own is pinned to claude via CLAUDECODE=1 so the "fall through to own"
|
|
71
|
+
# cases are reproducible. Each row sets crew-harness / secondmate-harness in a
|
|
72
|
+
# fresh config dir (a literal '-' means leave the file absent) and asserts BOTH
|
|
73
|
+
# the secondmate resolution AND that crew resolution is unchanged (backward-compat).
|
|
74
|
+
# <label>^<crew-harness>^<secondmate-harness>^<expect-secondmate>^<expect-crew>
|
|
75
|
+
test_harness_resolution() {
|
|
76
|
+
local label crew sm exp_sm exp_crew case_dir cfg got_sm got_crew n
|
|
77
|
+
n=0
|
|
78
|
+
while IFS='^' read -r label crew sm exp_sm exp_crew; do
|
|
79
|
+
[ -n "$label" ] || continue
|
|
80
|
+
n=$((n + 1))
|
|
81
|
+
case_dir="$TMP_ROOT/harness-$n"
|
|
82
|
+
cfg="$case_dir/config"
|
|
83
|
+
mkdir -p "$cfg"
|
|
84
|
+
[ "$crew" = "-" ] || printf '%s\n' "$crew" > "$cfg/crew-harness"
|
|
85
|
+
[ "$sm" = "-" ] || printf '%s\n' "$sm" > "$cfg/secondmate-harness"
|
|
86
|
+
got_sm=$(CLAUDECODE=1 FM_CONFIG_OVERRIDE="$cfg" "$ROOT/bin/fm-harness.sh" secondmate)
|
|
87
|
+
got_crew=$(CLAUDECODE=1 FM_CONFIG_OVERRIDE="$cfg" "$ROOT/bin/fm-harness.sh" crew)
|
|
88
|
+
[ "$got_sm" = "$exp_sm" ] || fail "$label: secondmate resolved '$got_sm', expected '$exp_sm'"
|
|
89
|
+
[ "$got_crew" = "$exp_crew" ] || fail "$label: crew resolved '$got_crew', expected '$exp_crew'"
|
|
90
|
+
done <<'ROWS'
|
|
91
|
+
both absent -> own (backward-compat)^-^-^claude^claude
|
|
92
|
+
crew set, secondmate absent -> crew (backward-compat)^codex^-^codex^codex
|
|
93
|
+
crew set, secondmate set -> secondmate wins, crew untouched^codex^grok^grok^codex
|
|
94
|
+
crew absent, secondmate set -> secondmate value, crew own^-^grok^grok^claude
|
|
95
|
+
signed Pi wrapper remains a distinct secondmate value^codex^pi-signed^pi-signed^codex
|
|
96
|
+
secondmate=default defers to crew^codex^default^codex^codex
|
|
97
|
+
crew=default resolves to own, secondmate follows^default^-^claude^claude
|
|
98
|
+
secondmate=default with crew absent -> own^-^default^claude^claude
|
|
99
|
+
ROWS
|
|
100
|
+
pass "A1 fm-harness.sh secondmate resolves the fallback chain; crew mode unchanged"
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
test_cursor_marker_detection() {
|
|
104
|
+
local dir fakebin got
|
|
105
|
+
dir="$TMP_ROOT/cursor-marker"
|
|
106
|
+
fakebin=$(fm_fakebin "$dir")
|
|
107
|
+
cat > "$fakebin/ps" <<'SH'
|
|
108
|
+
#!/usr/bin/env bash
|
|
109
|
+
case "$*" in
|
|
110
|
+
*'ppid='*) printf '%s\n' 1 ;;
|
|
111
|
+
*) printf '%s\n' bash ;;
|
|
112
|
+
esac
|
|
113
|
+
SH
|
|
114
|
+
chmod +x "$fakebin/ps"
|
|
115
|
+
got=$(env -u CLAUDECODE -u PI_CODING_AGENT -u GROK_AGENT \
|
|
116
|
+
PATH="$fakebin:$BASE_PATH" CURSOR_INVOKED_AS=cursor-agent "$ROOT/bin/fm-harness.sh")
|
|
117
|
+
[ "$got" = cursor ] || fail "Cursor's exact launcher marker resolved '$got', expected cursor"
|
|
118
|
+
got=$(env -u CLAUDECODE -u PI_CODING_AGENT -u GROK_AGENT \
|
|
119
|
+
PATH="$fakebin:$BASE_PATH" CURSOR_INVOKED_AS=cursor "$ROOT/bin/fm-harness.sh")
|
|
120
|
+
[ "$got" != cursor ] || fail "an inexact Cursor marker value was accepted as Cursor Agent CLI"
|
|
121
|
+
pass "fm-harness detects only Cursor Agent CLI's exact invocation marker"
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
# ===========================================================================
|
|
125
|
+
# C) fm-harness.sh secondmate-model / secondmate-effort token resolution
|
|
126
|
+
# ===========================================================================
|
|
127
|
+
# config/secondmate-harness holds "<harness> [<model>] [<effort>]" on one line.
|
|
128
|
+
# A bare harness (today's format) must yield empty model/effort - the
|
|
129
|
+
# backward-compat requirement. The file-line field uses \n for an embedded
|
|
130
|
+
# newline (expanded via printf '%b') so a row can express a multi-line file; the
|
|
131
|
+
# literal token ABSENT skips creating the file entirely.
|
|
132
|
+
# <label>^<file-line-or-ABSENT>^<expect-harness>^<expect-model>^<expect-effort>
|
|
133
|
+
test_secondmate_model_effort_tokens() {
|
|
134
|
+
local label line exp_harness exp_model exp_effort case_dir cfg got_h got_m got_e n
|
|
135
|
+
n=0
|
|
136
|
+
while IFS='^' read -r label line exp_harness exp_model exp_effort; do
|
|
137
|
+
[ -n "$label" ] || continue
|
|
138
|
+
n=$((n + 1))
|
|
139
|
+
case_dir="$TMP_ROOT/tokens-$n"
|
|
140
|
+
cfg="$case_dir/config"
|
|
141
|
+
mkdir -p "$cfg"
|
|
142
|
+
[ "$line" = ABSENT ] || printf '%b\n' "$line" > "$cfg/secondmate-harness"
|
|
143
|
+
got_h=$(CLAUDECODE=1 FM_CONFIG_OVERRIDE="$cfg" "$ROOT/bin/fm-harness.sh" secondmate)
|
|
144
|
+
got_m=$(CLAUDECODE=1 FM_CONFIG_OVERRIDE="$cfg" "$ROOT/bin/fm-harness.sh" secondmate-model)
|
|
145
|
+
got_e=$(CLAUDECODE=1 FM_CONFIG_OVERRIDE="$cfg" "$ROOT/bin/fm-harness.sh" secondmate-effort)
|
|
146
|
+
[ "$got_h" = "$exp_harness" ] || fail "$label: harness resolved '$got_h', expected '$exp_harness'"
|
|
147
|
+
[ "$got_m" = "$exp_model" ] || fail "$label: model resolved '$got_m', expected '$exp_model'"
|
|
148
|
+
[ "$got_e" = "$exp_effort" ] || fail "$label: effort resolved '$got_e', expected '$exp_effort'"
|
|
149
|
+
done <<'ROWS'
|
|
150
|
+
absent file -> own harness, empty model/effort^ABSENT^claude^^
|
|
151
|
+
bare harness only -> empty model/effort (backward-compat)^claude^claude^^
|
|
152
|
+
harness + model -> model only^claude opus^claude^opus^
|
|
153
|
+
harness + model + effort -> both^claude opus high^claude^opus^high
|
|
154
|
+
signed Pi wrapper + model + effort preserves every token^pi-signed openai-codex/gpt-5.6-sol max^pi-signed^openai-codex/gpt-5.6-sol^max
|
|
155
|
+
default harness token -> falls back to crew, empty model/effort^default^claude^^
|
|
156
|
+
extra whitespace between tokens is tolerated^grok grok-4 xhigh^grok^grok-4^xhigh
|
|
157
|
+
leading/trailing blank lines and a comment are skipped^# a comment\n\nclaude opus low\n^claude^opus^low
|
|
158
|
+
ROWS
|
|
159
|
+
pass "C1 fm-harness.sh secondmate-model/secondmate-effort resolve the optional tokens; bare harness stays empty (backward-compat)"
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
# ===========================================================================
|
|
163
|
+
# A/C) pi-signed process identity and shared Pi marker behavior
|
|
164
|
+
# ===========================================================================
|
|
165
|
+
test_pi_signed_detection_and_session_lock_identity() {
|
|
166
|
+
local dir fakebin got
|
|
167
|
+
dir="$TMP_ROOT/pi-signed-identity"
|
|
168
|
+
fakebin=$(fm_fakebin "$dir")
|
|
169
|
+
cat > "$fakebin/ps" <<'SH'
|
|
170
|
+
#!/usr/bin/env bash
|
|
171
|
+
set -u
|
|
172
|
+
field= pid=
|
|
173
|
+
while [ "$#" -gt 0 ]; do
|
|
174
|
+
case "$1" in
|
|
175
|
+
-o) field=$2; shift 2 ;;
|
|
176
|
+
-p) pid=$2; shift 2 ;;
|
|
177
|
+
*) shift ;;
|
|
178
|
+
esac
|
|
179
|
+
done
|
|
180
|
+
case "$pid:$field:${FM_TEST_SIGNED_SHAPE:-exact}" in
|
|
181
|
+
100:comm=:*) printf '%s\n' '/test/Pi.app/bin/pi' ;;
|
|
182
|
+
100:args=:*) printf '%s\n' 'Pi' ;;
|
|
183
|
+
100:ppid=:*) printf '%s\n' 200 ;;
|
|
184
|
+
200:comm=:exact) printf '%s\n' '/opt/test/bin/pi-signed' ;;
|
|
185
|
+
200:args=:exact) printf '%s\n' 'pi-signed --model test/model' ;;
|
|
186
|
+
200:comm=:helper) printf '%s\n' '/opt/test/bin/pi-signed-helper' ;;
|
|
187
|
+
200:args=:helper) printf '%s\n' 'pi-signed-helper' ;;
|
|
188
|
+
200:comm=:plain) printf '%s\n' '/bin/zsh' ;;
|
|
189
|
+
200:args=:plain) printf '%s\n' 'zsh' ;;
|
|
190
|
+
200:ppid=:*) printf '%s\n' 1 ;;
|
|
191
|
+
*:comm=:*) printf '%s\n' bash ;;
|
|
192
|
+
*:args=:*) printf '%s\n' bash ;;
|
|
193
|
+
*:ppid=:*) printf '%s\n' 100 ;;
|
|
194
|
+
esac
|
|
195
|
+
SH
|
|
196
|
+
chmod +x "$fakebin/ps"
|
|
197
|
+
|
|
198
|
+
got=$(env -u CLAUDECODE -u GROK_AGENT PATH="$fakebin:$BASE_PATH" PI_CODING_AGENT=true "$ROOT/bin/fm-harness.sh")
|
|
199
|
+
[ "$got" = pi ] || fail "unmarked shared signed-wrapper ancestry resolved '$got', expected pi"
|
|
200
|
+
got=$(env -u CLAUDECODE -u GROK_AGENT PATH="$fakebin:$BASE_PATH" PI_CODING_AGENT=true FM_PI_HARNESS=pi-signed "$ROOT/bin/fm-harness.sh")
|
|
201
|
+
[ "$got" = pi-signed ] || fail "selected signed wrapper resolved '$got', expected pi-signed"
|
|
202
|
+
got=$(env -u CLAUDECODE -u GROK_AGENT PATH="$fakebin:$BASE_PATH" PI_CODING_AGENT=true FM_PI_HARNESS=pi "$ROOT/bin/fm-harness.sh")
|
|
203
|
+
[ "$got" = pi ] || fail "selected plain Pi resolved '$got', expected pi"
|
|
204
|
+
got=$(env -u CLAUDECODE -u GROK_AGENT PATH="$fakebin:$BASE_PATH" PI_CODING_AGENT=true FM_PI_HARNESS=pi-signed-helper "$ROOT/bin/fm-harness.sh")
|
|
205
|
+
[ "$got" = pi ] || fail "inexact signed selection marker resolved '$got', expected pi"
|
|
206
|
+
got=$(env -u CLAUDECODE -u GROK_AGENT -u PI_CODING_AGENT PATH="$fakebin:$BASE_PATH" FM_PI_HARNESS=pi-signed "$ROOT/bin/fm-harness.sh")
|
|
207
|
+
[ "$got" = pi ] || fail "signed selection marker without Pi's family marker resolved '$got', expected pi"
|
|
208
|
+
got=$(env -u CLAUDECODE -u GROK_AGENT PATH="$fakebin:$BASE_PATH" PI_CODING_AGENT=true FM_TEST_SIGNED_SHAPE=plain "$ROOT/bin/fm-harness.sh")
|
|
209
|
+
[ "$got" = pi ] || fail "plain Pi marker resolved '$got', expected pi"
|
|
210
|
+
got=$(env -u CLAUDECODE -u GROK_AGENT PATH="$fakebin:$BASE_PATH" PI_CODING_AGENT=true FM_TEST_SIGNED_SHAPE=helper "$ROOT/bin/fm-harness.sh")
|
|
211
|
+
[ "$got" = pi ] || fail "unrelated pi-signed-helper ancestry resolved '$got', expected pi"
|
|
212
|
+
|
|
213
|
+
got=$(PATH="$fakebin:$BASE_PATH" bash -c \
|
|
214
|
+
'. "$0/bin/fm-session-lock-lib.sh"; fm_harness_ancestry_pid' "$ROOT")
|
|
215
|
+
[ "$got" = 100 ] || fail "session-lock ancestry selected '$got', expected the inner Pi engine pid 100"
|
|
216
|
+
PATH="$fakebin:$BASE_PATH" bash -c \
|
|
217
|
+
'. "$0/bin/fm-session-lock-lib.sh"; kill() { return 0; }; fm_harness_pid_alive 200' "$ROOT" \
|
|
218
|
+
|| fail "session-lock liveness rejected exact pi-signed holder"
|
|
219
|
+
if PATH="$fakebin:$BASE_PATH" FM_TEST_SIGNED_SHAPE=helper bash -c \
|
|
220
|
+
'. "$0/bin/fm-session-lock-lib.sh"; kill() { return 0; }; fm_harness_pid_alive 200' "$ROOT"; then
|
|
221
|
+
fail "session-lock liveness accepted unrelated pi-signed-helper"
|
|
222
|
+
fi
|
|
223
|
+
|
|
224
|
+
pass "pi-signed identity: authoritative launch selection distinguishes shared wrapper ancestry"
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
test_dash_leading_process_names_are_basename_operands() {
|
|
228
|
+
local dir fakebin got err status
|
|
229
|
+
dir="$TMP_ROOT/dash-leading-process-names"
|
|
230
|
+
fakebin=$(fm_fakebin "$dir")
|
|
231
|
+
cat > "$fakebin/ps" <<'SH'
|
|
232
|
+
#!/usr/bin/env bash
|
|
233
|
+
set -u
|
|
234
|
+
field= pid=
|
|
235
|
+
while [ "$#" -gt 0 ]; do
|
|
236
|
+
case "$1" in
|
|
237
|
+
-o) field=$2; shift 2 ;;
|
|
238
|
+
-p) pid=$2; shift 2 ;;
|
|
239
|
+
*) shift ;;
|
|
240
|
+
esac
|
|
241
|
+
done
|
|
242
|
+
case "$pid:$field" in
|
|
243
|
+
4242:comm=) printf '%s\n' '/opt/test/bin/codex' ;;
|
|
244
|
+
4242:args=) printf '%s\n' 'codex' ;;
|
|
245
|
+
4242:ppid=) printf '%s\n' 1 ;;
|
|
246
|
+
5252:comm=) printf '%s\n' '-codex' ;;
|
|
247
|
+
5252:args=) printf '%s\n' '-codex' ;;
|
|
248
|
+
5252:ppid=) printf '%s\n' 1 ;;
|
|
249
|
+
*:comm=) printf '%s\n' '-zsh' ;;
|
|
250
|
+
*:args=) printf '%s\n' '-zsh' ;;
|
|
251
|
+
*:ppid=) printf '%s\n' 4242 ;;
|
|
252
|
+
esac
|
|
253
|
+
SH
|
|
254
|
+
chmod +x "$fakebin/ps"
|
|
255
|
+
|
|
256
|
+
err="$dir/fm-harness.err"
|
|
257
|
+
got=$(env -u CLAUDECODE -u PI_CODING_AGENT -u GROK_AGENT \
|
|
258
|
+
PATH="$fakebin:$BASE_PATH" "$ROOT/bin/fm-harness.sh" 2>"$err")
|
|
259
|
+
[ "$got" = codex ] || fail "dash-leading shell ancestry resolved '$got', expected codex"
|
|
260
|
+
[ ! -s "$err" ] || fail "fm-harness wrote basename option noise for literal -zsh: $(cat "$err")"
|
|
261
|
+
|
|
262
|
+
err="$dir/fm-session-lock-ancestry.err"
|
|
263
|
+
got=$(PATH="$fakebin:$BASE_PATH" bash -c \
|
|
264
|
+
'. "$0/bin/fm-session-lock-lib.sh"; fm_harness_ancestry_pid' "$ROOT" 2>"$err")
|
|
265
|
+
[ "$got" = 4242 ] || fail "session-lock dash-leading ancestry selected '$got', expected pid 4242"
|
|
266
|
+
[ ! -s "$err" ] || fail "session-lock ancestry wrote basename option noise for literal -zsh: $(cat "$err")"
|
|
267
|
+
|
|
268
|
+
err="$dir/fm-session-lock-alive.err"
|
|
269
|
+
PATH="$fakebin:$BASE_PATH" bash -c \
|
|
270
|
+
'. "$0/bin/fm-session-lock-lib.sh"; kill() { return 0; }; fm_harness_pid_alive 5252' \
|
|
271
|
+
"$ROOT" 2>"$err"; status=$?
|
|
272
|
+
expect_code 0 "$status" "session-lock liveness should accept literal -codex as a harness process name"
|
|
273
|
+
[ ! -s "$err" ] || fail "session-lock liveness wrote basename option noise for literal -codex: $(cat "$err")"
|
|
274
|
+
|
|
275
|
+
pass "harness identity: dash-leading ps command names are basename operands, not options"
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
# ===========================================================================
|
|
279
|
+
# B) propagate_inheritable_config unit behavior
|
|
280
|
+
# ===========================================================================
|
|
281
|
+
test_propagate_lib() {
|
|
282
|
+
local d src dest home m1 m2 outside stdout stderr guard_repo err_text
|
|
283
|
+
d="$TMP_ROOT/prop-lib"
|
|
284
|
+
src="$d/src"
|
|
285
|
+
home="$d/home1"
|
|
286
|
+
dest="$home/config"
|
|
287
|
+
mkdir -p "$src" "$dest" "$home/state"
|
|
288
|
+
|
|
289
|
+
# 1. present source is copied
|
|
290
|
+
printf '{"default":{"harness":"codex"}}\n' > "$src/crew-dispatch.json"
|
|
291
|
+
printf 'codex\n' > "$src/crew-harness"
|
|
292
|
+
printf 'manual\n' > "$src/backlog-backend"
|
|
293
|
+
printf 'tmux\n' > "$src/backend"
|
|
294
|
+
: > "$src/herdr-presentation-spaces"
|
|
295
|
+
: > "$src/trace-context"
|
|
296
|
+
stdout="$d/clean-copy.out"
|
|
297
|
+
stderr="$d/clean-copy.err"
|
|
298
|
+
propagate_inheritable_config "$src" "$dest" >"$stdout" 2>"$stderr" || fail "propagate returned non-zero"
|
|
299
|
+
[ ! -s "$stdout" ] || fail "clean copy wrote to stdout"
|
|
300
|
+
[ ! -s "$stderr" ] || fail "clean copy wrote to stderr"
|
|
301
|
+
[ "$(cat "$dest/crew-dispatch.json")" = '{"default":{"harness":"codex"}}' ] || fail "crew-dispatch.json not propagated"
|
|
302
|
+
[ "$(cat "$dest/crew-harness")" = codex ] || fail "crew-harness not propagated"
|
|
303
|
+
[ "$(cat "$dest/backlog-backend")" = manual ] || fail "backlog-backend not propagated"
|
|
304
|
+
[ "$(cat "$dest/backend")" = tmux ] || fail "backend not propagated"
|
|
305
|
+
[ -f "$dest/herdr-presentation-spaces" ] || fail "herdr-presentation-spaces not propagated"
|
|
306
|
+
printf 'herdr\n' > "$dest/backend"
|
|
307
|
+
propagate_inheritable_config "$src" "$dest"
|
|
308
|
+
[ "$(cat "$dest/backend")" = tmux ] || fail "primary backend did not overwrite a divergent destination"
|
|
309
|
+
[ -f "$dest/trace-context" ] || fail "trace-context not propagated by the default inheritable set"
|
|
310
|
+
|
|
311
|
+
# 2. idempotent: an unchanged re-run does not churn the mtime
|
|
312
|
+
m1=$(date -r "$dest/crew-harness" +%s 2>/dev/null || stat -c %Y "$dest/crew-harness")
|
|
313
|
+
sleep 1
|
|
314
|
+
stdout="$d/unchanged.out"
|
|
315
|
+
stderr="$d/unchanged.err"
|
|
316
|
+
propagate_inheritable_config "$src" "$dest" >"$stdout" 2>"$stderr"
|
|
317
|
+
[ ! -s "$stdout" ] || fail "unchanged propagation wrote to stdout"
|
|
318
|
+
[ ! -s "$stderr" ] || fail "unchanged propagation wrote to stderr"
|
|
319
|
+
m2=$(date -r "$dest/crew-harness" +%s 2>/dev/null || stat -c %Y "$dest/crew-harness")
|
|
320
|
+
[ "$m1" = "$m2" ] || fail "idempotent re-run churned mtime ($m1 -> $m2)"
|
|
321
|
+
|
|
322
|
+
# 3. a changed source value converges downstream
|
|
323
|
+
printf '{"default":{"harness":"claude"}}\n' > "$src/crew-dispatch.json"
|
|
324
|
+
printf 'claude\n' > "$src/crew-harness"
|
|
325
|
+
printf 'tasks-axi\n' > "$src/backlog-backend"
|
|
326
|
+
printf 'zellij\n' > "$src/backend"
|
|
327
|
+
propagate_inheritable_config "$src" "$dest"
|
|
328
|
+
[ "$(cat "$dest/crew-dispatch.json")" = '{"default":{"harness":"claude"}}' ] || fail "changed dispatch profile did not converge"
|
|
329
|
+
[ "$(cat "$dest/crew-harness")" = claude ] || fail "changed value did not converge"
|
|
330
|
+
[ "$(cat "$dest/backlog-backend")" = tasks-axi ] || fail "changed backlog backend did not converge"
|
|
331
|
+
[ "$(cat "$dest/backend")" = zellij ] || fail "changed backend did not converge"
|
|
332
|
+
|
|
333
|
+
outside="$d/outside-target"
|
|
334
|
+
rm -f "$dest/crew-harness" "$outside"
|
|
335
|
+
printf 'outside\n' > "$outside"
|
|
336
|
+
ln -s "$outside" "$dest/crew-harness"
|
|
337
|
+
printf 'pi\n' > "$src/crew-harness"
|
|
338
|
+
propagate_inheritable_config "$src" "$dest"
|
|
339
|
+
[ ! -L "$dest/crew-harness" ] || fail "destination symlink was not replaced"
|
|
340
|
+
[ "$(cat "$dest/crew-harness")" = pi ] || fail "destination symlink replacement has wrong content"
|
|
341
|
+
[ "$(cat "$outside")" = outside ] || fail "destination symlink target was overwritten"
|
|
342
|
+
|
|
343
|
+
# 4. removing the source mirrors absence downstream (primary-authoritative)
|
|
344
|
+
printf 'herdr\n' > "$dest/backend"
|
|
345
|
+
rm -f "$src/crew-dispatch.json" "$src/crew-harness" "$src/backlog-backend" \
|
|
346
|
+
"$src/backend" "$src/herdr-presentation-spaces" "$src/trace-context"
|
|
347
|
+
propagate_inheritable_config "$src" "$dest"
|
|
348
|
+
[ -e "$dest/crew-dispatch.json" ] && fail "dispatch profile absence not mirrored downstream"
|
|
349
|
+
[ -e "$dest/crew-harness" ] && fail "absence not mirrored downstream"
|
|
350
|
+
[ -e "$dest/backlog-backend" ] && fail "backlog-backend absence not mirrored downstream"
|
|
351
|
+
[ -e "$dest/backend" ] && fail "backend absence not mirrored downstream"
|
|
352
|
+
[ -e "$dest/herdr-presentation-spaces" ] && fail "herdr-presentation-spaces absence not mirrored downstream"
|
|
353
|
+
[ -e "$dest/trace-context" ] && fail "trace-context absence not mirrored downstream"
|
|
354
|
+
|
|
355
|
+
rm -f "$dest/crew-harness"
|
|
356
|
+
ln -s "$d/missing-target" "$dest/crew-harness"
|
|
357
|
+
propagate_inheritable_config "$src" "$dest"
|
|
358
|
+
[ -L "$dest/crew-harness" ] && fail "broken destination symlink not removed on absence mirror"
|
|
359
|
+
|
|
360
|
+
mkdir -p "$dest/crew-harness"
|
|
361
|
+
stderr="$d/remove-error.err"
|
|
362
|
+
if propagate_inheritable_config "$src" "$dest" 2>"$stderr"; then
|
|
363
|
+
fail "failed absence mirror returned success"
|
|
364
|
+
fi
|
|
365
|
+
assert_contains "$(cat "$stderr")" "fm-config-inherit: error: failed to remove crew-harness" \
|
|
366
|
+
"remove error did not emit a stderr diagnostic"
|
|
367
|
+
[ -d "$dest/crew-harness" ] || fail "failed absence mirror removed the wrong path"
|
|
368
|
+
rm -rf "$dest/crew-harness"
|
|
369
|
+
|
|
370
|
+
# 5. secondmate-harness is never inherited; backend still is
|
|
371
|
+
printf 'grok\n' > "$src/secondmate-harness"
|
|
372
|
+
printf '{"default":{"harness":"codex"}}\n' > "$src/crew-dispatch.json"
|
|
373
|
+
printf 'codex\n' > "$src/crew-harness"
|
|
374
|
+
printf 'manual\n' > "$src/backlog-backend"
|
|
375
|
+
printf 'herdr\n' > "$src/backend"
|
|
376
|
+
rm -rf "$d/home2"
|
|
377
|
+
mkdir -p "$d/home2/config" "$d/home2/state"
|
|
378
|
+
propagate_inheritable_config "$src" "$d/home2/config"
|
|
379
|
+
[ -e "$d/home2/config/secondmate-harness" ] && fail "secondmate-harness was inherited (must not be)"
|
|
380
|
+
[ "$(cat "$d/home2/config/crew-dispatch.json")" = '{"default":{"harness":"codex"}}' ] || fail "crew-dispatch.json not propagated alongside"
|
|
381
|
+
[ "$(cat "$d/home2/config/crew-harness")" = codex ] || fail "crew-harness not propagated alongside"
|
|
382
|
+
[ "$(cat "$d/home2/config/backlog-backend")" = manual ] || fail "backlog-backend not propagated alongside"
|
|
383
|
+
[ "$(cat "$d/home2/config/backend")" = herdr ] || fail "backend not propagated alongside"
|
|
384
|
+
|
|
385
|
+
# 6. nothing to propagate -> destination dir is never created (a true no-op)
|
|
386
|
+
rm -rf "$d/src3" "$d/dest3"
|
|
387
|
+
mkdir -p "$d/src3"
|
|
388
|
+
# Keep backend out of the empty-source case by clearing it from src3 only.
|
|
389
|
+
propagate_inheritable_config "$d/src3" "$d/dest3/config"
|
|
390
|
+
[ -e "$d/dest3/config" ] && fail "empty-source propagation created a destination dir"
|
|
391
|
+
|
|
392
|
+
# 7. a git worktree that does not ignore an inherited item gets a visible
|
|
393
|
+
# stderr warning and a skip, not a silent miss.
|
|
394
|
+
guard_repo="$d/guard-repo"
|
|
395
|
+
git init -q -b main "$guard_repo"
|
|
396
|
+
printf 'config/crew-harness\nconfig/backlog-backend\n' > "$guard_repo/.gitignore"
|
|
397
|
+
printf 'guard\n' > "$guard_repo/README.md"
|
|
398
|
+
git -C "$guard_repo" add -A
|
|
399
|
+
git -C "$guard_repo" commit -qm guard
|
|
400
|
+
printf '{"default":{"harness":"grok"}}\n' > "$src/crew-dispatch.json"
|
|
401
|
+
stdout="$d/guard-skip.out"
|
|
402
|
+
stderr="$d/guard-skip.err"
|
|
403
|
+
FM_INHERITABLE_CONFIG=crew-dispatch.json propagate_inheritable_config "$src" "$guard_repo/config" >"$stdout" 2>"$stderr" \
|
|
404
|
+
|| fail "guard skip should not make propagation fail"
|
|
405
|
+
[ ! -s "$stdout" ] || fail "guard skip wrote to stdout"
|
|
406
|
+
err_text=$(cat "$stderr")
|
|
407
|
+
assert_contains "$err_text" "fm-config-inherit: warning: skipped crew-dispatch.json" \
|
|
408
|
+
"guard skip did not emit a stderr warning"
|
|
409
|
+
[ ! -e "$guard_repo/config/crew-dispatch.json" ] || fail "guard skip still copied the unignored item"
|
|
410
|
+
|
|
411
|
+
pass "B1 propagate_inheritable_config: copy, idempotence, convergence, absence-mirror, exclusion, no-op, skip diagnostics"
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
# ===========================================================================
|
|
415
|
+
# B/A integration: a secondmate spawn resolves the secondmate harness and
|
|
416
|
+
# propagates the crew harness into the home's config.
|
|
417
|
+
# ===========================================================================
|
|
418
|
+
|
|
419
|
+
# A tmux stub that accepts every subcommand and prints nothing, so no window
|
|
420
|
+
# pre-exists and the spawn proceeds to write its meta. Echoes the fakebin dir.
|
|
421
|
+
make_noop_tmux() {
|
|
422
|
+
local dir=$1 fakebin="$1/fakebin"
|
|
423
|
+
mkdir -p "$fakebin"
|
|
424
|
+
cat > "$fakebin/tmux" <<'SH'
|
|
425
|
+
#!/usr/bin/env bash
|
|
426
|
+
exit 0
|
|
427
|
+
SH
|
|
428
|
+
chmod +x "$fakebin/tmux"
|
|
429
|
+
printf '%s\n' "$fakebin"
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
# A minimal seeded secondmate home (validate_firstmate_home_for_spawn needs the
|
|
433
|
+
# seed marker, AGENTS.md, bin/, and a charter to launch). config/ is intentionally
|
|
434
|
+
# left absent so the spawn's propagation is what creates it.
|
|
435
|
+
make_seeded_home() {
|
|
436
|
+
local home=$1 id=$2
|
|
437
|
+
mkdir -p "$home/bin" "$home/data"
|
|
438
|
+
printf '# Firstmate\n' > "$home/AGENTS.md"
|
|
439
|
+
printf '%s\n' "$id" > "$home/.fm-secondmate-home"
|
|
440
|
+
printf 'charter\n' > "$home/data/charter.md"
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
# spawn_secondmate <world> <id> <home> [explicit-harness]
|
|
444
|
+
# Runs fm-spawn.sh in secondmate mode. FM_ROOT is the real repo (so fm-harness.sh
|
|
445
|
+
# resolves), the primary config dir is <world>/home/config, and CLAUDECODE pins
|
|
446
|
+
# detect_own. stderr is discarded (the local-HEAD ff sync harmlessly skips a
|
|
447
|
+
# non-worktree home). Inspect <world>/home/state/<id>.meta and <home>/config after.
|
|
448
|
+
spawn_secondmate() {
|
|
449
|
+
local world=$1 id=$2 home=$3 harness=${4:-} fakebin
|
|
450
|
+
mkdir -p "$world/home/state" "$world/home/data"
|
|
451
|
+
fakebin=$(make_noop_tmux "$world/tmux-$id")
|
|
452
|
+
# An empty harness must contribute zero args, not an empty positional; build the
|
|
453
|
+
# arg list explicitly so the optional harness is omitted cleanly.
|
|
454
|
+
local spawn_args=("$id" "$home")
|
|
455
|
+
[ -n "$harness" ] && spawn_args+=("$harness")
|
|
456
|
+
spawn_args+=(--secondmate)
|
|
457
|
+
PATH="$fakebin:$BASE_PATH" TMUX='' CLAUDECODE=1 \
|
|
458
|
+
FM_ROOT_OVERRIDE="$ROOT" FM_HOME="$world/home" \
|
|
459
|
+
FM_STATE_OVERRIDE="$world/home/state" FM_DATA_OVERRIDE="$world/home/data" \
|
|
460
|
+
FM_PROJECTS_OVERRIDE="$world/home/projects" FM_CONFIG_OVERRIDE="$world/home/config" \
|
|
461
|
+
FM_SPAWN_NO_GUARD=1 \
|
|
462
|
+
"$ROOT/bin/fm-spawn.sh" "${spawn_args[@]}" >/dev/null 2>&1 || true
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
meta_harness() { grep '^harness=' "$1" 2>/dev/null | tail -1 | cut -d= -f2-; }
|
|
466
|
+
|
|
467
|
+
# Split active: crew-harness=claude + secondmate-harness=codex. The secondmate
|
|
468
|
+
# AGENT launches on codex; its own crewmates inherit claude; secondmate-harness
|
|
469
|
+
# does not flow into the home.
|
|
470
|
+
test_spawn_split_and_inherit() {
|
|
471
|
+
local w sm meta
|
|
472
|
+
w="$TMP_ROOT/spawn-split"
|
|
473
|
+
sm="$w/sm"
|
|
474
|
+
mkdir -p "$w/home/config"
|
|
475
|
+
printf '{"default":{"harness":"claude","model":"haiku","effort":"low"}}\n' > "$w/home/config/crew-dispatch.json"
|
|
476
|
+
printf 'claude\n' > "$w/home/config/crew-harness"
|
|
477
|
+
printf 'codex\n' > "$w/home/config/secondmate-harness"
|
|
478
|
+
printf 'manual\n' > "$w/home/config/backlog-backend"
|
|
479
|
+
printf 'zellij\n' > "$w/home/config/backend"
|
|
480
|
+
make_seeded_home "$sm" sm
|
|
481
|
+
|
|
482
|
+
spawn_secondmate "$w" sm "$sm"
|
|
483
|
+
|
|
484
|
+
meta="$w/home/state/sm.meta"
|
|
485
|
+
[ -f "$meta" ] || fail "split: no meta written"
|
|
486
|
+
[ "$(meta_harness "$meta")" = codex ] \
|
|
487
|
+
|| fail "split: secondmate launched on '$(meta_harness "$meta")', expected codex"
|
|
488
|
+
[ "$(cat "$sm/config/crew-harness" 2>/dev/null)" = claude ] \
|
|
489
|
+
|| fail "split: home crew-harness not inherited as claude (got '$(cat "$sm/config/crew-harness" 2>/dev/null)')"
|
|
490
|
+
[ "$(cat "$sm/config/crew-dispatch.json" 2>/dev/null)" = '{"default":{"harness":"claude","model":"haiku","effort":"low"}}' ] \
|
|
491
|
+
|| fail "split: home crew-dispatch.json not inherited"
|
|
492
|
+
[ "$(cat "$sm/config/backlog-backend" 2>/dev/null)" = manual ] \
|
|
493
|
+
|| fail "split: home backlog-backend not inherited as manual"
|
|
494
|
+
[ "$(cat "$sm/config/backend" 2>/dev/null)" = zellij ] \
|
|
495
|
+
|| fail "split: home backend not inherited as zellij"
|
|
496
|
+
[ -e "$sm/config/secondmate-harness" ] \
|
|
497
|
+
&& fail "split: secondmate-harness leaked into the secondmate home"
|
|
498
|
+
pass "B2 spawn: secondmate runs the secondmate harness; its home inherits declared config"
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
# Backward-compat: secondmate-harness absent -> the secondmate launches on the
|
|
502
|
+
# crew harness, exactly as before this knob existed, and that crew value is the
|
|
503
|
+
# one inherited.
|
|
504
|
+
test_spawn_backward_compat_crew_fallback() {
|
|
505
|
+
local w sm meta
|
|
506
|
+
w="$TMP_ROOT/spawn-compat"
|
|
507
|
+
sm="$w/sm"
|
|
508
|
+
mkdir -p "$w/home/config"
|
|
509
|
+
printf 'codex\n' > "$w/home/config/crew-harness"
|
|
510
|
+
make_seeded_home "$sm" sm
|
|
511
|
+
|
|
512
|
+
spawn_secondmate "$w" sm "$sm"
|
|
513
|
+
|
|
514
|
+
meta="$w/home/state/sm.meta"
|
|
515
|
+
[ "$(meta_harness "$meta")" = codex ] \
|
|
516
|
+
|| fail "compat: secondmate launched on '$(meta_harness "$meta")', expected the crew harness codex"
|
|
517
|
+
[ "$(cat "$sm/config/crew-harness" 2>/dev/null)" = codex ] \
|
|
518
|
+
|| fail "compat: home crew-harness not inherited as codex"
|
|
519
|
+
pass "B3 spawn: an absent secondmate-harness falls back to the crew harness (backward-compat)"
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
# Bare backward-compat: no config at all. The secondmate falls through to its own
|
|
523
|
+
# harness (claude here), and with no inheritable file the home is left untouched -
|
|
524
|
+
# no config/ side effects.
|
|
525
|
+
test_spawn_bare_backward_compat() {
|
|
526
|
+
local w sm meta
|
|
527
|
+
w="$TMP_ROOT/spawn-bare"
|
|
528
|
+
sm="$w/sm"
|
|
529
|
+
make_seeded_home "$sm" sm
|
|
530
|
+
|
|
531
|
+
spawn_secondmate "$w" sm "$sm"
|
|
532
|
+
|
|
533
|
+
meta="$w/home/state/sm.meta"
|
|
534
|
+
[ "$(meta_harness "$meta")" = claude ] \
|
|
535
|
+
|| fail "bare: secondmate launched on '$(meta_harness "$meta")', expected own harness claude"
|
|
536
|
+
[ -e "$sm/config/crew-dispatch.json" ] && fail "bare: an unset primary still created a home crew-dispatch.json"
|
|
537
|
+
[ -e "$sm/config/crew-harness" ] && fail "bare: an unset primary still created a home crew-harness"
|
|
538
|
+
pass "B4 spawn: no config at all -> own harness and no propagation side effects"
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
# An explicit per-spawn harness arg wins over config/secondmate-harness.
|
|
542
|
+
test_spawn_explicit_harness_wins() {
|
|
543
|
+
local w sm meta
|
|
544
|
+
w="$TMP_ROOT/spawn-explicit"
|
|
545
|
+
sm="$w/sm"
|
|
546
|
+
mkdir -p "$w/home/config"
|
|
547
|
+
printf 'codex\n' > "$w/home/config/secondmate-harness"
|
|
548
|
+
make_seeded_home "$sm" sm
|
|
549
|
+
|
|
550
|
+
spawn_secondmate "$w" sm "$sm" claude
|
|
551
|
+
|
|
552
|
+
meta="$w/home/state/sm.meta"
|
|
553
|
+
[ "$(meta_harness "$meta")" = claude ] \
|
|
554
|
+
|| fail "explicit: launched on '$(meta_harness "$meta")', expected explicit claude over config codex"
|
|
555
|
+
pass "B5 spawn: an explicit per-spawn harness arg overrides config/secondmate-harness"
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
# The unverified-adapter guard holds on the resolved secondmate path: an unknown
|
|
559
|
+
# config/secondmate-harness aborts the spawn (no meta written) and names the source.
|
|
560
|
+
test_spawn_unverified_secondmate_harness_refused() {
|
|
561
|
+
local w sm fakebin err rc
|
|
562
|
+
w="$TMP_ROOT/spawn-unverified"
|
|
563
|
+
sm="$w/sm"
|
|
564
|
+
mkdir -p "$w/home/config" "$w/home/state"
|
|
565
|
+
printf 'bogus\n' > "$w/home/config/secondmate-harness"
|
|
566
|
+
make_seeded_home "$sm" sm
|
|
567
|
+
fakebin=$(make_noop_tmux "$w/tmux")
|
|
568
|
+
err="$w/spawn.err"
|
|
569
|
+
rc=0
|
|
570
|
+
PATH="$fakebin:$BASE_PATH" TMUX='' CLAUDECODE=1 \
|
|
571
|
+
FM_ROOT_OVERRIDE="$ROOT" FM_HOME="$w/home" \
|
|
572
|
+
FM_STATE_OVERRIDE="$w/home/state" FM_DATA_OVERRIDE="$w/home/data" \
|
|
573
|
+
FM_PROJECTS_OVERRIDE="$w/home/projects" FM_CONFIG_OVERRIDE="$w/home/config" \
|
|
574
|
+
FM_SPAWN_NO_GUARD=1 \
|
|
575
|
+
"$ROOT/bin/fm-spawn.sh" sm "$sm" --secondmate >/dev/null 2>"$err" || rc=$?
|
|
576
|
+
|
|
577
|
+
[ "$rc" -ne 0 ] || fail "unverified: spawn should have failed"
|
|
578
|
+
assert_contains "$(cat "$err")" "no launch template for harness 'bogus'" \
|
|
579
|
+
"unverified: error names the rejected harness"
|
|
580
|
+
assert_contains "$(cat "$err")" "config/secondmate-harness" \
|
|
581
|
+
"unverified: error names the secondmate-harness source"
|
|
582
|
+
[ -e "$w/home/state/sm.meta" ] && fail "unverified: a meta was written despite the abort"
|
|
583
|
+
pass "B6 spawn: an unverified resolved secondmate harness is refused (guard intact)"
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
test_spawn_cursor_secondmate_launches_with_its_primary_contract() {
|
|
587
|
+
local w sm fakebin launchlog launch meta rc
|
|
588
|
+
w="$TMP_ROOT/spawn-cursor-secondmate"
|
|
589
|
+
sm="$w/sm"
|
|
590
|
+
launchlog="$w/launch.log"
|
|
591
|
+
mkdir -p "$w/home/config" "$w/home/state" "$w/home/data" "$w/home/projects"
|
|
592
|
+
printf 'cursor\n' > "$w/home/config/secondmate-harness"
|
|
593
|
+
make_seeded_home "$sm" sm
|
|
594
|
+
fakebin=$(make_launch_capturing_tmux "$w/tmux")
|
|
595
|
+
: > "$launchlog"
|
|
596
|
+
rc=0
|
|
597
|
+
PATH="$fakebin:$BASE_PATH" TMUX='' CLAUDECODE=1 \
|
|
598
|
+
FM_ROOT_OVERRIDE="$ROOT" FM_HOME="$w/home" \
|
|
599
|
+
FM_STATE_OVERRIDE="$w/home/state" FM_DATA_OVERRIDE="$w/home/data" \
|
|
600
|
+
FM_PROJECTS_OVERRIDE="$w/home/projects" FM_CONFIG_OVERRIDE="$w/home/config" \
|
|
601
|
+
FM_SPAWN_NO_GUARD=1 FM_FAKE_LAUNCH_LOG="$launchlog" FM_FAKE_PANE_PATH="$sm" \
|
|
602
|
+
"$ROOT/bin/fm-spawn.sh" sm "$sm" --secondmate >/dev/null 2>&1 || rc=$?
|
|
603
|
+
|
|
604
|
+
[ "$rc" -eq 0 ] || {
|
|
605
|
+
echo "skip: cursor executable not resolvable in this environment, so the launch could not be built"
|
|
606
|
+
return
|
|
607
|
+
}
|
|
608
|
+
meta="$w/home/state/sm.meta"
|
|
609
|
+
[ "$(meta_field "$meta" harness)" = cursor ] || fail "a cursor secondmate must record its own harness"
|
|
610
|
+
[ "$(meta_field "$meta" kind)" = secondmate ] || fail "a cursor secondmate must record kind=secondmate"
|
|
611
|
+
launch=$(cat "$launchlog")
|
|
612
|
+
assert_contains "$launch" "--trust" \
|
|
613
|
+
"a cursor secondmate must launch with --trust, or none of its project hooks load and its home has no supervision at all"
|
|
614
|
+
assert_contains "$launch" "--workspace" \
|
|
615
|
+
"a cursor secondmate must be pinned to its own home as the workspace"
|
|
616
|
+
assert_contains "$launch" "FM_SUPERVISION_MODEL=autoarm" \
|
|
617
|
+
"cursor's stop-hook park runs the watcher only between turns, so its home must inherit the autoarm model"
|
|
618
|
+
pass "Cursor is accepted for secondmates and launches with the contract its park needs"
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
# ===========================================================================
|
|
622
|
+
# C integration: config/secondmate-harness's optional model/effort tokens thread
|
|
623
|
+
# into the secondmate launch command and meta, durably and without a new file.
|
|
624
|
+
# ===========================================================================
|
|
625
|
+
|
|
626
|
+
meta_field() { grep "^$2=" "$1" 2>/dev/null | tail -1 | cut -d= -f2-; }
|
|
627
|
+
|
|
628
|
+
# A tmux stub that behaves like make_noop_tmux but also captures the literal
|
|
629
|
+
# `send-keys -l <cmd>` launch command into FM_FAKE_LAUNCH_LOG, mirroring the
|
|
630
|
+
# capture technique in fm-spawn-dispatch-profile.test.sh so the constructed
|
|
631
|
+
# launch command (not just meta) can be asserted on. Also answers the
|
|
632
|
+
# `#{pane_current_path}` probe from FM_FAKE_PANE_PATH so this same stub works
|
|
633
|
+
# for a crew/scout (non-secondmate) spawn's treehouse-worktree wait loop.
|
|
634
|
+
make_launch_capturing_tmux() {
|
|
635
|
+
local dir=$1 fakebin="$1/fakebin"
|
|
636
|
+
mkdir -p "$fakebin"
|
|
637
|
+
cat > "$fakebin/tmux" <<'SH'
|
|
638
|
+
#!/usr/bin/env bash
|
|
639
|
+
set -u
|
|
640
|
+
case "$*" in
|
|
641
|
+
*"#{pane_current_path}"*) printf '%s\n' "${FM_FAKE_PANE_PATH:-}"; exit 0 ;;
|
|
642
|
+
esac
|
|
643
|
+
case "${1:-}" in
|
|
644
|
+
display-message) printf 'firstmate\n'; exit 0 ;;
|
|
645
|
+
list-windows) exit 0 ;;
|
|
646
|
+
has-session|new-session|new-window|kill-window) exit 0 ;;
|
|
647
|
+
send-keys)
|
|
648
|
+
if [ -n "${FM_FAKE_LAUNCH_LOG:-}" ]; then
|
|
649
|
+
prev=
|
|
650
|
+
for a in "$@"; do
|
|
651
|
+
if [ "$prev" = "-l" ]; then
|
|
652
|
+
printf '%s\n' "$a" >> "$FM_FAKE_LAUNCH_LOG"
|
|
653
|
+
fi
|
|
654
|
+
prev=$a
|
|
655
|
+
done
|
|
656
|
+
fi
|
|
657
|
+
exit 0
|
|
658
|
+
;;
|
|
659
|
+
esac
|
|
660
|
+
exit 0
|
|
661
|
+
SH
|
|
662
|
+
chmod +x "$fakebin/tmux"
|
|
663
|
+
fm_fake_exit0 "$fakebin" pi
|
|
664
|
+
printf '%s\n' "$fakebin"
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
# spawn_secondmate_capture <world> <id> <home> <launchlog> [extra fm-spawn.sh args...]
|
|
668
|
+
# Same shape as spawn_secondmate but captures the launch command into <launchlog>
|
|
669
|
+
# and does not discard stderr, so callers can assert on both.
|
|
670
|
+
spawn_secondmate_capture() {
|
|
671
|
+
local world=$1 id=$2 home=$3 launchlog=$4 fakebin
|
|
672
|
+
shift 4
|
|
673
|
+
mkdir -p "$world/home/state" "$world/home/data"
|
|
674
|
+
fakebin=$(make_launch_capturing_tmux "$world/tmux-$id")
|
|
675
|
+
: > "$launchlog"
|
|
676
|
+
PATH="$fakebin:$BASE_PATH" TMUX='' CLAUDECODE=1 \
|
|
677
|
+
FM_ROOT_OVERRIDE="$ROOT" FM_HOME="$world/home" \
|
|
678
|
+
FM_STATE_OVERRIDE="$world/home/state" FM_DATA_OVERRIDE="$world/home/data" \
|
|
679
|
+
FM_PROJECTS_OVERRIDE="$world/home/projects" FM_CONFIG_OVERRIDE="$world/home/config" \
|
|
680
|
+
FM_SPAWN_NO_GUARD=1 FM_FAKE_LAUNCH_LOG="$launchlog" \
|
|
681
|
+
"$ROOT/bin/fm-spawn.sh" "$id" "$home" "$@" --secondmate
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
test_spawn_backend_precedence_over_inherited_config() {
|
|
685
|
+
local w sm meta launchlog out status
|
|
686
|
+
w="$TMP_ROOT/spawn-backend-env-precedence"
|
|
687
|
+
sm="$w/sm"
|
|
688
|
+
launchlog="$w/launch.log"
|
|
689
|
+
mkdir -p "$w/home/config"
|
|
690
|
+
printf 'herdr\n' > "$w/home/config/backend"
|
|
691
|
+
make_seeded_home "$sm" sm
|
|
692
|
+
|
|
693
|
+
out=$(FM_BACKEND=tmux spawn_secondmate_capture \
|
|
694
|
+
"$w" sm "$sm" "$launchlog" 2>&1); status=$?
|
|
695
|
+
expect_code 0 "$status" \
|
|
696
|
+
"FM_BACKEND=tmux should beat inherited config/backend=herdr"$'\n'"$out"
|
|
697
|
+
|
|
698
|
+
meta="$w/home/state/sm.meta"
|
|
699
|
+
[ "$(cat "$sm/config/backend")" = herdr ] \
|
|
700
|
+
|| fail "backend precedence fixture did not inherit config/backend=herdr"
|
|
701
|
+
assert_no_grep '^backend=' "$meta" \
|
|
702
|
+
"FM_BACKEND=tmux did not beat inherited config/backend=herdr"
|
|
703
|
+
pass "B5b spawn: FM_BACKEND wins over inherited config/backend"
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
test_spawn_explicit_backend_precedence_over_env_and_inherited_config() {
|
|
707
|
+
local w sm meta launchlog out status
|
|
708
|
+
w="$TMP_ROOT/spawn-backend-flag-precedence"
|
|
709
|
+
sm="$w/sm"
|
|
710
|
+
launchlog="$w/launch.log"
|
|
711
|
+
mkdir -p "$w/home/config"
|
|
712
|
+
printf 'herdr\n' > "$w/home/config/backend"
|
|
713
|
+
make_seeded_home "$sm" sm
|
|
714
|
+
|
|
715
|
+
out=$(FM_BACKEND=zellij spawn_secondmate_capture \
|
|
716
|
+
"$w" sm "$sm" "$launchlog" --backend tmux 2>&1); status=$?
|
|
717
|
+
expect_code 0 "$status" \
|
|
718
|
+
"explicit --backend tmux should beat FM_BACKEND=zellij and inherited config/backend=herdr"$'\n'"$out"
|
|
719
|
+
|
|
720
|
+
meta="$w/home/state/sm.meta"
|
|
721
|
+
[ "$(cat "$sm/config/backend")" = herdr ] \
|
|
722
|
+
|| fail "explicit backend precedence fixture did not inherit config/backend=herdr"
|
|
723
|
+
assert_no_grep '^backend=' "$meta" \
|
|
724
|
+
"explicit --backend tmux did not beat FM_BACKEND=zellij and inherited config/backend=herdr"
|
|
725
|
+
pass "B5c spawn: explicit --backend wins over FM_BACKEND and inherited config/backend"
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
# A bare "<harness>" secondmate-harness file (today's format) must launch with
|
|
729
|
+
# NO --model/--effort flag at all, and meta must keep recording model=default,
|
|
730
|
+
# effort=default - the core backward-compat requirement of the new format.
|
|
731
|
+
test_spawn_bare_harness_no_model_effort_flag() {
|
|
732
|
+
local w sm meta launchlog launch out status
|
|
733
|
+
w="$TMP_ROOT/spawn-bare-tokens"
|
|
734
|
+
sm="$w/sm"
|
|
735
|
+
launchlog="$w/launch.log"
|
|
736
|
+
mkdir -p "$w/home/config"
|
|
737
|
+
printf 'claude\n' > "$w/home/config/secondmate-harness"
|
|
738
|
+
make_seeded_home "$sm" sm
|
|
739
|
+
|
|
740
|
+
out=$(spawn_secondmate_capture "$w" sm "$sm" "$launchlog" 2>&1); status=$?
|
|
741
|
+
expect_code 0 "$status" "bare-harness secondmate spawn should succeed"
|
|
742
|
+
|
|
743
|
+
meta="$w/home/state/sm.meta"
|
|
744
|
+
[ "$(meta_field "$meta" model)" = default ] || fail "bare-tokens: meta model not default (got '$(meta_field "$meta" model)')"
|
|
745
|
+
[ "$(meta_field "$meta" effort)" = default ] || fail "bare-tokens: meta effort not default (got '$(meta_field "$meta" effort)')"
|
|
746
|
+
launch=$(cat "$launchlog")
|
|
747
|
+
assert_not_contains "$launch" "--model" "bare-tokens: launch must not carry a --model flag"
|
|
748
|
+
assert_not_contains "$launch" "--effort" "bare-tokens: launch must not carry an --effort flag"
|
|
749
|
+
pass "C2 spawn: a bare harness-only secondmate-harness file launches with no model/effort flag (backward-compat)"
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
# "<harness> <model>" durably threads --model into the secondmate launch and
|
|
753
|
+
# records it in meta, with no --effort flag (no effort token supplied).
|
|
754
|
+
test_spawn_secondmate_harness_model_token() {
|
|
755
|
+
local w sm meta launchlog launch
|
|
756
|
+
w="$TMP_ROOT/spawn-model-token"
|
|
757
|
+
sm="$w/sm"
|
|
758
|
+
launchlog="$w/launch.log"
|
|
759
|
+
mkdir -p "$w/home/config"
|
|
760
|
+
printf 'claude opus\n' > "$w/home/config/secondmate-harness"
|
|
761
|
+
make_seeded_home "$sm" sm
|
|
762
|
+
|
|
763
|
+
spawn_secondmate_capture "$w" sm "$sm" "$launchlog" >/dev/null 2>&1
|
|
764
|
+
|
|
765
|
+
meta="$w/home/state/sm.meta"
|
|
766
|
+
[ "$(meta_field "$meta" harness)" = claude ] || fail "model-token: meta harness not claude"
|
|
767
|
+
[ "$(meta_field "$meta" model)" = opus ] || fail "model-token: meta model not opus (got '$(meta_field "$meta" model)')"
|
|
768
|
+
[ "$(meta_field "$meta" effort)" = default ] || fail "model-token: meta effort not default (got '$(meta_field "$meta" effort)')"
|
|
769
|
+
launch=$(cat "$launchlog")
|
|
770
|
+
assert_contains "$launch" "claude --dangerously-skip-permissions --model 'opus'" \
|
|
771
|
+
"model-token: launch did not carry --model opus"
|
|
772
|
+
assert_not_contains "$launch" "--effort" "model-token: launch must not carry an --effort flag"
|
|
773
|
+
pass "C3 spawn: config/secondmate-harness's model token threads --model into the launch and meta"
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
# "<harness> <model> <effort>" threads both flags into the launch and meta.
|
|
777
|
+
test_spawn_secondmate_harness_model_and_effort_tokens() {
|
|
778
|
+
local w sm meta launchlog launch
|
|
779
|
+
w="$TMP_ROOT/spawn-model-effort-tokens"
|
|
780
|
+
sm="$w/sm"
|
|
781
|
+
launchlog="$w/launch.log"
|
|
782
|
+
mkdir -p "$w/home/config"
|
|
783
|
+
printf 'claude opus high\n' > "$w/home/config/secondmate-harness"
|
|
784
|
+
make_seeded_home "$sm" sm
|
|
785
|
+
|
|
786
|
+
spawn_secondmate_capture "$w" sm "$sm" "$launchlog" >/dev/null 2>&1
|
|
787
|
+
|
|
788
|
+
meta="$w/home/state/sm.meta"
|
|
789
|
+
[ "$(meta_field "$meta" model)" = opus ] || fail "model-effort-tokens: meta model not opus"
|
|
790
|
+
[ "$(meta_field "$meta" effort)" = high ] || fail "model-effort-tokens: meta effort not high (got '$(meta_field "$meta" effort)')"
|
|
791
|
+
launch=$(cat "$launchlog")
|
|
792
|
+
assert_contains "$launch" "claude --dangerously-skip-permissions --model 'opus' --effort 'high'" \
|
|
793
|
+
"model-effort-tokens: launch did not carry both --model opus and --effort high"
|
|
794
|
+
pass "C4 spawn: config/secondmate-harness's model+effort tokens thread into the launch and meta"
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
# Precedence: an explicit per-spawn --model overrides the file's model token.
|
|
798
|
+
test_spawn_explicit_model_overrides_secondmate_harness_token() {
|
|
799
|
+
local w sm meta launchlog launch
|
|
800
|
+
w="$TMP_ROOT/spawn-explicit-model"
|
|
801
|
+
sm="$w/sm"
|
|
802
|
+
launchlog="$w/launch.log"
|
|
803
|
+
mkdir -p "$w/home/config"
|
|
804
|
+
printf 'claude opus high\n' > "$w/home/config/secondmate-harness"
|
|
805
|
+
make_seeded_home "$sm" sm
|
|
806
|
+
|
|
807
|
+
spawn_secondmate_capture "$w" sm "$sm" "$launchlog" --model sonnet >/dev/null 2>&1
|
|
808
|
+
|
|
809
|
+
meta="$w/home/state/sm.meta"
|
|
810
|
+
[ "$(meta_field "$meta" model)" = sonnet ] \
|
|
811
|
+
|| fail "explicit-model: meta model not sonnet (got '$(meta_field "$meta" model)'), explicit flag did not win over file token"
|
|
812
|
+
[ "$(meta_field "$meta" effort)" = high ] || fail "explicit-model: file's effort token should still apply"
|
|
813
|
+
launch=$(cat "$launchlog")
|
|
814
|
+
assert_contains "$launch" "--model 'sonnet'" "explicit-model: launch did not use the explicit --model"
|
|
815
|
+
assert_not_contains "$launch" "--model 'opus'" "explicit-model: launch leaked the file's model token"
|
|
816
|
+
pass "C5 spawn: an explicit --model overrides config/secondmate-harness's model token; the file's effort token still applies"
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
# Precedence: an explicit per-spawn --effort overrides the file's effort token.
|
|
820
|
+
test_spawn_explicit_effort_overrides_secondmate_harness_token() {
|
|
821
|
+
local w sm meta launchlog launch
|
|
822
|
+
w="$TMP_ROOT/spawn-explicit-effort"
|
|
823
|
+
sm="$w/sm"
|
|
824
|
+
launchlog="$w/launch.log"
|
|
825
|
+
mkdir -p "$w/home/config"
|
|
826
|
+
printf 'claude opus high\n' > "$w/home/config/secondmate-harness"
|
|
827
|
+
make_seeded_home "$sm" sm
|
|
828
|
+
|
|
829
|
+
spawn_secondmate_capture "$w" sm "$sm" "$launchlog" --effort low >/dev/null 2>&1
|
|
830
|
+
|
|
831
|
+
meta="$w/home/state/sm.meta"
|
|
832
|
+
[ "$(meta_field "$meta" model)" = opus ] || fail "explicit-effort: file's model token should still apply"
|
|
833
|
+
[ "$(meta_field "$meta" effort)" = low ] \
|
|
834
|
+
|| fail "explicit-effort: meta effort not low (got '$(meta_field "$meta" effort)'), explicit flag did not win over file token"
|
|
835
|
+
launch=$(cat "$launchlog")
|
|
836
|
+
assert_contains "$launch" "--effort 'low'" "explicit-effort: launch did not use the explicit --effort"
|
|
837
|
+
assert_not_contains "$launch" "--effort 'high'" "explicit-effort: launch leaked the file's effort token"
|
|
838
|
+
pass "C6 spawn: an explicit --effort overrides config/secondmate-harness's effort token; the file's model token still applies"
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
test_spawn_explicit_harness_does_not_inherit_secondmate_harness_tokens() {
|
|
842
|
+
local w sm meta launchlog launch
|
|
843
|
+
w="$TMP_ROOT/spawn-explicit-harness-no-tokens"
|
|
844
|
+
sm="$w/sm"
|
|
845
|
+
launchlog="$w/launch.log"
|
|
846
|
+
mkdir -p "$w/home/config"
|
|
847
|
+
printf 'claude opus high\n' > "$w/home/config/secondmate-harness"
|
|
848
|
+
make_seeded_home "$sm" sm
|
|
849
|
+
|
|
850
|
+
spawn_secondmate_capture "$w" sm "$sm" "$launchlog" --harness codex >/dev/null 2>&1
|
|
851
|
+
|
|
852
|
+
meta="$w/home/state/sm.meta"
|
|
853
|
+
[ "$(meta_field "$meta" harness)" = codex ] || fail "explicit-harness-no-tokens: meta harness not codex"
|
|
854
|
+
[ "$(meta_field "$meta" model)" = default ] || fail "explicit-harness-no-tokens: meta model should stay default"
|
|
855
|
+
[ "$(meta_field "$meta" effort)" = default ] || fail "explicit-harness-no-tokens: meta effort should stay default"
|
|
856
|
+
launch=$(cat "$launchlog")
|
|
857
|
+
assert_contains "$launch" "codex --dangerously-bypass-approvals-and-sandbox" \
|
|
858
|
+
"explicit-harness-no-tokens: launch did not use codex"
|
|
859
|
+
assert_not_contains "$launch" "--model" "explicit-harness-no-tokens: launch must not carry a --model flag"
|
|
860
|
+
assert_not_contains "$launch" "model_reasoning_effort" \
|
|
861
|
+
"explicit-harness-no-tokens: launch must not carry a codex effort flag"
|
|
862
|
+
pass "C7 spawn: an explicit --harness starts with clean model/effort defaults"
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
test_spawn_explicit_harness_uses_explicit_profile_axes() {
|
|
866
|
+
local w sm meta launchlog launch
|
|
867
|
+
w="$TMP_ROOT/spawn-explicit-harness-explicit-axes"
|
|
868
|
+
sm="$w/sm"
|
|
869
|
+
launchlog="$w/launch.log"
|
|
870
|
+
mkdir -p "$w/home/config"
|
|
871
|
+
printf 'claude opus high\n' > "$w/home/config/secondmate-harness"
|
|
872
|
+
make_seeded_home "$sm" sm
|
|
873
|
+
|
|
874
|
+
spawn_secondmate_capture "$w" sm "$sm" "$launchlog" --harness codex --model gpt-5.5 --effort xhigh >/dev/null 2>&1
|
|
875
|
+
|
|
876
|
+
meta="$w/home/state/sm.meta"
|
|
877
|
+
[ "$(meta_field "$meta" harness)" = codex ] || fail "explicit-harness-explicit-axes: meta harness not codex"
|
|
878
|
+
[ "$(meta_field "$meta" model)" = gpt-5.5 ] || fail "explicit-harness-explicit-axes: meta model did not use explicit value"
|
|
879
|
+
[ "$(meta_field "$meta" effort)" = xhigh ] || fail "explicit-harness-explicit-axes: meta effort did not use explicit value"
|
|
880
|
+
launch=$(cat "$launchlog")
|
|
881
|
+
assert_contains "$launch" "--model 'gpt-5.5'" \
|
|
882
|
+
"explicit-harness-explicit-axes: launch did not use the explicit --model"
|
|
883
|
+
assert_contains "$launch" "-c 'model_reasoning_effort=\"xhigh\"'" \
|
|
884
|
+
"explicit-harness-explicit-axes: launch did not use the explicit --effort"
|
|
885
|
+
assert_not_contains "$launch" "--model 'opus'" \
|
|
886
|
+
"explicit-harness-explicit-axes: launch leaked the file's model token"
|
|
887
|
+
assert_not_contains "$launch" "model_reasoning_effort=\"high\"" \
|
|
888
|
+
"explicit-harness-explicit-axes: launch leaked the file's effort token"
|
|
889
|
+
pass "C8 spawn: an explicit --harness still honors explicit model/effort flags"
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
test_spawned_secondmate_uses_its_harness_supervision_model() {
|
|
893
|
+
local harness expected w sm launchlog launch fakebin out
|
|
894
|
+
for harness in codex claude; do
|
|
895
|
+
w="$TMP_ROOT/spawn-supervision-model-$harness"
|
|
896
|
+
sm="$w/sm"
|
|
897
|
+
launchlog="$w/launch.log"
|
|
898
|
+
mkdir -p "$w/home/config"
|
|
899
|
+
printf '%s\n' "$harness" > "$w/home/config/secondmate-harness"
|
|
900
|
+
make_seeded_home "$sm" sm
|
|
901
|
+
spawn_secondmate_capture "$w" sm "$sm" "$launchlog" >/dev/null 2>&1
|
|
902
|
+
fm_write_meta "$sm/state/task.meta" "window=firstmate:fm-task" "kind=ship"
|
|
903
|
+
touch "$sm/state/.last-watcher-beat"
|
|
904
|
+
fakebin="$w/tmux-sm/fakebin"
|
|
905
|
+
# Point the guard at the fixture home, not at whatever checkout this suite
|
|
906
|
+
# happens to be running from. The guard also reports a tangled primary
|
|
907
|
+
# checkout, so without this the branch a contributor is working on decides
|
|
908
|
+
# whether this assertion passes.
|
|
909
|
+
cat > "$fakebin/$harness" <<SH
|
|
910
|
+
#!/usr/bin/env bash
|
|
911
|
+
FM_ROOT_OVERRIDE="$sm" "$ROOT/bin/fm-guard.sh"
|
|
912
|
+
SH
|
|
913
|
+
chmod +x "$fakebin/$harness"
|
|
914
|
+
launch=$(cat "$launchlog")
|
|
915
|
+
out=$(PATH="$fakebin:$BASE_PATH" CLAUDECODE=1 bash -c "$launch" 2>&1)
|
|
916
|
+
case "$harness" in
|
|
917
|
+
codex)
|
|
918
|
+
expected='WATCHER DOWN - SUPERVISION IS OFF'
|
|
919
|
+
assert_contains "$out" "$expected" \
|
|
920
|
+
"Codex secondmate inherited Claude auto-arm despite its persistent watcher model"
|
|
921
|
+
;;
|
|
922
|
+
claude)
|
|
923
|
+
[ -z "$out" ] \
|
|
924
|
+
|| fail "Claude secondmate with a fresh beacon should use auto-arm supervision, got: $out"
|
|
925
|
+
;;
|
|
926
|
+
esac
|
|
927
|
+
done
|
|
928
|
+
pass "C9 spawn: secondmate launch pins supervision to its own harness"
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
# The harness fallback chain (secondmate-harness -> crew-harness -> own) still
|
|
932
|
+
# resolves correctly with no model/effort tokens anywhere in the chain, and a
|
|
933
|
+
# crew/scout (non-secondmate) launch is entirely unaffected by this feature: no
|
|
934
|
+
# model/effort is invented for it even though its own project has no profile set.
|
|
935
|
+
test_spawn_fallback_chain_and_crew_scout_unaffected() {
|
|
936
|
+
local w sm meta home proj wt fakebin launchlog id launch
|
|
937
|
+
w="$TMP_ROOT/spawn-fallback-and-crew"
|
|
938
|
+
sm="$w/sm"
|
|
939
|
+
launchlog="$w/launch.log"
|
|
940
|
+
mkdir -p "$w/home/config"
|
|
941
|
+
printf 'codex\n' > "$w/home/config/crew-harness"
|
|
942
|
+
make_seeded_home "$sm" sm
|
|
943
|
+
|
|
944
|
+
spawn_secondmate_capture "$w" sm "$sm" "$launchlog" >/dev/null 2>&1
|
|
945
|
+
|
|
946
|
+
meta="$w/home/state/sm.meta"
|
|
947
|
+
[ "$(meta_field "$meta" harness)" = codex ] \
|
|
948
|
+
|| fail "fallback: secondmate harness did not fall back to crew-harness codex"
|
|
949
|
+
[ "$(meta_field "$meta" model)" = default ] || fail "fallback: meta model should stay default with no tokens anywhere"
|
|
950
|
+
[ "$(meta_field "$meta" effort)" = default ] || fail "fallback: meta effort should stay default with no tokens anywhere"
|
|
951
|
+
|
|
952
|
+
# Crew/scout launch: same crew-harness config, no --secondmate. Must resolve
|
|
953
|
+
# the crew harness and record no model/effort - this codepath must never read
|
|
954
|
+
# config/secondmate-harness's tokens at all.
|
|
955
|
+
id="crew-unaffected-z1"
|
|
956
|
+
home="$w/home"
|
|
957
|
+
proj="$w/crew-project"
|
|
958
|
+
wt="$w/crew-wt"
|
|
959
|
+
fakebin=$(make_launch_capturing_tmux "$w/tmux-crew")
|
|
960
|
+
fm_git_worktree "$proj" "$wt" "wt-crew"
|
|
961
|
+
mkdir -p "$home/data/$id" "$home/projects" "$home/state"
|
|
962
|
+
printf 'brief\n' > "$home/data/$id/brief.md"
|
|
963
|
+
: > "$launchlog"
|
|
964
|
+
PATH="$fakebin:$BASE_PATH" TMUX="fake,1,0" CLAUDECODE=1 \
|
|
965
|
+
FM_ROOT_OVERRIDE="$ROOT" FM_HOME="$home" \
|
|
966
|
+
FM_STATE_OVERRIDE="$home/state" FM_DATA_OVERRIDE="$home/data" \
|
|
967
|
+
FM_PROJECTS_OVERRIDE="$home/projects" FM_CONFIG_OVERRIDE="$home/config" \
|
|
968
|
+
FM_SPAWN_NO_GUARD=1 FM_FAKE_PANE_PATH="$wt" FM_FAKE_LAUNCH_LOG="$launchlog" \
|
|
969
|
+
"$ROOT/bin/fm-spawn.sh" "$id" "$proj" --mode no-mistakes --yolo off >/dev/null 2>&1
|
|
970
|
+
meta="$home/state/$id.meta"
|
|
971
|
+
[ "$(meta_field "$meta" kind)" = ship ] || fail "crew-unaffected: expected an ordinary ship task"
|
|
972
|
+
[ "$(meta_field "$meta" harness)" = codex ] || fail "crew-unaffected: crew harness resolution changed"
|
|
973
|
+
[ "$(meta_field "$meta" model)" = default ] || fail "crew-unaffected: crew task must not invent a model"
|
|
974
|
+
[ "$(meta_field "$meta" effort)" = default ] || fail "crew-unaffected: crew task must not invent an effort"
|
|
975
|
+
launch=$(cat "$launchlog")
|
|
976
|
+
assert_not_contains "$launch" "--model" "crew-unaffected: crew launch must not carry a --model flag"
|
|
977
|
+
assert_not_contains "$launch" "--effort" "crew-unaffected: crew launch must not carry an --effort flag"
|
|
978
|
+
pass "C9 spawn: the harness fallback chain still resolves with no tokens; crew/scout launches are unaffected by this feature"
|
|
979
|
+
}
|
|
980
|
+
|
|
981
|
+
# ===========================================================================
|
|
982
|
+
# B integration: spawn, bootstrap, and config push propagate inherited local
|
|
983
|
+
# material and keep it converged on the primary (independent of tracked-file ff
|
|
984
|
+
# status).
|
|
985
|
+
# ===========================================================================
|
|
986
|
+
|
|
987
|
+
# A PRIMARY firstmate repo on main with one commit + a home dir, mirroring the
|
|
988
|
+
# real gitignore (config/crew-harness ignored, so a propagated value never dirties
|
|
989
|
+
# the secondmate worktree on a later sweep). Echoes the world dir.
|
|
990
|
+
new_world() {
|
|
991
|
+
local name=$1 dispatch_ignore=${2:-yes} w
|
|
992
|
+
w="$TMP_ROOT/$name"
|
|
993
|
+
mkdir -p "$w/home/state" "$w/home/data" "$w/home/config"
|
|
994
|
+
touch "$w/home/state/.last-watcher-beat"
|
|
995
|
+
git init -q -b main "$w/main"
|
|
996
|
+
{
|
|
997
|
+
printf 'projects/\nstate/\ndata/\n.no-mistakes/\n'
|
|
998
|
+
[ "$dispatch_ignore" = no ] || printf 'config/crew-dispatch.json\n'
|
|
999
|
+
printf 'config/crew-harness\nconfig/secondmate-harness\nconfig/backlog-backend\n'
|
|
1000
|
+
printf 'config/backend\nconfig/herdr-presentation-spaces\nconfig/startup-memory-budget\n'
|
|
1001
|
+
} > "$w/main/.gitignore"
|
|
1002
|
+
printf 'v1\n' > "$w/main/AGENTS.md"
|
|
1003
|
+
printf 'r1\n' > "$w/main/README.md"
|
|
1004
|
+
mkdir -p "$w/main/bin"
|
|
1005
|
+
printf 'echo a\n' > "$w/main/bin/tool.sh"
|
|
1006
|
+
git -C "$w/main" add -A
|
|
1007
|
+
git -C "$w/main" commit -qm c1
|
|
1008
|
+
printf '%s\n' "$w"
|
|
1009
|
+
}
|
|
1010
|
+
|
|
1011
|
+
record_live_watcher_fixture() {
|
|
1012
|
+
local home=$1 identity
|
|
1013
|
+
identity=$(FM_STATE_OVERRIDE="$home/state" bash -c '. "$1"; fm_pid_identity "$2"' _ \
|
|
1014
|
+
"$ROOT/bin/fm-wake-lib.sh" "$$") || fail "could not identify the live watcher fixture"
|
|
1015
|
+
mkdir "$home/state/.watch.lock"
|
|
1016
|
+
printf '%s\n' "$$" > "$home/state/.watch.lock/pid"
|
|
1017
|
+
printf '%s\n' "$home" > "$home/state/.watch.lock/fm-home"
|
|
1018
|
+
printf '%s\n' "$ROOT/bin/fm-watch.sh" > "$home/state/.watch.lock/watcher-path"
|
|
1019
|
+
printf '%s\n' "$identity" > "$home/state/.watch.lock/pid-identity"
|
|
1020
|
+
touch "$home/state/.last-watcher-beat"
|
|
1021
|
+
}
|
|
1022
|
+
|
|
1023
|
+
# A live secondmate home as a DETACHED worktree of the primary at <commit>, with
|
|
1024
|
+
# its seed marker and a live kind=secondmate meta.
|
|
1025
|
+
add_sm_worktree() {
|
|
1026
|
+
local w=$1 id=$2 commit=$3
|
|
1027
|
+
git -C "$w/main" worktree add -q --detach "$w/$id" "$commit"
|
|
1028
|
+
printf '%s\n' "$id" > "$w/$id/.fm-secondmate-home"
|
|
1029
|
+
{
|
|
1030
|
+
printf 'window=firstmate:fm-%s\n' "$id"
|
|
1031
|
+
printf 'kind=secondmate\n'
|
|
1032
|
+
printf 'home=%s/%s\n' "$w" "$id"
|
|
1033
|
+
} > "$w/home/state/$id.meta"
|
|
1034
|
+
}
|
|
1035
|
+
|
|
1036
|
+
make_fake_toolchain() {
|
|
1037
|
+
local dir=$1 fakebin
|
|
1038
|
+
fakebin="$dir/fakebin"
|
|
1039
|
+
mkdir -p "$fakebin"
|
|
1040
|
+
fm_fake_exit0 "$fakebin" node chrome-devtools-axi
|
|
1041
|
+
fm_fake_version_tool "$fakebin" lavish-axi FM_FAKE_LAVISH_AXI_VERSION 0.1.46
|
|
1042
|
+
cat > "$fakebin/gh-axi" <<'SH'
|
|
1043
|
+
#!/usr/bin/env bash
|
|
1044
|
+
if [ "${1:-}" = --version ]; then
|
|
1045
|
+
printf '%s\n' '0.1.29'
|
|
1046
|
+
exit 0
|
|
1047
|
+
fi
|
|
1048
|
+
exit 0
|
|
1049
|
+
SH
|
|
1050
|
+
chmod +x "$fakebin/gh-axi"
|
|
1051
|
+
# tmux fake supports fm-send's composer-verified submit path and optional
|
|
1052
|
+
# FM_FAKE_TMUX_LOG / FM_FAKE_TMUX_FAIL_LITERAL for reread-nudge assertions.
|
|
1053
|
+
cat > "$fakebin/tmux" <<'SH'
|
|
1054
|
+
#!/usr/bin/env bash
|
|
1055
|
+
if [ -n "${FM_FAKE_TMUX_LOG:-}" ]; then
|
|
1056
|
+
printf '%s\n' "$*" >> "$FM_FAKE_TMUX_LOG"
|
|
1057
|
+
fi
|
|
1058
|
+
case "$*" in
|
|
1059
|
+
*display-message*'#{pane_current_command}'*) printf '%s\n' codex; exit 0 ;;
|
|
1060
|
+
*display-message*'#{pane_id}'*) printf '%s\n' '%1'; exit 0 ;;
|
|
1061
|
+
*display-message*'#{cursor_y}'*) printf '%s\n' 0; exit 0 ;;
|
|
1062
|
+
*capture-pane*) printf '❯\n'; exit 0 ;;
|
|
1063
|
+
*'send-keys'*' -l '*)
|
|
1064
|
+
[ "${FM_FAKE_TMUX_FAIL_LITERAL:-0}" = 1 ] && exit 1
|
|
1065
|
+
exit 0
|
|
1066
|
+
;;
|
|
1067
|
+
*send-keys*)
|
|
1068
|
+
[ "${FM_FAKE_TMUX_FAIL_LITERAL:-0}" = 1 ] && exit 1
|
|
1069
|
+
exit 0
|
|
1070
|
+
;;
|
|
1071
|
+
esac
|
|
1072
|
+
exit 0
|
|
1073
|
+
SH
|
|
1074
|
+
chmod +x "$fakebin/tmux"
|
|
1075
|
+
cat > "$fakebin/gh" <<'SH'
|
|
1076
|
+
#!/usr/bin/env bash
|
|
1077
|
+
exit 0
|
|
1078
|
+
SH
|
|
1079
|
+
chmod +x "$fakebin/gh"
|
|
1080
|
+
cat > "$fakebin/treehouse" <<'SH'
|
|
1081
|
+
#!/usr/bin/env bash
|
|
1082
|
+
if [ "${1:-}" = get ] && [ "${2:-}" = --help ]; then
|
|
1083
|
+
printf '%s\n' 'Usage: treehouse get [--lease]'
|
|
1084
|
+
fi
|
|
1085
|
+
exit 0
|
|
1086
|
+
SH
|
|
1087
|
+
chmod +x "$fakebin/treehouse"
|
|
1088
|
+
cat > "$fakebin/no-mistakes" <<'SH'
|
|
1089
|
+
#!/usr/bin/env bash
|
|
1090
|
+
if [ "${1:-}" = --version ]; then
|
|
1091
|
+
printf '%s\n' 'no-mistakes version v1.46.0 (fake)'
|
|
1092
|
+
exit 0
|
|
1093
|
+
fi
|
|
1094
|
+
exit 0
|
|
1095
|
+
SH
|
|
1096
|
+
chmod +x "$fakebin/no-mistakes"
|
|
1097
|
+
cat > "$fakebin/tasks-axi" <<'SH'
|
|
1098
|
+
#!/usr/bin/env bash
|
|
1099
|
+
case "${1:-} ${2:-}" in
|
|
1100
|
+
"--version ") printf '%s\n' '0.2.4' ;;
|
|
1101
|
+
"update --help") printf '%s\n' 'usage: tasks-axi update <id> [flags]' ' --archive-body' ;;
|
|
1102
|
+
"mv --help") printf '%s\n' 'usage: tasks-axi mv <id> [<id>...] --to <path-or-dir>' ;;
|
|
1103
|
+
esac
|
|
1104
|
+
exit 0
|
|
1105
|
+
SH
|
|
1106
|
+
chmod +x "$fakebin/tasks-axi"
|
|
1107
|
+
cat > "$fakebin/quota-axi" <<'SH'
|
|
1108
|
+
#!/usr/bin/env bash
|
|
1109
|
+
if [ "${1:-}" = --version ]; then
|
|
1110
|
+
printf '%s\n' '0.1.29'
|
|
1111
|
+
exit 0
|
|
1112
|
+
fi
|
|
1113
|
+
exit 0
|
|
1114
|
+
SH
|
|
1115
|
+
chmod +x "$fakebin/quota-axi"
|
|
1116
|
+
printf '%s\n' "$fakebin"
|
|
1117
|
+
}
|
|
1118
|
+
|
|
1119
|
+
run_bootstrap() {
|
|
1120
|
+
local w=$1 fakebin log=${2:-}
|
|
1121
|
+
fakebin=$(make_fake_toolchain "$w")
|
|
1122
|
+
if [ -n "$log" ]; then
|
|
1123
|
+
PATH="$fakebin:$BASE_PATH" FM_HOME="$w/home" FM_ROOT_OVERRIDE="$w/main" \
|
|
1124
|
+
FM_SEND_SETTLE=0 FM_FAKE_TMUX_LOG="$log" \
|
|
1125
|
+
"$ROOT/bin/fm-bootstrap.sh" 2>/dev/null
|
|
1126
|
+
else
|
|
1127
|
+
PATH="$fakebin:$BASE_PATH" FM_HOME="$w/home" FM_ROOT_OVERRIDE="$w/main" \
|
|
1128
|
+
FM_SEND_SETTLE=0 "$ROOT/bin/fm-bootstrap.sh" 2>/dev/null
|
|
1129
|
+
fi
|
|
1130
|
+
}
|
|
1131
|
+
|
|
1132
|
+
run_config_push() {
|
|
1133
|
+
local w=$1 fakebin log=${2:-}
|
|
1134
|
+
fakebin=$(make_fake_toolchain "$w")
|
|
1135
|
+
if [ -n "$log" ]; then
|
|
1136
|
+
PATH="$fakebin:$BASE_PATH" FM_HOME="$w/home" FM_ROOT_OVERRIDE="$w/main" \
|
|
1137
|
+
FM_SEND_SETTLE=0 FM_FAKE_TMUX_LOG="$log" \
|
|
1138
|
+
"$ROOT/bin/fm-config-push.sh"
|
|
1139
|
+
else
|
|
1140
|
+
PATH="$fakebin:$BASE_PATH" FM_HOME="$w/home" FM_ROOT_OVERRIDE="$w/main" \
|
|
1141
|
+
FM_SEND_SETTLE=0 \
|
|
1142
|
+
"$ROOT/bin/fm-config-push.sh"
|
|
1143
|
+
fi
|
|
1144
|
+
}
|
|
1145
|
+
|
|
1146
|
+
# Config-reread pointers now ride fm-send's durable steering inbox: the typed
|
|
1147
|
+
# channel carries only the constant doorbell, while each pointer message is a
|
|
1148
|
+
# sequenced record under the parent state's <task>.inbox/. Print every recorded
|
|
1149
|
+
# steer body in sequence order (one line per pointer-only message), read
|
|
1150
|
+
# through the production owner so no format knowledge is duplicated here.
|
|
1151
|
+
inbox_stream() { # <parent-state-dir> <task-id>
|
|
1152
|
+
local rec
|
|
1153
|
+
for rec in "$1/$2.inbox"/*.msg; do
|
|
1154
|
+
[ -e "$rec" ] || continue
|
|
1155
|
+
bash -c '. "$1"; fm_task_inbox_body "$2"' _ "$ROOT/bin/fm-task-inbox-lib.sh" "$rec"
|
|
1156
|
+
printf '\n'
|
|
1157
|
+
done
|
|
1158
|
+
}
|
|
1159
|
+
|
|
1160
|
+
reread_instruction_path() {
|
|
1161
|
+
local home=$1 state path latest=
|
|
1162
|
+
state="$(cd "$home/state" && pwd -P)"
|
|
1163
|
+
for path in "$state"/.fm-inherited-config-reread.*; do
|
|
1164
|
+
case "$path" in
|
|
1165
|
+
*.pending) continue ;;
|
|
1166
|
+
esac
|
|
1167
|
+
[ -f "$path" ] && [ ! -L "$path" ] || continue
|
|
1168
|
+
latest="$path"
|
|
1169
|
+
done
|
|
1170
|
+
[ -n "$latest" ] || return 1
|
|
1171
|
+
printf '%s\n' "$latest"
|
|
1172
|
+
}
|
|
1173
|
+
|
|
1174
|
+
reread_pending_path() {
|
|
1175
|
+
printf '%s.pending\n' "$(reread_instruction_path "$1")"
|
|
1176
|
+
}
|
|
1177
|
+
|
|
1178
|
+
reread_retry_stage_path() {
|
|
1179
|
+
local home=$1 id=$2 retry_dir path latest=
|
|
1180
|
+
retry_dir="$home/state/.fm-inherited-config-reread-retry/$id"
|
|
1181
|
+
for path in "$retry_dir"/.fm-inherited-config-reread.*; do
|
|
1182
|
+
[ -f "$path" ] && [ ! -L "$path" ] || continue
|
|
1183
|
+
latest="$path"
|
|
1184
|
+
done
|
|
1185
|
+
[ -n "$latest" ] || return 1
|
|
1186
|
+
printf '%s\n' "$latest"
|
|
1187
|
+
}
|
|
1188
|
+
|
|
1189
|
+
reread_retry_report_path() {
|
|
1190
|
+
local home=$1 id=$2 retry_dir path latest=
|
|
1191
|
+
retry_dir="$home/state/.fm-inherited-config-reread-retry/$id"
|
|
1192
|
+
for path in "$retry_dir"/.fm-inherited-config-reread.*.report; do
|
|
1193
|
+
[ -f "$path" ] && [ ! -L "$path" ] || continue
|
|
1194
|
+
latest="$path"
|
|
1195
|
+
done
|
|
1196
|
+
[ -n "$latest" ] || return 1
|
|
1197
|
+
printf '%s\n' "$latest"
|
|
1198
|
+
}
|
|
1199
|
+
|
|
1200
|
+
reread_mode() {
|
|
1201
|
+
if [ "$(uname)" = Darwin ]; then
|
|
1202
|
+
stat -f %Lp "$1"
|
|
1203
|
+
else
|
|
1204
|
+
stat -c %a "$1"
|
|
1205
|
+
fi
|
|
1206
|
+
}
|
|
1207
|
+
|
|
1208
|
+
assert_no_reread_instructions() {
|
|
1209
|
+
local home=$1 state path
|
|
1210
|
+
state="$home/state"
|
|
1211
|
+
for path in "$state"/.fm-inherited-config-reread.*; do
|
|
1212
|
+
case "$path" in
|
|
1213
|
+
*.pending) continue ;;
|
|
1214
|
+
esac
|
|
1215
|
+
[ -f "$path" ] || [ -L "$path" ] || continue
|
|
1216
|
+
fail "unexpected config reread instruction: $path"
|
|
1217
|
+
done
|
|
1218
|
+
}
|
|
1219
|
+
|
|
1220
|
+
assert_no_reread_pending() {
|
|
1221
|
+
local home=$1 state path
|
|
1222
|
+
state="$home/state"
|
|
1223
|
+
for path in "$state"/.fm-inherited-config-reread.*.pending; do
|
|
1224
|
+
[ -e "$path" ] || [ -L "$path" ] || continue
|
|
1225
|
+
fail "unexpected pending config reread marker: $path"
|
|
1226
|
+
done
|
|
1227
|
+
}
|
|
1228
|
+
|
|
1229
|
+
assert_no_reread_retry_stages() {
|
|
1230
|
+
local home=$1 id=$2 retry_dir path
|
|
1231
|
+
retry_dir="$home/state/.fm-inherited-config-reread-retry/$id"
|
|
1232
|
+
for path in "$retry_dir"/.fm-inherited-config-reread.*; do
|
|
1233
|
+
[ -e "$path" ] || [ -L "$path" ] || continue
|
|
1234
|
+
fail "unexpected staged config reread retry: $path"
|
|
1235
|
+
done
|
|
1236
|
+
}
|
|
1237
|
+
|
|
1238
|
+
# The sweep pushes the primary's declared inherited config into a live home,
|
|
1239
|
+
# re-converges it when the primary changes it, and mirrors absence when the
|
|
1240
|
+
# primary clears it - all while never inheriting secondmate-harness.
|
|
1241
|
+
test_bootstrap_sweep_propagates_and_reconverges() {
|
|
1242
|
+
local w c1
|
|
1243
|
+
w=$(new_world boot-prop)
|
|
1244
|
+
c1=$(git -C "$w/main" rev-parse HEAD)
|
|
1245
|
+
add_sm_worktree "$w" sm "$c1"
|
|
1246
|
+
|
|
1247
|
+
# Initial push: primary crew-harness=codex, secondmate-harness=grok (must NOT flow).
|
|
1248
|
+
printf '{"default":{"harness":"codex"}}\n' > "$w/home/config/crew-dispatch.json"
|
|
1249
|
+
printf 'codex\n' > "$w/home/config/crew-harness"
|
|
1250
|
+
printf 'manual\n' > "$w/home/config/backlog-backend"
|
|
1251
|
+
printf 'tmux\n' > "$w/home/config/backend"
|
|
1252
|
+
: > "$w/home/config/trace-context"
|
|
1253
|
+
printf 'grok\n' > "$w/home/config/secondmate-harness"
|
|
1254
|
+
run_bootstrap "$w" >/dev/null
|
|
1255
|
+
[ "$(cat "$w/sm/config/crew-harness" 2>/dev/null)" = codex ] \
|
|
1256
|
+
|| fail "sweep: crew-harness not pushed into the live home"
|
|
1257
|
+
[ "$(cat "$w/sm/config/crew-dispatch.json" 2>/dev/null)" = '{"default":{"harness":"codex"}}' ] \
|
|
1258
|
+
|| fail "sweep: crew-dispatch.json not pushed into the live home"
|
|
1259
|
+
[ "$(cat "$w/sm/config/backlog-backend" 2>/dev/null)" = manual ] \
|
|
1260
|
+
|| fail "sweep: backlog-backend not pushed into the live home"
|
|
1261
|
+
[ "$(cat "$w/sm/config/backend" 2>/dev/null)" = tmux ] \
|
|
1262
|
+
|| fail "sweep: backend not pushed into the live home"
|
|
1263
|
+
[ ! -e "$w/sm/config/trace-context" ] \
|
|
1264
|
+
|| fail "sweep: trace-context changed a legacy live home before relaunch"
|
|
1265
|
+
[ -e "$w/sm/config/secondmate-harness" ] \
|
|
1266
|
+
&& fail "sweep: secondmate-harness was inherited (must not be)"
|
|
1267
|
+
|
|
1268
|
+
# Re-converge: primary changes inherited config values; the home follows on the next sweep.
|
|
1269
|
+
printf '{"default":{"harness":"claude"}}\n' > "$w/home/config/crew-dispatch.json"
|
|
1270
|
+
printf 'claude\n' > "$w/home/config/crew-harness"
|
|
1271
|
+
printf 'tasks-axi\n' > "$w/home/config/backlog-backend"
|
|
1272
|
+
printf 'zellij\n' > "$w/home/config/backend"
|
|
1273
|
+
run_bootstrap "$w" >/dev/null
|
|
1274
|
+
[ "$(cat "$w/sm/config/crew-harness" 2>/dev/null)" = claude ] \
|
|
1275
|
+
|| fail "sweep: home did not re-converge to the primary's new crew-harness"
|
|
1276
|
+
[ "$(cat "$w/sm/config/crew-dispatch.json" 2>/dev/null)" = '{"default":{"harness":"claude"}}' ] \
|
|
1277
|
+
|| fail "sweep: home did not re-converge to the primary's new crew-dispatch.json"
|
|
1278
|
+
[ "$(cat "$w/sm/config/backlog-backend" 2>/dev/null)" = tasks-axi ] \
|
|
1279
|
+
|| fail "sweep: home did not re-converge to the primary's new backlog-backend"
|
|
1280
|
+
[ "$(cat "$w/sm/config/backend" 2>/dev/null)" = zellij ] \
|
|
1281
|
+
|| fail "sweep: home did not re-converge to the primary's new backend"
|
|
1282
|
+
|
|
1283
|
+
# Mirror absence: primary clears inherited config; the home's copies are removed.
|
|
1284
|
+
rm -f "$w/home/config/crew-dispatch.json" "$w/home/config/crew-harness" \
|
|
1285
|
+
"$w/home/config/backlog-backend" "$w/home/config/backend"
|
|
1286
|
+
run_bootstrap "$w" >/dev/null
|
|
1287
|
+
[ -e "$w/sm/config/crew-dispatch.json" ] \
|
|
1288
|
+
&& fail "sweep: home crew-dispatch.json not removed after the primary cleared it"
|
|
1289
|
+
[ -e "$w/sm/config/crew-harness" ] \
|
|
1290
|
+
&& fail "sweep: home crew-harness not removed after the primary cleared it"
|
|
1291
|
+
[ -e "$w/sm/config/backlog-backend" ] \
|
|
1292
|
+
&& fail "sweep: home backlog-backend not removed after the primary cleared it"
|
|
1293
|
+
[ -e "$w/sm/config/backend" ] \
|
|
1294
|
+
&& fail "sweep: home backend not removed after the primary cleared it"
|
|
1295
|
+
pass "B7 bootstrap sweep pushes, re-converges, and mirrors absence; never inherits secondmate-harness"
|
|
1296
|
+
}
|
|
1297
|
+
|
|
1298
|
+
# Convergence is independent of the tracked-files fast-forward: a home already
|
|
1299
|
+
# current on tracked files still receives a config change.
|
|
1300
|
+
test_bootstrap_sweep_propagates_when_tracked_current() {
|
|
1301
|
+
local w head
|
|
1302
|
+
w=$(new_world boot-prop-current)
|
|
1303
|
+
head=$(git -C "$w/main" rev-parse HEAD)
|
|
1304
|
+
add_sm_worktree "$w" sm "$head" # already on the primary's HEAD (ff is a no-op)
|
|
1305
|
+
|
|
1306
|
+
printf '{"default":{"harness":"codex"}}\n' > "$w/home/config/crew-dispatch.json"
|
|
1307
|
+
printf 'codex\n' > "$w/home/config/crew-harness"
|
|
1308
|
+
printf 'manual\n' > "$w/home/config/backlog-backend"
|
|
1309
|
+
printf 'tmux\n' > "$w/home/config/backend"
|
|
1310
|
+
run_bootstrap "$w" >/dev/null
|
|
1311
|
+
[ "$(cat "$w/sm/config/crew-dispatch.json" 2>/dev/null)" = '{"default":{"harness":"codex"}}' ] \
|
|
1312
|
+
|| fail "crew-dispatch.json did not propagate to a tracked-current home"
|
|
1313
|
+
[ "$(cat "$w/sm/config/crew-harness" 2>/dev/null)" = codex ] \
|
|
1314
|
+
|| fail "config did not propagate to a tracked-current home"
|
|
1315
|
+
[ "$(cat "$w/sm/config/backlog-backend" 2>/dev/null)" = manual ] \
|
|
1316
|
+
|| fail "backlog-backend did not propagate to a tracked-current home"
|
|
1317
|
+
[ "$(cat "$w/sm/config/backend" 2>/dev/null)" = tmux ] \
|
|
1318
|
+
|| fail "backend did not propagate to a tracked-current home"
|
|
1319
|
+
pass "B8 bootstrap sweep propagates config even when the home's tracked files are already current"
|
|
1320
|
+
}
|
|
1321
|
+
|
|
1322
|
+
test_bootstrap_sweep_defers_dispatch_on_stale_unignored_home() {
|
|
1323
|
+
local w out status
|
|
1324
|
+
w=$(new_world boot-stale-dispatch no)
|
|
1325
|
+
add_sm_worktree "$w" sm "$(git -C "$w/main" rev-parse HEAD)"
|
|
1326
|
+
printf 'local divergence\n' >> "$w/sm/README.md"
|
|
1327
|
+
git -C "$w/sm" add README.md
|
|
1328
|
+
git -C "$w/sm" commit -qm local
|
|
1329
|
+
printf 'config/crew-dispatch.json\n' >> "$w/main/.gitignore"
|
|
1330
|
+
git -C "$w/main" add .gitignore
|
|
1331
|
+
git -C "$w/main" commit -qm c2
|
|
1332
|
+
|
|
1333
|
+
printf '{"default":{"harness":"codex"}}\n' > "$w/home/config/crew-dispatch.json"
|
|
1334
|
+
printf 'codex\n' > "$w/home/config/crew-harness"
|
|
1335
|
+
printf 'manual\n' > "$w/home/config/backlog-backend"
|
|
1336
|
+
out=$(run_bootstrap "$w")
|
|
1337
|
+
|
|
1338
|
+
assert_contains "$out" "SECONDMATE_SYNC: secondmate sm: skipped: diverged from" \
|
|
1339
|
+
"stale dispatch: expected fast-forward skip"
|
|
1340
|
+
[ ! -e "$w/sm/config/crew-dispatch.json" ] \
|
|
1341
|
+
|| fail "stale dispatch: crew-dispatch.json was copied before the home ignored it"
|
|
1342
|
+
[ "$(cat "$w/sm/config/crew-harness" 2>/dev/null)" = codex ] \
|
|
1343
|
+
|| fail "stale dispatch: existing ignored config stopped propagating"
|
|
1344
|
+
[ "$(cat "$w/sm/config/backlog-backend" 2>/dev/null)" = manual ] \
|
|
1345
|
+
|| fail "stale dispatch: backlog backend stopped propagating"
|
|
1346
|
+
status=$(git -C "$w/sm" status --porcelain -- config/crew-dispatch.json)
|
|
1347
|
+
[ -z "$status" ] || fail "stale dispatch: crew-dispatch.json dirtied the home: $status"
|
|
1348
|
+
pass "B9 bootstrap sweep defers new inherited config until the home ignores it"
|
|
1349
|
+
}
|
|
1350
|
+
|
|
1351
|
+
# The primary bootstrap always materializes the startup-memory default, so an
|
|
1352
|
+
# otherwise empty inherited surface converges that one visible value while
|
|
1353
|
+
# ordinary tracked-file fast-forward behavior remains unchanged.
|
|
1354
|
+
test_bootstrap_sweep_materializes_and_inherits_memory_default() {
|
|
1355
|
+
local w c1
|
|
1356
|
+
w=$(new_world boot-noop)
|
|
1357
|
+
c1=$(git -C "$w/main" rev-parse HEAD)
|
|
1358
|
+
add_sm_worktree "$w" sm "$c1"
|
|
1359
|
+
# Advance the primary so the sweep has a real fast-forward to perform.
|
|
1360
|
+
printf 'v2\n' > "$w/main/AGENTS.md"
|
|
1361
|
+
git -C "$w/main" add -A
|
|
1362
|
+
git -C "$w/main" commit -qm c2
|
|
1363
|
+
local head
|
|
1364
|
+
head=$(git -C "$w/main" rev-parse HEAD)
|
|
1365
|
+
|
|
1366
|
+
run_bootstrap "$w" >/dev/null
|
|
1367
|
+
|
|
1368
|
+
[ -e "$w/sm/config/crew-dispatch.json" ] && fail "default-only sweep created a home crew-dispatch.json"
|
|
1369
|
+
[ -e "$w/sm/config/crew-harness" ] && fail "default-only sweep created a home crew-harness"
|
|
1370
|
+
[ -e "$w/sm/config/backend" ] && fail "default-only sweep created a home backend"
|
|
1371
|
+
[ "$(cat "$w/home/config/startup-memory-budget")" = 7500 ] \
|
|
1372
|
+
|| fail "primary bootstrap did not materialize the startup-memory default"
|
|
1373
|
+
[ "$(cat "$w/sm/config/startup-memory-budget")" = 7500 ] \
|
|
1374
|
+
|| fail "default-only sweep did not converge startup-memory-budget"
|
|
1375
|
+
[ "$(git -C "$w/sm" rev-parse HEAD)" = "$head" ] \
|
|
1376
|
+
|| fail "default-only sweep did not still fast-forward the tracked files"
|
|
1377
|
+
pass "B10 bootstrap sweep materializes and inherits the startup-memory default while fast-forwarding"
|
|
1378
|
+
}
|
|
1379
|
+
|
|
1380
|
+
# config/backend: present and absent primary state converges exactly.
|
|
1381
|
+
test_backend_inheritance_present_and_absent() {
|
|
1382
|
+
local w head out err status instruction
|
|
1383
|
+
w=$(new_world backend-inherit)
|
|
1384
|
+
head=$(git -C "$w/main" rev-parse HEAD)
|
|
1385
|
+
add_sm_worktree "$w" sm "$head"
|
|
1386
|
+
|
|
1387
|
+
printf 'tmux\n' > "$w/home/config/backend"
|
|
1388
|
+
err="$w/backend-inherit.err"
|
|
1389
|
+
out=$(run_config_push "$w" 2>"$err"); status=$?
|
|
1390
|
+
expect_code 0 "$status" "backend present push should succeed"
|
|
1391
|
+
assert_contains "$out" "backend: pushed" "backend present value should report pushed"
|
|
1392
|
+
[ "$(cat "$w/sm/config/backend")" = tmux ] || fail "backend present value not pushed"
|
|
1393
|
+
instruction=$(reread_instruction_path "$w/sm") || fail "backend present reread instruction missing"
|
|
1394
|
+
assert_contains "$(cat "$instruction")" $'-----BEGIN config/backend-----\ntmux\n-----END config/backend-----' \
|
|
1395
|
+
"backend present reread must include exact bytes"
|
|
1396
|
+
|
|
1397
|
+
printf 'herdr\n' > "$w/sm/config/backend"
|
|
1398
|
+
printf 'zellij\n' > "$w/home/config/backend"
|
|
1399
|
+
out=$(run_config_push "$w" 2>"$err"); status=$?
|
|
1400
|
+
expect_code 0 "$status" "backend changed push should succeed"
|
|
1401
|
+
assert_contains "$out" "backend: pushed" "backend changed value should report pushed"
|
|
1402
|
+
[ "$(cat "$w/sm/config/backend")" = zellij ] \
|
|
1403
|
+
|| fail "primary backend did not overwrite the divergent destination"
|
|
1404
|
+
|
|
1405
|
+
rm -f "$w/home/config/backend"
|
|
1406
|
+
out=$(run_config_push "$w" 2>"$err"); status=$?
|
|
1407
|
+
expect_code 0 "$status" "backend absence push should succeed"
|
|
1408
|
+
assert_contains "$out" "backend: pushed - mirrored primary absence" "backend should mirror primary absence"
|
|
1409
|
+
[ -e "$w/sm/config/backend" ] && fail "backend not removed on primary absence"
|
|
1410
|
+
instruction=$(reread_instruction_path "$w/sm") || fail "backend absence reread instruction missing"
|
|
1411
|
+
assert_contains "$(cat "$instruction")" $'-----BEGIN config/backend-----\nABSENT\n-----END config/backend-----' \
|
|
1412
|
+
"backend absence reread must use ABSENT token"
|
|
1413
|
+
pass "B12b backend inheritance: present values and primary absence converge exactly"
|
|
1414
|
+
}
|
|
1415
|
+
|
|
1416
|
+
# config/herdr-presentation-spaces has an unconfigured default, so this item's
|
|
1417
|
+
# convergence is asserted through the preference the spawn gate actually reads
|
|
1418
|
+
# in the destination home, not through file presence alone: mirroring the primary's
|
|
1419
|
+
# absence must converge a secondmate to the same unconfigured default rather
|
|
1420
|
+
# than turning its projection off. The Herdr version floor that decides what
|
|
1421
|
+
# that default resolves to is a property of the running release, not of
|
|
1422
|
+
# inheritance, so it is pinned in tests/fm-backend-herdr.test.sh instead.
|
|
1423
|
+
sm_presentation_verdict() { # <config-dir> -> on|off
|
|
1424
|
+
bash -c '
|
|
1425
|
+
. "$0/bin/backends/herdr.sh"
|
|
1426
|
+
case "$(fm_backend_herdr_presentation_preference "$1")" in
|
|
1427
|
+
off) printf "off\n" ;;
|
|
1428
|
+
*) printf "on\n" ;;
|
|
1429
|
+
esac
|
|
1430
|
+
' "$ROOT" "$1" 2>/dev/null
|
|
1431
|
+
}
|
|
1432
|
+
|
|
1433
|
+
test_presentation_inheritance_default_on_and_opt_out() {
|
|
1434
|
+
local w head out err status verdict
|
|
1435
|
+
w=$(new_world presentation-inherit)
|
|
1436
|
+
head=$(git -C "$w/main" rev-parse HEAD)
|
|
1437
|
+
add_sm_worktree "$w" sm "$head"
|
|
1438
|
+
err="$w/presentation-inherit.err"
|
|
1439
|
+
|
|
1440
|
+
out=$(run_config_push "$w" 2>"$err"); status=$?
|
|
1441
|
+
expect_code 0 "$status" "presentation default push should succeed"
|
|
1442
|
+
[ -e "$w/sm/config/herdr-presentation-spaces" ] \
|
|
1443
|
+
&& fail "primary default must not write an opt-out downstream"
|
|
1444
|
+
verdict=$(sm_presentation_verdict "$w/sm/config")
|
|
1445
|
+
[ "$verdict" = on ] || fail "primary default left the secondmate projection $verdict"
|
|
1446
|
+
|
|
1447
|
+
mkdir -p "$w/sm/config"
|
|
1448
|
+
printf 'off\n' > "$w/sm/config/herdr-presentation-spaces"
|
|
1449
|
+
out=$(run_config_push "$w" 2>"$err"); status=$?
|
|
1450
|
+
expect_code 0 "$status" "presentation reconverge push should succeed"
|
|
1451
|
+
assert_contains "$out" "herdr-presentation-spaces: pushed - mirrored primary absence" \
|
|
1452
|
+
"a local secondmate opt-out should reconverge on the primary default"
|
|
1453
|
+
verdict=$(sm_presentation_verdict "$w/sm/config")
|
|
1454
|
+
[ "$verdict" = on ] || fail "primary default did not reconverge a locally opted-out secondmate ($verdict)"
|
|
1455
|
+
|
|
1456
|
+
printf 'off\n' > "$w/home/config/herdr-presentation-spaces"
|
|
1457
|
+
out=$(run_config_push "$w" 2>"$err"); status=$?
|
|
1458
|
+
expect_code 0 "$status" "presentation opt-out push should succeed"
|
|
1459
|
+
assert_contains "$out" "herdr-presentation-spaces: pushed" "explicit opt-out should report pushed"
|
|
1460
|
+
verdict=$(sm_presentation_verdict "$w/sm/config")
|
|
1461
|
+
[ "$verdict" = off ] || fail "explicit primary opt-out left the secondmate projection $verdict"
|
|
1462
|
+
|
|
1463
|
+
: > "$w/home/config/herdr-presentation-spaces"
|
|
1464
|
+
out=$(run_config_push "$w" 2>"$err"); status=$?
|
|
1465
|
+
expect_code 0 "$status" "presentation legacy opt-in push should succeed"
|
|
1466
|
+
verdict=$(sm_presentation_verdict "$w/sm/config")
|
|
1467
|
+
[ "$verdict" = on ] || fail "a legacy primary opt-in file left the secondmate projection $verdict"
|
|
1468
|
+
pass "B12c presentation inheritance: the primary default converges on, and only an explicit opt-out propagates off"
|
|
1469
|
+
}
|
|
1470
|
+
|
|
1471
|
+
test_bootstrap_sweep_surfaces_config_propagation_failure() {
|
|
1472
|
+
local w c1 out fail_line
|
|
1473
|
+
w=$(new_world boot-prop-fail)
|
|
1474
|
+
c1=$(git -C "$w/main" rev-parse HEAD)
|
|
1475
|
+
add_sm_worktree "$w" sm "$c1"
|
|
1476
|
+
mkdir -p "$w/sm/config/crew-harness"
|
|
1477
|
+
|
|
1478
|
+
out=$(run_bootstrap "$w")
|
|
1479
|
+
|
|
1480
|
+
fail_line=$(printf '%s\n' "$out" | grep '^SECONDMATE_SYNC: secondmate sm: skipped: inheritance failed' || true)
|
|
1481
|
+
[ -n "$fail_line" ] || fail "bootstrap did not surface inheritance propagation failure (got: $out)"
|
|
1482
|
+
[ -d "$w/sm/config/crew-harness" ] || fail "failed propagation removed the wrong path"
|
|
1483
|
+
pass "B11 bootstrap sweep surfaces config propagation failures"
|
|
1484
|
+
}
|
|
1485
|
+
|
|
1486
|
+
test_bootstrap_rereads_after_partial_propagation() {
|
|
1487
|
+
local w head log out instruction pointer
|
|
1488
|
+
w=$(new_world boot-prop-partial)
|
|
1489
|
+
head=$(git -C "$w/main" rev-parse HEAD)
|
|
1490
|
+
add_sm_worktree "$w" sm "$head"
|
|
1491
|
+
printf '{"default":{"harness":"codex"}}\n' > "$w/home/config/crew-dispatch.json"
|
|
1492
|
+
printf 'invalid shared header\n' > "$w/home/data/captain-shared.md"
|
|
1493
|
+
log="$w/boot-prop-partial.tmux.log"
|
|
1494
|
+
|
|
1495
|
+
out=$(run_bootstrap "$w" "$log")
|
|
1496
|
+
assert_contains "$out" "SECONDMATE_SYNC: secondmate sm: skipped: inheritance failed" \
|
|
1497
|
+
"partial bootstrap propagation did not remain diagnostic"
|
|
1498
|
+
[ "$(cat "$w/sm/config/crew-dispatch.json")" = '{"default":{"harness":"codex"}}' ] \
|
|
1499
|
+
|| fail "partial bootstrap propagation did not retain the completed config write"
|
|
1500
|
+
instruction=$(reread_instruction_path "$w/sm") || fail "partial bootstrap reread instruction missing"
|
|
1501
|
+
assert_present "$instruction" "partial bootstrap propagation did not write a reread instruction"
|
|
1502
|
+
pointer="CONFIG_REREAD: $(reread_instruction_path "$w/sm")"
|
|
1503
|
+
assert_contains "$(inbox_stream "$w/home/state" sm)" "$pointer" \
|
|
1504
|
+
"partial bootstrap propagation did not route the instruction pointer"
|
|
1505
|
+
pass "B11 bootstrap rereads completed config writes after partial propagation"
|
|
1506
|
+
}
|
|
1507
|
+
|
|
1508
|
+
test_config_push_propagates_reports_without_ff_or_nudge() {
|
|
1509
|
+
local w c1 sm_real old_head out err status out2 tmp log instruction
|
|
1510
|
+
w=$(new_world config-push-basic)
|
|
1511
|
+
c1=$(git -C "$w/main" rev-parse HEAD)
|
|
1512
|
+
add_sm_worktree "$w" sm "$c1"
|
|
1513
|
+
sm_real=$(cd "$w/sm" && pwd -P)
|
|
1514
|
+
printf -- '- sm - config push target (home: %s; scope: config; projects: alpha; added 2026-06-30)\n' "$sm_real" > "$w/home/data/secondmates.md"
|
|
1515
|
+
tmp="$w/home/state/sm.meta.tmp"
|
|
1516
|
+
grep -v '^home=' "$w/home/state/sm.meta" > "$tmp"
|
|
1517
|
+
mv "$tmp" "$w/home/state/sm.meta"
|
|
1518
|
+
|
|
1519
|
+
printf 'v2\n' > "$w/main/AGENTS.md"
|
|
1520
|
+
git -C "$w/main" add AGENTS.md
|
|
1521
|
+
git -C "$w/main" commit -qm c2
|
|
1522
|
+
old_head=$(git -C "$w/sm" rev-parse HEAD)
|
|
1523
|
+
|
|
1524
|
+
printf '{"default":{"harness":"codex"}}\n' > "$w/home/config/crew-dispatch.json"
|
|
1525
|
+
printf 'codex\n' > "$w/home/config/crew-harness"
|
|
1526
|
+
printf 'manual\n' > "$w/home/config/backlog-backend"
|
|
1527
|
+
printf 'tmux\n' > "$w/home/config/backend"
|
|
1528
|
+
record_live_watcher_fixture "$w/home"
|
|
1529
|
+
: > "$w/home/config/trace-context"
|
|
1530
|
+
err="$w/config-push-basic.err"
|
|
1531
|
+
log="$w/config-push-basic.tmux.log"
|
|
1532
|
+
out=$(run_config_push "$w" "$log" 2>"$err"); status=$?
|
|
1533
|
+
|
|
1534
|
+
expect_code 0 "$status" "config push should succeed"
|
|
1535
|
+
assert_contains "$out" "config-push: $w/home -> live secondmate homes" \
|
|
1536
|
+
"config push lacked the header"
|
|
1537
|
+
assert_contains "$out" "secondmate sm ($sm_real):" \
|
|
1538
|
+
"config push did not discover the live secondmate through registry fallback"
|
|
1539
|
+
assert_contains "$out" "crew-dispatch.json: pushed" \
|
|
1540
|
+
"config push did not report crew-dispatch as pushed"
|
|
1541
|
+
assert_contains "$out" "crew-harness: pushed" \
|
|
1542
|
+
"config push did not report crew-harness as pushed"
|
|
1543
|
+
assert_contains "$out" "backlog-backend: pushed" \
|
|
1544
|
+
"config push did not report backlog-backend as pushed"
|
|
1545
|
+
assert_contains "$out" "backend: pushed" \
|
|
1546
|
+
"config push did not report backend as pushed"
|
|
1547
|
+
assert_contains "$out" "trace-context: unchanged" \
|
|
1548
|
+
"live config push must report trace-context as session-scoped and unchanged"
|
|
1549
|
+
[ ! -e "$w/sm/config/trace-context" ] \
|
|
1550
|
+
|| fail "live config push retroactively enabled trace context in a legacy secondmate home"
|
|
1551
|
+
assert_contains "$out" "config-reread: sent" \
|
|
1552
|
+
"config push with changed config must send a literal reread instruction"
|
|
1553
|
+
assert_not_contains "$out" "NUDGE_SECONDMATES" \
|
|
1554
|
+
"config push must not use the AGENTS.md instruction-surface nudge channel"
|
|
1555
|
+
[ "$(git -C "$w/sm" rev-parse HEAD)" = "$old_head" ] \
|
|
1556
|
+
|| fail "config push fast-forwarded tracked files"
|
|
1557
|
+
[ "$(cat "$w/sm/config/backend")" = tmux ] || fail "config push did not write backend"
|
|
1558
|
+
instruction=$(reread_instruction_path "$w/sm") || fail "config-push reread instruction missing"
|
|
1559
|
+
assert_contains "$(cat "$instruction")" $'-----BEGIN config/backend-----\ntmux\n-----END config/backend-----' \
|
|
1560
|
+
"config-push reread must include exact backend bytes"
|
|
1561
|
+
[ ! -s "$err" ] || fail "clean config push wrote unexpected stderr: $(cat "$err")"
|
|
1562
|
+
assert_contains "$(inbox_stream "$w/home/state" sm)" "[fm-from-firstmate]" \
|
|
1563
|
+
"config reread must use the marked routed secondmate path"
|
|
1564
|
+
|
|
1565
|
+
: > "$log"
|
|
1566
|
+
out2=$(run_config_push "$w" "$log" 2>"$err"); status=$?
|
|
1567
|
+
expect_code 0 "$status" "idempotent config push should succeed"
|
|
1568
|
+
assert_contains "$out2" "crew-dispatch.json: unchanged" \
|
|
1569
|
+
"idempotent config push did not report crew-dispatch as unchanged"
|
|
1570
|
+
assert_contains "$out2" "crew-harness: unchanged" \
|
|
1571
|
+
"idempotent config push did not report crew-harness as unchanged"
|
|
1572
|
+
assert_contains "$out2" "backlog-backend: unchanged" \
|
|
1573
|
+
"idempotent config push did not report backlog-backend as unchanged"
|
|
1574
|
+
assert_contains "$out2" "backend: unchanged" \
|
|
1575
|
+
"idempotent config push did not report backend as unchanged"
|
|
1576
|
+
assert_contains "$out2" "trace-context: unchanged" \
|
|
1577
|
+
"idempotent config push did not preserve session-scoped trace context"
|
|
1578
|
+
assert_not_contains "$out2" "config-reread: sent" \
|
|
1579
|
+
"unchanged config must not send a reread message"
|
|
1580
|
+
[ ! -s "$log" ] || fail "unchanged config push still invoked tmux send: $(cat "$log")"
|
|
1581
|
+
pass "B12 config-push propagates via shared live discovery, reports items, rereads on change only, and does not fast-forward"
|
|
1582
|
+
}
|
|
1583
|
+
|
|
1584
|
+
test_config_push_reports_skips_dirty_and_invalid_home() {
|
|
1585
|
+
local w head out err status stale_real dirty_real bad_home err_text tmp
|
|
1586
|
+
w=$(new_world config-push-warnings)
|
|
1587
|
+
head=$(git -C "$w/main" rev-parse HEAD)
|
|
1588
|
+
add_sm_worktree "$w" dirty "$head"
|
|
1589
|
+
add_sm_worktree "$w" stale "$head"
|
|
1590
|
+
dirty_real=$(cd "$w/dirty" && pwd -P)
|
|
1591
|
+
stale_real=$(cd "$w/stale" && pwd -P)
|
|
1592
|
+
|
|
1593
|
+
printf 'local edit\n' >> "$w/dirty/README.md"
|
|
1594
|
+
tmp="$w/stale/.gitignore.tmp"
|
|
1595
|
+
grep -v '^config/crew-dispatch.json$' "$w/stale/.gitignore" > "$tmp"
|
|
1596
|
+
mv "$tmp" "$w/stale/.gitignore"
|
|
1597
|
+
|
|
1598
|
+
bad_home="$w/not-secondmate"
|
|
1599
|
+
mkdir -p "$bad_home"
|
|
1600
|
+
{
|
|
1601
|
+
printf 'window=firstmate:fm-bad\n'
|
|
1602
|
+
printf 'kind=secondmate\n'
|
|
1603
|
+
printf 'home=%s\n' "$bad_home"
|
|
1604
|
+
} > "$w/home/state/bad.meta"
|
|
1605
|
+
|
|
1606
|
+
printf '{"default":{"harness":"codex"}}\n' > "$w/home/config/crew-dispatch.json"
|
|
1607
|
+
printf 'codex\n' > "$w/home/config/crew-harness"
|
|
1608
|
+
printf 'manual\n' > "$w/home/config/backlog-backend"
|
|
1609
|
+
err="$w/config-push-warnings.err"
|
|
1610
|
+
out=$(run_config_push "$w" 2>"$err"); status=$?
|
|
1611
|
+
|
|
1612
|
+
expect_code 0 "$status" "warnings-only config push should exit zero"
|
|
1613
|
+
assert_contains "$out" "secondmate dirty ($dirty_real):" \
|
|
1614
|
+
"config push did not report dirty home"
|
|
1615
|
+
assert_contains "$out" "home: dirty working tree - local-material push continuing" \
|
|
1616
|
+
"config push did not surface dirty state"
|
|
1617
|
+
assert_contains "$out" "secondmate stale ($stale_real):" \
|
|
1618
|
+
"config push did not report stale home"
|
|
1619
|
+
assert_contains "$out" "crew-dispatch.json: skipped - destination does not allow inherited item" \
|
|
1620
|
+
"config push did not report non-allowing item skip"
|
|
1621
|
+
assert_contains "$out" "secondmate bad ($bad_home): skipped - unsafe home: not a seeded secondmate home" \
|
|
1622
|
+
"config push did not report invalid secondmate home"
|
|
1623
|
+
err_text=$(cat "$err")
|
|
1624
|
+
assert_contains "$err_text" "fm-config-inherit: warning: skipped crew-dispatch.json" \
|
|
1625
|
+
"config push did not inherit the lib's skip stderr warning"
|
|
1626
|
+
pass "B13 config-push reports dirty, non-allowing, and invalid homes without failing warnings-only runs"
|
|
1627
|
+
}
|
|
1628
|
+
|
|
1629
|
+
test_config_push_exits_nonzero_on_copy_error() {
|
|
1630
|
+
local w head out err status sm_real err_text
|
|
1631
|
+
w=$(new_world config-push-error)
|
|
1632
|
+
head=$(git -C "$w/main" rev-parse HEAD)
|
|
1633
|
+
add_sm_worktree "$w" sm "$head"
|
|
1634
|
+
sm_real=$(cd "$w/sm" && pwd -P)
|
|
1635
|
+
printf 'codex\n' > "$w/home/config/crew-harness"
|
|
1636
|
+
mkdir -p "$w/sm/config/crew-harness"
|
|
1637
|
+
|
|
1638
|
+
err="$w/config-push-error.err"
|
|
1639
|
+
out=$(run_config_push "$w" 2>"$err"); status=$?
|
|
1640
|
+
|
|
1641
|
+
expect_code 1 "$status" "copy-error config push should exit non-zero"
|
|
1642
|
+
assert_contains "$out" "secondmate sm ($sm_real):" \
|
|
1643
|
+
"config push error output missed the home"
|
|
1644
|
+
assert_contains "$out" "crew-harness: error - failed to copy" \
|
|
1645
|
+
"config push did not report the per-item copy error"
|
|
1646
|
+
err_text=$(cat "$err")
|
|
1647
|
+
assert_contains "$err_text" "fm-config-inherit: error: failed to copy crew-harness" \
|
|
1648
|
+
"copy error did not emit a stderr diagnostic"
|
|
1649
|
+
pass "B14 config-push exits nonzero on real propagation errors"
|
|
1650
|
+
}
|
|
1651
|
+
|
|
1652
|
+
test_config_push_rereads_after_partial_propagation() {
|
|
1653
|
+
local w head log out err status instruction pointer
|
|
1654
|
+
w=$(new_world config-push-partial)
|
|
1655
|
+
head=$(git -C "$w/main" rev-parse HEAD)
|
|
1656
|
+
add_sm_worktree "$w" sm "$head"
|
|
1657
|
+
printf '{"default":{"harness":"codex"}}\n' > "$w/home/config/crew-dispatch.json"
|
|
1658
|
+
printf 'invalid shared header\n' > "$w/home/data/captain-shared.md"
|
|
1659
|
+
log="$w/config-push-partial.tmux.log"
|
|
1660
|
+
err="$w/config-push-partial.err"
|
|
1661
|
+
|
|
1662
|
+
out=$(run_config_push "$w" "$log" 2>"$err"); status=$?
|
|
1663
|
+
expect_code 1 "$status" "partial propagation should remain non-zero"
|
|
1664
|
+
assert_contains "$out" "crew-dispatch.json: pushed" \
|
|
1665
|
+
"partial propagation did not report the completed config item"
|
|
1666
|
+
assert_contains "$out" "data/captain-shared.md: error" \
|
|
1667
|
+
"partial propagation did not report the failed shared item"
|
|
1668
|
+
assert_contains "$out" "config-reread: sent" \
|
|
1669
|
+
"partial propagation lost the completed config reread"
|
|
1670
|
+
[ "$(cat "$w/sm/config/crew-dispatch.json")" = '{"default":{"harness":"codex"}}' ] \
|
|
1671
|
+
|| fail "partial propagation did not retain the completed config write"
|
|
1672
|
+
instruction=$(reread_instruction_path "$w/sm") || fail "partial propagation reread instruction missing"
|
|
1673
|
+
assert_present "$instruction" "partial propagation did not write a reread instruction"
|
|
1674
|
+
pointer="CONFIG_REREAD: $(reread_instruction_path "$w/sm")"
|
|
1675
|
+
assert_contains "$(inbox_stream "$w/home/state" sm)" "$pointer" \
|
|
1676
|
+
"partial propagation did not route the instruction pointer"
|
|
1677
|
+
pass "B14 config-push rereads completed config writes after partial propagation"
|
|
1678
|
+
}
|
|
1679
|
+
|
|
1680
|
+
# ---------------------------------------------------------------------------
|
|
1681
|
+
# Literal-content config reread nudge (post-propagation live-agent wake)
|
|
1682
|
+
# ---------------------------------------------------------------------------
|
|
1683
|
+
|
|
1684
|
+
shared_captain_header_for_tests() {
|
|
1685
|
+
cat <<'EOF'
|
|
1686
|
+
# Shared captain preferences
|
|
1687
|
+
|
|
1688
|
+
This file is main-authoritative in the main firstmate home.
|
|
1689
|
+
In secondmate homes it is read-only in secondmate homes and must not be edited there.
|
|
1690
|
+
Route new captain-preference discoveries to the main firstmate through marked status or a document pointer.
|
|
1691
|
+
EOF
|
|
1692
|
+
}
|
|
1693
|
+
|
|
1694
|
+
# End-user-aligned reproduction of the pre-fix gap, then the fixed behavior:
|
|
1695
|
+
# two live homes start with different stale config subsets; after push each is
|
|
1696
|
+
# updated and each live agent receives only its own changed-content instruction.
|
|
1697
|
+
test_config_reread_per_home_changed_sets_and_exact_bytes() {
|
|
1698
|
+
local w head log out err status instr_a instr_b multiline_json pointer
|
|
1699
|
+
w=$(new_world config-reread-per-home)
|
|
1700
|
+
head=$(git -C "$w/main" rev-parse HEAD)
|
|
1701
|
+
add_sm_worktree "$w" alpha "$head"
|
|
1702
|
+
add_sm_worktree "$w" beta "$head"
|
|
1703
|
+
mkdir -p "$w/alpha/config" "$w/beta/config" "$w/alpha/state" "$w/beta/state"
|
|
1704
|
+
|
|
1705
|
+
# alpha is stale on harness + backlog; beta is stale on multiline dispatch only.
|
|
1706
|
+
printf 'pi\n' > "$w/alpha/config/crew-harness"
|
|
1707
|
+
printf 'tasks-axi\n' > "$w/alpha/config/backlog-backend"
|
|
1708
|
+
printf '{"default":{"harness":"old"}}\n' > "$w/beta/config/crew-dispatch.json"
|
|
1709
|
+
|
|
1710
|
+
multiline_json=$(printf '{\n "default": {\n "harness": "grok",\n "model": "grok-4.5"\n },\n "rules": [\n {"when": "news", "use": {"harness": "grok"}}\n ]\n}\n')
|
|
1711
|
+
printf '%s' "$multiline_json" > "$w/home/config/crew-dispatch.json"
|
|
1712
|
+
printf 'codex\n' > "$w/home/config/crew-harness"
|
|
1713
|
+
printf 'manual\n' > "$w/home/config/backlog-backend"
|
|
1714
|
+
printf 'tmux\n' > "$w/home/config/backend"
|
|
1715
|
+
{
|
|
1716
|
+
shared_captain_header_for_tests
|
|
1717
|
+
printf '%s\n' "shared secret preference body that must never appear in a config reread"
|
|
1718
|
+
} > "$w/home/data/captain-shared.md"
|
|
1719
|
+
|
|
1720
|
+
record_live_watcher_fixture "$w/home"
|
|
1721
|
+
log="$w/config-reread-per-home.tmux.log"
|
|
1722
|
+
err="$w/config-reread-per-home.err"
|
|
1723
|
+
out=$(run_config_push "$w" "$log" 2>"$err"); status=$?
|
|
1724
|
+
expect_code 0 "$status" "per-home reread config push should succeed"
|
|
1725
|
+
[ ! -s "$err" ] || fail "unexpected stderr: $(cat "$err")"
|
|
1726
|
+
|
|
1727
|
+
# Destination bytes converged per home.
|
|
1728
|
+
cmp -s "$w/home/config/crew-dispatch.json" "$w/alpha/config/crew-dispatch.json" \
|
|
1729
|
+
|| fail "alpha did not receive multiline dispatch"
|
|
1730
|
+
cmp -s "$w/home/config/crew-dispatch.json" "$w/beta/config/crew-dispatch.json" \
|
|
1731
|
+
|| fail "beta did not receive multiline dispatch"
|
|
1732
|
+
[ "$(cat "$w/alpha/config/crew-harness")" = codex ] || fail "alpha harness not updated"
|
|
1733
|
+
[ "$(cat "$w/alpha/config/backlog-backend")" = manual ] || fail "alpha backlog-backend not updated"
|
|
1734
|
+
[ "$(cat "$w/alpha/config/backend")" = tmux ] || fail "alpha backend not updated"
|
|
1735
|
+
|
|
1736
|
+
instr_a=$(reread_instruction_path "$w/alpha") || fail "alpha instruction missing after config push"
|
|
1737
|
+
instr_b=$(reread_instruction_path "$w/beta") || fail "beta instruction missing after config push"
|
|
1738
|
+
assert_present "$instr_a" "alpha should receive a config-reread instruction file"
|
|
1739
|
+
assert_present "$instr_b" "beta should receive a config-reread instruction file"
|
|
1740
|
+
[ "$(reread_mode "$instr_a")" = 600 ] || fail "alpha instruction is not private"
|
|
1741
|
+
[ "$(reread_mode "$instr_b")" = 600 ] || fail "beta instruction is not private"
|
|
1742
|
+
|
|
1743
|
+
# Deterministic allowlist path order and exact destination bytes for alpha
|
|
1744
|
+
# (allowlisted config items were missing/stale and therefore pushed).
|
|
1745
|
+
assert_grep "These inherited config files changed" "$instr_a" "alpha framing missing"
|
|
1746
|
+
assert_grep "defaults/rules" "$instr_a" "alpha must preserve agent judgment framing"
|
|
1747
|
+
assert_contains "$(cat "$instr_a")" "config/crew-dispatch.json" "alpha missing dispatch path"
|
|
1748
|
+
assert_contains "$(cat "$instr_a")" "config/crew-harness" "alpha missing harness path"
|
|
1749
|
+
assert_contains "$(cat "$instr_a")" "config/backlog-backend" "alpha missing backlog path"
|
|
1750
|
+
assert_contains "$(cat "$instr_a")" "config/backend" "alpha missing backend path"
|
|
1751
|
+
# Path order follows FM_INHERITABLE_CONFIG.
|
|
1752
|
+
awk '
|
|
1753
|
+
/config\/crew-dispatch\.json/ { d=NR }
|
|
1754
|
+
/config\/crew-harness/ { h=NR }
|
|
1755
|
+
/config\/backlog-backend/ { b=NR }
|
|
1756
|
+
/config\/backend/ && !/backlog-backend/ { k=NR }
|
|
1757
|
+
END {
|
|
1758
|
+
if (!(d && h && b && k && d < h && h < b && b < k)) exit 1
|
|
1759
|
+
}
|
|
1760
|
+
' "$instr_a" || fail "alpha instruction path order is not deterministic allowlist order"
|
|
1761
|
+
|
|
1762
|
+
# Exact multiline JSON appears byte-for-byte between delimiters.
|
|
1763
|
+
assert_contains "$(cat "$instr_a")" "$multiline_json" \
|
|
1764
|
+
"alpha instruction must include exact multiline dispatch bytes"
|
|
1765
|
+
assert_contains "$(cat "$instr_a")" $'-----BEGIN config/crew-harness-----\ncodex\n-----END config/crew-harness-----' \
|
|
1766
|
+
"alpha instruction must include exact harness scalar bytes"
|
|
1767
|
+
assert_contains "$(cat "$instr_a")" $'-----BEGIN config/backlog-backend-----\nmanual\n-----END config/backlog-backend-----' \
|
|
1768
|
+
"alpha instruction must include exact backlog-backend scalar bytes"
|
|
1769
|
+
assert_contains "$(cat "$instr_a")" $'-----BEGIN config/backend-----\ntmux\n-----END config/backend-----' \
|
|
1770
|
+
"alpha instruction must include exact backend scalar bytes"
|
|
1771
|
+
|
|
1772
|
+
# No parsed/effective summary, no SHA, no captain-shared dump.
|
|
1773
|
+
assert_not_contains "$(cat "$instr_a")" "Default worker" "must not emit parsed worker summary"
|
|
1774
|
+
assert_not_contains "$(cat "$instr_a")" "sha" "must not emit sha tokens"
|
|
1775
|
+
assert_not_contains "$(cat "$instr_a")" "SHA" "must not emit SHA tokens"
|
|
1776
|
+
assert_not_contains "$(cat "$instr_a")" "captain-shared" "captain-shared path must not appear"
|
|
1777
|
+
assert_not_contains "$(cat "$instr_a")" "shared secret preference body" \
|
|
1778
|
+
"captain-shared content must never be inlined"
|
|
1779
|
+
assert_not_contains "$(cat "$instr_b")" "shared secret preference body" \
|
|
1780
|
+
"beta must not inline captain-shared either"
|
|
1781
|
+
|
|
1782
|
+
# Beta started with only dispatch stale; harness/backlog were absent on both
|
|
1783
|
+
# sides for beta... wait: primary has harness+backlog, beta lacked them, so
|
|
1784
|
+
# they are also pushed. Seed beta with matching harness/backlog so only
|
|
1785
|
+
# dispatch changes for beta - re-run a focused unit of the write helper below.
|
|
1786
|
+
# For this push, beta was missing harness and backlog too, so all three push.
|
|
1787
|
+
# Prove isolation by comparing that neither instruction references the other's
|
|
1788
|
+
# pre-push stale unique value.
|
|
1789
|
+
assert_not_contains "$(cat "$instr_a")" '"harness":"old"' \
|
|
1790
|
+
"alpha must not receive beta's pre-push stale dispatch"
|
|
1791
|
+
assert_not_contains "$(cat "$instr_b")" $'pi\n' \
|
|
1792
|
+
"beta instruction must not leak alpha-only stale harness bytes as a standalone scalar block incorrectly"
|
|
1793
|
+
|
|
1794
|
+
# Routed send used the from-firstmate marker and carried only the pointer,
|
|
1795
|
+
# read from alpha's durable steer records (the typed channel now carries only
|
|
1796
|
+
# the constant doorbell, which never inlines message content).
|
|
1797
|
+
pointer="CONFIG_REREAD: $(reread_instruction_path "$w/alpha")"
|
|
1798
|
+
assert_contains "$(inbox_stream "$w/home/state" alpha)" "[fm-from-firstmate]" "reread send must be marked"
|
|
1799
|
+
assert_contains "$(inbox_stream "$w/home/state" alpha)" "$pointer" "reread send must point to the durable instruction file"
|
|
1800
|
+
assert_not_contains "$(inbox_stream "$w/home/state" alpha)" '"harness": "grok"' "sent message must not inline multiline JSON"
|
|
1801
|
+
assert_not_contains "$(inbox_stream "$w/home/state" alpha)" "Default worker" "sent message must not summarize"
|
|
1802
|
+
assert_not_contains "$(cat "$log")" '"harness": "grok"' "the typed doorbell must not inline multiline JSON"
|
|
1803
|
+
pass "B15 config reread is per-home, exact-byte, ordered, and pointer-only"
|
|
1804
|
+
}
|
|
1805
|
+
|
|
1806
|
+
test_config_reread_isolation_and_absent_and_send_failure() {
|
|
1807
|
+
local w head log out out2 err status status2 instr_a instr_b report retry_log retry_out retry_status retry_pointer
|
|
1808
|
+
local first_instr first_copy second_instr second_pointer
|
|
1809
|
+
w=$(new_world config-reread-absent)
|
|
1810
|
+
head=$(git -C "$w/main" rev-parse HEAD)
|
|
1811
|
+
add_sm_worktree "$w" alpha "$head"
|
|
1812
|
+
add_sm_worktree "$w" beta "$head"
|
|
1813
|
+
mkdir -p "$w/alpha/config" "$w/beta/config" "$w/alpha/state" "$w/beta/state"
|
|
1814
|
+
|
|
1815
|
+
# alpha: only harness will change (dispatch+backlog already match primary absence).
|
|
1816
|
+
# beta: only dispatch will change.
|
|
1817
|
+
printf 'old-harness\n' > "$w/alpha/config/crew-harness"
|
|
1818
|
+
printf '{"stale":true}\n' > "$w/beta/config/crew-dispatch.json"
|
|
1819
|
+
# Primary has only crew-harness set; dispatch and backlog absent.
|
|
1820
|
+
printf 'codex\n' > "$w/home/config/crew-harness"
|
|
1821
|
+
rm -f "$w/home/config/crew-dispatch.json" "$w/home/config/backlog-backend"
|
|
1822
|
+
|
|
1823
|
+
log="$w/config-reread-absent.tmux.log"
|
|
1824
|
+
err="$w/config-reread-absent.err"
|
|
1825
|
+
out=$(run_config_push "$w" "$log" 2>"$err"); status=$?
|
|
1826
|
+
expect_code 0 "$status" "absent-mirror reread push should succeed"
|
|
1827
|
+
|
|
1828
|
+
instr_a=$(reread_instruction_path "$w/alpha") || fail "alpha instruction missing after config push"
|
|
1829
|
+
instr_b=$(reread_instruction_path "$w/beta") || fail "beta instruction missing after config push"
|
|
1830
|
+
assert_present "$instr_a" "alpha instruction missing after harness change"
|
|
1831
|
+
assert_present "$instr_b" "beta instruction missing after dispatch removal"
|
|
1832
|
+
|
|
1833
|
+
# alpha changed harness only.
|
|
1834
|
+
assert_contains "$(cat "$instr_a")" "config/crew-harness" "alpha should mention harness"
|
|
1835
|
+
assert_contains "$(cat "$instr_a")" $'-----BEGIN config/crew-harness-----\ncodex\n-----END config/crew-harness-----' \
|
|
1836
|
+
"alpha harness block exact"
|
|
1837
|
+
assert_not_contains "$(cat "$instr_a")" "config/crew-dispatch.json" \
|
|
1838
|
+
"alpha must not list unchanged/absent-both dispatch"
|
|
1839
|
+
assert_not_contains "$(cat "$instr_a")" "config/backlog-backend" \
|
|
1840
|
+
"alpha must not list unchanged/absent-both backlog"
|
|
1841
|
+
assert_not_contains "$(cat "$instr_a")" '{"stale":true}' \
|
|
1842
|
+
"alpha must not receive beta's changed dispatch content"
|
|
1843
|
+
|
|
1844
|
+
# beta: dispatch mirrored to ABSENT (and harness is also newly pushed from primary).
|
|
1845
|
+
assert_contains "$(cat "$instr_b")" "config/crew-dispatch.json" "beta should mention dispatch"
|
|
1846
|
+
assert_contains "$(cat "$instr_b")" $'-----BEGIN config/crew-dispatch.json-----\nABSENT\n-----END config/crew-dispatch.json-----' \
|
|
1847
|
+
"beta must represent removal as ABSENT"
|
|
1848
|
+
assert_not_contains "$(cat "$instr_b")" "old-harness" \
|
|
1849
|
+
"beta must not receive alpha's pre-push stale harness content"
|
|
1850
|
+
# Pure ABSENT + unchanged isolation via the write helper (no second inheritance path).
|
|
1851
|
+
report="$w/absent-only.report"
|
|
1852
|
+
{
|
|
1853
|
+
printf '%s\n' $'crew-dispatch.json\tpushed\tmirrored primary absence'
|
|
1854
|
+
printf '%s\n' $'crew-harness\tunchanged\t'
|
|
1855
|
+
printf '%s\n' $'backlog-backend\tunchanged\t'
|
|
1856
|
+
printf '%s\n' $'backend\tunchanged\t'
|
|
1857
|
+
printf '%s\n' $'data/captain-shared.md\tpushed\t'
|
|
1858
|
+
} > "$report"
|
|
1859
|
+
rm -f "$w/beta/config/crew-dispatch.json"
|
|
1860
|
+
fm_config_write_reread_instruction "$w/beta" "$report" "$w/beta/state/.fm-inherited-config-reread-absent" \
|
|
1861
|
+
|| fail "ABSENT instruction write failed"
|
|
1862
|
+
assert_contains "$(cat "$w/beta/state/.fm-inherited-config-reread-absent")" \
|
|
1863
|
+
$'-----BEGIN config/crew-dispatch.json-----\nABSENT\n-----END config/crew-dispatch.json-----' \
|
|
1864
|
+
"helper ABSENT representation"
|
|
1865
|
+
assert_not_contains "$(cat "$w/beta/state/.fm-inherited-config-reread-absent")" "captain-shared" \
|
|
1866
|
+
"helper must ignore captain-shared even when report says pushed"
|
|
1867
|
+
assert_not_contains "$(cat "$w/beta/state/.fm-inherited-config-reread-absent")" "config/crew-harness" \
|
|
1868
|
+
"helper must omit unchanged items"
|
|
1869
|
+
|
|
1870
|
+
# Send failure becomes a retryable diagnostic and non-zero exit. On the
|
|
1871
|
+
# inbox plane the real local failure is an unwritable steer record (a
|
|
1872
|
+
# keystroke failure alone no longer fails a durably enqueued pointer), so
|
|
1873
|
+
# replace each inbox dir with a plain file that blocks the enqueue.
|
|
1874
|
+
rm -rf "$w/home/state/alpha.inbox" "$w/home/state/beta.inbox"
|
|
1875
|
+
: > "$w/home/state/alpha.inbox"
|
|
1876
|
+
: > "$w/home/state/beta.inbox"
|
|
1877
|
+
printf 'claude\n' > "$w/home/config/crew-harness"
|
|
1878
|
+
err="$w/config-reread-send-fail.err"
|
|
1879
|
+
out=$(PATH="$(make_fake_toolchain "$w"):$BASE_PATH" \
|
|
1880
|
+
FM_HOME="$w/home" FM_ROOT_OVERRIDE="$w/main" FM_SEND_SETTLE=0 \
|
|
1881
|
+
"$ROOT/bin/fm-config-push.sh" 2>"$err"); status=$?
|
|
1882
|
+
expect_code 1 "$status" "send failure should make config-push exit non-zero"
|
|
1883
|
+
assert_contains "$out" "CONFIG_REREAD: secondmate" "send failure diagnostic missing"
|
|
1884
|
+
assert_contains "$out" "send failed" "send failure must say send failed"
|
|
1885
|
+
assert_not_contains "$out" "config-reread: sent" \
|
|
1886
|
+
"must not claim reread landed when send failed"
|
|
1887
|
+
first_instr=$(reread_instruction_path "$w/alpha") || fail "alpha failed-send instruction missing"
|
|
1888
|
+
first_copy="$w/alpha/first-reread-generation.copy"
|
|
1889
|
+
cp "$first_instr" "$first_copy"
|
|
1890
|
+
assert_present "$(reread_pending_path "$w/alpha")" \
|
|
1891
|
+
"alpha send failure did not record a retry marker"
|
|
1892
|
+
assert_present "$(reread_pending_path "$w/beta")" \
|
|
1893
|
+
"beta send failure did not record a retry marker"
|
|
1894
|
+
|
|
1895
|
+
# A later changed push publishes a distinct generation without overwriting
|
|
1896
|
+
# the failed generation, then an unchanged push retries both pointers.
|
|
1897
|
+
printf 'pi\n' > "$w/home/config/crew-harness"
|
|
1898
|
+
err="$w/config-reread-send-fail-second.err"
|
|
1899
|
+
out2=$(PATH="$(make_fake_toolchain "$w"):$BASE_PATH" \
|
|
1900
|
+
FM_HOME="$w/home" FM_ROOT_OVERRIDE="$w/main" FM_SEND_SETTLE=0 \
|
|
1901
|
+
"$ROOT/bin/fm-config-push.sh" 2>"$err"); status2=$?
|
|
1902
|
+
expect_code 1 "$status2" "second send failure should make config-push exit non-zero"
|
|
1903
|
+
assert_not_contains "$out2" "config-reread: sent" \
|
|
1904
|
+
"second send failure must not claim reread delivery"
|
|
1905
|
+
second_instr=$(reread_instruction_path "$w/alpha") || fail "alpha second generation missing"
|
|
1906
|
+
[ "$first_instr" != "$second_instr" ] || fail "successive pushes reused the same generation path"
|
|
1907
|
+
cmp -s "$first_copy" "$first_instr" || fail "later push overwrote the earlier generation bytes"
|
|
1908
|
+
second_pointer="CONFIG_REREAD: $second_instr"
|
|
1909
|
+
assert_present "$(reread_pending_path "$w/alpha")" \
|
|
1910
|
+
"alpha second generation did not remain pending"
|
|
1911
|
+
|
|
1912
|
+
# A normal later push retries the durable pointers even though propagation is
|
|
1913
|
+
# unchanged, then clears every marker after delivery succeeds.
|
|
1914
|
+
rm -f "$w/home/state/alpha.inbox" "$w/home/state/beta.inbox"
|
|
1915
|
+
retry_log="$w/config-reread-send-retry.tmux.log"
|
|
1916
|
+
retry_out=$(run_config_push "$w" "$retry_log" 2>"$err"); retry_status=$?
|
|
1917
|
+
expect_code 0 "$retry_status" "send failure should be retryable"
|
|
1918
|
+
assert_contains "$retry_out" "config-reread: sent" \
|
|
1919
|
+
"retry should report the reread as sent"
|
|
1920
|
+
retry_pointer="CONFIG_REREAD: $(reread_instruction_path "$w/beta")"
|
|
1921
|
+
assert_contains "$(inbox_stream "$w/home/state" beta)" "$retry_pointer" \
|
|
1922
|
+
"retry did not resend the durable pointer"
|
|
1923
|
+
assert_contains "$(inbox_stream "$w/home/state" alpha)" "CONFIG_REREAD: $first_instr" \
|
|
1924
|
+
"retry did not resend the first pending generation"
|
|
1925
|
+
assert_contains "$(inbox_stream "$w/home/state" alpha)" "$second_pointer" \
|
|
1926
|
+
"retry did not resend the second pending generation"
|
|
1927
|
+
assert_no_reread_pending "$w/alpha"
|
|
1928
|
+
assert_no_reread_pending "$w/beta"
|
|
1929
|
+
pass "B16 config reread isolation, ABSENT, generation safety, send failure, and retry"
|
|
1930
|
+
}
|
|
1931
|
+
|
|
1932
|
+
test_config_reread_publication_failure_retries_exact_generation() {
|
|
1933
|
+
local w head fakebin real_mv alpha_state out status stage log instr retry_out retry_status
|
|
1934
|
+
w=$(new_world config-reread-publication-retry)
|
|
1935
|
+
head=$(git -C "$w/main" rev-parse HEAD)
|
|
1936
|
+
add_sm_worktree "$w" alpha "$head"
|
|
1937
|
+
mkdir -p "$w/alpha/config" "$w/alpha/state"
|
|
1938
|
+
printf 'old\n' > "$w/alpha/config/crew-harness"
|
|
1939
|
+
printf 'codex\n' > "$w/home/config/crew-harness"
|
|
1940
|
+
|
|
1941
|
+
fakebin=$(make_fake_toolchain "$w")
|
|
1942
|
+
real_mv=$(command -v mv)
|
|
1943
|
+
alpha_state=$(cd "$w/alpha/state" && pwd -P)
|
|
1944
|
+
cat > "$fakebin/mv" <<SH
|
|
1945
|
+
#!/usr/bin/env bash
|
|
1946
|
+
case "\$*" in
|
|
1947
|
+
*"$alpha_state/.fm-inherited-config-reread."*) exit 1 ;;
|
|
1948
|
+
esac
|
|
1949
|
+
exec "$real_mv" "\$@"
|
|
1950
|
+
SH
|
|
1951
|
+
chmod +x "$fakebin/mv"
|
|
1952
|
+
out=$(PATH="$fakebin:$BASE_PATH" FM_HOME="$w/home" FM_ROOT_OVERRIDE="$w/main" \
|
|
1953
|
+
FM_SEND_SETTLE=0 "$ROOT/bin/fm-config-push.sh" 2>&1); status=$?
|
|
1954
|
+
expect_code 1 "$status" "publication failure should remain diagnostic"
|
|
1955
|
+
assert_contains "$out" "CONFIG_REREAD: secondmate" "publication failure diagnostic missing"
|
|
1956
|
+
assert_not_contains "$out" "config-reread: sent" \
|
|
1957
|
+
"publication failure must not claim reread delivery"
|
|
1958
|
+
[ "$(cat "$w/alpha/config/crew-harness")" = codex ] \
|
|
1959
|
+
|| fail "publication failure did not retain the completed config write"
|
|
1960
|
+
stage=$(reread_retry_stage_path "$w/home" alpha) \
|
|
1961
|
+
|| fail "publication failure did not retain an exact retry generation"
|
|
1962
|
+
assert_contains "$(cat "$stage")" \
|
|
1963
|
+
$'-----BEGIN config/crew-harness-----\ncodex\n-----END config/crew-harness-----' \
|
|
1964
|
+
"retry generation did not retain exact destination bytes"
|
|
1965
|
+
assert_no_reread_instructions "$w/alpha"
|
|
1966
|
+
|
|
1967
|
+
rm -f "$fakebin/mv"
|
|
1968
|
+
log="$w/config-reread-publication-retry.tmux.log"
|
|
1969
|
+
retry_out=$(run_config_push "$w" "$log" 2>/dev/null); retry_status=$?
|
|
1970
|
+
expect_code 0 "$retry_status" "publication failure should retry on an unchanged push"
|
|
1971
|
+
assert_contains "$retry_out" "config-reread: sent" \
|
|
1972
|
+
"successful publication retry should report delivery"
|
|
1973
|
+
instr=$(reread_instruction_path "$w/alpha") \
|
|
1974
|
+
|| fail "publication retry did not publish an instruction"
|
|
1975
|
+
assert_contains "$(inbox_stream "$w/home/state" alpha)" "CONFIG_REREAD: $instr" \
|
|
1976
|
+
"publication retry did not send the durable pointer"
|
|
1977
|
+
assert_no_reread_retry_stages "$w/home" alpha
|
|
1978
|
+
pass "B20 config reread publication failures retain exact generations for retry"
|
|
1979
|
+
}
|
|
1980
|
+
|
|
1981
|
+
test_config_reread_write_failure_retains_exact_retry_generation() {
|
|
1982
|
+
local w head fakebin real_mv retry_dir out status stage_path log retry_out retry_status instr
|
|
1983
|
+
local old_instr new_instr
|
|
1984
|
+
w=$(new_world config-reread-write-retry)
|
|
1985
|
+
head=$(git -C "$w/main" rev-parse HEAD)
|
|
1986
|
+
add_sm_worktree "$w" sm "$head"
|
|
1987
|
+
mkdir -p "$w/sm/config" "$w/sm/state"
|
|
1988
|
+
printf 'old\n' > "$w/sm/config/crew-harness"
|
|
1989
|
+
printf 'codex\n' > "$w/home/config/crew-harness"
|
|
1990
|
+
fakebin=$(make_fake_toolchain "$w")
|
|
1991
|
+
real_mv=$(command -v mv)
|
|
1992
|
+
mkdir -p "$w/home/state/.fm-inherited-config-reread-retry/sm"
|
|
1993
|
+
retry_dir=$(cd "$w/home/state/.fm-inherited-config-reread-retry/sm" && pwd -P)
|
|
1994
|
+
cat > "$fakebin/mv" <<SH
|
|
1995
|
+
#!/usr/bin/env bash
|
|
1996
|
+
target=
|
|
1997
|
+
for arg in "\$@"; do target="\$arg"; done
|
|
1998
|
+
case "\$target" in
|
|
1999
|
+
*"$retry_dir"/.fm-inherited-config-reread.*)
|
|
2000
|
+
case "\$target" in
|
|
2001
|
+
*.exact) ;;
|
|
2002
|
+
*) exit 1 ;;
|
|
2003
|
+
esac
|
|
2004
|
+
;;
|
|
2005
|
+
esac
|
|
2006
|
+
exec "$real_mv" "\$@"
|
|
2007
|
+
SH
|
|
2008
|
+
chmod +x "$fakebin/mv"
|
|
2009
|
+
out=$(PATH="$fakebin:$BASE_PATH" FM_HOME="$w/home" FM_ROOT_OVERRIDE="$w/main" \
|
|
2010
|
+
FM_SEND_SETTLE=0 "$ROOT/bin/fm-config-push.sh" 2>&1); status=$?
|
|
2011
|
+
expect_code 1 "$status" "instruction-write failure should remain diagnostic"
|
|
2012
|
+
assert_contains "$out" "retained exact retry generation" \
|
|
2013
|
+
"instruction-write failure did not retain exact retry bytes"
|
|
2014
|
+
stage_path=$(reread_retry_stage_path "$w/home" sm) \
|
|
2015
|
+
|| fail "instruction-write failure did not leave a durable exact generation"
|
|
2016
|
+
assert_contains "$(cat "$stage_path")" \
|
|
2017
|
+
$'-----BEGIN config/crew-harness-----\ncodex\n-----END config/crew-harness-----' \
|
|
2018
|
+
"instruction-write failure did not retain the original exact bytes"
|
|
2019
|
+
printf 'changed-before-retry\n' > "$w/home/config/crew-harness"
|
|
2020
|
+
rm -f "$fakebin/mv"
|
|
2021
|
+
log="$w/config-reread-write-retry.tmux.log"
|
|
2022
|
+
retry_out=$(run_config_push "$w" "$log" 2>/dev/null); retry_status=$?
|
|
2023
|
+
expect_code 0 "$retry_status" "a later changed push should retry an instruction-write failure"
|
|
2024
|
+
assert_contains "$retry_out" "config-reread: sent" \
|
|
2025
|
+
"later changed push did not deliver the retained exact generation"
|
|
2026
|
+
old_instr=$(inbox_stream "$w/home/state" sm | grep 'CONFIG_REREAD:' | head -n 1 | sed 's/.*CONFIG_REREAD: //')
|
|
2027
|
+
new_instr=$(inbox_stream "$w/home/state" sm | grep 'CONFIG_REREAD:' | tail -n 1 | sed 's/.*CONFIG_REREAD: //')
|
|
2028
|
+
[ -n "$old_instr" ] && [ -n "$new_instr" ] && [ "$old_instr" != "$new_instr" ] \
|
|
2029
|
+
|| fail "later changed push did not deliver both generations"
|
|
2030
|
+
instr="$old_instr"
|
|
2031
|
+
assert_contains "$(cat "$instr")" \
|
|
2032
|
+
$'-----BEGIN config/crew-harness-----\ncodex\n-----END config/crew-harness-----' \
|
|
2033
|
+
"exact retry delivery did not preserve the original destination bytes"
|
|
2034
|
+
assert_contains "$(cat "$new_instr")" "changed-before-retry" \
|
|
2035
|
+
"later changed push did not deliver its new destination bytes"
|
|
2036
|
+
assert_no_reread_retry_stages "$w/home" sm
|
|
2037
|
+
pass "B21 config reread instruction-write failures retain exact retry generations"
|
|
2038
|
+
}
|
|
2039
|
+
|
|
2040
|
+
test_config_reread_exact_temp_survives_adoption_failure() {
|
|
2041
|
+
local w head fakebin real_mv real_cp retry_dir out status stage_path log retry_out retry_status
|
|
2042
|
+
local old_instr new_instr
|
|
2043
|
+
w=$(new_world config-reread-exact-temp-fallback)
|
|
2044
|
+
head=$(git -C "$w/main" rev-parse HEAD)
|
|
2045
|
+
add_sm_worktree "$w" sm "$head"
|
|
2046
|
+
mkdir -p "$w/sm/config" "$w/sm/state"
|
|
2047
|
+
printf 'old\n' > "$w/sm/config/crew-harness"
|
|
2048
|
+
printf 'codex\n' > "$w/home/config/crew-harness"
|
|
2049
|
+
fakebin=$(make_fake_toolchain "$w")
|
|
2050
|
+
real_mv=$(command -v mv)
|
|
2051
|
+
real_cp=$(command -v cp)
|
|
2052
|
+
mkdir -p "$w/home/state/.fm-inherited-config-reread-retry/sm"
|
|
2053
|
+
retry_dir=$(cd "$w/home/state/.fm-inherited-config-reread-retry/sm" && pwd -P)
|
|
2054
|
+
cat > "$fakebin/mv" <<SH
|
|
2055
|
+
#!/usr/bin/env bash
|
|
2056
|
+
target=
|
|
2057
|
+
for arg in "\$@"; do target="\$arg"; done
|
|
2058
|
+
case "\$target" in
|
|
2059
|
+
*"$retry_dir"/.fm-inherited-config-reread.*) exit 1 ;;
|
|
2060
|
+
esac
|
|
2061
|
+
exec "$real_mv" "\$@"
|
|
2062
|
+
SH
|
|
2063
|
+
chmod +x "$fakebin/mv"
|
|
2064
|
+
cat > "$fakebin/cp" <<SH
|
|
2065
|
+
#!/usr/bin/env bash
|
|
2066
|
+
target=
|
|
2067
|
+
for arg in "\$@"; do target="\$arg"; done
|
|
2068
|
+
case "\$target" in
|
|
2069
|
+
*"$retry_dir"/.fm-inherited-config-reread.*) exit 1 ;;
|
|
2070
|
+
esac
|
|
2071
|
+
exec "$real_cp" "\$@"
|
|
2072
|
+
SH
|
|
2073
|
+
chmod +x "$fakebin/cp"
|
|
2074
|
+
out=$(PATH="$fakebin:$BASE_PATH" FM_HOME="$w/home" FM_ROOT_OVERRIDE="$w/main" \
|
|
2075
|
+
FM_SEND_SETTLE=0 "$ROOT/bin/fm-config-push.sh" 2>&1); status=$?
|
|
2076
|
+
expect_code 1 "$status" "exact temporary fallback failure should remain diagnostic"
|
|
2077
|
+
assert_contains "$out" "retained exact retry temporary" \
|
|
2078
|
+
"exact temporary fallback failure did not retain the immutable bytes"
|
|
2079
|
+
stage_path=$(reread_retry_stage_path "$w/home" sm) \
|
|
2080
|
+
|| fail "exact temporary fallback failure lost its retry artifact"
|
|
2081
|
+
case "$stage_path" in
|
|
2082
|
+
*.tmp.*) : ;;
|
|
2083
|
+
*) fail "exact temporary fallback retained an unexpected artifact: $stage_path" ;;
|
|
2084
|
+
esac
|
|
2085
|
+
[ ! -e "$stage_path.report" ] \
|
|
2086
|
+
|| fail "exact temporary fallback created a lossy retry report"
|
|
2087
|
+
assert_contains "$(cat "$stage_path")" \
|
|
2088
|
+
$'-----BEGIN config/crew-harness-----\ncodex\n-----END config/crew-harness-----' \
|
|
2089
|
+
"exact temporary fallback did not preserve the original bytes"
|
|
2090
|
+
printf 'changed-before-retry\n' > "$w/home/config/crew-harness"
|
|
2091
|
+
rm -f "$fakebin/mv" "$fakebin/cp"
|
|
2092
|
+
log="$w/config-reread-exact-temp-fallback.tmux.log"
|
|
2093
|
+
retry_out=$(run_config_push "$w" "$log" 2>/dev/null); retry_status=$?
|
|
2094
|
+
expect_code 0 "$retry_status" "later push should deliver retained exact temporary bytes"
|
|
2095
|
+
old_instr=$(inbox_stream "$w/home/state" sm | grep 'CONFIG_REREAD:' | head -n 1 | sed 's/.*CONFIG_REREAD: //')
|
|
2096
|
+
new_instr=$(inbox_stream "$w/home/state" sm | grep 'CONFIG_REREAD:' | tail -n 1 | sed 's/.*CONFIG_REREAD: //')
|
|
2097
|
+
[ -n "$old_instr" ] && [ -n "$new_instr" ] && [ "$old_instr" != "$new_instr" ] \
|
|
2098
|
+
|| fail "later push did not deliver both exact generations"
|
|
2099
|
+
assert_contains "$(cat "$old_instr")" \
|
|
2100
|
+
$'-----BEGIN config/crew-harness-----\ncodex\n-----END config/crew-harness-----' \
|
|
2101
|
+
"later push rebuilt the retained temporary from newer bytes"
|
|
2102
|
+
assert_contains "$(cat "$new_instr")" "changed-before-retry" \
|
|
2103
|
+
"later push did not deliver the new destination bytes"
|
|
2104
|
+
assert_no_reread_retry_stages "$w/home" sm
|
|
2105
|
+
pass "B21 config reread preserves exact bytes when temporary adoption also fails"
|
|
2106
|
+
}
|
|
2107
|
+
|
|
2108
|
+
test_config_reread_serializes_concurrent_pushes() {
|
|
2109
|
+
local w head fakebin marker entered log first_out second_out first_pid first_status second_status
|
|
2110
|
+
local first_instr second_instr first_line second_line
|
|
2111
|
+
w=$(new_world config-reread-serialized-pushes)
|
|
2112
|
+
head=$(git -C "$w/main" rev-parse HEAD)
|
|
2113
|
+
add_sm_worktree "$w" sm "$head"
|
|
2114
|
+
mkdir -p "$w/sm/config" "$w/sm/state"
|
|
2115
|
+
printf 'old\n' > "$w/sm/config/crew-harness"
|
|
2116
|
+
printf 'one\n' > "$w/home/config/crew-harness"
|
|
2117
|
+
|
|
2118
|
+
fakebin=$(make_fake_toolchain "$w")
|
|
2119
|
+
mv "$fakebin/tmux" "$fakebin/tmux.real"
|
|
2120
|
+
marker="$w/first-send.marker"
|
|
2121
|
+
entered="$w/first-send.entered"
|
|
2122
|
+
log="$w/config-reread-serialized.tmux.log"
|
|
2123
|
+
cat > "$fakebin/tmux" <<SH
|
|
2124
|
+
#!/usr/bin/env bash
|
|
2125
|
+
case "\$*" in
|
|
2126
|
+
*send-keys*)
|
|
2127
|
+
if (set -o noclobber; : > "$marker") 2>/dev/null; then
|
|
2128
|
+
: > "$entered"
|
|
2129
|
+
sleep 1
|
|
2130
|
+
fi
|
|
2131
|
+
;;
|
|
2132
|
+
esac
|
|
2133
|
+
exec "$fakebin/tmux.real" "\$@"
|
|
2134
|
+
SH
|
|
2135
|
+
chmod +x "$fakebin/tmux"
|
|
2136
|
+
|
|
2137
|
+
first_out="$w/first-push.out"
|
|
2138
|
+
(
|
|
2139
|
+
PATH="$fakebin:$BASE_PATH" FM_HOME="$w/home" FM_ROOT_OVERRIDE="$w/main" \
|
|
2140
|
+
FM_SEND_SETTLE=0 FM_FAKE_TMUX_LOG="$log" \
|
|
2141
|
+
"$ROOT/bin/fm-config-push.sh" > "$first_out" 2>&1
|
|
2142
|
+
) &
|
|
2143
|
+
first_pid=$!
|
|
2144
|
+
for _ in $(seq 1 100); do
|
|
2145
|
+
[ -e "$entered" ] && break
|
|
2146
|
+
sleep 0.02
|
|
2147
|
+
done
|
|
2148
|
+
[ -e "$entered" ] || fail "first config push did not reach pointer delivery"
|
|
2149
|
+
first_instr=$(reread_instruction_path "$w/sm") \
|
|
2150
|
+
|| fail "first concurrent push did not publish its generation"
|
|
2151
|
+
printf 'two\n' > "$w/home/config/crew-harness"
|
|
2152
|
+
second_out="$w/second-push.out"
|
|
2153
|
+
PATH="$fakebin:$BASE_PATH" FM_HOME="$w/home" FM_ROOT_OVERRIDE="$w/main" \
|
|
2154
|
+
FM_SEND_SETTLE=0 FM_FAKE_TMUX_LOG="$log" \
|
|
2155
|
+
"$ROOT/bin/fm-config-push.sh" > "$second_out" 2>&1
|
|
2156
|
+
second_status=$?
|
|
2157
|
+
wait "$first_pid"; first_status=$?
|
|
2158
|
+
expect_code 0 "$first_status" "first serialized config push failed"
|
|
2159
|
+
expect_code 0 "$second_status" "second serialized config push failed"
|
|
2160
|
+
second_instr=$(reread_instruction_path "$w/sm") \
|
|
2161
|
+
|| fail "second concurrent push did not publish its generation"
|
|
2162
|
+
[ "$first_instr" != "$second_instr" ] || fail "concurrent pushes reused a generation"
|
|
2163
|
+
[ "$(cat "$w/sm/config/crew-harness")" = two ] \
|
|
2164
|
+
|| fail "concurrent pushes did not converge the latest config bytes"
|
|
2165
|
+
# Delivery order is now the durable enqueue order: the steering-inbox
|
|
2166
|
+
# sequence numbers are the serialization evidence the typed log used to be.
|
|
2167
|
+
first_line=$(inbox_stream "$w/home/state" sm | grep -n -F "CONFIG_REREAD: $first_instr" | head -n 1 | cut -d: -f1)
|
|
2168
|
+
second_line=$(inbox_stream "$w/home/state" sm | grep -n -F "CONFIG_REREAD: $second_instr" | head -n 1 | cut -d: -f1)
|
|
2169
|
+
[ -n "$first_line" ] && [ -n "$second_line" ] && [ "$first_line" -lt "$second_line" ] \
|
|
2170
|
+
|| fail "concurrent pushes delivered generations out of order"
|
|
2171
|
+
pass "B21 config reread serializes concurrent propagation and delivery"
|
|
2172
|
+
}
|
|
2173
|
+
|
|
2174
|
+
test_config_reread_full_retry_queue_drains_before_new_push() {
|
|
2175
|
+
local w head retry_dir path n fakebin log out status pointer_count
|
|
2176
|
+
w=$(new_world config-reread-full-queue)
|
|
2177
|
+
head=$(git -C "$w/main" rev-parse HEAD)
|
|
2178
|
+
add_sm_worktree "$w" sm "$head"
|
|
2179
|
+
mkdir -p "$w/sm/config" "$w/sm/state"
|
|
2180
|
+
printf 'old\n' > "$w/sm/config/crew-harness"
|
|
2181
|
+
printf 'new\n' > "$w/home/config/crew-harness"
|
|
2182
|
+
retry_dir="$w/home/state/.fm-inherited-config-reread-retry/sm"
|
|
2183
|
+
mkdir -p "$retry_dir"
|
|
2184
|
+
for n in $(seq -w 1 16); do
|
|
2185
|
+
path="$retry_dir/.fm-inherited-config-reread.20260721T000000.$n"
|
|
2186
|
+
printf 'generation-%s\n' "$n" > "$path"
|
|
2187
|
+
chmod 0600 "$path"
|
|
2188
|
+
done
|
|
2189
|
+
fakebin=$(make_fake_toolchain "$w")
|
|
2190
|
+
log="$w/config-reread-full-queue.tmux.log"
|
|
2191
|
+
out=$(PATH="$fakebin:$BASE_PATH" FM_HOME="$w/home" FM_ROOT_OVERRIDE="$w/main" \
|
|
2192
|
+
FM_SEND_SETTLE=0 FM_FAKE_TMUX_LOG="$log" \
|
|
2193
|
+
"$ROOT/bin/fm-config-push.sh" 2>&1); status=$?
|
|
2194
|
+
expect_code 0 "$status" "a full retry queue should drain before a new push"
|
|
2195
|
+
assert_contains "$out" "config-reread: sent" \
|
|
2196
|
+
"a new config generation was not delivered after retry draining"
|
|
2197
|
+
[ "$(cat "$w/sm/config/crew-harness")" = new ] \
|
|
2198
|
+
|| fail "the new config generation did not propagate after retry draining"
|
|
2199
|
+
assert_no_reread_retry_stages "$w/home" sm
|
|
2200
|
+
pointer_count=$(inbox_stream "$w/home/state" sm | grep -c 'CONFIG_REREAD:' || true)
|
|
2201
|
+
[ "$pointer_count" -ge 17 ] \
|
|
2202
|
+
|| fail "full retry queue did not deliver all pending generations before the new one"
|
|
2203
|
+
pass "B22 full config reread retry queues drain before new publication"
|
|
2204
|
+
}
|
|
2205
|
+
|
|
2206
|
+
test_config_reread_cleanup_runs_after_mixed_delivery_failure() {
|
|
2207
|
+
local w head fakebin state_real fail_path path n report out status count
|
|
2208
|
+
w=$(new_world config-reread-mixed-delivery)
|
|
2209
|
+
head=$(git -C "$w/main" rev-parse HEAD)
|
|
2210
|
+
add_sm_worktree "$w" sm "$head"
|
|
2211
|
+
mkdir -p "$w/sm/state"
|
|
2212
|
+
state_real=$(cd "$w/sm/state" && pwd -P)
|
|
2213
|
+
fail_path="$state_real/.fm-inherited-config-reread.9999-fail"
|
|
2214
|
+
for n in $(seq -w 1 18); do
|
|
2215
|
+
path="$state_real/.fm-inherited-config-reread.00$n"
|
|
2216
|
+
printf 'generation-%s\n' "$n" > "$path"
|
|
2217
|
+
chmod 0600 "$path"
|
|
2218
|
+
done
|
|
2219
|
+
printf 'failed-generation\n' > "$fail_path"
|
|
2220
|
+
chmod 0600 "$fail_path"
|
|
2221
|
+
for path in "$state_real"/.fm-inherited-config-reread.*; do
|
|
2222
|
+
fm_config_reread_mark_pending "$path" "$path.pending" \
|
|
2223
|
+
|| fail "could not mark mixed-delivery generation pending"
|
|
2224
|
+
done
|
|
2225
|
+
fakebin=$(make_fake_toolchain "$w")
|
|
2226
|
+
# Fail only the .9999-fail generation's delivery, at the layer that can now
|
|
2227
|
+
# fail: the durable inbox enqueue. The staged steer record carries that
|
|
2228
|
+
# generation's pointer path in its body, so a content-matching mv wrapper
|
|
2229
|
+
# rejects exactly that one atomic publish and nothing else.
|
|
2230
|
+
real_mv=$(command -v mv)
|
|
2231
|
+
cat > "$fakebin/mv" <<SH
|
|
2232
|
+
#!/usr/bin/env bash
|
|
2233
|
+
if [ -f "\${1:-}" ] && grep -q '\.9999-fail' "\${1:-}" 2>/dev/null; then
|
|
2234
|
+
exit 1
|
|
2235
|
+
fi
|
|
2236
|
+
exec "$real_mv" "\$@"
|
|
2237
|
+
SH
|
|
2238
|
+
chmod +x "$fakebin/mv"
|
|
2239
|
+
report="$w/empty-reread.report"
|
|
2240
|
+
: > "$report"
|
|
2241
|
+
out=$(PATH="$fakebin:$BASE_PATH" FM_HOME="$w/home" FM_ROOT_OVERRIDE="$w/main" \
|
|
2242
|
+
FM_SEND_SETTLE=0 fm_config_send_reread_nudge sm "$w/sm" "$report" 2>&1); status=$?
|
|
2243
|
+
expect_code 1 "$status" "mixed delivery failure should remain diagnostic"
|
|
2244
|
+
assert_contains "$out" "CONFIG_REREAD: secondmate sm: send failed" \
|
|
2245
|
+
"mixed delivery failure diagnostic missing"
|
|
2246
|
+
count=0
|
|
2247
|
+
for path in "$state_real"/.fm-inherited-config-reread.*; do
|
|
2248
|
+
case "$path" in
|
|
2249
|
+
*.pending) continue ;;
|
|
2250
|
+
esac
|
|
2251
|
+
[ -f "$path" ] && [ ! -L "$path" ] || continue
|
|
2252
|
+
[ ! -e "$path.pending" ] || continue
|
|
2253
|
+
count=$((count + 1))
|
|
2254
|
+
done
|
|
2255
|
+
[ "$count" = 16 ] || fail "mixed delivery failure skipped bounded sent-history cleanup (count=$count)"
|
|
2256
|
+
assert_present "$fail_path.pending" "failed generation lost its retry marker"
|
|
2257
|
+
pass "B23 mixed config reread delivery failures still bound sent history"
|
|
2258
|
+
}
|
|
2259
|
+
|
|
2260
|
+
test_config_reread_stops_after_failed_generation() {
|
|
2261
|
+
local w fakebin state_real old new report log out status
|
|
2262
|
+
w=$(new_world config-reread-order)
|
|
2263
|
+
mkdir -p "$w/sm/state"
|
|
2264
|
+
state_real=$(cd "$w/sm/state" && pwd -P)
|
|
2265
|
+
old="$state_real/.fm-inherited-config-reread.0000-fail"
|
|
2266
|
+
new="$state_real/.fm-inherited-config-reread.0001-new"
|
|
2267
|
+
printf 'old-generation\n' > "$old"
|
|
2268
|
+
printf 'new-generation\n' > "$new"
|
|
2269
|
+
chmod 0600 "$old" "$new"
|
|
2270
|
+
fm_config_reread_mark_pending "$old" "$old.pending" \
|
|
2271
|
+
|| fail "could not mark older generation pending"
|
|
2272
|
+
fm_config_reread_mark_pending "$new" "$new.pending" \
|
|
2273
|
+
|| fail "could not mark newer generation pending"
|
|
2274
|
+
fakebin=$(make_fake_toolchain "$w")
|
|
2275
|
+
mv "$fakebin/tmux" "$fakebin/tmux.real"
|
|
2276
|
+
cat > "$fakebin/tmux" <<SH
|
|
2277
|
+
#!/usr/bin/env bash
|
|
2278
|
+
case "\$*" in
|
|
2279
|
+
*send-keys*'.0000-fail'*) exit 1 ;;
|
|
2280
|
+
esac
|
|
2281
|
+
exec "$fakebin/tmux.real" "\$@"
|
|
2282
|
+
SH
|
|
2283
|
+
chmod +x "$fakebin/tmux"
|
|
2284
|
+
report="$w/empty-reread.report"
|
|
2285
|
+
: > "$report"
|
|
2286
|
+
log="$w/config-reread-order.tmux.log"
|
|
2287
|
+
out=$(PATH="$fakebin:$BASE_PATH" FM_HOME="$w/home" FM_ROOT_OVERRIDE="$ROOT" \
|
|
2288
|
+
FM_SEND_SETTLE=0 FM_FAKE_TMUX_LOG="$log" \
|
|
2289
|
+
fm_config_send_reread_nudge sm "$w/sm" "$report" 2>&1); status=$?
|
|
2290
|
+
expect_code 1 "$status" "an older failed generation should remain diagnostic"
|
|
2291
|
+
assert_contains "$out" "CONFIG_REREAD: secondmate sm: send failed" \
|
|
2292
|
+
"older generation failure diagnostic missing"
|
|
2293
|
+
assert_not_contains "$(cat "$log" 2>/dev/null || true)" ".0001-new" \
|
|
2294
|
+
"newer generation was delivered after an older failure"
|
|
2295
|
+
assert_present "$old.pending" "older failed generation lost its retry marker"
|
|
2296
|
+
assert_present "$new.pending" "newer generation was sent after an older failure"
|
|
2297
|
+
pass "B26 config reread delivery stops after the oldest failed generation"
|
|
2298
|
+
}
|
|
2299
|
+
|
|
2300
|
+
test_bootstrap_detect_only_does_not_create_state() {
|
|
2301
|
+
local w fakebin detect_state out status
|
|
2302
|
+
w=$(new_world bootstrap-detect-only)
|
|
2303
|
+
detect_state="$w/detect-state"
|
|
2304
|
+
fakebin=$(make_fake_toolchain "$w")
|
|
2305
|
+
out=$(PATH="$fakebin:$BASE_PATH" FM_HOME="$w/home" FM_ROOT_OVERRIDE="$w/main" \
|
|
2306
|
+
FM_STATE_OVERRIDE="$detect_state" FM_BOOTSTRAP_DETECT_ONLY=1 \
|
|
2307
|
+
"$ROOT/bin/fm-bootstrap.sh" 2>&1); status=$?
|
|
2308
|
+
expect_code 0 "$status" "detect-only bootstrap should succeed"
|
|
2309
|
+
[ ! -e "$detect_state" ] || fail "detect-only bootstrap created its state directory"
|
|
2310
|
+
pass "B24 bootstrap detect-only mode remains filesystem read-only"
|
|
2311
|
+
}
|
|
2312
|
+
|
|
2313
|
+
test_config_reread_skips_when_unchanged_and_reads_after_push() {
|
|
2314
|
+
local w head log out err status report instr n path pending_instruction count
|
|
2315
|
+
w=$(new_world config-reread-after-push)
|
|
2316
|
+
head=$(git -C "$w/main" rev-parse HEAD)
|
|
2317
|
+
add_sm_worktree "$w" sm "$head"
|
|
2318
|
+
mkdir -p "$w/sm/config" "$w/sm/state"
|
|
2319
|
+
|
|
2320
|
+
printf 'codex\n' > "$w/home/config/crew-harness"
|
|
2321
|
+
printf 'codex\n' > "$w/sm/config/crew-harness"
|
|
2322
|
+
log="$w/config-reread-unchanged.tmux.log"
|
|
2323
|
+
out=$(run_config_push "$w" "$log" 2>/dev/null); status=$?
|
|
2324
|
+
expect_code 0 "$status" "unchanged push should succeed"
|
|
2325
|
+
assert_not_contains "$out" "config-reread: sent" "no reread when nothing changed"
|
|
2326
|
+
[ ! -s "$log" ] || fail "unchanged push still sent text: $(cat "$log")"
|
|
2327
|
+
assert_no_reread_instructions "$w/sm"
|
|
2328
|
+
|
|
2329
|
+
# Prove instruction bytes are taken from destination after write: if we only
|
|
2330
|
+
# read the primary source, a post-copy destination mutation would not matter.
|
|
2331
|
+
# Here we call the write helper after planting distinct dest bytes and a
|
|
2332
|
+
# pushed report line.
|
|
2333
|
+
printf '%s' 'destination-post-write' > "$w/sm/config/crew-harness"
|
|
2334
|
+
printf 'primary-source-only\n' > "$w/home/config/crew-harness"
|
|
2335
|
+
report="$w/after-push.report"
|
|
2336
|
+
printf '%s\n' $'crew-harness\tpushed\t' > "$report"
|
|
2337
|
+
instr="$w/sm/state/.fm-inherited-config-reread-dest"
|
|
2338
|
+
fm_config_write_reread_instruction "$w/sm" "$report" "$instr" \
|
|
2339
|
+
|| fail "destination-byte instruction write failed"
|
|
2340
|
+
assert_contains "$(cat "$instr")" "destination-post-write" \
|
|
2341
|
+
"instruction must use destination post-write bytes"
|
|
2342
|
+
assert_contains "$(cat "$instr")" $'destination-post-write-----END config/crew-harness-----' \
|
|
2343
|
+
"instruction must not append a byte to a non-newline-terminated destination"
|
|
2344
|
+
assert_not_contains "$(cat "$instr")" "primary-source-only" \
|
|
2345
|
+
"instruction must not fall back to primary source bytes"
|
|
2346
|
+
: > "$w/sm/config/crew-harness"
|
|
2347
|
+
fm_config_write_reread_instruction "$w/sm" "$report" "$instr" \
|
|
2348
|
+
|| fail "empty destination instruction write failed"
|
|
2349
|
+
assert_contains "$(cat "$instr")" $'-----BEGIN config/crew-harness-----
|
|
2350
|
+
-----END config/crew-harness-----' \
|
|
2351
|
+
"instruction must represent an empty destination without a synthetic byte"
|
|
2352
|
+
pending_instruction="$w/sm/state/.fm-inherited-config-reread.20260721T000000.01"
|
|
2353
|
+
printf '%s\n' generation > "$pending_instruction"
|
|
2354
|
+
fm_config_reread_mark_pending "$pending_instruction" "$pending_instruction.pending" \
|
|
2355
|
+
|| fail "could not create bounded-lifecycle pending marker"
|
|
2356
|
+
for n in $(seq -w 2 18); do
|
|
2357
|
+
path="$w/sm/state/.fm-inherited-config-reread.20260721T000000.$n"
|
|
2358
|
+
printf '%s\n' generation > "$path"
|
|
2359
|
+
chmod 0600 "$path"
|
|
2360
|
+
done
|
|
2361
|
+
fm_config_reread_cleanup_sent "$w/sm"
|
|
2362
|
+
count=0
|
|
2363
|
+
for path in "$w/sm/state"/.fm-inherited-config-reread.*; do
|
|
2364
|
+
case "$path" in
|
|
2365
|
+
*.pending) continue ;;
|
|
2366
|
+
esac
|
|
2367
|
+
[ -f "$path" ] && [ ! -L "$path" ] || continue
|
|
2368
|
+
[ ! -e "$path.pending" ] || continue
|
|
2369
|
+
count=$((count + 1))
|
|
2370
|
+
done
|
|
2371
|
+
[ "$count" = 16 ] || fail "sent reread generations were not bounded"
|
|
2372
|
+
assert_present "$pending_instruction" "cleanup deleted a pending reread generation"
|
|
2373
|
+
assert_present "$pending_instruction.pending" "cleanup deleted a pending reread marker"
|
|
2374
|
+
pass "B17 config reread skips unchanged homes and reads destination post-write bytes"
|
|
2375
|
+
}
|
|
2376
|
+
|
|
2377
|
+
test_config_reread_bootstrap_path_and_spawn_flexibility() {
|
|
2378
|
+
local w head log out fakebin sm launchlog launch instr report stale
|
|
2379
|
+
w=$(new_world config-reread-bootstrap)
|
|
2380
|
+
head=$(git -C "$w/main" rev-parse HEAD)
|
|
2381
|
+
add_sm_worktree "$w" sm "$head"
|
|
2382
|
+
mkdir -p "$w/sm/config" "$w/sm/state"
|
|
2383
|
+
printf 'old\n' > "$w/sm/config/crew-harness"
|
|
2384
|
+
printf 'codex\n' > "$w/home/config/crew-harness"
|
|
2385
|
+
|
|
2386
|
+
fakebin=$(make_fake_toolchain "$w")
|
|
2387
|
+
log="$w/bootstrap-reread.tmux.log"
|
|
2388
|
+
out=$(PATH="$fakebin:$BASE_PATH" FM_HOME="$w/home" FM_ROOT_OVERRIDE="$w/main" \
|
|
2389
|
+
FM_SEND_SETTLE=0 FM_FAKE_TMUX_LOG="$log" \
|
|
2390
|
+
"$ROOT/bin/fm-bootstrap.sh" 2>/dev/null)
|
|
2391
|
+
[ "$(cat "$w/sm/config/crew-harness")" = codex ] || fail "bootstrap did not push harness"
|
|
2392
|
+
instr=$(reread_instruction_path "$w/sm") || fail "bootstrap reread instruction missing"
|
|
2393
|
+
assert_present "$instr" "bootstrap must write a config reread instruction when config changed"
|
|
2394
|
+
assert_contains "$(inbox_stream "$w/home/state" sm)" "[fm-from-firstmate]" \
|
|
2395
|
+
"bootstrap config reread must use routed secondmate send"
|
|
2396
|
+
assert_contains "$(cat "$instr")" \
|
|
2397
|
+
$'-----BEGIN config/crew-harness-----\ncodex\n-----END config/crew-harness-----' \
|
|
2398
|
+
"bootstrap instruction must carry exact post-write harness bytes"
|
|
2399
|
+
|
|
2400
|
+
# fm-spawn still permits a conscious explicit runtime outside the config
|
|
2401
|
+
# (defaults/rules only - never harden spawn against deliberate choice).
|
|
2402
|
+
w=$(new_world config-reread-spawn-flex)
|
|
2403
|
+
printf 'codex\n' > "$w/home/config/crew-harness"
|
|
2404
|
+
printf 'codex\n' > "$w/home/config/secondmate-harness"
|
|
2405
|
+
sm="$w/sm-flex"
|
|
2406
|
+
make_seeded_home "$sm" sm-flex
|
|
2407
|
+
mkdir -p "$sm/state"
|
|
2408
|
+
report="$sm/state/stale-reread.report"
|
|
2409
|
+
printf '%s\n' $'crew-harness\tpushed\t' > "$report"
|
|
2410
|
+
stale="$sm/state/.fm-inherited-config-reread.spawn-stale"
|
|
2411
|
+
fm_config_write_reread_instruction "$sm" "$report" "$stale" \
|
|
2412
|
+
|| fail "could not create spawn stale reread generation"
|
|
2413
|
+
fm_config_reread_mark_pending "$stale" "$stale.pending" \
|
|
2414
|
+
|| fail "could not create spawn stale reread marker"
|
|
2415
|
+
launchlog="$w/spawn-flex.launch.log"
|
|
2416
|
+
spawn_secondmate_capture "$w" sm-flex "$sm" "$launchlog" --harness pi >/dev/null 2>&1
|
|
2417
|
+
assert_no_reread_pending "$sm"
|
|
2418
|
+
assert_no_reread_instructions "$sm"
|
|
2419
|
+
launch=$(cat "$launchlog")
|
|
2420
|
+
assert_contains "$launch" "pi" \
|
|
2421
|
+
"explicit --harness pi must still win over configured codex defaults"
|
|
2422
|
+
pass "B18 bootstrap config reread path works; spawn flexibility remains defaults-only"
|
|
2423
|
+
}
|
|
2424
|
+
|
|
2425
|
+
test_bootstrap_respawns_before_config_reread() {
|
|
2426
|
+
local w head fakebin log report stale
|
|
2427
|
+
w=$(new_world config-reread-respawn-order)
|
|
2428
|
+
head=$(git -C "$w/main" rev-parse HEAD)
|
|
2429
|
+
add_sm_worktree "$w" sm "$head"
|
|
2430
|
+
mkdir -p "$w/sm/config" "$w/sm/state"
|
|
2431
|
+
printf 'harness=codex\n' >> "$w/home/state/sm.meta"
|
|
2432
|
+
printf '%s' old > "$w/sm/config/crew-harness"
|
|
2433
|
+
printf '%s' codex > "$w/home/config/crew-harness"
|
|
2434
|
+
report="$w/sm/state/stale-reread.report"
|
|
2435
|
+
printf '%s\n' $'crew-harness\tpushed\t' > "$report"
|
|
2436
|
+
stale="$w/sm/state/.fm-inherited-config-reread.stale-generation"
|
|
2437
|
+
fm_config_write_reread_instruction "$w/sm" "$report" "$stale" \
|
|
2438
|
+
|| fail "could not create stale reread generation"
|
|
2439
|
+
fm_config_reread_mark_pending "$stale" "$stale.pending" \
|
|
2440
|
+
|| fail "could not create stale reread marker"
|
|
2441
|
+
log="$w/config-reread-respawn-order.log"
|
|
2442
|
+
|
|
2443
|
+
cat > "$w/main/bin/fm-spawn.sh" <<SH
|
|
2444
|
+
#!/usr/bin/env bash
|
|
2445
|
+
. '$w/main/bin/fm-config-inherit-lib.sh'
|
|
2446
|
+
printf '%s' spawn >> '$log'
|
|
2447
|
+
printf '%s' codex > '$w/sm/config/crew-harness'
|
|
2448
|
+
printf '%s\n' 7500 > '$w/sm/config/startup-memory-budget'
|
|
2449
|
+
SH
|
|
2450
|
+
chmod +x "$w/main/bin/fm-spawn.sh"
|
|
2451
|
+
fakebin=$(make_fake_toolchain "$w")
|
|
2452
|
+
cat > "$fakebin/tmux" <<SH
|
|
2453
|
+
#!/usr/bin/env bash
|
|
2454
|
+
case "\$*" in
|
|
2455
|
+
*display-message*'#{pane_current_command}'*) printf '%s' zsh ;;
|
|
2456
|
+
*display-message*'#{pane_id}'*) printf '%s' '%1' ;;
|
|
2457
|
+
*display-message*'#{cursor_y}'*) printf '%s' 0 ;;
|
|
2458
|
+
*capture-pane*) printf '❯\n'
|
|
2459
|
+
;;
|
|
2460
|
+
*send-keys*) printf '%s' send-keys >> '$log' ;;
|
|
2461
|
+
esac
|
|
2462
|
+
SH
|
|
2463
|
+
chmod +x "$fakebin/tmux"
|
|
2464
|
+
PATH="$fakebin:$BASE_PATH" FM_HOME="$w/home" FM_ROOT_OVERRIDE="$w/main" \
|
|
2465
|
+
FM_SEND_SETTLE=0 FM_FAKE_TMUX_LOG="$log" \
|
|
2466
|
+
"$ROOT/bin/fm-bootstrap.sh" >/dev/null 2>&1
|
|
2467
|
+
assert_contains "$(cat "$log")" "spawn" \
|
|
2468
|
+
"bootstrap did not respawn the dead secondmate"
|
|
2469
|
+
assert_not_contains "$(cat "$log")" "send-keys" \
|
|
2470
|
+
"bootstrap nudged a secondmate before its respawn completed"
|
|
2471
|
+
assert_present "$stale" "bootstrap removed the stale generation before relaunch handling"
|
|
2472
|
+
assert_present "$stale.pending" "bootstrap removed the stale marker before relaunch handling"
|
|
2473
|
+
fm_config_reread_discard_pending "$w/sm" || fail "could not clean respawn test generation"
|
|
2474
|
+
assert_no_reread_pending "$w/sm"
|
|
2475
|
+
assert_no_reread_instructions "$w/sm"
|
|
2476
|
+
pass "B19 bootstrap respawns before inherited-config reread"
|
|
2477
|
+
}
|
|
2478
|
+
|
|
2479
|
+
test_spawn_quarantines_pending_rereads_on_cleanup_failure() {
|
|
2480
|
+
local w sm report stale fakebin real_rm out status launchlog quarantine_root quarantined_count
|
|
2481
|
+
local quarantine_dirs before_quarantine_dirs after_quarantine_dirs n dir
|
|
2482
|
+
w=$(new_world config-reread-spawn-quarantine)
|
|
2483
|
+
sm="$w/sm"
|
|
2484
|
+
mkdir -p "$w/home/config"
|
|
2485
|
+
printf 'codex\n' > "$w/home/config/crew-harness"
|
|
2486
|
+
make_seeded_home "$sm" sm
|
|
2487
|
+
mkdir -p "$sm/state"
|
|
2488
|
+
report="$sm/state/stale-reread.report"
|
|
2489
|
+
printf '%s\n' $'crew-harness\tpushed\t' > "$report"
|
|
2490
|
+
stale="$sm/state/.fm-inherited-config-reread.spawn-stale"
|
|
2491
|
+
fm_config_write_reread_instruction "$sm" "$report" "$stale" \
|
|
2492
|
+
|| fail "could not create pending spawn reread generation"
|
|
2493
|
+
fm_config_reread_mark_pending "$stale" "$stale.pending" \
|
|
2494
|
+
|| fail "could not mark pending spawn reread generation"
|
|
2495
|
+
quarantine_root="$sm/state/.fm-inherited-config-reread-quarantine"
|
|
2496
|
+
mkdir -p "$quarantine_root"
|
|
2497
|
+
for n in $(seq -w 1 16); do
|
|
2498
|
+
dir="$quarantine_root/generation.old$n"
|
|
2499
|
+
mkdir -p "$dir"
|
|
2500
|
+
printf 'old-quarantine-%s\n' "$n" > "$dir/snapshot"
|
|
2501
|
+
printf 'hidden-quarantine-%s\n' "$n" > "$dir/.hidden-snapshot"
|
|
2502
|
+
done
|
|
2503
|
+
fakebin=$(make_launch_capturing_tmux "$w/tmux-spawn-quarantine")
|
|
2504
|
+
real_rm=$(command -v rm)
|
|
2505
|
+
cat > "$fakebin/rm" <<SH
|
|
2506
|
+
#!/usr/bin/env bash
|
|
2507
|
+
case "\$*" in
|
|
2508
|
+
*'.fm-inherited-config-reread.'*) exit 1 ;;
|
|
2509
|
+
esac
|
|
2510
|
+
exec "$real_rm" "\$@"
|
|
2511
|
+
SH
|
|
2512
|
+
chmod +x "$fakebin/rm"
|
|
2513
|
+
launchlog="$w/spawn-quarantine.launch.log"
|
|
2514
|
+
out=$(PATH="$fakebin:$BASE_PATH" TMUX='' CLAUDECODE=1 \
|
|
2515
|
+
FM_ROOT_OVERRIDE="$ROOT" FM_HOME="$w/home" \
|
|
2516
|
+
FM_STATE_OVERRIDE="$w/home/state" FM_DATA_OVERRIDE="$w/home/data" \
|
|
2517
|
+
FM_PROJECTS_OVERRIDE="$w/home/projects" FM_CONFIG_OVERRIDE="$w/home/config" \
|
|
2518
|
+
FM_SPAWN_NO_GUARD=1 FM_FAKE_LAUNCH_LOG="$launchlog" \
|
|
2519
|
+
"$ROOT/bin/fm-spawn.sh" sm "$sm" --secondmate 2>&1); status=$?
|
|
2520
|
+
expect_code 0 "$status" "spawn should remain available after reread cleanup failure"
|
|
2521
|
+
assert_contains "$out" "CONFIG_REREAD: secondmate sm: quarantined pre-relaunch generations" \
|
|
2522
|
+
"spawn cleanup failure did not emit a CONFIG_REREAD quarantine diagnostic"
|
|
2523
|
+
assert_no_reread_pending "$sm"
|
|
2524
|
+
assert_no_reread_instructions "$sm"
|
|
2525
|
+
assert_present "$quarantine_root" "spawn cleanup failure did not create a quarantine directory"
|
|
2526
|
+
quarantined_count=$(find "$quarantine_root" -type f | wc -l | tr -d ' ')
|
|
2527
|
+
[ "$quarantined_count" -ge 2 ] \
|
|
2528
|
+
|| fail "spawn cleanup failure did not quarantine both generation artifacts"
|
|
2529
|
+
quarantine_dirs=0
|
|
2530
|
+
for dir in "$quarantine_root"/generation.*; do
|
|
2531
|
+
[ -d "$dir" ] && [ ! -L "$dir" ] || continue
|
|
2532
|
+
quarantine_dirs=$((quarantine_dirs + 1))
|
|
2533
|
+
done
|
|
2534
|
+
[ "$quarantine_dirs" -le 16 ] \
|
|
2535
|
+
|| fail "spawn cleanup failure exceeded bounded quarantine history ($quarantine_dirs)"
|
|
2536
|
+
before_quarantine_dirs=$quarantine_dirs
|
|
2537
|
+
fm_config_reread_quarantine_pending "$sm" sm "$w/home" || true
|
|
2538
|
+
after_quarantine_dirs=0
|
|
2539
|
+
for dir in "$quarantine_root"/generation.*; do
|
|
2540
|
+
[ -d "$dir" ] && [ ! -L "$dir" ] || continue
|
|
2541
|
+
after_quarantine_dirs=$((after_quarantine_dirs + 1))
|
|
2542
|
+
done
|
|
2543
|
+
[ "$after_quarantine_dirs" = "$before_quarantine_dirs" ] \
|
|
2544
|
+
|| fail "empty quarantine cleanup created an extra generation directory"
|
|
2545
|
+
assert_not_contains "$(cat "$launchlog")" "CONFIG_REREAD:" \
|
|
2546
|
+
"spawn cleanup failure left a stale reread pointer eligible for delivery"
|
|
2547
|
+
pass "B25 spawn quarantines stale rereads without blocking relaunch"
|
|
2548
|
+
}
|
|
2549
|
+
|
|
2550
|
+
test_harness_resolution
|
|
2551
|
+
test_cursor_marker_detection
|
|
2552
|
+
test_secondmate_model_effort_tokens
|
|
2553
|
+
test_pi_signed_detection_and_session_lock_identity
|
|
2554
|
+
test_dash_leading_process_names_are_basename_operands
|
|
2555
|
+
test_propagate_lib
|
|
2556
|
+
test_spawn_split_and_inherit
|
|
2557
|
+
test_spawn_backward_compat_crew_fallback
|
|
2558
|
+
test_spawn_bare_backward_compat
|
|
2559
|
+
test_spawn_explicit_harness_wins
|
|
2560
|
+
test_spawn_unverified_secondmate_harness_refused
|
|
2561
|
+
test_spawn_cursor_secondmate_launches_with_its_primary_contract
|
|
2562
|
+
test_spawn_backend_precedence_over_inherited_config
|
|
2563
|
+
test_spawn_explicit_backend_precedence_over_env_and_inherited_config
|
|
2564
|
+
test_spawn_bare_harness_no_model_effort_flag
|
|
2565
|
+
test_spawn_secondmate_harness_model_token
|
|
2566
|
+
test_spawn_secondmate_harness_model_and_effort_tokens
|
|
2567
|
+
test_spawn_explicit_model_overrides_secondmate_harness_token
|
|
2568
|
+
test_spawn_explicit_effort_overrides_secondmate_harness_token
|
|
2569
|
+
test_spawn_explicit_harness_does_not_inherit_secondmate_harness_tokens
|
|
2570
|
+
test_spawn_explicit_harness_uses_explicit_profile_axes
|
|
2571
|
+
test_spawned_secondmate_uses_its_harness_supervision_model
|
|
2572
|
+
test_spawn_fallback_chain_and_crew_scout_unaffected
|
|
2573
|
+
test_bootstrap_sweep_propagates_and_reconverges
|
|
2574
|
+
test_bootstrap_sweep_propagates_when_tracked_current
|
|
2575
|
+
test_bootstrap_sweep_defers_dispatch_on_stale_unignored_home
|
|
2576
|
+
test_bootstrap_sweep_materializes_and_inherits_memory_default
|
|
2577
|
+
test_backend_inheritance_present_and_absent
|
|
2578
|
+
test_presentation_inheritance_default_on_and_opt_out
|
|
2579
|
+
test_bootstrap_sweep_surfaces_config_propagation_failure
|
|
2580
|
+
test_bootstrap_rereads_after_partial_propagation
|
|
2581
|
+
test_config_push_propagates_reports_without_ff_or_nudge
|
|
2582
|
+
test_config_push_reports_skips_dirty_and_invalid_home
|
|
2583
|
+
test_config_push_exits_nonzero_on_copy_error
|
|
2584
|
+
test_config_push_rereads_after_partial_propagation
|
|
2585
|
+
test_config_reread_per_home_changed_sets_and_exact_bytes
|
|
2586
|
+
test_config_reread_isolation_and_absent_and_send_failure
|
|
2587
|
+
test_config_reread_publication_failure_retries_exact_generation
|
|
2588
|
+
test_config_reread_write_failure_retains_exact_retry_generation
|
|
2589
|
+
test_config_reread_exact_temp_survives_adoption_failure
|
|
2590
|
+
test_config_reread_serializes_concurrent_pushes
|
|
2591
|
+
test_config_reread_full_retry_queue_drains_before_new_push
|
|
2592
|
+
test_config_reread_cleanup_runs_after_mixed_delivery_failure
|
|
2593
|
+
test_config_reread_stops_after_failed_generation
|
|
2594
|
+
test_config_reread_skips_when_unchanged_and_reads_after_push
|
|
2595
|
+
test_config_reread_bootstrap_path_and_spawn_flexibility
|
|
2596
|
+
test_bootstrap_respawns_before_config_reread
|
|
2597
|
+
test_spawn_quarantines_pending_rereads_on_cleanup_failure
|
|
2598
|
+
test_bootstrap_detect_only_does_not_create_state
|
|
2599
|
+
|
|
2600
|
+
echo "# all fm-secondmate-harness tests passed"
|