@zerwiz/ymir 0.1.3 → 0.1.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.agents/README.md +291 -0
- package/.agents/agents/bragi-marketer.md +111 -0
- package/.agents/agents/brokk.md +352 -0
- package/.agents/agents/forseti-reviewer.md +93 -0
- package/.agents/agents/frigg-consent.md +77 -0
- package/.agents/agents/groa-update.md +77 -0
- package/.agents/agents/hnoss-designer.md +69 -0
- package/.agents/agents/huginn-researcher.md +99 -0
- package/.agents/agents/jord-projects.md +78 -0
- package/.agents/agents/kvasir-scout.md +91 -0
- package/.agents/agents/mimir-planner.md +92 -0
- package/.agents/agents/muninn-stow.md +77 -0
- package/.agents/agents/saga-bearings.md +78 -0
- package/.agents/agents/sindri-developer.md +114 -0
- package/.agents/agents/snotra-documenter.md +105 -0
- package/.agents/agents/syn-recovery.md +78 -0
- package/.agents/agents/tyr-check.md +78 -0
- package/.agents/agents/urdh-hold.md +77 -0
- package/.agents/agents/volundr-smith.md +80 -0
- package/.agents/agents/vor-diagnostics.md +77 -0
- package/.agents/assets/agents/naming.md +50 -0
- package/.agents/assets/agents/registry.md +93 -0
- package/.agents/assets/agents/runtime.md +36 -0
- package/.agents/assets/agents/toon-tasks-cli.md +42 -0
- package/.agents/assets/templates/company_entity.template.md +37 -0
- package/.agents/backend/backends/cmux.sh +649 -0
- package/.agents/backend/backends/herdr-eventwait.py +157 -0
- package/.agents/backend/backends/herdr-workspace-move.py +114 -0
- package/.agents/backend/backends/herdr.sh +3386 -0
- package/.agents/backend/backends/orca.sh +290 -0
- package/.agents/backend/backends/tmux.sh +357 -0
- package/.agents/backend/backends/zellij.sh +700 -0
- package/.agents/backend/fm-afk-launch.sh +654 -0
- package/.agents/backend/fm-afk-return.sh +241 -0
- package/.agents/backend/fm-afk-start.sh +172 -0
- package/.agents/backend/fm-arm-command-policy.mjs +960 -0
- package/.agents/backend/fm-arm-pretool-check.sh +200 -0
- package/.agents/backend/fm-backend-hometag-lib.sh +52 -0
- package/.agents/backend/fm-backend.sh +981 -0
- package/.agents/backend/fm-backlog-handoff.sh +909 -0
- package/.agents/backend/fm-backlog-receive.sh +187 -0
- package/.agents/backend/fm-backlog-transition-lib.sh +779 -0
- package/.agents/backend/fm-bearings-board.sh +204 -0
- package/.agents/backend/fm-bearings-snapshot.sh +546 -0
- package/.agents/backend/fm-bootstrap.sh +1568 -0
- package/.agents/backend/fm-branch-outcome.sh +203 -0
- package/.agents/backend/fm-branch-prompt.sh +101 -0
- package/.agents/backend/fm-brief.sh +452 -0
- package/.agents/backend/fm-busy-event.sh +234 -0
- package/.agents/backend/fm-busy-lib.sh +986 -0
- package/.agents/backend/fm-captain-hold.sh +1001 -0
- package/.agents/backend/fm-cd-command-policy.mjs +153 -0
- package/.agents/backend/fm-cd-pretool-check.sh +190 -0
- package/.agents/backend/fm-check-lib.sh +74 -0
- package/.agents/backend/fm-check-register.sh +42 -0
- package/.agents/backend/fm-check-unregister.sh +52 -0
- package/.agents/backend/fm-classify-lib.sh +1770 -0
- package/.agents/backend/fm-claude-stop-autoarm.sh +334 -0
- package/.agents/backend/fm-composer-lib.sh +1416 -0
- package/.agents/backend/fm-config-inherit-lib.sh +1202 -0
- package/.agents/backend/fm-config-push.sh +254 -0
- package/.agents/backend/fm-control-lib.sh +251 -0
- package/.agents/backend/fm-control.sh +878 -0
- package/.agents/backend/fm-crew-state.sh +625 -0
- package/.agents/backend/fm-cursor-lib.sh +243 -0
- package/.agents/backend/fm-decision-hold.sh +232 -0
- package/.agents/backend/fm-doc-audience-check.sh +269 -0
- package/.agents/backend/fm-dod-lib.sh +67 -0
- package/.agents/backend/fm-ensure-agents-md.sh +253 -0
- package/.agents/backend/fm-extension-launch-barrier.mjs +129 -0
- package/.agents/backend/fm-extension.mjs +2577 -0
- package/.agents/backend/fm-extension.sh +16 -0
- package/.agents/backend/fm-ff-lib.sh +420 -0
- package/.agents/backend/fm-fleet-snapshot.sh +1463 -0
- package/.agents/backend/fm-fleet-sync.sh +458 -0
- package/.agents/backend/fm-fleet-view.sh +96 -0
- package/.agents/backend/fm-gate-refuse-lib.sh +102 -0
- package/.agents/backend/fm-guard.sh +242 -0
- package/.agents/backend/fm-harness.sh +195 -0
- package/.agents/backend/fm-herdr-ci-cleanup.sh +115 -0
- package/.agents/backend/fm-herdr-lab.sh +346 -0
- package/.agents/backend/fm-herdr-session-cleanup.sh +337 -0
- package/.agents/backend/fm-home-seed.sh +984 -0
- package/.agents/backend/fm-home-summary-refresh.sh +255 -0
- package/.agents/backend/fm-hook-host-lib.sh +36 -0
- package/.agents/backend/fm-inactive-reconcile.sh +531 -0
- package/.agents/backend/fm-inbox.sh +399 -0
- package/.agents/backend/fm-install-actionlint.sh +84 -0
- package/.agents/backend/fm-install-herdr.sh +96 -0
- package/.agents/backend/fm-install-shellcheck.sh +84 -0
- package/.agents/backend/fm-install-treehouse.sh +95 -0
- package/.agents/backend/fm-kimi-turnend-hook.sh +276 -0
- package/.agents/backend/fm-lease-lib.sh +218 -0
- package/.agents/backend/fm-lease.sh +189 -0
- package/.agents/backend/fm-line-cap-lib.sh +51 -0
- package/.agents/backend/fm-lint-workflows.sh +137 -0
- package/.agents/backend/fm-lint.sh +591 -0
- package/.agents/backend/fm-lock-lib.sh +104 -0
- package/.agents/backend/fm-lock.sh +107 -0
- package/.agents/backend/fm-marker-lib.sh +12 -0
- package/.agents/backend/fm-merge-local.sh +74 -0
- package/.agents/backend/fm-merge-outcome-lib.sh +138 -0
- package/.agents/backend/fm-nm-run-lib.sh +124 -0
- package/.agents/backend/fm-on.sh +125 -0
- package/.agents/backend/fm-operational-input.sh +253 -0
- package/.agents/backend/fm-peek.sh +46 -0
- package/.agents/backend/fm-pending-reply-lib.sh +1409 -0
- package/.agents/backend/fm-pr-check.sh +135 -0
- package/.agents/backend/fm-pr-lib.sh +1016 -0
- package/.agents/backend/fm-pr-merge.sh +707 -0
- package/.agents/backend/fm-pr-poll.sh +110 -0
- package/.agents/backend/fm-primary-scope-lib.sh +33 -0
- package/.agents/backend/fm-procevent-extension-capture.pl +259 -0
- package/.agents/backend/fm-procevent-lavish.sh +628 -0
- package/.agents/backend/fm-procevent-lib.sh +892 -0
- package/.agents/backend/fm-procevent-quota.sh +290 -0
- package/.agents/backend/fm-procevent-remote-reply.sh +577 -0
- package/.agents/backend/fm-procevent-when.sh +504 -0
- package/.agents/backend/fm-procevent.sh +1466 -0
- package/.agents/backend/fm-project-mode.sh +92 -0
- package/.agents/backend/fm-project-origin-lib.sh +180 -0
- package/.agents/backend/fm-promote.sh +282 -0
- package/.agents/backend/fm-public-followup-emit.sh +260 -0
- package/.agents/backend/fm-public-followup-lib.sh +398 -0
- package/.agents/backend/fm-public-followup.sh +1330 -0
- package/.agents/backend/fm-push-transition-lib.sh +168 -0
- package/.agents/backend/fm-quota-axi-lib.sh +93 -0
- package/.agents/backend/fm-quota-choose.sh +384 -0
- package/.agents/backend/fm-remote-delta-read.sh +189 -0
- package/.agents/backend/fm-remote-doctor.sh +800 -0
- package/.agents/backend/fm-remote-entrypoint.sh +193 -0
- package/.agents/backend/fm-remote-file.sh +234 -0
- package/.agents/backend/fm-remote-home-provision.sh +259 -0
- package/.agents/backend/fm-remote-home-seed.sh +259 -0
- package/.agents/backend/fm-remote-inherit-push.sh +90 -0
- package/.agents/backend/fm-remote-inherit.sh +179 -0
- package/.agents/backend/fm-remote-job-lib.sh +1241 -0
- package/.agents/backend/fm-remote-job-reap-orphans.sh +137 -0
- package/.agents/backend/fm-remote-job-worker.sh +1124 -0
- package/.agents/backend/fm-remote-readiness-lib.sh +44 -0
- package/.agents/backend/fm-remote-secondmate-control.sh +340 -0
- package/.agents/backend/fm-review-diff.sh +158 -0
- package/.agents/backend/fm-secondmate-charter-lib.sh +44 -0
- package/.agents/backend/fm-secondmate-nudge-lib.sh +68 -0
- package/.agents/backend/fm-secondmate-parent-lib.sh +70 -0
- package/.agents/backend/fm-secondmate-reconcile.sh +371 -0
- package/.agents/backend/fm-secondmate-registry-lib.sh +311 -0
- package/.agents/backend/fm-secondmate-report.sh +88 -0
- package/.agents/backend/fm-send.sh +1059 -0
- package/.agents/backend/fm-session-lock-lib.sh +176 -0
- package/.agents/backend/fm-session-start.sh +970 -0
- package/.agents/backend/fm-sessionstart-cursor.sh +40 -0
- package/.agents/backend/fm-sessionstart-nudge.sh +45 -0
- package/.agents/backend/fm-sessionstart-run.sh +145 -0
- package/.agents/backend/fm-spawn.sh +3153 -0
- package/.agents/backend/fm-startup-memory-budget-lib.sh +224 -0
- package/.agents/backend/fm-startup-memory-budget.sh +94 -0
- package/.agents/backend/fm-startup-network.sh +642 -0
- package/.agents/backend/fm-stow-cascade.sh +251 -0
- package/.agents/backend/fm-subagent-pretool-check.sh +207 -0
- package/.agents/backend/fm-supervise-daemon.sh +1732 -0
- package/.agents/backend/fm-supervision-instructions.sh +215 -0
- package/.agents/backend/fm-supervision-lib.sh +90 -0
- package/.agents/backend/fm-supervisor-target-lib.sh +78 -0
- package/.agents/backend/fm-tangle-lib.sh +53 -0
- package/.agents/backend/fm-task-inbox-lib.sh +402 -0
- package/.agents/backend/fm-tasks-axi-lib.sh +123 -0
- package/.agents/backend/fm-teardown.sh +2922 -0
- package/.agents/backend/fm-test-isolation-proof.sh +588 -0
- package/.agents/backend/fm-test-run.sh +2191 -0
- package/.agents/backend/fm-timeout-lib.sh +141 -0
- package/.agents/backend/fm-timing-lib.sh +179 -0
- package/.agents/backend/fm-tmux-lib.sh +291 -0
- package/.agents/backend/fm-tool-update-check.sh +898 -0
- package/.agents/backend/fm-trace-context-lib.sh +227 -0
- package/.agents/backend/fm-transition-lib.sh +103 -0
- package/.agents/backend/fm-turnend-guard-cursor.sh +391 -0
- package/.agents/backend/fm-turnend-guard-grok.sh +90 -0
- package/.agents/backend/fm-turnend-guard.sh +432 -0
- package/.agents/backend/fm-update.sh +110 -0
- package/.agents/backend/fm-vendor-auth-probe.sh +190 -0
- package/.agents/backend/fm-voice-client.py +1373 -0
- package/.agents/backend/fm-voice-relay.py +1256 -0
- package/.agents/backend/fm-wake-drain.sh +592 -0
- package/.agents/backend/fm-wake-grant.sh +127 -0
- package/.agents/backend/fm-wake-lib.sh +1849 -0
- package/.agents/backend/fm-watch-arm.sh +602 -0
- package/.agents/backend/fm-watch-checkpoint.sh +109 -0
- package/.agents/backend/fm-watch.sh +1962 -0
- package/.agents/backend/fm-x-dismiss.sh +116 -0
- package/.agents/backend/fm-x-followup.sh +286 -0
- package/.agents/backend/fm-x-lib.sh +1000 -0
- package/.agents/backend/fm-x-link.sh +234 -0
- package/.agents/backend/fm-x-poll.sh +185 -0
- package/.agents/backend/fm-x-reply.sh +385 -0
- package/.agents/backend/fm_voice_frame.py +166 -0
- package/.agents/backend/fm_voice_records.py +574 -0
- package/.agents/backend/model-bridge.py +290 -0
- package/.agents/backend/opencode-go-bridge.py +30 -0
- package/.agents/bus/README.md +12 -0
- package/.agents/bus/messages.json +1 -0
- package/.agents/config/agents.machine.example.yaml +16 -0
- package/.agents/config/agents.yaml +50 -0
- package/.agents/config/agents.yaml.example +72 -0
- package/.agents/config/crew-dispatch.json +22 -0
- package/.agents/config/cron.yaml +9 -0
- package/.agents/config/eindri-dispatch.json +67 -0
- package/.agents/config/eindri-harness +1 -0
- package/.agents/config/startup-memory-budget +1 -0
- package/.agents/config/tailscale-sync.yaml +27 -0
- package/.agents/filebrowser/.filebrowser.json +6 -0
- package/.agents/filebrowser/README.md +11 -0
- package/.agents/gateway/README.md +12 -0
- package/.agents/github/README.md +15 -0
- package/.agents/github/webhooks/.gitkeep +0 -0
- package/.agents/github/workflows/.gitkeep +0 -0
- package/.agents/harness/opencode/plugins/lib/rodd-operational-input.js +37 -0
- package/.agents/harness/opencode/plugins/package.json +4 -0
- package/.agents/harness/opencode/plugins/saga-sessionstart.js +77 -0
- package/.agents/harness/opencode/plugins/syn-cd-check.js +61 -0
- package/.agents/harness/opencode/plugins/syn-pretool-check.js +68 -0
- package/.agents/harness/opencode/plugins/syn-turnend-guard.js +105 -0
- package/.agents/harness/opencode/plugins/syn-watch-arm.js +521 -0
- package/.agents/memory/README.md +46 -0
- package/.agents/memory/kaia.engram +0 -0
- package/.agents/memory/kaia.engram-shm +0 -0
- package/.agents/memory/kaia.engram-wal +0 -0
- package/.agents/memory/runes_audit.md +5 -0
- package/.agents/memory/well/episodes.jsonl +367 -0
- package/.agents/memory/well/workspace.jsonl +27 -0
- package/.agents/migrations/0001-hodd-layout.sh +38 -0
- package/.agents/migrations/0002-a2a-mcp.sh +27 -0
- package/.agents/migrations/0003-private-data-separation.sh +111 -0
- package/.agents/migrations/0004-hoard-and-realms.sh +104 -0
- package/.agents/sandbox/Dockerfile.utgard +19 -0
- package/.agents/sandbox/README.md +14 -0
- package/.agents/sandbox/utgard.config.json +14 -0
- package/.agents/skills/README.md +59 -0
- package/.agents/skills/bragi-marketing/README.md +3 -0
- package/.agents/skills/bragi-marketing/SKILL.md +82 -0
- package/.agents/skills/eindri-homes/README.md +3 -0
- package/.agents/skills/eindri-homes/SKILL.md +291 -0
- package/.agents/skills/eindri-homes/assets/control-plane.md +122 -0
- package/.agents/skills/eindri-homes/assets/dispatch.md +80 -0
- package/.agents/skills/frigg-consent/README.md +3 -0
- package/.agents/skills/frigg-consent/SKILL.md +56 -0
- package/.agents/skills/galdr-ymirsystem/README.md +3 -0
- package/.agents/skills/galdr-ymirsystem/SKILL.md +158 -0
- package/.agents/skills/galdr-ymirsystem/assets/README.md +74 -0
- package/.agents/skills/galdr-ymirsystem/assets/brokk-distro-runtime.md +390 -0
- package/.agents/skills/galdr-ymirsystem/assets/build-method.md +113 -0
- package/.agents/skills/galdr-ymirsystem/assets/build-tool-categories.md +178 -0
- package/.agents/skills/galdr-ymirsystem/assets/eindri-orchestration.md +518 -0
- package/.agents/skills/galdr-ymirsystem/assets/eindri-profiles.md +136 -0
- package/.agents/skills/galdr-ymirsystem/assets/harness-integration/README.md +673 -0
- package/.agents/skills/galdr-ymirsystem/assets/harness-integration/claude-code.md +197 -0
- package/.agents/skills/galdr-ymirsystem/assets/harness-integration/codex.md +191 -0
- package/.agents/skills/galdr-ymirsystem/assets/harness-integration/cursor.md +193 -0
- package/.agents/skills/galdr-ymirsystem/assets/harness-integration/opencode.md +262 -0
- package/.agents/skills/galdr-ymirsystem/assets/harness-integration/pi.md +295 -0
- package/.agents/skills/galdr-ymirsystem/assets/hlidskjalf-ui.md +496 -0
- package/.agents/skills/galdr-ymirsystem/assets/installation.md +571 -0
- package/.agents/skills/galdr-ymirsystem/assets/local-models/HOST-DATA.example.md +72 -0
- package/.agents/skills/galdr-ymirsystem/assets/local-models.md +274 -0
- package/.agents/skills/galdr-ymirsystem/assets/memory-well.md +161 -0
- package/.agents/skills/galdr-ymirsystem/assets/nornir-jobs.md +377 -0
- package/.agents/skills/galdr-ymirsystem/assets/norse-naming.md +325 -0
- package/.agents/skills/galdr-ymirsystem/assets/odrerir-hall.md +129 -0
- package/.agents/skills/galdr-ymirsystem/assets/pi-boot/einherjar-spawn.schema.json +81 -0
- package/.agents/skills/galdr-ymirsystem/assets/pi-boot/herdr-profile.toml +75 -0
- package/.agents/skills/galdr-ymirsystem/assets/pi-boot/pi-profile.yml +96 -0
- package/.agents/skills/galdr-ymirsystem/assets/pi-boot/supervision-tree.yml +163 -0
- package/.agents/skills/galdr-ymirsystem/assets/pi-boot-guide.md +58 -0
- package/.agents/skills/galdr-ymirsystem/assets/porting-upstream-to-norse.md +301 -0
- package/.agents/skills/galdr-ymirsystem/assets/principles.md +385 -0
- package/.agents/skills/galdr-ymirsystem/assets/reference-adoption.md +99 -0
- package/.agents/skills/galdr-ymirsystem/assets/registry.md +379 -0
- package/.agents/skills/galdr-ymirsystem/assets/runtime-compliance.md +463 -0
- package/.agents/skills/galdr-ymirsystem/assets/runtime-components.md +553 -0
- package/.agents/skills/galdr-ymirsystem/assets/smidja.md +175 -0
- package/.agents/skills/galdr-ymirsystem/schemas/toon-schemas.md +190 -0
- package/.agents/skills/galdr-ymirsystem/scripts/README.md +45 -0
- package/.agents/skills/galdr-ymirsystem/scripts/bench-one.sh +248 -0
- package/.agents/skills/galdr-ymirsystem/scripts/compliance-check.sh +360 -0
- package/.agents/skills/galdr-ymirsystem/scripts/gpu-sample.sh +43 -0
- package/.agents/skills/galdr-ymirsystem/scripts/stop-all.sh +34 -0
- package/.agents/skills/galdr-ymirsystem/scripts/toon-check.py +188 -0
- package/.agents/skills/gjallarhorn-relay/README.md +3 -0
- package/.agents/skills/gjallarhorn-relay/SKILL.md +283 -0
- package/.agents/skills/groa-update/SKILL.md +70 -0
- package/.agents/skills/hamr-adapters/README.md +3 -0
- package/.agents/skills/hamr-adapters/SKILL.md +123 -0
- package/.agents/skills/hamr-adapters/references/common/control-and-recovery.md +37 -0
- package/.agents/skills/hamr-adapters/references/common/dispatch.md +32 -0
- package/.agents/skills/hamr-adapters/references/common/model-and-effort.md +42 -0
- package/.agents/skills/hamr-adapters/references/common/primary-hooks.md +40 -0
- package/.agents/skills/hamr-adapters/references/harness/claude.md +55 -0
- package/.agents/skills/hamr-adapters/references/harness/codex.md +43 -0
- package/.agents/skills/hamr-adapters/references/harness/cursor.md +75 -0
- package/.agents/skills/hamr-adapters/references/harness/grok.md +69 -0
- package/.agents/skills/hamr-adapters/references/harness/kimi.md +51 -0
- package/.agents/skills/hamr-adapters/references/harness/muse.md +70 -0
- package/.agents/skills/hamr-adapters/references/harness/opencode.md +42 -0
- package/.agents/skills/hamr-adapters/references/harness/pi.md +56 -0
- package/.agents/skills/herdr-panes/README.md +3 -0
- package/.agents/skills/herdr-panes/SKILL.md +92 -0
- package/.agents/skills/herdr-panes/assets/herdr-backend.md +346 -0
- package/.agents/skills/herdr-panes/assets/tmux-backend.md +115 -0
- package/.agents/skills/hnoss-design/DESIGN.md +19 -0
- package/.agents/skills/hnoss-design/README.md +3 -0
- package/.agents/skills/hnoss-design/SKILL.md +83 -0
- package/.agents/skills/hvild-afk/README.md +3 -0
- package/.agents/skills/hvild-afk/SKILL.md +241 -0
- package/.agents/skills/jord-projects/README.md +3 -0
- package/.agents/skills/jord-projects/SKILL.md +92 -0
- package/.agents/skills/lifecycle/SKILL.md +56 -0
- package/.agents/skills/lifecycle/smoke_test.sh +52 -0
- package/.agents/skills/lifecycle/start.sh +21 -0
- package/.agents/skills/lifecycle/status.sh +35 -0
- package/.agents/skills/lifecycle/stop.sh +16 -0
- package/.agents/skills/muninn-stow/README.md +3 -0
- package/.agents/skills/muninn-stow/SKILL.md +309 -0
- package/.agents/skills/no-mistakes/SKILL.md +379 -0
- package/.agents/skills/nornir-schedule/README.md +3 -0
- package/.agents/skills/nornir-schedule/SKILL.md +25 -0
- package/.agents/skills/nornir-schedule/assets/events.md +137 -0
- package/.agents/skills/nornir-schedule/assets/quota.md +131 -0
- package/.agents/skills/nsr-compliance/README.md +3 -0
- package/.agents/skills/nsr-compliance/SKILL.md +29 -0
- package/.agents/skills/nsr-compliance/assets/nsr/README.md +40 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/RULES/README.md +15 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/RULES/git-workflow.md +18 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/RULES/security.md +22 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/RULES/tech-stack.md +8 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/SKILL.md +71 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/config/core_four.yaml +16 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/gates/check_danger.sh +102 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/gates/check_wiring.sh +69 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/gates/validate_code.sh +29 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/gates/verify_docs.py +66 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/generate.sh +109 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/harness/envelopes/result_envelope.json +14 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/harness/envelopes/task_envelope.json +20 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/harness/runner.py +303 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/telemetry/logger.py +112 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/templates/compliance/README.md +18 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/templates/compliance/config/README.md +10 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/templates/compliance/gates/README.md +12 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/templates/compliance/gates/check_env.sh +23 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/templates/compliance/gates/check_paths.sh +20 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/templates/compliance/gates/check_platform.sh +23 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/templates/compliance/harness/README.md +11 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/templates/compliance/harness/envelopes/README.md +10 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/templates/compliance/installer/README.md +10 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/templates/compliance/installer/install.sh +125 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/templates/compliance/telemetry/README.md +11 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/README.md +20 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/features/README.md +36 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/features/SKILL-template.md +14 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/git_ops/README.md +25 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/git_ops/SKILL.md +13 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/git_ops/create_branch.sh +6 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/git_ops/safe_commit.sh +9 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/git_ops/sync_upstream.sh +6 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/lifecycle/README.md +27 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/lifecycle/SKILL.md +15 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/lifecycle/smoke_test.sh +12 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/lifecycle/start.sh +19 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/lifecycle/status.sh +12 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/lifecycle/stop.sh +15 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/README.md +18 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/config/README.md +10 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/config/core_four.yaml +13 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/gates/README.md +12 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/gates/check_danger.sh +102 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/gates/check_env.sh +23 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/gates/check_paths.sh +20 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/gates/check_platform.sh +23 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/gates/check_wiring.sh +69 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/harness/README.md +11 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/harness/envelopes/README.md +10 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/harness/envelopes/result_envelope.json +14 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/harness/envelopes/task_envelope.json +20 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/harness/runner.py +303 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/installer/README.md +10 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/installer/install.sh +125 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/telemetry/README.md +11 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/telemetry/logger.py +112 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/BEST_PRACTICES/README.md +14 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/BEST_PRACTICES/api.md +17 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/BEST_PRACTICES/compliance-wiring.md +155 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/BEST_PRACTICES/frontend.md +15 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/CI_CD/README.md +11 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/CI_CD/deployment/README.md +23 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/CI_CD/deployment/clients/README.md +16 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/CI_CD/deployment/clients/template-README.md +11 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/CI_CD/deployment/deploy.sh +49 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/CI_CD/deployment/envs/README.md +12 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/CI_CD/deployment/envs/development.env.example +17 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/CI_CD/deployment/envs/production.env.example +17 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/CI_CD/deployment/envs/staging.env.example +17 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/CI_CD/deployment/tenants/README.md +11 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/CI_CD/deployment/tenants/template.env.example +8 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/DEVELOPER_SETUP/README.md +26 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/DEVELOPER_SETUP/agent.md +7 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/DEVELOPER_SETUP/developers/dev-1.md +20 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/DEVELOPER_SETUP/linux.md +9 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/DEVELOPER_SETUP/macos.md +10 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/DEVELOPER_SETUP/shared.md +19 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/DEVELOPER_SETUP/windows.md +14 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/HOSTING/README.md +25 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/HOSTING/_template.md +41 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/HOSTING/hosting-1.md +41 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/README.md +21 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/RUNBOOK/README.md +15 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/RUNBOOK/deployment.md +19 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/RUNBOOK/disaster-recovery.md +15 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/RUNBOOK/ops-tasks.md +14 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/features/README.md +16 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/project-skills.md +146 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/research/README.md +11 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/research/software-compliance-research.md +269 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/working-with-agents.md +260 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/root/AGENTS.md +38 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/root/ARCHITECTURE.md +36 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/root/BEST_PRACTICES.md +27 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/root/CI_CD.md +23 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/root/FEATURES.md +15 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/root/README.md +22 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/root/RULES.md +37 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/root/STRUCTURE.md +45 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/root/TECH_STACK.md +28 -0
- package/.agents/skills/nsr-compliance/assets/nsr/scaffold-spec.md +63 -0
- package/.agents/skills/nsr-compliance/assets/nsr/scripts/scaffold.sh +103 -0
- package/.agents/skills/nsr-compliance/assets/nsr/scripts/validate.sh +52 -0
- package/.agents/skills/nsr-compliance/assets/nsrcompliance/SKILL.md +71 -0
- package/.agents/skills/nsr-compliance/assets/nsrcompliance/config/core_four.yaml +16 -0
- package/.agents/skills/nsr-compliance/assets/nsrcompliance/gates/check_danger.sh +102 -0
- package/.agents/skills/nsr-compliance/assets/nsrcompliance/gates/check_wiring.sh +69 -0
- package/.agents/skills/nsr-compliance/assets/nsrcompliance/gates/validate_code.sh +29 -0
- package/.agents/skills/nsr-compliance/assets/nsrcompliance/gates/verify_docs.py +66 -0
- package/.agents/skills/nsr-compliance/assets/nsrcompliance/generate.sh +109 -0
- package/.agents/skills/nsr-compliance/assets/nsrcompliance/harness/envelopes/result_envelope.json +14 -0
- package/.agents/skills/nsr-compliance/assets/nsrcompliance/harness/envelopes/task_envelope.json +20 -0
- package/.agents/skills/nsr-compliance/assets/nsrcompliance/harness/runner.py +303 -0
- package/.agents/skills/nsr-compliance/assets/nsrcompliance/telemetry/logger.py +112 -0
- package/.agents/skills/nsr-compliance/assets/nsrcompliance/templates/compliance/README.md +18 -0
- package/.agents/skills/nsr-compliance/assets/nsrcompliance/templates/compliance/config/README.md +10 -0
- package/.agents/skills/nsr-compliance/assets/nsrcompliance/templates/compliance/gates/README.md +12 -0
- package/.agents/skills/nsr-compliance/assets/nsrcompliance/templates/compliance/gates/check_env.sh +23 -0
- package/.agents/skills/nsr-compliance/assets/nsrcompliance/templates/compliance/gates/check_paths.sh +20 -0
- package/.agents/skills/nsr-compliance/assets/nsrcompliance/templates/compliance/gates/check_platform.sh +23 -0
- package/.agents/skills/nsr-compliance/assets/nsrcompliance/templates/compliance/harness/README.md +11 -0
- package/.agents/skills/nsr-compliance/assets/nsrcompliance/templates/compliance/harness/envelopes/README.md +10 -0
- package/.agents/skills/nsr-compliance/assets/nsrcompliance/templates/compliance/installer/README.md +10 -0
- package/.agents/skills/nsr-compliance/assets/nsrcompliance/templates/compliance/installer/install.sh +125 -0
- package/.agents/skills/nsr-compliance/assets/nsrcompliance/templates/compliance/telemetry/README.md +11 -0
- package/.agents/skills/pr-ops/README.md +3 -0
- package/.agents/skills/pr-ops/SKILL.md +109 -0
- package/.agents/skills/ratatoskr-a2a/README.md +3 -0
- package/.agents/skills/ratatoskr-a2a/SKILL.md +148 -0
- package/.agents/skills/rules-check-drift/SKILL.md +81 -0
- package/.agents/skills/saga-bearings/README.md +3 -0
- package/.agents/skills/saga-bearings/SKILL.md +26 -0
- package/.agents/skills/saga-bearings/assets/bearings.md +161 -0
- package/.agents/skills/saga-bearings/assets/board-template.html +731 -0
- package/.agents/skills/saga-bearings/assets/recap.md +56 -0
- package/.agents/skills/syn-recovery/README.md +3 -0
- package/.agents/skills/syn-recovery/SKILL.md +54 -0
- package/.agents/skills/tyr-check/README.md +3 -0
- package/.agents/skills/tyr-check/SKILL.md +218 -0
- package/.agents/skills/urdh-hold/README.md +3 -0
- package/.agents/skills/urdh-hold/SKILL.md +25 -0
- package/.agents/skills/urdh-hold/assets/decisions.md +15 -0
- package/.agents/skills/urdh-hold/assets/hold.md +54 -0
- package/.agents/skills/vor-diagnostics/README.md +3 -0
- package/.agents/skills/vor-diagnostics/SKILL.md +68 -0
- package/.agents/skills/ymir-host/README.md +3 -0
- package/.agents/skills/ymir-host/SKILL.md +34 -0
- package/.agents/skills/ymir-host/assets/desktop.md +36 -0
- package/.agents/skills/ymir-host/assets/omarchy.md +297 -0
- package/.agents/skills/ymir-host/assets/runbooks.md +15 -0
- package/.agents/skills/ymir-host/assets/thjazi.md +220 -0
- package/.agents/skills/ymir-host/assets/update.md +64 -0
- package/.agents/tests/assets/board-render-harness.mjs +117 -0
- package/.agents/tests/cmux-test-safety.sh +51 -0
- package/.agents/tests/fixtures.sh +291 -0
- package/.agents/tests/fm-afk-inject-e2e.test.sh +428 -0
- package/.agents/tests/fm-afk-inject-herdr-e2e.test.sh +536 -0
- package/.agents/tests/fm-afk-launch.test.sh +961 -0
- package/.agents/tests/fm-afk-pi-herdr-return-e2e.test.sh +297 -0
- package/.agents/tests/fm-afk-return.test.sh +283 -0
- package/.agents/tests/fm-arm-pretool-check.test.sh +482 -0
- package/.agents/tests/fm-ask-user-authority.test.sh +42 -0
- package/.agents/tests/fm-backend-autodetect-smoke.test.sh +176 -0
- package/.agents/tests/fm-backend-cmux-smoke.test.sh +188 -0
- package/.agents/tests/fm-backend-cmux.test.sh +1166 -0
- package/.agents/tests/fm-backend-herdr-eventwait-smoke.test.sh +136 -0
- package/.agents/tests/fm-backend-herdr-eventwait.test.py +106 -0
- package/.agents/tests/fm-backend-herdr-focus-flash-e2e.test.sh +409 -0
- package/.agents/tests/fm-backend-herdr-launcher-workspace-e2e.test.sh +437 -0
- package/.agents/tests/fm-backend-herdr-presentation-e2e.test.sh +1424 -0
- package/.agents/tests/fm-backend-herdr-prune-safety-e2e.test.sh +180 -0
- package/.agents/tests/fm-backend-herdr-respawn-idem-e2e.test.sh +182 -0
- package/.agents/tests/fm-backend-herdr-smoke.test.sh +355 -0
- package/.agents/tests/fm-backend-herdr-workspace-per-home-e2e.test.sh +251 -0
- package/.agents/tests/fm-backend-herdr.test.sh +4606 -0
- package/.agents/tests/fm-backend-orca.test.sh +1357 -0
- package/.agents/tests/fm-backend-tmux-smoke.test.sh +173 -0
- package/.agents/tests/fm-backend-zellij-smoke.test.sh +205 -0
- package/.agents/tests/fm-backend-zellij.test.sh +1360 -0
- package/.agents/tests/fm-backend.test.sh +1142 -0
- package/.agents/tests/fm-backlog-atomicity.test.sh +2302 -0
- package/.agents/tests/fm-backlog-handoff.test.sh +1360 -0
- package/.agents/tests/fm-bearings-board-render.test.sh +135 -0
- package/.agents/tests/fm-bearings-board.test.sh +414 -0
- package/.agents/tests/fm-bearings-snapshot.test.sh +1990 -0
- package/.agents/tests/fm-bootstrap-network-parallel.test.sh +316 -0
- package/.agents/tests/fm-bootstrap.test.sh +1178 -0
- package/.agents/tests/fm-branch-supervision.test.sh +552 -0
- package/.agents/tests/fm-brief.test.sh +785 -0
- package/.agents/tests/fm-busy-adapter-wiring.test.sh +324 -0
- package/.agents/tests/fm-busy-state.test.sh +465 -0
- package/.agents/tests/fm-calm-pi-extension.test.sh +3979 -0
- package/.agents/tests/fm-captain-hold-lifecycle.test.sh +1187 -0
- package/.agents/tests/fm-cd-pretool-check.test.sh +400 -0
- package/.agents/tests/fm-check-unregister.test.sh +198 -0
- package/.agents/tests/fm-classify-corr-token.test.sh +543 -0
- package/.agents/tests/fm-classify-decision-key.test.sh +340 -0
- package/.agents/tests/fm-claude-stop-autoarm-live-e2e.test.sh +164 -0
- package/.agents/tests/fm-claude-stop-autoarm.test.sh +1190 -0
- package/.agents/tests/fm-cmux-claude-composer-live-e2e.test.sh +110 -0
- package/.agents/tests/fm-codex-continuity-live-e2e.test.sh +55 -0
- package/.agents/tests/fm-composer-ghost.test.sh +716 -0
- package/.agents/tests/fm-composer-lib.test.sh +666 -0
- package/.agents/tests/fm-composer-matrix-live-e2e.test.sh +220 -0
- package/.agents/tests/fm-control-herdr-smoke.test.sh +149 -0
- package/.agents/tests/fm-control-relaunch.test.sh +1530 -0
- package/.agents/tests/fm-control.test.sh +910 -0
- package/.agents/tests/fm-crew-state.test.sh +1610 -0
- package/.agents/tests/fm-cursor-harness.test.sh +404 -0
- package/.agents/tests/fm-cursor-primary-live-e2e.test.sh +217 -0
- package/.agents/tests/fm-cursor-primary.test.sh +708 -0
- package/.agents/tests/fm-daemon.test.sh +2740 -0
- package/.agents/tests/fm-documentation-audiences.test.sh +141 -0
- package/.agents/tests/fm-ensure-agents-md.test.sh +371 -0
- package/.agents/tests/fm-extension-binding.test.sh +2187 -0
- package/.agents/tests/fm-fleet-snapshot-view.test.sh +816 -0
- package/.agents/tests/fm-fleet-sync.test.sh +721 -0
- package/.agents/tests/fm-gate-refuse.test.sh +356 -0
- package/.agents/tests/fm-gitignore-config.test.sh +89 -0
- package/.agents/tests/fm-gotmp.test.sh +230 -0
- package/.agents/tests/fm-grok-continuity-live-e2e.test.sh +112 -0
- package/.agents/tests/fm-grok-harness.test.sh +117 -0
- package/.agents/tests/fm-grok-stop-live-e2e.test.sh +219 -0
- package/.agents/tests/fm-guard-stale-banner.test.sh +709 -0
- package/.agents/tests/fm-harness-adapter-instructions-live-e2e.test.sh +134 -0
- package/.agents/tests/fm-harness-adapter-references.test.sh +30 -0
- package/.agents/tests/fm-harness-liveness-drift-live-e2e.test.sh +151 -0
- package/.agents/tests/fm-herdr-lab.test.sh +243 -0
- package/.agents/tests/fm-herdr-session-cleanup-e2e.test.sh +146 -0
- package/.agents/tests/fm-herdr-session-cleanup.test.sh +329 -0
- package/.agents/tests/fm-herdr-submit-confirm-live-e2e.test.sh +125 -0
- package/.agents/tests/fm-herdr-version-floor-live-e2e.test.sh +144 -0
- package/.agents/tests/fm-home-summary-refresh.test.sh +971 -0
- package/.agents/tests/fm-inactive-reconcile.test.sh +485 -0
- package/.agents/tests/fm-kimi-harness.test.sh +687 -0
- package/.agents/tests/fm-lint-workflows.test.sh +532 -0
- package/.agents/tests/fm-lint.test.sh +1023 -0
- package/.agents/tests/fm-muse-harness.test.sh +932 -0
- package/.agents/tests/fm-muse-signals-live-e2e.test.sh +205 -0
- package/.agents/tests/fm-no-mistakes-required.test.sh +72 -0
- package/.agents/tests/fm-on.test.sh +507 -0
- package/.agents/tests/fm-opencode-primary-live-e2e.test.sh +357 -0
- package/.agents/tests/fm-operational-input.test.sh +160 -0
- package/.agents/tests/fm-peek-remote.test.sh +110 -0
- package/.agents/tests/fm-pending-reply.test.sh +1288 -0
- package/.agents/tests/fm-pi-branch-extension.test.sh +3182 -0
- package/.agents/tests/fm-pi-branch-live-e2e.test.sh +527 -0
- package/.agents/tests/fm-pi-primary-live-e2e.test.sh +345 -0
- package/.agents/tests/fm-pi-primary-types.test.sh +68 -0
- package/.agents/tests/fm-pi-watch-extension.test.sh +2843 -0
- package/.agents/tests/fm-pr-check-security.test.sh +2140 -0
- package/.agents/tests/fm-pr-merge.test.sh +2135 -0
- package/.agents/tests/fm-procevent-quota.test.sh +225 -0
- package/.agents/tests/fm-procevent-when.test.sh +406 -0
- package/.agents/tests/fm-procevent.test.sh +1745 -0
- package/.agents/tests/fm-project-origin.test.sh +110 -0
- package/.agents/tests/fm-public-followup.test.sh +2272 -0
- package/.agents/tests/fm-quota-array-dispatch-live-e2e.test.sh +526 -0
- package/.agents/tests/fm-quota-choose.test.sh +626 -0
- package/.agents/tests/fm-remote-backlog-handoff.test.sh +453 -0
- package/.agents/tests/fm-remote-doctor.test.sh +625 -0
- package/.agents/tests/fm-remote-entrypoint.test.sh +59 -0
- package/.agents/tests/fm-remote-job-orphan-reap.test.sh +205 -0
- package/.agents/tests/fm-remote-job.test.sh +768 -0
- package/.agents/tests/fm-remote-reply.test.sh +520 -0
- package/.agents/tests/fm-remote-secondmate-lifecycle-e2e.test.sh +1256 -0
- package/.agents/tests/fm-remote-secondmate-parent-binding.test.sh +326 -0
- package/.agents/tests/fm-remote-secondmate-trace-context.test.sh +310 -0
- package/.agents/tests/fm-remote-transport-lanes.test.sh +431 -0
- package/.agents/tests/fm-review-diff.test.sh +176 -0
- package/.agents/tests/fm-secondmate-harness.test.sh +2600 -0
- package/.agents/tests/fm-secondmate-lifecycle-e2e.test.sh +259 -0
- package/.agents/tests/fm-secondmate-liveness.test.sh +559 -0
- package/.agents/tests/fm-secondmate-reconcile.test.sh +732 -0
- package/.agents/tests/fm-secondmate-safety.test.sh +2972 -0
- package/.agents/tests/fm-secondmate-sync.test.sh +913 -0
- package/.agents/tests/fm-send-inbox-doorbell-live-e2e.test.sh +206 -0
- package/.agents/tests/fm-send-inbox.test.sh +352 -0
- package/.agents/tests/fm-send-popup-settle.test.sh +167 -0
- package/.agents/tests/fm-send-remote-delivery.test.sh +796 -0
- package/.agents/tests/fm-send-resolve-key.test.sh +551 -0
- package/.agents/tests/fm-send-secondmate-marker-herdr-e2e.test.sh +187 -0
- package/.agents/tests/fm-send-secondmate-marker.test.sh +278 -0
- package/.agents/tests/fm-send-settle.test.sh +147 -0
- package/.agents/tests/fm-send-strict.test.sh +241 -0
- package/.agents/tests/fm-session-lock-ancestry.test.sh +365 -0
- package/.agents/tests/fm-session-start.test.sh +2515 -0
- package/.agents/tests/fm-sessionstart-hook-live-e2e.test.sh +631 -0
- package/.agents/tests/fm-sessionstart-instruction-refresh-live-e2e.test.sh +230 -0
- package/.agents/tests/fm-sessionstart-nudge.test.sh +1039 -0
- package/.agents/tests/fm-shared-captain-inheritance.test.sh +404 -0
- package/.agents/tests/fm-spawn-batch.test.sh +148 -0
- package/.agents/tests/fm-spawn-dispatch-profile.test.sh +827 -0
- package/.agents/tests/fm-spawn-pool-base-freshen.test.sh +440 -0
- package/.agents/tests/fm-spawn-worktree-settle.test.sh +147 -0
- package/.agents/tests/fm-startup-memory-budget.test.sh +333 -0
- package/.agents/tests/fm-startup-network.test.sh +712 -0
- package/.agents/tests/fm-stow-cascade.test.sh +370 -0
- package/.agents/tests/fm-subagent-pretool-check.test.sh +291 -0
- package/.agents/tests/fm-supervision-events.test.sh +156 -0
- package/.agents/tests/fm-supervision-instructions.test.sh +189 -0
- package/.agents/tests/fm-tangle-guard.test.sh +277 -0
- package/.agents/tests/fm-task-delivery.test.sh +420 -0
- package/.agents/tests/fm-task-inbox.test.sh +513 -0
- package/.agents/tests/fm-teardown-endpoint-safety.test.sh +374 -0
- package/.agents/tests/fm-teardown.test.sh +2665 -0
- package/.agents/tests/fm-test-fixture-cleanup.test.sh +172 -0
- package/.agents/tests/fm-test-fixtures.test.sh +132 -0
- package/.agents/tests/fm-test-isolation-proof.test.sh +278 -0
- package/.agents/tests/fm-test-run.test.sh +1196 -0
- package/.agents/tests/fm-tmux-agent-liveness.test.sh +354 -0
- package/.agents/tests/fm-tmux-submit-busy.test.sh +340 -0
- package/.agents/tests/fm-tool-update-check.test.sh +1044 -0
- package/.agents/tests/fm-trace-context-lib.test.sh +253 -0
- package/.agents/tests/fm-trace-context-spawn.test.sh +581 -0
- package/.agents/tests/fm-transition-lib.test.sh +54 -0
- package/.agents/tests/fm-turnend-guard.test.sh +1822 -0
- package/.agents/tests/fm-update.test.sh +304 -0
- package/.agents/tests/fm-vendor-auth-probe.test.sh +395 -0
- package/.agents/tests/fm-voice-relay.test.sh +4741 -0
- package/.agents/tests/fm-wake-daemon-lifecycle-e2e.test.sh +171 -0
- package/.agents/tests/fm-wake-drain-open-decisions-cursor.test.sh +356 -0
- package/.agents/tests/fm-wake-drain-open-decisions.test.sh +226 -0
- package/.agents/tests/fm-wake-drain-unread-status.test.sh +379 -0
- package/.agents/tests/fm-wake-queue.test.sh +1224 -0
- package/.agents/tests/fm-watch-arm.test.sh +821 -0
- package/.agents/tests/fm-watch-checkpoint.test.sh +87 -0
- package/.agents/tests/fm-watch-recovery-loop.test.sh +223 -0
- package/.agents/tests/fm-watch-triage.test.sh +3889 -0
- package/.agents/tests/fm-watcher-lock.test.sh +1133 -0
- package/.agents/tests/fm-x-mode.test.sh +3042 -0
- package/.agents/tests/gleipnir-guard-pretool-check.test.sh +51 -0
- package/.agents/tests/gleipnir-machine-lock.test.sh +130 -0
- package/.agents/tests/herdr-test-safety.sh +42 -0
- package/.agents/tests/lib.sh +315 -0
- package/.agents/tests/remote-herdr-fixture.sh +125 -0
- package/.agents/tests/secondmate-helpers.sh +193 -0
- package/.agents/tests/smoke.test.sh +93 -0
- package/.agents/tests/wake-helpers.sh +338 -0
- package/.agents/tests/zellij-test-safety.sh +60 -0
- package/.agents/tools/README.md +10 -0
- package/.agents/tools/bin/tasks-cli +267 -0
- package/.agents/tools/tasks-cli.ts +216 -0
- package/AGENTS.md +454 -0
- package/CHANGELOG.md +881 -0
- package/LICENSE +1 -1
- package/NOTICE +63 -0
- package/README.md +113 -39
- package/RULES/01-domains.md +51 -0
- package/RULES/02-agents.md +78 -0
- package/RULES/03-houses.md +61 -0
- package/RULES/04-hoard.md +65 -0
- package/RULES/05-platforms.md +67 -0
- package/RULES/06-append-only.md +58 -0
- package/RULES/07-config.md +45 -0
- package/RULES/08-delivery-gate.md +75 -0
- package/RULES/README.md +20 -0
- package/bin/README.md +107 -0
- package/bin/a2a-mcp.sh +98 -0
- package/bin/a2a-serve.py +103 -0
- package/bin/a2a-serve.sh +8 -0
- package/bin/a2a-talk.sh +50 -0
- package/bin/a2abridge-ensure.sh +108 -0
- package/bin/agent-run.sh +43 -0
- package/bin/agents-config.sh +233 -0
- package/bin/apodex-smoke-test.sh +92 -0
- package/bin/bifrost-bridge.sh +112 -0
- package/bin/bifrost-ingress.sh +77 -0
- package/bin/bootstrap-macos.sh +107 -0
- package/bin/bootstrap-windows.ps1 +112 -0
- package/bin/branch-guard.sh +64 -0
- package/bin/brokk +73 -0
- package/bin/brokk-classify-lib.sh +1770 -0
- package/bin/brokk-lease-lib.sh +222 -0
- package/bin/brokk-lease.sh +193 -0
- package/bin/brokk-lint.sh +65 -0
- package/bin/brokk-send.sh +38 -0
- package/bin/brokk-update.sh +9 -0
- package/bin/brokk-wake-grant.sh +127 -0
- package/bin/brokk-wake-lib.sh +1849 -0
- package/bin/changelog-guard.sh +165 -0
- package/bin/crash-sense.sh +160 -0
- package/bin/desktop-place.sh +224 -0
- package/bin/docs-guard.sh +39 -0
- package/bin/editor-place.sh +207 -0
- package/bin/eindri-acclaim.sh +34 -0
- package/bin/eindri-control.sh +28 -0
- package/bin/eindri-role.sh +83 -0
- package/bin/eindri-seen.sh +41 -0
- package/bin/eindri-send.sh +15 -0
- package/bin/eindri-start.sh +85 -0
- package/bin/eindri-watch.sh +57 -0
- package/bin/einherjar-spawn.sh +576 -0
- package/bin/eir-doctor.sh +112 -0
- package/bin/erindi-brief.sh +271 -0
- package/bin/fleet-apply.sh +68 -0
- package/bin/github-deploy.sh +81 -0
- package/bin/gjallarhorn-expose.sh +102 -0
- package/bin/gjallarhorn-notify.sh +58 -0
- package/bin/gjallarhorn-purge.sh +54 -0
- package/bin/gjallarhorn-tunnel.sh +72 -0
- package/bin/gleipnir-lock-lib.sh +284 -0
- package/bin/groa-update.sh +122 -0
- package/bin/hall-snapshot.sh +92 -0
- package/bin/hamr-harness.sh +246 -0
- package/bin/heimdall-ssh-keys.sh +374 -0
- package/bin/herdr-agents.py +44 -0
- package/bin/herdr-ensure.sh +104 -0
- package/bin/herdr-run.sh +431 -0
- package/bin/hermes-ensure.sh +88 -0
- package/bin/hoard-lib.sh +25 -0
- package/bin/hodd.sh +78 -0
- package/bin/host-sense.sh +199 -0
- package/bin/huginn-research-worker.sh +108 -0
- package/bin/local-model-lock.sh +51 -0
- package/bin/mimir-bridge.py +226 -0
- package/bin/mimir-bridge.sh +87 -0
- package/bin/mimir-ingest.sh +131 -0
- package/bin/mimir.sh +113 -0
- package/bin/mjollnir-webhook.sh +59 -0
- package/bin/mjollnir.sh +85 -0
- package/bin/model-hardware.sh +82 -0
- package/bin/model-resolve.sh +147 -0
- package/bin/models-detect.sh +178 -0
- package/bin/nornir-cron-start.sh +162 -0
- package/bin/nornir-job-daily-briefing.sh +191 -0
- package/bin/nornir-job-git-sync.sh +152 -0
- package/bin/nornir-job-memory-housekeeping.sh +114 -0
- package/bin/nornir-job-observer.sh +166 -0
- package/bin/nornir-job-skillopt-sleep.sh +76 -0
- package/bin/omarchy-hook-install.sh +60 -0
- package/bin/omarchy-install.sh +148 -0
- package/bin/omarchy-plugins.sh +174 -0
- package/bin/omarchy-sense.sh +149 -0
- package/bin/perm-guard.sh +30 -0
- package/bin/pi-agent.sh +68 -0
- package/bin/pi-ensure.sh +77 -0
- package/bin/pi-local.sh +37 -0
- package/bin/pi-seat.sh +63 -0
- package/bin/prereq-ensure.sh +166 -0
- package/bin/private-guard.sh +38 -0
- package/bin/project-git.sh +55 -0
- package/bin/public-guard.sh +70 -0
- package/bin/ratatoskr.sh +49 -0
- package/bin/realm-lib.sh +32 -0
- package/bin/repo-scrub.sh +62 -0
- package/bin/rodd-operational-input.sh +250 -0
- package/bin/runes-append.sh +198 -0
- package/bin/saga-session-start.sh +172 -0
- package/bin/saga-sessionstart-run.sh +79 -0
- package/bin/saga-wake-drain.sh +41 -0
- package/bin/secret-guard.sh +63 -0
- package/bin/sessrumnir-ensure.sh +100 -0
- package/bin/sessrumnir-sync.sh +237 -0
- package/bin/sessrumnir.sh +91 -0
- package/bin/skillopt-setup.sh +76 -0
- package/bin/skuld-branch-outcome.sh +203 -0
- package/bin/skuld-branch-prompt.sh +101 -0
- package/bin/smidja-bootstrap.sh +90 -0
- package/bin/smidja-observe.sh +86 -0
- package/bin/syn-arm-pretool-check.sh +57 -0
- package/bin/syn-asset-pretool-check.sh +76 -0
- package/bin/syn-cd-pretool-check.sh +28 -0
- package/bin/syn-guard-pretool-check.sh +83 -0
- package/bin/syn-turnend-guard.sh +36 -0
- package/bin/syn-watch-arm.sh +99 -0
- package/bin/tailscale-sync.sh +190 -0
- package/bin/telegram-bot.sh +69 -0
- package/bin/toolchain.sh +74 -0
- package/bin/utgard.sh +111 -0
- package/bin/valhalla.sh +115 -0
- package/bin/valknut-load.sh +260 -0
- package/bin/vor-crew-state.sh +124 -0
- package/bin/wedge-notify.sh +47 -0
- package/bin/workspace-provision.sh +80 -0
- package/bin/workspace-rag.sh +150 -0
- package/bin/wyrd-db.sh +47 -0
- package/bin/yggdrasil.sh +152 -0
- package/bin/ymir-edit.sh +145 -0
- package/bin/ymir-install.sh +641 -0
- package/bin/ymir-invite.sh +93 -0
- package/bin/ymir-isolation.sh +88 -0
- package/bin/ymir-migrate.sh +61 -0
- package/bin/ymir-platform.sh +348 -0
- package/bin/ymir-say.sh +97 -0
- package/bin/ymir-setup-auth.sh +108 -0
- package/bin/ymir-validate.sh +173 -0
- package/bin/ymir.js +74 -16
- package/docs/Architecture.md +339 -0
- package/docs/design.md +351 -0
- package/docs/installations/README.md +63 -0
- package/docs/installations/ymir-home.md +93 -0
- package/docs/integration.md +105 -0
- package/docs/lore.md +1056 -0
- package/docs/research/agent-harnesses.md +290 -0
- package/docs/runbooks/README.md +19 -0
- package/docs/runbooks/a2a.md +91 -0
- package/docs/runbooks/agent-permissions.md +99 -0
- package/docs/runbooks/agents.md +57 -0
- package/docs/runbooks/models.md +69 -0
- package/docs/runbooks/realm-onboarding.md +60 -0
- package/docs/runbooks/secrets-and-hoard.md +60 -0
- package/docs/runbooks/updates-and-migrations.md +110 -0
- package/docs/runbooks/worktrees-isolation.md +72 -0
- package/docs/session-start.md +105 -0
- package/docs/skillopt-integration.md +321 -0
- package/docs/sync-system.md +66 -0
- package/docs/workspaces.md +104 -0
- package/docs/ymir-rut.md +260 -0
- package/package.json +44 -14
- package/packaging/build.sh +74 -0
- package/packaging/macos/Ymir Installer.command +43 -0
- package/packaging/windows/ymir-setup.nsi +74 -0
- package/scripts/README.md +22 -0
- package/scripts/electron.sh +210 -0
- package/scripts/start.sh +167 -0
- package/scripts/stop.sh +113 -0
- package/install.sh +0 -35
|
@@ -0,0 +1,1190 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Behavior tests for the Claude Stop-owned watcher auto-arm
|
|
3
|
+
# (bin/fm-claude-stop-autoarm.sh, docs/watcher-continuity.md).
|
|
4
|
+
#
|
|
5
|
+
# The hook fires as a Claude asyncRewake Stop hook. These tests run it hermetically
|
|
6
|
+
# as a child of a fake harness (a bash symlink named "claude") whose pid is
|
|
7
|
+
# written into the fixture home's state/.lock for ordinary owned-lock cases.
|
|
8
|
+
# Stale-owner cases instead leave a dead recorded pid for the hook to reclaim
|
|
9
|
+
# through the real fm-lock.sh path. The arm wrapper is a per-test fixture, so no
|
|
10
|
+
# real watcher, model, or fleet state is touched.
|
|
11
|
+
# shellcheck disable=SC2016 # single quotes are deliberate: $FM_HOME expands inside the fake harness child, and grep needles are literal strings
|
|
12
|
+
set -u
|
|
13
|
+
|
|
14
|
+
# shellcheck source=tests/lib.sh
|
|
15
|
+
. "$(dirname "${BASH_SOURCE[0]}")/lib.sh"
|
|
16
|
+
|
|
17
|
+
TMP_ROOT=$(fm_test_tmproot fm-claude-stop-autoarm)
|
|
18
|
+
fm_git_identity fmtest fmtest@example.invalid
|
|
19
|
+
|
|
20
|
+
FAKEBIN=$(fm_fakebin "$TMP_ROOT/fakebin")
|
|
21
|
+
ln -s /bin/bash "$FAKEBIN/claude"
|
|
22
|
+
FAKE_CLAUDE="$FAKEBIN/claude"
|
|
23
|
+
export FAKE_CLAUDE
|
|
24
|
+
|
|
25
|
+
# Copy the hook and its sourced dependencies into a fixture checkout.
|
|
26
|
+
install_autoarm_scripts() {
|
|
27
|
+
local dir=$1
|
|
28
|
+
mkdir -p "$dir/bin"
|
|
29
|
+
cp "$ROOT/bin/fm-claude-stop-autoarm.sh" "$dir/bin/fm-claude-stop-autoarm.sh"
|
|
30
|
+
cp "$ROOT/bin/fm-primary-scope-lib.sh" "$dir/bin/fm-primary-scope-lib.sh"
|
|
31
|
+
cp "$ROOT/bin/fm-supervision-lib.sh" "$dir/bin/fm-supervision-lib.sh"
|
|
32
|
+
cp "$ROOT/bin/fm-wake-lib.sh" "$dir/bin/fm-wake-lib.sh"
|
|
33
|
+
cp "$ROOT/bin/fm-session-lock-lib.sh" "$dir/bin/fm-session-lock-lib.sh"
|
|
34
|
+
cp "$ROOT/bin/fm-cursor-lib.sh" "$dir/bin/fm-cursor-lib.sh"
|
|
35
|
+
cp "$ROOT/bin/fm-hook-host-lib.sh" "$dir/bin/fm-hook-host-lib.sh"
|
|
36
|
+
cp "$ROOT/bin/fm-lock.sh" "$dir/bin/fm-lock.sh"
|
|
37
|
+
chmod +x "$dir/bin/fm-claude-stop-autoarm.sh" "$dir/bin/fm-lock.sh"
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
make_primary_dir() {
|
|
41
|
+
local dir=$1
|
|
42
|
+
mkdir -p "$dir/state"
|
|
43
|
+
git init -q "$dir"
|
|
44
|
+
git -C "$dir" commit -q --allow-empty -m init
|
|
45
|
+
: > "$dir/AGENTS.md"
|
|
46
|
+
install_autoarm_scripts "$dir"
|
|
47
|
+
printf '%s\n' "$dir"
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
make_secondmate_dir() {
|
|
51
|
+
local dir=$1
|
|
52
|
+
make_primary_dir "$dir" >/dev/null
|
|
53
|
+
printf 'sm-autoarm-1\n' > "$dir/.fm-secondmate-home"
|
|
54
|
+
printf '%s\n' "$dir"
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
# A genuine linked git worktree: the shape every crewmate/scout task worktree
|
|
58
|
+
# has (git-dir != git-common-dir), which must keep the hook inert.
|
|
59
|
+
make_crewmate_worktree_dir() {
|
|
60
|
+
local base=$1 dir=$2
|
|
61
|
+
fm_git_worktree "$base" "$dir" fm/autoarm-test-branch
|
|
62
|
+
mkdir -p "$dir/state"
|
|
63
|
+
: > "$dir/AGENTS.md"
|
|
64
|
+
install_autoarm_scripts "$dir"
|
|
65
|
+
printf '%s\n' "$dir"
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
# Run the hook as a child of the fake harness holding the fixture home's
|
|
69
|
+
# session lock. $1 = fixture dir. Any extra env assignments must be exported
|
|
70
|
+
# before invocation. Captures stdout+stderr; exit code on stdout of the caller.
|
|
71
|
+
run_autoarm() {
|
|
72
|
+
local dir=$1 rc=0
|
|
73
|
+
printf '%s\n' '{"session_id":"sess-autoarm","stop_hook_active":false}' \
|
|
74
|
+
| FM_HOME="$dir" "$FAKE_CLAUDE" -c '
|
|
75
|
+
printf "%s\n" "$$" > "$FM_HOME/state/.lock"
|
|
76
|
+
"$FM_HOME/bin/fm-claude-stop-autoarm.sh"
|
|
77
|
+
' 2>&1 || rc=$?
|
|
78
|
+
printf 'RC=%s\n' "$rc" >&2
|
|
79
|
+
return "$rc"
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
# Arm fixture variants, installed per test as <dir>/bin/fm-watch-arm.sh.
|
|
83
|
+
write_arm_fixture() {
|
|
84
|
+
local dir=$1 kind=$2
|
|
85
|
+
case "$kind" in
|
|
86
|
+
actionable)
|
|
87
|
+
cat > "$dir/bin/fm-watch-arm.sh" <<'SH'
|
|
88
|
+
#!/usr/bin/env bash
|
|
89
|
+
echo "$$" >> "$FM_HOME/state/arm-ran"
|
|
90
|
+
printf 'watcher: started pid=%s (beacon fresh)\n' "$$"
|
|
91
|
+
printf 'stale: fixture-win actionable\n'
|
|
92
|
+
exit 0
|
|
93
|
+
SH
|
|
94
|
+
;;
|
|
95
|
+
failed)
|
|
96
|
+
cat > "$dir/bin/fm-watch-arm.sh" <<'SH'
|
|
97
|
+
#!/usr/bin/env bash
|
|
98
|
+
echo "$$" >> "$FM_HOME/state/arm-ran"
|
|
99
|
+
printf 'watcher: FAILED - no live watcher with a fresh beacon\n'
|
|
100
|
+
exit 1
|
|
101
|
+
SH
|
|
102
|
+
;;
|
|
103
|
+
clean)
|
|
104
|
+
cat > "$dir/bin/fm-watch-arm.sh" <<'SH'
|
|
105
|
+
#!/usr/bin/env bash
|
|
106
|
+
echo "$$" >> "$FM_HOME/state/arm-ran"
|
|
107
|
+
printf 'watcher: attached pid=%s (beacon 2s)\n' "$$"
|
|
108
|
+
exit 0
|
|
109
|
+
SH
|
|
110
|
+
;;
|
|
111
|
+
benign-live)
|
|
112
|
+
cat > "$dir/bin/fm-watch-arm.sh" <<'SH'
|
|
113
|
+
#!/usr/bin/env bash
|
|
114
|
+
echo "$$" >> "$FM_HOME/state/arm-ran"
|
|
115
|
+
printf 'watcher: FAILED - cycle ended without an actionable reason\n'
|
|
116
|
+
exit 1
|
|
117
|
+
SH
|
|
118
|
+
;;
|
|
119
|
+
reset-boundary)
|
|
120
|
+
cat > "$dir/bin/fm-watch-arm.sh" <<'SH'
|
|
121
|
+
#!/usr/bin/env bash
|
|
122
|
+
echo "$$" >> "$FM_HOME/state/arm-ran"
|
|
123
|
+
: > "$FM_HOME/state/arm-waiting"
|
|
124
|
+
while [ ! -e "$FM_HOME/state/arm-release" ]; do sleep 0.02; done
|
|
125
|
+
printf 'watcher: FAILED - cycle ended without an actionable reason\n'
|
|
126
|
+
exit 1
|
|
127
|
+
SH
|
|
128
|
+
;;
|
|
129
|
+
slow-actionable)
|
|
130
|
+
cat > "$dir/bin/fm-watch-arm.sh" <<'SH'
|
|
131
|
+
#!/usr/bin/env bash
|
|
132
|
+
echo "$$" >> "$FM_HOME/state/arm-ran"
|
|
133
|
+
sleep 2
|
|
134
|
+
printf 'watcher: started pid=%s (beacon fresh)\n' "$$"
|
|
135
|
+
printf 'signal: task.status done: slow fixture\n'
|
|
136
|
+
exit 0
|
|
137
|
+
SH
|
|
138
|
+
;;
|
|
139
|
+
blocking-actionable)
|
|
140
|
+
cat > "$dir/bin/fm-watch-arm.sh" <<'SH'
|
|
141
|
+
#!/usr/bin/env bash
|
|
142
|
+
echo "$$" >> "$FM_HOME/state/arm-ran"
|
|
143
|
+
sleep 6
|
|
144
|
+
printf 'watcher: started pid=%s (beacon fresh)\n' "$$"
|
|
145
|
+
printf 'stale: fixture-win actionable\n'
|
|
146
|
+
exit 0
|
|
147
|
+
SH
|
|
148
|
+
;;
|
|
149
|
+
supersede-then-fail)
|
|
150
|
+
cat > "$dir/bin/fm-watch-arm.sh" <<'SH'
|
|
151
|
+
#!/usr/bin/env bash
|
|
152
|
+
echo "$$" >> "$FM_HOME/state/arm-ran"
|
|
153
|
+
printf 'epoch=999 owner_pid=1 outcome=arming updated_at=%s\nfixture-superseder-identity\n' "$(date +%s)" \
|
|
154
|
+
> "$FM_HOME/state/.claude-autoarm-epoch"
|
|
155
|
+
printf 'watcher: FAILED - no live watcher with a fresh beacon\n'
|
|
156
|
+
exit 1
|
|
157
|
+
SH
|
|
158
|
+
;;
|
|
159
|
+
meta-vanishes)
|
|
160
|
+
cat > "$dir/bin/fm-watch-arm.sh" <<'SH'
|
|
161
|
+
#!/usr/bin/env bash
|
|
162
|
+
echo "$$" >> "$FM_HOME/state/arm-ran"
|
|
163
|
+
rm -f "$FM_HOME/state/task.meta"
|
|
164
|
+
printf 'watcher: started pid=%s (beacon fresh)\n' "$$"
|
|
165
|
+
printf 'signal: task.status done: fixture\n'
|
|
166
|
+
exit 0
|
|
167
|
+
SH
|
|
168
|
+
;;
|
|
169
|
+
afk-appears)
|
|
170
|
+
cat > "$dir/bin/fm-watch-arm.sh" <<'SH'
|
|
171
|
+
#!/usr/bin/env bash
|
|
172
|
+
echo "$$" >> "$FM_HOME/state/arm-ran"
|
|
173
|
+
: > "$FM_HOME/state/.afk"
|
|
174
|
+
printf 'watcher: started pid=%s (beacon fresh)\n' "$$"
|
|
175
|
+
printf 'stale: fixture-win actionable\n'
|
|
176
|
+
exit 0
|
|
177
|
+
SH
|
|
178
|
+
;;
|
|
179
|
+
*)
|
|
180
|
+
echo "unknown arm fixture: $kind" >&2
|
|
181
|
+
return 2
|
|
182
|
+
;;
|
|
183
|
+
esac
|
|
184
|
+
chmod +x "$dir/bin/fm-watch-arm.sh"
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
epoch_outcome() {
|
|
188
|
+
sed -n '1s/^.*outcome=\([a-z][a-z-]*\) .*$/\1/p' "$1/state/.claude-autoarm-epoch" 2>/dev/null || true
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
# Run the hook in the background under the fake harness, output captured to a
|
|
192
|
+
# file. Sets RUN_AUTOARM_BG_PID (a direct child of the calling shell, so the
|
|
193
|
+
# caller can `wait` on it for the hook's exit status).
|
|
194
|
+
RUN_AUTOARM_BG_PID=
|
|
195
|
+
run_autoarm_bg() {
|
|
196
|
+
local dir=$1 out=$2
|
|
197
|
+
printf '%s\n' '{"session_id":"sess-autoarm","stop_hook_active":false}' \
|
|
198
|
+
| FM_HOME="$dir" "$FAKE_CLAUDE" -c '
|
|
199
|
+
printf "%s\n" "$$" > "$FM_HOME/state/.lock"
|
|
200
|
+
"$FM_HOME/bin/fm-claude-stop-autoarm.sh"
|
|
201
|
+
' > "$out" 2>&1 &
|
|
202
|
+
RUN_AUTOARM_BG_PID=$!
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
watcher_identity() {
|
|
206
|
+
local dir=$1 pid=$2
|
|
207
|
+
FM_STATE_OVERRIDE="$dir/state" bash -c '. "$1"; fm_pid_identity "$2"' _ "$dir/bin/fm-wake-lib.sh" "$pid"
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
record_watcher_lock() {
|
|
211
|
+
local dir=$1 pid=$2 identity=$3 root bin_dir
|
|
212
|
+
root=$dir
|
|
213
|
+
bin_dir=$(cd "$dir/bin" && pwd)
|
|
214
|
+
mkdir -p "$dir/state/.watch.lock"
|
|
215
|
+
printf '%s\n' "$pid" > "$dir/state/.watch.lock/pid"
|
|
216
|
+
printf '%s\n' "$root" > "$dir/state/.watch.lock/fm-home"
|
|
217
|
+
printf '%s\n' "$bin_dir/fm-watch.sh" > "$dir/state/.watch.lock/watcher-path"
|
|
218
|
+
printf '%s\n' "$identity" > "$dir/state/.watch.lock/pid-identity"
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
# --- registration contract ----------------------------------------------------
|
|
222
|
+
|
|
223
|
+
# --- scope and gates ----------------------------------------------------------
|
|
224
|
+
|
|
225
|
+
test_inert_in_child_worktree() {
|
|
226
|
+
local base dir out status
|
|
227
|
+
base="$TMP_ROOT/crew-base"
|
|
228
|
+
dir="$TMP_ROOT/crew-wt"
|
|
229
|
+
make_crewmate_worktree_dir "$base" "$dir" >/dev/null
|
|
230
|
+
: > "$dir/state/task.meta"
|
|
231
|
+
write_arm_fixture "$dir" actionable
|
|
232
|
+
out=$(run_autoarm "$dir" 2>/dev/null); status=$?
|
|
233
|
+
expect_code 0 "$status" "hook must stay inert in a child task worktree"
|
|
234
|
+
[ ! -e "$dir/state/arm-ran" ] || fail "hook armed inside a child worktree"
|
|
235
|
+
[ ! -e "$dir/state/.claude-autoarm-epoch" ] || fail "hook wrote an epoch inside a child worktree"
|
|
236
|
+
pass "auto-arm: inert in a linked child worktree even when in-flight"
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
test_inert_without_session_lock() {
|
|
240
|
+
local dir out status
|
|
241
|
+
dir=$(make_primary_dir "$TMP_ROOT/no-lock")
|
|
242
|
+
: > "$dir/state/task.meta"
|
|
243
|
+
write_arm_fixture "$dir" actionable
|
|
244
|
+
# No state/.lock: run the hook directly (no fake harness, no lock file).
|
|
245
|
+
out=$(printf '%s\n' '{"session_id":"s"}' | FM_HOME="$dir" bash "$dir/bin/fm-claude-stop-autoarm.sh" 2>&1); status=$?
|
|
246
|
+
expect_code 0 "$status" "hook must stay inert when no session holds the home lock"
|
|
247
|
+
[ ! -e "$dir/state/arm-ran" ] || fail "hook armed without a session lock"
|
|
248
|
+
pass "auto-arm: inert with no session lock"
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
test_reclaims_stale_session_lock_before_arming() {
|
|
252
|
+
local dir out status expected_owner actual_owner
|
|
253
|
+
dir=$(make_primary_dir "$TMP_ROOT/stale-lock")
|
|
254
|
+
: > "$dir/state/task.meta"
|
|
255
|
+
printf '9999999\n' > "$dir/state/.lock"
|
|
256
|
+
write_arm_fixture "$dir" actionable
|
|
257
|
+
out=$(printf '%s\n' '{"session_id":"stale"}' \
|
|
258
|
+
| FM_HOME="$dir" "$FAKE_CLAUDE" -c '
|
|
259
|
+
printf "%s\n" "$$" > "$FM_HOME/state/expected-owner"
|
|
260
|
+
"$FM_HOME/bin/fm-claude-stop-autoarm.sh"
|
|
261
|
+
' 2>&1); status=$?
|
|
262
|
+
expect_code 2 "$status" "a dead recorded session owner must be reclaimed before the actionable rewake"
|
|
263
|
+
expected_owner=$(cat "$dir/state/expected-owner")
|
|
264
|
+
actual_owner=$(cat "$dir/state/.lock")
|
|
265
|
+
[ "$actual_owner" = "$expected_owner" ] || fail "stale session lock was not claimed by the current harness: expected $expected_owner, got $actual_owner"
|
|
266
|
+
[ -e "$dir/state/arm-ran" ] || fail "hook did not arm after reclaiming the stale session lock"
|
|
267
|
+
[ "$(epoch_outcome "$dir")" = rewake ] || fail "stale-lock recovery must record outcome=rewake"
|
|
268
|
+
pass "auto-arm: a demonstrably dead recorded session owner is reclaimed through fm-lock.sh before arming"
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
test_inert_when_lock_held_by_other_harness() {
|
|
272
|
+
local dir other out status owner_after
|
|
273
|
+
dir=$(make_primary_dir "$TMP_ROOT/other-lock")
|
|
274
|
+
: > "$dir/state/task.meta"
|
|
275
|
+
write_arm_fixture "$dir" actionable
|
|
276
|
+
# The trailing no-op keeps the fake harness process alive instead of allowing
|
|
277
|
+
# bash to exec the final sleep into a non-harness process.
|
|
278
|
+
"$FAKE_CLAUDE" -c 'sleep 60; :' &
|
|
279
|
+
other=$!
|
|
280
|
+
printf '%s\n' "$other" > "$dir/state/.lock"
|
|
281
|
+
out=$(printf '%s\n' '{"session_id":"s"}' | FM_HOME="$dir" "$FAKE_CLAUDE" -c '"$FM_HOME/bin/fm-claude-stop-autoarm.sh"' 2>&1); status=$?
|
|
282
|
+
owner_after=$(cat "$dir/state/.lock")
|
|
283
|
+
kill "$other" 2>/dev/null || true
|
|
284
|
+
wait "$other" 2>/dev/null || true
|
|
285
|
+
expect_code 0 "$status" "hook must stay inert when another live harness holds the session lock"
|
|
286
|
+
[ "$owner_after" = "$other" ] || fail "hook replaced another live harness owner: expected $other, got $owner_after"
|
|
287
|
+
[ ! -e "$dir/state/arm-ran" ] || fail "hook armed while another session owned the lock"
|
|
288
|
+
[ ! -e "$dir/state/.claude-autoarm-epoch" ] || fail "hook wrote an epoch while another session owned the lock"
|
|
289
|
+
pass "auto-arm: inert without arm, rewake, or lock replacement when another live harness owns the home"
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
test_inert_when_afk() {
|
|
293
|
+
local dir out status
|
|
294
|
+
dir=$(make_primary_dir "$TMP_ROOT/afk")
|
|
295
|
+
: > "$dir/state/task.meta"
|
|
296
|
+
: > "$dir/state/.afk"
|
|
297
|
+
: > "$dir/state/.claude-autoarm-failure-notified"
|
|
298
|
+
: > "$dir/state/.claude-autoarm-failure-alarmed"
|
|
299
|
+
write_arm_fixture "$dir" actionable
|
|
300
|
+
out=$(run_autoarm "$dir" 2>/dev/null); status=$?
|
|
301
|
+
expect_code 0 "$status" "hook must never arm or rewake while away mode owns triage"
|
|
302
|
+
[ ! -e "$dir/state/arm-ran" ] || fail "hook armed while state/.afk existed"
|
|
303
|
+
assert_present "$dir/state/.claude-autoarm-failure-notified" "AFK without positive recovery reset the failure notice"
|
|
304
|
+
assert_present "$dir/state/.claude-autoarm-failure-alarmed" "AFK without positive recovery reset the attended alarm"
|
|
305
|
+
pass "auto-arm: inert while AFK owns supervision"
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
test_stale_lock_recovery_preserves_afk_and_need_gates() {
|
|
309
|
+
local afk_dir idle_dir out status
|
|
310
|
+
afk_dir=$(make_primary_dir "$TMP_ROOT/stale-afk")
|
|
311
|
+
: > "$afk_dir/state/task.meta"
|
|
312
|
+
: > "$afk_dir/state/.afk"
|
|
313
|
+
printf '9999999\n' > "$afk_dir/state/.lock"
|
|
314
|
+
write_arm_fixture "$afk_dir" actionable
|
|
315
|
+
out=$(printf '%s\n' '{"session_id":"stale-afk"}' | FM_HOME="$afk_dir" "$FAKE_CLAUDE" -c '"$FM_HOME/bin/fm-claude-stop-autoarm.sh"' 2>&1); status=$?
|
|
316
|
+
expect_code 0 "$status" "a stale owner must not widen the AFK gate"
|
|
317
|
+
[ "$(cat "$afk_dir/state/.lock")" = 9999999 ] || fail "AFK stale lock was reclaimed despite away ownership"
|
|
318
|
+
[ ! -e "$afk_dir/state/arm-ran" ] || fail "stale AFK home armed"
|
|
319
|
+
|
|
320
|
+
idle_dir=$(make_primary_dir "$TMP_ROOT/stale-idle")
|
|
321
|
+
printf '9999999\n' > "$idle_dir/state/.lock"
|
|
322
|
+
write_arm_fixture "$idle_dir" actionable
|
|
323
|
+
out=$(printf '%s\n' '{"session_id":"stale-idle"}' | FM_HOME="$idle_dir" "$FAKE_CLAUDE" -c '"$FM_HOME/bin/fm-claude-stop-autoarm.sh"' 2>&1); status=$?
|
|
324
|
+
expect_code 0 "$status" "a stale owner must not widen the supervision-need gate"
|
|
325
|
+
[ "$(cat "$idle_dir/state/.lock")" = 9999999 ] || fail "idle stale lock was reclaimed without supervision need"
|
|
326
|
+
[ ! -e "$idle_dir/state/arm-ran" ] || fail "stale idle home armed"
|
|
327
|
+
pass "auto-arm: stale-owner recovery leaves the AFK and supervision-need gates unchanged"
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
test_resolves_outermost_claude_pid_in_nested_bgspare_chain() {
|
|
331
|
+
local dir out status inner_pid lock_pid
|
|
332
|
+
dir=$(make_primary_dir "$TMP_ROOT/nested-chain")
|
|
333
|
+
: > "$dir/state/task.meta"
|
|
334
|
+
write_arm_fixture "$dir" actionable
|
|
335
|
+
# A genuine multi-level contiguous claude-named ancestry: the hook fires
|
|
336
|
+
# inside an inner fake-claude process (its recorded pid is distinct from its
|
|
337
|
+
# own parent, a second, outer fake-claude process holding the session lock -
|
|
338
|
+
# the bg-spare shape). Only the outer pid may own the lock; a
|
|
339
|
+
# first-match-wins walk would resolve to the inner pid instead and leave the
|
|
340
|
+
# hook inert. The inner process records its own pid before running the hook
|
|
341
|
+
# so bash cannot tail-exec-collapse it into the outer pid, which would
|
|
342
|
+
# collapse the two-hop chain this test depends on down to one hop.
|
|
343
|
+
out=$(printf '%s\n' '{"session_id":"nested"}' \
|
|
344
|
+
| FM_HOME="$dir" "$FAKE_CLAUDE" -c '
|
|
345
|
+
printf "%s\n" "$$" > "$FM_HOME/state/.lock"
|
|
346
|
+
"$FAKE_CLAUDE" -c "
|
|
347
|
+
printf \"%s\n\" \"\$\$\" > \"\$FM_HOME/state/inner-pid\"
|
|
348
|
+
\"\$FM_HOME/bin/fm-claude-stop-autoarm.sh\"
|
|
349
|
+
"
|
|
350
|
+
' 2>&1); status=$?
|
|
351
|
+
inner_pid=$(cat "$dir/state/inner-pid" 2>/dev/null || true)
|
|
352
|
+
lock_pid=$(cat "$dir/state/.lock" 2>/dev/null || true)
|
|
353
|
+
[ -n "$inner_pid" ] && [ "$inner_pid" != "$lock_pid" ] \
|
|
354
|
+
|| fail "test setup did not produce a genuine two-hop claude chain: inner=$inner_pid lock=$lock_pid"
|
|
355
|
+
expect_code 2 "$status" "a nested contiguous claude ancestry must resolve to the outer lock-owning pid and arm"
|
|
356
|
+
[ -e "$dir/state/arm-ran" ] || fail "hook did not resolve past the inner claude-named process to the outer lock owner"
|
|
357
|
+
[ "$(epoch_outcome "$dir")" = rewake ] || fail "nested-chain arm must record outcome=rewake"
|
|
358
|
+
pass "auto-arm: resolves the outermost pid of a nested contiguous claude ancestry (bg-spare chain)"
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
test_inert_when_fleet_idle() {
|
|
362
|
+
local dir out status
|
|
363
|
+
dir=$(make_primary_dir "$TMP_ROOT/idle")
|
|
364
|
+
: > "$dir/state/.claude-autoarm-failure-notified"
|
|
365
|
+
: > "$dir/state/.claude-autoarm-failure-alarmed"
|
|
366
|
+
write_arm_fixture "$dir" actionable
|
|
367
|
+
out=$(run_autoarm "$dir" 2>/dev/null); status=$?
|
|
368
|
+
expect_code 0 "$status" "hook must exit 0 in an idle home with no X-mode poll"
|
|
369
|
+
[ ! -e "$dir/state/arm-ran" ] || fail "hook armed an idle home"
|
|
370
|
+
assert_present "$dir/state/.claude-autoarm-failure-notified" "idle state without positive recovery reset the failure notice"
|
|
371
|
+
assert_present "$dir/state/.claude-autoarm-failure-alarmed" "idle state without positive recovery reset the attended alarm"
|
|
372
|
+
pass "auto-arm: inert with nothing in flight and no X-mode need"
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
# --- the armed cycle ----------------------------------------------------------
|
|
376
|
+
|
|
377
|
+
test_actionable_close_rewakes_with_reason() {
|
|
378
|
+
local dir out status
|
|
379
|
+
dir=$(make_primary_dir "$TMP_ROOT/actionable")
|
|
380
|
+
: > "$dir/state/task.meta"
|
|
381
|
+
write_arm_fixture "$dir" actionable
|
|
382
|
+
out=$(run_autoarm "$dir" 2>/dev/null); status=$?
|
|
383
|
+
expect_code 2 "$status" "an actionable arm close must exit 2 so Claude rewakes"
|
|
384
|
+
assert_contains "$out" "firstmate watcher wake" "rewake must carry the wake banner"
|
|
385
|
+
assert_contains "$out" "stale: fixture-win actionable" "rewake must carry the arm's reason line"
|
|
386
|
+
assert_contains "$out" "bin/fm-wake-drain.sh" "rewake must direct the drain-first protocol"
|
|
387
|
+
assert_contains "$out" "do NOT run bin/fm-watch-arm.sh" "rewake must forbid a duplicate model re-arm"
|
|
388
|
+
[ "$(epoch_outcome "$dir")" = rewake ] || fail "epoch must record outcome=rewake, got: $(epoch_outcome "$dir")"
|
|
389
|
+
[ ! -e "$dir/state/.claude-autoarm.lock" ] || fail "owner lock must be released after the cycle"
|
|
390
|
+
[ -e "$dir/state/arm-ran" ] || fail "hook never foregrounded the arm wrapper"
|
|
391
|
+
pass "auto-arm: actionable close translates to exactly one exit-2 rewake with reason"
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
test_actionable_close_with_live_successor_rewakes_once() {
|
|
395
|
+
local dir out out2 status status2 pid identity
|
|
396
|
+
dir=$(make_primary_dir "$TMP_ROOT/actionable-live-successor")
|
|
397
|
+
: > "$dir/state/task.meta"
|
|
398
|
+
write_arm_fixture "$dir" actionable
|
|
399
|
+
sleep 60 &
|
|
400
|
+
pid=$!
|
|
401
|
+
identity=$(watcher_identity "$dir" "$pid") || fail "could not identify live successor for actionable close"
|
|
402
|
+
record_watcher_lock "$dir" "$pid" "$identity"
|
|
403
|
+
touch "$dir/state/.last-watcher-beat"
|
|
404
|
+
|
|
405
|
+
out=$(run_autoarm "$dir" 2>/dev/null); status=$?
|
|
406
|
+
write_arm_fixture "$dir" benign-live
|
|
407
|
+
out2=$(run_autoarm "$dir" 2>/dev/null); status2=$?
|
|
408
|
+
|
|
409
|
+
expect_code 2 "$status" "an actionable close must rewake when a live successor already exists"
|
|
410
|
+
expect_code 0 "$status2" "a repeated non-actionable close with the live successor must stay quiet"
|
|
411
|
+
[ "$(printf '%s\n' "$out" | grep -c '^firstmate watcher wake')" -eq 1 ] \
|
|
412
|
+
|| fail "actionable close with a live successor did not emit exactly one wake banner: $out"
|
|
413
|
+
[ "$(printf '%s\n' "$out" | grep -c '^stale: fixture-win actionable')" -eq 1 ] \
|
|
414
|
+
|| fail "actionable close with a live successor did not surface its reason exactly once: $out"
|
|
415
|
+
[ -z "$out2" ] || fail "repeated hook duplicated the delivered actionable result: $out2"
|
|
416
|
+
kill -0 "$pid" 2>/dev/null || fail "actionable delivery stopped or replaced the live successor"
|
|
417
|
+
[ "$(epoch_outcome "$dir")" = clean ] || fail "the later benign close must record outcome=clean"
|
|
418
|
+
|
|
419
|
+
kill "$pid" 2>/dev/null || true
|
|
420
|
+
wait "$pid" 2>/dev/null || true
|
|
421
|
+
pass "auto-arm: actionable close survives a healthy successor without duplicate delivery"
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
test_failed_close_rewakes_with_failure_banner() {
|
|
425
|
+
local dir out status
|
|
426
|
+
dir=$(make_primary_dir "$TMP_ROOT/failed")
|
|
427
|
+
: > "$dir/state/task.meta"
|
|
428
|
+
write_arm_fixture "$dir" failed
|
|
429
|
+
out=$(run_autoarm "$dir" 2>/dev/null); status=$?
|
|
430
|
+
expect_code 2 "$status" "a typed watcher failure must rewake as an alarm"
|
|
431
|
+
assert_contains "$out" "automatic supervision mechanism is broken" "failure rewake must describe the automatic mechanism failure"
|
|
432
|
+
assert_contains "$out" "watcher: FAILED" "failure rewake must carry the arm's typed failure"
|
|
433
|
+
assert_not_contains "$out" "bin/fm-watch-arm.sh" "failure rewake must not create a manual arm loop"
|
|
434
|
+
[ "$(epoch_outcome "$dir")" = failed ] || fail "epoch must record outcome=failed, got: $(epoch_outcome "$dir")"
|
|
435
|
+
[ "$(wc -l < "$dir/state/arm-ran" | tr -d ' ')" -eq 2 ] || fail "failure must exhaust exactly two bounded arm attempts"
|
|
436
|
+
pass "auto-arm: bounded failure verification emits one automatic-mechanism alarm"
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
test_failed_cycles_notify_once_and_keep_retrying() {
|
|
440
|
+
local dir out1 out2 status1 status2
|
|
441
|
+
dir=$(make_primary_dir "$TMP_ROOT/failed-dedup")
|
|
442
|
+
: > "$dir/state/task.meta"
|
|
443
|
+
write_arm_fixture "$dir" failed
|
|
444
|
+
out1=$(run_autoarm "$dir" 2>/dev/null); status1=$?
|
|
445
|
+
out2=$(run_autoarm "$dir" 2>/dev/null); status2=$?
|
|
446
|
+
expect_code 2 "$status1" "the first exhausted failure must notify"
|
|
447
|
+
expect_code 2 "$status2" "a consecutive exhausted failure must force another Stop-owned retry"
|
|
448
|
+
[ -n "$out1" ] || fail "the first exhausted failure did not notify"
|
|
449
|
+
[ -z "$out2" ] || fail "consecutive exhausted failure repeated an operator notice: $out2"
|
|
450
|
+
[ "$(wc -l < "$dir/state/arm-ran" | tr -d ' ')" -eq 4 ] || fail "each cycle must retain bounded automatic retries"
|
|
451
|
+
assert_present "$dir/state/.claude-autoarm-failure-notified" "failure episode marker was not recorded"
|
|
452
|
+
[ "$(epoch_outcome "$dir")" = failed-suppressed ] || fail "second failure must record failed-suppressed"
|
|
453
|
+
pass "auto-arm: consecutive failures keep Stop-owned retry without repeating notice"
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
test_failure_notice_marker_write_refuses_delivery_and_retries() {
|
|
457
|
+
local dir marker out1 out2 out3 status1 status2 status3 gen1 delivered
|
|
458
|
+
dir=$(make_primary_dir "$TMP_ROOT/failed-marker-refusal")
|
|
459
|
+
: > "$dir/state/task.meta"
|
|
460
|
+
write_arm_fixture "$dir" failed
|
|
461
|
+
marker="$dir/state/.claude-autoarm-failure-notified"
|
|
462
|
+
ln -s "$dir/state/missing/notice" "$marker"
|
|
463
|
+
|
|
464
|
+
out1=$(run_autoarm "$dir" 2>/dev/null); status1=$?
|
|
465
|
+
expect_code 0 "$status1" "an unrecordable failure notice must refuse delivery"
|
|
466
|
+
[ -L "$marker" ] || fail "the failed marker write unexpectedly replaced its dangling symlink"
|
|
467
|
+
[ "$(epoch_outcome "$dir")" = failed ] || fail "the refused generation must leave its terminal ledger outcome"
|
|
468
|
+
gen1=$(epoch_field "$dir" epoch)
|
|
469
|
+
|
|
470
|
+
rm -f "$marker"
|
|
471
|
+
out2=$(run_autoarm "$dir" 2>/dev/null); status2=$?
|
|
472
|
+
out3=$(run_autoarm "$dir" 2>/dev/null); status3=$?
|
|
473
|
+
expect_code 2 "$status2" "a successor must retry and deliver after the marker path is restored"
|
|
474
|
+
expect_code 2 "$status3" "a later failure must retain the Stop-owned retry"
|
|
475
|
+
[ "$(epoch_field "$dir" epoch)" -gt "$gen1" ] || fail "the successor did not supersede the refused terminal entry"
|
|
476
|
+
assert_present "$marker" "the successful successor did not record the failure notice"
|
|
477
|
+
assert_contains "$out2" "automatic supervision mechanism is broken" "the successful successor did not deliver the failure notice"
|
|
478
|
+
[ -z "$out3" ] || fail "the firing after the successful marker commit repeated the notice: $out3"
|
|
479
|
+
delivered=$(printf '%s\n%s\n' "$out2" "$out3" | grep -c 'automatic supervision mechanism is broken' || true)
|
|
480
|
+
[ "$delivered" -eq 1 ] || fail "the restored episode delivered $delivered failure notices instead of one"
|
|
481
|
+
pass "auto-arm: marker-write refusal defers delivery until one successor commits the notice"
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
test_unverified_clean_close_exhausts_retries() {
|
|
485
|
+
local dir out status
|
|
486
|
+
dir=$(make_primary_dir "$TMP_ROOT/clean")
|
|
487
|
+
: > "$dir/state/task.meta"
|
|
488
|
+
write_arm_fixture "$dir" clean
|
|
489
|
+
out=$(run_autoarm "$dir" 2>/dev/null); status=$?
|
|
490
|
+
expect_code 2 "$status" "a non-actionable close without a healthy watcher must fail closed"
|
|
491
|
+
assert_contains "$out" "automatic supervision mechanism is broken" "unverified close must report automatic failure"
|
|
492
|
+
[ "$(wc -l < "$dir/state/arm-ran" | tr -d ' ')" -eq 2 ] || fail "unverified close must exhaust exactly two bounded attempts"
|
|
493
|
+
[ "$(epoch_outcome "$dir")" = failed ] || fail "epoch must record outcome=failed, got: $(epoch_outcome "$dir")"
|
|
494
|
+
pass "auto-arm: unverified clean close exhausts retries and fails closed"
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
test_post_alarm_actionable_close_is_suppressed() {
|
|
498
|
+
local dir out status
|
|
499
|
+
dir=$(make_primary_dir "$TMP_ROOT/post-alarm-actionable")
|
|
500
|
+
: > "$dir/state/task.meta"
|
|
501
|
+
: > "$dir/state/.claude-autoarm-failure-notified"
|
|
502
|
+
: > "$dir/state/.claude-autoarm-failure-alarmed"
|
|
503
|
+
write_arm_fixture "$dir" actionable
|
|
504
|
+
out=$(run_autoarm "$dir" 2>/dev/null); status=$?
|
|
505
|
+
expect_code 0 "$status" "an actionable result after attended fail-open must not continue"
|
|
506
|
+
[ -z "$out" ] || fail "post-alarm actionable result produced continuation output: $out"
|
|
507
|
+
assert_present "$dir/state/.claude-autoarm-failure-notified" "post-alarm actionable result cleared the failure notice"
|
|
508
|
+
assert_present "$dir/state/.claude-autoarm-failure-alarmed" "post-alarm actionable result cleared the attended alarm"
|
|
509
|
+
[ "$(epoch_outcome "$dir")" = failed-suppressed ] || fail "post-alarm actionable result must record failed-suppressed"
|
|
510
|
+
pass "auto-arm: post-alarm actionable outcomes cannot continue or reset failure state"
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
test_benign_cycle_end_with_live_watcher_is_silent() {
|
|
514
|
+
local dir out out2 status status2 pid identity
|
|
515
|
+
dir=$(make_primary_dir "$TMP_ROOT/benign-live")
|
|
516
|
+
: > "$dir/state/task.meta"
|
|
517
|
+
write_arm_fixture "$dir" benign-live
|
|
518
|
+
sleep 60 &
|
|
519
|
+
pid=$!
|
|
520
|
+
identity=$(watcher_identity "$dir" "$pid") || fail "could not identify live watcher holder for benign close"
|
|
521
|
+
record_watcher_lock "$dir" "$pid" "$identity"
|
|
522
|
+
touch "$dir/state/.last-watcher-beat"
|
|
523
|
+
printf 'session=sess-autoarm\ncount=3\nepoch=9\n' > "$dir/state/.turnend-claude-blocks"
|
|
524
|
+
: > "$dir/state/.claude-autoarm-failure-notified"
|
|
525
|
+
: > "$dir/state/.claude-autoarm-failure-alarmed"
|
|
526
|
+
out=$(run_autoarm "$dir" 2>/dev/null); status=$?
|
|
527
|
+
out2=$(run_autoarm "$dir" 2>/dev/null); status2=$?
|
|
528
|
+
kill "$pid" 2>/dev/null || true
|
|
529
|
+
wait "$pid" 2>/dev/null || true
|
|
530
|
+
expect_code 0 "$status" "a failed-looking cycle with a live fresh watcher must be benign"
|
|
531
|
+
expect_code 0 "$status2" "the next Stop-owned cycle must remain benign with the live watcher"
|
|
532
|
+
[ -z "$out" ] || fail "benign live cycle produced an operator notice: $out"
|
|
533
|
+
[ -z "$out2" ] || fail "next benign live cycle produced an operator notice: $out2"
|
|
534
|
+
[ "$(epoch_outcome "$dir")" = clean ] || fail "benign live cycle must record outcome=clean, got: $(epoch_outcome "$dir")"
|
|
535
|
+
[ "$(wc -l < "$dir/state/arm-ran" | tr -d ' ')" -eq 2 ] || fail "the next Stop-owned cycle must run its own bounded arm"
|
|
536
|
+
[ ! -e "$dir/state/.turnend-claude-blocks" ] || fail "benign live cycle must clear the prior block budget"
|
|
537
|
+
[ ! -e "$dir/state/.claude-autoarm-failure-notified" ] || fail "benign live cycle must not leave a failure-notice marker"
|
|
538
|
+
[ ! -e "$dir/state/.claude-autoarm-failure-alarmed" ] || fail "benign live cycle must not leave an attended-alarm marker"
|
|
539
|
+
pass "auto-arm: benign cycle end with a live watcher and fresh beacon stays silent across the next cycle"
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
test_positive_recovery_budget_contention_preserves_episode() {
|
|
543
|
+
local dir out status pid identity holder
|
|
544
|
+
dir=$(make_primary_dir "$TMP_ROOT/recovery-budget-contention")
|
|
545
|
+
: > "$dir/state/task.meta"
|
|
546
|
+
write_arm_fixture "$dir" benign-live
|
|
547
|
+
sleep 60 &
|
|
548
|
+
pid=$!
|
|
549
|
+
identity=$(watcher_identity "$dir" "$pid") || fail "could not identify live watcher holder for recovery contention"
|
|
550
|
+
record_watcher_lock "$dir" "$pid" "$identity"
|
|
551
|
+
touch "$dir/state/.last-watcher-beat"
|
|
552
|
+
printf 'session=sess-autoarm\ncount=3\nepoch=9\n' > "$dir/state/.turnend-claude-blocks"
|
|
553
|
+
: > "$dir/state/.claude-autoarm-failure-notified"
|
|
554
|
+
sleep 60 &
|
|
555
|
+
holder=$!
|
|
556
|
+
mkdir -p "$dir/state/.turnend-claude-blocks.lock"
|
|
557
|
+
printf '%s\n' "$holder" > "$dir/state/.turnend-claude-blocks.lock/pid"
|
|
558
|
+
out=$(run_autoarm "$dir" 2>/dev/null); status=$?
|
|
559
|
+
expect_code 2 "$status" "a healthy auto-arm must continue when the episode reset lock is busy"
|
|
560
|
+
[ -z "$out" ] || fail "recovery contention produced an operator notice: $out"
|
|
561
|
+
[ "$(epoch_outcome "$dir")" = failed-suppressed ] || fail "recovery contention must not record ordinary clean recovery"
|
|
562
|
+
assert_present "$dir/state/.turnend-claude-blocks" "recovery contention partially cleared the block budget"
|
|
563
|
+
assert_present "$dir/state/.claude-autoarm-failure-notified" "recovery contention partially cleared the failure notice"
|
|
564
|
+
kill "$holder" 2>/dev/null || true
|
|
565
|
+
wait "$holder" 2>/dev/null || true
|
|
566
|
+
out=$(run_autoarm "$dir" 2>/dev/null); status=$?
|
|
567
|
+
kill "$pid" 2>/dev/null || true
|
|
568
|
+
wait "$pid" 2>/dev/null || true
|
|
569
|
+
expect_code 0 "$status" "a later healthy auto-arm must complete the episode reset"
|
|
570
|
+
assert_absent "$dir/state/.turnend-claude-blocks" "successful retry left the block budget"
|
|
571
|
+
assert_absent "$dir/state/.claude-autoarm-failure-notified" "successful retry left the failure notice"
|
|
572
|
+
pass "auto-arm: budget contention preserves the episode and forces a reset retry"
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
test_owner_mutex_contention_preserves_failure_episode_reset() {
|
|
576
|
+
local dir out hook_pid status watcher watcher_id holder i
|
|
577
|
+
dir=$(make_primary_dir "$TMP_ROOT/reset-owner-contention")
|
|
578
|
+
: > "$dir/state/task.meta"
|
|
579
|
+
: > "$dir/state/.turnend-claude-blocks"
|
|
580
|
+
: > "$dir/state/.claude-autoarm-failure-notified"
|
|
581
|
+
: > "$dir/state/.claude-autoarm-failure-alarmed"
|
|
582
|
+
write_arm_fixture "$dir" reset-boundary
|
|
583
|
+
sleep 60 &
|
|
584
|
+
watcher=$!
|
|
585
|
+
watcher_id=$(watcher_identity "$dir" "$watcher") || fail "could not identify reset-contention watcher"
|
|
586
|
+
record_watcher_lock "$dir" "$watcher" "$watcher_id"
|
|
587
|
+
touch "$dir/state/.last-watcher-beat"
|
|
588
|
+
out="$dir/state/hook.out"
|
|
589
|
+
run_autoarm_bg "$dir" "$out"
|
|
590
|
+
hook_pid=$RUN_AUTOARM_BG_PID
|
|
591
|
+
i=0
|
|
592
|
+
while [ ! -e "$dir/state/arm-waiting" ]; do
|
|
593
|
+
[ "$i" -lt 50 ] || fail "healthy owner never reached the reset boundary"
|
|
594
|
+
sleep 0.05
|
|
595
|
+
i=$((i + 1))
|
|
596
|
+
done
|
|
597
|
+
sleep 60 &
|
|
598
|
+
holder=$!
|
|
599
|
+
mkdir -p "$dir/state/.claude-autoarm.lock"
|
|
600
|
+
printf '%s\n' "$holder" > "$dir/state/.claude-autoarm.lock/pid"
|
|
601
|
+
: > "$dir/state/arm-release"
|
|
602
|
+
wait "$hook_pid"; status=$?
|
|
603
|
+
expect_code 0 "$status" "owner-mutex contention at reset must close quietly"
|
|
604
|
+
[ ! -s "$out" ] || fail "owner-mutex contention at reset produced output: $(cat "$out")"
|
|
605
|
+
assert_present "$dir/state/.turnend-claude-blocks" "contended reset deleted the block budget"
|
|
606
|
+
assert_present "$dir/state/.claude-autoarm-failure-notified" "contended reset deleted the failure notice"
|
|
607
|
+
assert_present "$dir/state/.claude-autoarm-failure-alarmed" "contended reset deleted the attended alarm"
|
|
608
|
+
kill "$holder" "$watcher" 2>/dev/null || true
|
|
609
|
+
wait "$holder" 2>/dev/null || true
|
|
610
|
+
wait "$watcher" 2>/dev/null || true
|
|
611
|
+
rm -rf "$dir/state/.claude-autoarm.lock"
|
|
612
|
+
pass "auto-arm: owner-mutex contention preserves successor episode state"
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
test_arms_for_x_mode_poll_need_without_inflight() {
|
|
616
|
+
local dir out status
|
|
617
|
+
dir=$(make_primary_dir "$TMP_ROOT/x-need")
|
|
618
|
+
printf '#!/usr/bin/env bash\nexit 0\n' > "$dir/state/x-watch.check.sh"
|
|
619
|
+
write_arm_fixture "$dir" actionable
|
|
620
|
+
out=$(run_autoarm "$dir" 2>/dev/null); status=$?
|
|
621
|
+
expect_code 2 "$status" "an X-mode relay poll need must keep the auto-arm active with zero tasks in flight"
|
|
622
|
+
[ -e "$dir/state/arm-ran" ] || fail "hook did not arm for the X-mode poll need"
|
|
623
|
+
pass "auto-arm: X-mode poll need arms the cycle even with no tasks in flight"
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
test_single_flight_admits_exactly_one_owner() {
|
|
627
|
+
local dir rc1 rc2 count
|
|
628
|
+
dir=$(make_primary_dir "$TMP_ROOT/single-flight")
|
|
629
|
+
: > "$dir/state/task.meta"
|
|
630
|
+
write_arm_fixture "$dir" slow-actionable
|
|
631
|
+
FM_HOME="$dir" "$FAKE_CLAUDE" -c '
|
|
632
|
+
printf "%s\n" "$$" > "$FM_HOME/state/.lock"
|
|
633
|
+
printf "%s\n" "{\"session_id\":\"s\"}" | "$FM_HOME/bin/fm-claude-stop-autoarm.sh" >/dev/null 2>"$FM_HOME/state/err1" &
|
|
634
|
+
p1=$!
|
|
635
|
+
printf "%s\n" "{\"session_id\":\"s\"}" | "$FM_HOME/bin/fm-claude-stop-autoarm.sh" >/dev/null 2>"$FM_HOME/state/err2" &
|
|
636
|
+
p2=$!
|
|
637
|
+
wait "$p1"; echo $? > "$FM_HOME/state/rc1"
|
|
638
|
+
wait "$p2"; echo $? > "$FM_HOME/state/rc2"
|
|
639
|
+
'
|
|
640
|
+
rc1=$(cat "$dir/state/rc1")
|
|
641
|
+
rc2=$(cat "$dir/state/rc2")
|
|
642
|
+
count=$(wc -l < "$dir/state/arm-ran" | tr -d ' ')
|
|
643
|
+
[ "$count" -eq 1 ] || fail "concurrent firings must foreground exactly one arm, saw $count"
|
|
644
|
+
{ [ "$rc1" = 2 ] && [ "$rc2" = 0 ]; } || { [ "$rc1" = 0 ] && [ "$rc2" = 2 ]; } \
|
|
645
|
+
|| fail "exactly one firing must translate the close (rc 2) and the other must no-op (rc 0), got rc1=$rc1 rc2=$rc2"
|
|
646
|
+
pass "auto-arm: concurrent firings admit one owner and one rewake translation"
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
# --- abandoned single-flight claim recovery (legacy shim) ----------------------
|
|
650
|
+
# The 2026-08-14 lapse: one cycle armed, beat its beacon, delivered a single
|
|
651
|
+
# rewake, and exited, leaving its owner lock behind with a live pid. The single
|
|
652
|
+
# flight gate then turned every later firing into exit 0, so with two tasks in
|
|
653
|
+
# flight and a beacon 40 minutes cold nothing re-armed and both workers' reports
|
|
654
|
+
# sat unread until an operator drained the queue by hand. The lock alone is not
|
|
655
|
+
# enough to prove that: the ledger naming that same pid with a finished outcome,
|
|
656
|
+
# or a recorded pid-identity the live pid no longer matches, is what distinguishes
|
|
657
|
+
# an abandoned claim from one still deciding.
|
|
658
|
+
#
|
|
659
|
+
# These fixtures fabricate the LOCK-HOLDING claim shape a pre-generation build
|
|
660
|
+
# leaves behind, so this section pins the legacy shim: a live legacy owner
|
|
661
|
+
# still defers the gate, and an abandoned one is reclaimed once so the home
|
|
662
|
+
# re-arms - with an identity-verified live owner retired via TERM first, and
|
|
663
|
+
# an identityless one reclaimed without any signalling. The generation-claim
|
|
664
|
+
# section below pins the current contract.
|
|
665
|
+
|
|
666
|
+
# Fabricate a held owner lock: <dir> <pid> <role>. Plain-dir shape on purpose -
|
|
667
|
+
# the hook must reclaim whatever a crashed or blocked owner left behind.
|
|
668
|
+
record_autoarm_owner() {
|
|
669
|
+
local dir=$1 pid=$2 role=${3:-autoarm}
|
|
670
|
+
mkdir -p "$dir/state/.claude-autoarm.lock"
|
|
671
|
+
printf '%s\n' "$pid" > "$dir/state/.claude-autoarm.lock/pid"
|
|
672
|
+
printf '%s\n' "$role" > "$dir/state/.claude-autoarm.lock/role"
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
# Record the pid-identity a claim leaves inside its own lock: <dir> <pid>. The
|
|
676
|
+
# claim writes the identity of the process that took the lock, so passing a pid
|
|
677
|
+
# OTHER than the lock's own reproduces pid reuse - the recorded claimant is gone
|
|
678
|
+
# and an unrelated live process now answers to its number.
|
|
679
|
+
record_autoarm_owner_identity() {
|
|
680
|
+
local dir=$1 pid=$2 identity
|
|
681
|
+
identity=$(fm_test_pid_identity "$pid") || return 1
|
|
682
|
+
[ -n "$identity" ] || return 1
|
|
683
|
+
printf '%s\n' "$identity" > "$dir/state/.claude-autoarm.lock/pid-identity"
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
# <dir> <epoch-seq> <owner-pid> <outcome>, aged well past any freshness window.
|
|
687
|
+
record_autoarm_epoch() {
|
|
688
|
+
local dir=$1 seq=$2 owner=$3 outcome=$4
|
|
689
|
+
printf 'epoch=%s owner_pid=%s outcome=%s updated_at=1\n' "$seq" "$owner" "$outcome" \
|
|
690
|
+
> "$dir/state/.claude-autoarm-epoch"
|
|
691
|
+
touch -t 202001010000 "$dir/state/.claude-autoarm-epoch"
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
epoch_field() {
|
|
695
|
+
local dir=$1 field=$2
|
|
696
|
+
sed -n "s/^.*[[:space:]]\{0,1\}$field=\([A-Za-z0-9_-]*\).*\$/\1/p" \
|
|
697
|
+
"$dir/state/.claude-autoarm-epoch" 2>/dev/null || true
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
test_abandoned_owner_claim_is_reclaimed_and_rearms() {
|
|
701
|
+
local dir out status pid
|
|
702
|
+
dir=$(make_primary_dir "$TMP_ROOT/abandoned-claim")
|
|
703
|
+
: > "$dir/state/task1.meta"
|
|
704
|
+
: > "$dir/state/task2.meta"
|
|
705
|
+
write_arm_fixture "$dir" actionable
|
|
706
|
+
sleep 60 &
|
|
707
|
+
pid=$!
|
|
708
|
+
record_autoarm_owner "$dir" "$pid"
|
|
709
|
+
record_autoarm_epoch "$dir" 464 "$pid" rewake
|
|
710
|
+
out=$(run_autoarm "$dir" 2>/dev/null); status=$?
|
|
711
|
+
kill -0 "$pid" 2>/dev/null || fail "an identityless abandoned owner must be reclaimed without being signalled"
|
|
712
|
+
kill "$pid" 2>/dev/null || true
|
|
713
|
+
wait "$pid" 2>/dev/null || true
|
|
714
|
+
expect_code 2 "$status" "a claim whose ledger outcome is already terminal must be reclaimed, not deferred to forever"
|
|
715
|
+
[ -e "$dir/state/arm-ran" ] || fail "abandoned claim left the home unarmed with work in flight"
|
|
716
|
+
assert_contains "$out" "firstmate watcher wake" "the reclaimed cycle must still translate its wake"
|
|
717
|
+
[ "$(epoch_field "$dir" epoch)" -gt 464 ] || fail "reclaimed cycle did not advance the frozen ledger: $(epoch_field "$dir" epoch)"
|
|
718
|
+
[ "$(epoch_outcome "$dir")" = rewake ] || fail "reclaimed cycle did not record its own outcome: $(epoch_outcome "$dir")"
|
|
719
|
+
[ "$(epoch_field "$dir" owner_pid)" != "$pid" ] || fail "reclaimed ledger still names the abandoned owner"
|
|
720
|
+
assert_absent "$dir/state/.claude-autoarm.lock" "reclaimed cycle left an owner lock behind"
|
|
721
|
+
assert_absent "$dir/state/.claude-autoarm.lock.steal" "reclaim left its serialization mutex behind"
|
|
722
|
+
pass "auto-arm: an abandoned owner claim is reclaimed so a lapsed cycle re-arms"
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
test_arming_claim_with_fresh_beacon_is_never_reclaimed() {
|
|
726
|
+
local dir out status pid
|
|
727
|
+
dir=$(make_primary_dir "$TMP_ROOT/arming-claim")
|
|
728
|
+
: > "$dir/state/task1.meta"
|
|
729
|
+
write_arm_fixture "$dir" actionable
|
|
730
|
+
sleep 60 &
|
|
731
|
+
pid=$!
|
|
732
|
+
record_autoarm_owner "$dir" "$pid"
|
|
733
|
+
# An owner foregrounds the arm for the whole watcher cycle, so an old "arming"
|
|
734
|
+
# entry is still in progress while its watcher keeps beating the beacon.
|
|
735
|
+
record_autoarm_epoch "$dir" 464 "$pid" arming
|
|
736
|
+
: > "$dir/state/.last-watcher-beat"
|
|
737
|
+
out=$(run_autoarm "$dir" 2>/dev/null); status=$?
|
|
738
|
+
kill "$pid" 2>/dev/null || true
|
|
739
|
+
wait "$pid" 2>/dev/null || true
|
|
740
|
+
expect_code 0 "$status" "a legacy claim still arming under a fresh beacon must keep the single-flight gate closed"
|
|
741
|
+
[ -z "$out" ] || fail "deferring to an arming claim produced output: $out"
|
|
742
|
+
assert_absent "$dir/state/arm-ran" "an arming claim was stolen and double-armed"
|
|
743
|
+
[ "$(epoch_field "$dir" epoch)" = 464 ] || fail "deferred firing rewrote the arming ledger entry"
|
|
744
|
+
assert_present "$dir/state/.claude-autoarm.lock" "an arming claim lost its owner lock"
|
|
745
|
+
pass "auto-arm: a legacy owner still arming is never reclaimed while its watcher keeps beating"
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
# The other legitimate legacy arming shape: a claim that JUST started arming
|
|
749
|
+
# after a real lapse, so the beacon is long stale but the entry is fresh. The
|
|
750
|
+
# arm's bounded startup window must never be stolen out from under it.
|
|
751
|
+
test_fresh_arming_claim_with_stale_beacon_is_never_reclaimed() {
|
|
752
|
+
local dir out status pid
|
|
753
|
+
dir=$(make_primary_dir "$TMP_ROOT/fresh-arming-claim")
|
|
754
|
+
: > "$dir/state/task1.meta"
|
|
755
|
+
write_arm_fixture "$dir" actionable
|
|
756
|
+
sleep 60 &
|
|
757
|
+
pid=$!
|
|
758
|
+
record_autoarm_owner "$dir" "$pid"
|
|
759
|
+
record_autoarm_owner_identity "$dir" "$pid" || fail "could not record a claim pid-identity"
|
|
760
|
+
printf 'epoch=464 owner_pid=%s outcome=arming updated_at=%s\n' "$pid" "$(date +%s)" \
|
|
761
|
+
> "$dir/state/.claude-autoarm-epoch"
|
|
762
|
+
touch -t 202001010000 "$dir/state/.last-watcher-beat"
|
|
763
|
+
out=$(run_autoarm "$dir" 2>/dev/null); status=$?
|
|
764
|
+
kill "$pid" 2>/dev/null || true
|
|
765
|
+
wait "$pid" 2>/dev/null || true
|
|
766
|
+
expect_code 0 "$status" "a freshly arming legacy claim must keep the single-flight gate closed even after a long lapse"
|
|
767
|
+
[ -z "$out" ] || fail "deferring to a fresh arming claim produced output: $out"
|
|
768
|
+
assert_absent "$dir/state/arm-ran" "a fresh arming claim was stolen and double-armed"
|
|
769
|
+
assert_present "$dir/state/.claude-autoarm.lock" "a fresh arming claim lost its owner lock"
|
|
770
|
+
pass "auto-arm: a fresh legacy arming claim is never reclaimed while its startup window is still open"
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
test_claim_not_named_by_the_ledger_is_never_reclaimed() {
|
|
774
|
+
local dir out status pid
|
|
775
|
+
dir=$(make_primary_dir "$TMP_ROOT/unnamed-claim")
|
|
776
|
+
: > "$dir/state/task1.meta"
|
|
777
|
+
write_arm_fixture "$dir" actionable
|
|
778
|
+
sleep 60 &
|
|
779
|
+
pid=$!
|
|
780
|
+
record_autoarm_owner "$dir" "$pid"
|
|
781
|
+
# A fresh claimant holds the lock before it writes "arming", so until it does
|
|
782
|
+
# the ledger still names the PREVIOUS owner. Requiring the two pids to match is
|
|
783
|
+
# what keeps that window from being mistaken for abandonment.
|
|
784
|
+
record_autoarm_epoch "$dir" 464 999 rewake
|
|
785
|
+
out=$(run_autoarm "$dir" 2>/dev/null); status=$?
|
|
786
|
+
kill "$pid" 2>/dev/null || true
|
|
787
|
+
wait "$pid" 2>/dev/null || true
|
|
788
|
+
expect_code 0 "$status" "a live claim the ledger does not name is unproven and must be left alone"
|
|
789
|
+
[ -z "$out" ] || fail "deferring to an unnamed claim produced output: $out"
|
|
790
|
+
assert_absent "$dir/state/arm-ran" "a claim the ledger does not name was stolen and double-armed"
|
|
791
|
+
assert_present "$dir/state/.claude-autoarm.lock" "an unproven claim lost its owner lock"
|
|
792
|
+
pass "auto-arm: a live claim the ledger does not name is never reclaimed"
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
# The same unrecoverable lapse, reached where the ledger cannot prove it: a session
|
|
796
|
+
# teardown kills the claim's whole process group before it records any outcome, so
|
|
797
|
+
# the entry still reads "arming" while the recorded pid is later handed to an
|
|
798
|
+
# unrelated live process. Only the identity the claim recorded inside its own lock
|
|
799
|
+
# separates that from a real arm in progress, so keep the beacon fresh here: this
|
|
800
|
+
# case must reclaim on the identity leg alone, not the stuck-arming leg. The
|
|
801
|
+
# reclaim must not signal the unrelated live process that inherited the number.
|
|
802
|
+
test_pid_reused_arming_claim_is_reclaimed_and_rearms() {
|
|
803
|
+
local dir out status pid
|
|
804
|
+
dir=$(make_primary_dir "$TMP_ROOT/reused-pid-arming")
|
|
805
|
+
: > "$dir/state/task1.meta"
|
|
806
|
+
: > "$dir/state/task2.meta"
|
|
807
|
+
write_arm_fixture "$dir" actionable
|
|
808
|
+
sleep 60 &
|
|
809
|
+
pid=$!
|
|
810
|
+
record_autoarm_owner "$dir" "$pid"
|
|
811
|
+
record_autoarm_owner_identity "$dir" "$$" || fail "could not record a claim pid-identity"
|
|
812
|
+
record_autoarm_epoch "$dir" 464 "$pid" arming
|
|
813
|
+
: > "$dir/state/.last-watcher-beat"
|
|
814
|
+
out=$(run_autoarm "$dir" 2>/dev/null); status=$?
|
|
815
|
+
kill -0 "$pid" 2>/dev/null || fail "the unrelated live process inheriting the number must never be signalled"
|
|
816
|
+
kill "$pid" 2>/dev/null || true
|
|
817
|
+
wait "$pid" 2>/dev/null || true
|
|
818
|
+
expect_code 2 "$status" "a claim whose recorded identity no longer matches its live pid must be reclaimed, arming entry or not"
|
|
819
|
+
[ -e "$dir/state/arm-ran" ] || fail "a reused-pid claim left the home unarmed with work in flight"
|
|
820
|
+
assert_contains "$out" "firstmate watcher wake" "the reclaimed cycle must still translate its wake"
|
|
821
|
+
[ "$(epoch_field "$dir" epoch)" -gt 464 ] || fail "reclaimed cycle did not advance the frozen ledger: $(epoch_field "$dir" epoch)"
|
|
822
|
+
assert_absent "$dir/state/.claude-autoarm.lock" "reclaimed cycle left an owner lock behind"
|
|
823
|
+
assert_absent "$dir/state/.claude-autoarm.lock.steal" "reclaim left its serialization mutex behind"
|
|
824
|
+
pass "auto-arm: a claim whose pid was reused is reclaimed even while its ledger entry still reads arming"
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
# The other ledger-blind shape: no ledger at all (a fresh or hand-cleared home)
|
|
828
|
+
# plus a reused pid. Without the recorded identity nothing proves abandonment, so
|
|
829
|
+
# every later firing exits at the lock and the home never re-arms.
|
|
830
|
+
test_pid_reused_claim_with_no_ledger_is_reclaimed_and_rearms() {
|
|
831
|
+
local dir out status pid
|
|
832
|
+
dir=$(make_primary_dir "$TMP_ROOT/reused-pid-no-ledger")
|
|
833
|
+
: > "$dir/state/task1.meta"
|
|
834
|
+
write_arm_fixture "$dir" actionable
|
|
835
|
+
sleep 60 &
|
|
836
|
+
pid=$!
|
|
837
|
+
record_autoarm_owner "$dir" "$pid"
|
|
838
|
+
record_autoarm_owner_identity "$dir" "$$" || fail "could not record a claim pid-identity"
|
|
839
|
+
assert_absent "$dir/state/.claude-autoarm-epoch" "this case must start with no ledger at all"
|
|
840
|
+
out=$(run_autoarm "$dir" 2>/dev/null); status=$?
|
|
841
|
+
kill "$pid" 2>/dev/null || true
|
|
842
|
+
wait "$pid" 2>/dev/null || true
|
|
843
|
+
expect_code 2 "$status" "a reused-pid claim with no ledger to consult must still be reclaimed"
|
|
844
|
+
[ -e "$dir/state/arm-ran" ] || fail "a reused-pid claim with no ledger left the home unarmed"
|
|
845
|
+
assert_contains "$out" "firstmate watcher wake" "the reclaimed cycle must still translate its wake"
|
|
846
|
+
[ "$(epoch_outcome "$dir")" = rewake ] || fail "reclaimed cycle did not record its own outcome: $(epoch_outcome "$dir")"
|
|
847
|
+
assert_absent "$dir/state/.claude-autoarm.lock" "reclaimed cycle left an owner lock behind"
|
|
848
|
+
pass "auto-arm: a reused-pid claim is reclaimed even with no ledger entry to prove it"
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
# The negative control for the identity leg: a claim whose recorded identity still
|
|
852
|
+
# matches the process holding the lock is genuinely in flight, so an arm that has
|
|
853
|
+
# legitimately been running for hours - its watcher beating the whole time - must
|
|
854
|
+
# keep the single-flight gate closed.
|
|
855
|
+
test_identity_matched_arming_claim_is_never_reclaimed() {
|
|
856
|
+
local dir out status pid
|
|
857
|
+
dir=$(make_primary_dir "$TMP_ROOT/identity-matched-arming")
|
|
858
|
+
: > "$dir/state/task1.meta"
|
|
859
|
+
write_arm_fixture "$dir" actionable
|
|
860
|
+
sleep 60 &
|
|
861
|
+
pid=$!
|
|
862
|
+
record_autoarm_owner "$dir" "$pid"
|
|
863
|
+
record_autoarm_owner_identity "$dir" "$pid" || fail "could not record a claim pid-identity"
|
|
864
|
+
record_autoarm_epoch "$dir" 464 "$pid" arming
|
|
865
|
+
: > "$dir/state/.last-watcher-beat"
|
|
866
|
+
out=$(run_autoarm "$dir" 2>/dev/null); status=$?
|
|
867
|
+
kill "$pid" 2>/dev/null || true
|
|
868
|
+
wait "$pid" 2>/dev/null || true
|
|
869
|
+
expect_code 0 "$status" "an identity-matched claim still arming must keep the single-flight gate closed"
|
|
870
|
+
[ -z "$out" ] || fail "deferring to an identity-matched arming claim produced output: $out"
|
|
871
|
+
assert_absent "$dir/state/arm-ran" "an identity-matched arming claim was stolen and double-armed"
|
|
872
|
+
[ "$(epoch_field "$dir" epoch)" = 464 ] || fail "deferred firing rewrote the arming ledger entry"
|
|
873
|
+
assert_present "$dir/state/.claude-autoarm.lock" "an identity-matched arming claim lost its owner lock"
|
|
874
|
+
pass "auto-arm: an identity-matched owner still arming is never reclaimed"
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
test_terminal_check_claim_is_never_reclaimed() {
|
|
878
|
+
local dir out status pid
|
|
879
|
+
dir=$(make_primary_dir "$TMP_ROOT/terminal-check-claim")
|
|
880
|
+
: > "$dir/state/task1.meta"
|
|
881
|
+
write_arm_fixture "$dir" actionable
|
|
882
|
+
sleep 60 &
|
|
883
|
+
pid=$!
|
|
884
|
+
# The synchronous guard takes the same lock under its own role while it decides
|
|
885
|
+
# the attended fail-open. Reclaiming that would race the guard's own decision.
|
|
886
|
+
record_autoarm_owner "$dir" "$pid" terminal-check
|
|
887
|
+
record_autoarm_epoch "$dir" 464 "$pid" failed-suppressed
|
|
888
|
+
out=$(run_autoarm "$dir" 2>/dev/null); status=$?
|
|
889
|
+
kill "$pid" 2>/dev/null || true
|
|
890
|
+
wait "$pid" 2>/dev/null || true
|
|
891
|
+
expect_code 0 "$status" "the guard's own terminal-check claim must never be reclaimed by the arm hook"
|
|
892
|
+
[ -z "$out" ] || fail "deferring to a terminal-check claim produced output: $out"
|
|
893
|
+
assert_absent "$dir/state/arm-ran" "a terminal-check claim was stolen and double-armed"
|
|
894
|
+
assert_present "$dir/state/.claude-autoarm.lock" "a terminal-check claim lost its owner lock"
|
|
895
|
+
pass "auto-arm: the guard's terminal-check claim is never reclaimed"
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
# A proven-stuck legacy owner that is still ALIVE and identity-verified is
|
|
899
|
+
# retired with TERM before its lock is removed, because old-build code cannot
|
|
900
|
+
# re-check generations and would otherwise resume and act after supersession.
|
|
901
|
+
test_stuck_live_legacy_owner_is_retired_and_reclaimed() {
|
|
902
|
+
local dir out status pid
|
|
903
|
+
dir=$(make_primary_dir "$TMP_ROOT/legacy-term")
|
|
904
|
+
: > "$dir/state/task1.meta"
|
|
905
|
+
write_arm_fixture "$dir" actionable
|
|
906
|
+
sleep 60 &
|
|
907
|
+
pid=$!
|
|
908
|
+
record_autoarm_owner "$dir" "$pid"
|
|
909
|
+
record_autoarm_owner_identity "$dir" "$pid" || fail "could not record a claim pid-identity"
|
|
910
|
+
record_autoarm_epoch "$dir" 464 "$pid" arming
|
|
911
|
+
touch -t 202001010000 "$dir/state/.last-watcher-beat"
|
|
912
|
+
out=$(run_autoarm "$dir" 2>/dev/null); status=$?
|
|
913
|
+
expect_code 2 "$status" "a proven-stuck identity-verified live legacy owner must be retired and reclaimed"
|
|
914
|
+
kill -0 "$pid" 2>/dev/null && fail "the stuck legacy owner was reclaimed without being retired"
|
|
915
|
+
wait "$pid" 2>/dev/null || true
|
|
916
|
+
[ -e "$dir/state/arm-ran" ] || fail "the reclaimed home did not re-arm"
|
|
917
|
+
assert_contains "$out" "firstmate watcher wake" "the reclaimed cycle must still translate its wake"
|
|
918
|
+
assert_absent "$dir/state/.claude-autoarm.lock" "reclaim left the legacy owner lock behind"
|
|
919
|
+
pass "auto-arm: a stuck live legacy owner is retired via TERM and its lock reclaimed"
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
# The SIGSTOP counterfactual: a stopped legacy owner survives the bounded
|
|
923
|
+
# retirement wait with TERM queued, and the reclaim must proceed anyway - a
|
|
924
|
+
# pending TERM on the verified owner is retirement-safe because delivery
|
|
925
|
+
# precedes any further user code when the process continues.
|
|
926
|
+
test_stopped_legacy_owner_is_reclaimed_with_term_pending() {
|
|
927
|
+
local dir out status pid i
|
|
928
|
+
dir=$(make_primary_dir "$TMP_ROOT/legacy-term-stopped")
|
|
929
|
+
: > "$dir/state/task1.meta"
|
|
930
|
+
write_arm_fixture "$dir" actionable
|
|
931
|
+
sleep 60 &
|
|
932
|
+
pid=$!
|
|
933
|
+
record_autoarm_owner "$dir" "$pid"
|
|
934
|
+
record_autoarm_owner_identity "$dir" "$pid" || fail "could not record a claim pid-identity"
|
|
935
|
+
record_autoarm_epoch "$dir" 464 "$pid" arming
|
|
936
|
+
touch -t 202001010000 "$dir/state/.last-watcher-beat"
|
|
937
|
+
kill -STOP "$pid" 2>/dev/null || fail "could not stop the legacy owner fixture"
|
|
938
|
+
out=$(run_autoarm "$dir" 2>/dev/null); status=$?
|
|
939
|
+
expect_code 2 "$status" "a stopped legacy owner with TERM queued must not block the reclaim forever"
|
|
940
|
+
[ -e "$dir/state/arm-ran" ] || fail "the reclaimed home did not re-arm past the stopped owner"
|
|
941
|
+
assert_absent "$dir/state/.claude-autoarm.lock" "reclaim left the stopped owner's lock behind"
|
|
942
|
+
kill -CONT "$pid" 2>/dev/null || true
|
|
943
|
+
i=0
|
|
944
|
+
while [ "$i" -lt 40 ] && kill -0 "$pid" 2>/dev/null; do
|
|
945
|
+
sleep 0.05
|
|
946
|
+
i=$((i + 1))
|
|
947
|
+
done
|
|
948
|
+
kill -0 "$pid" 2>/dev/null && fail "the queued TERM did not retire the owner on continue"
|
|
949
|
+
wait "$pid" 2>/dev/null || true
|
|
950
|
+
pass "auto-arm: a SIGSTOPped legacy owner is reclaimed with TERM pending and dies on continue"
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
# --- generation claims: optimistic single-flight and supersession --------------
|
|
954
|
+
# The current claim is the two-line ledger entry itself (line 1 the classic
|
|
955
|
+
# epoch record, line 2 the owner's MANDATORY pid-identity); no lock is held
|
|
956
|
+
# across arming or output. A live open claim defers every firing; a stuck,
|
|
957
|
+
# dead, identity-mismatched, identityless, or finished claim is superseded by
|
|
958
|
+
# taking the next generation; a superseded owner goes completely silent.
|
|
959
|
+
|
|
960
|
+
# Fabricate a v2 generation claim: <dir> <gen> <owner-pid> <outcome>
|
|
961
|
+
# <identity-pid>. The identity of <identity-pid> is recorded as line 2 (the
|
|
962
|
+
# claim's own pid for a matched claim, another pid to reproduce pid reuse).
|
|
963
|
+
record_autoarm_v2_claim() {
|
|
964
|
+
local dir=$1 gen=$2 owner=$3 outcome=$4 identity_pid=$5 identity
|
|
965
|
+
identity=$(fm_test_pid_identity "$identity_pid") || return 1
|
|
966
|
+
[ -n "$identity" ] || return 1
|
|
967
|
+
printf 'epoch=%s owner_pid=%s outcome=%s updated_at=1\n%s\n' \
|
|
968
|
+
"$gen" "$owner" "$outcome" "$identity" > "$dir/state/.claude-autoarm-epoch"
|
|
969
|
+
}
|
|
970
|
+
|
|
971
|
+
# A live open generation claim needs no lock to keep the gate closed: the
|
|
972
|
+
# ledger alone defers a concurrent firing, however old the entry, while the
|
|
973
|
+
# watcher keeps beating the beacon.
|
|
974
|
+
test_open_generation_claim_defers_without_any_lock() {
|
|
975
|
+
local dir out status pid
|
|
976
|
+
dir=$(make_primary_dir "$TMP_ROOT/v2-open-claim")
|
|
977
|
+
: > "$dir/state/task1.meta"
|
|
978
|
+
write_arm_fixture "$dir" actionable
|
|
979
|
+
sleep 60 &
|
|
980
|
+
pid=$!
|
|
981
|
+
record_autoarm_v2_claim "$dir" 464 "$pid" arming "$pid" || fail "could not record a v2 claim"
|
|
982
|
+
touch -t 202001010000 "$dir/state/.claude-autoarm-epoch"
|
|
983
|
+
: > "$dir/state/.last-watcher-beat"
|
|
984
|
+
assert_absent "$dir/state/.claude-autoarm.lock" "this case must start with no owner lock at all"
|
|
985
|
+
out=$(run_autoarm "$dir" 2>/dev/null); status=$?
|
|
986
|
+
kill "$pid" 2>/dev/null || true
|
|
987
|
+
wait "$pid" 2>/dev/null || true
|
|
988
|
+
expect_code 0 "$status" "a live open generation claim must keep the single-flight gate closed with no lock held"
|
|
989
|
+
[ -z "$out" ] || fail "deferring to an open generation claim produced output: $out"
|
|
990
|
+
assert_absent "$dir/state/arm-ran" "an open generation claim was superseded and double-armed"
|
|
991
|
+
[ "$(epoch_field "$dir" epoch)" = 464 ] || fail "deferred firing rewrote the open claim's ledger entry"
|
|
992
|
+
pass "auto-arm: a live open generation claim defers concurrent firings with no lock held"
|
|
993
|
+
}
|
|
994
|
+
|
|
995
|
+
# The 2026-08-26 watcher flap in the generation model: a live, identity-matched
|
|
996
|
+
# owner whose ledger entry and watcher beacon are both older than grace is
|
|
997
|
+
# stuck, and the next firing supersedes it by taking the next generation.
|
|
998
|
+
test_stuck_generation_claim_is_superseded_and_rearms() {
|
|
999
|
+
local dir out status pid
|
|
1000
|
+
dir=$(make_primary_dir "$TMP_ROOT/v2-stuck-claim")
|
|
1001
|
+
: > "$dir/state/task1.meta"
|
|
1002
|
+
: > "$dir/state/task2.meta"
|
|
1003
|
+
write_arm_fixture "$dir" actionable
|
|
1004
|
+
sleep 60 &
|
|
1005
|
+
pid=$!
|
|
1006
|
+
record_autoarm_v2_claim "$dir" 464 "$pid" arming "$pid" || fail "could not record a v2 claim"
|
|
1007
|
+
touch -t 202001010000 "$dir/state/.claude-autoarm-epoch"
|
|
1008
|
+
touch -t 202001010000 "$dir/state/.last-watcher-beat"
|
|
1009
|
+
out=$(run_autoarm "$dir" 2>/dev/null); status=$?
|
|
1010
|
+
kill "$pid" 2>/dev/null || true
|
|
1011
|
+
wait "$pid" 2>/dev/null || true
|
|
1012
|
+
expect_code 2 "$status" "a live owner stuck arming past grace with a beacon just as stale must be superseded, not deferred to forever"
|
|
1013
|
+
[ -e "$dir/state/arm-ran" ] || fail "a stuck generation claim left the home unarmed with work in flight"
|
|
1014
|
+
assert_contains "$out" "firstmate watcher wake" "the superseding generation must still translate its wake"
|
|
1015
|
+
[ "$(epoch_field "$dir" epoch)" -gt 464 ] || fail "superseding claim did not advance the frozen ledger: $(epoch_field "$dir" epoch)"
|
|
1016
|
+
[ "$(epoch_field "$dir" owner_pid)" != "$pid" ] || fail "superseding claim left the stuck owner on the ledger"
|
|
1017
|
+
assert_absent "$dir/state/.claude-autoarm.lock" "the generation claim left a lock held after finishing"
|
|
1018
|
+
pass "auto-arm: a hung generation owner with no watcher beat is superseded so re-arming self-heals"
|
|
1019
|
+
}
|
|
1020
|
+
|
|
1021
|
+
# Identity is mandatory at read time: a bare identityless one-line arming
|
|
1022
|
+
# ledger naming an unrelated live pid is NOT an open claim - it must neither
|
|
1023
|
+
# defer the hook nor survive as the current entry, whatever the beacon says.
|
|
1024
|
+
test_identityless_ledger_never_defers() {
|
|
1025
|
+
local dir out status pid
|
|
1026
|
+
dir=$(make_primary_dir "$TMP_ROOT/v2-identityless-ledger")
|
|
1027
|
+
: > "$dir/state/task1.meta"
|
|
1028
|
+
write_arm_fixture "$dir" actionable
|
|
1029
|
+
sleep 60 &
|
|
1030
|
+
pid=$!
|
|
1031
|
+
printf 'epoch=464 owner_pid=%s outcome=arming updated_at=1\n' "$pid" \
|
|
1032
|
+
> "$dir/state/.claude-autoarm-epoch"
|
|
1033
|
+
touch -t 202001010000 "$dir/state/.claude-autoarm-epoch"
|
|
1034
|
+
: > "$dir/state/.last-watcher-beat"
|
|
1035
|
+
out=$(run_autoarm "$dir" 2>/dev/null); status=$?
|
|
1036
|
+
kill -0 "$pid" 2>/dev/null || fail "the unrelated live pid on an identityless ledger must never be signalled"
|
|
1037
|
+
kill "$pid" 2>/dev/null || true
|
|
1038
|
+
wait "$pid" 2>/dev/null || true
|
|
1039
|
+
expect_code 2 "$status" "an identityless arming ledger must be superseded, never deferred to"
|
|
1040
|
+
[ -e "$dir/state/arm-ran" ] || fail "an identityless ledger left the home unarmed"
|
|
1041
|
+
[ "$(epoch_field "$dir" epoch)" -gt 464 ] || fail "the identityless entry was not superseded: $(epoch_field "$dir" epoch)"
|
|
1042
|
+
pass "auto-arm: an identityless arming ledger never defers the gate (reused-pid loophole closed)"
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
# A superseded owner must not start or attach another watcher: when its claim
|
|
1046
|
+
# is superseded between arm attempts, the retry boundary goes silent instead
|
|
1047
|
+
# of invoking the arm again.
|
|
1048
|
+
test_superseded_owner_never_reinvokes_the_arm() {
|
|
1049
|
+
local dir out status count
|
|
1050
|
+
dir=$(make_primary_dir "$TMP_ROOT/v2-superseded-arm-boundary")
|
|
1051
|
+
: > "$dir/state/task1.meta"
|
|
1052
|
+
write_arm_fixture "$dir" supersede-then-fail
|
|
1053
|
+
out=$(run_autoarm "$dir" 2>/dev/null); status=$?
|
|
1054
|
+
expect_code 0 "$status" "an owner superseded between arm attempts must exit 0 silently"
|
|
1055
|
+
[ -z "$out" ] || fail "a superseded owner produced output at the arm boundary: $out"
|
|
1056
|
+
count=$(wc -l < "$dir/state/arm-ran" | tr -d ' ')
|
|
1057
|
+
[ "$count" -eq 1 ] || fail "a superseded owner re-invoked the arm, saw $count arms"
|
|
1058
|
+
[ "$(epoch_field "$dir" epoch)" = 999 ] || fail "a superseded owner rewrote its successor's ledger entry: $(epoch_field "$dir" epoch)"
|
|
1059
|
+
pass "auto-arm: a superseded owner never re-invokes the arm and leaves its successor's claim untouched"
|
|
1060
|
+
}
|
|
1061
|
+
|
|
1062
|
+
# End-to-end regression for all three concurrency edge classes at once, with a
|
|
1063
|
+
# REAL hook process hung mid-arm:
|
|
1064
|
+
# 1. no mutex across blocking steps - while owner A is mid-arm, a concurrent
|
|
1065
|
+
# firing B defers promptly instead of queueing on any lock;
|
|
1066
|
+
# 2. stuck-owner supersession - once A's claim and the beacon age past grace
|
|
1067
|
+
# while A is still alive arming, firing C takes the next generation and
|
|
1068
|
+
# translates its own close (exit 2);
|
|
1069
|
+
# 3. no double-translation - when A's arm finally returns, A finds itself
|
|
1070
|
+
# superseded and goes completely silent (exit 0, no banner, no ledger
|
|
1071
|
+
# write), so one supersession episode produces exactly one translation.
|
|
1072
|
+
test_superseded_owner_goes_silent_and_never_double_translates() {
|
|
1073
|
+
local dir a_out a_pid b_out b_status c_out c_status a_status i count
|
|
1074
|
+
dir=$(make_primary_dir "$TMP_ROOT/v2-superseded-silence")
|
|
1075
|
+
: > "$dir/state/task1.meta"
|
|
1076
|
+
write_arm_fixture "$dir" blocking-actionable
|
|
1077
|
+
a_out="$dir/state/a.out"
|
|
1078
|
+
run_autoarm_bg "$dir" "$a_out"
|
|
1079
|
+
a_pid=$RUN_AUTOARM_BG_PID
|
|
1080
|
+
i=0
|
|
1081
|
+
while [ "$(epoch_outcome "$dir")" != arming ] || [ ! -e "$dir/state/arm-ran" ]; do
|
|
1082
|
+
[ "$i" -lt 50 ] || fail "owner A never published its arming claim"
|
|
1083
|
+
sleep 0.1
|
|
1084
|
+
i=$((i + 1))
|
|
1085
|
+
done
|
|
1086
|
+
b_out=$(run_autoarm "$dir" 2>/dev/null); b_status=$?
|
|
1087
|
+
expect_code 0 "$b_status" "a firing during a live open claim must defer promptly (no mutex is held across arming)"
|
|
1088
|
+
[ -z "$b_out" ] || fail "deferring firing produced output: $b_out"
|
|
1089
|
+
count=$(wc -l < "$dir/state/arm-ran" | tr -d ' ')
|
|
1090
|
+
[ "$count" -eq 1 ] || fail "deferring firing must not arm, saw $count arms"
|
|
1091
|
+
# A is still alive mid-arm; make its claim stuck-shaped.
|
|
1092
|
+
kill -0 "$a_pid" 2>/dev/null || fail "owner A finished before the supersession could be exercised"
|
|
1093
|
+
touch -t 202001010000 "$dir/state/.claude-autoarm-epoch"
|
|
1094
|
+
touch -t 202001010000 "$dir/state/.last-watcher-beat"
|
|
1095
|
+
c_out=$(run_autoarm "$dir" 2>/dev/null); c_status=$?
|
|
1096
|
+
expect_code 2 "$c_status" "the superseding generation must translate its own close"
|
|
1097
|
+
assert_contains "$c_out" "firstmate watcher wake" "the superseding generation must carry the rewake banner"
|
|
1098
|
+
wait "$a_pid"
|
|
1099
|
+
a_status=$?
|
|
1100
|
+
expect_code 0 "$a_status" "the superseded owner must exit 0 instead of double-translating"
|
|
1101
|
+
[ ! -s "$a_out" ] || fail "the superseded owner emitted output after losing its generation: $(cat "$a_out")"
|
|
1102
|
+
[ "$(epoch_field "$dir" epoch)" = 2 ] || fail "the superseded owner advanced the ledger past its successor: $(epoch_field "$dir" epoch)"
|
|
1103
|
+
[ "$(epoch_outcome "$dir")" = rewake ] || fail "the superseding generation's outcome was overwritten: $(epoch_outcome "$dir")"
|
|
1104
|
+
count=$(wc -l < "$dir/state/arm-ran" | tr -d ' ')
|
|
1105
|
+
[ "$count" -eq 2 ] || fail "expected exactly the owner and superseder arms, saw $count"
|
|
1106
|
+
pass "auto-arm: a superseded owner goes silent - one supersession episode, one translation, no held mutex"
|
|
1107
|
+
}
|
|
1108
|
+
|
|
1109
|
+
test_need_vanished_mid_cycle_closes_quietly() {
|
|
1110
|
+
local dir out status
|
|
1111
|
+
dir=$(make_primary_dir "$TMP_ROOT/vanished")
|
|
1112
|
+
: > "$dir/state/task.meta"
|
|
1113
|
+
write_arm_fixture "$dir" meta-vanishes
|
|
1114
|
+
out=$(run_autoarm "$dir" 2>/dev/null); status=$?
|
|
1115
|
+
expect_code 0 "$status" "an actionable close after the fleet went idle must not rewake"
|
|
1116
|
+
[ -z "$out" ] || fail "vanished-need close produced output: $out"
|
|
1117
|
+
[ "$(epoch_outcome "$dir")" = clean ] || fail "epoch must record outcome=clean, got: $(epoch_outcome "$dir")"
|
|
1118
|
+
pass "auto-arm: need vanishing mid-cycle closes without a rewake"
|
|
1119
|
+
}
|
|
1120
|
+
|
|
1121
|
+
test_afk_mid_cycle_suppresses_rewake() {
|
|
1122
|
+
local dir out status
|
|
1123
|
+
dir=$(make_primary_dir "$TMP_ROOT/afk-mid")
|
|
1124
|
+
: > "$dir/state/task.meta"
|
|
1125
|
+
write_arm_fixture "$dir" afk-appears
|
|
1126
|
+
out=$(run_autoarm "$dir" 2>/dev/null); status=$?
|
|
1127
|
+
expect_code 0 "$status" "AFK appearing mid-cycle must suppress the primary rewake"
|
|
1128
|
+
[ -z "$out" ] || fail "AFK-suppressed close produced output: $out"
|
|
1129
|
+
[ "$(epoch_outcome "$dir")" = afk ] || fail "epoch must record outcome=afk, got: $(epoch_outcome "$dir")"
|
|
1130
|
+
pass "auto-arm: mid-cycle AFK hands triage to the daemon with no rewake"
|
|
1131
|
+
}
|
|
1132
|
+
|
|
1133
|
+
test_active_in_marked_secondmate_home() {
|
|
1134
|
+
local dir out status
|
|
1135
|
+
dir=$(make_secondmate_dir "$TMP_ROOT/secondmate")
|
|
1136
|
+
: > "$dir/state/task.meta"
|
|
1137
|
+
write_arm_fixture "$dir" actionable
|
|
1138
|
+
out=$(run_autoarm "$dir" 2>/dev/null); status=$?
|
|
1139
|
+
expect_code 2 "$status" "a marked secondmate home must get the same active auto-arm as the main primary"
|
|
1140
|
+
[ -e "$dir/state/arm-ran" ] || fail "hook did not arm in a marked secondmate home"
|
|
1141
|
+
[ "$(epoch_outcome "$dir")" = rewake ] || fail "secondmate epoch must record outcome=rewake"
|
|
1142
|
+
pass "auto-arm: active in a marked secondmate home"
|
|
1143
|
+
}
|
|
1144
|
+
|
|
1145
|
+
test_fm_lock_status_still_works_with_shared_lib() {
|
|
1146
|
+
local out
|
|
1147
|
+
out=$(FM_HOME="$TMP_ROOT/lock-status-home" bash "$ROOT/bin/fm-lock.sh" status 2>&1)
|
|
1148
|
+
assert_contains "$out" "lock: free" "fm-lock.sh status must keep working after the session-lock lib extraction"
|
|
1149
|
+
pass "fm-lock: shared session-lock lib preserves the status path"
|
|
1150
|
+
}
|
|
1151
|
+
|
|
1152
|
+
test_inert_in_child_worktree
|
|
1153
|
+
test_inert_without_session_lock
|
|
1154
|
+
test_reclaims_stale_session_lock_before_arming
|
|
1155
|
+
test_inert_when_lock_held_by_other_harness
|
|
1156
|
+
test_inert_when_afk
|
|
1157
|
+
test_stale_lock_recovery_preserves_afk_and_need_gates
|
|
1158
|
+
test_resolves_outermost_claude_pid_in_nested_bgspare_chain
|
|
1159
|
+
test_inert_when_fleet_idle
|
|
1160
|
+
test_actionable_close_rewakes_with_reason
|
|
1161
|
+
test_actionable_close_with_live_successor_rewakes_once
|
|
1162
|
+
test_failed_close_rewakes_with_failure_banner
|
|
1163
|
+
test_failed_cycles_notify_once_and_keep_retrying
|
|
1164
|
+
test_failure_notice_marker_write_refuses_delivery_and_retries
|
|
1165
|
+
test_unverified_clean_close_exhausts_retries
|
|
1166
|
+
test_post_alarm_actionable_close_is_suppressed
|
|
1167
|
+
test_benign_cycle_end_with_live_watcher_is_silent
|
|
1168
|
+
test_positive_recovery_budget_contention_preserves_episode
|
|
1169
|
+
test_owner_mutex_contention_preserves_failure_episode_reset
|
|
1170
|
+
test_arms_for_x_mode_poll_need_without_inflight
|
|
1171
|
+
test_single_flight_admits_exactly_one_owner
|
|
1172
|
+
test_abandoned_owner_claim_is_reclaimed_and_rearms
|
|
1173
|
+
test_arming_claim_with_fresh_beacon_is_never_reclaimed
|
|
1174
|
+
test_fresh_arming_claim_with_stale_beacon_is_never_reclaimed
|
|
1175
|
+
test_claim_not_named_by_the_ledger_is_never_reclaimed
|
|
1176
|
+
test_pid_reused_arming_claim_is_reclaimed_and_rearms
|
|
1177
|
+
test_pid_reused_claim_with_no_ledger_is_reclaimed_and_rearms
|
|
1178
|
+
test_identity_matched_arming_claim_is_never_reclaimed
|
|
1179
|
+
test_terminal_check_claim_is_never_reclaimed
|
|
1180
|
+
test_stuck_live_legacy_owner_is_retired_and_reclaimed
|
|
1181
|
+
test_stopped_legacy_owner_is_reclaimed_with_term_pending
|
|
1182
|
+
test_open_generation_claim_defers_without_any_lock
|
|
1183
|
+
test_stuck_generation_claim_is_superseded_and_rearms
|
|
1184
|
+
test_identityless_ledger_never_defers
|
|
1185
|
+
test_superseded_owner_never_reinvokes_the_arm
|
|
1186
|
+
test_superseded_owner_goes_silent_and_never_double_translates
|
|
1187
|
+
test_need_vanished_mid_cycle_closes_quietly
|
|
1188
|
+
test_afk_mid_cycle_suppresses_rewake
|
|
1189
|
+
test_active_in_marked_secondmate_home
|
|
1190
|
+
test_fm_lock_status_still_works_with_shared_lib
|