@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,1822 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Behavior tests for the primary turn-end supervision guard (docs/turnend-guard.md).
|
|
3
|
+
#
|
|
4
|
+
# Two layers:
|
|
5
|
+
# PREDICATE - bin/fm-supervision-lib.sh, the shared beacon/status computation
|
|
6
|
+
# used by fm-guard.sh and by the hook's banner details.
|
|
7
|
+
# HOOK - bin/fm-turnend-guard.sh, the shared primary hook predicate that
|
|
8
|
+
# scopes in-flight work to the PRIMARY checkout only and requires
|
|
9
|
+
# a live, identity-matched watcher lock plus a fresh beacon.
|
|
10
|
+
# All hermetic over temp dirs; no real agent session is invoked.
|
|
11
|
+
set -u
|
|
12
|
+
|
|
13
|
+
# shellcheck source=tests/lib.sh
|
|
14
|
+
. "$(dirname "${BASH_SOURCE[0]}")/lib.sh"
|
|
15
|
+
|
|
16
|
+
# shellcheck source=/dev/null
|
|
17
|
+
. "$ROOT/bin/fm-supervision-lib.sh"
|
|
18
|
+
|
|
19
|
+
TMP_ROOT=$(fm_test_tmproot fm-turnend-guard)
|
|
20
|
+
fm_git_identity fmtest fmtest@example.invalid
|
|
21
|
+
|
|
22
|
+
REQUIRED_REASON='watcher supervision needs Stop-owned automatic recovery; inspect the hook registration and startup status before ending the turn'
|
|
23
|
+
|
|
24
|
+
# --- PREDICATE: bin/fm-supervision-lib.sh -----------------------------------
|
|
25
|
+
|
|
26
|
+
test_predicate_healthy_no_inflight() {
|
|
27
|
+
local state="$TMP_ROOT/pred-empty/state"
|
|
28
|
+
mkdir -p "$state"
|
|
29
|
+
if fm_supervision_unhealthy "$state" 300; then
|
|
30
|
+
fail "predicate reported unhealthy with zero in-flight tasks"
|
|
31
|
+
fi
|
|
32
|
+
[ "$FM_SUP_IN_FLIGHT" -eq 0 ] || fail "expected zero in-flight, got $FM_SUP_IN_FLIGHT"
|
|
33
|
+
pass "fm_supervision_unhealthy: false with no state/*.meta at all"
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
test_predicate_unhealthy_no_beacon() {
|
|
37
|
+
local state="$TMP_ROOT/pred-nobeat/state"
|
|
38
|
+
mkdir -p "$state"
|
|
39
|
+
: > "$state/task1.meta"
|
|
40
|
+
fm_supervision_unhealthy "$state" 300 || fail "predicate did not fire: in-flight task, beacon never seen"
|
|
41
|
+
[ "$FM_SUP_IN_FLIGHT" -eq 1 ] || fail "expected 1 in-flight, got $FM_SUP_IN_FLIGHT"
|
|
42
|
+
[ "$FM_SUP_WATCHER_FRESH" = false ] || fail "beacon absent must not read as fresh"
|
|
43
|
+
[ "$FM_SUP_BEACON_DESC" = never ] || fail "beacon description should be 'never', got $FM_SUP_BEACON_DESC"
|
|
44
|
+
pass "fm_supervision_unhealthy: true with in-flight task and no beacon ever"
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
test_predicate_unhealthy_stale_beacon() {
|
|
48
|
+
local state="$TMP_ROOT/pred-stale/state"
|
|
49
|
+
mkdir -p "$state"
|
|
50
|
+
: > "$state/task1.meta"
|
|
51
|
+
touch -t 202001010000 "$state/.last-watcher-beat"
|
|
52
|
+
fm_supervision_unhealthy "$state" 300 || fail "predicate did not fire: in-flight task, beacon far outside grace"
|
|
53
|
+
[ "$FM_SUP_WATCHER_FRESH" = false ] || fail "an ancient beacon must not read as fresh"
|
|
54
|
+
pass "fm_supervision_unhealthy: true with in-flight task and a beacon far outside the grace window"
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
test_predicate_healthy_fresh_beacon() {
|
|
58
|
+
local state="$TMP_ROOT/pred-fresh/state"
|
|
59
|
+
mkdir -p "$state"
|
|
60
|
+
: > "$state/task1.meta"
|
|
61
|
+
touch "$state/.last-watcher-beat"
|
|
62
|
+
if fm_supervision_unhealthy "$state" 300; then
|
|
63
|
+
fail "predicate fired despite a fresh beacon"
|
|
64
|
+
fi
|
|
65
|
+
[ "$FM_SUP_WATCHER_FRESH" = true ] || fail "a beacon touched just now must read as fresh"
|
|
66
|
+
pass "fm_supervision_unhealthy: false with in-flight task and a fresh beacon"
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
test_predicate_queue_pending_flag() {
|
|
70
|
+
local state="$TMP_ROOT/pred-queue/state"
|
|
71
|
+
mkdir -p "$state"
|
|
72
|
+
fm_supervision_status "$state" 300
|
|
73
|
+
[ "$FM_SUP_QUEUE_PENDING" = false ] || fail "empty/absent wake queue must not read as pending"
|
|
74
|
+
printf 'record\n' > "$state/.wake-queue"
|
|
75
|
+
fm_supervision_status "$state" 300
|
|
76
|
+
[ "$FM_SUP_QUEUE_PENDING" = true ] || fail "a non-empty wake queue must read as pending"
|
|
77
|
+
pass "fm_supervision_status: FM_SUP_QUEUE_PENDING tracks state/.wake-queue"
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
test_predicate_x_mode_needs_supervision() {
|
|
81
|
+
local state="$TMP_ROOT/pred-x-mode/state"
|
|
82
|
+
mkdir -p "$state"
|
|
83
|
+
: > "$state/x-watch.check.sh"
|
|
84
|
+
fm_supervision_needed "$state" 300 || fail "X-mode relay poll did not register as supervision need"
|
|
85
|
+
[ "$FM_SUP_IN_FLIGHT" -eq 0 ] || fail "X-mode relay poll must not count as an in-flight task"
|
|
86
|
+
[ "$FM_SUP_NEEDED" = true ] || fail "X-mode relay poll must set FM_SUP_NEEDED"
|
|
87
|
+
fm_supervision_unhealthy "$state" 300 || fail "X-mode relay poll with no beacon must be unhealthy"
|
|
88
|
+
pass "fm_supervision_needed: X-mode relay poll needs supervision"
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
test_predicate_source_needs_supervision() {
|
|
92
|
+
local state="$TMP_ROOT/pred-source/state"
|
|
93
|
+
mkdir -p "$state/procevent"
|
|
94
|
+
: > "$state/procevent/source-only.source"
|
|
95
|
+
fm_supervision_unhealthy "$state" 300 || fail "registered source with no beacon must be unhealthy"
|
|
96
|
+
[ "$FM_SUP_IN_FLIGHT" -eq 0 ] || fail "a process-event source must not count as a task"
|
|
97
|
+
[ "$FM_SUP_SOURCES" -eq 1 ] || fail "expected one registered process-event source"
|
|
98
|
+
pass "fm_supervision_unhealthy: source-only home needs supervision"
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
# --- HOOK: bin/fm-turnend-guard.sh ------------------------------------------
|
|
102
|
+
#
|
|
103
|
+
# Each scenario gets its own directory carrying a copy of the two guard scripts
|
|
104
|
+
# under bin/, so the hook (invoked by absolute path) resolves its own FM_ROOT to
|
|
105
|
+
# that scenario dir regardless of the test's cwd.
|
|
106
|
+
|
|
107
|
+
install_guard_scripts() {
|
|
108
|
+
local dir=$1
|
|
109
|
+
mkdir -p "$dir/bin"
|
|
110
|
+
cp "$ROOT/bin/fm-turnend-guard.sh" "$dir/bin/fm-turnend-guard.sh"
|
|
111
|
+
cp "$ROOT/bin/fm-turnend-guard-grok.sh" "$dir/bin/fm-turnend-guard-grok.sh"
|
|
112
|
+
cp "$ROOT/bin/fm-operational-input.sh" "$dir/bin/fm-operational-input.sh"
|
|
113
|
+
cp "$ROOT/bin/fm-supervision-instructions.sh" "$dir/bin/fm-supervision-instructions.sh"
|
|
114
|
+
cp "$ROOT/bin/fm-harness.sh" "$dir/bin/fm-harness.sh"
|
|
115
|
+
cp "$ROOT/bin/fm-primary-scope-lib.sh" "$dir/bin/fm-primary-scope-lib.sh"
|
|
116
|
+
cp "$ROOT/bin/fm-supervision-lib.sh" "$dir/bin/fm-supervision-lib.sh"
|
|
117
|
+
cp "$ROOT/bin/fm-wake-lib.sh" "$dir/bin/fm-wake-lib.sh"
|
|
118
|
+
cp "$ROOT/bin/fm-hook-host-lib.sh" "$dir/bin/fm-hook-host-lib.sh"
|
|
119
|
+
mkdir -p "$dir/docs"
|
|
120
|
+
cp -R "$ROOT/docs/supervision-protocols" "$dir/docs/supervision-protocols"
|
|
121
|
+
chmod +x "$dir/bin/fm-turnend-guard.sh" "$dir/bin/fm-turnend-guard-grok.sh" "$dir/bin/fm-operational-input.sh" "$dir/bin/fm-supervision-instructions.sh" "$dir/bin/fm-harness.sh"
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
mark_codex_hook_root() {
|
|
125
|
+
local dir=$1
|
|
126
|
+
mkdir -p "$dir/.codex"
|
|
127
|
+
printf '{"hooks":{"Stop":[{"hooks":[{"type":"command","command":"fm-turnend-guard.sh"}]}]}}\n' > "$dir/.codex/hooks.json"
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
# A primary-shaped checkout: plain (non-worktree) git repo, AGENTS.md, bin/,
|
|
131
|
+
# state/ - everything the hook's scoping check requires to treat it as primary.
|
|
132
|
+
make_primary_dir() {
|
|
133
|
+
local dir=$1
|
|
134
|
+
mkdir -p "$dir/state"
|
|
135
|
+
git init -q "$dir"
|
|
136
|
+
git -C "$dir" commit -q --allow-empty -m init
|
|
137
|
+
: > "$dir/AGENTS.md"
|
|
138
|
+
install_guard_scripts "$dir"
|
|
139
|
+
printf '%s\n' "$dir"
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
# Same shape as primary, plus the .fm-secondmate-home marker bin/fm-home-seed.sh
|
|
143
|
+
# writes at seed time (regardless of treehouse-lease or git-clone acquisition).
|
|
144
|
+
make_secondmate_dir() {
|
|
145
|
+
local dir=$1
|
|
146
|
+
make_primary_dir "$dir" >/dev/null
|
|
147
|
+
printf 'sm-test-1\n' > "$dir/.fm-secondmate-home"
|
|
148
|
+
printf '%s\n' "$dir"
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
# A genuine linked `git worktree` of a base repo - the shape bin/fm-spawn.sh
|
|
152
|
+
# always hands crewmate/scout tasks working on firstmate itself. git-dir and
|
|
153
|
+
# git-common-dir differ here, unlike a plain checkout.
|
|
154
|
+
make_crewmate_worktree_dir() {
|
|
155
|
+
local base=$1 dir=$2
|
|
156
|
+
fm_git_worktree "$base" "$dir" fm/turnend-guard-test-branch
|
|
157
|
+
mkdir -p "$dir/state"
|
|
158
|
+
: > "$dir/AGENTS.md"
|
|
159
|
+
install_guard_scripts "$dir"
|
|
160
|
+
printf '%s\n' "$dir"
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
# A secondmate home's OWN child crew/scout worktree: a genuine linked git
|
|
164
|
+
# worktree of the secondmate home, so git-dir != git-common-dir exactly as for a
|
|
165
|
+
# main-home child worktree. A child worktree never carries the gitignored
|
|
166
|
+
# .fm-secondmate-home marker, so the marker force-include never fires for it and
|
|
167
|
+
# it stays exempt through the linked-worktree git-dir test.
|
|
168
|
+
make_secondmate_child_worktree_dir() {
|
|
169
|
+
local home=$1 dir=$2
|
|
170
|
+
git -C "$home" worktree add --quiet -b fm/turnend-secondmate-child "$dir"
|
|
171
|
+
mkdir -p "$dir/state"
|
|
172
|
+
: > "$dir/AGENTS.md"
|
|
173
|
+
install_guard_scripts "$dir"
|
|
174
|
+
printf '%s\n' "$dir"
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
# A treehouse-leased secondmate HOME: a genuine linked `git worktree` (git-dir !=
|
|
178
|
+
# git-common-dir, exactly like a default treehouse-leased home) that DOES carry a
|
|
179
|
+
# valid .fm-secondmate-home marker. This is the production topology the plain
|
|
180
|
+
# git-init secondmate fixture cannot represent; the guard must force-INCLUDE it
|
|
181
|
+
# as a guarded primary via the marker, not exempt it as a linked worktree.
|
|
182
|
+
make_secondmate_linked_home_dir() {
|
|
183
|
+
local base=$1 dir=$2
|
|
184
|
+
fm_git_worktree "$base" "$dir" fm/turnend-secondmate-linked-home
|
|
185
|
+
mkdir -p "$dir/state"
|
|
186
|
+
: > "$dir/AGENTS.md"
|
|
187
|
+
install_guard_scripts "$dir"
|
|
188
|
+
printf 'sm-linked-1\n' > "$dir/.fm-secondmate-home"
|
|
189
|
+
printf '%s\n' "$dir"
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
run_hook() {
|
|
193
|
+
local dir=$1 stop_active=$2 home
|
|
194
|
+
home=$(cd "$dir" && pwd)
|
|
195
|
+
printf '{"stop_hook_active":%s}' "$stop_active" | CLAUDECODE=1 FM_HOME="$home" bash "$dir/bin/fm-turnend-guard.sh" 2>&1
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
nonexistent_pid() {
|
|
199
|
+
local pid=999999
|
|
200
|
+
while kill -0 "$pid" 2>/dev/null; do
|
|
201
|
+
pid=$((pid + 1))
|
|
202
|
+
done
|
|
203
|
+
printf '%s\n' "$pid"
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
watcher_identity() {
|
|
207
|
+
local dir=$1 pid=$2
|
|
208
|
+
FM_STATE_OVERRIDE="$dir/state" bash -c '. "$1"; fm_pid_identity "$2"' _ "$dir/bin/fm-wake-lib.sh" "$pid"
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
record_watcher_lock() {
|
|
212
|
+
local dir=$1 pid=$2 identity=$3 root bin_dir
|
|
213
|
+
root=$(cd "$dir" && pwd)
|
|
214
|
+
bin_dir=$(cd "$dir/bin" && pwd)
|
|
215
|
+
mkdir -p "$dir/state/.watch.lock"
|
|
216
|
+
printf '%s\n' "$pid" > "$dir/state/.watch.lock/pid"
|
|
217
|
+
printf '%s\n' "$root" > "$dir/state/.watch.lock/fm-home"
|
|
218
|
+
printf '%s\n' "$bin_dir/fm-watch.sh" > "$dir/state/.watch.lock/watcher-path"
|
|
219
|
+
printf '%s\n' "$identity" > "$dir/state/.watch.lock/pid-identity"
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
test_hook_silent_when_no_work_in_flight() {
|
|
223
|
+
local dir out status
|
|
224
|
+
dir=$(make_primary_dir "$TMP_ROOT/hook-idle")
|
|
225
|
+
out=$(run_hook "$dir" false); status=$?
|
|
226
|
+
expect_code 0 "$status" "hook must exit 0 with no in-flight work"
|
|
227
|
+
[ -z "$out" ] || fail "hook produced output with no in-flight work: $out"
|
|
228
|
+
pass "fm-turnend-guard: silent no-op with nothing in flight"
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
test_hook_blocks_when_fresh_beacon_has_no_live_lock() {
|
|
232
|
+
local dir out status
|
|
233
|
+
dir=$(make_primary_dir "$TMP_ROOT/hook-fresh-no-lock")
|
|
234
|
+
: > "$dir/state/task1.meta"
|
|
235
|
+
touch "$dir/state/.last-watcher-beat"
|
|
236
|
+
out=$(run_hook "$dir" false); status=$?
|
|
237
|
+
expect_code 2 "$status" "hook must block when a fresh beacon has no live watcher lock"
|
|
238
|
+
assert_contains "$out" "$REQUIRED_REASON" "block reason must contain the exact required instruction"
|
|
239
|
+
pass "fm-turnend-guard: blocks when a fresh beacon has no live watcher lock"
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
test_hook_blocks_source_only_home() {
|
|
243
|
+
local dir out status
|
|
244
|
+
dir=$(make_primary_dir "$TMP_ROOT/hook-source-only")
|
|
245
|
+
mkdir -p "$dir/state/procevent"
|
|
246
|
+
: > "$dir/state/procevent/source-only.source"
|
|
247
|
+
out=$(run_hook "$dir" false); status=$?
|
|
248
|
+
expect_code 2 "$status" "non-Claude hook must block when a source-only home has no watcher"
|
|
249
|
+
assert_contains "$out" "1 process-event source(s) registered" "block reason must identify the source-only supervision need"
|
|
250
|
+
pass "fm-turnend-guard: non-Claude path blocks a source-only home"
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
test_hook_blocks_when_dead_lock_has_fresh_beacon() {
|
|
254
|
+
local dir dead out status
|
|
255
|
+
dir=$(make_primary_dir "$TMP_ROOT/hook-dead-lock-fresh")
|
|
256
|
+
dead=$(nonexistent_pid)
|
|
257
|
+
: > "$dir/state/task1.meta"
|
|
258
|
+
record_watcher_lock "$dir" "$dead" "dead watcher identity"
|
|
259
|
+
touch "$dir/state/.last-watcher-beat"
|
|
260
|
+
out=$(run_hook "$dir" false); status=$?
|
|
261
|
+
expect_code 2 "$status" "hook must block when the watcher lock pid is dead despite a fresh beacon"
|
|
262
|
+
assert_contains "$out" "$REQUIRED_REASON" "block reason must contain the exact required instruction"
|
|
263
|
+
pass "fm-turnend-guard: blocks on a dead watcher lock even when the beacon is fresh"
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
test_hook_silent_with_live_lock_and_fresh_beacon() {
|
|
267
|
+
local dir pid identity out status
|
|
268
|
+
dir=$(make_primary_dir "$TMP_ROOT/hook-live-lock-fresh")
|
|
269
|
+
: > "$dir/state/task1.meta"
|
|
270
|
+
sleep 60 &
|
|
271
|
+
pid=$!
|
|
272
|
+
identity=$(watcher_identity "$dir" "$pid") || {
|
|
273
|
+
kill "$pid" 2>/dev/null || true
|
|
274
|
+
wait "$pid" 2>/dev/null || true
|
|
275
|
+
fail "could not identify live watcher holder"
|
|
276
|
+
}
|
|
277
|
+
record_watcher_lock "$dir" "$pid" "$identity"
|
|
278
|
+
touch "$dir/state/.last-watcher-beat"
|
|
279
|
+
out=$(run_hook "$dir" false); status=$?
|
|
280
|
+
kill "$pid" 2>/dev/null || true
|
|
281
|
+
wait "$pid" 2>/dev/null || true
|
|
282
|
+
expect_code 0 "$status" "hook must exit 0 with a live identity-matched watcher lock and fresh beacon"
|
|
283
|
+
[ -z "$out" ] || fail "hook produced output despite a live fresh watcher lock: $out"
|
|
284
|
+
pass "fm-turnend-guard: silent no-op with a live watcher lock and fresh beacon"
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
test_hook_non_claude_health_ignores_claude_budget_contention() {
|
|
288
|
+
local dir home pid identity holder harness payload out status
|
|
289
|
+
dir=$(make_primary_dir "$TMP_ROOT/hook-non-claude-budget-contention")
|
|
290
|
+
home=$(cd "$dir" && pwd)
|
|
291
|
+
: > "$dir/state/task1.meta"
|
|
292
|
+
sleep 60 &
|
|
293
|
+
pid=$!
|
|
294
|
+
identity=$(watcher_identity "$dir" "$pid") || {
|
|
295
|
+
kill "$pid" 2>/dev/null || true
|
|
296
|
+
wait "$pid" 2>/dev/null || true
|
|
297
|
+
fail "could not identify non-Claude contention watcher"
|
|
298
|
+
}
|
|
299
|
+
record_watcher_lock "$dir" "$pid" "$identity"
|
|
300
|
+
touch "$dir/state/.last-watcher-beat"
|
|
301
|
+
printf 'session=claude-episode\ncount=3\nepoch=9\n' > "$dir/state/.turnend-claude-blocks"
|
|
302
|
+
printf 'notice-state\n' > "$dir/state/.claude-autoarm-failure-notified"
|
|
303
|
+
printf 'alarm-state\n' > "$dir/state/.claude-autoarm-failure-alarmed"
|
|
304
|
+
sleep 60 &
|
|
305
|
+
holder=$!
|
|
306
|
+
mkdir -p "$dir/state/.turnend-claude-blocks.lock"
|
|
307
|
+
printf '%s\n' "$holder" > "$dir/state/.turnend-claude-blocks.lock/pid"
|
|
308
|
+
while IFS='|' read -r harness payload; do
|
|
309
|
+
out=$(printf '%s' "$payload" | FM_HOME="$home" bash "$dir/bin/fm-turnend-guard.sh" 2>&1); status=$?
|
|
310
|
+
expect_code 0 "$status" "$harness healthy path must ignore Claude budget-lock contention"
|
|
311
|
+
[ -z "$out" ] || fail "$harness healthy path produced output: $out"
|
|
312
|
+
[ "$(cat "$dir/state/.turnend-claude-blocks")" = $'session=claude-episode\ncount=3\nepoch=9' ] \
|
|
313
|
+
|| fail "$harness healthy path mutated the Claude block budget"
|
|
314
|
+
[ "$(cat "$dir/state/.claude-autoarm-failure-notified")" = notice-state ] \
|
|
315
|
+
|| fail "$harness healthy path mutated the Claude failure notice"
|
|
316
|
+
[ "$(cat "$dir/state/.claude-autoarm-failure-alarmed")" = alarm-state ] \
|
|
317
|
+
|| fail "$harness healthy path mutated the Claude attended alarm"
|
|
318
|
+
[ "$(cat "$dir/state/.turnend-claude-blocks.lock/pid")" = "$holder" ] \
|
|
319
|
+
|| fail "$harness healthy path replaced the Claude budget-lock owner"
|
|
320
|
+
done <<EOF
|
|
321
|
+
default|{"stop_hook_active":false}
|
|
322
|
+
Codex|{"cwd":"$dir","stop_hook_active":false}
|
|
323
|
+
OpenCode|{"stop_hook_active":false}
|
|
324
|
+
Pi|{"stop_hook_active":false}
|
|
325
|
+
pi-signed|{"stop_hook_active":false}
|
|
326
|
+
Grok|{"sessionId":"grok-session","stopHookActive":false}
|
|
327
|
+
Kimi|{"stop_hook_active":false}
|
|
328
|
+
EOF
|
|
329
|
+
kill "$holder" "$pid" 2>/dev/null || true
|
|
330
|
+
wait "$holder" "$pid" 2>/dev/null || true
|
|
331
|
+
pass "fm-turnend-guard: healthy non-Claude harness paths ignore Claude episode contention"
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
test_hook_blocks_with_live_lock_and_stale_beacon() {
|
|
335
|
+
local dir pid identity out status
|
|
336
|
+
dir=$(make_primary_dir "$TMP_ROOT/hook-live-lock-stale")
|
|
337
|
+
: > "$dir/state/task1.meta"
|
|
338
|
+
sleep 60 &
|
|
339
|
+
pid=$!
|
|
340
|
+
identity=$(watcher_identity "$dir" "$pid") || {
|
|
341
|
+
kill "$pid" 2>/dev/null || true
|
|
342
|
+
wait "$pid" 2>/dev/null || true
|
|
343
|
+
fail "could not identify live watcher holder"
|
|
344
|
+
}
|
|
345
|
+
record_watcher_lock "$dir" "$pid" "$identity"
|
|
346
|
+
touch -t 202001010000 "$dir/state/.last-watcher-beat"
|
|
347
|
+
out=$(run_hook "$dir" false); status=$?
|
|
348
|
+
kill "$pid" 2>/dev/null || true
|
|
349
|
+
wait "$pid" 2>/dev/null || true
|
|
350
|
+
expect_code 2 "$status" "hook must block when a live watcher lock has an ancient beacon"
|
|
351
|
+
assert_contains "$out" "$REQUIRED_REASON" "block reason must contain the exact required instruction"
|
|
352
|
+
pass "fm-turnend-guard: blocks on a live watcher lock with an ancient beacon"
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
test_hook_blocks_when_unhealthy_in_primary() {
|
|
356
|
+
local dir out status
|
|
357
|
+
dir=$(make_primary_dir "$TMP_ROOT/hook-block")
|
|
358
|
+
: > "$dir/state/task1.meta"
|
|
359
|
+
out=$(run_hook "$dir" false); status=$?
|
|
360
|
+
expect_code 2 "$status" "hook must block (exit 2) when in-flight work has no live watcher"
|
|
361
|
+
assert_contains "$out" "$REQUIRED_REASON" "block reason must contain the exact required instruction"
|
|
362
|
+
assert_contains "$out" "TURN WOULD END BLIND" "block banner must read as an alarm"
|
|
363
|
+
pass "fm-turnend-guard: blocks with the exact required reason in the primary when unhealthy"
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
test_hook_blocks_from_fm_home_state() {
|
|
367
|
+
local dir home out status
|
|
368
|
+
dir=$(make_primary_dir "$TMP_ROOT/hook-fm-home")
|
|
369
|
+
home="$TMP_ROOT/hook-fm-home-op"
|
|
370
|
+
mkdir -p "$home/state"
|
|
371
|
+
: > "$home/state/task1.meta"
|
|
372
|
+
out=$(printf '{"stop_hook_active":false}' | CLAUDECODE=1 FM_HOME="$home" bash "$dir/bin/fm-turnend-guard.sh" 2>&1); status=$?
|
|
373
|
+
expect_code 2 "$status" "hook must inspect the active FM_HOME state dir"
|
|
374
|
+
assert_contains "$out" "$REQUIRED_REASON" "block reason must contain the exact required instruction"
|
|
375
|
+
pass "fm-turnend-guard: blocks from active FM_HOME state, not only repo-root state"
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
test_hook_x_mode_reason_sources_cadence() {
|
|
379
|
+
local dir home out status
|
|
380
|
+
dir=$(make_primary_dir "$TMP_ROOT/hook-x-mode")
|
|
381
|
+
home=$(cd "$dir" && pwd)
|
|
382
|
+
mkdir -p "$dir/config"
|
|
383
|
+
: > "$dir/config/x-mode.env"
|
|
384
|
+
: > "$dir/state/task1.meta"
|
|
385
|
+
out=$(run_hook "$dir" false); status=$?
|
|
386
|
+
expect_code 2 "$status" "hook must block when in-flight X-mode work has no live watcher"
|
|
387
|
+
assert_contains "$out" "source '$home/config/x-mode.env' first" "block reason must source the effective X-mode cadence"
|
|
388
|
+
pass "fm-turnend-guard: X-mode repair reason sources the cadence config"
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
test_hook_x_mode_only_blocks_in_default_mode() {
|
|
392
|
+
local dir out status
|
|
393
|
+
dir=$(make_primary_dir "$TMP_ROOT/hook-x-mode-only")
|
|
394
|
+
: > "$dir/state/x-watch.check.sh"
|
|
395
|
+
out=$(run_hook "$dir" false); status=$?
|
|
396
|
+
expect_code 2 "$status" "default hook mode must block an X-mode-only blind turn"
|
|
397
|
+
assert_contains "$out" "X-mode relay polling needs supervision" "X-mode-only blind stop must identify its supervision need"
|
|
398
|
+
pass "fm-turnend-guard: X-mode-only supervision remains guarded in default mode"
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
test_hook_ignores_repo_state_when_fm_home_set() {
|
|
402
|
+
local dir home out status
|
|
403
|
+
dir=$(make_primary_dir "$TMP_ROOT/hook-fm-home-ignore-root")
|
|
404
|
+
home="$TMP_ROOT/hook-fm-home-quiet"
|
|
405
|
+
mkdir -p "$home/state"
|
|
406
|
+
: > "$dir/state/task1.meta"
|
|
407
|
+
out=$(printf '{"stop_hook_active":false}' | FM_HOME="$home" bash "$dir/bin/fm-turnend-guard.sh" 2>&1); status=$?
|
|
408
|
+
expect_code 0 "$status" "hook must ignore repo-root state when FM_HOME selects another state dir"
|
|
409
|
+
[ -z "$out" ] || fail "hook produced output from stale repo-root state despite FM_HOME: $out"
|
|
410
|
+
pass "fm-turnend-guard: ignores stale repo-root state when FM_HOME is set"
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
test_hook_uses_state_override() {
|
|
414
|
+
local dir home state out status
|
|
415
|
+
dir=$(make_primary_dir "$TMP_ROOT/hook-state-override")
|
|
416
|
+
home="$TMP_ROOT/hook-state-override-home"
|
|
417
|
+
state="$TMP_ROOT/hook-state-override-active"
|
|
418
|
+
mkdir -p "$home/state" "$state"
|
|
419
|
+
: > "$state/task1.meta"
|
|
420
|
+
out=$(printf '{"stop_hook_active":false}' | CLAUDECODE=1 FM_HOME="$home" FM_STATE_OVERRIDE="$state" bash "$dir/bin/fm-turnend-guard.sh" 2>&1); status=$?
|
|
421
|
+
expect_code 2 "$status" "hook must let FM_STATE_OVERRIDE win over FM_HOME/state"
|
|
422
|
+
assert_contains "$out" "$REQUIRED_REASON" "block reason must contain the exact required instruction"
|
|
423
|
+
pass "fm-turnend-guard: uses FM_STATE_OVERRIDE ahead of FM_HOME/state"
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
test_hook_loop_guard_allows_retry() {
|
|
427
|
+
local dir out status
|
|
428
|
+
dir=$(make_primary_dir "$TMP_ROOT/hook-loopguard")
|
|
429
|
+
: > "$dir/state/task1.meta"
|
|
430
|
+
out=$(run_hook "$dir" true); status=$?
|
|
431
|
+
expect_code 0 "$status" "hook must allow the stop when stop_hook_active is already true"
|
|
432
|
+
[ -z "$out" ] || fail "hook produced output on the loop-guarded retry: $out"
|
|
433
|
+
pass "fm-turnend-guard: stop_hook_active=true always allows the stop (never blocks twice in one turn)"
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
# A secondmate's OWN home runs a primary firstmate session and must be guarded
|
|
437
|
+
# exactly like the main primary. This was the guard's proven blind spot: the
|
|
438
|
+
# .fm-secondmate-home marker used to early-exit here, so an overnight secondmate
|
|
439
|
+
# could end a turn with an unsupervised child and sit blind. Removing that marker
|
|
440
|
+
# check makes the guard fire, mirroring the cd-guard.
|
|
441
|
+
test_hook_blocks_in_secondmate_own_home() {
|
|
442
|
+
local dir out status
|
|
443
|
+
dir=$(make_secondmate_dir "$TMP_ROOT/hook-secondmate")
|
|
444
|
+
: > "$dir/state/task1.meta"
|
|
445
|
+
out=$(run_hook "$dir" false); status=$?
|
|
446
|
+
expect_code 2 "$status" "hook must guard a secondmate's own home like the main primary when unhealthy"
|
|
447
|
+
assert_contains "$out" "$REQUIRED_REASON" "block reason must contain the exact required instruction"
|
|
448
|
+
assert_contains "$out" "TURN WOULD END BLIND" "block banner must read as an alarm"
|
|
449
|
+
pass "fm-turnend-guard: blocks a blind turn end in a secondmate's own home (.fm-secondmate-home no longer excludes it)"
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
# Idle-by-default: an empty-queue secondmate has no in-flight meta, so the guard
|
|
453
|
+
# exits at the in-flight gate - never forcing a busy continuation loop.
|
|
454
|
+
test_hook_silent_in_idle_secondmate_home() {
|
|
455
|
+
local dir out status
|
|
456
|
+
dir=$(make_secondmate_dir "$TMP_ROOT/hook-secondmate-idle")
|
|
457
|
+
out=$(run_hook "$dir" false); status=$?
|
|
458
|
+
expect_code 0 "$status" "hook must stay silent in an idle, empty-queue secondmate home"
|
|
459
|
+
[ -z "$out" ] || fail "idle secondmate home produced guard output: $out"
|
|
460
|
+
pass "fm-turnend-guard: idle-by-default - silent in a secondmate home with nothing in flight"
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
# The stop_hook_active loop guard bounds the secondmate to one forced
|
|
464
|
+
# continuation per turn, exactly as it does for the main primary - no wedged,
|
|
465
|
+
# un-endable session.
|
|
466
|
+
test_hook_secondmate_loop_guard_allows_retry() {
|
|
467
|
+
local dir out status
|
|
468
|
+
dir=$(make_secondmate_dir "$TMP_ROOT/hook-secondmate-loopguard")
|
|
469
|
+
: > "$dir/state/task1.meta"
|
|
470
|
+
out=$(run_hook "$dir" true); status=$?
|
|
471
|
+
expect_code 0 "$status" "hook must allow the stop in a secondmate home when stop_hook_active is already true"
|
|
472
|
+
[ -z "$out" ] || fail "secondmate loop-guarded retry produced output: $out"
|
|
473
|
+
pass "fm-turnend-guard: stop_hook_active=true allows the stop in a secondmate home (never blocks twice in one turn)"
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
# The guard's half of the deferred-death recovery loop in a secondmate home,
|
|
477
|
+
# proven deterministically without a live model or any daemon: silent while the
|
|
478
|
+
# watcher is live (the secondmate ends its turn and relies on the background
|
|
479
|
+
# re-invoke), then blocks to force the re-arm once the watcher has exited and a
|
|
480
|
+
# second child event lands. The live half - that Claude Code autonomously
|
|
481
|
+
# re-invokes the model when the background watcher exits (Mechanism A) - is a
|
|
482
|
+
# harness property recorded empirically in docs/turnend-guard.md; it needs a live
|
|
483
|
+
# session and cannot be a hermetic CI assertion.
|
|
484
|
+
test_hook_secondmate_reinvoke_recovery_loop() {
|
|
485
|
+
local dir pid identity out status
|
|
486
|
+
dir=$(make_secondmate_dir "$TMP_ROOT/hook-secondmate-reinvoke")
|
|
487
|
+
: > "$dir/state/child1.meta"
|
|
488
|
+
sleep 60 &
|
|
489
|
+
pid=$!
|
|
490
|
+
identity=$(watcher_identity "$dir" "$pid") || {
|
|
491
|
+
kill "$pid" 2>/dev/null || true
|
|
492
|
+
wait "$pid" 2>/dev/null || true
|
|
493
|
+
fail "could not identify live watcher holder"
|
|
494
|
+
}
|
|
495
|
+
record_watcher_lock "$dir" "$pid" "$identity"
|
|
496
|
+
touch "$dir/state/.last-watcher-beat"
|
|
497
|
+
out=$(run_hook "$dir" false); status=$?
|
|
498
|
+
expect_code 0 "$status" "secondmate turn must end silently while its watcher is live (Stop #1)"
|
|
499
|
+
[ -z "$out" ] || {
|
|
500
|
+
kill "$pid" 2>/dev/null || true
|
|
501
|
+
wait "$pid" 2>/dev/null || true
|
|
502
|
+
fail "guard nagged a healthy secondmate at Stop #1: $out"
|
|
503
|
+
}
|
|
504
|
+
# The watcher exits on the wake (its normal lifecycle) and a SECOND child event
|
|
505
|
+
# lands. On the re-invoked recovery turn the secondmate must re-arm; if it did
|
|
506
|
+
# not, the guard blocks that turn's end and forces the re-arm (Stop #2).
|
|
507
|
+
kill "$pid" 2>/dev/null || true
|
|
508
|
+
wait "$pid" 2>/dev/null || true
|
|
509
|
+
rm -rf "$dir/state/.watch.lock"
|
|
510
|
+
: > "$dir/state/child2.meta"
|
|
511
|
+
touch "$dir/state/.last-watcher-beat"
|
|
512
|
+
out=$(run_hook "$dir" false); status=$?
|
|
513
|
+
expect_code 2 "$status" "secondmate recovery turn must not end blind after the watcher exits (Stop #2)"
|
|
514
|
+
assert_contains "$out" "$REQUIRED_REASON" "block reason must contain the exact required instruction"
|
|
515
|
+
pass "fm-turnend-guard: secondmate deferred-death recovery - silent while watched, forces re-arm once the watcher exits"
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
# The marker force-include must guard only the secondmate's OWN home, never its
|
|
519
|
+
# children: a secondmate's linked crew/scout worktree carries no marker, so it
|
|
520
|
+
# stays exempt by the same git-dir/git-common-dir test that exempts the main
|
|
521
|
+
# home's children.
|
|
522
|
+
test_hook_silent_in_secondmate_child_worktree() {
|
|
523
|
+
local home dir out status
|
|
524
|
+
home=$(make_secondmate_dir "$TMP_ROOT/hook-sm-child-home")
|
|
525
|
+
dir="$TMP_ROOT/hook-sm-child-wt"
|
|
526
|
+
make_secondmate_child_worktree_dir "$home" "$dir" >/dev/null
|
|
527
|
+
: > "$dir/state/task1.meta"
|
|
528
|
+
out=$(run_hook "$dir" false); status=$?
|
|
529
|
+
expect_code 0 "$status" "hook must stay exempt in a secondmate's own child crew/scout worktree"
|
|
530
|
+
[ -z "$out" ] || fail "hook produced output inside a secondmate's child worktree: $out"
|
|
531
|
+
pass "fm-turnend-guard: inert in a secondmate's own child worktree (linked git worktree) even when unhealthy"
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
# THE regression the plain git-init fixtures masked: a treehouse-leased secondmate
|
|
535
|
+
# home is a genuine LINKED worktree (git-dir != git-common-dir), which the
|
|
536
|
+
# remove-only form wrongly exempted. With the marker force-include, its own
|
|
537
|
+
# primary session is GUARDED. The test asserts the fixture really is a linked
|
|
538
|
+
# worktree so it can never silently regress back into a plain-checkout shape.
|
|
539
|
+
test_hook_blocks_in_treehouse_leased_secondmate_home() {
|
|
540
|
+
local base dir gd gcd out status
|
|
541
|
+
base="$TMP_ROOT/hook-sm-leased-base"
|
|
542
|
+
dir="$TMP_ROOT/hook-sm-leased-home"
|
|
543
|
+
make_secondmate_linked_home_dir "$base" "$dir" >/dev/null
|
|
544
|
+
gd=$(git -C "$dir" rev-parse --git-dir)
|
|
545
|
+
gcd=$(git -C "$dir" rev-parse --git-common-dir)
|
|
546
|
+
[ "$gd" != "$gcd" ] || fail "leased-home fixture must be a linked worktree (git-dir != git-common-dir), got equal: $gd"
|
|
547
|
+
: > "$dir/state/task1.meta"
|
|
548
|
+
out=$(run_hook "$dir" false); status=$?
|
|
549
|
+
expect_code 2 "$status" "hook must GUARD a treehouse-leased (linked) secondmate home via its marker when unhealthy"
|
|
550
|
+
assert_contains "$out" "$REQUIRED_REASON" "block reason must contain the exact required instruction"
|
|
551
|
+
assert_contains "$out" "TURN WOULD END BLIND" "block banner must read as an alarm"
|
|
552
|
+
pass "fm-turnend-guard: blocks a blind turn end in a treehouse-leased LINKED secondmate home (marker force-include)"
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
# Anti-spoof: a linked worktree with an INVALID (empty) marker must NOT be
|
|
556
|
+
# force-included. Marker validation rejects it, so it falls through to the
|
|
557
|
+
# linked-worktree exemption and stays exempt - a stray/empty marker file can
|
|
558
|
+
# never spoof a child worktree into being guarded.
|
|
559
|
+
test_hook_exempts_linked_worktree_with_stray_marker() {
|
|
560
|
+
local base dir out status
|
|
561
|
+
base="$TMP_ROOT/hook-stray-marker-base"
|
|
562
|
+
dir="$TMP_ROOT/hook-stray-marker-wt"
|
|
563
|
+
make_crewmate_worktree_dir "$base" "$dir" >/dev/null
|
|
564
|
+
: > "$dir/.fm-secondmate-home"
|
|
565
|
+
: > "$dir/state/task1.meta"
|
|
566
|
+
out=$(run_hook "$dir" false); status=$?
|
|
567
|
+
expect_code 0 "$status" "an empty/invalid marker must not spoof force-inclusion in a linked worktree"
|
|
568
|
+
[ -z "$out" ] || fail "stray empty marker wrongly force-included a linked worktree: $out"
|
|
569
|
+
pass "fm-turnend-guard: an invalid (empty) marker cannot spoof inclusion; linked worktree stays exempt"
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
# Anti-spoof under any locale: a NON-ASCII marker id must be REJECTED by the
|
|
573
|
+
# ASCII-only (C-collation) allowlist, so it can never force-include a linked
|
|
574
|
+
# worktree even where the ambient locale's collation would treat it as a letter.
|
|
575
|
+
# Rejection -> git-dir exemption -> the linked worktree stays exempt.
|
|
576
|
+
test_hook_exempts_linked_worktree_with_non_ascii_marker() {
|
|
577
|
+
local base dir out status
|
|
578
|
+
base="$TMP_ROOT/hook-nonascii-marker-base"
|
|
579
|
+
dir="$TMP_ROOT/hook-nonascii-marker-wt"
|
|
580
|
+
make_crewmate_worktree_dir "$base" "$dir" >/dev/null
|
|
581
|
+
printf 'caf\xc3\xa9\n' > "$dir/.fm-secondmate-home"
|
|
582
|
+
: > "$dir/state/task1.meta"
|
|
583
|
+
out=$(run_hook "$dir" false); status=$?
|
|
584
|
+
expect_code 0 "$status" "a non-ASCII marker id must not spoof force-inclusion in a linked worktree"
|
|
585
|
+
[ -z "$out" ] || fail "non-ASCII marker wrongly force-included a linked worktree: $out"
|
|
586
|
+
pass "fm-turnend-guard: a non-ASCII marker cannot spoof inclusion; linked worktree stays exempt"
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
test_hook_silent_in_crewmate_worktree() {
|
|
590
|
+
local base dir out status
|
|
591
|
+
base="$TMP_ROOT/hook-crew-base"
|
|
592
|
+
dir="$TMP_ROOT/hook-crew-wt"
|
|
593
|
+
make_crewmate_worktree_dir "$base" "$dir" >/dev/null
|
|
594
|
+
: > "$dir/state/task1.meta"
|
|
595
|
+
out=$(run_hook "$dir" false); status=$?
|
|
596
|
+
expect_code 0 "$status" "hook must never block inside a crewmate task worktree"
|
|
597
|
+
[ -z "$out" ] || fail "hook produced output inside a crewmate task worktree: $out"
|
|
598
|
+
pass "fm-turnend-guard: inert in a crewmate/scout task worktree (linked git worktree) even when unhealthy"
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
test_hook_silent_without_jq() {
|
|
602
|
+
local dir out status fakebin tool tool_path
|
|
603
|
+
dir=$(make_primary_dir "$TMP_ROOT/hook-nojq")
|
|
604
|
+
: > "$dir/state/task1.meta"
|
|
605
|
+
fakebin=$(fm_fakebin "$TMP_ROOT/hook-nojq-fake")
|
|
606
|
+
for tool in bash sh git cat printf date uname stat mkdir dirname; do
|
|
607
|
+
tool_path=$(command -v "$tool") || fail "test host must provide $tool"
|
|
608
|
+
ln -s "$tool_path" "$fakebin/$tool"
|
|
609
|
+
done
|
|
610
|
+
out=$(printf '{"stop_hook_active":false}' | PATH="$fakebin" bash "$dir/bin/fm-turnend-guard.sh" 2>&1)
|
|
611
|
+
status=$?
|
|
612
|
+
expect_code 0 "$status" "hook must fail open (exit 0) when jq is unavailable"
|
|
613
|
+
[ -z "$out" ] || fail "hook produced output without jq: $out"
|
|
614
|
+
pass "fm-turnend-guard: fails open (never blocks) when jq is missing"
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
test_hook_silent_without_stdin() {
|
|
618
|
+
local dir out status
|
|
619
|
+
dir=$(make_primary_dir "$TMP_ROOT/hook-nostdin")
|
|
620
|
+
: > "$dir/state/task1.meta"
|
|
621
|
+
out=$(bash "$dir/bin/fm-turnend-guard.sh" < /dev/null 2>&1); status=$?
|
|
622
|
+
expect_code 0 "$status" "hook must exit 0 on empty/absent stdin"
|
|
623
|
+
[ -z "$out" ] || fail "hook produced output on empty stdin: $out"
|
|
624
|
+
pass "fm-turnend-guard: silent no-op on empty stdin"
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
test_hook_runs_fast() {
|
|
628
|
+
local dir start elapsed_s
|
|
629
|
+
dir=$(make_primary_dir "$TMP_ROOT/hook-timing")
|
|
630
|
+
: > "$dir/state/task1.meta"
|
|
631
|
+
start=$SECONDS
|
|
632
|
+
run_hook "$dir" false >/dev/null
|
|
633
|
+
elapsed_s=$((SECONDS - start))
|
|
634
|
+
[ "$elapsed_s" -lt 3 ] || fail "hook took ${elapsed_s}s, expected well under a second (generous 3s CI margin)"
|
|
635
|
+
pass "fm-turnend-guard: runs well under the generous timing margin (${elapsed_s}s)"
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
test_grok_adapter_forces_one_resume_when_unhealthy() {
|
|
639
|
+
local dir fakebin log out status
|
|
640
|
+
dir=$(make_primary_dir "$TMP_ROOT/grok-adapter-block")
|
|
641
|
+
: > "$dir/state/task1.meta"
|
|
642
|
+
fakebin=$(fm_fakebin "$TMP_ROOT/grok-adapter-fakebin")
|
|
643
|
+
log="$TMP_ROOT/grok-adapter-call.log"
|
|
644
|
+
cat > "$fakebin/grok" <<EOF
|
|
645
|
+
#!/usr/bin/env bash
|
|
646
|
+
{
|
|
647
|
+
printf 'active=%s\n' "\${GROK_TURNEND_GUARD_ACTIVE:-}"
|
|
648
|
+
printf 'home=%s\n' "\${GROK_HOME:-}"
|
|
649
|
+
printf 'args:'
|
|
650
|
+
for arg in "\$@"; do
|
|
651
|
+
printf ' <%s>' "\$arg"
|
|
652
|
+
done
|
|
653
|
+
printf '\n'
|
|
654
|
+
} >> "$log"
|
|
655
|
+
EOF
|
|
656
|
+
chmod +x "$fakebin/grok"
|
|
657
|
+
out=$(printf '{"sessionId":"session-test","hookEventName":"stop"}' | PATH="$fakebin:$PATH" GROK_WORKSPACE_ROOT="$dir" bash "$dir/bin/fm-turnend-guard-grok.sh" 2>&1); status=$?
|
|
658
|
+
expect_code 0 "$status" "grok adapter must fail open after queuing a forced resume"
|
|
659
|
+
[ -z "$out" ] || fail "grok adapter printed output: $out"
|
|
660
|
+
assert_contains "$(cat "$log")" 'active=1' "grok adapter must mark its forced resume as loop-guarded"
|
|
661
|
+
assert_contains "$(cat "$log")" '<--resume>' "grok adapter must resume the current session"
|
|
662
|
+
assert_contains "$(cat "$log")" '<session-test>' "grok adapter must pass the hook session id"
|
|
663
|
+
assert_not_contains "$(cat "$log")" '<--permission-mode>' "grok adapter must not add a stronger permission mode"
|
|
664
|
+
assert_not_contains "$(cat "$log")" '<bypassPermissions>' "grok adapter must not bypass permissions on forced resume"
|
|
665
|
+
assert_contains "$(cat "$log")" 'FIRSTMATE_OP: v1 turn-end-guard: TURN WOULD END BLIND' "grok adapter must retain the typed guard kind"
|
|
666
|
+
pass "fm-turnend-guard-grok: forces one explicitly marked same-session resume when the shared predicate blocks"
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
test_grok_adapter_loop_guard_skips_resume() {
|
|
670
|
+
local dir fakebin log out status
|
|
671
|
+
dir=$(make_primary_dir "$TMP_ROOT/grok-adapter-loop")
|
|
672
|
+
: > "$dir/state/task1.meta"
|
|
673
|
+
fakebin=$(fm_fakebin "$TMP_ROOT/grok-adapter-loop-fakebin")
|
|
674
|
+
log="$TMP_ROOT/grok-adapter-loop-call.log"
|
|
675
|
+
cat > "$fakebin/grok" <<EOF
|
|
676
|
+
#!/usr/bin/env bash
|
|
677
|
+
printf 'called\n' >> "$log"
|
|
678
|
+
EOF
|
|
679
|
+
chmod +x "$fakebin/grok"
|
|
680
|
+
out=$(printf '{"sessionId":"session-test","hookEventName":"stop"}' | PATH="$fakebin:$PATH" GROK_WORKSPACE_ROOT="$dir" GROK_TURNEND_GUARD_ACTIVE=1 bash "$dir/bin/fm-turnend-guard-grok.sh" 2>&1); status=$?
|
|
681
|
+
expect_code 0 "$status" "grok adapter must allow its own forced resume turn to end"
|
|
682
|
+
[ -z "$out" ] || fail "grok adapter printed output while loop-guarded: $out"
|
|
683
|
+
[ ! -e "$log" ] || fail "grok adapter spawned another resume while loop-guarded: $(cat "$log")"
|
|
684
|
+
pass "fm-turnend-guard-grok: legacy environment loop guard prevents a nested resume loop"
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
test_grok_adapter_native_false_blocks_without_resume() {
|
|
688
|
+
local dir fakebin log out status
|
|
689
|
+
dir=$(make_primary_dir "$TMP_ROOT/grok-native-false")
|
|
690
|
+
: > "$dir/state/task1.meta"
|
|
691
|
+
fakebin=$(fm_fakebin "$TMP_ROOT/grok-native-false-bin")
|
|
692
|
+
log="$TMP_ROOT/grok-native-false.log"
|
|
693
|
+
printf '#!/usr/bin/env bash\nprintf called >> %q\n' "$log" > "$fakebin/grok"
|
|
694
|
+
chmod +x "$fakebin/grok"
|
|
695
|
+
out=$(printf '%s' '{"sessionId":"native","stopHookActive":false}' | PATH="$fakebin:$PATH" GROK_WORKSPACE_ROOT="$dir" bash "$dir/bin/fm-turnend-guard-grok.sh" 2>&1); status=$?
|
|
696
|
+
expect_code 2 "$status" "native stopHookActive=false must return the shared blocking status"
|
|
697
|
+
assert_contains "$out" 'TURN WOULD END BLIND' "native block must pass shared guard feedback to Grok"
|
|
698
|
+
[ ! -e "$log" ] || fail "native path started grok --resume"
|
|
699
|
+
pass "fm-turnend-guard-grok: native false delegates blocking feedback with zero resume processes"
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
test_grok_adapter_native_true_allows_without_resume() {
|
|
703
|
+
local dir fakebin log out status
|
|
704
|
+
dir=$(make_primary_dir "$TMP_ROOT/grok-native-true")
|
|
705
|
+
: > "$dir/state/task1.meta"
|
|
706
|
+
fakebin=$(fm_fakebin "$TMP_ROOT/grok-native-true-bin")
|
|
707
|
+
log="$TMP_ROOT/grok-native-true.log"
|
|
708
|
+
printf '#!/usr/bin/env bash\nprintf called >> %q\n' "$log" > "$fakebin/grok"
|
|
709
|
+
chmod +x "$fakebin/grok"
|
|
710
|
+
out=$(printf '%s' '{"sessionId":"native","stopHookActive":true}' | PATH="$fakebin:$PATH" GROK_WORKSPACE_ROOT="$dir" bash "$dir/bin/fm-turnend-guard-grok.sh" 2>&1); status=$?
|
|
711
|
+
expect_code 0 "$status" "native stopHookActive=true must allow the bounded continuation to stop"
|
|
712
|
+
[ -z "$out" ] || fail "native true produced output: $out"
|
|
713
|
+
[ ! -e "$log" ] || fail "native true started grok --resume"
|
|
714
|
+
pass "fm-turnend-guard-grok: native true remains bounded and starts no resume process"
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
test_grok_adapter_snake_case_native_and_camel_precedence() {
|
|
718
|
+
local dir out status
|
|
719
|
+
dir=$(make_primary_dir "$TMP_ROOT/grok-native-spellings")
|
|
720
|
+
: > "$dir/state/task1.meta"
|
|
721
|
+
out=$(printf '%s' '{"sessionId":"native","stop_hook_active":false}' | GROK_WORKSPACE_ROOT="$dir" bash "$dir/bin/fm-turnend-guard-grok.sh" 2>&1); status=$?
|
|
722
|
+
expect_code 2 "$status" "typed snake_case false must select native blocking"
|
|
723
|
+
assert_contains "$out" 'TURN WOULD END BLIND' "snake_case native block lost feedback"
|
|
724
|
+
out=$(printf '%s' '{"sessionId":"native","stopHookActive":true,"stop_hook_active":false}' | GROK_WORKSPACE_ROOT="$dir" bash "$dir/bin/fm-turnend-guard-grok.sh" 2>&1); status=$?
|
|
725
|
+
expect_code 0 "$status" "camelCase true must win over snake_case false"
|
|
726
|
+
out=$(printf '%s' '{"sessionId":"native","stopHookActive":false,"stop_hook_active":true}' | GROK_WORKSPACE_ROOT="$dir" bash "$dir/bin/fm-turnend-guard-grok.sh" 2>&1); status=$?
|
|
727
|
+
expect_code 2 "$status" "camelCase false must win over snake_case true"
|
|
728
|
+
pass "fm-turnend-guard-grok: both spellings are typed and camelCase has deterministic precedence"
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
test_grok_adapter_invalid_inputs_start_neither_path() {
|
|
732
|
+
local dir fakebin log payload out status
|
|
733
|
+
dir=$(make_primary_dir "$TMP_ROOT/grok-invalid-inputs")
|
|
734
|
+
: > "$dir/state/task1.meta"
|
|
735
|
+
fakebin=$(fm_fakebin "$TMP_ROOT/grok-invalid-bin")
|
|
736
|
+
log="$TMP_ROOT/grok-invalid.log"
|
|
737
|
+
printf '#!/usr/bin/env bash\nprintf called >> %q\n' "$log" > "$fakebin/grok"
|
|
738
|
+
chmod +x "$fakebin/grok"
|
|
739
|
+
for payload in \
|
|
740
|
+
' ' \
|
|
741
|
+
'{' \
|
|
742
|
+
'{"sessionId":"x","stopHookActive":"false"}' \
|
|
743
|
+
'{"sessionId":"x","stop_hook_active":1}' \
|
|
744
|
+
'{"sessionId":"x"}{"sessionId":"y"}' \
|
|
745
|
+
'{"sessionId":"x","stopHookActive":false}{"sessionId":"y","stopHookActive":false}' \
|
|
746
|
+
'{"sessionId":"x","stopHookActive":"bad","stopHookActive":false}' \
|
|
747
|
+
'{"sessionId":"x","stop_hook_active":false,"stop_hook_active":false}' \
|
|
748
|
+
'{"sessionId":"x","sessionId":"y"}'
|
|
749
|
+
do
|
|
750
|
+
out=$(printf '%s' "$payload" | PATH="$fakebin:$PATH" GROK_WORKSPACE_ROOT="$dir" bash "$dir/bin/fm-turnend-guard-grok.sh" 2>&1); status=$?
|
|
751
|
+
expect_code 0 "$status" "invalid Grok payload must conservatively allow without choosing a path"
|
|
752
|
+
[ -z "$out" ] || fail "invalid Grok payload produced output: $out"
|
|
753
|
+
done
|
|
754
|
+
[ ! -e "$log" ] || fail "invalid Grok payload started a resume process"
|
|
755
|
+
out=$(printf '%s' '{"sessionId":"x","stopHookActive":false}' | PATH="$fakebin:$PATH" GROK_WORKSPACE_ROOT="$TMP_ROOT/missing-grok-root" bash "$dir/bin/fm-turnend-guard-grok.sh" 2>&1); status=$?
|
|
756
|
+
expect_code 0 "$status" "missing shared-guard prerequisite must conservatively allow"
|
|
757
|
+
[ -z "$out" ] || fail "missing prerequisite produced output: $out"
|
|
758
|
+
[ ! -e "$log" ] || fail "missing prerequisite started a resume process"
|
|
759
|
+
pass "fm-turnend-guard-grok: malformed, invalidly typed, and missing-prerequisite payloads start neither path"
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
test_grok_adapter_missing_jq_and_no_supervision_allow() {
|
|
763
|
+
local dir fakebin log out status tool tool_path
|
|
764
|
+
dir=$(make_primary_dir "$TMP_ROOT/grok-nojq")
|
|
765
|
+
: > "$dir/state/task1.meta"
|
|
766
|
+
fakebin=$(fm_fakebin "$TMP_ROOT/grok-nojq-bin")
|
|
767
|
+
log="$TMP_ROOT/grok-nojq.log"
|
|
768
|
+
for tool in bash cat printf; do
|
|
769
|
+
tool_path=$(command -v "$tool") || fail "test host must provide $tool"
|
|
770
|
+
ln -s "$tool_path" "$fakebin/$tool"
|
|
771
|
+
done
|
|
772
|
+
printf '#!/usr/bin/env bash\nprintf called >> %q\n' "$log" > "$fakebin/grok"
|
|
773
|
+
chmod +x "$fakebin/grok"
|
|
774
|
+
out=$(printf '%s' '{"sessionId":"x","stopHookActive":false}' | PATH="$fakebin" GROK_WORKSPACE_ROOT="$dir" bash "$dir/bin/fm-turnend-guard-grok.sh" 2>&1); status=$?
|
|
775
|
+
expect_code 0 "$status" "missing jq must conservatively allow"
|
|
776
|
+
[ -z "$out" ] || fail "missing jq produced output: $out"
|
|
777
|
+
[ ! -e "$log" ] || fail "missing jq started a resume process"
|
|
778
|
+
|
|
779
|
+
dir=$(make_primary_dir "$TMP_ROOT/grok-native-no-work")
|
|
780
|
+
out=$(printf '%s' '{"sessionId":"x","stopHookActive":false}' | GROK_WORKSPACE_ROOT="$dir" bash "$dir/bin/fm-turnend-guard-grok.sh" 2>&1); status=$?
|
|
781
|
+
expect_code 0 "$status" "healthy no-supervision-needed native stop must allow"
|
|
782
|
+
[ -z "$out" ] || fail "no-supervision-needed native stop produced output: $out"
|
|
783
|
+
pass "fm-turnend-guard-grok: missing jq and no-supervision-needed stops stay silent and bounded"
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
# Grok loads Claude-compatible settings, so a TRACKED .claude/settings.json entry
|
|
787
|
+
# that also has a .grok/hooks/ counterpart must refuse to run under Grok, or the
|
|
788
|
+
# home gets a duplicate path. The regression this pins: the guard once tested
|
|
789
|
+
# GROK_AGENT alone, which a grok 1.0.0 HOOK process does not carry, so the
|
|
790
|
+
# Claude-only Stop auto-arm ran synchronously under Grok, foregrounded the
|
|
791
|
+
# watcher, and wedged the Grok turn for its declared 28800-second timeout.
|
|
792
|
+
#
|
|
793
|
+
# bin/fm-subagent-pretool-check.sh is the deliberate exception: Grok has no
|
|
794
|
+
# counterpart registration, so guarding it would REMOVE the guard from Grok
|
|
795
|
+
# rather than deduplicate it (docs/subagent-guard.md "Known residual gap").
|
|
796
|
+
# It is asserted to stay unguarded so the exception cannot be closed silently.
|
|
797
|
+
test_tracked_claude_entries_inert_under_grok() {
|
|
798
|
+
local dir cmd script target guarded=0 unguarded=0
|
|
799
|
+
command -v jq >/dev/null 2>&1 || fail "test host must provide jq"
|
|
800
|
+
dir="$TMP_ROOT/claude-entries-grok-inert"
|
|
801
|
+
mkdir -p "$dir/bin"
|
|
802
|
+
for script in fm-turnend-guard.sh fm-claude-stop-autoarm.sh fm-sessionstart-run.sh \
|
|
803
|
+
fm-arm-pretool-check.sh fm-cd-pretool-check.sh fm-subagent-pretool-check.sh; do
|
|
804
|
+
printf '#!/usr/bin/env bash\nprintf ran >> %q\n' "$dir/invoked" > "$dir/bin/$script"
|
|
805
|
+
chmod +x "$dir/bin/$script"
|
|
806
|
+
done
|
|
807
|
+
|
|
808
|
+
# Runs one tracked command string and reports whether it reached its script.
|
|
809
|
+
ran_under() {
|
|
810
|
+
rm -f "$dir/invoked"
|
|
811
|
+
env "$@" CLAUDE_PROJECT_DIR="$dir" bash -c "$cmd" </dev/null >/dev/null 2>&1
|
|
812
|
+
[ -e "$dir/invoked" ]
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
while IFS= read -r cmd; do
|
|
816
|
+
[ -n "$cmd" ] || continue
|
|
817
|
+
target=$(printf '%s\n' "$cmd" | sed -n 's|.*/bin/\([a-z0-9-]*\.sh\).*|\1|p')
|
|
818
|
+
[ -n "$target" ] || fail "could not identify the target script of tracked entry: $cmd"
|
|
819
|
+
|
|
820
|
+
# Native Claude: EVERY tracked entry must still reach its script, or a guard
|
|
821
|
+
# has silently disarmed Claude's own protection.
|
|
822
|
+
ran_under -u GROK_AGENT -u GROK_HOOK_EVENT -u GROK_HOOK_NAME -u GROK_SESSION_ID \
|
|
823
|
+
-u GROK_WORKSPACE_ROOT \
|
|
824
|
+
|| fail "tracked entry for $target did not run under a native Claude environment"
|
|
825
|
+
|
|
826
|
+
if [ "$target" = fm-subagent-pretool-check.sh ]; then
|
|
827
|
+
unguarded=$((unguarded + 1))
|
|
828
|
+
ran_under -u GROK_AGENT GROK_HOOK_EVENT=pre_tool_use GROK_SESSION_ID=grok-test-session \
|
|
829
|
+
|| fail "the documented $target exception must stay unguarded; Grok has no counterpart to fall back to"
|
|
830
|
+
continue
|
|
831
|
+
fi
|
|
832
|
+
|
|
833
|
+
guarded=$((guarded + 1))
|
|
834
|
+
# grok 1.0.0 hook process: hook markers present, GROK_AGENT absent.
|
|
835
|
+
! ran_under -u GROK_AGENT GROK_HOOK_EVENT=stop \
|
|
836
|
+
GROK_HOOK_NAME='project/settings:stop[0].hooks[0]' \
|
|
837
|
+
GROK_SESSION_ID=grok-test-session GROK_WORKSPACE_ROOT="$dir" \
|
|
838
|
+
|| fail "tracked entry for $target ran under a grok 1.0.0 hook environment"
|
|
839
|
+
# grok 0.2.73 child/tool process: GROK_AGENT present, hook markers absent.
|
|
840
|
+
! ran_under -u GROK_HOOK_EVENT -u GROK_HOOK_NAME GROK_AGENT=1 \
|
|
841
|
+
|| fail "tracked entry for $target ran under a legacy GROK_AGENT environment"
|
|
842
|
+
done < <(jq -r '.hooks[][].hooks[].command' "$ROOT/.claude/settings.json")
|
|
843
|
+
|
|
844
|
+
[ "$guarded" -eq 5 ] || fail "expected 5 grok-guarded tracked entries, saw $guarded"
|
|
845
|
+
[ "$unguarded" -eq 1 ] || fail "expected 1 documented unguarded tracked entry, saw $unguarded"
|
|
846
|
+
pass "tracked .claude/settings.json entries: $guarded inert under grok, the documented subagent exception still armed, all live under Claude"
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
test_codex_hook_uses_process_pwd_when_payload_cwd_is_outside_root() {
|
|
850
|
+
local settings command dir expected_root outside payload out status
|
|
851
|
+
settings="$ROOT/.codex/hooks.json"
|
|
852
|
+
[ -f "$settings" ] || fail "tracked .codex/hooks.json is missing"
|
|
853
|
+
command=$(jq -r '.hooks.Stop[0].hooks[0].command // empty' "$settings")
|
|
854
|
+
[ -n "$command" ] || fail "Stop hook command is missing from .codex/hooks.json"
|
|
855
|
+
dir=$(make_primary_dir "$TMP_ROOT/codex-hook-root")
|
|
856
|
+
mark_codex_hook_root "$dir"
|
|
857
|
+
expected_root=$(cd "$dir" && pwd -P)
|
|
858
|
+
outside="$TMP_ROOT/codex-hook-outside"
|
|
859
|
+
mkdir -p "$outside"
|
|
860
|
+
cat > "$dir/bin/fm-turnend-guard.sh" <<'EOF'
|
|
861
|
+
#!/usr/bin/env bash
|
|
862
|
+
printf 'guard=%s\n' "$0"
|
|
863
|
+
cat
|
|
864
|
+
EOF
|
|
865
|
+
chmod +x "$dir/bin/fm-turnend-guard.sh"
|
|
866
|
+
payload=$(jq -cn --arg cwd "$outside" '{cwd:$cwd,stop_hook_active:false}')
|
|
867
|
+
out=$(printf '%s' "$payload" | (cd "$dir" && bash -c "$command") 2>&1); status=$?
|
|
868
|
+
expect_code 0 "$status" "codex hook must execute successfully when payload cwd is outside the firstmate root"
|
|
869
|
+
assert_contains "$out" "guard=$expected_root/bin/fm-turnend-guard.sh" "codex hook must use the hook process root"
|
|
870
|
+
assert_contains "$out" "$payload" "codex hook must pass the original payload to the guard"
|
|
871
|
+
pass ".codex/hooks.json: Stop hook uses hook process root when payload cwd is outside"
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
test_codex_hook_ignores_nested_git_root_guard() {
|
|
875
|
+
local settings command dir nested subdir expected_root payload out status
|
|
876
|
+
settings="$ROOT/.codex/hooks.json"
|
|
877
|
+
[ -f "$settings" ] || fail "tracked .codex/hooks.json is missing"
|
|
878
|
+
command=$(jq -r '.hooks.Stop[0].hooks[0].command // empty' "$settings")
|
|
879
|
+
[ -n "$command" ] || fail "Stop hook command is missing from .codex/hooks.json"
|
|
880
|
+
dir=$(make_primary_dir "$TMP_ROOT/codex-hook-outer")
|
|
881
|
+
mark_codex_hook_root "$dir"
|
|
882
|
+
expected_root=$(cd "$dir" && pwd -P)
|
|
883
|
+
nested="$dir/projects/other"
|
|
884
|
+
mkdir -p "$nested"
|
|
885
|
+
git init -q "$nested"
|
|
886
|
+
git -C "$nested" commit -q --allow-empty -m init
|
|
887
|
+
mkdir -p "$nested/bin" "$nested/.codex"
|
|
888
|
+
: > "$nested/AGENTS.md"
|
|
889
|
+
printf '{"hooks":{"Stop":[{"hooks":[{"type":"command","command":"fm-turnend-guard.sh"}]}]}}\n' > "$nested/.codex/hooks.json"
|
|
890
|
+
cat > "$nested/bin/fm-turnend-guard.sh" <<'EOF'
|
|
891
|
+
#!/usr/bin/env bash
|
|
892
|
+
printf 'nested guard executed\n'
|
|
893
|
+
exit 99
|
|
894
|
+
EOF
|
|
895
|
+
chmod +x "$nested/bin/fm-turnend-guard.sh"
|
|
896
|
+
cat > "$dir/bin/fm-turnend-guard.sh" <<'EOF'
|
|
897
|
+
#!/usr/bin/env bash
|
|
898
|
+
printf 'guard=%s\n' "$0"
|
|
899
|
+
cat
|
|
900
|
+
EOF
|
|
901
|
+
chmod +x "$dir/bin/fm-turnend-guard.sh"
|
|
902
|
+
subdir="$nested/deep/path"
|
|
903
|
+
mkdir -p "$subdir"
|
|
904
|
+
payload=$(jq -cn --arg cwd "$subdir" '{cwd:$cwd,stop_hook_active:false}')
|
|
905
|
+
out=$(printf '%s' "$payload" | (cd "$dir" && bash -c "$command") 2>&1); status=$?
|
|
906
|
+
expect_code 0 "$status" "codex hook must not execute a nested project guard"
|
|
907
|
+
assert_contains "$out" "guard=$expected_root/bin/fm-turnend-guard.sh" "codex hook must keep using the outer firstmate guard"
|
|
908
|
+
assert_not_contains "$out" "nested guard executed" "codex hook must not execute nested project code"
|
|
909
|
+
pass ".codex/hooks.json: Stop hook ignores nested git root guard scripts"
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
test_opencode_plugin_anchors_guard_to_worktree() {
|
|
913
|
+
local plugin parent worktree_dir wrong_dir out status
|
|
914
|
+
plugin="$ROOT/.opencode/plugins/fm-primary-turnend-guard.js"
|
|
915
|
+
[ -f "$plugin" ] || fail "tracked OpenCode primary plugin is missing"
|
|
916
|
+
parent="$TMP_ROOT/opencode-plugin-parent"
|
|
917
|
+
git init -q "$parent"
|
|
918
|
+
worktree_dir="$parent/nested/opencode-plugin-worktree"
|
|
919
|
+
wrong_dir="$TMP_ROOT/opencode-plugin-cwd/subdir"
|
|
920
|
+
mkdir -p "$worktree_dir/bin" "$wrong_dir"
|
|
921
|
+
cat > "$worktree_dir/bin/fm-turnend-guard.sh" <<'EOF'
|
|
922
|
+
#!/usr/bin/env bash
|
|
923
|
+
cat >/dev/null
|
|
924
|
+
printf 'guard-fired\n' >&2
|
|
925
|
+
exit 2
|
|
926
|
+
EOF
|
|
927
|
+
chmod +x "$worktree_dir/bin/fm-turnend-guard.sh"
|
|
928
|
+
# Runtime module-format warnings are host noise; this assertion owns plugin output only.
|
|
929
|
+
out=$(NODE_NO_WARNINGS=1 PLUGIN="$plugin" DIRECTORY="$wrong_dir" WORKTREE="$worktree_dir" node 2>&1 <<'EOF'
|
|
930
|
+
import { pathToFileURL } from "node:url";
|
|
931
|
+
|
|
932
|
+
const mod = await import(pathToFileURL(process.env.PLUGIN).href);
|
|
933
|
+
let promptBody = "";
|
|
934
|
+
const client = {
|
|
935
|
+
session: {
|
|
936
|
+
promptAsync: async (request) => {
|
|
937
|
+
promptBody = request.body.parts[0].text;
|
|
938
|
+
},
|
|
939
|
+
},
|
|
940
|
+
};
|
|
941
|
+
const hooks = await mod.FmPrimaryTurnendGuard({
|
|
942
|
+
client,
|
|
943
|
+
directory: process.env.DIRECTORY,
|
|
944
|
+
worktree: process.env.WORKTREE,
|
|
945
|
+
});
|
|
946
|
+
await hooks.event({ event: { type: "session.idle", properties: { sessionID: "session-test" } } });
|
|
947
|
+
if (!promptBody.startsWith("\u2063FIRSTMATE_OP: v1 turn-end-guard: ")) {
|
|
948
|
+
console.error(`untyped operational prompt: ${promptBody}`);
|
|
949
|
+
process.exit(1);
|
|
950
|
+
}
|
|
951
|
+
if (!promptBody.includes("guard-fired")) {
|
|
952
|
+
console.error(`missing prompt body: ${promptBody}`);
|
|
953
|
+
process.exit(1);
|
|
954
|
+
}
|
|
955
|
+
if (!promptBody.includes("watcher cycle is missing, failed, or unhealthy")) {
|
|
956
|
+
console.error(`missing recovery-only preamble: ${promptBody}`);
|
|
957
|
+
process.exit(1);
|
|
958
|
+
}
|
|
959
|
+
if (promptBody.includes("Resume supervision according to the session-start operating block")) {
|
|
960
|
+
console.error(`ordinary continuity leaked into guard follow-up: ${promptBody}`);
|
|
961
|
+
process.exit(1);
|
|
962
|
+
}
|
|
963
|
+
EOF
|
|
964
|
+
)
|
|
965
|
+
status=$?
|
|
966
|
+
expect_code 0 "$status" "OpenCode plugin must run the guard from worktree even when directory is elsewhere"
|
|
967
|
+
[ -z "$out" ] || fail "OpenCode plugin worktree-root test printed output: $out"
|
|
968
|
+
pass ".opencode primary plugin: guard path is anchored to worktree, not directory"
|
|
969
|
+
}
|
|
970
|
+
|
|
971
|
+
test_pi_extension_injects_once_per_logical_agent_run() {
|
|
972
|
+
local repo home ext log out status
|
|
973
|
+
repo="$TMP_ROOT/pi-logical-run-root"
|
|
974
|
+
home="$TMP_ROOT/pi-logical-run-home"
|
|
975
|
+
ext="$repo/.pi/extensions/fm-primary-turnend-guard.ts"
|
|
976
|
+
log="$TMP_ROOT/pi-logical-run-guard.log"
|
|
977
|
+
mkdir -p "$repo/.pi/extensions/lib" "$repo/bin" "$home/state"
|
|
978
|
+
cp "$ROOT/.pi/extensions/fm-primary-turnend-guard.ts" "$ext"
|
|
979
|
+
cp "$ROOT/.pi/extensions/lib/fm-operational-input.ts" "$repo/.pi/extensions/lib/fm-operational-input.ts"
|
|
980
|
+
cp "$ROOT/bin/fm-operational-input.sh" "$repo/bin/fm-operational-input.sh"
|
|
981
|
+
cat > "$repo/bin/fm-turnend-guard.sh" <<'SH'
|
|
982
|
+
#!/usr/bin/env bash
|
|
983
|
+
cat >/dev/null
|
|
984
|
+
printf 'guard\n' >> "${FM_GUARD_LOG:?}"
|
|
985
|
+
printf 'logical-run guard fired\n' >&2
|
|
986
|
+
exit 2
|
|
987
|
+
SH
|
|
988
|
+
cat > "$repo/bin/fm-arm-pretool-check.sh" <<'SH'
|
|
989
|
+
#!/usr/bin/env bash
|
|
990
|
+
exit 0
|
|
991
|
+
SH
|
|
992
|
+
chmod +x "$repo/bin/fm-turnend-guard.sh" "$repo/bin/fm-arm-pretool-check.sh"
|
|
993
|
+
out=$(PLUGIN="$ext" FM_HOME="$home" FM_GUARD_LOG="$log" node --input-type=module 2>&1 <<'EOF'
|
|
994
|
+
import { readFileSync } from "node:fs";
|
|
995
|
+
import { pathToFileURL } from "node:url";
|
|
996
|
+
|
|
997
|
+
const handlers = new Map();
|
|
998
|
+
let prompts = 0;
|
|
999
|
+
const pi = {
|
|
1000
|
+
on(event, handler) {
|
|
1001
|
+
handlers.set(event, handler);
|
|
1002
|
+
},
|
|
1003
|
+
async sendUserMessage(message, options) {
|
|
1004
|
+
prompts += 1;
|
|
1005
|
+
if (!message.startsWith("\u2063FIRSTMATE_OP: v1 turn-end-guard: ")) throw new Error(`untyped operational prompt: ${message}`);
|
|
1006
|
+
if (!message.includes("TURN WOULD END BLIND")) throw new Error(`unexpected prompt: ${message}`);
|
|
1007
|
+
if (!message.includes("watcher cycle is missing, failed, or unhealthy")) throw new Error(`guard prompt omitted recovery-only state: ${message}`);
|
|
1008
|
+
if (message.includes("Resume supervision according to the session-start operating block")) throw new Error(`guard prompt used ordinary continuity: ${message}`);
|
|
1009
|
+
if (options?.deliverAs !== "followUp") throw new Error("guard prompt was not a follow-up");
|
|
1010
|
+
await handlers.get("agent_settled")?.({ type: "agent_settled" }, {});
|
|
1011
|
+
},
|
|
1012
|
+
};
|
|
1013
|
+
const mod = await import(pathToFileURL(process.env.PLUGIN).href);
|
|
1014
|
+
mod.default(pi);
|
|
1015
|
+
if (handlers.has("turn_end")) throw new Error("guard still treats internal Pi turns as logical runs");
|
|
1016
|
+
const settled = handlers.get("agent_settled");
|
|
1017
|
+
if (!settled) throw new Error("agent_settled handler was not registered");
|
|
1018
|
+
|
|
1019
|
+
await settled({ type: "agent_settled" }, {});
|
|
1020
|
+
if (prompts !== 1) throw new Error(`no-tool run injected ${prompts} follow-ups`);
|
|
1021
|
+
|
|
1022
|
+
for (let i = 0; i < 3; i += 1) {
|
|
1023
|
+
await handlers.get("turn_end")?.({ type: "turn_end", turnIndex: i }, {});
|
|
1024
|
+
}
|
|
1025
|
+
await settled({ type: "agent_settled" }, {});
|
|
1026
|
+
if (prompts !== 2) throw new Error(`multi-tool run produced ${prompts - 1} follow-ups`);
|
|
1027
|
+
|
|
1028
|
+
const guardRuns = readFileSync(process.env.FM_GUARD_LOG, "utf8").trim().split("\n").length;
|
|
1029
|
+
if (guardRuns !== 2) throw new Error(`guard predicate ran ${guardRuns} times for two logical runs`);
|
|
1030
|
+
EOF
|
|
1031
|
+
)
|
|
1032
|
+
status=$?
|
|
1033
|
+
expect_code 0 "$status" "Pi guard must inject once for no-tool and multi-tool logical runs"
|
|
1034
|
+
[ -z "$out" ] || fail "Pi logical-run guard test printed output: $out"
|
|
1035
|
+
pass ".pi primary extension: no-tool and multi-tool runs each inject exactly one guard follow-up"
|
|
1036
|
+
}
|
|
1037
|
+
|
|
1038
|
+
test_pi_extension_retries_after_followup_delivery_failure() {
|
|
1039
|
+
local repo home ext out status
|
|
1040
|
+
repo="$TMP_ROOT/pi-delivery-failure-root"
|
|
1041
|
+
home="$TMP_ROOT/pi-delivery-failure-home"
|
|
1042
|
+
ext="$repo/.pi/extensions/fm-primary-turnend-guard.ts"
|
|
1043
|
+
mkdir -p "$repo/.pi/extensions/lib" "$repo/bin" "$home/state"
|
|
1044
|
+
cp "$ROOT/.pi/extensions/fm-primary-turnend-guard.ts" "$ext"
|
|
1045
|
+
cp "$ROOT/.pi/extensions/lib/fm-operational-input.ts" "$repo/.pi/extensions/lib/fm-operational-input.ts"
|
|
1046
|
+
cp "$ROOT/bin/fm-operational-input.sh" "$repo/bin/fm-operational-input.sh"
|
|
1047
|
+
cat > "$repo/bin/fm-turnend-guard.sh" <<'SH'
|
|
1048
|
+
#!/usr/bin/env bash
|
|
1049
|
+
cat >/dev/null
|
|
1050
|
+
printf 'delivery failure guard\n' >&2
|
|
1051
|
+
exit 2
|
|
1052
|
+
SH
|
|
1053
|
+
cat > "$repo/bin/fm-arm-pretool-check.sh" <<'SH'
|
|
1054
|
+
#!/usr/bin/env bash
|
|
1055
|
+
exit 0
|
|
1056
|
+
SH
|
|
1057
|
+
chmod +x "$repo/bin/fm-turnend-guard.sh" "$repo/bin/fm-arm-pretool-check.sh"
|
|
1058
|
+
out=$(PLUGIN="$ext" FM_HOME="$home" node --input-type=module 2>&1 <<'EOF'
|
|
1059
|
+
import { pathToFileURL } from "node:url";
|
|
1060
|
+
|
|
1061
|
+
const handlers = new Map();
|
|
1062
|
+
let attempts = 0;
|
|
1063
|
+
const pi = {
|
|
1064
|
+
on(event, handler) {
|
|
1065
|
+
handlers.set(event, handler);
|
|
1066
|
+
},
|
|
1067
|
+
async sendUserMessage() {
|
|
1068
|
+
attempts += 1;
|
|
1069
|
+
if (attempts === 1) throw new Error("synthetic delivery failure");
|
|
1070
|
+
await handlers.get("agent_settled")?.({ type: "agent_settled" }, {});
|
|
1071
|
+
},
|
|
1072
|
+
};
|
|
1073
|
+
const mod = await import(pathToFileURL(process.env.PLUGIN).href);
|
|
1074
|
+
mod.default(pi);
|
|
1075
|
+
const settled = handlers.get("agent_settled");
|
|
1076
|
+
await settled({ type: "agent_settled" }, {});
|
|
1077
|
+
await settled({ type: "agent_settled" }, {});
|
|
1078
|
+
if (attempts !== 2) throw new Error(`expected delivery retry, saw ${attempts} attempts`);
|
|
1079
|
+
EOF
|
|
1080
|
+
)
|
|
1081
|
+
status=$?
|
|
1082
|
+
expect_code 0 "$status" "Pi guard latch must reset after follow-up delivery failure"
|
|
1083
|
+
[ -z "$out" ] || fail "Pi delivery-failure guard test printed output: $out"
|
|
1084
|
+
pass ".pi primary extension: delivery failure resets the logical-run latch"
|
|
1085
|
+
}
|
|
1086
|
+
|
|
1087
|
+
# --- --claude cooperative mode -----------------------------------------------
|
|
1088
|
+
# In --claude mode the guard ignores stop_hook_active (Claude marks every stop
|
|
1089
|
+
# after ANY stop-hook continuation true, including asyncRewake rewake turns) and
|
|
1090
|
+
# cooperates with the Stop-owned auto-arm instead: allow on health, live owner
|
|
1091
|
+
# claim, or a fresh rewake epoch; bounded re-block only when none materialize.
|
|
1092
|
+
|
|
1093
|
+
run_hook_claude() {
|
|
1094
|
+
local dir=$1 stop_active=$2 home
|
|
1095
|
+
home=$(cd "$dir" && pwd)
|
|
1096
|
+
printf '{"stop_hook_active":%s,"session_id":"sess-claude-mode"}' "$stop_active" | CLAUDECODE=1 FM_HOME="$home" bash "$dir/bin/fm-turnend-guard.sh" --claude 2>&1
|
|
1097
|
+
}
|
|
1098
|
+
|
|
1099
|
+
seed_claude_failure() {
|
|
1100
|
+
local dir=$1 outcome=${2:-failed-suppressed}
|
|
1101
|
+
: > "$dir/state/.claude-autoarm-failure-notified"
|
|
1102
|
+
printf 'epoch=3 owner_pid=999 outcome=%s updated_at=1\n' "$outcome" > "$dir/state/.claude-autoarm-epoch"
|
|
1103
|
+
touch -t 202001010000 "$dir/state/.claude-autoarm-epoch"
|
|
1104
|
+
}
|
|
1105
|
+
|
|
1106
|
+
seed_claude_budget() {
|
|
1107
|
+
local dir=$1 count=$2 epoch=${3:-2}
|
|
1108
|
+
printf 'session=sess-claude-mode\ncount=%s\nepoch=%s\n' "$count" "$epoch" > "$dir/state/.turnend-claude-blocks"
|
|
1109
|
+
}
|
|
1110
|
+
|
|
1111
|
+
record_autoarm_owner() {
|
|
1112
|
+
local dir=$1 pid=$2
|
|
1113
|
+
mkdir -p "$dir/state/.claude-autoarm.lock"
|
|
1114
|
+
printf '%s\n' "$pid" > "$dir/state/.claude-autoarm.lock/pid"
|
|
1115
|
+
printf 'autoarm\n' > "$dir/state/.claude-autoarm.lock/role"
|
|
1116
|
+
}
|
|
1117
|
+
|
|
1118
|
+
install_integrated_autoarm() {
|
|
1119
|
+
local dir=$1
|
|
1120
|
+
cp "$ROOT/bin/fm-claude-stop-autoarm.sh" "$dir/bin/fm-claude-stop-autoarm.sh"
|
|
1121
|
+
cp "$ROOT/bin/fm-primary-scope-lib.sh" "$dir/bin/fm-primary-scope-lib.sh"
|
|
1122
|
+
cp "$ROOT/bin/fm-supervision-lib.sh" "$dir/bin/fm-supervision-lib.sh"
|
|
1123
|
+
cp "$ROOT/bin/fm-wake-lib.sh" "$dir/bin/fm-wake-lib.sh"
|
|
1124
|
+
cp "$ROOT/bin/fm-hook-host-lib.sh" "$dir/bin/fm-hook-host-lib.sh"
|
|
1125
|
+
cp "$ROOT/bin/fm-session-lock-lib.sh" "$dir/bin/fm-session-lock-lib.sh"
|
|
1126
|
+
cp "$ROOT/bin/fm-cursor-lib.sh" "$dir/bin/fm-cursor-lib.sh"
|
|
1127
|
+
cp "$ROOT/bin/fm-lock.sh" "$dir/bin/fm-lock.sh"
|
|
1128
|
+
chmod +x "$dir/bin/fm-claude-stop-autoarm.sh" "$dir/bin/fm-lock.sh"
|
|
1129
|
+
ln -s /bin/bash "$dir/fake-claude"
|
|
1130
|
+
}
|
|
1131
|
+
|
|
1132
|
+
run_integrated_autoarm() {
|
|
1133
|
+
local dir=$1 home
|
|
1134
|
+
home=$(cd "$dir" && pwd)
|
|
1135
|
+
# shellcheck disable=SC2016 # the fake harness expands FM_HOME inside its child shell.
|
|
1136
|
+
printf '{"session_id":"sess-claude-mode","stop_hook_active":false}\n' \
|
|
1137
|
+
| FM_HOME="$home" "$dir/fake-claude" -c '
|
|
1138
|
+
printf "%s\n" "$$" > "$FM_HOME/state/.lock"
|
|
1139
|
+
"$FM_HOME/bin/fm-claude-stop-autoarm.sh"
|
|
1140
|
+
' 2>&1
|
|
1141
|
+
}
|
|
1142
|
+
|
|
1143
|
+
write_integrated_failed_arm() {
|
|
1144
|
+
local dir=$1
|
|
1145
|
+
cat > "$dir/bin/fm-watch-arm.sh" <<'SH'
|
|
1146
|
+
#!/usr/bin/env bash
|
|
1147
|
+
printf 'watcher: FAILED - persistent fixture failure\n'
|
|
1148
|
+
exit 1
|
|
1149
|
+
SH
|
|
1150
|
+
chmod +x "$dir/bin/fm-watch-arm.sh"
|
|
1151
|
+
}
|
|
1152
|
+
|
|
1153
|
+
# The 2026-07-21 incident regression: after a spent forced continuation the old
|
|
1154
|
+
# one-shot loop guard ALLOWED a blind stop (stop_hook_active=true) while the
|
|
1155
|
+
# watcher was already dead. In --claude mode the guard must re-block instead.
|
|
1156
|
+
test_hook_claude_mode_reblocks_stop_hook_active_when_unhealthy() {
|
|
1157
|
+
local dir out status
|
|
1158
|
+
dir=$(make_primary_dir "$TMP_ROOT/hook-claude-reblock")
|
|
1159
|
+
: > "$dir/state/task1.meta"
|
|
1160
|
+
out=$(FM_CLAUDE_AUTOARM_SYNC_WAIT_MS=200 run_hook_claude "$dir" true); status=$?
|
|
1161
|
+
expect_code 2 "$status" "--claude mode must re-block a stop_hook_active=true stop while unhealthy with no auto-arm claim"
|
|
1162
|
+
assert_contains "$out" "TURN WOULD END BLIND" "--claude re-block must carry the blind-turn banner"
|
|
1163
|
+
assert_contains "$out" "Stop-owned auto-arm did not claim" "--claude re-block must explain the missing auto-arm claim"
|
|
1164
|
+
pass "fm-turnend-guard --claude: re-blocks a loop-guarded stop while unhealthy and unclaimed (incident regression)"
|
|
1165
|
+
}
|
|
1166
|
+
|
|
1167
|
+
test_hook_claude_mode_reblocks_x_mode_without_tasks() {
|
|
1168
|
+
local dir out status
|
|
1169
|
+
dir=$(make_primary_dir "$TMP_ROOT/hook-claude-x-mode")
|
|
1170
|
+
: > "$dir/state/x-watch.check.sh"
|
|
1171
|
+
out=$(FM_CLAUDE_AUTOARM_SYNC_WAIT_MS=200 run_hook_claude "$dir" true); status=$?
|
|
1172
|
+
expect_code 2 "$status" "--claude mode must re-block an X-mode-only stop when no auto-arm claims recovery"
|
|
1173
|
+
assert_contains "$out" "X-mode relay polling needs supervision" "--claude X-mode re-block must name the active supervision need"
|
|
1174
|
+
[ -f "$dir/state/.turnend-claude-blocks" ] || fail "--claude X-mode re-block must consume the shared block budget"
|
|
1175
|
+
pass "fm-turnend-guard --claude: X-mode-only homes re-block when auto-arm recovery is absent"
|
|
1176
|
+
}
|
|
1177
|
+
|
|
1178
|
+
test_hook_claude_mode_allows_when_autoarm_owner_alive() {
|
|
1179
|
+
local dir pid out out2 status status2 count count2
|
|
1180
|
+
dir=$(make_primary_dir "$TMP_ROOT/hook-claude-owner")
|
|
1181
|
+
: > "$dir/state/task1.meta"
|
|
1182
|
+
seed_claude_failure "$dir"
|
|
1183
|
+
seed_claude_budget "$dir" 3
|
|
1184
|
+
sleep 60 &
|
|
1185
|
+
pid=$!
|
|
1186
|
+
record_autoarm_owner "$dir" "$pid"
|
|
1187
|
+
out=$(run_hook_claude "$dir" false); status=$?
|
|
1188
|
+
count=$(sed -n '2s/^count=//p' "$dir/state/.turnend-claude-blocks")
|
|
1189
|
+
out2=$(run_hook_claude "$dir" false); status2=$?
|
|
1190
|
+
count2=$(sed -n '2s/^count=//p' "$dir/state/.turnend-claude-blocks")
|
|
1191
|
+
kill "$pid" 2>/dev/null || true
|
|
1192
|
+
wait "$pid" 2>/dev/null || true
|
|
1193
|
+
expect_code 0 "$status" "--claude mode must allow when the auto-arm owner process is alive"
|
|
1194
|
+
expect_code 0 "$status2" "--claude mode must keep allowing the same live auto-arm epoch"
|
|
1195
|
+
[ -z "$out" ] || fail "--claude owner-claimed allow produced output: $out"
|
|
1196
|
+
[ -z "$out2" ] || fail "repeated same-owner allow produced output: $out2"
|
|
1197
|
+
[ "$count" = 4 ] || fail "new live auto-arm epoch did not advance failure progression from 3 to 4: $count"
|
|
1198
|
+
[ "$count2" = 4 ] || fail "repeated observation advanced the same auto-arm epoch twice: $count2"
|
|
1199
|
+
assert_present "$dir/state/.claude-autoarm-failure-notified" "live auto-arm owner cleared the failure episode"
|
|
1200
|
+
assert_absent "$dir/state/.claude-autoarm-failure-alarmed" "live automatic continuation emitted the attended fail-open alarm"
|
|
1201
|
+
pass "fm-turnend-guard --claude: a live arming epoch advances once and repeated observation is idempotent"
|
|
1202
|
+
}
|
|
1203
|
+
|
|
1204
|
+
test_hook_claude_mode_repeated_failed_to_arming_interleavings_reach_fail_open() {
|
|
1205
|
+
local dir out status pid i count epoch
|
|
1206
|
+
dir=$(make_primary_dir "$TMP_ROOT/hook-claude-arming-interleavings")
|
|
1207
|
+
: > "$dir/state/task1.meta"
|
|
1208
|
+
: > "$dir/state/.claude-autoarm-failure-notified"
|
|
1209
|
+
printf 'epoch=3 owner_pid=999 outcome=failed updated_at=%s\n' "$(date +%s)" > "$dir/state/.claude-autoarm-epoch"
|
|
1210
|
+
out=$(run_hook_claude "$dir" true); status=$?
|
|
1211
|
+
expect_code 0 "$status" "the first verified failed epoch must own its automatic handoff"
|
|
1212
|
+
|
|
1213
|
+
epoch=3
|
|
1214
|
+
for i in 1 2 3 4; do
|
|
1215
|
+
epoch=$((epoch + 1))
|
|
1216
|
+
sleep 60 &
|
|
1217
|
+
pid=$!
|
|
1218
|
+
record_autoarm_owner "$dir" "$pid"
|
|
1219
|
+
printf 'epoch=%s owner_pid=%s outcome=arming updated_at=%s\n' "$epoch" "$pid" "$(date +%s)" > "$dir/state/.claude-autoarm-epoch"
|
|
1220
|
+
out=$(run_hook_claude "$dir" true); status=$?
|
|
1221
|
+
expect_code 0 "$status" "active arming epoch $i must own its Stop while advancing the failure budget"
|
|
1222
|
+
count=$(sed -n '2s/^count=//p' "$dir/state/.turnend-claude-blocks")
|
|
1223
|
+
[ "$count" = "$i" ] || fail "arming epoch $i produced non-monotonic count $count"
|
|
1224
|
+
kill "$pid" 2>/dev/null || true
|
|
1225
|
+
wait "$pid" 2>/dev/null || true
|
|
1226
|
+
rm -rf "$dir/state/.claude-autoarm.lock"
|
|
1227
|
+
epoch=$((epoch + 1))
|
|
1228
|
+
printf 'epoch=%s owner_pid=999 outcome=failed-suppressed updated_at=%s\n' "$epoch" "$(date +%s)" > "$dir/state/.claude-autoarm-epoch"
|
|
1229
|
+
done
|
|
1230
|
+
|
|
1231
|
+
out=$(run_hook_claude "$dir" true); status=$?
|
|
1232
|
+
expect_code 0 "$status" "repeated failed-to-arming interleavings must reach terminal fail-open"
|
|
1233
|
+
assert_contains "$out" 'FIRSTMATE SUPERVISION IS GENUINELY DOWN' "arming interleavings stalled before the bounded fail-open"
|
|
1234
|
+
assert_present "$dir/state/.claude-autoarm-failure-alarmed" "arming interleavings did not consume the one-time alarm"
|
|
1235
|
+
pass "fm-turnend-guard --claude: repeated failed-to-arming races make bounded monotonic progress"
|
|
1236
|
+
}
|
|
1237
|
+
|
|
1238
|
+
test_hook_claude_mode_terminal_boundary_excludes_starting_owner() {
|
|
1239
|
+
local dir fakebin ready release once guard_out guard_status auto_out auto_status guard_pid
|
|
1240
|
+
dir=$(make_primary_dir "$TMP_ROOT/hook-claude-terminal-boundary")
|
|
1241
|
+
: > "$dir/state/task1.meta"
|
|
1242
|
+
: > "$dir/state/.claude-autoarm-failure-notified"
|
|
1243
|
+
printf 'epoch=3 owner_pid=999 outcome=failed-suppressed updated_at=%s\n' "$(date +%s)" > "$dir/state/.claude-autoarm-epoch"
|
|
1244
|
+
seed_claude_budget "$dir" 4 3
|
|
1245
|
+
install_integrated_autoarm "$dir"
|
|
1246
|
+
write_integrated_failed_arm "$dir"
|
|
1247
|
+
fakebin="$dir/fakebin"
|
|
1248
|
+
ready="$dir/terminal-ready"
|
|
1249
|
+
release="$dir/terminal-release"
|
|
1250
|
+
once="$dir/terminal-once"
|
|
1251
|
+
guard_out="$dir/guard.out"
|
|
1252
|
+
guard_status="$dir/guard.status"
|
|
1253
|
+
mkdir -p "$fakebin"
|
|
1254
|
+
mkfifo "$ready" "$release"
|
|
1255
|
+
cat > "$fakebin/cat" <<'SH'
|
|
1256
|
+
#!/usr/bin/env bash
|
|
1257
|
+
if [ "$1" = "$FM_TERMINAL_ROLE_PATH" ] \
|
|
1258
|
+
&& [ "$(/bin/cat "$1" 2>/dev/null || true)" = terminal-check ] \
|
|
1259
|
+
&& (set -C; : > "$FM_TERMINAL_ONCE") 2>/dev/null; then
|
|
1260
|
+
printf 'ready\n' > "$FM_TERMINAL_READY"
|
|
1261
|
+
IFS= read -r _ < "$FM_TERMINAL_RELEASE"
|
|
1262
|
+
fi
|
|
1263
|
+
exec /bin/cat "$@"
|
|
1264
|
+
SH
|
|
1265
|
+
chmod +x "$fakebin/cat"
|
|
1266
|
+
(
|
|
1267
|
+
printf '{"stop_hook_active":true,"session_id":"sess-claude-mode"}' \
|
|
1268
|
+
| PATH="$fakebin:$PATH" \
|
|
1269
|
+
FM_TERMINAL_ROLE_PATH="$dir/state/.claude-autoarm.lock/role" \
|
|
1270
|
+
FM_TERMINAL_READY="$ready" \
|
|
1271
|
+
FM_TERMINAL_RELEASE="$release" \
|
|
1272
|
+
FM_TERMINAL_ONCE="$once" \
|
|
1273
|
+
CLAUDECODE=1 FM_HOME="$dir" bash "$dir/bin/fm-turnend-guard.sh" --claude \
|
|
1274
|
+
> "$guard_out" 2>&1
|
|
1275
|
+
printf '%s\n' "$?" > "$guard_status"
|
|
1276
|
+
) &
|
|
1277
|
+
guard_pid=$!
|
|
1278
|
+
IFS= read -r _ < "$ready"
|
|
1279
|
+
auto_out=$(run_integrated_autoarm "$dir"); auto_status=$?
|
|
1280
|
+
printf 'release\n' > "$release"
|
|
1281
|
+
wait "$guard_pid"
|
|
1282
|
+
expect_code 0 "$auto_status" "an owner starting inside the terminal window must lose the existing owner boundary"
|
|
1283
|
+
[ -z "$auto_out" ] || fail "excluded terminal-window owner produced output: $auto_out"
|
|
1284
|
+
assert_absent "$dir/state/arm-ran" "excluded terminal-window owner started an arm cycle"
|
|
1285
|
+
expect_code 0 "$(cat "$guard_status")" "terminal boundary guard must complete without deadlock"
|
|
1286
|
+
assert_contains "$(cat "$guard_out")" 'FIRSTMATE SUPERVISION IS GENUINELY DOWN' "terminal boundary did not produce the one-time alarm"
|
|
1287
|
+
assert_absent "$dir/state/.claude-autoarm.lock" "terminal boundary left its owner lock behind"
|
|
1288
|
+
pass "fm-turnend-guard --claude: terminal owner boundary excludes a concurrent start without deadlock"
|
|
1289
|
+
}
|
|
1290
|
+
|
|
1291
|
+
test_hook_claude_mode_allows_on_fresh_rewake_epoch() {
|
|
1292
|
+
local dir out status
|
|
1293
|
+
dir=$(make_primary_dir "$TMP_ROOT/hook-claude-epoch")
|
|
1294
|
+
: > "$dir/state/task1.meta"
|
|
1295
|
+
printf 'epoch=3 owner_pid=999 outcome=rewake updated_at=%s\n' "$(date +%s)" > "$dir/state/.claude-autoarm-epoch"
|
|
1296
|
+
out=$(run_hook_claude "$dir" true); status=$?
|
|
1297
|
+
expect_code 0 "$status" "--claude mode must allow the stop whose rewake the auto-arm already owns"
|
|
1298
|
+
[ -z "$out" ] || fail "--claude rewake-epoch allow produced output: $out"
|
|
1299
|
+
pass "fm-turnend-guard --claude: fresh rewake epoch prevents a duplicate continuation for the same event"
|
|
1300
|
+
}
|
|
1301
|
+
|
|
1302
|
+
# The 2026-08-14 lapse: a cycle armed, delivered one rewake, exited, and left its
|
|
1303
|
+
# owner lock behind holding a live pid. Both Stop participants read that lock as
|
|
1304
|
+
# "recovery is already under way", so with work in flight and a beacon 40 minutes
|
|
1305
|
+
# cold every turn ended blind and nothing re-armed. A stale ledger outcome for
|
|
1306
|
+
# the lock's own pid is the proof that no decision is in flight any more.
|
|
1307
|
+
test_hook_claude_mode_blocks_on_abandoned_autoarm_claim() {
|
|
1308
|
+
local dir out status pid
|
|
1309
|
+
dir=$(make_primary_dir "$TMP_ROOT/hook-claude-abandoned-claim")
|
|
1310
|
+
: > "$dir/state/task1.meta"
|
|
1311
|
+
: > "$dir/state/task2.meta"
|
|
1312
|
+
sleep 60 &
|
|
1313
|
+
pid=$!
|
|
1314
|
+
record_autoarm_owner "$dir" "$pid"
|
|
1315
|
+
printf 'epoch=464 owner_pid=%s outcome=rewake updated_at=1\n' "$pid" > "$dir/state/.claude-autoarm-epoch"
|
|
1316
|
+
touch -t 202001010000 "$dir/state/.claude-autoarm-epoch"
|
|
1317
|
+
out=$(FM_CLAUDE_AUTOARM_SYNC_WAIT_MS=200 run_hook_claude "$dir" true); status=$?
|
|
1318
|
+
kill "$pid" 2>/dev/null || true
|
|
1319
|
+
wait "$pid" 2>/dev/null || true
|
|
1320
|
+
expect_code 2 "$status" "an owner lock left behind by a finished claim must not pass for recovery under way"
|
|
1321
|
+
assert_contains "$out" "TURN WOULD END BLIND" "abandoned-claim block must carry the blind-turn banner"
|
|
1322
|
+
assert_contains "$out" "2 task(s) in flight" "abandoned-claim block must name the unsupervised work"
|
|
1323
|
+
pass "fm-turnend-guard --claude: an abandoned auto-arm claim no longer allows a blind stop (incident regression)"
|
|
1324
|
+
}
|
|
1325
|
+
|
|
1326
|
+
# The ledger-blind variant of the same lapse: a session teardown killed the claim's
|
|
1327
|
+
# process group before it could record any outcome, so its entry still reads
|
|
1328
|
+
# "arming" - in flight however old, by contract - while the recorded pid now belongs
|
|
1329
|
+
# to an unrelated live process. The identity the claim wrote into its own lock is
|
|
1330
|
+
# the only thing that separates that from a real arm still running.
|
|
1331
|
+
test_hook_claude_mode_blocks_on_pid_reused_arming_claim() {
|
|
1332
|
+
local dir out status pid identity
|
|
1333
|
+
dir=$(make_primary_dir "$TMP_ROOT/hook-claude-reused-pid-claim")
|
|
1334
|
+
: > "$dir/state/task1.meta"
|
|
1335
|
+
: > "$dir/state/task2.meta"
|
|
1336
|
+
sleep 60 &
|
|
1337
|
+
pid=$!
|
|
1338
|
+
record_autoarm_owner "$dir" "$pid"
|
|
1339
|
+
# The claim recorded ITS OWN identity; this test shell now stands in for the
|
|
1340
|
+
# unrelated process that inherited the number.
|
|
1341
|
+
identity=$(fm_test_pid_identity "$$") || fail "could not compute a claim pid-identity"
|
|
1342
|
+
printf '%s\n' "$identity" > "$dir/state/.claude-autoarm.lock/pid-identity"
|
|
1343
|
+
printf 'epoch=464 owner_pid=%s outcome=arming updated_at=1\n' "$pid" > "$dir/state/.claude-autoarm-epoch"
|
|
1344
|
+
touch -t 202001010000 "$dir/state/.claude-autoarm-epoch"
|
|
1345
|
+
: > "$dir/state/.last-watcher-beat"
|
|
1346
|
+
out=$(FM_CLAUDE_AUTOARM_SYNC_WAIT_MS=200 run_hook_claude "$dir" true); status=$?
|
|
1347
|
+
kill "$pid" 2>/dev/null || true
|
|
1348
|
+
wait "$pid" 2>/dev/null || true
|
|
1349
|
+
expect_code 2 "$status" "a claim whose recorded identity no longer matches its live pid must not pass for recovery under way"
|
|
1350
|
+
assert_contains "$out" "TURN WOULD END BLIND" "reused-pid claim block must carry the blind-turn banner"
|
|
1351
|
+
assert_contains "$out" "2 task(s) in flight" "reused-pid claim block must name the unsupervised work"
|
|
1352
|
+
pass "fm-turnend-guard --claude: a claim whose pid was reused stops counting as recovery even while its entry reads arming"
|
|
1353
|
+
}
|
|
1354
|
+
|
|
1355
|
+
# The legacy stuck-arming shape (the 2026-08-26 flap): a live identity-matched
|
|
1356
|
+
# lock-holding owner frozen at arming past grace with a beacon just as stale
|
|
1357
|
+
# must not count as recovery under way.
|
|
1358
|
+
test_hook_claude_mode_blocks_on_stuck_arming_claim() {
|
|
1359
|
+
local dir out status pid identity
|
|
1360
|
+
dir=$(make_primary_dir "$TMP_ROOT/hook-claude-stuck-arming-claim")
|
|
1361
|
+
: > "$dir/state/task1.meta"
|
|
1362
|
+
: > "$dir/state/task2.meta"
|
|
1363
|
+
sleep 60 &
|
|
1364
|
+
pid=$!
|
|
1365
|
+
record_autoarm_owner "$dir" "$pid"
|
|
1366
|
+
identity=$(fm_test_pid_identity "$pid") || fail "could not compute a claim pid-identity"
|
|
1367
|
+
printf '%s\n' "$identity" > "$dir/state/.claude-autoarm.lock/pid-identity"
|
|
1368
|
+
printf 'epoch=464 owner_pid=%s outcome=arming updated_at=1\n' "$pid" > "$dir/state/.claude-autoarm-epoch"
|
|
1369
|
+
touch -t 202001010000 "$dir/state/.claude-autoarm-epoch"
|
|
1370
|
+
touch -t 202001010000 "$dir/state/.last-watcher-beat"
|
|
1371
|
+
out=$(FM_CLAUDE_AUTOARM_SYNC_WAIT_MS=200 run_hook_claude "$dir" true); status=$?
|
|
1372
|
+
kill "$pid" 2>/dev/null || true
|
|
1373
|
+
wait "$pid" 2>/dev/null || true
|
|
1374
|
+
expect_code 2 "$status" "a live owner stuck arming past grace with a stale beacon must not pass for recovery under way"
|
|
1375
|
+
assert_contains "$out" "TURN WOULD END BLIND" "stuck-arming claim block must carry the blind-turn banner"
|
|
1376
|
+
assert_contains "$out" "2 task(s) in flight" "stuck-arming claim block must name the unsupervised work"
|
|
1377
|
+
pass "fm-turnend-guard --claude: a hung owner frozen at arming with no watcher beat no longer allows a blind stop"
|
|
1378
|
+
}
|
|
1379
|
+
|
|
1380
|
+
# The generation model's ownership proof: a live open ledger claim (two-line
|
|
1381
|
+
# entry, identity-matched owner, watcher still beating) owns recovery with no
|
|
1382
|
+
# lock held at all.
|
|
1383
|
+
test_hook_claude_mode_allows_on_open_generation_claim() {
|
|
1384
|
+
local dir out status pid identity
|
|
1385
|
+
dir=$(make_primary_dir "$TMP_ROOT/hook-claude-open-generation")
|
|
1386
|
+
: > "$dir/state/task1.meta"
|
|
1387
|
+
sleep 60 &
|
|
1388
|
+
pid=$!
|
|
1389
|
+
identity=$(fm_test_pid_identity "$pid") || fail "could not compute a claim pid-identity"
|
|
1390
|
+
printf 'epoch=464 owner_pid=%s outcome=arming updated_at=1\n%s\n' "$pid" "$identity" \
|
|
1391
|
+
> "$dir/state/.claude-autoarm-epoch"
|
|
1392
|
+
touch -t 202001010000 "$dir/state/.claude-autoarm-epoch"
|
|
1393
|
+
: > "$dir/state/.last-watcher-beat"
|
|
1394
|
+
[ ! -e "$dir/state/.claude-autoarm.lock" ] || fail "this case must start with no owner lock at all"
|
|
1395
|
+
out=$(run_hook_claude "$dir" false); status=$?
|
|
1396
|
+
kill "$pid" 2>/dev/null || true
|
|
1397
|
+
wait "$pid" 2>/dev/null || true
|
|
1398
|
+
expect_code 0 "$status" "--claude mode must allow when a live open generation claim owns recovery"
|
|
1399
|
+
[ -z "$out" ] || fail "open-generation-claim allow produced output: $out"
|
|
1400
|
+
pass "fm-turnend-guard --claude: a live open generation claim owns recovery with no lock held"
|
|
1401
|
+
}
|
|
1402
|
+
|
|
1403
|
+
# The same claim gone stuck (entry and beacon both past grace) stops counting
|
|
1404
|
+
# as recovery even though its owner is alive and identity-matched.
|
|
1405
|
+
test_hook_claude_mode_blocks_on_stuck_generation_claim() {
|
|
1406
|
+
local dir out status pid identity
|
|
1407
|
+
dir=$(make_primary_dir "$TMP_ROOT/hook-claude-stuck-generation")
|
|
1408
|
+
: > "$dir/state/task1.meta"
|
|
1409
|
+
: > "$dir/state/task2.meta"
|
|
1410
|
+
sleep 60 &
|
|
1411
|
+
pid=$!
|
|
1412
|
+
identity=$(fm_test_pid_identity "$pid") || fail "could not compute a claim pid-identity"
|
|
1413
|
+
printf 'epoch=464 owner_pid=%s outcome=arming updated_at=1\n%s\n' "$pid" "$identity" \
|
|
1414
|
+
> "$dir/state/.claude-autoarm-epoch"
|
|
1415
|
+
touch -t 202001010000 "$dir/state/.claude-autoarm-epoch"
|
|
1416
|
+
touch -t 202001010000 "$dir/state/.last-watcher-beat"
|
|
1417
|
+
out=$(FM_CLAUDE_AUTOARM_SYNC_WAIT_MS=200 run_hook_claude "$dir" true); status=$?
|
|
1418
|
+
kill "$pid" 2>/dev/null || true
|
|
1419
|
+
wait "$pid" 2>/dev/null || true
|
|
1420
|
+
expect_code 2 "$status" "a stuck generation claim must not pass for recovery under way"
|
|
1421
|
+
assert_contains "$out" "TURN WOULD END BLIND" "stuck-generation-claim block must carry the blind-turn banner"
|
|
1422
|
+
assert_contains "$out" "2 task(s) in flight" "stuck-generation-claim block must name the unsupervised work"
|
|
1423
|
+
pass "fm-turnend-guard --claude: a stuck generation claim no longer allows a blind stop"
|
|
1424
|
+
}
|
|
1425
|
+
|
|
1426
|
+
# The same abandoned claim on the terminal path: stepping aside for it allowed the
|
|
1427
|
+
# stop silently AND spent no attended alarm, so a genuinely broken automatic
|
|
1428
|
+
# mechanism stayed invisible. The guard must clear the claim and finish instead.
|
|
1429
|
+
test_hook_claude_mode_terminal_fail_open_clears_abandoned_claim() {
|
|
1430
|
+
local dir out status pid
|
|
1431
|
+
dir=$(make_primary_dir "$TMP_ROOT/hook-claude-abandoned-terminal")
|
|
1432
|
+
: > "$dir/state/task1.meta"
|
|
1433
|
+
: > "$dir/state/.claude-autoarm-failure-notified"
|
|
1434
|
+
seed_claude_budget "$dir" 4 3
|
|
1435
|
+
sleep 60 &
|
|
1436
|
+
pid=$!
|
|
1437
|
+
record_autoarm_owner "$dir" "$pid"
|
|
1438
|
+
printf 'epoch=3 owner_pid=%s outcome=failed-suppressed updated_at=1\n' "$pid" > "$dir/state/.claude-autoarm-epoch"
|
|
1439
|
+
touch -t 202001010000 "$dir/state/.claude-autoarm-epoch"
|
|
1440
|
+
out=$(FM_CLAUDE_AUTOARM_SYNC_WAIT_MS=200 run_hook_claude "$dir" true); status=$?
|
|
1441
|
+
kill "$pid" 2>/dev/null || true
|
|
1442
|
+
wait "$pid" 2>/dev/null || true
|
|
1443
|
+
expect_code 0 "$status" "the verified attended fail-open still ends the turn once it is spent"
|
|
1444
|
+
assert_contains "$out" 'FIRSTMATE SUPERVISION IS GENUINELY DOWN' "an abandoned claim suppressed the episode's attended alarm"
|
|
1445
|
+
assert_present "$dir/state/.claude-autoarm-failure-alarmed" "abandoned-claim terminal path did not consume the one-time alarm"
|
|
1446
|
+
assert_absent "$dir/state/.claude-autoarm.lock" "abandoned-claim terminal path left the stale claim in place"
|
|
1447
|
+
assert_absent "$dir/state/.claude-autoarm.lock.steal" "abandoned-claim reclaim left its serialization mutex behind"
|
|
1448
|
+
pass "fm-turnend-guard --claude: the terminal path clears an abandoned claim instead of stepping aside silently"
|
|
1449
|
+
}
|
|
1450
|
+
|
|
1451
|
+
test_hook_claude_mode_preserves_fresh_failed_progression() {
|
|
1452
|
+
local dir out status count
|
|
1453
|
+
dir=$(make_primary_dir "$TMP_ROOT/hook-claude-failed-epoch")
|
|
1454
|
+
: > "$dir/state/task1.meta"
|
|
1455
|
+
: > "$dir/state/.claude-autoarm-failure-notified"
|
|
1456
|
+
printf 'epoch=3 owner_pid=999 outcome=failed updated_at=%s\n' "$(date +%s)" > "$dir/state/.claude-autoarm-epoch"
|
|
1457
|
+
out=$(run_hook_claude "$dir" true); status=$?
|
|
1458
|
+
expect_code 0 "$status" "the first fresh failed epoch must count as its automatic continuation"
|
|
1459
|
+
[ -z "$out" ] || fail "fresh failed-epoch allow produced output: $out"
|
|
1460
|
+
assert_present "$dir/state/.turnend-claude-blocks" "fresh failed epoch did not preserve bounded progression"
|
|
1461
|
+
count=$(sed -n '2s/^count=//p' "$dir/state/.turnend-claude-blocks")
|
|
1462
|
+
[ "$count" = 0 ] || fail "the owned first failed epoch must not consume a blocked-stop count, got $count"
|
|
1463
|
+
printf 'epoch=4 owner_pid=999 outcome=failed-suppressed updated_at=%s\n' "$(date +%s)" > "$dir/state/.claude-autoarm-epoch"
|
|
1464
|
+
out=$(run_hook_claude "$dir" true); status=$?
|
|
1465
|
+
expect_code 2 "$status" "a later fresh failed epoch must consume the bounded progression"
|
|
1466
|
+
assert_absent "$dir/state/.claude-autoarm-failure-alarmed" "fresh failure progression emitted the attended fail-open alarm too early"
|
|
1467
|
+
count=$(sed -n '2s/^count=//p' "$dir/state/.turnend-claude-blocks")
|
|
1468
|
+
[ "$count" = 1 ] || fail "the later failed epoch must advance the blocked-stop count, got $count"
|
|
1469
|
+
pass "fm-turnend-guard --claude: fresh failed epochs preserve and advance monotonic fail-open progression"
|
|
1470
|
+
}
|
|
1471
|
+
|
|
1472
|
+
test_hook_claude_mode_integrated_monotonic_fail_open() {
|
|
1473
|
+
local dir out status guard_out guard_status i pid identity count
|
|
1474
|
+
dir=$(make_primary_dir "$TMP_ROOT/hook-claude-integrated-fail-open")
|
|
1475
|
+
: > "$dir/state/task1.meta"
|
|
1476
|
+
install_integrated_autoarm "$dir"
|
|
1477
|
+
write_integrated_failed_arm "$dir"
|
|
1478
|
+
|
|
1479
|
+
out=$(run_integrated_autoarm "$dir"); status=$?
|
|
1480
|
+
expect_code 2 "$status" "the first exhausted auto-arm cycle must emit its one failure notice"
|
|
1481
|
+
assert_contains "$out" "automatic supervision mechanism is broken" "the first integrated failure notice is missing"
|
|
1482
|
+
guard_out=$(FM_CLAUDE_AUTOARM_SYNC_WAIT_MS=100 run_hook_claude "$dir" true); guard_status=$?
|
|
1483
|
+
expect_code 0 "$guard_status" "the first failed epoch must own its Stop handoff"
|
|
1484
|
+
count=$(sed -n '2s/^count=//p' "$dir/state/.turnend-claude-blocks")
|
|
1485
|
+
[ "$count" = 0 ] || fail "the first owned failure epoch must preserve a zero blocked-stop count, got $count"
|
|
1486
|
+
|
|
1487
|
+
for i in 1 2 3 4; do
|
|
1488
|
+
out=$(run_integrated_autoarm "$dir"); status=$?
|
|
1489
|
+
expect_code 2 "$status" "failed epoch $i must retain the automatic retry handoff"
|
|
1490
|
+
[ -z "$out" ] || fail "failed epoch $i repeated the operator notice: $out"
|
|
1491
|
+
guard_out=$(FM_CLAUDE_AUTOARM_SYNC_WAIT_MS=100 run_hook_claude "$dir" true); guard_status=$?
|
|
1492
|
+
if [ "$i" -lt 4 ]; then
|
|
1493
|
+
expect_code 2 "$guard_status" "failed epoch $i must consume a bounded blind-stop block"
|
|
1494
|
+
assert_not_contains "$guard_out" 'FIRSTMATE SUPERVISION IS GENUINELY DOWN' "fail-open fired before the bounded progression ended"
|
|
1495
|
+
else
|
|
1496
|
+
expect_code 0 "$guard_status" "the bounded failure progression must reach the attended fail-open"
|
|
1497
|
+
assert_contains "$guard_out" 'FIRSTMATE SUPERVISION IS GENUINELY DOWN' "the integrated fail-open alarm is missing"
|
|
1498
|
+
assert_present "$dir/state/.claude-autoarm-failure-alarmed" "the integrated fail-open did not consume its episode alarm"
|
|
1499
|
+
fi
|
|
1500
|
+
done
|
|
1501
|
+
|
|
1502
|
+
out=$(run_integrated_autoarm "$dir"); status=$?
|
|
1503
|
+
expect_code 0 "$status" "the auto-arm must not re-trigger continuation after the final fail-open"
|
|
1504
|
+
[ -z "$out" ] || fail "post-fail-open auto-arm produced continuation output: $out"
|
|
1505
|
+
guard_out=$(FM_CLAUDE_AUTOARM_SYNC_WAIT_MS=100 run_hook_claude "$dir" true); guard_status=$?
|
|
1506
|
+
expect_code 2 "$guard_status" "a later unhealthy stop in the same episode must remain attended"
|
|
1507
|
+
assert_not_contains "$guard_out" 'FIRSTMATE SUPERVISION IS GENUINELY DOWN' "the attended alarm repeated in the same episode"
|
|
1508
|
+
|
|
1509
|
+
sleep 60 &
|
|
1510
|
+
pid=$!
|
|
1511
|
+
identity=$(watcher_identity "$dir" "$pid") || {
|
|
1512
|
+
kill "$pid" 2>/dev/null || true
|
|
1513
|
+
wait "$pid" 2>/dev/null || true
|
|
1514
|
+
fail "could not identify the positive recovery watcher"
|
|
1515
|
+
}
|
|
1516
|
+
record_watcher_lock "$dir" "$pid" "$identity"
|
|
1517
|
+
touch "$dir/state/.last-watcher-beat"
|
|
1518
|
+
out=$(run_integrated_autoarm "$dir"); status=$?
|
|
1519
|
+
kill "$pid" 2>/dev/null || true
|
|
1520
|
+
wait "$pid" 2>/dev/null || true
|
|
1521
|
+
rm -rf "$dir/state/.watch.lock"
|
|
1522
|
+
expect_code 0 "$status" "positive watcher recovery must make the auto-arm silent"
|
|
1523
|
+
assert_absent "$dir/state/.claude-autoarm-failure-notified" "positive recovery left the failure notice marker"
|
|
1524
|
+
assert_absent "$dir/state/.claude-autoarm-failure-alarmed" "positive recovery left the attended alarm marker"
|
|
1525
|
+
assert_absent "$dir/state/.turnend-claude-blocks" "positive recovery left the bounded block budget"
|
|
1526
|
+
guard_out=$(FM_CLAUDE_AUTOARM_SYNC_WAIT_MS=100 run_hook_claude "$dir" false); guard_status=$?
|
|
1527
|
+
expect_code 2 "$guard_status" "a guard after one-shot recovery must start a fresh failure budget"
|
|
1528
|
+
count=$(sed -n '2s/^count=//p' "$dir/state/.turnend-claude-blocks")
|
|
1529
|
+
[ "$count" = 1 ] || fail "the independent post-recovery failure must start at count 1, got $count"
|
|
1530
|
+
|
|
1531
|
+
out=$(run_integrated_autoarm "$dir"); status=$?
|
|
1532
|
+
expect_code 2 "$status" "a later failure after positive recovery must start a new episode"
|
|
1533
|
+
assert_contains "$out" "automatic supervision mechanism is broken" "the new failure episode notice was suppressed"
|
|
1534
|
+
pass "fm-turnend-guard --claude: integrated fresh failures reach one bounded fail-open, stop continuation, and reset on recovery"
|
|
1535
|
+
}
|
|
1536
|
+
|
|
1537
|
+
test_hook_claude_mode_recovery_contention_is_not_ordinary_allow() {
|
|
1538
|
+
local dir pid identity holder out status
|
|
1539
|
+
dir=$(make_primary_dir "$TMP_ROOT/hook-claude-recovery-contention")
|
|
1540
|
+
: > "$dir/state/task1.meta"
|
|
1541
|
+
seed_claude_budget "$dir" 3
|
|
1542
|
+
: > "$dir/state/.claude-autoarm-failure-notified"
|
|
1543
|
+
: > "$dir/state/.claude-autoarm-failure-alarmed"
|
|
1544
|
+
sleep 60 &
|
|
1545
|
+
pid=$!
|
|
1546
|
+
identity=$(watcher_identity "$dir" "$pid") || fail "could not identify recovery-contention watcher"
|
|
1547
|
+
record_watcher_lock "$dir" "$pid" "$identity"
|
|
1548
|
+
touch "$dir/state/.last-watcher-beat"
|
|
1549
|
+
sleep 60 &
|
|
1550
|
+
holder=$!
|
|
1551
|
+
mkdir -p "$dir/state/.turnend-claude-blocks.lock"
|
|
1552
|
+
printf '%s\n' "$holder" > "$dir/state/.turnend-claude-blocks.lock/pid"
|
|
1553
|
+
out=$(run_hook_claude "$dir" false); status=$?
|
|
1554
|
+
expect_code 2 "$status" "a healthy guard must continue when the episode reset lock is busy"
|
|
1555
|
+
[ -z "$out" ] || fail "guard recovery contention produced output: $out"
|
|
1556
|
+
assert_present "$dir/state/.turnend-claude-blocks" "guard contention partially cleared the block budget"
|
|
1557
|
+
assert_present "$dir/state/.claude-autoarm-failure-notified" "guard contention partially cleared the failure notice"
|
|
1558
|
+
assert_present "$dir/state/.claude-autoarm-failure-alarmed" "guard contention partially cleared the attended alarm"
|
|
1559
|
+
kill "$holder" 2>/dev/null || true
|
|
1560
|
+
wait "$holder" 2>/dev/null || true
|
|
1561
|
+
out=$(run_hook_claude "$dir" false); status=$?
|
|
1562
|
+
kill "$pid" 2>/dev/null || true
|
|
1563
|
+
wait "$pid" 2>/dev/null || true
|
|
1564
|
+
expect_code 0 "$status" "the healthy guard must allow after completing the episode reset"
|
|
1565
|
+
assert_absent "$dir/state/.turnend-claude-blocks" "successful guard reset left the block budget"
|
|
1566
|
+
assert_absent "$dir/state/.claude-autoarm-failure-notified" "successful guard reset left the failure notice"
|
|
1567
|
+
assert_absent "$dir/state/.claude-autoarm-failure-alarmed" "successful guard reset left the attended alarm"
|
|
1568
|
+
pass "fm-turnend-guard --claude: reset contention preserves all episode state until retry"
|
|
1569
|
+
}
|
|
1570
|
+
|
|
1571
|
+
test_hook_claude_mode_concurrent_recovery_resets_are_idempotent() {
|
|
1572
|
+
local dir pid identity auto_pid guard_pid auto_status guard_status
|
|
1573
|
+
dir=$(make_primary_dir "$TMP_ROOT/hook-claude-concurrent-recovery")
|
|
1574
|
+
: > "$dir/state/task1.meta"
|
|
1575
|
+
install_integrated_autoarm "$dir"
|
|
1576
|
+
write_integrated_failed_arm "$dir"
|
|
1577
|
+
seed_claude_budget "$dir" 3
|
|
1578
|
+
: > "$dir/state/.claude-autoarm-failure-notified"
|
|
1579
|
+
: > "$dir/state/.claude-autoarm-failure-alarmed"
|
|
1580
|
+
sleep 60 &
|
|
1581
|
+
pid=$!
|
|
1582
|
+
identity=$(watcher_identity "$dir" "$pid") || fail "could not identify concurrent recovery watcher"
|
|
1583
|
+
record_watcher_lock "$dir" "$pid" "$identity"
|
|
1584
|
+
touch "$dir/state/.last-watcher-beat"
|
|
1585
|
+
(run_integrated_autoarm "$dir" > "$dir/auto.out"; printf '%s\n' "$?" > "$dir/auto.status") &
|
|
1586
|
+
auto_pid=$!
|
|
1587
|
+
(FM_CLAUDE_AUTOARM_SYNC_WAIT_MS=100 run_hook_claude "$dir" false > "$dir/guard.out"; printf '%s\n' "$?" > "$dir/guard.status") &
|
|
1588
|
+
guard_pid=$!
|
|
1589
|
+
wait "$auto_pid"
|
|
1590
|
+
wait "$guard_pid"
|
|
1591
|
+
kill "$pid" 2>/dev/null || true
|
|
1592
|
+
wait "$pid" 2>/dev/null || true
|
|
1593
|
+
auto_status=$(cat "$dir/auto.status")
|
|
1594
|
+
guard_status=$(cat "$dir/guard.status")
|
|
1595
|
+
case "$auto_status:$guard_status" in
|
|
1596
|
+
0:0|0:2|2:0) : ;;
|
|
1597
|
+
*) fail "concurrent reset callers returned unsafe statuses auto=$auto_status guard=$guard_status" ;;
|
|
1598
|
+
esac
|
|
1599
|
+
assert_absent "$dir/state/.turnend-claude-blocks" "concurrent recovery left the block budget"
|
|
1600
|
+
assert_absent "$dir/state/.claude-autoarm-failure-notified" "concurrent recovery left the failure notice"
|
|
1601
|
+
assert_absent "$dir/state/.claude-autoarm-failure-alarmed" "concurrent recovery left the attended alarm"
|
|
1602
|
+
assert_absent "$dir/state/.claude-autoarm.lock" "concurrent recovery left the owner lock"
|
|
1603
|
+
assert_absent "$dir/state/.turnend-claude-blocks.lock" "concurrent recovery left the budget lock"
|
|
1604
|
+
pass "fm-turnend-guard --claude: concurrent auto-arm and guard resets are idempotent and deadlock-free"
|
|
1605
|
+
}
|
|
1606
|
+
|
|
1607
|
+
test_hook_claude_mode_stale_rewake_epoch_blocks() {
|
|
1608
|
+
local dir out status
|
|
1609
|
+
dir=$(make_primary_dir "$TMP_ROOT/hook-claude-stale-epoch")
|
|
1610
|
+
: > "$dir/state/task1.meta"
|
|
1611
|
+
printf 'epoch=3 owner_pid=999 outcome=rewake updated_at=1\n' > "$dir/state/.claude-autoarm-epoch"
|
|
1612
|
+
touch -t 202001010000 "$dir/state/.claude-autoarm-epoch"
|
|
1613
|
+
out=$(FM_CLAUDE_AUTOARM_SYNC_WAIT_MS=200 run_hook_claude "$dir" true); status=$?
|
|
1614
|
+
expect_code 2 "$status" "--claude mode must not treat an ancient rewake epoch as this event's recovery"
|
|
1615
|
+
pass "fm-turnend-guard --claude: stale rewake epoch does not allow a blind stop"
|
|
1616
|
+
}
|
|
1617
|
+
|
|
1618
|
+
test_hook_claude_mode_budget_without_verified_failure_keeps_blocking() {
|
|
1619
|
+
local dir out status i
|
|
1620
|
+
dir=$(make_primary_dir "$TMP_ROOT/hook-claude-budget")
|
|
1621
|
+
: > "$dir/state/task1.meta"
|
|
1622
|
+
for i in 1 2 3 4; do
|
|
1623
|
+
out=$(FM_CLAUDE_AUTOARM_SYNC_WAIT_MS=100 run_hook_claude "$dir" false); status=$?
|
|
1624
|
+
expect_code 2 "$status" "--claude block $i must exit 2 within the budget"
|
|
1625
|
+
done
|
|
1626
|
+
assert_not_contains "$out" 'systemMessage' "budget exhaustion without verified auto-arm failure must not fail open"
|
|
1627
|
+
assert_absent "$dir/state/.claude-autoarm-failure-alarmed" "unverified budget exhaustion recorded an attended alarm"
|
|
1628
|
+
pass "fm-turnend-guard --claude: budget exhaustion alone cannot permit a blind stop"
|
|
1629
|
+
}
|
|
1630
|
+
|
|
1631
|
+
test_hook_claude_mode_verified_failure_alarm_is_loud_and_once() {
|
|
1632
|
+
local dir out out2 status status2
|
|
1633
|
+
dir=$(make_primary_dir "$TMP_ROOT/hook-claude-verified-alarm")
|
|
1634
|
+
: > "$dir/state/task1.meta"
|
|
1635
|
+
seed_claude_failure "$dir"
|
|
1636
|
+
seed_claude_budget "$dir" 3
|
|
1637
|
+
out=$(FM_CLAUDE_AUTOARM_SYNC_WAIT_MS=100 run_hook_claude "$dir" true); status=$?
|
|
1638
|
+
expect_code 0 "$status" "verified failure with exhausted budget must take the bounded attended fail-open"
|
|
1639
|
+
assert_contains "$out" 'FIRSTMATE SUPERVISION IS GENUINELY DOWN' "bounded fail-open alarm was not unmistakable"
|
|
1640
|
+
assert_contains "$out" 'Keep this session attended' "bounded fail-open alarm omitted the attended-session action"
|
|
1641
|
+
assert_contains "$out" 'diagnose the automatic Stop-hook and watcher startup' "bounded fail-open alarm omitted automatic-mechanism diagnosis"
|
|
1642
|
+
assert_not_contains "$out" 'fm-watch-arm.sh' "bounded fail-open alarm assigned a manual watcher launch"
|
|
1643
|
+
assert_present "$dir/state/.claude-autoarm-failure-alarmed" "bounded fail-open did not consume the episode alarm"
|
|
1644
|
+
out2=$(FM_CLAUDE_AUTOARM_SYNC_WAIT_MS=100 run_hook_claude "$dir" true); status2=$?
|
|
1645
|
+
expect_code 2 "$status2" "a consumed attended alarm must make later unhealthy stops block again"
|
|
1646
|
+
assert_not_contains "$out2" 'FIRSTMATE SUPERVISION IS GENUINELY DOWN' "attended failure alarm repeated in one episode"
|
|
1647
|
+
pass "fm-turnend-guard --claude: verified fail-open is loud, bounded, attended, and non-repeating"
|
|
1648
|
+
}
|
|
1649
|
+
|
|
1650
|
+
test_hook_claude_mode_fail_open_requires_notice_and_failure_epoch() {
|
|
1651
|
+
local no_notice notice_only out status
|
|
1652
|
+
no_notice=$(make_primary_dir "$TMP_ROOT/hook-claude-alarm-no-notice")
|
|
1653
|
+
: > "$no_notice/state/task1.meta"
|
|
1654
|
+
printf 'epoch=3 owner_pid=999 outcome=failed-suppressed updated_at=1\n' > "$no_notice/state/.claude-autoarm-epoch"
|
|
1655
|
+
touch -t 202001010000 "$no_notice/state/.claude-autoarm-epoch"
|
|
1656
|
+
seed_claude_budget "$no_notice" 3
|
|
1657
|
+
out=$(FM_CLAUDE_AUTOARM_SYNC_WAIT_MS=100 run_hook_claude "$no_notice" true); status=$?
|
|
1658
|
+
expect_code 2 "$status" "an exhausted failure epoch without the consumed notice must remain blocking"
|
|
1659
|
+
|
|
1660
|
+
notice_only=$(make_primary_dir "$TMP_ROOT/hook-claude-alarm-no-epoch")
|
|
1661
|
+
: > "$notice_only/state/task1.meta"
|
|
1662
|
+
: > "$notice_only/state/.claude-autoarm-failure-notified"
|
|
1663
|
+
seed_claude_budget "$notice_only" 3
|
|
1664
|
+
out=$(FM_CLAUDE_AUTOARM_SYNC_WAIT_MS=100 run_hook_claude "$notice_only" true); status=$?
|
|
1665
|
+
expect_code 2 "$status" "a consumed notice without an exhausted failure epoch must remain blocking"
|
|
1666
|
+
pass "fm-turnend-guard --claude: fail-open requires both exhausted retries and consumed notice"
|
|
1667
|
+
}
|
|
1668
|
+
|
|
1669
|
+
test_hook_claude_mode_away_mode_never_uses_stop_autoarm_fail_open() {
|
|
1670
|
+
local dir out status
|
|
1671
|
+
dir=$(make_primary_dir "$TMP_ROOT/hook-claude-alarm-afk")
|
|
1672
|
+
: > "$dir/state/task1.meta"
|
|
1673
|
+
: > "$dir/state/.afk"
|
|
1674
|
+
seed_claude_failure "$dir"
|
|
1675
|
+
seed_claude_budget "$dir" 3
|
|
1676
|
+
out=$(FM_CLAUDE_AUTOARM_SYNC_WAIT_MS=100 run_hook_claude "$dir" true); status=$?
|
|
1677
|
+
expect_code 2 "$status" "away mode must not use a stale Stop-autoarm failure to fail open"
|
|
1678
|
+
assert_contains "$out" 'Away mode owns watcher supervision' "away-mode block lost its daemon ownership guidance"
|
|
1679
|
+
assert_absent "$dir/state/.claude-autoarm-failure-alarmed" "away mode consumed the Stop-autoarm attended alarm"
|
|
1680
|
+
pass "fm-turnend-guard --claude: away ownership excludes the Stop-autoarm fail-open"
|
|
1681
|
+
}
|
|
1682
|
+
|
|
1683
|
+
test_hook_claude_mode_allow_resets_budget() {
|
|
1684
|
+
local dir pid identity out status
|
|
1685
|
+
dir=$(make_primary_dir "$TMP_ROOT/hook-claude-reset")
|
|
1686
|
+
: > "$dir/state/task1.meta"
|
|
1687
|
+
out=$(FM_CLAUDE_AUTOARM_SYNC_WAIT_MS=100 run_hook_claude "$dir" false); status=$?
|
|
1688
|
+
expect_code 2 "$status" "first --claude block must exit 2"
|
|
1689
|
+
[ -f "$dir/state/.turnend-claude-blocks" ] || fail "--claude block must record the consecutive-block budget"
|
|
1690
|
+
: > "$dir/state/.claude-autoarm-failure-notified"
|
|
1691
|
+
: > "$dir/state/.claude-autoarm-failure-alarmed"
|
|
1692
|
+
sleep 60 &
|
|
1693
|
+
pid=$!
|
|
1694
|
+
identity=$(watcher_identity "$dir" "$pid") || {
|
|
1695
|
+
kill "$pid" 2>/dev/null || true
|
|
1696
|
+
wait "$pid" 2>/dev/null || true
|
|
1697
|
+
fail "could not identify live watcher holder"
|
|
1698
|
+
}
|
|
1699
|
+
record_watcher_lock "$dir" "$pid" "$identity"
|
|
1700
|
+
touch "$dir/state/.last-watcher-beat"
|
|
1701
|
+
out=$(run_hook_claude "$dir" false); status=$?
|
|
1702
|
+
kill "$pid" 2>/dev/null || true
|
|
1703
|
+
wait "$pid" 2>/dev/null || true
|
|
1704
|
+
rm -rf "$dir/state/.watch.lock"
|
|
1705
|
+
expect_code 0 "$status" "--claude must allow once the watcher is healthy again"
|
|
1706
|
+
[ ! -f "$dir/state/.turnend-claude-blocks" ] || fail "--claude allow must reset the consecutive-block budget"
|
|
1707
|
+
[ ! -f "$dir/state/.claude-autoarm-failure-notified" ] || fail "positive watcher recovery must reset the failure notice"
|
|
1708
|
+
[ ! -f "$dir/state/.claude-autoarm-failure-alarmed" ] || fail "positive watcher recovery must reset the attended alarm"
|
|
1709
|
+
out=$(FM_CLAUDE_AUTOARM_SYNC_WAIT_MS=100 run_hook_claude "$dir" false); status=$?
|
|
1710
|
+
expect_code 2 "$status" "a later unhealthy chain must re-block from a fresh budget"
|
|
1711
|
+
pass "fm-turnend-guard --claude: positive watcher recovery resets failure episode state"
|
|
1712
|
+
}
|
|
1713
|
+
|
|
1714
|
+
test_hook_claude_mode_waits_for_late_claim() {
|
|
1715
|
+
local dir helper out status holder
|
|
1716
|
+
dir=$(make_primary_dir "$TMP_ROOT/hook-claude-wait")
|
|
1717
|
+
: > "$dir/state/task1.meta"
|
|
1718
|
+
(
|
|
1719
|
+
sleep 0.4
|
|
1720
|
+
sleep 60 &
|
|
1721
|
+
record_autoarm_owner "$dir" $!
|
|
1722
|
+
printf '%s\n' $! > "$dir/holder.pid"
|
|
1723
|
+
wait
|
|
1724
|
+
) &
|
|
1725
|
+
helper=$!
|
|
1726
|
+
out=$(FM_CLAUDE_AUTOARM_SYNC_WAIT_MS=3000 run_hook_claude "$dir" false); status=$?
|
|
1727
|
+
holder=$(cat "$dir/holder.pid" 2>/dev/null || true)
|
|
1728
|
+
kill "$holder" 2>/dev/null || true
|
|
1729
|
+
kill "$helper" 2>/dev/null || true
|
|
1730
|
+
wait "$helper" 2>/dev/null || true
|
|
1731
|
+
expect_code 0 "$status" "--claude must wait briefly for a late auto-arm claim instead of forcing a continuation"
|
|
1732
|
+
[ -z "$out" ] || fail "--claude late-claim wait produced output: $out"
|
|
1733
|
+
pass "fm-turnend-guard --claude: bounded claim wait avoids a token-consuming forced continuation"
|
|
1734
|
+
}
|
|
1735
|
+
|
|
1736
|
+
test_hook_claude_mode_secondmate_reblocks_like_primary() {
|
|
1737
|
+
local dir pid out status
|
|
1738
|
+
dir=$(make_secondmate_dir "$TMP_ROOT/hook-claude-sm-reblock")
|
|
1739
|
+
: > "$dir/state/task1.meta"
|
|
1740
|
+
out=$(FM_CLAUDE_AUTOARM_SYNC_WAIT_MS=200 run_hook_claude "$dir" true); status=$?
|
|
1741
|
+
expect_code 2 "$status" "--claude mode must re-block in a marked secondmate home exactly like the main primary"
|
|
1742
|
+
assert_contains "$out" "TURN WOULD END BLIND" "--claude secondmate re-block must carry the blind-turn banner"
|
|
1743
|
+
sleep 60 &
|
|
1744
|
+
pid=$!
|
|
1745
|
+
record_autoarm_owner "$dir" "$pid"
|
|
1746
|
+
out=$(run_hook_claude "$dir" false); status=$?
|
|
1747
|
+
kill "$pid" 2>/dev/null || true
|
|
1748
|
+
wait "$pid" 2>/dev/null || true
|
|
1749
|
+
expect_code 0 "$status" "--claude mode must allow a claimed secondmate home"
|
|
1750
|
+
pass "fm-turnend-guard --claude: secondmate home re-blocks unclaimed and allows auto-arm-claimed stops"
|
|
1751
|
+
}
|
|
1752
|
+
|
|
1753
|
+
test_predicate_healthy_no_inflight
|
|
1754
|
+
test_predicate_unhealthy_no_beacon
|
|
1755
|
+
test_predicate_unhealthy_stale_beacon
|
|
1756
|
+
test_predicate_healthy_fresh_beacon
|
|
1757
|
+
test_predicate_queue_pending_flag
|
|
1758
|
+
test_predicate_x_mode_needs_supervision
|
|
1759
|
+
test_predicate_source_needs_supervision
|
|
1760
|
+
test_hook_silent_when_no_work_in_flight
|
|
1761
|
+
test_hook_blocks_when_fresh_beacon_has_no_live_lock
|
|
1762
|
+
test_hook_blocks_source_only_home
|
|
1763
|
+
test_hook_blocks_when_dead_lock_has_fresh_beacon
|
|
1764
|
+
test_hook_silent_with_live_lock_and_fresh_beacon
|
|
1765
|
+
test_hook_non_claude_health_ignores_claude_budget_contention
|
|
1766
|
+
test_hook_blocks_with_live_lock_and_stale_beacon
|
|
1767
|
+
test_hook_blocks_when_unhealthy_in_primary
|
|
1768
|
+
test_hook_blocks_from_fm_home_state
|
|
1769
|
+
test_hook_x_mode_reason_sources_cadence
|
|
1770
|
+
test_hook_x_mode_only_blocks_in_default_mode
|
|
1771
|
+
test_hook_ignores_repo_state_when_fm_home_set
|
|
1772
|
+
test_hook_uses_state_override
|
|
1773
|
+
test_hook_loop_guard_allows_retry
|
|
1774
|
+
test_hook_blocks_in_secondmate_own_home
|
|
1775
|
+
test_hook_silent_in_idle_secondmate_home
|
|
1776
|
+
test_hook_secondmate_loop_guard_allows_retry
|
|
1777
|
+
test_hook_secondmate_reinvoke_recovery_loop
|
|
1778
|
+
test_hook_silent_in_secondmate_child_worktree
|
|
1779
|
+
test_hook_blocks_in_treehouse_leased_secondmate_home
|
|
1780
|
+
test_hook_exempts_linked_worktree_with_stray_marker
|
|
1781
|
+
test_hook_exempts_linked_worktree_with_non_ascii_marker
|
|
1782
|
+
test_hook_silent_in_crewmate_worktree
|
|
1783
|
+
test_hook_silent_without_jq
|
|
1784
|
+
test_hook_silent_without_stdin
|
|
1785
|
+
test_hook_runs_fast
|
|
1786
|
+
test_grok_adapter_forces_one_resume_when_unhealthy
|
|
1787
|
+
test_grok_adapter_loop_guard_skips_resume
|
|
1788
|
+
test_grok_adapter_native_false_blocks_without_resume
|
|
1789
|
+
test_grok_adapter_native_true_allows_without_resume
|
|
1790
|
+
test_grok_adapter_snake_case_native_and_camel_precedence
|
|
1791
|
+
test_grok_adapter_invalid_inputs_start_neither_path
|
|
1792
|
+
test_grok_adapter_missing_jq_and_no_supervision_allow
|
|
1793
|
+
test_tracked_claude_entries_inert_under_grok
|
|
1794
|
+
test_codex_hook_uses_process_pwd_when_payload_cwd_is_outside_root
|
|
1795
|
+
test_codex_hook_ignores_nested_git_root_guard
|
|
1796
|
+
test_opencode_plugin_anchors_guard_to_worktree
|
|
1797
|
+
test_pi_extension_injects_once_per_logical_agent_run
|
|
1798
|
+
test_pi_extension_retries_after_followup_delivery_failure
|
|
1799
|
+
test_hook_claude_mode_reblocks_stop_hook_active_when_unhealthy
|
|
1800
|
+
test_hook_claude_mode_reblocks_x_mode_without_tasks
|
|
1801
|
+
test_hook_claude_mode_allows_when_autoarm_owner_alive
|
|
1802
|
+
test_hook_claude_mode_repeated_failed_to_arming_interleavings_reach_fail_open
|
|
1803
|
+
test_hook_claude_mode_terminal_boundary_excludes_starting_owner
|
|
1804
|
+
test_hook_claude_mode_allows_on_fresh_rewake_epoch
|
|
1805
|
+
test_hook_claude_mode_blocks_on_abandoned_autoarm_claim
|
|
1806
|
+
test_hook_claude_mode_blocks_on_pid_reused_arming_claim
|
|
1807
|
+
test_hook_claude_mode_blocks_on_stuck_arming_claim
|
|
1808
|
+
test_hook_claude_mode_allows_on_open_generation_claim
|
|
1809
|
+
test_hook_claude_mode_blocks_on_stuck_generation_claim
|
|
1810
|
+
test_hook_claude_mode_terminal_fail_open_clears_abandoned_claim
|
|
1811
|
+
test_hook_claude_mode_preserves_fresh_failed_progression
|
|
1812
|
+
test_hook_claude_mode_integrated_monotonic_fail_open
|
|
1813
|
+
test_hook_claude_mode_recovery_contention_is_not_ordinary_allow
|
|
1814
|
+
test_hook_claude_mode_concurrent_recovery_resets_are_idempotent
|
|
1815
|
+
test_hook_claude_mode_stale_rewake_epoch_blocks
|
|
1816
|
+
test_hook_claude_mode_budget_without_verified_failure_keeps_blocking
|
|
1817
|
+
test_hook_claude_mode_verified_failure_alarm_is_loud_and_once
|
|
1818
|
+
test_hook_claude_mode_fail_open_requires_notice_and_failure_epoch
|
|
1819
|
+
test_hook_claude_mode_away_mode_never_uses_stop_autoarm_fail_open
|
|
1820
|
+
test_hook_claude_mode_allow_resets_budget
|
|
1821
|
+
test_hook_claude_mode_waits_for_late_claim
|
|
1822
|
+
test_hook_claude_mode_secondmate_reblocks_like_primary
|