@zerwiz/ymir 0.1.4 → 0.1.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.agents/README.md +291 -0
- package/.agents/agents/bragi-marketer.md +111 -0
- package/.agents/agents/brokk.md +352 -0
- package/.agents/agents/forseti-reviewer.md +93 -0
- package/.agents/agents/frigg-consent.md +77 -0
- package/.agents/agents/groa-update.md +77 -0
- package/.agents/agents/hnoss-designer.md +69 -0
- package/.agents/agents/huginn-researcher.md +99 -0
- package/.agents/agents/jord-projects.md +78 -0
- package/.agents/agents/kvasir-scout.md +91 -0
- package/.agents/agents/mimir-planner.md +92 -0
- package/.agents/agents/muninn-stow.md +77 -0
- package/.agents/agents/saga-bearings.md +78 -0
- package/.agents/agents/sindri-developer.md +114 -0
- package/.agents/agents/snotra-documenter.md +105 -0
- package/.agents/agents/syn-recovery.md +78 -0
- package/.agents/agents/tyr-check.md +78 -0
- package/.agents/agents/urdh-hold.md +77 -0
- package/.agents/agents/volundr-smith.md +80 -0
- package/.agents/agents/vor-diagnostics.md +77 -0
- package/.agents/assets/agents/naming.md +50 -0
- package/.agents/assets/agents/registry.md +93 -0
- package/.agents/assets/agents/runtime.md +36 -0
- package/.agents/assets/agents/toon-tasks-cli.md +42 -0
- package/.agents/assets/templates/company_entity.template.md +37 -0
- package/.agents/backend/backends/cmux.sh +649 -0
- package/.agents/backend/backends/herdr-eventwait.py +157 -0
- package/.agents/backend/backends/herdr-workspace-move.py +114 -0
- package/.agents/backend/backends/herdr.sh +3386 -0
- package/.agents/backend/backends/orca.sh +290 -0
- package/.agents/backend/backends/tmux.sh +357 -0
- package/.agents/backend/backends/zellij.sh +700 -0
- package/.agents/backend/fm-afk-launch.sh +654 -0
- package/.agents/backend/fm-afk-return.sh +241 -0
- package/.agents/backend/fm-afk-start.sh +172 -0
- package/.agents/backend/fm-arm-command-policy.mjs +960 -0
- package/.agents/backend/fm-arm-pretool-check.sh +200 -0
- package/.agents/backend/fm-backend-hometag-lib.sh +52 -0
- package/.agents/backend/fm-backend.sh +981 -0
- package/.agents/backend/fm-backlog-handoff.sh +909 -0
- package/.agents/backend/fm-backlog-receive.sh +187 -0
- package/.agents/backend/fm-backlog-transition-lib.sh +779 -0
- package/.agents/backend/fm-bearings-board.sh +204 -0
- package/.agents/backend/fm-bearings-snapshot.sh +546 -0
- package/.agents/backend/fm-bootstrap.sh +1568 -0
- package/.agents/backend/fm-branch-outcome.sh +203 -0
- package/.agents/backend/fm-branch-prompt.sh +101 -0
- package/.agents/backend/fm-brief.sh +452 -0
- package/.agents/backend/fm-busy-event.sh +234 -0
- package/.agents/backend/fm-busy-lib.sh +986 -0
- package/.agents/backend/fm-captain-hold.sh +1001 -0
- package/.agents/backend/fm-cd-command-policy.mjs +153 -0
- package/.agents/backend/fm-cd-pretool-check.sh +190 -0
- package/.agents/backend/fm-check-lib.sh +74 -0
- package/.agents/backend/fm-check-register.sh +42 -0
- package/.agents/backend/fm-check-unregister.sh +52 -0
- package/.agents/backend/fm-classify-lib.sh +1770 -0
- package/.agents/backend/fm-claude-stop-autoarm.sh +334 -0
- package/.agents/backend/fm-composer-lib.sh +1416 -0
- package/.agents/backend/fm-config-inherit-lib.sh +1202 -0
- package/.agents/backend/fm-config-push.sh +254 -0
- package/.agents/backend/fm-control-lib.sh +251 -0
- package/.agents/backend/fm-control.sh +878 -0
- package/.agents/backend/fm-crew-state.sh +625 -0
- package/.agents/backend/fm-cursor-lib.sh +243 -0
- package/.agents/backend/fm-decision-hold.sh +232 -0
- package/.agents/backend/fm-doc-audience-check.sh +269 -0
- package/.agents/backend/fm-dod-lib.sh +67 -0
- package/.agents/backend/fm-ensure-agents-md.sh +253 -0
- package/.agents/backend/fm-extension-launch-barrier.mjs +129 -0
- package/.agents/backend/fm-extension.mjs +2577 -0
- package/.agents/backend/fm-extension.sh +16 -0
- package/.agents/backend/fm-ff-lib.sh +420 -0
- package/.agents/backend/fm-fleet-snapshot.sh +1463 -0
- package/.agents/backend/fm-fleet-sync.sh +458 -0
- package/.agents/backend/fm-fleet-view.sh +96 -0
- package/.agents/backend/fm-gate-refuse-lib.sh +102 -0
- package/.agents/backend/fm-guard.sh +242 -0
- package/.agents/backend/fm-harness.sh +195 -0
- package/.agents/backend/fm-herdr-ci-cleanup.sh +115 -0
- package/.agents/backend/fm-herdr-lab.sh +346 -0
- package/.agents/backend/fm-herdr-session-cleanup.sh +337 -0
- package/.agents/backend/fm-home-seed.sh +984 -0
- package/.agents/backend/fm-home-summary-refresh.sh +255 -0
- package/.agents/backend/fm-hook-host-lib.sh +36 -0
- package/.agents/backend/fm-inactive-reconcile.sh +531 -0
- package/.agents/backend/fm-inbox.sh +399 -0
- package/.agents/backend/fm-install-actionlint.sh +84 -0
- package/.agents/backend/fm-install-herdr.sh +96 -0
- package/.agents/backend/fm-install-shellcheck.sh +84 -0
- package/.agents/backend/fm-install-treehouse.sh +95 -0
- package/.agents/backend/fm-kimi-turnend-hook.sh +276 -0
- package/.agents/backend/fm-lease-lib.sh +218 -0
- package/.agents/backend/fm-lease.sh +189 -0
- package/.agents/backend/fm-line-cap-lib.sh +51 -0
- package/.agents/backend/fm-lint-workflows.sh +137 -0
- package/.agents/backend/fm-lint.sh +591 -0
- package/.agents/backend/fm-lock-lib.sh +104 -0
- package/.agents/backend/fm-lock.sh +107 -0
- package/.agents/backend/fm-marker-lib.sh +12 -0
- package/.agents/backend/fm-merge-local.sh +74 -0
- package/.agents/backend/fm-merge-outcome-lib.sh +138 -0
- package/.agents/backend/fm-nm-run-lib.sh +124 -0
- package/.agents/backend/fm-on.sh +125 -0
- package/.agents/backend/fm-operational-input.sh +253 -0
- package/.agents/backend/fm-peek.sh +46 -0
- package/.agents/backend/fm-pending-reply-lib.sh +1409 -0
- package/.agents/backend/fm-pr-check.sh +135 -0
- package/.agents/backend/fm-pr-lib.sh +1016 -0
- package/.agents/backend/fm-pr-merge.sh +707 -0
- package/.agents/backend/fm-pr-poll.sh +110 -0
- package/.agents/backend/fm-primary-scope-lib.sh +33 -0
- package/.agents/backend/fm-procevent-extension-capture.pl +259 -0
- package/.agents/backend/fm-procevent-lavish.sh +628 -0
- package/.agents/backend/fm-procevent-lib.sh +892 -0
- package/.agents/backend/fm-procevent-quota.sh +290 -0
- package/.agents/backend/fm-procevent-remote-reply.sh +577 -0
- package/.agents/backend/fm-procevent-when.sh +504 -0
- package/.agents/backend/fm-procevent.sh +1466 -0
- package/.agents/backend/fm-project-mode.sh +92 -0
- package/.agents/backend/fm-project-origin-lib.sh +180 -0
- package/.agents/backend/fm-promote.sh +282 -0
- package/.agents/backend/fm-public-followup-emit.sh +260 -0
- package/.agents/backend/fm-public-followup-lib.sh +398 -0
- package/.agents/backend/fm-public-followup.sh +1330 -0
- package/.agents/backend/fm-push-transition-lib.sh +168 -0
- package/.agents/backend/fm-quota-axi-lib.sh +93 -0
- package/.agents/backend/fm-quota-choose.sh +384 -0
- package/.agents/backend/fm-remote-delta-read.sh +189 -0
- package/.agents/backend/fm-remote-doctor.sh +800 -0
- package/.agents/backend/fm-remote-entrypoint.sh +193 -0
- package/.agents/backend/fm-remote-file.sh +234 -0
- package/.agents/backend/fm-remote-home-provision.sh +259 -0
- package/.agents/backend/fm-remote-home-seed.sh +259 -0
- package/.agents/backend/fm-remote-inherit-push.sh +90 -0
- package/.agents/backend/fm-remote-inherit.sh +179 -0
- package/.agents/backend/fm-remote-job-lib.sh +1241 -0
- package/.agents/backend/fm-remote-job-reap-orphans.sh +137 -0
- package/.agents/backend/fm-remote-job-worker.sh +1124 -0
- package/.agents/backend/fm-remote-readiness-lib.sh +44 -0
- package/.agents/backend/fm-remote-secondmate-control.sh +340 -0
- package/.agents/backend/fm-review-diff.sh +158 -0
- package/.agents/backend/fm-secondmate-charter-lib.sh +44 -0
- package/.agents/backend/fm-secondmate-nudge-lib.sh +68 -0
- package/.agents/backend/fm-secondmate-parent-lib.sh +70 -0
- package/.agents/backend/fm-secondmate-reconcile.sh +371 -0
- package/.agents/backend/fm-secondmate-registry-lib.sh +311 -0
- package/.agents/backend/fm-secondmate-report.sh +88 -0
- package/.agents/backend/fm-send.sh +1059 -0
- package/.agents/backend/fm-session-lock-lib.sh +176 -0
- package/.agents/backend/fm-session-start.sh +970 -0
- package/.agents/backend/fm-sessionstart-cursor.sh +40 -0
- package/.agents/backend/fm-sessionstart-nudge.sh +45 -0
- package/.agents/backend/fm-sessionstart-run.sh +145 -0
- package/.agents/backend/fm-spawn.sh +3153 -0
- package/.agents/backend/fm-startup-memory-budget-lib.sh +224 -0
- package/.agents/backend/fm-startup-memory-budget.sh +94 -0
- package/.agents/backend/fm-startup-network.sh +642 -0
- package/.agents/backend/fm-stow-cascade.sh +251 -0
- package/.agents/backend/fm-subagent-pretool-check.sh +207 -0
- package/.agents/backend/fm-supervise-daemon.sh +1732 -0
- package/.agents/backend/fm-supervision-instructions.sh +215 -0
- package/.agents/backend/fm-supervision-lib.sh +90 -0
- package/.agents/backend/fm-supervisor-target-lib.sh +78 -0
- package/.agents/backend/fm-tangle-lib.sh +53 -0
- package/.agents/backend/fm-task-inbox-lib.sh +402 -0
- package/.agents/backend/fm-tasks-axi-lib.sh +123 -0
- package/.agents/backend/fm-teardown.sh +2922 -0
- package/.agents/backend/fm-test-isolation-proof.sh +588 -0
- package/.agents/backend/fm-test-run.sh +2191 -0
- package/.agents/backend/fm-timeout-lib.sh +141 -0
- package/.agents/backend/fm-timing-lib.sh +179 -0
- package/.agents/backend/fm-tmux-lib.sh +291 -0
- package/.agents/backend/fm-tool-update-check.sh +898 -0
- package/.agents/backend/fm-trace-context-lib.sh +227 -0
- package/.agents/backend/fm-transition-lib.sh +103 -0
- package/.agents/backend/fm-turnend-guard-cursor.sh +391 -0
- package/.agents/backend/fm-turnend-guard-grok.sh +90 -0
- package/.agents/backend/fm-turnend-guard.sh +432 -0
- package/.agents/backend/fm-update.sh +110 -0
- package/.agents/backend/fm-vendor-auth-probe.sh +190 -0
- package/.agents/backend/fm-voice-client.py +1373 -0
- package/.agents/backend/fm-voice-relay.py +1256 -0
- package/.agents/backend/fm-wake-drain.sh +592 -0
- package/.agents/backend/fm-wake-grant.sh +127 -0
- package/.agents/backend/fm-wake-lib.sh +1849 -0
- package/.agents/backend/fm-watch-arm.sh +602 -0
- package/.agents/backend/fm-watch-checkpoint.sh +109 -0
- package/.agents/backend/fm-watch.sh +1962 -0
- package/.agents/backend/fm-x-dismiss.sh +116 -0
- package/.agents/backend/fm-x-followup.sh +286 -0
- package/.agents/backend/fm-x-lib.sh +1000 -0
- package/.agents/backend/fm-x-link.sh +234 -0
- package/.agents/backend/fm-x-poll.sh +185 -0
- package/.agents/backend/fm-x-reply.sh +385 -0
- package/.agents/backend/fm_voice_frame.py +166 -0
- package/.agents/backend/fm_voice_records.py +574 -0
- package/.agents/backend/model-bridge.py +290 -0
- package/.agents/backend/opencode-go-bridge.py +30 -0
- package/.agents/bus/README.md +12 -0
- package/.agents/bus/messages.json +1 -0
- package/.agents/config/agents.machine.example.yaml +16 -0
- package/.agents/config/agents.yaml +50 -0
- package/.agents/config/agents.yaml.example +72 -0
- package/.agents/config/crew-dispatch.json +22 -0
- package/.agents/config/cron.yaml +9 -0
- package/.agents/config/eindri-dispatch.json +67 -0
- package/.agents/config/eindri-harness +1 -0
- package/.agents/config/startup-memory-budget +1 -0
- package/.agents/config/tailscale-sync.yaml +27 -0
- package/.agents/filebrowser/.filebrowser.json +6 -0
- package/.agents/filebrowser/README.md +11 -0
- package/.agents/gateway/README.md +12 -0
- package/.agents/github/README.md +15 -0
- package/.agents/github/webhooks/.gitkeep +0 -0
- package/.agents/github/workflows/.gitkeep +0 -0
- package/.agents/harness/opencode/plugins/lib/rodd-operational-input.js +37 -0
- package/.agents/harness/opencode/plugins/package.json +4 -0
- package/.agents/harness/opencode/plugins/saga-sessionstart.js +77 -0
- package/.agents/harness/opencode/plugins/syn-cd-check.js +61 -0
- package/.agents/harness/opencode/plugins/syn-pretool-check.js +68 -0
- package/.agents/harness/opencode/plugins/syn-turnend-guard.js +105 -0
- package/.agents/harness/opencode/plugins/syn-watch-arm.js +521 -0
- package/.agents/memory/README.md +46 -0
- package/.agents/memory/kaia.engram +0 -0
- package/.agents/memory/kaia.engram-shm +0 -0
- package/.agents/memory/kaia.engram-wal +0 -0
- package/.agents/memory/runes_audit.md +5 -0
- package/.agents/memory/well/episodes.jsonl +367 -0
- package/.agents/memory/well/workspace.jsonl +27 -0
- package/.agents/migrations/0001-hodd-layout.sh +38 -0
- package/.agents/migrations/0002-a2a-mcp.sh +27 -0
- package/.agents/migrations/0003-private-data-separation.sh +111 -0
- package/.agents/migrations/0004-hoard-and-realms.sh +104 -0
- package/.agents/sandbox/Dockerfile.utgard +19 -0
- package/.agents/sandbox/README.md +14 -0
- package/.agents/sandbox/utgard.config.json +14 -0
- package/.agents/skills/README.md +59 -0
- package/.agents/skills/bragi-marketing/README.md +3 -0
- package/.agents/skills/bragi-marketing/SKILL.md +82 -0
- package/.agents/skills/eindri-homes/README.md +3 -0
- package/.agents/skills/eindri-homes/SKILL.md +291 -0
- package/.agents/skills/eindri-homes/assets/control-plane.md +122 -0
- package/.agents/skills/eindri-homes/assets/dispatch.md +80 -0
- package/.agents/skills/frigg-consent/README.md +3 -0
- package/.agents/skills/frigg-consent/SKILL.md +56 -0
- package/.agents/skills/galdr-ymirsystem/README.md +3 -0
- package/.agents/skills/galdr-ymirsystem/SKILL.md +158 -0
- package/.agents/skills/galdr-ymirsystem/assets/README.md +74 -0
- package/.agents/skills/galdr-ymirsystem/assets/brokk-distro-runtime.md +390 -0
- package/.agents/skills/galdr-ymirsystem/assets/build-method.md +113 -0
- package/.agents/skills/galdr-ymirsystem/assets/build-tool-categories.md +178 -0
- package/.agents/skills/galdr-ymirsystem/assets/eindri-orchestration.md +518 -0
- package/.agents/skills/galdr-ymirsystem/assets/eindri-profiles.md +136 -0
- package/.agents/skills/galdr-ymirsystem/assets/harness-integration/README.md +673 -0
- package/.agents/skills/galdr-ymirsystem/assets/harness-integration/claude-code.md +197 -0
- package/.agents/skills/galdr-ymirsystem/assets/harness-integration/codex.md +191 -0
- package/.agents/skills/galdr-ymirsystem/assets/harness-integration/cursor.md +193 -0
- package/.agents/skills/galdr-ymirsystem/assets/harness-integration/opencode.md +262 -0
- package/.agents/skills/galdr-ymirsystem/assets/harness-integration/pi.md +295 -0
- package/.agents/skills/galdr-ymirsystem/assets/hlidskjalf-ui.md +496 -0
- package/.agents/skills/galdr-ymirsystem/assets/installation.md +571 -0
- package/.agents/skills/galdr-ymirsystem/assets/local-models/HOST-DATA.example.md +72 -0
- package/.agents/skills/galdr-ymirsystem/assets/local-models.md +274 -0
- package/.agents/skills/galdr-ymirsystem/assets/memory-well.md +161 -0
- package/.agents/skills/galdr-ymirsystem/assets/nornir-jobs.md +377 -0
- package/.agents/skills/galdr-ymirsystem/assets/norse-naming.md +325 -0
- package/.agents/skills/galdr-ymirsystem/assets/odrerir-hall.md +129 -0
- package/.agents/skills/galdr-ymirsystem/assets/pi-boot/einherjar-spawn.schema.json +81 -0
- package/.agents/skills/galdr-ymirsystem/assets/pi-boot/herdr-profile.toml +75 -0
- package/.agents/skills/galdr-ymirsystem/assets/pi-boot/pi-profile.yml +96 -0
- package/.agents/skills/galdr-ymirsystem/assets/pi-boot/supervision-tree.yml +163 -0
- package/.agents/skills/galdr-ymirsystem/assets/pi-boot-guide.md +58 -0
- package/.agents/skills/galdr-ymirsystem/assets/porting-upstream-to-norse.md +301 -0
- package/.agents/skills/galdr-ymirsystem/assets/principles.md +385 -0
- package/.agents/skills/galdr-ymirsystem/assets/reference-adoption.md +99 -0
- package/.agents/skills/galdr-ymirsystem/assets/registry.md +379 -0
- package/.agents/skills/galdr-ymirsystem/assets/runtime-compliance.md +463 -0
- package/.agents/skills/galdr-ymirsystem/assets/runtime-components.md +553 -0
- package/.agents/skills/galdr-ymirsystem/assets/smidja.md +175 -0
- package/.agents/skills/galdr-ymirsystem/schemas/toon-schemas.md +190 -0
- package/.agents/skills/galdr-ymirsystem/scripts/README.md +45 -0
- package/.agents/skills/galdr-ymirsystem/scripts/bench-one.sh +248 -0
- package/.agents/skills/galdr-ymirsystem/scripts/compliance-check.sh +360 -0
- package/.agents/skills/galdr-ymirsystem/scripts/gpu-sample.sh +43 -0
- package/.agents/skills/galdr-ymirsystem/scripts/stop-all.sh +34 -0
- package/.agents/skills/galdr-ymirsystem/scripts/toon-check.py +188 -0
- package/.agents/skills/gjallarhorn-relay/README.md +3 -0
- package/.agents/skills/gjallarhorn-relay/SKILL.md +283 -0
- package/.agents/skills/groa-update/SKILL.md +70 -0
- package/.agents/skills/hamr-adapters/README.md +3 -0
- package/.agents/skills/hamr-adapters/SKILL.md +123 -0
- package/.agents/skills/hamr-adapters/references/common/control-and-recovery.md +37 -0
- package/.agents/skills/hamr-adapters/references/common/dispatch.md +32 -0
- package/.agents/skills/hamr-adapters/references/common/model-and-effort.md +42 -0
- package/.agents/skills/hamr-adapters/references/common/primary-hooks.md +40 -0
- package/.agents/skills/hamr-adapters/references/harness/claude.md +55 -0
- package/.agents/skills/hamr-adapters/references/harness/codex.md +43 -0
- package/.agents/skills/hamr-adapters/references/harness/cursor.md +75 -0
- package/.agents/skills/hamr-adapters/references/harness/grok.md +69 -0
- package/.agents/skills/hamr-adapters/references/harness/kimi.md +51 -0
- package/.agents/skills/hamr-adapters/references/harness/muse.md +70 -0
- package/.agents/skills/hamr-adapters/references/harness/opencode.md +42 -0
- package/.agents/skills/hamr-adapters/references/harness/pi.md +56 -0
- package/.agents/skills/herdr-panes/README.md +3 -0
- package/.agents/skills/herdr-panes/SKILL.md +92 -0
- package/.agents/skills/herdr-panes/assets/herdr-backend.md +346 -0
- package/.agents/skills/herdr-panes/assets/tmux-backend.md +115 -0
- package/.agents/skills/hnoss-design/DESIGN.md +19 -0
- package/.agents/skills/hnoss-design/README.md +3 -0
- package/.agents/skills/hnoss-design/SKILL.md +83 -0
- package/.agents/skills/hvild-afk/README.md +3 -0
- package/.agents/skills/hvild-afk/SKILL.md +241 -0
- package/.agents/skills/jord-projects/README.md +3 -0
- package/.agents/skills/jord-projects/SKILL.md +92 -0
- package/.agents/skills/lifecycle/SKILL.md +56 -0
- package/.agents/skills/lifecycle/smoke_test.sh +52 -0
- package/.agents/skills/lifecycle/start.sh +21 -0
- package/.agents/skills/lifecycle/status.sh +35 -0
- package/.agents/skills/lifecycle/stop.sh +16 -0
- package/.agents/skills/muninn-stow/README.md +3 -0
- package/.agents/skills/muninn-stow/SKILL.md +309 -0
- package/.agents/skills/no-mistakes/SKILL.md +379 -0
- package/.agents/skills/nornir-schedule/README.md +3 -0
- package/.agents/skills/nornir-schedule/SKILL.md +25 -0
- package/.agents/skills/nornir-schedule/assets/events.md +137 -0
- package/.agents/skills/nornir-schedule/assets/quota.md +131 -0
- package/.agents/skills/nsr-compliance/README.md +3 -0
- package/.agents/skills/nsr-compliance/SKILL.md +29 -0
- package/.agents/skills/nsr-compliance/assets/nsr/README.md +40 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/RULES/README.md +15 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/RULES/git-workflow.md +18 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/RULES/security.md +22 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/RULES/tech-stack.md +8 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/SKILL.md +71 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/config/core_four.yaml +16 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/gates/check_danger.sh +102 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/gates/check_wiring.sh +69 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/gates/validate_code.sh +29 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/gates/verify_docs.py +66 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/generate.sh +109 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/harness/envelopes/result_envelope.json +14 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/harness/envelopes/task_envelope.json +20 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/harness/runner.py +303 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/telemetry/logger.py +112 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/templates/compliance/README.md +18 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/templates/compliance/config/README.md +10 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/templates/compliance/gates/README.md +12 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/templates/compliance/gates/check_env.sh +23 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/templates/compliance/gates/check_paths.sh +20 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/templates/compliance/gates/check_platform.sh +23 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/templates/compliance/harness/README.md +11 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/templates/compliance/harness/envelopes/README.md +10 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/templates/compliance/installer/README.md +10 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/templates/compliance/installer/install.sh +125 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/templates/compliance/telemetry/README.md +11 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/README.md +20 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/features/README.md +36 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/features/SKILL-template.md +14 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/git_ops/README.md +25 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/git_ops/SKILL.md +13 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/git_ops/create_branch.sh +6 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/git_ops/safe_commit.sh +9 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/git_ops/sync_upstream.sh +6 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/lifecycle/README.md +27 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/lifecycle/SKILL.md +15 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/lifecycle/smoke_test.sh +12 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/lifecycle/start.sh +19 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/lifecycle/status.sh +12 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/lifecycle/stop.sh +15 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/README.md +18 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/config/README.md +10 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/config/core_four.yaml +13 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/gates/README.md +12 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/gates/check_danger.sh +102 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/gates/check_env.sh +23 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/gates/check_paths.sh +20 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/gates/check_platform.sh +23 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/gates/check_wiring.sh +69 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/harness/README.md +11 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/harness/envelopes/README.md +10 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/harness/envelopes/result_envelope.json +14 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/harness/envelopes/task_envelope.json +20 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/harness/runner.py +303 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/installer/README.md +10 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/installer/install.sh +125 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/telemetry/README.md +11 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/telemetry/logger.py +112 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/BEST_PRACTICES/README.md +14 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/BEST_PRACTICES/api.md +17 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/BEST_PRACTICES/compliance-wiring.md +155 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/BEST_PRACTICES/frontend.md +15 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/CI_CD/README.md +11 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/CI_CD/deployment/README.md +23 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/CI_CD/deployment/clients/README.md +16 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/CI_CD/deployment/clients/template-README.md +11 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/CI_CD/deployment/deploy.sh +49 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/CI_CD/deployment/envs/README.md +12 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/CI_CD/deployment/envs/development.env.example +17 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/CI_CD/deployment/envs/production.env.example +17 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/CI_CD/deployment/envs/staging.env.example +17 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/CI_CD/deployment/tenants/README.md +11 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/CI_CD/deployment/tenants/template.env.example +8 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/DEVELOPER_SETUP/README.md +26 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/DEVELOPER_SETUP/agent.md +7 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/DEVELOPER_SETUP/developers/dev-1.md +20 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/DEVELOPER_SETUP/linux.md +9 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/DEVELOPER_SETUP/macos.md +10 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/DEVELOPER_SETUP/shared.md +19 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/DEVELOPER_SETUP/windows.md +14 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/HOSTING/README.md +25 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/HOSTING/_template.md +41 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/HOSTING/hosting-1.md +41 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/README.md +21 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/RUNBOOK/README.md +15 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/RUNBOOK/deployment.md +19 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/RUNBOOK/disaster-recovery.md +15 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/RUNBOOK/ops-tasks.md +14 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/features/README.md +16 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/project-skills.md +146 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/research/README.md +11 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/research/software-compliance-research.md +269 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/working-with-agents.md +260 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/root/AGENTS.md +38 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/root/ARCHITECTURE.md +36 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/root/BEST_PRACTICES.md +27 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/root/CI_CD.md +23 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/root/FEATURES.md +15 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/root/README.md +22 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/root/RULES.md +37 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/root/STRUCTURE.md +45 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/root/TECH_STACK.md +28 -0
- package/.agents/skills/nsr-compliance/assets/nsr/scaffold-spec.md +63 -0
- package/.agents/skills/nsr-compliance/assets/nsr/scripts/scaffold.sh +103 -0
- package/.agents/skills/nsr-compliance/assets/nsr/scripts/validate.sh +52 -0
- package/.agents/skills/nsr-compliance/assets/nsrcompliance/SKILL.md +71 -0
- package/.agents/skills/nsr-compliance/assets/nsrcompliance/config/core_four.yaml +16 -0
- package/.agents/skills/nsr-compliance/assets/nsrcompliance/gates/check_danger.sh +102 -0
- package/.agents/skills/nsr-compliance/assets/nsrcompliance/gates/check_wiring.sh +69 -0
- package/.agents/skills/nsr-compliance/assets/nsrcompliance/gates/validate_code.sh +29 -0
- package/.agents/skills/nsr-compliance/assets/nsrcompliance/gates/verify_docs.py +66 -0
- package/.agents/skills/nsr-compliance/assets/nsrcompliance/generate.sh +109 -0
- package/.agents/skills/nsr-compliance/assets/nsrcompliance/harness/envelopes/result_envelope.json +14 -0
- package/.agents/skills/nsr-compliance/assets/nsrcompliance/harness/envelopes/task_envelope.json +20 -0
- package/.agents/skills/nsr-compliance/assets/nsrcompliance/harness/runner.py +303 -0
- package/.agents/skills/nsr-compliance/assets/nsrcompliance/telemetry/logger.py +112 -0
- package/.agents/skills/nsr-compliance/assets/nsrcompliance/templates/compliance/README.md +18 -0
- package/.agents/skills/nsr-compliance/assets/nsrcompliance/templates/compliance/config/README.md +10 -0
- package/.agents/skills/nsr-compliance/assets/nsrcompliance/templates/compliance/gates/README.md +12 -0
- package/.agents/skills/nsr-compliance/assets/nsrcompliance/templates/compliance/gates/check_env.sh +23 -0
- package/.agents/skills/nsr-compliance/assets/nsrcompliance/templates/compliance/gates/check_paths.sh +20 -0
- package/.agents/skills/nsr-compliance/assets/nsrcompliance/templates/compliance/gates/check_platform.sh +23 -0
- package/.agents/skills/nsr-compliance/assets/nsrcompliance/templates/compliance/harness/README.md +11 -0
- package/.agents/skills/nsr-compliance/assets/nsrcompliance/templates/compliance/harness/envelopes/README.md +10 -0
- package/.agents/skills/nsr-compliance/assets/nsrcompliance/templates/compliance/installer/README.md +10 -0
- package/.agents/skills/nsr-compliance/assets/nsrcompliance/templates/compliance/installer/install.sh +125 -0
- package/.agents/skills/nsr-compliance/assets/nsrcompliance/templates/compliance/telemetry/README.md +11 -0
- package/.agents/skills/pr-ops/README.md +3 -0
- package/.agents/skills/pr-ops/SKILL.md +109 -0
- package/.agents/skills/ratatoskr-a2a/README.md +3 -0
- package/.agents/skills/ratatoskr-a2a/SKILL.md +148 -0
- package/.agents/skills/rules-check-drift/SKILL.md +81 -0
- package/.agents/skills/saga-bearings/README.md +3 -0
- package/.agents/skills/saga-bearings/SKILL.md +26 -0
- package/.agents/skills/saga-bearings/assets/bearings.md +161 -0
- package/.agents/skills/saga-bearings/assets/board-template.html +731 -0
- package/.agents/skills/saga-bearings/assets/recap.md +56 -0
- package/.agents/skills/syn-recovery/README.md +3 -0
- package/.agents/skills/syn-recovery/SKILL.md +54 -0
- package/.agents/skills/tyr-check/README.md +3 -0
- package/.agents/skills/tyr-check/SKILL.md +218 -0
- package/.agents/skills/urdh-hold/README.md +3 -0
- package/.agents/skills/urdh-hold/SKILL.md +25 -0
- package/.agents/skills/urdh-hold/assets/decisions.md +15 -0
- package/.agents/skills/urdh-hold/assets/hold.md +54 -0
- package/.agents/skills/vor-diagnostics/README.md +3 -0
- package/.agents/skills/vor-diagnostics/SKILL.md +68 -0
- package/.agents/skills/ymir-host/README.md +3 -0
- package/.agents/skills/ymir-host/SKILL.md +34 -0
- package/.agents/skills/ymir-host/assets/desktop.md +36 -0
- package/.agents/skills/ymir-host/assets/omarchy.md +297 -0
- package/.agents/skills/ymir-host/assets/runbooks.md +15 -0
- package/.agents/skills/ymir-host/assets/thjazi.md +220 -0
- package/.agents/skills/ymir-host/assets/update.md +64 -0
- package/.agents/tests/assets/board-render-harness.mjs +117 -0
- package/.agents/tests/cmux-test-safety.sh +51 -0
- package/.agents/tests/fixtures.sh +291 -0
- package/.agents/tests/fm-afk-inject-e2e.test.sh +428 -0
- package/.agents/tests/fm-afk-inject-herdr-e2e.test.sh +536 -0
- package/.agents/tests/fm-afk-launch.test.sh +961 -0
- package/.agents/tests/fm-afk-pi-herdr-return-e2e.test.sh +297 -0
- package/.agents/tests/fm-afk-return.test.sh +283 -0
- package/.agents/tests/fm-arm-pretool-check.test.sh +482 -0
- package/.agents/tests/fm-ask-user-authority.test.sh +42 -0
- package/.agents/tests/fm-backend-autodetect-smoke.test.sh +176 -0
- package/.agents/tests/fm-backend-cmux-smoke.test.sh +188 -0
- package/.agents/tests/fm-backend-cmux.test.sh +1166 -0
- package/.agents/tests/fm-backend-herdr-eventwait-smoke.test.sh +136 -0
- package/.agents/tests/fm-backend-herdr-eventwait.test.py +106 -0
- package/.agents/tests/fm-backend-herdr-focus-flash-e2e.test.sh +409 -0
- package/.agents/tests/fm-backend-herdr-launcher-workspace-e2e.test.sh +437 -0
- package/.agents/tests/fm-backend-herdr-presentation-e2e.test.sh +1424 -0
- package/.agents/tests/fm-backend-herdr-prune-safety-e2e.test.sh +180 -0
- package/.agents/tests/fm-backend-herdr-respawn-idem-e2e.test.sh +182 -0
- package/.agents/tests/fm-backend-herdr-smoke.test.sh +355 -0
- package/.agents/tests/fm-backend-herdr-workspace-per-home-e2e.test.sh +251 -0
- package/.agents/tests/fm-backend-herdr.test.sh +4606 -0
- package/.agents/tests/fm-backend-orca.test.sh +1357 -0
- package/.agents/tests/fm-backend-tmux-smoke.test.sh +173 -0
- package/.agents/tests/fm-backend-zellij-smoke.test.sh +205 -0
- package/.agents/tests/fm-backend-zellij.test.sh +1360 -0
- package/.agents/tests/fm-backend.test.sh +1142 -0
- package/.agents/tests/fm-backlog-atomicity.test.sh +2302 -0
- package/.agents/tests/fm-backlog-handoff.test.sh +1360 -0
- package/.agents/tests/fm-bearings-board-render.test.sh +135 -0
- package/.agents/tests/fm-bearings-board.test.sh +414 -0
- package/.agents/tests/fm-bearings-snapshot.test.sh +1990 -0
- package/.agents/tests/fm-bootstrap-network-parallel.test.sh +316 -0
- package/.agents/tests/fm-bootstrap.test.sh +1178 -0
- package/.agents/tests/fm-branch-supervision.test.sh +552 -0
- package/.agents/tests/fm-brief.test.sh +785 -0
- package/.agents/tests/fm-busy-adapter-wiring.test.sh +324 -0
- package/.agents/tests/fm-busy-state.test.sh +465 -0
- package/.agents/tests/fm-calm-pi-extension.test.sh +3979 -0
- package/.agents/tests/fm-captain-hold-lifecycle.test.sh +1187 -0
- package/.agents/tests/fm-cd-pretool-check.test.sh +400 -0
- package/.agents/tests/fm-check-unregister.test.sh +198 -0
- package/.agents/tests/fm-classify-corr-token.test.sh +543 -0
- package/.agents/tests/fm-classify-decision-key.test.sh +340 -0
- package/.agents/tests/fm-claude-stop-autoarm-live-e2e.test.sh +164 -0
- package/.agents/tests/fm-claude-stop-autoarm.test.sh +1190 -0
- package/.agents/tests/fm-cmux-claude-composer-live-e2e.test.sh +110 -0
- package/.agents/tests/fm-codex-continuity-live-e2e.test.sh +55 -0
- package/.agents/tests/fm-composer-ghost.test.sh +716 -0
- package/.agents/tests/fm-composer-lib.test.sh +666 -0
- package/.agents/tests/fm-composer-matrix-live-e2e.test.sh +220 -0
- package/.agents/tests/fm-control-herdr-smoke.test.sh +149 -0
- package/.agents/tests/fm-control-relaunch.test.sh +1530 -0
- package/.agents/tests/fm-control.test.sh +910 -0
- package/.agents/tests/fm-crew-state.test.sh +1610 -0
- package/.agents/tests/fm-cursor-harness.test.sh +404 -0
- package/.agents/tests/fm-cursor-primary-live-e2e.test.sh +217 -0
- package/.agents/tests/fm-cursor-primary.test.sh +708 -0
- package/.agents/tests/fm-daemon.test.sh +2740 -0
- package/.agents/tests/fm-documentation-audiences.test.sh +141 -0
- package/.agents/tests/fm-ensure-agents-md.test.sh +371 -0
- package/.agents/tests/fm-extension-binding.test.sh +2187 -0
- package/.agents/tests/fm-fleet-snapshot-view.test.sh +816 -0
- package/.agents/tests/fm-fleet-sync.test.sh +721 -0
- package/.agents/tests/fm-gate-refuse.test.sh +356 -0
- package/.agents/tests/fm-gitignore-config.test.sh +89 -0
- package/.agents/tests/fm-gotmp.test.sh +230 -0
- package/.agents/tests/fm-grok-continuity-live-e2e.test.sh +112 -0
- package/.agents/tests/fm-grok-harness.test.sh +117 -0
- package/.agents/tests/fm-grok-stop-live-e2e.test.sh +219 -0
- package/.agents/tests/fm-guard-stale-banner.test.sh +709 -0
- package/.agents/tests/fm-harness-adapter-instructions-live-e2e.test.sh +134 -0
- package/.agents/tests/fm-harness-adapter-references.test.sh +30 -0
- package/.agents/tests/fm-harness-liveness-drift-live-e2e.test.sh +151 -0
- package/.agents/tests/fm-herdr-lab.test.sh +243 -0
- package/.agents/tests/fm-herdr-session-cleanup-e2e.test.sh +146 -0
- package/.agents/tests/fm-herdr-session-cleanup.test.sh +329 -0
- package/.agents/tests/fm-herdr-submit-confirm-live-e2e.test.sh +125 -0
- package/.agents/tests/fm-herdr-version-floor-live-e2e.test.sh +144 -0
- package/.agents/tests/fm-home-summary-refresh.test.sh +971 -0
- package/.agents/tests/fm-inactive-reconcile.test.sh +485 -0
- package/.agents/tests/fm-kimi-harness.test.sh +687 -0
- package/.agents/tests/fm-lint-workflows.test.sh +532 -0
- package/.agents/tests/fm-lint.test.sh +1023 -0
- package/.agents/tests/fm-muse-harness.test.sh +932 -0
- package/.agents/tests/fm-muse-signals-live-e2e.test.sh +205 -0
- package/.agents/tests/fm-no-mistakes-required.test.sh +72 -0
- package/.agents/tests/fm-on.test.sh +507 -0
- package/.agents/tests/fm-opencode-primary-live-e2e.test.sh +357 -0
- package/.agents/tests/fm-operational-input.test.sh +160 -0
- package/.agents/tests/fm-peek-remote.test.sh +110 -0
- package/.agents/tests/fm-pending-reply.test.sh +1288 -0
- package/.agents/tests/fm-pi-branch-extension.test.sh +3182 -0
- package/.agents/tests/fm-pi-branch-live-e2e.test.sh +527 -0
- package/.agents/tests/fm-pi-primary-live-e2e.test.sh +345 -0
- package/.agents/tests/fm-pi-primary-types.test.sh +68 -0
- package/.agents/tests/fm-pi-watch-extension.test.sh +2843 -0
- package/.agents/tests/fm-pr-check-security.test.sh +2140 -0
- package/.agents/tests/fm-pr-merge.test.sh +2135 -0
- package/.agents/tests/fm-procevent-quota.test.sh +225 -0
- package/.agents/tests/fm-procevent-when.test.sh +406 -0
- package/.agents/tests/fm-procevent.test.sh +1745 -0
- package/.agents/tests/fm-project-origin.test.sh +110 -0
- package/.agents/tests/fm-public-followup.test.sh +2272 -0
- package/.agents/tests/fm-quota-array-dispatch-live-e2e.test.sh +526 -0
- package/.agents/tests/fm-quota-choose.test.sh +626 -0
- package/.agents/tests/fm-remote-backlog-handoff.test.sh +453 -0
- package/.agents/tests/fm-remote-doctor.test.sh +625 -0
- package/.agents/tests/fm-remote-entrypoint.test.sh +59 -0
- package/.agents/tests/fm-remote-job-orphan-reap.test.sh +205 -0
- package/.agents/tests/fm-remote-job.test.sh +768 -0
- package/.agents/tests/fm-remote-reply.test.sh +520 -0
- package/.agents/tests/fm-remote-secondmate-lifecycle-e2e.test.sh +1256 -0
- package/.agents/tests/fm-remote-secondmate-parent-binding.test.sh +326 -0
- package/.agents/tests/fm-remote-secondmate-trace-context.test.sh +310 -0
- package/.agents/tests/fm-remote-transport-lanes.test.sh +431 -0
- package/.agents/tests/fm-review-diff.test.sh +176 -0
- package/.agents/tests/fm-secondmate-harness.test.sh +2600 -0
- package/.agents/tests/fm-secondmate-lifecycle-e2e.test.sh +259 -0
- package/.agents/tests/fm-secondmate-liveness.test.sh +559 -0
- package/.agents/tests/fm-secondmate-reconcile.test.sh +732 -0
- package/.agents/tests/fm-secondmate-safety.test.sh +2972 -0
- package/.agents/tests/fm-secondmate-sync.test.sh +913 -0
- package/.agents/tests/fm-send-inbox-doorbell-live-e2e.test.sh +206 -0
- package/.agents/tests/fm-send-inbox.test.sh +352 -0
- package/.agents/tests/fm-send-popup-settle.test.sh +167 -0
- package/.agents/tests/fm-send-remote-delivery.test.sh +796 -0
- package/.agents/tests/fm-send-resolve-key.test.sh +551 -0
- package/.agents/tests/fm-send-secondmate-marker-herdr-e2e.test.sh +187 -0
- package/.agents/tests/fm-send-secondmate-marker.test.sh +278 -0
- package/.agents/tests/fm-send-settle.test.sh +147 -0
- package/.agents/tests/fm-send-strict.test.sh +241 -0
- package/.agents/tests/fm-session-lock-ancestry.test.sh +365 -0
- package/.agents/tests/fm-session-start.test.sh +2515 -0
- package/.agents/tests/fm-sessionstart-hook-live-e2e.test.sh +631 -0
- package/.agents/tests/fm-sessionstart-instruction-refresh-live-e2e.test.sh +230 -0
- package/.agents/tests/fm-sessionstart-nudge.test.sh +1039 -0
- package/.agents/tests/fm-shared-captain-inheritance.test.sh +404 -0
- package/.agents/tests/fm-spawn-batch.test.sh +148 -0
- package/.agents/tests/fm-spawn-dispatch-profile.test.sh +827 -0
- package/.agents/tests/fm-spawn-pool-base-freshen.test.sh +440 -0
- package/.agents/tests/fm-spawn-worktree-settle.test.sh +147 -0
- package/.agents/tests/fm-startup-memory-budget.test.sh +333 -0
- package/.agents/tests/fm-startup-network.test.sh +712 -0
- package/.agents/tests/fm-stow-cascade.test.sh +370 -0
- package/.agents/tests/fm-subagent-pretool-check.test.sh +291 -0
- package/.agents/tests/fm-supervision-events.test.sh +156 -0
- package/.agents/tests/fm-supervision-instructions.test.sh +189 -0
- package/.agents/tests/fm-tangle-guard.test.sh +277 -0
- package/.agents/tests/fm-task-delivery.test.sh +420 -0
- package/.agents/tests/fm-task-inbox.test.sh +513 -0
- package/.agents/tests/fm-teardown-endpoint-safety.test.sh +374 -0
- package/.agents/tests/fm-teardown.test.sh +2665 -0
- package/.agents/tests/fm-test-fixture-cleanup.test.sh +172 -0
- package/.agents/tests/fm-test-fixtures.test.sh +132 -0
- package/.agents/tests/fm-test-isolation-proof.test.sh +278 -0
- package/.agents/tests/fm-test-run.test.sh +1196 -0
- package/.agents/tests/fm-tmux-agent-liveness.test.sh +354 -0
- package/.agents/tests/fm-tmux-submit-busy.test.sh +340 -0
- package/.agents/tests/fm-tool-update-check.test.sh +1044 -0
- package/.agents/tests/fm-trace-context-lib.test.sh +253 -0
- package/.agents/tests/fm-trace-context-spawn.test.sh +581 -0
- package/.agents/tests/fm-transition-lib.test.sh +54 -0
- package/.agents/tests/fm-turnend-guard.test.sh +1822 -0
- package/.agents/tests/fm-update.test.sh +304 -0
- package/.agents/tests/fm-vendor-auth-probe.test.sh +395 -0
- package/.agents/tests/fm-voice-relay.test.sh +4741 -0
- package/.agents/tests/fm-wake-daemon-lifecycle-e2e.test.sh +171 -0
- package/.agents/tests/fm-wake-drain-open-decisions-cursor.test.sh +356 -0
- package/.agents/tests/fm-wake-drain-open-decisions.test.sh +226 -0
- package/.agents/tests/fm-wake-drain-unread-status.test.sh +379 -0
- package/.agents/tests/fm-wake-queue.test.sh +1224 -0
- package/.agents/tests/fm-watch-arm.test.sh +821 -0
- package/.agents/tests/fm-watch-checkpoint.test.sh +87 -0
- package/.agents/tests/fm-watch-recovery-loop.test.sh +223 -0
- package/.agents/tests/fm-watch-triage.test.sh +3889 -0
- package/.agents/tests/fm-watcher-lock.test.sh +1133 -0
- package/.agents/tests/fm-x-mode.test.sh +3042 -0
- package/.agents/tests/gleipnir-guard-pretool-check.test.sh +51 -0
- package/.agents/tests/gleipnir-machine-lock.test.sh +130 -0
- package/.agents/tests/herdr-test-safety.sh +42 -0
- package/.agents/tests/lib.sh +315 -0
- package/.agents/tests/remote-herdr-fixture.sh +125 -0
- package/.agents/tests/secondmate-helpers.sh +193 -0
- package/.agents/tests/smoke.test.sh +93 -0
- package/.agents/tests/wake-helpers.sh +338 -0
- package/.agents/tests/zellij-test-safety.sh +60 -0
- package/.agents/tools/README.md +10 -0
- package/.agents/tools/bin/tasks-cli +267 -0
- package/.agents/tools/tasks-cli.ts +216 -0
- package/AGENTS.md +454 -0
- package/CHANGELOG.md +881 -0
- package/LICENSE +1 -1
- package/NOTICE +63 -0
- package/README.md +113 -59
- package/RULES/01-domains.md +51 -0
- package/RULES/02-agents.md +78 -0
- package/RULES/03-houses.md +61 -0
- package/RULES/04-hoard.md +65 -0
- package/RULES/05-platforms.md +67 -0
- package/RULES/06-append-only.md +58 -0
- package/RULES/07-config.md +45 -0
- package/RULES/08-delivery-gate.md +75 -0
- package/RULES/README.md +20 -0
- package/bin/README.md +107 -0
- package/bin/a2a-mcp.sh +98 -0
- package/bin/a2a-serve.py +103 -0
- package/bin/a2a-serve.sh +8 -0
- package/bin/a2a-talk.sh +50 -0
- package/bin/a2abridge-ensure.sh +108 -0
- package/bin/agent-run.sh +43 -0
- package/bin/agents-config.sh +233 -0
- package/bin/apodex-smoke-test.sh +92 -0
- package/bin/bifrost-bridge.sh +112 -0
- package/bin/bifrost-ingress.sh +77 -0
- package/bin/bootstrap-macos.sh +107 -0
- package/bin/bootstrap-windows.ps1 +112 -0
- package/bin/branch-guard.sh +64 -0
- package/bin/brokk +73 -0
- package/bin/brokk-classify-lib.sh +1770 -0
- package/bin/brokk-lease-lib.sh +222 -0
- package/bin/brokk-lease.sh +193 -0
- package/bin/brokk-lint.sh +65 -0
- package/bin/brokk-send.sh +38 -0
- package/bin/brokk-update.sh +9 -0
- package/bin/brokk-wake-grant.sh +127 -0
- package/bin/brokk-wake-lib.sh +1849 -0
- package/bin/changelog-guard.sh +165 -0
- package/bin/crash-sense.sh +160 -0
- package/bin/desktop-place.sh +224 -0
- package/bin/docs-guard.sh +39 -0
- package/bin/editor-place.sh +207 -0
- package/bin/eindri-acclaim.sh +34 -0
- package/bin/eindri-control.sh +28 -0
- package/bin/eindri-role.sh +83 -0
- package/bin/eindri-seen.sh +41 -0
- package/bin/eindri-send.sh +15 -0
- package/bin/eindri-start.sh +85 -0
- package/bin/eindri-watch.sh +57 -0
- package/bin/einherjar-spawn.sh +576 -0
- package/bin/eir-doctor.sh +112 -0
- package/bin/erindi-brief.sh +271 -0
- package/bin/fleet-apply.sh +68 -0
- package/bin/github-deploy.sh +81 -0
- package/bin/gjallarhorn-expose.sh +102 -0
- package/bin/gjallarhorn-notify.sh +58 -0
- package/bin/gjallarhorn-purge.sh +54 -0
- package/bin/gjallarhorn-tunnel.sh +72 -0
- package/bin/gleipnir-lock-lib.sh +284 -0
- package/bin/groa-update.sh +122 -0
- package/bin/hall-snapshot.sh +92 -0
- package/bin/hamr-harness.sh +246 -0
- package/bin/heimdall-ssh-keys.sh +374 -0
- package/bin/herdr-agents.py +44 -0
- package/bin/herdr-ensure.sh +104 -0
- package/bin/herdr-run.sh +431 -0
- package/bin/hermes-ensure.sh +88 -0
- package/bin/hoard-lib.sh +25 -0
- package/bin/hodd.sh +78 -0
- package/bin/host-sense.sh +199 -0
- package/bin/huginn-research-worker.sh +108 -0
- package/bin/local-model-lock.sh +51 -0
- package/bin/mimir-bridge.py +226 -0
- package/bin/mimir-bridge.sh +87 -0
- package/bin/mimir-ingest.sh +131 -0
- package/bin/mimir.sh +113 -0
- package/bin/mjollnir-webhook.sh +59 -0
- package/bin/mjollnir.sh +85 -0
- package/bin/model-hardware.sh +82 -0
- package/bin/model-resolve.sh +147 -0
- package/bin/models-detect.sh +178 -0
- package/bin/nornir-cron-start.sh +162 -0
- package/bin/nornir-job-daily-briefing.sh +191 -0
- package/bin/nornir-job-git-sync.sh +152 -0
- package/bin/nornir-job-memory-housekeeping.sh +114 -0
- package/bin/nornir-job-observer.sh +166 -0
- package/bin/nornir-job-skillopt-sleep.sh +76 -0
- package/bin/omarchy-hook-install.sh +60 -0
- package/bin/omarchy-install.sh +148 -0
- package/bin/omarchy-plugins.sh +174 -0
- package/bin/omarchy-sense.sh +149 -0
- package/bin/perm-guard.sh +30 -0
- package/bin/pi-agent.sh +68 -0
- package/bin/pi-ensure.sh +77 -0
- package/bin/pi-local.sh +37 -0
- package/bin/pi-seat.sh +63 -0
- package/bin/prereq-ensure.sh +166 -0
- package/bin/private-guard.sh +38 -0
- package/bin/project-git.sh +55 -0
- package/bin/public-guard.sh +70 -0
- package/bin/ratatoskr.sh +49 -0
- package/bin/realm-lib.sh +32 -0
- package/bin/repo-scrub.sh +62 -0
- package/bin/rodd-operational-input.sh +250 -0
- package/bin/runes-append.sh +198 -0
- package/bin/saga-session-start.sh +172 -0
- package/bin/saga-sessionstart-run.sh +79 -0
- package/bin/saga-wake-drain.sh +41 -0
- package/bin/secret-guard.sh +63 -0
- package/bin/sessrumnir-ensure.sh +100 -0
- package/bin/sessrumnir-sync.sh +237 -0
- package/bin/sessrumnir.sh +91 -0
- package/bin/skillopt-setup.sh +76 -0
- package/bin/skuld-branch-outcome.sh +203 -0
- package/bin/skuld-branch-prompt.sh +101 -0
- package/bin/smidja-bootstrap.sh +90 -0
- package/bin/smidja-observe.sh +86 -0
- package/bin/syn-arm-pretool-check.sh +57 -0
- package/bin/syn-asset-pretool-check.sh +76 -0
- package/bin/syn-cd-pretool-check.sh +28 -0
- package/bin/syn-guard-pretool-check.sh +83 -0
- package/bin/syn-turnend-guard.sh +36 -0
- package/bin/syn-watch-arm.sh +99 -0
- package/bin/tailscale-sync.sh +190 -0
- package/bin/telegram-bot.sh +69 -0
- package/bin/toolchain.sh +74 -0
- package/bin/utgard.sh +111 -0
- package/bin/valhalla.sh +115 -0
- package/bin/valknut-load.sh +260 -0
- package/bin/vor-crew-state.sh +124 -0
- package/bin/wedge-notify.sh +47 -0
- package/bin/workspace-provision.sh +80 -0
- package/bin/workspace-rag.sh +150 -0
- package/bin/wyrd-db.sh +47 -0
- package/bin/yggdrasil.sh +152 -0
- package/bin/ymir-edit.sh +145 -0
- package/bin/ymir-install.sh +641 -0
- package/bin/ymir-invite.sh +93 -0
- package/bin/ymir-isolation.sh +88 -0
- package/bin/ymir-migrate.sh +61 -0
- package/bin/ymir-platform.sh +348 -0
- package/bin/ymir-say.sh +97 -0
- package/bin/ymir-setup-auth.sh +108 -0
- package/bin/ymir-validate.sh +173 -0
- package/bin/ymir.js +74 -16
- package/docs/Architecture.md +339 -0
- package/docs/design.md +351 -0
- package/docs/installations/README.md +63 -0
- package/docs/installations/ymir-home.md +93 -0
- package/docs/integration.md +105 -0
- package/docs/lore.md +1056 -0
- package/docs/research/agent-harnesses.md +290 -0
- package/docs/runbooks/README.md +19 -0
- package/docs/runbooks/a2a.md +91 -0
- package/docs/runbooks/agent-permissions.md +99 -0
- package/docs/runbooks/agents.md +57 -0
- package/docs/runbooks/models.md +69 -0
- package/docs/runbooks/realm-onboarding.md +60 -0
- package/docs/runbooks/secrets-and-hoard.md +60 -0
- package/docs/runbooks/updates-and-migrations.md +110 -0
- package/docs/runbooks/worktrees-isolation.md +72 -0
- package/docs/session-start.md +105 -0
- package/docs/skillopt-integration.md +321 -0
- package/docs/sync-system.md +66 -0
- package/docs/workspaces.md +104 -0
- package/docs/ymir-rut.md +260 -0
- package/package.json +44 -14
- package/packaging/build.sh +74 -0
- package/packaging/macos/Ymir Installer.command +43 -0
- package/packaging/windows/ymir-setup.nsi +74 -0
- package/scripts/README.md +22 -0
- package/scripts/electron.sh +210 -0
- package/scripts/start.sh +167 -0
- package/scripts/stop.sh +113 -0
- package/install.sh +0 -35
|
@@ -0,0 +1,1133 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# tests/fm-watcher-lock.test.sh - watcher singleton + lock-primitive races +
|
|
3
|
+
# PID identity stability + watch-arm liveness + guard warnings. These are
|
|
4
|
+
# safety-critical process invariants (a race bug may not reproduce through an
|
|
5
|
+
# e2e), so they stay as focused real-process units.
|
|
6
|
+
set -u
|
|
7
|
+
|
|
8
|
+
# shellcheck source=tests/wake-helpers.sh
|
|
9
|
+
. "$(dirname "${BASH_SOURCE[0]}")/wake-helpers.sh"
|
|
10
|
+
|
|
11
|
+
WATCH="$ROOT/bin/fm-watch.sh"
|
|
12
|
+
WATCH_ARM="$ROOT/bin/fm-watch-arm.sh"
|
|
13
|
+
DRAIN="$ROOT/bin/fm-wake-drain.sh"
|
|
14
|
+
LIB="$ROOT/bin/fm-wake-lib.sh"
|
|
15
|
+
|
|
16
|
+
# An arm only reports its typed failure after wait_for_healthy_successor has
|
|
17
|
+
# spent the whole confirmation budget, so cases that wait for that failure must
|
|
18
|
+
# outlast the largest production default (30s on MSYS, 10s elsewhere - see
|
|
19
|
+
# ARM_CONFIRM_DEFAULT in bin/fm-watch-arm.sh). This is a ceiling spent only when
|
|
20
|
+
# an arm genuinely fails to exit; a passing case returns as soon as it does.
|
|
21
|
+
ARM_FAIL_EXIT_POLLS=400
|
|
22
|
+
|
|
23
|
+
TMP_ROOT=$(fm_test_tmproot fm-watcher-lock-tests)
|
|
24
|
+
|
|
25
|
+
drain_and_ack() { # <state>
|
|
26
|
+
local state=$1 err sequence generation
|
|
27
|
+
err="$state/.test-drain.err"
|
|
28
|
+
FM_STATE_OVERRIDE="$state" "$DRAIN" >/dev/null 2> "$err" || return 1
|
|
29
|
+
sequence=$(sed -n 's/^WAKE_ACK_REQUIRED:.*--ack-through \([0-9][0-9]*\) --recovery-generation [A-Za-z0-9._-][A-Za-z0-9._-]*$/\1/p' "$err")
|
|
30
|
+
generation=$(sed -n 's/^WAKE_ACK_REQUIRED:.*--ack-through [0-9][0-9]* --recovery-generation \([A-Za-z0-9._-][A-Za-z0-9._-]*\)$/\1/p' "$err")
|
|
31
|
+
rm -f "$err"
|
|
32
|
+
[ -n "$sequence" ] && [ -n "$generation" ] || return 1
|
|
33
|
+
FM_STATE_OVERRIDE="$state" "$DRAIN" --ack-through "$sequence" \
|
|
34
|
+
--recovery-generation "$generation"
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
test_singleton_start() {
|
|
38
|
+
local dir state fakebin out1 out2 pid1 pid2 live i
|
|
39
|
+
dir=$(make_case singleton)
|
|
40
|
+
state="$dir/state"
|
|
41
|
+
fakebin="$dir/fakebin"
|
|
42
|
+
out1="$dir/watch-one.out"
|
|
43
|
+
out2="$dir/watch-two.out"
|
|
44
|
+
PATH="$fakebin:$PATH" FM_STATE_OVERRIDE="$state" FM_POLL=5 FM_SIGNAL_GRACE=1 FM_CHECK_INTERVAL=999999 FM_HEARTBEAT=999999 "$WATCH" > "$out1" &
|
|
45
|
+
pid1=$!
|
|
46
|
+
PATH="$fakebin:$PATH" FM_STATE_OVERRIDE="$state" FM_POLL=5 FM_SIGNAL_GRACE=1 FM_CHECK_INTERVAL=999999 FM_HEARTBEAT=999999 "$WATCH" > "$out2" &
|
|
47
|
+
pid2=$!
|
|
48
|
+
i=0
|
|
49
|
+
while [ "$i" -lt 50 ]; do
|
|
50
|
+
live=0
|
|
51
|
+
is_live_non_zombie "$pid1" && live=$((live + 1))
|
|
52
|
+
is_live_non_zombie "$pid2" && live=$((live + 1))
|
|
53
|
+
[ "$live" -eq 1 ] && break
|
|
54
|
+
sleep 0.1
|
|
55
|
+
i=$((i + 1))
|
|
56
|
+
done
|
|
57
|
+
[ "$live" -eq 1 ] || fail "expected exactly one live watcher, got $live"
|
|
58
|
+
i=0
|
|
59
|
+
while [ "$i" -lt 50 ] && ! grep -h 'watcher: already running pid ' "$out1" "$out2" >/dev/null 2>&1; do
|
|
60
|
+
sleep 0.02
|
|
61
|
+
i=$((i + 1))
|
|
62
|
+
done
|
|
63
|
+
grep -h 'watcher: already running pid ' "$out1" "$out2" >/dev/null || fail "second watcher did not report existing singleton"
|
|
64
|
+
kill "$pid1" "$pid2" 2>/dev/null || true
|
|
65
|
+
wait "$pid1" 2>/dev/null || true
|
|
66
|
+
wait "$pid2" 2>/dev/null || true
|
|
67
|
+
pass "simultaneous watcher starts leave exactly one live process"
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
test_stale_watch_lock_reclaimed() {
|
|
71
|
+
local dir state fakebin out dead_pid pid live lock_pid i
|
|
72
|
+
dir=$(make_case stale-lock)
|
|
73
|
+
state="$dir/state"
|
|
74
|
+
fakebin="$dir/fakebin"
|
|
75
|
+
out="$dir/watch.out"
|
|
76
|
+
dead_pid=999999
|
|
77
|
+
while kill -0 "$dead_pid" 2>/dev/null; do
|
|
78
|
+
dead_pid=$((dead_pid + 1))
|
|
79
|
+
done
|
|
80
|
+
mkdir "$state/.watch.lock"
|
|
81
|
+
printf '%s\n' "$dead_pid" > "$state/.watch.lock/pid"
|
|
82
|
+
PATH="$fakebin:$PATH" FM_STATE_OVERRIDE="$state" FM_POLL=5 FM_SIGNAL_GRACE=1 FM_CHECK_INTERVAL=999999 FM_HEARTBEAT=999999 "$WATCH" > "$out" &
|
|
83
|
+
pid=$!
|
|
84
|
+
i=0
|
|
85
|
+
live=0
|
|
86
|
+
lock_pid=
|
|
87
|
+
while [ "$i" -lt 50 ]; do
|
|
88
|
+
live=0
|
|
89
|
+
is_live_non_zombie "$pid" && live=1
|
|
90
|
+
lock_pid=$(cat "$state/.watch.lock/pid" 2>/dev/null || true)
|
|
91
|
+
[ "$live" -eq 1 ] && [ "$lock_pid" != "$dead_pid" ] && break
|
|
92
|
+
sleep 0.1
|
|
93
|
+
i=$((i + 1))
|
|
94
|
+
done
|
|
95
|
+
[ "$live" -eq 1 ] || fail "watcher did not reclaim stale lock and stay alive"
|
|
96
|
+
[ "$lock_pid" != "$dead_pid" ] || fail "stale watch lock pid was not replaced"
|
|
97
|
+
kill "$pid" 2>/dev/null || true
|
|
98
|
+
wait "$pid" 2>/dev/null || true
|
|
99
|
+
pass "killed watcher stale lock is reclaimed"
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
test_live_stale_watch_lock_is_actionable() {
|
|
103
|
+
local dir state fakebin out err status
|
|
104
|
+
dir=$(make_case live-stale-lock)
|
|
105
|
+
state="$dir/state"
|
|
106
|
+
fakebin="$dir/fakebin"
|
|
107
|
+
out="$dir/watch.out"
|
|
108
|
+
err="$dir/watch.err"
|
|
109
|
+
mkdir "$state/.watch.lock"
|
|
110
|
+
printf '%s\n' "$$" > "$state/.watch.lock/pid"
|
|
111
|
+
touch -t 200001010000 "$state/.last-watcher-beat"
|
|
112
|
+
status=0
|
|
113
|
+
PATH="$fakebin:$PATH" FM_STATE_OVERRIDE="$state" FM_GUARD_GRACE=1 FM_POLL=5 FM_SIGNAL_GRACE=1 FM_CHECK_INTERVAL=999999 FM_HEARTBEAT=999999 "$WATCH" > "$out" 2> "$err" || status=$?
|
|
114
|
+
[ "$status" -ne 0 ] || fail "watcher silently no-opped behind a live stale holder"
|
|
115
|
+
grep -F 'heartbeat is stale' "$err" >/dev/null || fail "watcher did not explain the stale live lock"
|
|
116
|
+
pass "live watcher lock with stale heartbeat is actionable"
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
test_guard_warnings() {
|
|
120
|
+
# The guard's two operator-visible states, with resilient substrings instead of
|
|
121
|
+
# four copy-coupled tests:
|
|
122
|
+
# (1) watcher DOWN + queued wakes: a prominent no-watcher banner leads (alarm
|
|
123
|
+
# title, in-flight count, beacon age, fix command), the queued-wakes
|
|
124
|
+
# warning follows it, and the guidance is repair-after-drain (never the
|
|
125
|
+
# old conflicting "restart NOW first").
|
|
126
|
+
# (2) a fresh watcher and an empty queue: total silence.
|
|
127
|
+
local dir state err first banner_line queue_line pid identity
|
|
128
|
+
dir=$(make_case guard)
|
|
129
|
+
state="$dir/state"
|
|
130
|
+
err="$dir/guard.err"
|
|
131
|
+
|
|
132
|
+
# (1) watcher down (no beacon) + two in-flight tasks + a queued wake.
|
|
133
|
+
# FM_ROOT_OVERRIDE points the worktree-tangle check at a non-git dir so it stays
|
|
134
|
+
# inert here; this case is about the watcher-down banner, not the tangle guard.
|
|
135
|
+
# Pin Claude so the host test runner's harness ancestry cannot change this fixture.
|
|
136
|
+
printf 'project=x\n' > "$state/task.meta"
|
|
137
|
+
printf 'project=y\n' > "$state/task2.meta"
|
|
138
|
+
append_wake "$state" heartbeat heartbeat heartbeat || fail "guard heartbeat append failed"
|
|
139
|
+
CLAUDECODE=1 PI_CODING_AGENT='' GROK_AGENT='' FM_ROOT_OVERRIDE="$dir" FM_STATE_OVERRIDE="$state" FM_GUARD_GRACE=1 "$ROOT/bin/fm-guard.sh" 2> "$err" >/dev/null || fail "guard failed"
|
|
140
|
+
first=$(grep -v '^[[:space:]]*$' "$err" | head -1)
|
|
141
|
+
case "$first" in
|
|
142
|
+
'●'*) ;;
|
|
143
|
+
*) fail "no-watcher banner is not the first thing the guard prints (got '$first')" ;;
|
|
144
|
+
esac
|
|
145
|
+
grep -F 'WATCHER DOWN - SUPERVISION IS OFF' "$err" >/dev/null || fail "guard banner missing the alarm title"
|
|
146
|
+
grep -F '2 task(s) in flight' "$err" >/dev/null || fail "guard banner missing the in-flight count"
|
|
147
|
+
grep -F 'last beat: never' "$err" >/dev/null || fail "guard banner missing the beacon age"
|
|
148
|
+
grep -F 'guarded operation WILL still run' "$err" >/dev/null || fail "guard banner missing generic continuation wording"
|
|
149
|
+
! grep -F 'requested message WILL still be sent' "$err" >/dev/null || fail "shared guard used send-specific continuation wording"
|
|
150
|
+
grep -F 'watcher supervision needs Stop-owned automatic recovery' "$err" >/dev/null || fail "guard banner missing neutral automatic-recovery guidance"
|
|
151
|
+
grep -F 'queued wakes pending - drain them' "$err" >/dev/null || fail "guard did not warn about pending queue"
|
|
152
|
+
grep -F 'After draining queued wakes, watcher supervision needs Stop-owned automatic recovery' "$err" >/dev/null || fail "guard did not order neutral automatic recovery after drain"
|
|
153
|
+
! grep -F 'Restart it NOW, before anything else' "$err" >/dev/null || fail "guard still gave conflicting restart-first instruction"
|
|
154
|
+
! grep -F 'as the harness-tracked background task' "$err" >/dev/null || fail "guard still printed the old universal background-task repair text"
|
|
155
|
+
banner_line=$(grep -n 'WATCHER DOWN' "$err" | head -1 | cut -d: -f1)
|
|
156
|
+
queue_line=$(grep -n 'queued wakes pending - drain them' "$err" | head -1 | cut -d: -f1)
|
|
157
|
+
[ "$banner_line" -lt "$queue_line" ] || fail "queued-wakes warning printed before the no-watcher banner"
|
|
158
|
+
|
|
159
|
+
dir=$(make_case guard-xmode)
|
|
160
|
+
state="$dir/state"
|
|
161
|
+
err="$dir/guard.err"
|
|
162
|
+
mkdir -p "$dir/config"
|
|
163
|
+
printf 'project=x\n' > "$state/task.meta"
|
|
164
|
+
: > "$dir/config/x-mode.env"
|
|
165
|
+
CLAUDECODE=1 PI_CODING_AGENT='' GROK_AGENT='' FM_ROOT_OVERRIDE="$dir" FM_STATE_OVERRIDE="$state" FM_GUARD_GRACE=1 "$ROOT/bin/fm-guard.sh" 2> "$err" >/dev/null || fail "guard failed"
|
|
166
|
+
grep -F "source '$dir/config/x-mode.env' first" "$err" >/dev/null || fail "guard repair line did not source the X-mode cadence config"
|
|
167
|
+
|
|
168
|
+
# (2) live watcher plus fresh beacon, empty queue -> silence.
|
|
169
|
+
dir=$(make_case guard-fresh)
|
|
170
|
+
state="$dir/state"
|
|
171
|
+
err="$dir/guard.err"
|
|
172
|
+
printf 'project=x\n' > "$state/task.meta"
|
|
173
|
+
sleep 60 &
|
|
174
|
+
pid=$!
|
|
175
|
+
identity=$(FM_STATE_OVERRIDE="$state" bash -c '. "$1"; fm_pid_identity "$2"' _ "$LIB" "$pid") || fail "could not identify fresh guard watcher"
|
|
176
|
+
mkdir -p "$state/.watch.lock"
|
|
177
|
+
printf '%s\n' "$pid" > "$state/.watch.lock/pid"
|
|
178
|
+
printf '%s\n' "$dir" > "$state/.watch.lock/fm-home"
|
|
179
|
+
printf '%s\n' "$WATCH" > "$state/.watch.lock/watcher-path"
|
|
180
|
+
printf '%s\n' "$identity" > "$state/.watch.lock/pid-identity"
|
|
181
|
+
touch "$state/.last-watcher-beat"
|
|
182
|
+
# Non-git FM_ROOT keeps the worktree-tangle check inert so "fresh watcher ->
|
|
183
|
+
# total silence" stays a pure assertion about watcher state.
|
|
184
|
+
FM_ROOT_OVERRIDE="$dir" FM_STATE_OVERRIDE="$state" FM_GUARD_GRACE=300 "$ROOT/bin/fm-guard.sh" 2> "$err" >/dev/null || fail "guard failed"
|
|
185
|
+
kill "$pid" 2>/dev/null || true
|
|
186
|
+
wait "$pid" 2>/dev/null || true
|
|
187
|
+
[ ! -s "$err" ] || fail "guard warned with a live watcher and fresh beacon: $(cat "$err")"
|
|
188
|
+
pass "guard banner leads when down with pending wakes (repair-after-drain) and stays silent when live and fresh"
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
test_lock_single_winner_under_concurrency() {
|
|
192
|
+
local dir state lockdir marker i pids pid wins
|
|
193
|
+
dir=$(make_case lock-concurrency)
|
|
194
|
+
state="$dir/state"
|
|
195
|
+
lockdir="$state/.contend.lock"
|
|
196
|
+
marker="$dir/wins"
|
|
197
|
+
: > "$marker"
|
|
198
|
+
pids=
|
|
199
|
+
i=1
|
|
200
|
+
while [ "$i" -le 40 ]; do
|
|
201
|
+
FM_STATE_OVERRIDE="$state" bash -c '
|
|
202
|
+
. "$1"
|
|
203
|
+
if fm_lock_try_acquire "$2"; then
|
|
204
|
+
printf "%s\n" "$$" >> "$3"
|
|
205
|
+
# Stay alive so the held lock names a live pid for the whole window;
|
|
206
|
+
# otherwise a late contender could legitimately reclaim a dead-pid lock.
|
|
207
|
+
sleep 1
|
|
208
|
+
fi
|
|
209
|
+
' _ "$LIB" "$lockdir" "$marker" &
|
|
210
|
+
pids="$pids $!"
|
|
211
|
+
i=$((i + 1))
|
|
212
|
+
done
|
|
213
|
+
for pid in $pids; do
|
|
214
|
+
wait "$pid" 2>/dev/null || true
|
|
215
|
+
done
|
|
216
|
+
wins=$(awk 'NF { c++ } END { print c + 0 }' "$marker")
|
|
217
|
+
[ "$wins" -eq 1 ] || fail "expected exactly one lock winner under concurrency, got $wins"
|
|
218
|
+
pass "concurrent fm_lock_try_acquire yields exactly one winner"
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
test_lock_steals_dead_pid_lock() {
|
|
222
|
+
local dir state lockdir dead rc newpid
|
|
223
|
+
dir=$(make_case lock-dead-steal)
|
|
224
|
+
state="$dir/state"
|
|
225
|
+
lockdir="$state/.contend.lock"
|
|
226
|
+
dead=$(dead_pid)
|
|
227
|
+
mkdir "$lockdir"
|
|
228
|
+
printf '%s\n' "$dead" > "$lockdir/pid"
|
|
229
|
+
rc=0
|
|
230
|
+
newpid=$(FM_STATE_OVERRIDE="$state" bash -c '
|
|
231
|
+
. "$1"
|
|
232
|
+
if fm_lock_try_acquire "$2"; then cat "$2/pid"; else exit 7; fi
|
|
233
|
+
' _ "$LIB" "$lockdir") || rc=$?
|
|
234
|
+
[ "$rc" -eq 0 ] || fail "acquirer failed to steal a dead-pid stale lock (rc=$rc)"
|
|
235
|
+
[ "$newpid" != "$dead" ] || fail "stale dead-pid lock was not replaced (still $dead)"
|
|
236
|
+
[ -n "$newpid" ] || fail "reclaimed lock has no pid recorded"
|
|
237
|
+
pass "dead-pid stale lock is reclaimed by a single acquirer"
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
test_lock_stale_steal_single_winner_under_concurrency() {
|
|
241
|
+
local dir state lockdir dead marker i pids pid wins
|
|
242
|
+
dir=$(make_case lock-stale-concurrency)
|
|
243
|
+
state="$dir/state"
|
|
244
|
+
lockdir="$state/.contend.lock"
|
|
245
|
+
marker="$dir/wins"
|
|
246
|
+
dead=$(dead_pid)
|
|
247
|
+
mkdir "$lockdir"
|
|
248
|
+
printf '%s\n' "$dead" > "$lockdir/pid"
|
|
249
|
+
: > "$marker"
|
|
250
|
+
pids=
|
|
251
|
+
i=1
|
|
252
|
+
while [ "$i" -le 40 ]; do
|
|
253
|
+
FM_STATE_OVERRIDE="$state" bash -c '
|
|
254
|
+
. "$1"
|
|
255
|
+
if fm_lock_try_acquire "$2"; then
|
|
256
|
+
printf "%s\n" "${BASHPID:-$$}" >> "$3"
|
|
257
|
+
sleep 1
|
|
258
|
+
fi
|
|
259
|
+
' _ "$LIB" "$lockdir" "$marker" &
|
|
260
|
+
pids="$pids $!"
|
|
261
|
+
i=$((i + 1))
|
|
262
|
+
done
|
|
263
|
+
for pid in $pids; do
|
|
264
|
+
wait "$pid" 2>/dev/null || true
|
|
265
|
+
done
|
|
266
|
+
wins=$(awk 'NF { c++ } END { print c + 0 }' "$marker")
|
|
267
|
+
[ "$wins" -eq 1 ] || fail "expected exactly one stale-lock stealer, got $wins"
|
|
268
|
+
pass "concurrent stale-lock steal yields exactly one winner"
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
test_lock_live_steal_mutex_is_not_reclaimed() {
|
|
272
|
+
local dir state lockdir dead holder_file holder out i lockpid stealpid
|
|
273
|
+
dir=$(make_case lock-live-stealer)
|
|
274
|
+
state="$dir/state"
|
|
275
|
+
lockdir="$state/.contend.lock"
|
|
276
|
+
holder_file="$dir/holder"
|
|
277
|
+
dead=$(dead_pid)
|
|
278
|
+
mkdir "$lockdir"
|
|
279
|
+
printf '%s\n' "$dead" > "$lockdir/pid"
|
|
280
|
+
FM_STATE_OVERRIDE="$state" bash -c '
|
|
281
|
+
. "$1"
|
|
282
|
+
fm_lock_try_acquire "$2.steal" || exit 7
|
|
283
|
+
printf "%s\n" "${BASHPID:-$$}" > "$3"
|
|
284
|
+
sleep 2
|
|
285
|
+
fm_lock_release "$2.steal"
|
|
286
|
+
' _ "$LIB" "$lockdir" "$holder_file" &
|
|
287
|
+
holder=$!
|
|
288
|
+
i=0
|
|
289
|
+
while [ "$i" -lt 50 ] && [ ! -s "$holder_file" ]; do
|
|
290
|
+
sleep 0.1
|
|
291
|
+
i=$((i + 1))
|
|
292
|
+
done
|
|
293
|
+
[ -s "$holder_file" ] || fail "live steal mutex holder did not start"
|
|
294
|
+
out=$(FM_LOCK_STALE_AFTER=0 FM_STATE_OVERRIDE="$state" bash -c '
|
|
295
|
+
. "$1"
|
|
296
|
+
if fm_lock_try_acquire "$2"; then rc=0; else rc=1; fi
|
|
297
|
+
printf "rc=%s held=%s lockpid=%s stealpid=%s\n" "$rc" "${FM_LOCK_HELD_PID:-}" "$(cat "$2/pid" 2>/dev/null || true)" "$(cat "$2.steal/pid" 2>/dev/null || true)"
|
|
298
|
+
' _ "$LIB" "$lockdir")
|
|
299
|
+
wait "$holder" || fail "live steal mutex holder failed"
|
|
300
|
+
case "$out" in
|
|
301
|
+
*"rc=1"*) ;;
|
|
302
|
+
*) fail "stale lock was stolen while a live stealer held the mutex: $out" ;;
|
|
303
|
+
esac
|
|
304
|
+
lockpid=${out#*lockpid=}; lockpid=${lockpid%% *}
|
|
305
|
+
stealpid=${out#*stealpid=}; stealpid=${stealpid%% *}
|
|
306
|
+
[ "$lockpid" = "$dead" ] || fail "primary lock changed while live steal mutex was held: $out"
|
|
307
|
+
[ "$stealpid" = "$(cat "$holder_file")" ] || fail "live steal mutex owner changed: $out"
|
|
308
|
+
pass "live steal mutex is not reclaimed"
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
test_lock_does_not_steal_live_lock() {
|
|
312
|
+
local dir state lockdir live out lockpid
|
|
313
|
+
dir=$(make_case lock-live-noop)
|
|
314
|
+
state="$dir/state"
|
|
315
|
+
lockdir="$state/.contend.lock"
|
|
316
|
+
sleep 300 &
|
|
317
|
+
live=$!
|
|
318
|
+
mkdir "$lockdir"
|
|
319
|
+
printf '%s\n' "$live" > "$lockdir/pid"
|
|
320
|
+
out=$(FM_STATE_OVERRIDE="$state" bash -c '
|
|
321
|
+
. "$1"
|
|
322
|
+
if fm_lock_try_acquire "$2"; then rc=0; else rc=1; fi
|
|
323
|
+
printf "rc=%s held=%s\n" "$rc" "${FM_LOCK_HELD_PID:-}"
|
|
324
|
+
' _ "$LIB" "$lockdir")
|
|
325
|
+
kill "$live" 2>/dev/null || true
|
|
326
|
+
wait "$live" 2>/dev/null || true
|
|
327
|
+
case "$out" in
|
|
328
|
+
*"rc=1"*) ;;
|
|
329
|
+
*) fail "live-held lock was acquired instead of refused: $out" ;;
|
|
330
|
+
esac
|
|
331
|
+
case "$out" in
|
|
332
|
+
*"held=$live"*) ;;
|
|
333
|
+
*) fail "live holder pid not reported via FM_LOCK_HELD_PID: $out" ;;
|
|
334
|
+
esac
|
|
335
|
+
lockpid=$(cat "$lockdir/pid" 2>/dev/null || true)
|
|
336
|
+
[ "$lockpid" = "$live" ] || fail "live holder's lock pid was clobbered (got '$lockpid')"
|
|
337
|
+
pass "live-held lock is not stolen"
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
test_lock_empty_pid_uses_minimum_grace() {
|
|
341
|
+
local dir state lockdir out
|
|
342
|
+
dir=$(make_case lock-empty-grace)
|
|
343
|
+
state="$dir/state"
|
|
344
|
+
lockdir="$state/.contend.lock"
|
|
345
|
+
mkdir "$lockdir"
|
|
346
|
+
out=$(FM_LOCK_STALE_AFTER=0 FM_STATE_OVERRIDE="$state" bash -c '
|
|
347
|
+
. "$1"
|
|
348
|
+
if fm_lock_try_acquire "$2"; then rc=0; else rc=1; fi
|
|
349
|
+
printf "rc=%s held=%s\n" "$rc" "${FM_LOCK_HELD_PID:-}"
|
|
350
|
+
' _ "$LIB" "$lockdir")
|
|
351
|
+
case "$out" in
|
|
352
|
+
*"rc=1"*) ;;
|
|
353
|
+
*) fail "empty mid-acquire lock was stolen with zero stale threshold: $out" ;;
|
|
354
|
+
esac
|
|
355
|
+
[ -d "$lockdir" ] || fail "empty mid-acquire lock dir was removed during grace"
|
|
356
|
+
[ ! -e "$lockdir/pid" ] || fail "empty mid-acquire lock gained a pid during grace"
|
|
357
|
+
pass "empty mid-acquire lock keeps a minimum grace"
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
test_lock_late_claim_loses_after_recreate() {
|
|
361
|
+
local dir state lockdir out
|
|
362
|
+
dir=$(make_case lock-late-claim)
|
|
363
|
+
state="$dir/state"
|
|
364
|
+
lockdir="$state/.contend.lock"
|
|
365
|
+
out=$(FM_LOCK_STALE_AFTER=0 FM_STATE_OVERRIDE="$state" bash -c '
|
|
366
|
+
. "$1"
|
|
367
|
+
owner1=$(fm_lock_owner_dir "$2") || exit 20
|
|
368
|
+
ln -s "$owner1" "$2" || exit 21
|
|
369
|
+
touch -h -t 200001010000 "$2" 2>/dev/null || sleep 2
|
|
370
|
+
if ! fm_lock_try_acquire "$2"; then exit 22; fi
|
|
371
|
+
before=$(cat "$2/pid" 2>/dev/null || true)
|
|
372
|
+
if fm_lock_claim "$2" "$owner1"; then late=won; else late=lost; fi
|
|
373
|
+
after=$(cat "$2/pid" 2>/dev/null || true)
|
|
374
|
+
current_owner=$(readlink "$2" 2>/dev/null || true)
|
|
375
|
+
printf "late=%s before=%s after=%s owner_changed=%s\n" "$late" "$before" "$after" "$([ "$current_owner" != "$owner1" ] && echo yes || echo no)"
|
|
376
|
+
' _ "$LIB" "$lockdir")
|
|
377
|
+
case "$out" in
|
|
378
|
+
*"late=lost"*) ;;
|
|
379
|
+
*) fail "late original claimant succeeded after lock recreation: $out" ;;
|
|
380
|
+
esac
|
|
381
|
+
case "$out" in
|
|
382
|
+
*"owner_changed=yes"*) ;;
|
|
383
|
+
*) fail "stale owner was not replaced before late claim: $out" ;;
|
|
384
|
+
esac
|
|
385
|
+
before=${out#*before=}; before=${before%% *}
|
|
386
|
+
after=${out#*after=}; after=${after%% *}
|
|
387
|
+
[ -n "$before" ] || fail "recreated lock did not record a pid: $out"
|
|
388
|
+
[ "$before" = "$after" ] || fail "late claim changed the recreated lock pid: $out"
|
|
389
|
+
pass "late original claimant cannot claim a recreated lock"
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
test_lock_paused_mid_acquire_claim_fails_during_steal() {
|
|
393
|
+
local dir state lockdir out pid
|
|
394
|
+
dir=$(make_case lock-paused-claim-steal)
|
|
395
|
+
state="$dir/state"
|
|
396
|
+
lockdir="$state/.contend.lock"
|
|
397
|
+
out=$(FM_LOCK_STALE_AFTER=0 FM_STATE_OVERRIDE="$state" bash -c '
|
|
398
|
+
. "$1"
|
|
399
|
+
owner=$(fm_lock_owner_dir "$2") || exit 20
|
|
400
|
+
ln -s "$owner" "$2" || exit 21
|
|
401
|
+
fm_lock_try_acquire "$2.steal" || exit 22
|
|
402
|
+
steal_owner=${FM_LOCK_OWNER_DIR:-}
|
|
403
|
+
if fm_lock_claim "$2" "$owner"; then late=won; else late=lost; fi
|
|
404
|
+
if fm_lock_try_create "$2" "$steal_owner"; then stealer=won; else stealer=lost; fi
|
|
405
|
+
pid=$(cat "$2/pid" 2>/dev/null || true)
|
|
406
|
+
printf "late=%s stealer=%s pid=%s\n" "$late" "$stealer" "$pid"
|
|
407
|
+
' _ "$LIB" "$lockdir")
|
|
408
|
+
case "$out" in
|
|
409
|
+
*"late=lost"*) ;;
|
|
410
|
+
*) fail "paused claimant succeeded while steal mutex was held: $out" ;;
|
|
411
|
+
esac
|
|
412
|
+
case "$out" in
|
|
413
|
+
*"stealer=won"*) ;;
|
|
414
|
+
*) fail "stealer could not claim after paused claimant backed off: $out" ;;
|
|
415
|
+
esac
|
|
416
|
+
pid=${out#*pid=}; pid=${pid%% *}
|
|
417
|
+
[ -n "$pid" ] || fail "stealer claim did not record a pid: $out"
|
|
418
|
+
pass "paused mid-acquire claimant backs off to active stealer"
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
test_watch_restart_rejects_reused_pid() {
|
|
422
|
+
local dir state fakebin out live pid i
|
|
423
|
+
dir=$(make_case restart-reused-pid)
|
|
424
|
+
state="$dir/state"
|
|
425
|
+
fakebin="$dir/fakebin"
|
|
426
|
+
out="$dir/restart.out"
|
|
427
|
+
sleep 300 &
|
|
428
|
+
live=$!
|
|
429
|
+
mkdir "$state/.watch.lock"
|
|
430
|
+
printf '%s\n' "$live" > "$state/.watch.lock/pid"
|
|
431
|
+
printf '%s\n' "$dir" > "$state/.watch.lock/fm-home"
|
|
432
|
+
printf '%s\n' "$WATCH" > "$state/.watch.lock/watcher-path"
|
|
433
|
+
printf '%s\n' "stale watcher identity" > "$state/.watch.lock/pid-identity"
|
|
434
|
+
PATH="$fakebin:$PATH" FM_HOME="$dir" FM_POLL=5 FM_SIGNAL_GRACE=1 FM_CHECK_INTERVAL=999999 FM_HEARTBEAT=999999 "$WATCH_ARM" --restart > "$out" &
|
|
435
|
+
pid=$!
|
|
436
|
+
i=0
|
|
437
|
+
while [ "$i" -lt 80 ] && is_live_non_zombie "$pid"; do
|
|
438
|
+
sleep 0.1
|
|
439
|
+
i=$((i + 1))
|
|
440
|
+
done
|
|
441
|
+
is_live_non_zombie "$pid" \
|
|
442
|
+
&& fail "restart did not surface recovery after replacing a reused-pid lock"
|
|
443
|
+
wait "$pid" 2>/dev/null || true
|
|
444
|
+
grep -F 'check: rearm-resurface' "$out" >/dev/null \
|
|
445
|
+
|| fail "restart replaced reused-pid lock without surfacing recovery: $(cat "$out")"
|
|
446
|
+
is_live_non_zombie "$live" || fail "restart killed a reused unrelated pid"
|
|
447
|
+
kill "$live" 2>/dev/null || true
|
|
448
|
+
wait "$live" 2>/dev/null || true
|
|
449
|
+
pass "watch restart preserves recovery without signaling a reused pid"
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
test_watch_restart_attaches_to_healthy_peer() {
|
|
453
|
+
local dir state fakebin out peer_ready peer identity armpid status i
|
|
454
|
+
dir=$(make_case restart-healthy-peer)
|
|
455
|
+
state="$dir/state"
|
|
456
|
+
fakebin="$dir/fakebin"
|
|
457
|
+
out="$dir/restart.out"
|
|
458
|
+
peer_ready="$dir/peer.ready"
|
|
459
|
+
node -e 'const fs = require("node:fs"); process.on("SIGTERM", () => {}); fs.writeFileSync(process.argv[1], "ready\n"); setTimeout(() => {}, 300000)' "$peer_ready" &
|
|
460
|
+
peer=$!
|
|
461
|
+
i=0
|
|
462
|
+
while [ "$i" -lt 50 ] && [ ! -s "$peer_ready" ]; do
|
|
463
|
+
sleep 0.1
|
|
464
|
+
i=$((i + 1))
|
|
465
|
+
done
|
|
466
|
+
if [ ! -s "$peer_ready" ]; then
|
|
467
|
+
kill -KILL "$peer" 2>/dev/null || true
|
|
468
|
+
wait "$peer" 2>/dev/null || true
|
|
469
|
+
fail "TERM-resistant peer did not become ready"
|
|
470
|
+
fi
|
|
471
|
+
identity=$(FM_STATE_OVERRIDE="$state" bash -c '. "$1"; fm_pid_identity "$2"' _ "$LIB" "$peer") || fail "could not identify peer pid"
|
|
472
|
+
mkdir "$state/.watch.lock"
|
|
473
|
+
printf '%s\n' "$peer" > "$state/.watch.lock/pid"
|
|
474
|
+
printf '%s\n' "$dir" > "$state/.watch.lock/fm-home"
|
|
475
|
+
printf '%s\n' "$WATCH" > "$state/.watch.lock/watcher-path"
|
|
476
|
+
printf '%s\n' "$identity" > "$state/.watch.lock/pid-identity"
|
|
477
|
+
touch "$state/.last-watcher-beat"
|
|
478
|
+
PATH="$fakebin:$PATH" FM_HOME="$dir" FM_POLL=5 FM_SIGNAL_GRACE=1 FM_CHECK_INTERVAL=999999 FM_HEARTBEAT=999999 FM_ARM_ATTACH_POLL=0.1 FM_ARM_CONFIRM_TIMEOUT=1 "$WATCH_ARM" --restart > "$out" &
|
|
479
|
+
armpid=$!
|
|
480
|
+
i=0
|
|
481
|
+
while [ "$i" -lt 80 ]; do
|
|
482
|
+
grep -qF "watcher: attached pid=$peer" "$out" 2>/dev/null && break
|
|
483
|
+
sleep 0.1
|
|
484
|
+
i=$((i + 1))
|
|
485
|
+
done
|
|
486
|
+
grep -qF "watcher: attached pid=$peer" "$out" || fail "restart did not attach to the verified healthy peer: $(cat "$out")"
|
|
487
|
+
is_live_non_zombie "$armpid" || fail "restart arm exited instead of following the healthy peer"
|
|
488
|
+
is_live_non_zombie "$peer" || fail "restart killed a TERM-resistant peer unexpectedly"
|
|
489
|
+
kill -KILL "$peer" 2>/dev/null || true
|
|
490
|
+
wait "$peer" 2>/dev/null || true
|
|
491
|
+
wait_for_exit "$armpid" 80
|
|
492
|
+
status=$?
|
|
493
|
+
[ "$status" -ne 0 ] && [ "$status" -ne 124 ] || fail "restart arm did not fail after its attached peer ended without a successor (status $status)"
|
|
494
|
+
grep -qF 'watcher: FAILED - cycle ended without an actionable reason' "$out" || fail "restart arm did not surface the attached cycle end"
|
|
495
|
+
pass "watch restart attaches to a verified healthy peer and later surfaces a successor gap"
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
test_watcher_self_evicts_on_lock_takeover() {
|
|
499
|
+
local dir state fakebin out pid i lock_pid
|
|
500
|
+
dir=$(make_case self-evict)
|
|
501
|
+
state="$dir/state"
|
|
502
|
+
fakebin="$dir/fakebin"
|
|
503
|
+
out="$dir/watch.out"
|
|
504
|
+
PATH="$fakebin:$PATH" FM_STATE_OVERRIDE="$state" FM_POLL=0.2 FM_SIGNAL_GRACE=1 FM_CHECK_INTERVAL=999999 FM_HEARTBEAT=999999 "$WATCH" > "$out" &
|
|
505
|
+
pid=$!
|
|
506
|
+
i=0
|
|
507
|
+
while [ "$i" -lt 80 ]; do
|
|
508
|
+
[ "$(cat "$state/.watch.lock/pid" 2>/dev/null || true)" = "$pid" ] \
|
|
509
|
+
&& [ -s "$state/.watch.lock/pid-identity" ] \
|
|
510
|
+
&& [ -e "$state/.last-watcher-beat" ] \
|
|
511
|
+
&& break
|
|
512
|
+
sleep 0.1
|
|
513
|
+
i=$((i + 1))
|
|
514
|
+
done
|
|
515
|
+
[ "$(cat "$state/.watch.lock/pid" 2>/dev/null || true)" = "$pid" ] \
|
|
516
|
+
&& [ -s "$state/.watch.lock/pid-identity" ] \
|
|
517
|
+
&& [ -e "$state/.last-watcher-beat" ] \
|
|
518
|
+
|| fail "watcher did not finish publishing its lock ownership"
|
|
519
|
+
# Simulate a second watcher taking over the singleton lock. $$ (the test
|
|
520
|
+
# runner) is a live pid that is not the watcher.
|
|
521
|
+
printf '%s\n' "$$" > "$state/.watch.lock/pid"
|
|
522
|
+
wait_for_exit "$pid" 60 || fail "watcher did not self-evict after lock takeover"
|
|
523
|
+
lock_pid=$(cat "$state/.watch.lock/pid" 2>/dev/null || true)
|
|
524
|
+
[ "$lock_pid" = "$$" ] || fail "self-evicting watcher clobbered the new holder's lock (got '$lock_pid')"
|
|
525
|
+
pass "watcher self-evicts when the lock pid no longer names it"
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
test_arm_self_eviction_is_loud_without_successor() {
|
|
529
|
+
local dir state fakebin armout armpid watcher_pid status i
|
|
530
|
+
dir=$(make_case arm-self-evict)
|
|
531
|
+
state="$dir/state"
|
|
532
|
+
fakebin="$dir/fakebin"
|
|
533
|
+
armout="$dir/arm.out"
|
|
534
|
+
# The arm's confirmation budget bounds a REAL child startup (fork, exec, lock
|
|
535
|
+
# acquisition, beacon publication), so this case holds the arm to production's
|
|
536
|
+
# own budget rather than a shrunken fixture one: a one-second budget turned
|
|
537
|
+
# ordinary CPU contention into an honest "FAILED - no live watcher with a fresh
|
|
538
|
+
# beacon" and broke this case's premise under full-suite load (issue #2844).
|
|
539
|
+
# It stays at the production default rather than something roomier because the
|
|
540
|
+
# same budget bounds the successor wait this case deliberately spends below.
|
|
541
|
+
PATH="$fakebin:$PATH" FM_STATE_OVERRIDE="$state" FM_POLL=0.2 FM_SIGNAL_GRACE=1 FM_CHECK_INTERVAL=999999 FM_HEARTBEAT=999999 "$WATCH_ARM" > "$armout" &
|
|
542
|
+
armpid=$!
|
|
543
|
+
i=0
|
|
544
|
+
while [ "$i" -lt 80 ]; do
|
|
545
|
+
grep -qF 'watcher: started pid=' "$armout" 2>/dev/null && break
|
|
546
|
+
sleep 0.1
|
|
547
|
+
i=$((i + 1))
|
|
548
|
+
done
|
|
549
|
+
watcher_pid=$(cat "$state/.watch.lock/pid" 2>/dev/null || true)
|
|
550
|
+
grep -qF "watcher: started pid=$watcher_pid" "$armout" || fail "arm did not start before self-eviction check"
|
|
551
|
+
|
|
552
|
+
# A live but identity-mismatched replacement lock makes the owned watcher
|
|
553
|
+
# self-evict normally. With no verified successor, the arm must turn that
|
|
554
|
+
# otherwise clean empty close into the typed nonzero failure.
|
|
555
|
+
printf '%s\n' "$$" > "$state/.watch.lock/pid"
|
|
556
|
+
wait_for_exit "$armpid" "$ARM_FAIL_EXIT_POLLS"
|
|
557
|
+
status=$?
|
|
558
|
+
[ "$status" -ne 0 ] && [ "$status" -ne 124 ] || fail "self-evicted arm did not fail nonzero (status $status)"
|
|
559
|
+
grep -qF 'watcher: FAILED - cycle ended without an actionable reason' "$armout" || fail "self-evicted arm omitted the typed cycle-end failure"
|
|
560
|
+
grep -q "reason=unexpected-clean-exit" "$state/.watch-cycle-exits.log" || fail "self-evicted cycle was not classified in the lifecycle ledger"
|
|
561
|
+
pass "arm turns clean self-eviction without a successor into a typed failure"
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
test_arm_attaches_and_waits_for_live_fresh_watcher() {
|
|
565
|
+
local dir state fakebin out armout i wpid armpid status
|
|
566
|
+
dir=$(make_case arm-attach)
|
|
567
|
+
state="$dir/state"
|
|
568
|
+
fakebin="$dir/fakebin"
|
|
569
|
+
out="$dir/watch.out"
|
|
570
|
+
armout="$dir/arm.out"
|
|
571
|
+
# A genuinely live watcher with a fresh beacon already holds the singleton.
|
|
572
|
+
PATH="$fakebin:$PATH" FM_STATE_OVERRIDE="$state" FM_POLL=5 FM_SIGNAL_GRACE=1 FM_CHECK_INTERVAL=999999 FM_HEARTBEAT=999999 "$WATCH" > "$out" &
|
|
573
|
+
wpid=$!
|
|
574
|
+
i=0
|
|
575
|
+
while [ "$i" -lt 60 ]; do
|
|
576
|
+
[ "$(cat "$state/.watch.lock/pid" 2>/dev/null || true)" = "$wpid" ] && [ -e "$state/.last-watcher-beat" ] && break
|
|
577
|
+
sleep 0.1
|
|
578
|
+
i=$((i + 1))
|
|
579
|
+
done
|
|
580
|
+
[ "$(cat "$state/.watch.lock/pid" 2>/dev/null || true)" = "$wpid" ] || fail "seed watcher did not take the lock"
|
|
581
|
+
# Arming must attach to the existing watcher, NOT start a second one, and NOT
|
|
582
|
+
# exit while the seed still holds the healthy lock.
|
|
583
|
+
PATH="$fakebin:$PATH" FM_STATE_OVERRIDE="$state" FM_ARM_ATTACH_POLL=0.1 FM_ARM_CONFIRM_TIMEOUT=1 "$WATCH_ARM" > "$armout" &
|
|
584
|
+
armpid=$!
|
|
585
|
+
i=0
|
|
586
|
+
while [ "$i" -lt 80 ]; do
|
|
587
|
+
grep -qF "watcher: attached pid=$wpid" "$armout" 2>/dev/null && break
|
|
588
|
+
sleep 0.1
|
|
589
|
+
i=$((i + 1))
|
|
590
|
+
done
|
|
591
|
+
grep -qF "watcher: attached pid=$wpid" "$armout" || fail "arm did not report attach to the live watcher"
|
|
592
|
+
! grep -qF 'watcher: started' "$armout" || fail "arm started a second watcher behind a healthy one"
|
|
593
|
+
! grep -qF 'watcher: FAILED' "$armout" || fail "arm reported FAILED for a healthy watcher"
|
|
594
|
+
[ "$(cat "$state/.watch.lock/pid" 2>/dev/null || true)" = "$wpid" ] || fail "arm disturbed the healthy watcher's lock"
|
|
595
|
+
is_live_non_zombie "$armpid" || fail "arm exited while the seed watcher was still healthy"
|
|
596
|
+
# After the seed dies without a successor, the attached arm must fail loudly.
|
|
597
|
+
kill "$wpid" 2>/dev/null || true
|
|
598
|
+
wait "$wpid" 2>/dev/null || true
|
|
599
|
+
wait_for_exit "$armpid" 80
|
|
600
|
+
status=$?
|
|
601
|
+
[ "$status" -ne 0 ] && [ "$status" -ne 124 ] || fail "attached arm did not fail after seed died (status $status)"
|
|
602
|
+
grep -qF 'watcher: FAILED - cycle ended without an actionable reason' "$armout" || fail "attached arm did not emit the typed cycle-end failure"
|
|
603
|
+
pass "arm attaches to a live fresh watcher and fails loudly when that cycle has no successor"
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
test_attached_arm_signal_is_recorded_in_cycle_ledger() {
|
|
607
|
+
local dir state fakebin out armout i wpid armpid status
|
|
608
|
+
dir=$(make_case attached-arm-signal-ledger)
|
|
609
|
+
state="$dir/state"
|
|
610
|
+
fakebin="$dir/fakebin"
|
|
611
|
+
out="$dir/watch.out"
|
|
612
|
+
armout="$dir/arm.out"
|
|
613
|
+
PATH="$fakebin:$PATH" FM_STATE_OVERRIDE="$state" FM_POLL=5 FM_SIGNAL_GRACE=1 FM_CHECK_INTERVAL=999999 FM_HEARTBEAT=999999 "$WATCH" > "$out" &
|
|
614
|
+
wpid=$!
|
|
615
|
+
i=0
|
|
616
|
+
while [ "$i" -lt 60 ]; do
|
|
617
|
+
[ "$(cat "$state/.watch.lock/pid" 2>/dev/null || true)" = "$wpid" ] && [ -e "$state/.last-watcher-beat" ] && break
|
|
618
|
+
sleep 0.1
|
|
619
|
+
i=$((i + 1))
|
|
620
|
+
done
|
|
621
|
+
[ "$(cat "$state/.watch.lock/pid" 2>/dev/null || true)" = "$wpid" ] || fail "seed watcher did not take the lock"
|
|
622
|
+
PATH="$fakebin:$PATH" FM_STATE_OVERRIDE="$state" FM_ARM_ATTACH_POLL=0.1 FM_ARM_CONFIRM_TIMEOUT=1 "$WATCH_ARM" > "$armout" &
|
|
623
|
+
armpid=$!
|
|
624
|
+
i=0
|
|
625
|
+
while [ "$i" -lt 80 ]; do
|
|
626
|
+
grep -qF "watcher: attached pid=$wpid" "$armout" 2>/dev/null && break
|
|
627
|
+
sleep 0.1
|
|
628
|
+
i=$((i + 1))
|
|
629
|
+
done
|
|
630
|
+
grep -qF "watcher: attached pid=$wpid" "$armout" || fail "arm did not report attach before signal"
|
|
631
|
+
kill -TERM "$armpid" 2>/dev/null || fail "could not signal the attached arm"
|
|
632
|
+
wait_for_exit "$armpid" 80
|
|
633
|
+
status=$?
|
|
634
|
+
[ "$status" -eq 143 ] || fail "attached arm did not exit with TERM status (got $status)"
|
|
635
|
+
grep -q "arm_pid=$armpid.*watcher_pid=$wpid.*origin=attached.*exit_code=143.*signal=TERM.*reason=arm-interrupted" "$state/.watch-cycle-exits.log" \
|
|
636
|
+
|| fail "attached arm signal was not recorded in the lifecycle ledger"
|
|
637
|
+
is_live_non_zombie "$wpid" || fail "signaling an attached arm terminated the peer watcher"
|
|
638
|
+
kill "$wpid" 2>/dev/null || true
|
|
639
|
+
wait "$wpid" 2>/dev/null || true
|
|
640
|
+
pass "attached arm signals record a classified lifecycle entry"
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
test_arm_starts_and_self_heals() {
|
|
644
|
+
# Arming with no confirmable watcher must FORK one and confirm it live + fresh
|
|
645
|
+
# before reporting 'started' - whether the lock is empty (clean start) or held
|
|
646
|
+
# by a dead pid with a fresh-looking leftover beacon (self-heal). It must never
|
|
647
|
+
# report 'healthy' off a dead pid. One row per pre-state, one assertion block.
|
|
648
|
+
local row dir state fakebin armout armpid i lock_pid dead_pid
|
|
649
|
+
for row in clean dead-pid; do
|
|
650
|
+
dir=$(make_case "arm-$row")
|
|
651
|
+
state="$dir/state"
|
|
652
|
+
fakebin="$dir/fakebin"
|
|
653
|
+
armout="$dir/arm.out"
|
|
654
|
+
dead_pid=
|
|
655
|
+
if [ "$row" = dead-pid ]; then
|
|
656
|
+
dead_pid=999999
|
|
657
|
+
while kill -0 "$dead_pid" 2>/dev/null; do dead_pid=$((dead_pid + 1)); done
|
|
658
|
+
mkdir "$state/.watch.lock"
|
|
659
|
+
printf '%s\n' "$dead_pid" > "$state/.watch.lock/pid"
|
|
660
|
+
printf '%s\n' "$dir" > "$state/.watch.lock/fm-home"
|
|
661
|
+
printf '%s\n' "$WATCH" > "$state/.watch.lock/watcher-path"
|
|
662
|
+
printf '%s\n' "dead watcher identity" > "$state/.watch.lock/pid-identity"
|
|
663
|
+
touch "$state/.last-watcher-beat"
|
|
664
|
+
fi
|
|
665
|
+
PATH="$fakebin:$PATH" FM_HOME="$dir" FM_POLL=5 FM_SIGNAL_GRACE=1 FM_CHECK_INTERVAL=999999 FM_HEARTBEAT=999999 "$WATCH_ARM" > "$armout" &
|
|
666
|
+
armpid=$!
|
|
667
|
+
i=0
|
|
668
|
+
while [ "$i" -lt 80 ]; do
|
|
669
|
+
if [ "$row" = dead-pid ]; then
|
|
670
|
+
is_live_non_zombie "$armpid" || break
|
|
671
|
+
else
|
|
672
|
+
grep -qF 'watcher: started pid=' "$armout" 2>/dev/null && break
|
|
673
|
+
fi
|
|
674
|
+
sleep 0.1; i=$((i + 1))
|
|
675
|
+
done
|
|
676
|
+
if [ "$row" = dead-pid ]; then
|
|
677
|
+
is_live_non_zombie "$armpid" \
|
|
678
|
+
&& fail "arm did not surface recovery after reclaiming a dead-pid lock"
|
|
679
|
+
wait "$armpid" 2>/dev/null || true
|
|
680
|
+
grep -F 'check: rearm-resurface' "$armout" >/dev/null \
|
|
681
|
+
|| fail "arm reclaimed dead-pid lock without surfacing recovery: $(cat "$armout")"
|
|
682
|
+
continue
|
|
683
|
+
fi
|
|
684
|
+
grep -qF 'watcher: started pid=' "$armout" || fail "arm ($row) did not report a started watcher"
|
|
685
|
+
! grep -qE 'watcher: (healthy|attached)' "$armout" || fail "arm ($row) wrongly reported attached/healthy instead of starting a fresh watcher"
|
|
686
|
+
lock_pid=$(cat "$state/.watch.lock/pid" 2>/dev/null || true)
|
|
687
|
+
# The 'started' line prints only after the fresh watcher passed (live pid +
|
|
688
|
+
# fresh beacon), so it doubles as proof the beacon was confirmed fresh.
|
|
689
|
+
grep -F "watcher: started pid=$lock_pid (beacon fresh)" "$armout" >/dev/null \
|
|
690
|
+
|| fail "arm ($row) started line did not name the confirmed live watcher (lock '$lock_pid')"
|
|
691
|
+
kill -0 "$lock_pid" 2>/dev/null || fail "arm ($row) confirmed-started watcher is not actually alive"
|
|
692
|
+
kill "$armpid" "$lock_pid" 2>/dev/null || true
|
|
693
|
+
wait "$armpid" 2>/dev/null || true
|
|
694
|
+
done
|
|
695
|
+
pass "arm starts cleanly and resurfaces recovery after a dead-pid lock"
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
test_arm_hup_cleans_child_and_temp_output() {
|
|
699
|
+
local dir state fakebin armout i armpid lock_pid status
|
|
700
|
+
dir=$(make_case arm-hup-cleanup)
|
|
701
|
+
state="$dir/state"
|
|
702
|
+
fakebin="$dir/fakebin"
|
|
703
|
+
armout="$dir/arm.out"
|
|
704
|
+
PATH="$fakebin:$PATH" FM_HOME="$dir" FM_STATE_OVERRIDE="$state" FM_POLL=5 FM_SIGNAL_GRACE=1 FM_CHECK_INTERVAL=999999 FM_HEARTBEAT=999999 "$WATCH_ARM" > "$armout" &
|
|
705
|
+
armpid=$!
|
|
706
|
+
i=0
|
|
707
|
+
while [ "$i" -lt 80 ]; do
|
|
708
|
+
grep -qF 'watcher: started pid=' "$armout" 2>/dev/null && break
|
|
709
|
+
sleep 0.1
|
|
710
|
+
i=$((i + 1))
|
|
711
|
+
done
|
|
712
|
+
grep -qF 'watcher: started pid=' "$armout" || fail "arm did not start before HUP cleanup check"
|
|
713
|
+
lock_pid=$(cat "$state/.watch.lock/pid" 2>/dev/null || true)
|
|
714
|
+
kill -HUP "$armpid" 2>/dev/null || fail "could not send HUP to arm"
|
|
715
|
+
wait_for_exit "$armpid" 80
|
|
716
|
+
status=$?
|
|
717
|
+
[ "$status" -eq 129 ] || fail "arm did not exit with HUP status (got $status)"
|
|
718
|
+
i=0
|
|
719
|
+
while [ "$i" -lt 80 ] && is_live_non_zombie "$lock_pid"; do
|
|
720
|
+
sleep 0.1
|
|
721
|
+
i=$((i + 1))
|
|
722
|
+
done
|
|
723
|
+
! is_live_non_zombie "$lock_pid" || fail "HUP cleanup left watcher child running"
|
|
724
|
+
! ls "$state"/.watch-arm-output.* >/dev/null 2>&1 || fail "HUP cleanup left temp output behind"
|
|
725
|
+
pass "arm cleans child watcher and temp output on HUP"
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
test_arm_propagates_immediate_wake_before_confirmation() {
|
|
729
|
+
local dir state fakebin armout drain_out check_file rc
|
|
730
|
+
dir=$(make_case arm-immediate-wake)
|
|
731
|
+
state="$dir/state"
|
|
732
|
+
fakebin="$dir/fakebin"
|
|
733
|
+
armout="$dir/arm.out"
|
|
734
|
+
drain_out="$dir/drain.out"
|
|
735
|
+
check_file="$state/task.check.sh"
|
|
736
|
+
cat > "$check_file" <<'SH'
|
|
737
|
+
#!/usr/bin/env bash
|
|
738
|
+
printf 'merged: https://example.test/pr/7\n'
|
|
739
|
+
SH
|
|
740
|
+
chmod 0700 "$check_file"
|
|
741
|
+
FM_STATE_OVERRIDE="$state" "$ROOT/bin/fm-check-register.sh" task >/dev/null \
|
|
742
|
+
|| fail "could not register immediate-wake custom check"
|
|
743
|
+
rc=0
|
|
744
|
+
# This case asserts wake propagation, not the confirmation deadline, and its
|
|
745
|
+
# child must also run the registered check before exiting: measured at 1.9-2.3s
|
|
746
|
+
# idle but 9.1-13.1s at 3x CPU oversubscription, against an 11s production
|
|
747
|
+
# budget. An explicit budget takes the deadline out of the assertion and costs
|
|
748
|
+
# nothing on a passing run, because the arm returns as soon as the child
|
|
749
|
+
# settles (issue #2844).
|
|
750
|
+
PATH="$fakebin:$PATH" FM_STATE_OVERRIDE="$state" FM_GUARD_GRACE=0 FM_POLL=5 FM_SIGNAL_GRACE=1 FM_CHECK_INTERVAL=0 FM_HEARTBEAT=999999 FM_ARM_CONFIRM_TIMEOUT=60 "$WATCH_ARM" > "$armout" || rc=$?
|
|
751
|
+
[ "$rc" -eq 0 ] || fail "arm returned non-zero for an immediate wake (status $rc): $(cat "$armout")"
|
|
752
|
+
grep -F "check: $check_file: merged: https://example.test/pr/7" "$armout" >/dev/null || fail "arm did not propagate the immediate check wake"
|
|
753
|
+
! grep -qF 'watcher: FAILED' "$armout" || fail "arm printed FAILED after a valid immediate wake"
|
|
754
|
+
FM_STATE_OVERRIDE="$state" "$DRAIN" > "$drain_out" || fail "drain after immediate arm wake failed"
|
|
755
|
+
grep "$(printf '\tcheck\t')" "$drain_out" | grep -F "$check_file" | grep -F 'merged: https://example.test/pr/7' >/dev/null || fail "immediate check wake was not queued"
|
|
756
|
+
pass "arm propagates an immediate watcher wake before confirmation"
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
test_arm_waits_for_peer_beacon_after_child_stands_down() {
|
|
760
|
+
local dir state fakebin armout peer identity armpid status i
|
|
761
|
+
dir=$(make_case arm-peer-startup-race)
|
|
762
|
+
state="$dir/state"
|
|
763
|
+
fakebin="$dir/fakebin"
|
|
764
|
+
armout="$dir/arm.out"
|
|
765
|
+
sleep 300 &
|
|
766
|
+
peer=$!
|
|
767
|
+
identity=$(FM_STATE_OVERRIDE="$state" bash -c '. "$1"; fm_pid_identity "$2"' _ "$LIB" "$peer") || fail "could not identify peer pid"
|
|
768
|
+
mkdir "$state/.watch.lock"
|
|
769
|
+
printf '%s\n' "$peer" > "$state/.watch.lock/pid"
|
|
770
|
+
printf '%s\n' "$dir" > "$state/.watch.lock/fm-home"
|
|
771
|
+
printf '%s\n' "$WATCH" > "$state/.watch.lock/watcher-path"
|
|
772
|
+
printf '%s\n' "$identity" > "$state/.watch.lock/pid-identity"
|
|
773
|
+
# Same budget contract as the self-eviction case: the owned child's real
|
|
774
|
+
# startup and stand-down happen inside the arm's confirmation window, so the
|
|
775
|
+
# window stays production-sized (issue #2844).
|
|
776
|
+
PATH="$fakebin:$PATH" FM_HOME="$dir" FM_POLL=5 FM_SIGNAL_GRACE=1 FM_CHECK_INTERVAL=999999 FM_HEARTBEAT=999999 FM_ARM_ATTACH_POLL=0.1 "$WATCH_ARM" > "$armout" &
|
|
777
|
+
armpid=$!
|
|
778
|
+
# Synchronize on the owned child declining the live peer lock before making
|
|
779
|
+
# the peer healthy. Sleeping for the same budget the arm spends made this
|
|
780
|
+
# regression fixture race the confirmation deadline under full-suite load,
|
|
781
|
+
# rather than testing the intended successor-handshake boundary.
|
|
782
|
+
i=0
|
|
783
|
+
while [ "$i" -lt 80 ]; do
|
|
784
|
+
grep -qF "watcher: already running pid $peer" "$state"/.watch-arm-output.* 2>/dev/null && break
|
|
785
|
+
sleep 0.1
|
|
786
|
+
i=$((i + 1))
|
|
787
|
+
done
|
|
788
|
+
grep -qF "watcher: already running pid $peer" "$state"/.watch-arm-output.* 2>/dev/null \
|
|
789
|
+
|| fail "arm child did not stand down behind the peer watcher"
|
|
790
|
+
touch "$state/.last-watcher-beat"
|
|
791
|
+
i=0
|
|
792
|
+
while [ "$i" -lt 80 ]; do
|
|
793
|
+
grep -qF "watcher: attached pid=$peer" "$armout" 2>/dev/null && break
|
|
794
|
+
sleep 0.1
|
|
795
|
+
i=$((i + 1))
|
|
796
|
+
done
|
|
797
|
+
grep -qF "watcher: attached pid=$peer" "$armout" || fail "arm did not wait for and attach to the peer watcher: $(cat "$armout")"
|
|
798
|
+
! grep -qF 'watcher: FAILED' "$armout" || fail "arm falsely reported FAILED during peer startup race"
|
|
799
|
+
is_live_non_zombie "$armpid" || fail "arm exited while the peer was still healthy"
|
|
800
|
+
# After the peer dies without a successor, the attached arm must fail loudly.
|
|
801
|
+
kill "$peer" 2>/dev/null || true
|
|
802
|
+
wait "$peer" 2>/dev/null || true
|
|
803
|
+
wait_for_exit "$armpid" "$ARM_FAIL_EXIT_POLLS"
|
|
804
|
+
status=$?
|
|
805
|
+
[ "$status" -ne 0 ] && [ "$status" -ne 124 ] || fail "attached arm did not fail after peer died (status $status): $(cat "$armout")"
|
|
806
|
+
grep -qF 'watcher: FAILED - cycle ended without an actionable reason' "$armout" || fail "peer-attached arm did not emit the typed cycle-end failure"
|
|
807
|
+
pass "arm attaches to a peer watcher after child stands down and surfaces a missing successor"
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
test_arm_fails_loud_when_no_fresh_watcher_confirmable() {
|
|
811
|
+
local dir state fakebin armout live armpid status
|
|
812
|
+
dir=$(make_case arm-failed-stale)
|
|
813
|
+
state="$dir/state"
|
|
814
|
+
fakebin="$dir/fakebin"
|
|
815
|
+
armout="$dir/arm.out"
|
|
816
|
+
sleep 300 &
|
|
817
|
+
live=$!
|
|
818
|
+
# A live process holds the lock but is NOT a confirmable watcher (no identity),
|
|
819
|
+
# and the beacon is stale. The fresh child cannot steal a LIVE lock, so no
|
|
820
|
+
# watcher can ever be confirmed - the honest answer is FAILED, not healthy.
|
|
821
|
+
mkdir "$state/.watch.lock"
|
|
822
|
+
printf '%s\n' "$live" > "$state/.watch.lock/pid"
|
|
823
|
+
touch -t 200001010000 "$state/.last-watcher-beat"
|
|
824
|
+
PATH="$fakebin:$PATH" FM_STATE_OVERRIDE="$state" FM_POLL=5 FM_SIGNAL_GRACE=1 FM_CHECK_INTERVAL=999999 FM_HEARTBEAT=999999 FM_ARM_CONFIRM_TIMEOUT=3 "$WATCH_ARM" > "$armout" &
|
|
825
|
+
armpid=$!
|
|
826
|
+
wait_for_exit "$armpid" 120
|
|
827
|
+
status=$?
|
|
828
|
+
[ "$status" -ne 124 ] || fail "arm never returned for an unconfirmable watcher"
|
|
829
|
+
[ "$status" -ne 0 ] || fail "arm exited zero when no fresh watcher could be confirmed"
|
|
830
|
+
grep -F 'watcher: FAILED' "$armout" >/dev/null || fail "arm did not print a typed FAILED line"
|
|
831
|
+
! grep -qE 'watcher: (healthy|attached)' "$armout" || fail "arm reported attached/healthy off a stale beacon"
|
|
832
|
+
! grep -qF 'watcher: started' "$armout" || fail "arm falsely reported started"
|
|
833
|
+
is_live_non_zombie "$live" || fail "arm killed the unrelated live lock holder"
|
|
834
|
+
kill "$live" 2>/dev/null || true
|
|
835
|
+
wait "$live" 2>/dev/null || true
|
|
836
|
+
pass "arm reports FAILED and exits non-zero when no fresh watcher can be confirmed"
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
test_cycle_exit_ledger_links_successor_and_stays_bounded() {
|
|
840
|
+
local dir state fakebin armout check_file first_arm successor_arm successor_pid i size iteration
|
|
841
|
+
dir=$(make_case cycle-ledger)
|
|
842
|
+
state="$dir/state"
|
|
843
|
+
fakebin="$dir/fakebin"
|
|
844
|
+
armout="$dir/first-arm.out"
|
|
845
|
+
check_file="$state/task.check.sh"
|
|
846
|
+
cat > "$check_file" <<'SH'
|
|
847
|
+
#!/usr/bin/env bash
|
|
848
|
+
printf 'done: synthetic cycle\n'
|
|
849
|
+
SH
|
|
850
|
+
chmod 0700 "$check_file"
|
|
851
|
+
FM_STATE_OVERRIDE="$state" "$ROOT/bin/fm-check-register.sh" task >/dev/null \
|
|
852
|
+
|| fail "could not register cycle-ledger check"
|
|
853
|
+
|
|
854
|
+
PATH="$fakebin:$PATH" FM_STATE_OVERRIDE="$state" FM_GUARD_GRACE=0 FM_POLL=5 FM_SIGNAL_GRACE=1 FM_CHECK_INTERVAL=0 FM_HEARTBEAT=999999 "$WATCH_ARM" > "$armout" &
|
|
855
|
+
first_arm=$!
|
|
856
|
+
wait "$first_arm" || fail "first ledger cycle did not surface its actionable wake"
|
|
857
|
+
grep -q "arm_pid=$first_arm.*reason=actionable-check.*successor=none" "$state/.watch-cycle-exits.log" \
|
|
858
|
+
|| fail "first ledger record omitted its actionable classification"
|
|
859
|
+
drain_and_ack "$state" || fail "first ledger wake handling acknowledgement failed"
|
|
860
|
+
|
|
861
|
+
rm -f "$check_file" "$state/task.check-trust"
|
|
862
|
+
armout="$dir/successor-arm.out"
|
|
863
|
+
PATH="$fakebin:$PATH" FM_STATE_OVERRIDE="$state" FM_WATCH_PREDECESSOR_ARM_PID="$first_arm" FM_POLL=5 FM_SIGNAL_GRACE=1 FM_CHECK_INTERVAL=999999 FM_HEARTBEAT=999999 "$WATCH_ARM" > "$armout" &
|
|
864
|
+
successor_arm=$!
|
|
865
|
+
i=0
|
|
866
|
+
while [ "$i" -lt 80 ]; do
|
|
867
|
+
grep -qF 'watcher: started pid=' "$armout" 2>/dev/null && break
|
|
868
|
+
sleep 0.1
|
|
869
|
+
i=$((i + 1))
|
|
870
|
+
done
|
|
871
|
+
successor_pid=$(cat "$state/.watch.lock/pid" 2>/dev/null || true)
|
|
872
|
+
grep -qF "watcher: started pid=$successor_pid" "$armout" || fail "successor ledger cycle did not start"
|
|
873
|
+
grep -q "arm_pid=$first_arm.*successor=started:$successor_pid" "$state/.watch-cycle-exits.log" \
|
|
874
|
+
|| fail "predecessor ledger record was not linked to its verified successor"
|
|
875
|
+
kill -HUP "$successor_arm" 2>/dev/null || true
|
|
876
|
+
wait "$successor_arm" 2>/dev/null || true
|
|
877
|
+
# The forced interruption is a watcher-down interval. Consume the prior
|
|
878
|
+
# delivered wake before beginning independent ledger cycles, just as the
|
|
879
|
+
# recovery handling turn does, so this fixture does not intentionally carry a
|
|
880
|
+
# durable wake into the next arm.
|
|
881
|
+
drain_and_ack "$state" || fail "recovery drain after forced arm interruption failed"
|
|
882
|
+
|
|
883
|
+
# Produce enough short cycles to cross a deliberately small cap. The cap is
|
|
884
|
+
# applied by the arm layer itself and keeps only complete ledger records.
|
|
885
|
+
iteration=0
|
|
886
|
+
while [ "$iteration" -lt 6 ]; do
|
|
887
|
+
armout="$dir/bounded-$iteration.out"
|
|
888
|
+
PATH="$fakebin:$PATH" FM_STATE_OVERRIDE="$state" FM_WATCH_CYCLE_LOG_MAX_BYTES=1400 FM_WATCH_CYCLE_LOG_KEEP_LINES=2 FM_POLL=5 FM_SIGNAL_GRACE=1 FM_CHECK_INTERVAL=999999 FM_HEARTBEAT=999999 "$WATCH_ARM" > "$armout" &
|
|
889
|
+
successor_arm=$!
|
|
890
|
+
i=0
|
|
891
|
+
while [ "$i" -lt 80 ]; do
|
|
892
|
+
grep -qF 'watcher: started pid=' "$armout" 2>/dev/null && break
|
|
893
|
+
sleep 0.1
|
|
894
|
+
i=$((i + 1))
|
|
895
|
+
done
|
|
896
|
+
grep -qF 'watcher: started pid=' "$armout" || fail "bounded ledger cycle $iteration did not start"
|
|
897
|
+
kill -HUP "$successor_arm" 2>/dev/null || true
|
|
898
|
+
wait "$successor_arm" 2>/dev/null || true
|
|
899
|
+
drain_and_ack "$state" \
|
|
900
|
+
|| fail "recovery drain after bounded ledger cycle $iteration failed"
|
|
901
|
+
iteration=$((iteration + 1))
|
|
902
|
+
done
|
|
903
|
+
size=$(wc -c < "$state/.watch-cycle-exits.log" | tr -d '[:space:]')
|
|
904
|
+
[ "$size" -le 1400 ] || fail "cycle ledger exceeded its configured cap ($size bytes)"
|
|
905
|
+
! grep -v '^arm_pid=.*watcher_pid=.*started_at=.*ended_at=.*exit_code=.*signal=.*reason=.*beacon_age=.*lock_before=.*lock_after=.*successor=' "$state/.watch-cycle-exits.log" | grep . >/dev/null \
|
|
906
|
+
|| fail "bounded lifecycle ledger contains a partial or malformed record"
|
|
907
|
+
pass "cycle-exit ledger links a verified successor and remains size-capped"
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
test_stopped_watcher_is_live_but_stale_then_exit_is_classified() {
|
|
911
|
+
local dir state fakebin armout armpid watcher_pid i status
|
|
912
|
+
dir=$(make_case stopped-watcher)
|
|
913
|
+
state="$dir/state"
|
|
914
|
+
fakebin="$dir/fakebin"
|
|
915
|
+
armout="$dir/arm.out"
|
|
916
|
+
PATH="$fakebin:$PATH" FM_HOME="$dir" FM_STATE_OVERRIDE="$state" FM_POLL=5 FM_SIGNAL_GRACE=1 FM_CHECK_INTERVAL=999999 FM_HEARTBEAT=999999 "$WATCH_ARM" > "$armout" &
|
|
917
|
+
armpid=$!
|
|
918
|
+
i=0
|
|
919
|
+
while [ "$i" -lt 80 ]; do
|
|
920
|
+
grep -qF 'watcher: started pid=' "$armout" 2>/dev/null && break
|
|
921
|
+
sleep 0.1
|
|
922
|
+
i=$((i + 1))
|
|
923
|
+
done
|
|
924
|
+
watcher_pid=$(cat "$state/.watch.lock/pid" 2>/dev/null || true)
|
|
925
|
+
grep -qF "watcher: started pid=$watcher_pid" "$armout" || fail "load counterfactual watcher did not start"
|
|
926
|
+
|
|
927
|
+
kill -STOP "$watcher_pid" 2>/dev/null || fail "could not SIGSTOP watcher"
|
|
928
|
+
touch -t 200001010000 "$state/.last-watcher-beat"
|
|
929
|
+
FM_STATE_OVERRIDE="$state" bash -c '. "$1"; fm_pid_alive "$2"' _ "$LIB" "$watcher_pid" \
|
|
930
|
+
|| fail "SIGSTOP watcher was not classified as a live pid"
|
|
931
|
+
if FM_HOME="$dir" FM_STATE_OVERRIDE="$state" bash -c '. "$1"; fm_watcher_healthy "$2" "$3" 300 "$4"' _ "$LIB" "$state" "$WATCH" "$dir"; then
|
|
932
|
+
fail "SIGSTOP watcher with a stale beacon was classified healthy"
|
|
933
|
+
fi
|
|
934
|
+
|
|
935
|
+
kill -CONT "$watcher_pid" 2>/dev/null || true
|
|
936
|
+
kill -TERM "$watcher_pid" 2>/dev/null || true
|
|
937
|
+
wait_for_exit "$armpid" 80
|
|
938
|
+
status=$?
|
|
939
|
+
[ "$status" -ne 0 ] && [ "$status" -ne 124 ] || fail "terminated stopped-watcher cycle did not surface nonzero (status $status)"
|
|
940
|
+
grep -Eq 'reason=(nonzero-exit|signal-exit)' "$state/.watch-cycle-exits.log" \
|
|
941
|
+
|| fail "terminated watcher exit was not classified in the lifecycle ledger"
|
|
942
|
+
pass "SIGSTOP distinguishes live PID from stale beacon and termination records the exit class"
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
test_pid_identity_is_locale_invariant() {
|
|
946
|
+
# The portable fallback records its process identity under one locale, then
|
|
947
|
+
# arm/guard/turn-end re-read it under the machine's ambient locale. ps's lstart
|
|
948
|
+
# date format follows LC_TIME, so an unpinned read on a non-C locale (e.g. ko_KR)
|
|
949
|
+
# would reject a genuinely live watcher. The fallback pins LC_ALL=C inside
|
|
950
|
+
# fm_pid_identity, so its output must be byte-identical regardless of the caller's
|
|
951
|
+
# exported LC_ALL/LC_TIME. This stays deterministic on CI even where an alternate
|
|
952
|
+
# locale like ko_KR.UTF-8 is not installed (the equality then holds trivially).
|
|
953
|
+
local live no_proc fakebin locale_log baseline via_lc_all via_lc_time
|
|
954
|
+
local real_first real_second observed
|
|
955
|
+
sleep 300 &
|
|
956
|
+
live=$!
|
|
957
|
+
no_proc="$TMP_ROOT/no-proc"
|
|
958
|
+
fakebin="$TMP_ROOT/locale-ps"
|
|
959
|
+
locale_log="$TMP_ROOT/locale-ps.observed"
|
|
960
|
+
mkdir -p "$fakebin"
|
|
961
|
+
: > "$locale_log"
|
|
962
|
+
# The stub renders lstart through date under whatever locale it inherits, so its
|
|
963
|
+
# output really does change when the caller's locale leaks through. Dropping the
|
|
964
|
+
# LC_ALL=C pin in fm_pid_identity therefore breaks the equality assertions below
|
|
965
|
+
# on any host with a second locale installed, and the recorded LC_ALL below keeps
|
|
966
|
+
# the pin asserted even where ko_KR.UTF-8 is missing and date falls back to C.
|
|
967
|
+
cat > "$fakebin/ps" <<'SH'
|
|
968
|
+
#!/usr/bin/env bash
|
|
969
|
+
printf '%s\n' "${LC_ALL-<unset>}" >> "$FAKE_PS_LOCALE_LOG"
|
|
970
|
+
stamp=$(date -d @1784094040 '+%a %b %e %H:%M:%S %Y' 2>/dev/null) \
|
|
971
|
+
|| stamp=$(date -r 1784094040 '+%a %b %e %H:%M:%S %Y' 2>/dev/null) \
|
|
972
|
+
|| stamp='Mon Jul 28 20:00:00 2026'
|
|
973
|
+
printf '%s sleep 300\n' "$stamp"
|
|
974
|
+
SH
|
|
975
|
+
chmod +x "$fakebin/ps"
|
|
976
|
+
baseline=$(PATH="$fakebin:$PATH" FAKE_PS_LOCALE_LOG="$locale_log" FM_PROC_ROOT_OVERRIDE="$no_proc" LC_ALL=C bash -c '. "$1"; fm_pid_identity "$2"' _ "$LIB" "$live" 2>/dev/null)
|
|
977
|
+
via_lc_all=$(PATH="$fakebin:$PATH" FAKE_PS_LOCALE_LOG="$locale_log" FM_PROC_ROOT_OVERRIDE="$no_proc" LC_ALL=ko_KR.UTF-8 bash -c '. "$1"; fm_pid_identity "$2"' _ "$LIB" "$live" 2>/dev/null)
|
|
978
|
+
via_lc_time=$(PATH="$fakebin:$PATH" FAKE_PS_LOCALE_LOG="$locale_log" FM_PROC_ROOT_OVERRIDE="$no_proc" LC_TIME=ko_KR.UTF-8 bash -c 'unset LC_ALL; . "$1"; fm_pid_identity "$2"' _ "$LIB" "$live" 2>/dev/null)
|
|
979
|
+
# Keep the real ps fallback exercised wherever it supports the portable -o fields.
|
|
980
|
+
real_first=
|
|
981
|
+
real_second=
|
|
982
|
+
if LC_ALL=C ps -p "$live" -o lstart= -o command= >/dev/null 2>&1; then
|
|
983
|
+
real_first=$(FM_PROC_ROOT_OVERRIDE="$no_proc" LC_ALL=C bash -c '. "$1"; fm_pid_identity "$2"' _ "$LIB" "$live" 2>/dev/null)
|
|
984
|
+
real_second=$(FM_PROC_ROOT_OVERRIDE="$no_proc" LC_TIME=ko_KR.UTF-8 bash -c 'unset LC_ALL; . "$1"; fm_pid_identity "$2"' _ "$LIB" "$live" 2>/dev/null)
|
|
985
|
+
fi
|
|
986
|
+
kill "$live" 2>/dev/null || true
|
|
987
|
+
wait "$live" 2>/dev/null || true
|
|
988
|
+
[ -n "$baseline" ] || fail "fm_pid_identity produced no baseline identity under LC_ALL=C"
|
|
989
|
+
[ "$via_lc_all" = "$baseline" ] || fail "fm_pid_identity varied with exported LC_ALL (got '$via_lc_all', want '$baseline')"
|
|
990
|
+
[ "$via_lc_time" = "$baseline" ] || fail "fm_pid_identity varied with exported LC_TIME (got '$via_lc_time', want '$baseline')"
|
|
991
|
+
while read -r observed; do
|
|
992
|
+
[ "$observed" = C ] || fail "fm_pid_identity invoked ps without pinning LC_ALL=C (saw '$observed')"
|
|
993
|
+
done < "$locale_log"
|
|
994
|
+
if [ -n "$real_first" ]; then
|
|
995
|
+
[ "$real_second" = "$real_first" ] \
|
|
996
|
+
|| fail "real ps fallback varied with exported LC_TIME (got '$real_second', want '$real_first')"
|
|
997
|
+
pass "fm_pid_identity real ps fallback is locale-invariant"
|
|
998
|
+
else
|
|
999
|
+
pass "real ps fallback locale check skipped where ps -o lstart= is unsupported"
|
|
1000
|
+
fi
|
|
1001
|
+
pass "fm_pid_identity is locale-invariant across LC_ALL/LC_TIME"
|
|
1002
|
+
}
|
|
1003
|
+
|
|
1004
|
+
write_fake_proc_identity() {
|
|
1005
|
+
local proc_root=$1 pid=$2 starttime=$3
|
|
1006
|
+
mkdir -p "$proc_root/$pid"
|
|
1007
|
+
printf '%s\n' "$pid (watcher ) with spaces) S 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 $starttime 20 21 22" > "$proc_root/$pid/stat"
|
|
1008
|
+
printf 'bash\0/path with spaces/fm-watch.sh\0--flag\0' > "$proc_root/$pid/cmdline"
|
|
1009
|
+
}
|
|
1010
|
+
|
|
1011
|
+
test_proc_pid_identity_ignores_wall_clock_and_detects_pid_reuse() {
|
|
1012
|
+
local dir state proc_root pid identity_key before after_time_jump after_pid_reuse
|
|
1013
|
+
dir=$(make_case proc-pid-identity)
|
|
1014
|
+
state="$dir/state"
|
|
1015
|
+
proc_root="$dir/proc"
|
|
1016
|
+
pid=4242
|
|
1017
|
+
identity_key=proc-starttime
|
|
1018
|
+
[ "$(uname)" != Linux ] || identity_key=linux-starttime
|
|
1019
|
+
mkdir -p "$proc_root"
|
|
1020
|
+
printf 'btime 1784094040\n' > "$proc_root/stat"
|
|
1021
|
+
write_fake_proc_identity "$proc_root" "$pid" 987654
|
|
1022
|
+
|
|
1023
|
+
before=$(FM_PROC_ROOT_OVERRIDE="$proc_root" FM_STATE_OVERRIDE="$state" bash -c '. "$1"; fm_pid_identity "$2"' _ "$LIB" "$pid") \
|
|
1024
|
+
|| fail "could not read initial fake Linux process identity"
|
|
1025
|
+
printf 'btime 1784094016\n' > "$proc_root/stat"
|
|
1026
|
+
after_time_jump=$(FM_PROC_ROOT_OVERRIDE="$proc_root" FM_STATE_OVERRIDE="$state" bash -c '. "$1"; fm_pid_identity "$2"' _ "$LIB" "$pid") \
|
|
1027
|
+
|| fail "could not re-read fake Linux process identity after btime change"
|
|
1028
|
+
|
|
1029
|
+
[ "$after_time_jump" = "$before" ] \
|
|
1030
|
+
|| fail "/proc process identity changed with btime (before '$before', after '$after_time_jump')"
|
|
1031
|
+
[ "$before" = "$identity_key=987654 cmdline-hex=62617368002f706174682077697468207370616365732f666d2d77617463682e7368002d2d666c616700" ] \
|
|
1032
|
+
|| fail "/proc process identity did not combine parsed starttime field 22 with the full cmdline ('$before')"
|
|
1033
|
+
pass "/proc process identity ignores simulated btime changes"
|
|
1034
|
+
|
|
1035
|
+
write_fake_proc_identity "$proc_root" "$pid" 987655
|
|
1036
|
+
after_pid_reuse=$(FM_PROC_ROOT_OVERRIDE="$proc_root" FM_STATE_OVERRIDE="$state" bash -c '. "$1"; fm_pid_identity "$2"' _ "$LIB" "$pid") \
|
|
1037
|
+
|| fail "could not read reused fake /proc pid identity"
|
|
1038
|
+
[ "$after_pid_reuse" != "$before" ] || fail "/proc process identity missed changed starttime for reused pid"
|
|
1039
|
+
pass "/proc process identity detects pid reuse"
|
|
1040
|
+
}
|
|
1041
|
+
|
|
1042
|
+
test_stale_watch_reclaim_publishes_before_clear() {
|
|
1043
|
+
local dir state lockdir rc token
|
|
1044
|
+
dir=$(make_case stale-watch-publish-before-clear)
|
|
1045
|
+
state="$dir/state"
|
|
1046
|
+
lockdir="$state/.watch.lock"
|
|
1047
|
+
mkdir -p "$lockdir"
|
|
1048
|
+
printf '99999999\n' > "$lockdir/pid"
|
|
1049
|
+
|
|
1050
|
+
FM_STATE_OVERRIDE="$state" bash -c '
|
|
1051
|
+
. "$1"
|
|
1052
|
+
fm_lock_remove_path() {
|
|
1053
|
+
if [ "$1" = "$STATE/.watch.lock" ]; then
|
|
1054
|
+
kill -KILL "${BASHPID:-$$}"
|
|
1055
|
+
fi
|
|
1056
|
+
return 1
|
|
1057
|
+
}
|
|
1058
|
+
fm_lock_try_acquire "$2"
|
|
1059
|
+
' _ "$LIB" "$lockdir" >/dev/null 2>&1
|
|
1060
|
+
rc=$?
|
|
1061
|
+
[ "$rc" -ne 0 ] || fail "interrupted stale watcher reclaim unexpectedly completed"
|
|
1062
|
+
[ -e "$lockdir" ] || [ -L "$lockdir" ] \
|
|
1063
|
+
|| fail "stale watcher lock cleared before recovery publication boundary"
|
|
1064
|
+
token=$(FM_STATE_OVERRIDE="$state" bash -c '
|
|
1065
|
+
. "$1"
|
|
1066
|
+
fm_recovery_marker_read "$2" || exit 1
|
|
1067
|
+
printf "%s\n" "$FM_RECOVERY_MARKER_TOKEN"
|
|
1068
|
+
' _ "$LIB" "$state/.watcher-down") \
|
|
1069
|
+
|| fail "stale watcher reclaim interruption left no durable recovery evidence"
|
|
1070
|
+
case "$token" in
|
|
1071
|
+
pending:downtime:*) ;;
|
|
1072
|
+
*) fail "stale watcher reclaim published invalid recovery evidence: $token" ;;
|
|
1073
|
+
esac
|
|
1074
|
+
|
|
1075
|
+
FM_STATE_OVERRIDE="$state" bash -c '
|
|
1076
|
+
. "$1"
|
|
1077
|
+
fm_lock_try_acquire "$2" || exit 1
|
|
1078
|
+
fm_lock_release "$2"
|
|
1079
|
+
' _ "$LIB" "$lockdir" \
|
|
1080
|
+
|| fail "successor could not reclaim watcher lock after interrupted clear"
|
|
1081
|
+
pass "stale watcher reclaim publishes durable recovery evidence before clear"
|
|
1082
|
+
}
|
|
1083
|
+
|
|
1084
|
+
test_msys_pid_identity_uses_proc() {
|
|
1085
|
+
local live identity
|
|
1086
|
+
case "$(uname)" in
|
|
1087
|
+
MSYS*|MINGW*|CYGWIN*) ;;
|
|
1088
|
+
*)
|
|
1089
|
+
pass "MSYS /proc process identity regression skipped on non-Windows host"
|
|
1090
|
+
return
|
|
1091
|
+
;;
|
|
1092
|
+
esac
|
|
1093
|
+
sleep 300 &
|
|
1094
|
+
live=$!
|
|
1095
|
+
identity=$(bash -c '. "$1"; fm_pid_identity "$2"' _ "$LIB" "$live" 2>/dev/null)
|
|
1096
|
+
kill "$live" 2>/dev/null || true
|
|
1097
|
+
wait "$live" 2>/dev/null || true
|
|
1098
|
+
case "$identity" in
|
|
1099
|
+
proc-starttime=*" cmdline-hex="*) ;;
|
|
1100
|
+
*) fail "MSYS process identity did not use compatible /proc fields ('$identity')" ;;
|
|
1101
|
+
esac
|
|
1102
|
+
pass "MSYS process identity uses compatible /proc fields"
|
|
1103
|
+
}
|
|
1104
|
+
|
|
1105
|
+
test_singleton_start
|
|
1106
|
+
test_pid_identity_is_locale_invariant
|
|
1107
|
+
test_proc_pid_identity_ignores_wall_clock_and_detects_pid_reuse
|
|
1108
|
+
test_msys_pid_identity_uses_proc
|
|
1109
|
+
test_stale_watch_lock_reclaimed
|
|
1110
|
+
test_stale_watch_reclaim_publishes_before_clear
|
|
1111
|
+
test_live_stale_watch_lock_is_actionable
|
|
1112
|
+
test_guard_warnings
|
|
1113
|
+
test_lock_single_winner_under_concurrency
|
|
1114
|
+
test_lock_steals_dead_pid_lock
|
|
1115
|
+
test_lock_stale_steal_single_winner_under_concurrency
|
|
1116
|
+
test_lock_live_steal_mutex_is_not_reclaimed
|
|
1117
|
+
test_lock_does_not_steal_live_lock
|
|
1118
|
+
test_lock_empty_pid_uses_minimum_grace
|
|
1119
|
+
test_lock_late_claim_loses_after_recreate
|
|
1120
|
+
test_lock_paused_mid_acquire_claim_fails_during_steal
|
|
1121
|
+
test_watch_restart_rejects_reused_pid
|
|
1122
|
+
test_watch_restart_attaches_to_healthy_peer
|
|
1123
|
+
test_watcher_self_evicts_on_lock_takeover
|
|
1124
|
+
test_arm_self_eviction_is_loud_without_successor
|
|
1125
|
+
test_arm_attaches_and_waits_for_live_fresh_watcher
|
|
1126
|
+
test_attached_arm_signal_is_recorded_in_cycle_ledger
|
|
1127
|
+
test_arm_starts_and_self_heals
|
|
1128
|
+
test_arm_hup_cleans_child_and_temp_output
|
|
1129
|
+
test_arm_propagates_immediate_wake_before_confirmation
|
|
1130
|
+
test_arm_waits_for_peer_beacon_after_child_stands_down
|
|
1131
|
+
test_arm_fails_loud_when_no_fresh_watcher_confirmable
|
|
1132
|
+
test_cycle_exit_ledger_links_successor_and_stays_bounded
|
|
1133
|
+
test_stopped_watcher_is_live_but_stale_then_exit_is_classified
|