@zerwiz/ymir 0.1.3 → 0.1.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.agents/README.md +291 -0
- package/.agents/agents/bragi-marketer.md +111 -0
- package/.agents/agents/brokk.md +352 -0
- package/.agents/agents/forseti-reviewer.md +93 -0
- package/.agents/agents/frigg-consent.md +77 -0
- package/.agents/agents/groa-update.md +77 -0
- package/.agents/agents/hnoss-designer.md +69 -0
- package/.agents/agents/huginn-researcher.md +99 -0
- package/.agents/agents/jord-projects.md +78 -0
- package/.agents/agents/kvasir-scout.md +91 -0
- package/.agents/agents/mimir-planner.md +92 -0
- package/.agents/agents/muninn-stow.md +77 -0
- package/.agents/agents/saga-bearings.md +78 -0
- package/.agents/agents/sindri-developer.md +114 -0
- package/.agents/agents/snotra-documenter.md +105 -0
- package/.agents/agents/syn-recovery.md +78 -0
- package/.agents/agents/tyr-check.md +78 -0
- package/.agents/agents/urdh-hold.md +77 -0
- package/.agents/agents/volundr-smith.md +80 -0
- package/.agents/agents/vor-diagnostics.md +77 -0
- package/.agents/assets/agents/naming.md +50 -0
- package/.agents/assets/agents/registry.md +93 -0
- package/.agents/assets/agents/runtime.md +36 -0
- package/.agents/assets/agents/toon-tasks-cli.md +42 -0
- package/.agents/assets/templates/company_entity.template.md +37 -0
- package/.agents/backend/backends/cmux.sh +649 -0
- package/.agents/backend/backends/herdr-eventwait.py +157 -0
- package/.agents/backend/backends/herdr-workspace-move.py +114 -0
- package/.agents/backend/backends/herdr.sh +3386 -0
- package/.agents/backend/backends/orca.sh +290 -0
- package/.agents/backend/backends/tmux.sh +357 -0
- package/.agents/backend/backends/zellij.sh +700 -0
- package/.agents/backend/fm-afk-launch.sh +654 -0
- package/.agents/backend/fm-afk-return.sh +241 -0
- package/.agents/backend/fm-afk-start.sh +172 -0
- package/.agents/backend/fm-arm-command-policy.mjs +960 -0
- package/.agents/backend/fm-arm-pretool-check.sh +200 -0
- package/.agents/backend/fm-backend-hometag-lib.sh +52 -0
- package/.agents/backend/fm-backend.sh +981 -0
- package/.agents/backend/fm-backlog-handoff.sh +909 -0
- package/.agents/backend/fm-backlog-receive.sh +187 -0
- package/.agents/backend/fm-backlog-transition-lib.sh +779 -0
- package/.agents/backend/fm-bearings-board.sh +204 -0
- package/.agents/backend/fm-bearings-snapshot.sh +546 -0
- package/.agents/backend/fm-bootstrap.sh +1568 -0
- package/.agents/backend/fm-branch-outcome.sh +203 -0
- package/.agents/backend/fm-branch-prompt.sh +101 -0
- package/.agents/backend/fm-brief.sh +452 -0
- package/.agents/backend/fm-busy-event.sh +234 -0
- package/.agents/backend/fm-busy-lib.sh +986 -0
- package/.agents/backend/fm-captain-hold.sh +1001 -0
- package/.agents/backend/fm-cd-command-policy.mjs +153 -0
- package/.agents/backend/fm-cd-pretool-check.sh +190 -0
- package/.agents/backend/fm-check-lib.sh +74 -0
- package/.agents/backend/fm-check-register.sh +42 -0
- package/.agents/backend/fm-check-unregister.sh +52 -0
- package/.agents/backend/fm-classify-lib.sh +1770 -0
- package/.agents/backend/fm-claude-stop-autoarm.sh +334 -0
- package/.agents/backend/fm-composer-lib.sh +1416 -0
- package/.agents/backend/fm-config-inherit-lib.sh +1202 -0
- package/.agents/backend/fm-config-push.sh +254 -0
- package/.agents/backend/fm-control-lib.sh +251 -0
- package/.agents/backend/fm-control.sh +878 -0
- package/.agents/backend/fm-crew-state.sh +625 -0
- package/.agents/backend/fm-cursor-lib.sh +243 -0
- package/.agents/backend/fm-decision-hold.sh +232 -0
- package/.agents/backend/fm-doc-audience-check.sh +269 -0
- package/.agents/backend/fm-dod-lib.sh +67 -0
- package/.agents/backend/fm-ensure-agents-md.sh +253 -0
- package/.agents/backend/fm-extension-launch-barrier.mjs +129 -0
- package/.agents/backend/fm-extension.mjs +2577 -0
- package/.agents/backend/fm-extension.sh +16 -0
- package/.agents/backend/fm-ff-lib.sh +420 -0
- package/.agents/backend/fm-fleet-snapshot.sh +1463 -0
- package/.agents/backend/fm-fleet-sync.sh +458 -0
- package/.agents/backend/fm-fleet-view.sh +96 -0
- package/.agents/backend/fm-gate-refuse-lib.sh +102 -0
- package/.agents/backend/fm-guard.sh +242 -0
- package/.agents/backend/fm-harness.sh +195 -0
- package/.agents/backend/fm-herdr-ci-cleanup.sh +115 -0
- package/.agents/backend/fm-herdr-lab.sh +346 -0
- package/.agents/backend/fm-herdr-session-cleanup.sh +337 -0
- package/.agents/backend/fm-home-seed.sh +984 -0
- package/.agents/backend/fm-home-summary-refresh.sh +255 -0
- package/.agents/backend/fm-hook-host-lib.sh +36 -0
- package/.agents/backend/fm-inactive-reconcile.sh +531 -0
- package/.agents/backend/fm-inbox.sh +399 -0
- package/.agents/backend/fm-install-actionlint.sh +84 -0
- package/.agents/backend/fm-install-herdr.sh +96 -0
- package/.agents/backend/fm-install-shellcheck.sh +84 -0
- package/.agents/backend/fm-install-treehouse.sh +95 -0
- package/.agents/backend/fm-kimi-turnend-hook.sh +276 -0
- package/.agents/backend/fm-lease-lib.sh +218 -0
- package/.agents/backend/fm-lease.sh +189 -0
- package/.agents/backend/fm-line-cap-lib.sh +51 -0
- package/.agents/backend/fm-lint-workflows.sh +137 -0
- package/.agents/backend/fm-lint.sh +591 -0
- package/.agents/backend/fm-lock-lib.sh +104 -0
- package/.agents/backend/fm-lock.sh +107 -0
- package/.agents/backend/fm-marker-lib.sh +12 -0
- package/.agents/backend/fm-merge-local.sh +74 -0
- package/.agents/backend/fm-merge-outcome-lib.sh +138 -0
- package/.agents/backend/fm-nm-run-lib.sh +124 -0
- package/.agents/backend/fm-on.sh +125 -0
- package/.agents/backend/fm-operational-input.sh +253 -0
- package/.agents/backend/fm-peek.sh +46 -0
- package/.agents/backend/fm-pending-reply-lib.sh +1409 -0
- package/.agents/backend/fm-pr-check.sh +135 -0
- package/.agents/backend/fm-pr-lib.sh +1016 -0
- package/.agents/backend/fm-pr-merge.sh +707 -0
- package/.agents/backend/fm-pr-poll.sh +110 -0
- package/.agents/backend/fm-primary-scope-lib.sh +33 -0
- package/.agents/backend/fm-procevent-extension-capture.pl +259 -0
- package/.agents/backend/fm-procevent-lavish.sh +628 -0
- package/.agents/backend/fm-procevent-lib.sh +892 -0
- package/.agents/backend/fm-procevent-quota.sh +290 -0
- package/.agents/backend/fm-procevent-remote-reply.sh +577 -0
- package/.agents/backend/fm-procevent-when.sh +504 -0
- package/.agents/backend/fm-procevent.sh +1466 -0
- package/.agents/backend/fm-project-mode.sh +92 -0
- package/.agents/backend/fm-project-origin-lib.sh +180 -0
- package/.agents/backend/fm-promote.sh +282 -0
- package/.agents/backend/fm-public-followup-emit.sh +260 -0
- package/.agents/backend/fm-public-followup-lib.sh +398 -0
- package/.agents/backend/fm-public-followup.sh +1330 -0
- package/.agents/backend/fm-push-transition-lib.sh +168 -0
- package/.agents/backend/fm-quota-axi-lib.sh +93 -0
- package/.agents/backend/fm-quota-choose.sh +384 -0
- package/.agents/backend/fm-remote-delta-read.sh +189 -0
- package/.agents/backend/fm-remote-doctor.sh +800 -0
- package/.agents/backend/fm-remote-entrypoint.sh +193 -0
- package/.agents/backend/fm-remote-file.sh +234 -0
- package/.agents/backend/fm-remote-home-provision.sh +259 -0
- package/.agents/backend/fm-remote-home-seed.sh +259 -0
- package/.agents/backend/fm-remote-inherit-push.sh +90 -0
- package/.agents/backend/fm-remote-inherit.sh +179 -0
- package/.agents/backend/fm-remote-job-lib.sh +1241 -0
- package/.agents/backend/fm-remote-job-reap-orphans.sh +137 -0
- package/.agents/backend/fm-remote-job-worker.sh +1124 -0
- package/.agents/backend/fm-remote-readiness-lib.sh +44 -0
- package/.agents/backend/fm-remote-secondmate-control.sh +340 -0
- package/.agents/backend/fm-review-diff.sh +158 -0
- package/.agents/backend/fm-secondmate-charter-lib.sh +44 -0
- package/.agents/backend/fm-secondmate-nudge-lib.sh +68 -0
- package/.agents/backend/fm-secondmate-parent-lib.sh +70 -0
- package/.agents/backend/fm-secondmate-reconcile.sh +371 -0
- package/.agents/backend/fm-secondmate-registry-lib.sh +311 -0
- package/.agents/backend/fm-secondmate-report.sh +88 -0
- package/.agents/backend/fm-send.sh +1059 -0
- package/.agents/backend/fm-session-lock-lib.sh +176 -0
- package/.agents/backend/fm-session-start.sh +970 -0
- package/.agents/backend/fm-sessionstart-cursor.sh +40 -0
- package/.agents/backend/fm-sessionstart-nudge.sh +45 -0
- package/.agents/backend/fm-sessionstart-run.sh +145 -0
- package/.agents/backend/fm-spawn.sh +3153 -0
- package/.agents/backend/fm-startup-memory-budget-lib.sh +224 -0
- package/.agents/backend/fm-startup-memory-budget.sh +94 -0
- package/.agents/backend/fm-startup-network.sh +642 -0
- package/.agents/backend/fm-stow-cascade.sh +251 -0
- package/.agents/backend/fm-subagent-pretool-check.sh +207 -0
- package/.agents/backend/fm-supervise-daemon.sh +1732 -0
- package/.agents/backend/fm-supervision-instructions.sh +215 -0
- package/.agents/backend/fm-supervision-lib.sh +90 -0
- package/.agents/backend/fm-supervisor-target-lib.sh +78 -0
- package/.agents/backend/fm-tangle-lib.sh +53 -0
- package/.agents/backend/fm-task-inbox-lib.sh +402 -0
- package/.agents/backend/fm-tasks-axi-lib.sh +123 -0
- package/.agents/backend/fm-teardown.sh +2922 -0
- package/.agents/backend/fm-test-isolation-proof.sh +588 -0
- package/.agents/backend/fm-test-run.sh +2191 -0
- package/.agents/backend/fm-timeout-lib.sh +141 -0
- package/.agents/backend/fm-timing-lib.sh +179 -0
- package/.agents/backend/fm-tmux-lib.sh +291 -0
- package/.agents/backend/fm-tool-update-check.sh +898 -0
- package/.agents/backend/fm-trace-context-lib.sh +227 -0
- package/.agents/backend/fm-transition-lib.sh +103 -0
- package/.agents/backend/fm-turnend-guard-cursor.sh +391 -0
- package/.agents/backend/fm-turnend-guard-grok.sh +90 -0
- package/.agents/backend/fm-turnend-guard.sh +432 -0
- package/.agents/backend/fm-update.sh +110 -0
- package/.agents/backend/fm-vendor-auth-probe.sh +190 -0
- package/.agents/backend/fm-voice-client.py +1373 -0
- package/.agents/backend/fm-voice-relay.py +1256 -0
- package/.agents/backend/fm-wake-drain.sh +592 -0
- package/.agents/backend/fm-wake-grant.sh +127 -0
- package/.agents/backend/fm-wake-lib.sh +1849 -0
- package/.agents/backend/fm-watch-arm.sh +602 -0
- package/.agents/backend/fm-watch-checkpoint.sh +109 -0
- package/.agents/backend/fm-watch.sh +1962 -0
- package/.agents/backend/fm-x-dismiss.sh +116 -0
- package/.agents/backend/fm-x-followup.sh +286 -0
- package/.agents/backend/fm-x-lib.sh +1000 -0
- package/.agents/backend/fm-x-link.sh +234 -0
- package/.agents/backend/fm-x-poll.sh +185 -0
- package/.agents/backend/fm-x-reply.sh +385 -0
- package/.agents/backend/fm_voice_frame.py +166 -0
- package/.agents/backend/fm_voice_records.py +574 -0
- package/.agents/backend/model-bridge.py +290 -0
- package/.agents/backend/opencode-go-bridge.py +30 -0
- package/.agents/bus/README.md +12 -0
- package/.agents/bus/messages.json +1 -0
- package/.agents/config/agents.machine.example.yaml +16 -0
- package/.agents/config/agents.yaml +50 -0
- package/.agents/config/agents.yaml.example +72 -0
- package/.agents/config/crew-dispatch.json +22 -0
- package/.agents/config/cron.yaml +9 -0
- package/.agents/config/eindri-dispatch.json +67 -0
- package/.agents/config/eindri-harness +1 -0
- package/.agents/config/startup-memory-budget +1 -0
- package/.agents/config/tailscale-sync.yaml +27 -0
- package/.agents/filebrowser/.filebrowser.json +6 -0
- package/.agents/filebrowser/README.md +11 -0
- package/.agents/gateway/README.md +12 -0
- package/.agents/github/README.md +15 -0
- package/.agents/github/webhooks/.gitkeep +0 -0
- package/.agents/github/workflows/.gitkeep +0 -0
- package/.agents/harness/opencode/plugins/lib/rodd-operational-input.js +37 -0
- package/.agents/harness/opencode/plugins/package.json +4 -0
- package/.agents/harness/opencode/plugins/saga-sessionstart.js +77 -0
- package/.agents/harness/opencode/plugins/syn-cd-check.js +61 -0
- package/.agents/harness/opencode/plugins/syn-pretool-check.js +68 -0
- package/.agents/harness/opencode/plugins/syn-turnend-guard.js +105 -0
- package/.agents/harness/opencode/plugins/syn-watch-arm.js +521 -0
- package/.agents/memory/README.md +46 -0
- package/.agents/memory/kaia.engram +0 -0
- package/.agents/memory/kaia.engram-shm +0 -0
- package/.agents/memory/kaia.engram-wal +0 -0
- package/.agents/memory/runes_audit.md +5 -0
- package/.agents/memory/well/episodes.jsonl +367 -0
- package/.agents/memory/well/workspace.jsonl +27 -0
- package/.agents/migrations/0001-hodd-layout.sh +38 -0
- package/.agents/migrations/0002-a2a-mcp.sh +27 -0
- package/.agents/migrations/0003-private-data-separation.sh +111 -0
- package/.agents/migrations/0004-hoard-and-realms.sh +104 -0
- package/.agents/sandbox/Dockerfile.utgard +19 -0
- package/.agents/sandbox/README.md +14 -0
- package/.agents/sandbox/utgard.config.json +14 -0
- package/.agents/skills/README.md +59 -0
- package/.agents/skills/bragi-marketing/README.md +3 -0
- package/.agents/skills/bragi-marketing/SKILL.md +82 -0
- package/.agents/skills/eindri-homes/README.md +3 -0
- package/.agents/skills/eindri-homes/SKILL.md +291 -0
- package/.agents/skills/eindri-homes/assets/control-plane.md +122 -0
- package/.agents/skills/eindri-homes/assets/dispatch.md +80 -0
- package/.agents/skills/frigg-consent/README.md +3 -0
- package/.agents/skills/frigg-consent/SKILL.md +56 -0
- package/.agents/skills/galdr-ymirsystem/README.md +3 -0
- package/.agents/skills/galdr-ymirsystem/SKILL.md +158 -0
- package/.agents/skills/galdr-ymirsystem/assets/README.md +74 -0
- package/.agents/skills/galdr-ymirsystem/assets/brokk-distro-runtime.md +390 -0
- package/.agents/skills/galdr-ymirsystem/assets/build-method.md +113 -0
- package/.agents/skills/galdr-ymirsystem/assets/build-tool-categories.md +178 -0
- package/.agents/skills/galdr-ymirsystem/assets/eindri-orchestration.md +518 -0
- package/.agents/skills/galdr-ymirsystem/assets/eindri-profiles.md +136 -0
- package/.agents/skills/galdr-ymirsystem/assets/harness-integration/README.md +673 -0
- package/.agents/skills/galdr-ymirsystem/assets/harness-integration/claude-code.md +197 -0
- package/.agents/skills/galdr-ymirsystem/assets/harness-integration/codex.md +191 -0
- package/.agents/skills/galdr-ymirsystem/assets/harness-integration/cursor.md +193 -0
- package/.agents/skills/galdr-ymirsystem/assets/harness-integration/opencode.md +262 -0
- package/.agents/skills/galdr-ymirsystem/assets/harness-integration/pi.md +295 -0
- package/.agents/skills/galdr-ymirsystem/assets/hlidskjalf-ui.md +496 -0
- package/.agents/skills/galdr-ymirsystem/assets/installation.md +571 -0
- package/.agents/skills/galdr-ymirsystem/assets/local-models/HOST-DATA.example.md +72 -0
- package/.agents/skills/galdr-ymirsystem/assets/local-models.md +274 -0
- package/.agents/skills/galdr-ymirsystem/assets/memory-well.md +161 -0
- package/.agents/skills/galdr-ymirsystem/assets/nornir-jobs.md +377 -0
- package/.agents/skills/galdr-ymirsystem/assets/norse-naming.md +325 -0
- package/.agents/skills/galdr-ymirsystem/assets/odrerir-hall.md +129 -0
- package/.agents/skills/galdr-ymirsystem/assets/pi-boot/einherjar-spawn.schema.json +81 -0
- package/.agents/skills/galdr-ymirsystem/assets/pi-boot/herdr-profile.toml +75 -0
- package/.agents/skills/galdr-ymirsystem/assets/pi-boot/pi-profile.yml +96 -0
- package/.agents/skills/galdr-ymirsystem/assets/pi-boot/supervision-tree.yml +163 -0
- package/.agents/skills/galdr-ymirsystem/assets/pi-boot-guide.md +58 -0
- package/.agents/skills/galdr-ymirsystem/assets/porting-upstream-to-norse.md +301 -0
- package/.agents/skills/galdr-ymirsystem/assets/principles.md +385 -0
- package/.agents/skills/galdr-ymirsystem/assets/reference-adoption.md +99 -0
- package/.agents/skills/galdr-ymirsystem/assets/registry.md +379 -0
- package/.agents/skills/galdr-ymirsystem/assets/runtime-compliance.md +463 -0
- package/.agents/skills/galdr-ymirsystem/assets/runtime-components.md +553 -0
- package/.agents/skills/galdr-ymirsystem/assets/smidja.md +175 -0
- package/.agents/skills/galdr-ymirsystem/schemas/toon-schemas.md +190 -0
- package/.agents/skills/galdr-ymirsystem/scripts/README.md +45 -0
- package/.agents/skills/galdr-ymirsystem/scripts/bench-one.sh +248 -0
- package/.agents/skills/galdr-ymirsystem/scripts/compliance-check.sh +360 -0
- package/.agents/skills/galdr-ymirsystem/scripts/gpu-sample.sh +43 -0
- package/.agents/skills/galdr-ymirsystem/scripts/stop-all.sh +34 -0
- package/.agents/skills/galdr-ymirsystem/scripts/toon-check.py +188 -0
- package/.agents/skills/gjallarhorn-relay/README.md +3 -0
- package/.agents/skills/gjallarhorn-relay/SKILL.md +283 -0
- package/.agents/skills/groa-update/SKILL.md +70 -0
- package/.agents/skills/hamr-adapters/README.md +3 -0
- package/.agents/skills/hamr-adapters/SKILL.md +123 -0
- package/.agents/skills/hamr-adapters/references/common/control-and-recovery.md +37 -0
- package/.agents/skills/hamr-adapters/references/common/dispatch.md +32 -0
- package/.agents/skills/hamr-adapters/references/common/model-and-effort.md +42 -0
- package/.agents/skills/hamr-adapters/references/common/primary-hooks.md +40 -0
- package/.agents/skills/hamr-adapters/references/harness/claude.md +55 -0
- package/.agents/skills/hamr-adapters/references/harness/codex.md +43 -0
- package/.agents/skills/hamr-adapters/references/harness/cursor.md +75 -0
- package/.agents/skills/hamr-adapters/references/harness/grok.md +69 -0
- package/.agents/skills/hamr-adapters/references/harness/kimi.md +51 -0
- package/.agents/skills/hamr-adapters/references/harness/muse.md +70 -0
- package/.agents/skills/hamr-adapters/references/harness/opencode.md +42 -0
- package/.agents/skills/hamr-adapters/references/harness/pi.md +56 -0
- package/.agents/skills/herdr-panes/README.md +3 -0
- package/.agents/skills/herdr-panes/SKILL.md +92 -0
- package/.agents/skills/herdr-panes/assets/herdr-backend.md +346 -0
- package/.agents/skills/herdr-panes/assets/tmux-backend.md +115 -0
- package/.agents/skills/hnoss-design/DESIGN.md +19 -0
- package/.agents/skills/hnoss-design/README.md +3 -0
- package/.agents/skills/hnoss-design/SKILL.md +83 -0
- package/.agents/skills/hvild-afk/README.md +3 -0
- package/.agents/skills/hvild-afk/SKILL.md +241 -0
- package/.agents/skills/jord-projects/README.md +3 -0
- package/.agents/skills/jord-projects/SKILL.md +92 -0
- package/.agents/skills/lifecycle/SKILL.md +56 -0
- package/.agents/skills/lifecycle/smoke_test.sh +52 -0
- package/.agents/skills/lifecycle/start.sh +21 -0
- package/.agents/skills/lifecycle/status.sh +35 -0
- package/.agents/skills/lifecycle/stop.sh +16 -0
- package/.agents/skills/muninn-stow/README.md +3 -0
- package/.agents/skills/muninn-stow/SKILL.md +309 -0
- package/.agents/skills/no-mistakes/SKILL.md +379 -0
- package/.agents/skills/nornir-schedule/README.md +3 -0
- package/.agents/skills/nornir-schedule/SKILL.md +25 -0
- package/.agents/skills/nornir-schedule/assets/events.md +137 -0
- package/.agents/skills/nornir-schedule/assets/quota.md +131 -0
- package/.agents/skills/nsr-compliance/README.md +3 -0
- package/.agents/skills/nsr-compliance/SKILL.md +29 -0
- package/.agents/skills/nsr-compliance/assets/nsr/README.md +40 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/RULES/README.md +15 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/RULES/git-workflow.md +18 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/RULES/security.md +22 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/RULES/tech-stack.md +8 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/SKILL.md +71 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/config/core_four.yaml +16 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/gates/check_danger.sh +102 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/gates/check_wiring.sh +69 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/gates/validate_code.sh +29 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/gates/verify_docs.py +66 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/generate.sh +109 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/harness/envelopes/result_envelope.json +14 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/harness/envelopes/task_envelope.json +20 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/harness/runner.py +303 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/telemetry/logger.py +112 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/templates/compliance/README.md +18 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/templates/compliance/config/README.md +10 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/templates/compliance/gates/README.md +12 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/templates/compliance/gates/check_env.sh +23 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/templates/compliance/gates/check_paths.sh +20 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/templates/compliance/gates/check_platform.sh +23 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/templates/compliance/harness/README.md +11 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/templates/compliance/harness/envelopes/README.md +10 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/templates/compliance/installer/README.md +10 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/templates/compliance/installer/install.sh +125 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/templates/compliance/telemetry/README.md +11 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/README.md +20 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/features/README.md +36 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/features/SKILL-template.md +14 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/git_ops/README.md +25 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/git_ops/SKILL.md +13 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/git_ops/create_branch.sh +6 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/git_ops/safe_commit.sh +9 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/git_ops/sync_upstream.sh +6 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/lifecycle/README.md +27 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/lifecycle/SKILL.md +15 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/lifecycle/smoke_test.sh +12 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/lifecycle/start.sh +19 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/lifecycle/status.sh +12 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/lifecycle/stop.sh +15 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/README.md +18 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/config/README.md +10 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/config/core_four.yaml +13 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/gates/README.md +12 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/gates/check_danger.sh +102 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/gates/check_env.sh +23 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/gates/check_paths.sh +20 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/gates/check_platform.sh +23 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/gates/check_wiring.sh +69 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/harness/README.md +11 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/harness/envelopes/README.md +10 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/harness/envelopes/result_envelope.json +14 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/harness/envelopes/task_envelope.json +20 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/harness/runner.py +303 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/installer/README.md +10 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/installer/install.sh +125 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/telemetry/README.md +11 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/telemetry/logger.py +112 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/BEST_PRACTICES/README.md +14 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/BEST_PRACTICES/api.md +17 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/BEST_PRACTICES/compliance-wiring.md +155 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/BEST_PRACTICES/frontend.md +15 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/CI_CD/README.md +11 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/CI_CD/deployment/README.md +23 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/CI_CD/deployment/clients/README.md +16 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/CI_CD/deployment/clients/template-README.md +11 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/CI_CD/deployment/deploy.sh +49 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/CI_CD/deployment/envs/README.md +12 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/CI_CD/deployment/envs/development.env.example +17 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/CI_CD/deployment/envs/production.env.example +17 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/CI_CD/deployment/envs/staging.env.example +17 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/CI_CD/deployment/tenants/README.md +11 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/CI_CD/deployment/tenants/template.env.example +8 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/DEVELOPER_SETUP/README.md +26 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/DEVELOPER_SETUP/agent.md +7 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/DEVELOPER_SETUP/developers/dev-1.md +20 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/DEVELOPER_SETUP/linux.md +9 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/DEVELOPER_SETUP/macos.md +10 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/DEVELOPER_SETUP/shared.md +19 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/DEVELOPER_SETUP/windows.md +14 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/HOSTING/README.md +25 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/HOSTING/_template.md +41 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/HOSTING/hosting-1.md +41 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/README.md +21 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/RUNBOOK/README.md +15 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/RUNBOOK/deployment.md +19 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/RUNBOOK/disaster-recovery.md +15 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/RUNBOOK/ops-tasks.md +14 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/features/README.md +16 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/project-skills.md +146 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/research/README.md +11 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/research/software-compliance-research.md +269 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/working-with-agents.md +260 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/root/AGENTS.md +38 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/root/ARCHITECTURE.md +36 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/root/BEST_PRACTICES.md +27 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/root/CI_CD.md +23 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/root/FEATURES.md +15 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/root/README.md +22 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/root/RULES.md +37 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/root/STRUCTURE.md +45 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/root/TECH_STACK.md +28 -0
- package/.agents/skills/nsr-compliance/assets/nsr/scaffold-spec.md +63 -0
- package/.agents/skills/nsr-compliance/assets/nsr/scripts/scaffold.sh +103 -0
- package/.agents/skills/nsr-compliance/assets/nsr/scripts/validate.sh +52 -0
- package/.agents/skills/nsr-compliance/assets/nsrcompliance/SKILL.md +71 -0
- package/.agents/skills/nsr-compliance/assets/nsrcompliance/config/core_four.yaml +16 -0
- package/.agents/skills/nsr-compliance/assets/nsrcompliance/gates/check_danger.sh +102 -0
- package/.agents/skills/nsr-compliance/assets/nsrcompliance/gates/check_wiring.sh +69 -0
- package/.agents/skills/nsr-compliance/assets/nsrcompliance/gates/validate_code.sh +29 -0
- package/.agents/skills/nsr-compliance/assets/nsrcompliance/gates/verify_docs.py +66 -0
- package/.agents/skills/nsr-compliance/assets/nsrcompliance/generate.sh +109 -0
- package/.agents/skills/nsr-compliance/assets/nsrcompliance/harness/envelopes/result_envelope.json +14 -0
- package/.agents/skills/nsr-compliance/assets/nsrcompliance/harness/envelopes/task_envelope.json +20 -0
- package/.agents/skills/nsr-compliance/assets/nsrcompliance/harness/runner.py +303 -0
- package/.agents/skills/nsr-compliance/assets/nsrcompliance/telemetry/logger.py +112 -0
- package/.agents/skills/nsr-compliance/assets/nsrcompliance/templates/compliance/README.md +18 -0
- package/.agents/skills/nsr-compliance/assets/nsrcompliance/templates/compliance/config/README.md +10 -0
- package/.agents/skills/nsr-compliance/assets/nsrcompliance/templates/compliance/gates/README.md +12 -0
- package/.agents/skills/nsr-compliance/assets/nsrcompliance/templates/compliance/gates/check_env.sh +23 -0
- package/.agents/skills/nsr-compliance/assets/nsrcompliance/templates/compliance/gates/check_paths.sh +20 -0
- package/.agents/skills/nsr-compliance/assets/nsrcompliance/templates/compliance/gates/check_platform.sh +23 -0
- package/.agents/skills/nsr-compliance/assets/nsrcompliance/templates/compliance/harness/README.md +11 -0
- package/.agents/skills/nsr-compliance/assets/nsrcompliance/templates/compliance/harness/envelopes/README.md +10 -0
- package/.agents/skills/nsr-compliance/assets/nsrcompliance/templates/compliance/installer/README.md +10 -0
- package/.agents/skills/nsr-compliance/assets/nsrcompliance/templates/compliance/installer/install.sh +125 -0
- package/.agents/skills/nsr-compliance/assets/nsrcompliance/templates/compliance/telemetry/README.md +11 -0
- package/.agents/skills/pr-ops/README.md +3 -0
- package/.agents/skills/pr-ops/SKILL.md +109 -0
- package/.agents/skills/ratatoskr-a2a/README.md +3 -0
- package/.agents/skills/ratatoskr-a2a/SKILL.md +148 -0
- package/.agents/skills/rules-check-drift/SKILL.md +81 -0
- package/.agents/skills/saga-bearings/README.md +3 -0
- package/.agents/skills/saga-bearings/SKILL.md +26 -0
- package/.agents/skills/saga-bearings/assets/bearings.md +161 -0
- package/.agents/skills/saga-bearings/assets/board-template.html +731 -0
- package/.agents/skills/saga-bearings/assets/recap.md +56 -0
- package/.agents/skills/syn-recovery/README.md +3 -0
- package/.agents/skills/syn-recovery/SKILL.md +54 -0
- package/.agents/skills/tyr-check/README.md +3 -0
- package/.agents/skills/tyr-check/SKILL.md +218 -0
- package/.agents/skills/urdh-hold/README.md +3 -0
- package/.agents/skills/urdh-hold/SKILL.md +25 -0
- package/.agents/skills/urdh-hold/assets/decisions.md +15 -0
- package/.agents/skills/urdh-hold/assets/hold.md +54 -0
- package/.agents/skills/vor-diagnostics/README.md +3 -0
- package/.agents/skills/vor-diagnostics/SKILL.md +68 -0
- package/.agents/skills/ymir-host/README.md +3 -0
- package/.agents/skills/ymir-host/SKILL.md +34 -0
- package/.agents/skills/ymir-host/assets/desktop.md +36 -0
- package/.agents/skills/ymir-host/assets/omarchy.md +297 -0
- package/.agents/skills/ymir-host/assets/runbooks.md +15 -0
- package/.agents/skills/ymir-host/assets/thjazi.md +220 -0
- package/.agents/skills/ymir-host/assets/update.md +64 -0
- package/.agents/tests/assets/board-render-harness.mjs +117 -0
- package/.agents/tests/cmux-test-safety.sh +51 -0
- package/.agents/tests/fixtures.sh +291 -0
- package/.agents/tests/fm-afk-inject-e2e.test.sh +428 -0
- package/.agents/tests/fm-afk-inject-herdr-e2e.test.sh +536 -0
- package/.agents/tests/fm-afk-launch.test.sh +961 -0
- package/.agents/tests/fm-afk-pi-herdr-return-e2e.test.sh +297 -0
- package/.agents/tests/fm-afk-return.test.sh +283 -0
- package/.agents/tests/fm-arm-pretool-check.test.sh +482 -0
- package/.agents/tests/fm-ask-user-authority.test.sh +42 -0
- package/.agents/tests/fm-backend-autodetect-smoke.test.sh +176 -0
- package/.agents/tests/fm-backend-cmux-smoke.test.sh +188 -0
- package/.agents/tests/fm-backend-cmux.test.sh +1166 -0
- package/.agents/tests/fm-backend-herdr-eventwait-smoke.test.sh +136 -0
- package/.agents/tests/fm-backend-herdr-eventwait.test.py +106 -0
- package/.agents/tests/fm-backend-herdr-focus-flash-e2e.test.sh +409 -0
- package/.agents/tests/fm-backend-herdr-launcher-workspace-e2e.test.sh +437 -0
- package/.agents/tests/fm-backend-herdr-presentation-e2e.test.sh +1424 -0
- package/.agents/tests/fm-backend-herdr-prune-safety-e2e.test.sh +180 -0
- package/.agents/tests/fm-backend-herdr-respawn-idem-e2e.test.sh +182 -0
- package/.agents/tests/fm-backend-herdr-smoke.test.sh +355 -0
- package/.agents/tests/fm-backend-herdr-workspace-per-home-e2e.test.sh +251 -0
- package/.agents/tests/fm-backend-herdr.test.sh +4606 -0
- package/.agents/tests/fm-backend-orca.test.sh +1357 -0
- package/.agents/tests/fm-backend-tmux-smoke.test.sh +173 -0
- package/.agents/tests/fm-backend-zellij-smoke.test.sh +205 -0
- package/.agents/tests/fm-backend-zellij.test.sh +1360 -0
- package/.agents/tests/fm-backend.test.sh +1142 -0
- package/.agents/tests/fm-backlog-atomicity.test.sh +2302 -0
- package/.agents/tests/fm-backlog-handoff.test.sh +1360 -0
- package/.agents/tests/fm-bearings-board-render.test.sh +135 -0
- package/.agents/tests/fm-bearings-board.test.sh +414 -0
- package/.agents/tests/fm-bearings-snapshot.test.sh +1990 -0
- package/.agents/tests/fm-bootstrap-network-parallel.test.sh +316 -0
- package/.agents/tests/fm-bootstrap.test.sh +1178 -0
- package/.agents/tests/fm-branch-supervision.test.sh +552 -0
- package/.agents/tests/fm-brief.test.sh +785 -0
- package/.agents/tests/fm-busy-adapter-wiring.test.sh +324 -0
- package/.agents/tests/fm-busy-state.test.sh +465 -0
- package/.agents/tests/fm-calm-pi-extension.test.sh +3979 -0
- package/.agents/tests/fm-captain-hold-lifecycle.test.sh +1187 -0
- package/.agents/tests/fm-cd-pretool-check.test.sh +400 -0
- package/.agents/tests/fm-check-unregister.test.sh +198 -0
- package/.agents/tests/fm-classify-corr-token.test.sh +543 -0
- package/.agents/tests/fm-classify-decision-key.test.sh +340 -0
- package/.agents/tests/fm-claude-stop-autoarm-live-e2e.test.sh +164 -0
- package/.agents/tests/fm-claude-stop-autoarm.test.sh +1190 -0
- package/.agents/tests/fm-cmux-claude-composer-live-e2e.test.sh +110 -0
- package/.agents/tests/fm-codex-continuity-live-e2e.test.sh +55 -0
- package/.agents/tests/fm-composer-ghost.test.sh +716 -0
- package/.agents/tests/fm-composer-lib.test.sh +666 -0
- package/.agents/tests/fm-composer-matrix-live-e2e.test.sh +220 -0
- package/.agents/tests/fm-control-herdr-smoke.test.sh +149 -0
- package/.agents/tests/fm-control-relaunch.test.sh +1530 -0
- package/.agents/tests/fm-control.test.sh +910 -0
- package/.agents/tests/fm-crew-state.test.sh +1610 -0
- package/.agents/tests/fm-cursor-harness.test.sh +404 -0
- package/.agents/tests/fm-cursor-primary-live-e2e.test.sh +217 -0
- package/.agents/tests/fm-cursor-primary.test.sh +708 -0
- package/.agents/tests/fm-daemon.test.sh +2740 -0
- package/.agents/tests/fm-documentation-audiences.test.sh +141 -0
- package/.agents/tests/fm-ensure-agents-md.test.sh +371 -0
- package/.agents/tests/fm-extension-binding.test.sh +2187 -0
- package/.agents/tests/fm-fleet-snapshot-view.test.sh +816 -0
- package/.agents/tests/fm-fleet-sync.test.sh +721 -0
- package/.agents/tests/fm-gate-refuse.test.sh +356 -0
- package/.agents/tests/fm-gitignore-config.test.sh +89 -0
- package/.agents/tests/fm-gotmp.test.sh +230 -0
- package/.agents/tests/fm-grok-continuity-live-e2e.test.sh +112 -0
- package/.agents/tests/fm-grok-harness.test.sh +117 -0
- package/.agents/tests/fm-grok-stop-live-e2e.test.sh +219 -0
- package/.agents/tests/fm-guard-stale-banner.test.sh +709 -0
- package/.agents/tests/fm-harness-adapter-instructions-live-e2e.test.sh +134 -0
- package/.agents/tests/fm-harness-adapter-references.test.sh +30 -0
- package/.agents/tests/fm-harness-liveness-drift-live-e2e.test.sh +151 -0
- package/.agents/tests/fm-herdr-lab.test.sh +243 -0
- package/.agents/tests/fm-herdr-session-cleanup-e2e.test.sh +146 -0
- package/.agents/tests/fm-herdr-session-cleanup.test.sh +329 -0
- package/.agents/tests/fm-herdr-submit-confirm-live-e2e.test.sh +125 -0
- package/.agents/tests/fm-herdr-version-floor-live-e2e.test.sh +144 -0
- package/.agents/tests/fm-home-summary-refresh.test.sh +971 -0
- package/.agents/tests/fm-inactive-reconcile.test.sh +485 -0
- package/.agents/tests/fm-kimi-harness.test.sh +687 -0
- package/.agents/tests/fm-lint-workflows.test.sh +532 -0
- package/.agents/tests/fm-lint.test.sh +1023 -0
- package/.agents/tests/fm-muse-harness.test.sh +932 -0
- package/.agents/tests/fm-muse-signals-live-e2e.test.sh +205 -0
- package/.agents/tests/fm-no-mistakes-required.test.sh +72 -0
- package/.agents/tests/fm-on.test.sh +507 -0
- package/.agents/tests/fm-opencode-primary-live-e2e.test.sh +357 -0
- package/.agents/tests/fm-operational-input.test.sh +160 -0
- package/.agents/tests/fm-peek-remote.test.sh +110 -0
- package/.agents/tests/fm-pending-reply.test.sh +1288 -0
- package/.agents/tests/fm-pi-branch-extension.test.sh +3182 -0
- package/.agents/tests/fm-pi-branch-live-e2e.test.sh +527 -0
- package/.agents/tests/fm-pi-primary-live-e2e.test.sh +345 -0
- package/.agents/tests/fm-pi-primary-types.test.sh +68 -0
- package/.agents/tests/fm-pi-watch-extension.test.sh +2843 -0
- package/.agents/tests/fm-pr-check-security.test.sh +2140 -0
- package/.agents/tests/fm-pr-merge.test.sh +2135 -0
- package/.agents/tests/fm-procevent-quota.test.sh +225 -0
- package/.agents/tests/fm-procevent-when.test.sh +406 -0
- package/.agents/tests/fm-procevent.test.sh +1745 -0
- package/.agents/tests/fm-project-origin.test.sh +110 -0
- package/.agents/tests/fm-public-followup.test.sh +2272 -0
- package/.agents/tests/fm-quota-array-dispatch-live-e2e.test.sh +526 -0
- package/.agents/tests/fm-quota-choose.test.sh +626 -0
- package/.agents/tests/fm-remote-backlog-handoff.test.sh +453 -0
- package/.agents/tests/fm-remote-doctor.test.sh +625 -0
- package/.agents/tests/fm-remote-entrypoint.test.sh +59 -0
- package/.agents/tests/fm-remote-job-orphan-reap.test.sh +205 -0
- package/.agents/tests/fm-remote-job.test.sh +768 -0
- package/.agents/tests/fm-remote-reply.test.sh +520 -0
- package/.agents/tests/fm-remote-secondmate-lifecycle-e2e.test.sh +1256 -0
- package/.agents/tests/fm-remote-secondmate-parent-binding.test.sh +326 -0
- package/.agents/tests/fm-remote-secondmate-trace-context.test.sh +310 -0
- package/.agents/tests/fm-remote-transport-lanes.test.sh +431 -0
- package/.agents/tests/fm-review-diff.test.sh +176 -0
- package/.agents/tests/fm-secondmate-harness.test.sh +2600 -0
- package/.agents/tests/fm-secondmate-lifecycle-e2e.test.sh +259 -0
- package/.agents/tests/fm-secondmate-liveness.test.sh +559 -0
- package/.agents/tests/fm-secondmate-reconcile.test.sh +732 -0
- package/.agents/tests/fm-secondmate-safety.test.sh +2972 -0
- package/.agents/tests/fm-secondmate-sync.test.sh +913 -0
- package/.agents/tests/fm-send-inbox-doorbell-live-e2e.test.sh +206 -0
- package/.agents/tests/fm-send-inbox.test.sh +352 -0
- package/.agents/tests/fm-send-popup-settle.test.sh +167 -0
- package/.agents/tests/fm-send-remote-delivery.test.sh +796 -0
- package/.agents/tests/fm-send-resolve-key.test.sh +551 -0
- package/.agents/tests/fm-send-secondmate-marker-herdr-e2e.test.sh +187 -0
- package/.agents/tests/fm-send-secondmate-marker.test.sh +278 -0
- package/.agents/tests/fm-send-settle.test.sh +147 -0
- package/.agents/tests/fm-send-strict.test.sh +241 -0
- package/.agents/tests/fm-session-lock-ancestry.test.sh +365 -0
- package/.agents/tests/fm-session-start.test.sh +2515 -0
- package/.agents/tests/fm-sessionstart-hook-live-e2e.test.sh +631 -0
- package/.agents/tests/fm-sessionstart-instruction-refresh-live-e2e.test.sh +230 -0
- package/.agents/tests/fm-sessionstart-nudge.test.sh +1039 -0
- package/.agents/tests/fm-shared-captain-inheritance.test.sh +404 -0
- package/.agents/tests/fm-spawn-batch.test.sh +148 -0
- package/.agents/tests/fm-spawn-dispatch-profile.test.sh +827 -0
- package/.agents/tests/fm-spawn-pool-base-freshen.test.sh +440 -0
- package/.agents/tests/fm-spawn-worktree-settle.test.sh +147 -0
- package/.agents/tests/fm-startup-memory-budget.test.sh +333 -0
- package/.agents/tests/fm-startup-network.test.sh +712 -0
- package/.agents/tests/fm-stow-cascade.test.sh +370 -0
- package/.agents/tests/fm-subagent-pretool-check.test.sh +291 -0
- package/.agents/tests/fm-supervision-events.test.sh +156 -0
- package/.agents/tests/fm-supervision-instructions.test.sh +189 -0
- package/.agents/tests/fm-tangle-guard.test.sh +277 -0
- package/.agents/tests/fm-task-delivery.test.sh +420 -0
- package/.agents/tests/fm-task-inbox.test.sh +513 -0
- package/.agents/tests/fm-teardown-endpoint-safety.test.sh +374 -0
- package/.agents/tests/fm-teardown.test.sh +2665 -0
- package/.agents/tests/fm-test-fixture-cleanup.test.sh +172 -0
- package/.agents/tests/fm-test-fixtures.test.sh +132 -0
- package/.agents/tests/fm-test-isolation-proof.test.sh +278 -0
- package/.agents/tests/fm-test-run.test.sh +1196 -0
- package/.agents/tests/fm-tmux-agent-liveness.test.sh +354 -0
- package/.agents/tests/fm-tmux-submit-busy.test.sh +340 -0
- package/.agents/tests/fm-tool-update-check.test.sh +1044 -0
- package/.agents/tests/fm-trace-context-lib.test.sh +253 -0
- package/.agents/tests/fm-trace-context-spawn.test.sh +581 -0
- package/.agents/tests/fm-transition-lib.test.sh +54 -0
- package/.agents/tests/fm-turnend-guard.test.sh +1822 -0
- package/.agents/tests/fm-update.test.sh +304 -0
- package/.agents/tests/fm-vendor-auth-probe.test.sh +395 -0
- package/.agents/tests/fm-voice-relay.test.sh +4741 -0
- package/.agents/tests/fm-wake-daemon-lifecycle-e2e.test.sh +171 -0
- package/.agents/tests/fm-wake-drain-open-decisions-cursor.test.sh +356 -0
- package/.agents/tests/fm-wake-drain-open-decisions.test.sh +226 -0
- package/.agents/tests/fm-wake-drain-unread-status.test.sh +379 -0
- package/.agents/tests/fm-wake-queue.test.sh +1224 -0
- package/.agents/tests/fm-watch-arm.test.sh +821 -0
- package/.agents/tests/fm-watch-checkpoint.test.sh +87 -0
- package/.agents/tests/fm-watch-recovery-loop.test.sh +223 -0
- package/.agents/tests/fm-watch-triage.test.sh +3889 -0
- package/.agents/tests/fm-watcher-lock.test.sh +1133 -0
- package/.agents/tests/fm-x-mode.test.sh +3042 -0
- package/.agents/tests/gleipnir-guard-pretool-check.test.sh +51 -0
- package/.agents/tests/gleipnir-machine-lock.test.sh +130 -0
- package/.agents/tests/herdr-test-safety.sh +42 -0
- package/.agents/tests/lib.sh +315 -0
- package/.agents/tests/remote-herdr-fixture.sh +125 -0
- package/.agents/tests/secondmate-helpers.sh +193 -0
- package/.agents/tests/smoke.test.sh +93 -0
- package/.agents/tests/wake-helpers.sh +338 -0
- package/.agents/tests/zellij-test-safety.sh +60 -0
- package/.agents/tools/README.md +10 -0
- package/.agents/tools/bin/tasks-cli +267 -0
- package/.agents/tools/tasks-cli.ts +216 -0
- package/AGENTS.md +454 -0
- package/CHANGELOG.md +881 -0
- package/LICENSE +1 -1
- package/NOTICE +63 -0
- package/README.md +113 -39
- package/RULES/01-domains.md +51 -0
- package/RULES/02-agents.md +78 -0
- package/RULES/03-houses.md +61 -0
- package/RULES/04-hoard.md +65 -0
- package/RULES/05-platforms.md +67 -0
- package/RULES/06-append-only.md +58 -0
- package/RULES/07-config.md +45 -0
- package/RULES/08-delivery-gate.md +75 -0
- package/RULES/README.md +20 -0
- package/bin/README.md +107 -0
- package/bin/a2a-mcp.sh +98 -0
- package/bin/a2a-serve.py +103 -0
- package/bin/a2a-serve.sh +8 -0
- package/bin/a2a-talk.sh +50 -0
- package/bin/a2abridge-ensure.sh +108 -0
- package/bin/agent-run.sh +43 -0
- package/bin/agents-config.sh +233 -0
- package/bin/apodex-smoke-test.sh +92 -0
- package/bin/bifrost-bridge.sh +112 -0
- package/bin/bifrost-ingress.sh +77 -0
- package/bin/bootstrap-macos.sh +107 -0
- package/bin/bootstrap-windows.ps1 +112 -0
- package/bin/branch-guard.sh +64 -0
- package/bin/brokk +73 -0
- package/bin/brokk-classify-lib.sh +1770 -0
- package/bin/brokk-lease-lib.sh +222 -0
- package/bin/brokk-lease.sh +193 -0
- package/bin/brokk-lint.sh +65 -0
- package/bin/brokk-send.sh +38 -0
- package/bin/brokk-update.sh +9 -0
- package/bin/brokk-wake-grant.sh +127 -0
- package/bin/brokk-wake-lib.sh +1849 -0
- package/bin/changelog-guard.sh +165 -0
- package/bin/crash-sense.sh +160 -0
- package/bin/desktop-place.sh +224 -0
- package/bin/docs-guard.sh +39 -0
- package/bin/editor-place.sh +207 -0
- package/bin/eindri-acclaim.sh +34 -0
- package/bin/eindri-control.sh +28 -0
- package/bin/eindri-role.sh +83 -0
- package/bin/eindri-seen.sh +41 -0
- package/bin/eindri-send.sh +15 -0
- package/bin/eindri-start.sh +85 -0
- package/bin/eindri-watch.sh +57 -0
- package/bin/einherjar-spawn.sh +576 -0
- package/bin/eir-doctor.sh +112 -0
- package/bin/erindi-brief.sh +271 -0
- package/bin/fleet-apply.sh +68 -0
- package/bin/github-deploy.sh +81 -0
- package/bin/gjallarhorn-expose.sh +102 -0
- package/bin/gjallarhorn-notify.sh +58 -0
- package/bin/gjallarhorn-purge.sh +54 -0
- package/bin/gjallarhorn-tunnel.sh +72 -0
- package/bin/gleipnir-lock-lib.sh +284 -0
- package/bin/groa-update.sh +122 -0
- package/bin/hall-snapshot.sh +92 -0
- package/bin/hamr-harness.sh +246 -0
- package/bin/heimdall-ssh-keys.sh +374 -0
- package/bin/herdr-agents.py +44 -0
- package/bin/herdr-ensure.sh +104 -0
- package/bin/herdr-run.sh +431 -0
- package/bin/hermes-ensure.sh +88 -0
- package/bin/hoard-lib.sh +25 -0
- package/bin/hodd.sh +78 -0
- package/bin/host-sense.sh +199 -0
- package/bin/huginn-research-worker.sh +108 -0
- package/bin/local-model-lock.sh +51 -0
- package/bin/mimir-bridge.py +226 -0
- package/bin/mimir-bridge.sh +87 -0
- package/bin/mimir-ingest.sh +131 -0
- package/bin/mimir.sh +113 -0
- package/bin/mjollnir-webhook.sh +59 -0
- package/bin/mjollnir.sh +85 -0
- package/bin/model-hardware.sh +82 -0
- package/bin/model-resolve.sh +147 -0
- package/bin/models-detect.sh +178 -0
- package/bin/nornir-cron-start.sh +162 -0
- package/bin/nornir-job-daily-briefing.sh +191 -0
- package/bin/nornir-job-git-sync.sh +152 -0
- package/bin/nornir-job-memory-housekeeping.sh +114 -0
- package/bin/nornir-job-observer.sh +166 -0
- package/bin/nornir-job-skillopt-sleep.sh +76 -0
- package/bin/omarchy-hook-install.sh +60 -0
- package/bin/omarchy-install.sh +148 -0
- package/bin/omarchy-plugins.sh +174 -0
- package/bin/omarchy-sense.sh +149 -0
- package/bin/perm-guard.sh +30 -0
- package/bin/pi-agent.sh +68 -0
- package/bin/pi-ensure.sh +77 -0
- package/bin/pi-local.sh +37 -0
- package/bin/pi-seat.sh +63 -0
- package/bin/prereq-ensure.sh +166 -0
- package/bin/private-guard.sh +38 -0
- package/bin/project-git.sh +55 -0
- package/bin/public-guard.sh +70 -0
- package/bin/ratatoskr.sh +49 -0
- package/bin/realm-lib.sh +32 -0
- package/bin/repo-scrub.sh +62 -0
- package/bin/rodd-operational-input.sh +250 -0
- package/bin/runes-append.sh +198 -0
- package/bin/saga-session-start.sh +172 -0
- package/bin/saga-sessionstart-run.sh +79 -0
- package/bin/saga-wake-drain.sh +41 -0
- package/bin/secret-guard.sh +63 -0
- package/bin/sessrumnir-ensure.sh +100 -0
- package/bin/sessrumnir-sync.sh +237 -0
- package/bin/sessrumnir.sh +91 -0
- package/bin/skillopt-setup.sh +76 -0
- package/bin/skuld-branch-outcome.sh +203 -0
- package/bin/skuld-branch-prompt.sh +101 -0
- package/bin/smidja-bootstrap.sh +90 -0
- package/bin/smidja-observe.sh +86 -0
- package/bin/syn-arm-pretool-check.sh +57 -0
- package/bin/syn-asset-pretool-check.sh +76 -0
- package/bin/syn-cd-pretool-check.sh +28 -0
- package/bin/syn-guard-pretool-check.sh +83 -0
- package/bin/syn-turnend-guard.sh +36 -0
- package/bin/syn-watch-arm.sh +99 -0
- package/bin/tailscale-sync.sh +190 -0
- package/bin/telegram-bot.sh +69 -0
- package/bin/toolchain.sh +74 -0
- package/bin/utgard.sh +111 -0
- package/bin/valhalla.sh +115 -0
- package/bin/valknut-load.sh +260 -0
- package/bin/vor-crew-state.sh +124 -0
- package/bin/wedge-notify.sh +47 -0
- package/bin/workspace-provision.sh +80 -0
- package/bin/workspace-rag.sh +150 -0
- package/bin/wyrd-db.sh +47 -0
- package/bin/yggdrasil.sh +152 -0
- package/bin/ymir-edit.sh +145 -0
- package/bin/ymir-install.sh +641 -0
- package/bin/ymir-invite.sh +93 -0
- package/bin/ymir-isolation.sh +88 -0
- package/bin/ymir-migrate.sh +61 -0
- package/bin/ymir-platform.sh +348 -0
- package/bin/ymir-say.sh +97 -0
- package/bin/ymir-setup-auth.sh +108 -0
- package/bin/ymir-validate.sh +173 -0
- package/bin/ymir.js +74 -16
- package/docs/Architecture.md +339 -0
- package/docs/design.md +351 -0
- package/docs/installations/README.md +63 -0
- package/docs/installations/ymir-home.md +93 -0
- package/docs/integration.md +105 -0
- package/docs/lore.md +1056 -0
- package/docs/research/agent-harnesses.md +290 -0
- package/docs/runbooks/README.md +19 -0
- package/docs/runbooks/a2a.md +91 -0
- package/docs/runbooks/agent-permissions.md +99 -0
- package/docs/runbooks/agents.md +57 -0
- package/docs/runbooks/models.md +69 -0
- package/docs/runbooks/realm-onboarding.md +60 -0
- package/docs/runbooks/secrets-and-hoard.md +60 -0
- package/docs/runbooks/updates-and-migrations.md +110 -0
- package/docs/runbooks/worktrees-isolation.md +72 -0
- package/docs/session-start.md +105 -0
- package/docs/skillopt-integration.md +321 -0
- package/docs/sync-system.md +66 -0
- package/docs/workspaces.md +104 -0
- package/docs/ymir-rut.md +260 -0
- package/package.json +44 -14
- package/packaging/build.sh +74 -0
- package/packaging/macos/Ymir Installer.command +43 -0
- package/packaging/windows/ymir-setup.nsi +74 -0
- package/scripts/README.md +22 -0
- package/scripts/electron.sh +210 -0
- package/scripts/start.sh +167 -0
- package/scripts/stop.sh +113 -0
- package/install.sh +0 -35
|
@@ -0,0 +1,2740 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# tests/fm-daemon.test.sh - supervise-daemon classifiers, the captain-relevant
|
|
3
|
+
# status-phrase matrix (a product contract), escalation batching/dedupe, afk
|
|
4
|
+
# presence-gating, and the injection-hardening units that an e2e cannot
|
|
5
|
+
# deterministically reach (persistent-Enter-swallow, max-defer wedge alarms,
|
|
6
|
+
# fm-send typed-plane swallow reporting, composer-pending ANSI parsing). The operator-visible
|
|
7
|
+
# inject flow lives in fm-afk-inject-e2e and fm-wake-daemon-lifecycle-e2e.
|
|
8
|
+
set -u
|
|
9
|
+
|
|
10
|
+
# shellcheck source=tests/wake-helpers.sh
|
|
11
|
+
. "$(dirname "${BASH_SOURCE[0]}")/wake-helpers.sh"
|
|
12
|
+
|
|
13
|
+
DAEMON="$ROOT/bin/fm-supervise-daemon.sh"
|
|
14
|
+
AFK_START="$ROOT/bin/fm-afk-start.sh"
|
|
15
|
+
# Source the daemon's pure functions once. Its main loop is skipped under sourcing
|
|
16
|
+
# via a BASH_SOURCE guard, so only classify_*/housekeeping/escalate_*/afk_* and the
|
|
17
|
+
# pane/submit helpers become defined.
|
|
18
|
+
if [ -z "${FM_TEST_DAEMON_SOURCED:-}" ]; then
|
|
19
|
+
export FM_TEST_DAEMON_SOURCED=1
|
|
20
|
+
# shellcheck source=bin/fm-supervise-daemon.sh
|
|
21
|
+
. "$DAEMON"
|
|
22
|
+
fi
|
|
23
|
+
|
|
24
|
+
TMP_ROOT=$(fm_test_tmproot fm-daemon-tests)
|
|
25
|
+
FM_DAEMON_PRIMARY_HARNESS=claude
|
|
26
|
+
export FM_DAEMON_PRIMARY_HARNESS
|
|
27
|
+
|
|
28
|
+
test_afk_start_refuses_when_flag_cannot_be_written() {
|
|
29
|
+
local dir state out status
|
|
30
|
+
dir=$(make_supercase afk-start-flag-unwritable)
|
|
31
|
+
state="$dir/state"
|
|
32
|
+
mkdir -p "$state/.afk"
|
|
33
|
+
|
|
34
|
+
out=$(FM_STATE_OVERRIDE="$state" FM_SUPERVISOR_BACKEND=unsupported "$AFK_START" 2>&1)
|
|
35
|
+
status=$?
|
|
36
|
+
|
|
37
|
+
[ "$status" -ne 0 ] || fail "fm-afk-start.sh should fail when state/.afk cannot be written"
|
|
38
|
+
assert_not_contains "$out" "starting supervise daemon" "fm-afk-start.sh continued into daemon startup after .afk write failure"
|
|
39
|
+
assert_absent "$state/.supervise-daemon.log" "fm-afk-start.sh started the daemon after .afk write failure"
|
|
40
|
+
pass "fm-afk-start.sh fails before daemon startup when the afk flag cannot be written"
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
test_afk_start_ignores_stale_pidfile_without_lock() {
|
|
44
|
+
local dir state out status
|
|
45
|
+
dir=$(make_supercase afk-start-stale-pidfile)
|
|
46
|
+
state="$dir/state"
|
|
47
|
+
printf '%s\n' "$$" > "$state/.supervise-daemon.pid"
|
|
48
|
+
|
|
49
|
+
out=$(FM_STATE_OVERRIDE="$state" FM_SUPERVISOR_BACKEND=unsupported "$AFK_START" 2>&1)
|
|
50
|
+
status=$?
|
|
51
|
+
|
|
52
|
+
[ "$status" -ne 0 ] || fail "fm-afk-start.sh should attempt daemon startup instead of trusting a pidfile-only live pid"
|
|
53
|
+
assert_contains "$out" "starting supervise daemon" "fm-afk-start.sh did not attempt daemon startup"
|
|
54
|
+
assert_contains "$out" "does not support supervisor backend 'unsupported'" "daemon startup did not reach backend validation"
|
|
55
|
+
assert_not_contains "$out" "daemon already running" "fm-afk-start.sh trusted a stale pidfile-only live pid"
|
|
56
|
+
pass "fm-afk-start.sh ignores stale pidfile-only live pids"
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
test_afk_start_reclaims_stale_daemon_lock_reused_pid() {
|
|
60
|
+
local dir state out status lock
|
|
61
|
+
dir=$(make_supercase afk-start-stale-lock-reused-pid)
|
|
62
|
+
state="$dir/state"
|
|
63
|
+
lock="$state/.supervise-daemon.lock"
|
|
64
|
+
mkdir -p "$lock"
|
|
65
|
+
printf '%s\n' "$$" > "$state/.supervise-daemon.pid"
|
|
66
|
+
printf '%s\n' "$$" > "$lock/pid"
|
|
67
|
+
printf '%s\n' "stale daemon identity" > "$lock/pid-identity"
|
|
68
|
+
|
|
69
|
+
out=$(FM_STATE_OVERRIDE="$state" FM_SUPERVISOR_BACKEND=unsupported "$AFK_START" 2>&1)
|
|
70
|
+
status=$?
|
|
71
|
+
|
|
72
|
+
[ "$status" -ne 0 ] || fail "fm-afk-start.sh should attempt daemon startup after rejecting a reused-pid lock"
|
|
73
|
+
assert_contains "$out" "starting supervise daemon" "fm-afk-start.sh did not attempt daemon startup after rejecting the stale lock"
|
|
74
|
+
assert_contains "$out" "does not support supervisor backend 'unsupported'" "daemon startup did not reach backend validation after stale lock cleanup"
|
|
75
|
+
assert_not_contains "$out" "daemon already running" "fm-afk-start.sh trusted a stale daemon lock with a reused pid"
|
|
76
|
+
assert_not_contains "$out" "another fm-supervise-daemon is already running" "daemon singleton lock still trusted the reused pid"
|
|
77
|
+
pass "fm-afk-start.sh reclaims stale daemon locks whose live pid identity no longer matches"
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
test_daemon_state_root_uses_fm_home() {
|
|
81
|
+
local dir home override out
|
|
82
|
+
dir=$(make_supercase daemon-fm-home)
|
|
83
|
+
home="$dir/firstmate-home"
|
|
84
|
+
override="$dir/override-state"
|
|
85
|
+
mkdir -p "$home" "$override"
|
|
86
|
+
|
|
87
|
+
out=$(FM_HOME="$home" FM_STATE_OVERRIDE='' _state_root)
|
|
88
|
+
[ "$out" = "$home/state" ] || fail "daemon state root ignored FM_HOME: $out"
|
|
89
|
+
|
|
90
|
+
out=$(FM_HOME="$home" FM_STATE_OVERRIDE="$override" _state_root)
|
|
91
|
+
[ "$out" = "$override" ] || fail "daemon state root ignored FM_STATE_OVERRIDE: $out"
|
|
92
|
+
|
|
93
|
+
pass "supervise daemon state root is scoped by FM_HOME"
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
# Byte size of a status log: the daemon records escalation progress as a
|
|
97
|
+
# position in the append-only stream, so a fixture that means "already escalated
|
|
98
|
+
# through here" writes that position.
|
|
99
|
+
log_size() { LC_ALL=C wc -c < "$1" | tr -d '[:space:]'; }
|
|
100
|
+
|
|
101
|
+
seen_through() { # <state> <task>
|
|
102
|
+
local state=$1 task=$2 key ident
|
|
103
|
+
key=$(printf '%s' "$task" | tr ':/.' '___')
|
|
104
|
+
ident=$(_fm_open_decisions_file_ident "$state/$task.status")
|
|
105
|
+
printf '%s@%s' "$(log_size "$state/$task.status")" "$ident" > "$state/.subsuper-seen-status-$key"
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
# The reported bug in away mode: the captain is away, a worker reports something
|
|
109
|
+
# the captain must hear, then keeps appending routine progress. Classifying only
|
|
110
|
+
# the last line self-handles the wake and the work stalls silently until the
|
|
111
|
+
# captain returns.
|
|
112
|
+
test_classify_signal_skips_turn_end_markers() {
|
|
113
|
+
local dir state reader turn status out
|
|
114
|
+
dir=$(make_supercase signal-turn-end); state="$dir/state"
|
|
115
|
+
reader="$dir/identity-reader"
|
|
116
|
+
printf '#!/usr/bin/env bash\nexit 1\n' > "$reader"; chmod +x "$reader"
|
|
117
|
+
turn="$state/task.turn-ended"; : > "$turn"
|
|
118
|
+
out=$(FM_STATUS_IDENTITY_READER="$reader" classify_signal "$turn" "$state")
|
|
119
|
+
case "$out" in self\|routine\ signal:*) ;;
|
|
120
|
+
*) fail "an empty turn-end marker was not routine under unavailable identity: $out" ;;
|
|
121
|
+
esac
|
|
122
|
+
status="$state/task.status"
|
|
123
|
+
printf 'blocked: release approval required\nworking: preparing notes\n' > "$status"
|
|
124
|
+
out=$(classify_signal "$turn $status" "$state")
|
|
125
|
+
case "$out" in escalate\|*"blocked: release approval required"*) ;;
|
|
126
|
+
*) fail "a mixed turn-end and actionable status batch did not name the status event: $out" ;;
|
|
127
|
+
esac
|
|
128
|
+
pass "turn-end markers stay routine while mixed actionable status batches escalate"
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
test_classify_signal_survives_a_later_routine_append() {
|
|
132
|
+
local dir state out
|
|
133
|
+
dir=$(make_supercase classify-masked)
|
|
134
|
+
state="$dir/state"
|
|
135
|
+
|
|
136
|
+
printf 'working: setup\nblocked [key=release]: cannot reach the release host\nfailed: release verification broke\nworking: retrying the upload\n' \
|
|
137
|
+
> "$state/mask-b1.status"
|
|
138
|
+
out=$(FM_STATE_OVERRIDE="$state" classify_signal "$state/mask-b1.status" "$state")
|
|
139
|
+
case "$out" in
|
|
140
|
+
escalate\|*) ;;
|
|
141
|
+
*) fail "a blocker hidden behind a later working: line was self-handled: $out" ;;
|
|
142
|
+
esac
|
|
143
|
+
case "$out" in
|
|
144
|
+
*"blocked [key=release]: cannot reach the release host"*"failed: release verification broke"*) ;;
|
|
145
|
+
*) fail "the escalation did not report every actionable event before its endpoint: $out" ;;
|
|
146
|
+
esac
|
|
147
|
+
|
|
148
|
+
# The captain-reported shape: a finished release/install followed by routine
|
|
149
|
+
# cleanup chatter must still reach an away captain.
|
|
150
|
+
printf 'working: publishing\ndone: release 1.4.0 published and installed\nworking: cleaning the build dir\n' \
|
|
151
|
+
> "$state/mask-r1.status"
|
|
152
|
+
out=$(FM_STATE_OVERRIDE="$state" classify_signal "$state/mask-r1.status" "$state")
|
|
153
|
+
case "$out" in
|
|
154
|
+
escalate\|*) ;;
|
|
155
|
+
*) fail "a release/install completion hidden behind later routine appends was self-handled: $out" ;;
|
|
156
|
+
esac
|
|
157
|
+
case "$out" in
|
|
158
|
+
*"done: release 1.4.0 published and installed"*) ;;
|
|
159
|
+
*) fail "the escalation named the routine line instead of the completion: $out" ;;
|
|
160
|
+
esac
|
|
161
|
+
|
|
162
|
+
# Once escalated through the end of the log, a further routine append is
|
|
163
|
+
# routine again: the fix must not turn every later signal into a re-escalation.
|
|
164
|
+
seen_through "$state" "mask-b1"
|
|
165
|
+
printf 'working: still retrying\n' >> "$state/mask-b1.status"
|
|
166
|
+
out=$(FM_STATE_OVERRIDE="$state" classify_signal "$state/mask-b1.status" "$state")
|
|
167
|
+
case "$out" in
|
|
168
|
+
self\|*) ;;
|
|
169
|
+
*) fail "a routine append after the blocker was escalated re-escalated it: $out" ;;
|
|
170
|
+
esac
|
|
171
|
+
pass "an actionable event is escalated to an away captain despite later routine appends"
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
# The away-mode backstop must reach the same event when the per-wake path never
|
|
175
|
+
# ran, which is exactly what it exists for.
|
|
176
|
+
test_classification_commits_its_captured_endpoint() {
|
|
177
|
+
local dir state capture out captured
|
|
178
|
+
dir=$(make_supercase captured-endpoint); state="$dir/state"
|
|
179
|
+
capture="$dir/endpoints"
|
|
180
|
+
printf 'done: first release complete\n' > "$state/race-r1.status"
|
|
181
|
+
out=$(FM_STATUS_SPAN_ENDPOINT_FILE="$capture" \
|
|
182
|
+
classify_signal "$state/race-r1.status" "$state")
|
|
183
|
+
case "$out" in escalate\|*) ;; *) fail "the first event did not classify actionable: $out" ;; esac
|
|
184
|
+
captured=$(log_size "$state/race-r1.status")
|
|
185
|
+
printf 'failed: second release verification failed\n' >> "$state/race-r1.status"
|
|
186
|
+
mark_escalated_seen "$state" "$capture"
|
|
187
|
+
[ "$(status_seen_offset "$state" race-r1)" = "$captured" ] \
|
|
188
|
+
|| fail "the daemon advanced past bytes appended after classification"
|
|
189
|
+
out=$(classify_signal "$state/race-r1.status" "$state")
|
|
190
|
+
case "$out" in
|
|
191
|
+
escalate\|*"failed: second release verification failed"*) ;;
|
|
192
|
+
*) fail "an event appended between classification and commit was lost: $out" ;;
|
|
193
|
+
esac
|
|
194
|
+
pass "the daemon commits exactly the endpoint captured by classification"
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
test_stale_masked_event_escalates_at_captured_endpoint() {
|
|
198
|
+
local dir state key out
|
|
199
|
+
dir=$(make_supercase stale-masked); state="$dir/state"
|
|
200
|
+
printf 'blocked: release host unavailable\nworking: retrying upload\n' > "$state/stale-r2.status"
|
|
201
|
+
FM_ESCALATE_BATCH_SECS=999 handle_wake "stale: sess:fm-stale-r2" "$state"
|
|
202
|
+
out=$(cat "$state/.subsuper-escalations" 2>/dev/null || true)
|
|
203
|
+
case "$out" in *"blocked: release host unavailable"*) ;; *) fail "a stale wake hid the blocker behind routine progress: $out" ;; esac
|
|
204
|
+
key=$(printf '%s' stale-r2 | tr ':/.' '___')
|
|
205
|
+
[ "$(status_seen_offset "$state" stale-r2)" = "$(log_size "$state/stale-r2.status")" ] \
|
|
206
|
+
|| fail "the stale escalation did not commit its captured endpoint"
|
|
207
|
+
pass "a stale wake escalates a blocker hidden by later progress"
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
test_stale_read_failure_surfaces_without_advancing_seen() {
|
|
211
|
+
local dir state key out
|
|
212
|
+
dir=$(make_supercase stale-unreadable); state="$dir/state"
|
|
213
|
+
printf 'working: retrying upload\n' > "$state/stale-r3.status"
|
|
214
|
+
key=$(printf '%s' stale-r3 | tr ':/.' '___')
|
|
215
|
+
printf '3@%s' "$(_fm_open_decisions_file_ident "$state/stale-r3.status")" \
|
|
216
|
+
> "$state/.subsuper-seen-status-$key"
|
|
217
|
+
(
|
|
218
|
+
# shellcheck disable=SC2329 # Invoked indirectly by the function under test.
|
|
219
|
+
_fm_status_read_span() { return 1; }
|
|
220
|
+
FM_ESCALATE_BATCH_SECS=999 handle_wake "stale: sess:fm-stale-r3" "$state"
|
|
221
|
+
)
|
|
222
|
+
out=$(cat "$state/.subsuper-escalations" 2>/dev/null || true)
|
|
223
|
+
case "$out" in *"unreadable status span"*) ;; *) fail "a stale span read failure was silently absorbed" ;; esac
|
|
224
|
+
[ "$(status_seen_offset "$state" stale-r3)" = 3 ] \
|
|
225
|
+
|| fail "a stale span read failure advanced the daemon seen marker"
|
|
226
|
+
pass "a stale span read failure surfaces without advancing its marker"
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
test_recreated_status_rejects_captured_identity() {
|
|
230
|
+
local dir state reader record rest endpoint ident out
|
|
231
|
+
dir=$(make_supercase recreated-identity); state="$dir/state"
|
|
232
|
+
reader="$dir/identity-reader"
|
|
233
|
+
cat > "$reader" <<EOF
|
|
234
|
+
#!/usr/bin/env bash
|
|
235
|
+
cat "$dir/identity-value"
|
|
236
|
+
EOF
|
|
237
|
+
chmod +x "$reader"
|
|
238
|
+
printf '1:2:old-birth' > "$dir/identity-value"
|
|
239
|
+
printf 'done: old task complete\n' > "$state/reused-r4.status"
|
|
240
|
+
record=$(FM_STATUS_IDENTITY_READER="$reader" \
|
|
241
|
+
status_span_first_actionable_record "$state/reused-r4.status" 0)
|
|
242
|
+
endpoint=${record%%$'\t'*}; rest=${record#*$'\t'}; ident=${rest%%$'\t'*}
|
|
243
|
+
rm -f "$state/reused-r4.status"
|
|
244
|
+
printf 'blocked: replacement task needs captain\nworking: routine padding after replacement\n' \
|
|
245
|
+
> "$state/reused-r4.status"
|
|
246
|
+
printf '1:2:new-birth' > "$dir/identity-value"
|
|
247
|
+
FM_STATUS_IDENTITY_READER="$reader" mark_status_seen "$state" reused-r4 "$endpoint" "$ident"
|
|
248
|
+
[ ! -e "$state/.subsuper-seen-status-reused-r4" ] \
|
|
249
|
+
|| fail "a stale captured identity advanced the replacement task marker"
|
|
250
|
+
out=$(FM_STATUS_IDENTITY_READER="$reader" classify_signal "$state/reused-r4.status" "$state")
|
|
251
|
+
case "$out" in escalate\|*"blocked: replacement task needs captain"*) ;;
|
|
252
|
+
*) fail "the replacement task blocker did not surface after identity rejection: $out" ;;
|
|
253
|
+
esac
|
|
254
|
+
pass "a recreated status rejects the old captured identity"
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
test_unverifiable_identity_surfaces_without_marker() {
|
|
258
|
+
local dir state reader key out
|
|
259
|
+
dir=$(make_supercase unverifiable-identity); state="$dir/state"
|
|
260
|
+
reader="$dir/identity-reader"
|
|
261
|
+
printf '#!/usr/bin/env bash\nexit 1\n' > "$reader"; chmod +x "$reader"
|
|
262
|
+
printf 'working: routine progress\n' > "$state/unknown-r5.status"
|
|
263
|
+
key=$(printf '%s' unknown-r5 | tr ':/.' '___')
|
|
264
|
+
(
|
|
265
|
+
FM_STATUS_IDENTITY_READER="$reader" FM_ESCALATE_BATCH_SECS=999 \
|
|
266
|
+
handle_wake "signal: $state/unknown-r5.status" "$state"
|
|
267
|
+
)
|
|
268
|
+
out=$(cat "$state/.subsuper-escalations" 2>/dev/null || true)
|
|
269
|
+
case "$out" in *"unreadable status span"*) ;; *) fail "an unverifiable identity was silently absorbed" ;; esac
|
|
270
|
+
[ "$(status_seen_offset "$state" unknown-r5)" = 0 ] \
|
|
271
|
+
|| fail "an unverifiable identity advanced the daemon classification position"
|
|
272
|
+
pass "an unverifiable status identity surfaces without advancing markers"
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
test_status_read_failure_surfaces_without_advancing_seen() {
|
|
276
|
+
local dir state key out
|
|
277
|
+
dir=$(make_supercase unreadable-span); state="$dir/state"
|
|
278
|
+
printf 'done: release complete\n' > "$state/read-r1.status"
|
|
279
|
+
key=$(printf '%s' read-r1 | tr ':/.' '___')
|
|
280
|
+
printf '3@%s' "$(_fm_open_decisions_file_ident "$state/read-r1.status")" \
|
|
281
|
+
> "$state/.subsuper-seen-status-$key"
|
|
282
|
+
(
|
|
283
|
+
# shellcheck disable=SC2329 # Invoked indirectly by the function under test.
|
|
284
|
+
last_status_line() { return 0; }
|
|
285
|
+
# shellcheck disable=SC2329 # Invoked indirectly by the function under test.
|
|
286
|
+
_fm_status_read_span() { return 1; }
|
|
287
|
+
FM_ESCALATE_BATCH_SECS=999 handle_wake "signal: $state/read-r1.status" "$state"
|
|
288
|
+
)
|
|
289
|
+
out=$(cat "$state/.subsuper-escalations" 2>/dev/null || true)
|
|
290
|
+
case "$out" in *"unreadable status span"*) ;; *) fail "a status span read failure was silently absorbed" ;; esac
|
|
291
|
+
[ "$(status_seen_offset "$state" read-r1)" = 3 ] \
|
|
292
|
+
|| fail "a status span read failure advanced the daemon seen marker"
|
|
293
|
+
pass "a status read failure surfaces without advancing the daemon suppressor"
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
test_catchall_advances_routine_then_surfaces_append() {
|
|
297
|
+
local dir state out
|
|
298
|
+
dir=$(make_supercase catchall-routine); state="$dir/state"
|
|
299
|
+
printf 'working: routine history\nworking: still routine\n' > "$state/routine-r6.status"
|
|
300
|
+
rm -f "$state/.subsuper-last-scan"
|
|
301
|
+
FM_STATE_OVERRIDE="$state" housekeeping "$state"
|
|
302
|
+
[ "$(status_seen_offset "$state" routine-r6)" = "$(log_size "$state/routine-r6.status")" ] \
|
|
303
|
+
|| fail "routine catch-all classification did not advance its captured endpoint"
|
|
304
|
+
printf 'blocked: appended after routine endpoint\n' >> "$state/routine-r6.status"
|
|
305
|
+
rm -f "$state/.subsuper-last-scan"
|
|
306
|
+
FM_STATE_OVERRIDE="$state" housekeeping "$state"
|
|
307
|
+
out=$(cat "$state/.subsuper-escalations" 2>/dev/null || true)
|
|
308
|
+
case "$out" in *"blocked: appended after routine endpoint"*) ;;
|
|
309
|
+
*) fail "an actionable append after the routine endpoint did not surface: $out" ;;
|
|
310
|
+
esac
|
|
311
|
+
pass "catch-all routine scans advance before later actionable appends"
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
test_escalation_buffer_failure_retains_wake_and_position() {
|
|
315
|
+
local dir state fakebin buffer out
|
|
316
|
+
dir=$(make_supercase escalation-write-failure); state="$dir/state"; fakebin="$dir/daemon-bin"
|
|
317
|
+
buffer="$state/.subsuper-escalations"
|
|
318
|
+
printf 'blocked: release approval required\nworking: preparing notes\n' > "$state/write-r1.status"
|
|
319
|
+
mkdir -p "$fakebin" "$buffer"
|
|
320
|
+
cat > "$fakebin/fm-wake-drain.sh" <<EOF
|
|
321
|
+
#!/usr/bin/env bash
|
|
322
|
+
if [ "\${1:-}" = --ack-through ]; then printf '%s\n' ack >> "$dir/acked"; exit 0; fi
|
|
323
|
+
printf '1\t1\tsignal\twrite-r1.status\tsignal: $state/write-r1.status\n'
|
|
324
|
+
printf 'WAKE_ACK_REQUIRED: retry --ack-through 1 --recovery-generation gen\n' >&2
|
|
325
|
+
EOF
|
|
326
|
+
chmod +x "$fakebin/fm-wake-drain.sh"
|
|
327
|
+
|
|
328
|
+
! FM_DAEMON_DIR="$fakebin" handle_durable_wakes fallback "$state" 2>/dev/null \
|
|
329
|
+
|| fail "an unwritable escalation buffer acknowledged the wake"
|
|
330
|
+
[ ! -e "$dir/acked" ] || fail "a wake was acknowledged before its escalation was buffered"
|
|
331
|
+
[ "$(status_seen_offset "$state" write-r1)" = 0 ] \
|
|
332
|
+
|| fail "a failed escalation append advanced the classification position"
|
|
333
|
+
|
|
334
|
+
rmdir "$buffer"
|
|
335
|
+
FM_DAEMON_DIR="$fakebin" handle_durable_wakes fallback "$state" \
|
|
336
|
+
|| fail "the wake did not recover after the escalation buffer became writable"
|
|
337
|
+
out=$(cat "$buffer" 2>/dev/null || true)
|
|
338
|
+
case "$out" in *"blocked: release approval required"*) ;;
|
|
339
|
+
*) fail "the recovered wake did not buffer its actionable event: $out" ;;
|
|
340
|
+
esac
|
|
341
|
+
[ "$(status_seen_offset "$state" write-r1)" = "$(log_size "$state/write-r1.status")" ] \
|
|
342
|
+
|| fail "successful buffering did not advance the classification position"
|
|
343
|
+
[ "$(wc -l < "$dir/acked" | tr -d ' ')" = 1 ] \
|
|
344
|
+
|| fail "the recovered durable wake was not acknowledged exactly once"
|
|
345
|
+
pass "failed escalation writes retain durable wakes and classification positions"
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
test_catchall_buffer_failure_preserves_position() {
|
|
349
|
+
local dir state buffer out
|
|
350
|
+
dir=$(make_supercase catchall-write-failure); state="$dir/state"
|
|
351
|
+
buffer="$state/.subsuper-escalations"
|
|
352
|
+
printf 'failed: release verification broke\nworking: collecting logs\n' > "$state/catch-write-r2.status"
|
|
353
|
+
mkdir "$buffer"
|
|
354
|
+
rm -f "$state/.subsuper-last-scan"
|
|
355
|
+
FM_STATE_OVERRIDE="$state" housekeeping "$state" 2>/dev/null || true
|
|
356
|
+
[ "$(status_seen_offset "$state" catch-write-r2)" = 0 ] \
|
|
357
|
+
|| fail "a failed catch-all append advanced the classification position"
|
|
358
|
+
|
|
359
|
+
rmdir "$buffer"
|
|
360
|
+
rm -f "$state/.subsuper-last-scan"
|
|
361
|
+
FM_STATE_OVERRIDE="$state" housekeeping "$state"
|
|
362
|
+
out=$(cat "$buffer" 2>/dev/null || true)
|
|
363
|
+
case "$out" in *"failed: release verification broke"*) ;;
|
|
364
|
+
*) fail "the catch-all did not retry its actionable event after recovery: $out" ;;
|
|
365
|
+
esac
|
|
366
|
+
[ "$(status_seen_offset "$state" catch-write-r2)" = "$(log_size "$state/catch-write-r2.status")" ] \
|
|
367
|
+
|| fail "the recovered catch-all did not advance its classification position"
|
|
368
|
+
pass "catch-all markers advance only after escalation buffering succeeds"
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
test_durable_wake_failure_retains_entire_batch() {
|
|
372
|
+
local dir state fakebin attempts
|
|
373
|
+
dir=$(make_supercase durable-failure); state="$dir/state"; fakebin="$dir/daemon-bin"; attempts="$dir/attempts"
|
|
374
|
+
mkdir -p "$fakebin"
|
|
375
|
+
cat > "$fakebin/fm-wake-drain.sh" <<EOF
|
|
376
|
+
#!/usr/bin/env bash
|
|
377
|
+
if [ "\${1:-}" = --ack-through ]; then printf ack > "$dir/acked"; exit 0; fi
|
|
378
|
+
printf '1\t1\tsignal\ttask.status\tsignal: first\n1\t2\theartbeat\theartbeat\theartbeat\n'
|
|
379
|
+
printf 'WAKE_ACK_REQUIRED: retry --ack-through 2 --recovery-generation gen\n' >&2
|
|
380
|
+
EOF
|
|
381
|
+
chmod +x "$fakebin/fm-wake-drain.sh"
|
|
382
|
+
(
|
|
383
|
+
FM_DAEMON_DIR="$fakebin"
|
|
384
|
+
handle_wake() { printf '%s\n' "$1" >> "$attempts"; [ "$1" != 'signal: first' ]; }
|
|
385
|
+
! handle_durable_wakes fallback "$state"
|
|
386
|
+
) || fail "a failed wake classification was acknowledged"
|
|
387
|
+
[ "$(wc -l < "$attempts" | tr -d ' ')" = 2 ] \
|
|
388
|
+
|| fail "a failed wake prevented later batch entries from being accounted"
|
|
389
|
+
[ ! -e "$dir/acked" ] || fail "a partially handled durable batch was acknowledged"
|
|
390
|
+
pass "classification failure retains the complete durable wake batch"
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
test_missing_status_stale_is_acknowledged_without_diagnostic() {
|
|
394
|
+
local dir state fakebin
|
|
395
|
+
dir=$(make_supercase durable-no-status); state="$dir/state"; fakebin="$dir/daemon-bin"
|
|
396
|
+
mkdir -p "$fakebin"
|
|
397
|
+
cat > "$fakebin/fm-wake-drain.sh" <<EOF
|
|
398
|
+
#!/usr/bin/env bash
|
|
399
|
+
if [ "\${1:-}" = --ack-through ]; then printf '%s\n' ack >> "$dir/acked"; exit 0; fi
|
|
400
|
+
printf '1\t1\tstale\tmissing-r8\tstale: sess:fm-missing-r8\n'
|
|
401
|
+
printf 'WAKE_ACK_REQUIRED: ordinary --ack-through 1 --recovery-generation gen\n' >&2
|
|
402
|
+
EOF
|
|
403
|
+
chmod +x "$fakebin/fm-wake-drain.sh"
|
|
404
|
+
FM_DAEMON_DIR="$fakebin" handle_durable_wakes fallback "$state" \
|
|
405
|
+
|| fail "a stale wake without a status file was retained for retry"
|
|
406
|
+
FM_DAEMON_DIR="$fakebin" handle_durable_wakes fallback "$state" \
|
|
407
|
+
|| fail "repeated missing-status handling became a classification failure"
|
|
408
|
+
[ "$(wc -l < "$dir/acked" | tr -d ' ')" = 2 ] \
|
|
409
|
+
|| fail "a missing-status stale wake was not acknowledged"
|
|
410
|
+
[ ! -s "$state/.subsuper-escalations" ] \
|
|
411
|
+
|| fail "a missing status file produced an unreadable-span escalation"
|
|
412
|
+
pass "missing-status stale wakes remain ordinary and acknowledgeable"
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
test_transient_unreadable_signal_recovers_without_advancing() {
|
|
416
|
+
local dir state fakebin out key
|
|
417
|
+
dir=$(make_supercase durable-unreadable); state="$dir/state"; fakebin="$dir/daemon-bin"
|
|
418
|
+
printf 'blocked: status cannot be classified\n' > "$state/unreadable-r7.status"
|
|
419
|
+
mkdir -p "$fakebin"
|
|
420
|
+
cat > "$fakebin/fm-wake-drain.sh" <<EOF
|
|
421
|
+
#!/usr/bin/env bash
|
|
422
|
+
if [ "\${1:-}" = --ack-through ]; then printf '%s\n' ack >> "$dir/acked"; exit 0; fi
|
|
423
|
+
printf '1\t1\tsignal\tunreadable-r7.status\tsignal: $state/unreadable-r7.status\n'
|
|
424
|
+
printf 'WAKE_ACK_REQUIRED: retry --ack-through 1 --recovery-generation gen\n' >&2
|
|
425
|
+
EOF
|
|
426
|
+
chmod +x "$fakebin/fm-wake-drain.sh"
|
|
427
|
+
(
|
|
428
|
+
FM_DAEMON_DIR="$fakebin"
|
|
429
|
+
# shellcheck disable=SC2329 # Invoked indirectly by the function under test.
|
|
430
|
+
_fm_status_read_span() { return 1; }
|
|
431
|
+
handle_durable_wakes fallback "$state"
|
|
432
|
+
) || fail "an unreadable signal did not acknowledge its wake after reporting"
|
|
433
|
+
out=$(cat "$state/.subsuper-escalations" 2>/dev/null || true)
|
|
434
|
+
case "$out" in *"unreadable status span"*) ;;
|
|
435
|
+
*) fail "an unreadable durable signal did not surface its diagnostic: $out" ;;
|
|
436
|
+
esac
|
|
437
|
+
key=$(printf '%s' unreadable-r7 | tr ':/.' '___')
|
|
438
|
+
[ "$(status_seen_offset "$state" unreadable-r7)" = 0 ] \
|
|
439
|
+
|| fail "an unreadable signal advanced its classification position"
|
|
440
|
+
FM_DAEMON_DIR="$fakebin" handle_durable_wakes fallback "$state" \
|
|
441
|
+
|| fail "a readable status did not recover after a transient failure"
|
|
442
|
+
out=$(cat "$state/.subsuper-escalations" 2>/dev/null || true)
|
|
443
|
+
case "$out" in *"blocked: status cannot be classified"*) ;;
|
|
444
|
+
*) fail "the recovered status was not classified from its original position: $out" ;;
|
|
445
|
+
esac
|
|
446
|
+
pass "transient unreadable signals recover without advancing their position"
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
test_permission_recovery_reclassifies_catchall_status() {
|
|
450
|
+
local dir state status before_ident after_ident out
|
|
451
|
+
dir=$(make_supercase catchall-permission-recovery); state="$dir/state"
|
|
452
|
+
status="$state/permission-r8.status"
|
|
453
|
+
printf 'blocked: release approval required\nworking: preserving context\n' > "$status"
|
|
454
|
+
before_ident=$(_fm_open_decisions_file_ident "$status")
|
|
455
|
+
chmod 000 "$status"
|
|
456
|
+
if [ -r "$status" ]; then
|
|
457
|
+
chmod 600 "$status"
|
|
458
|
+
pass "daemon permission recovery skipped because permissions cannot deny reads"
|
|
459
|
+
return
|
|
460
|
+
fi
|
|
461
|
+
|
|
462
|
+
rm -f "$state/.subsuper-last-scan"
|
|
463
|
+
FM_STATE_OVERRIDE="$state" housekeeping "$state"
|
|
464
|
+
[ "$(status_seen_offset "$state" permission-r8)" = 0 ] \
|
|
465
|
+
|| { chmod 600 "$status"; fail "an unreadable catch-all status advanced its classification position"; }
|
|
466
|
+
rm -f "$state/.subsuper-last-scan"
|
|
467
|
+
FM_STATE_OVERRIDE="$state" housekeeping "$state"
|
|
468
|
+
[ "$(grep -c 'unreadable status span' "$state/.subsuper-escalations")" = 1 ] \
|
|
469
|
+
|| { chmod 600 "$status"; fail "an unchanged unreadable catch-all status reported repeatedly"; }
|
|
470
|
+
|
|
471
|
+
chmod 600 "$status"
|
|
472
|
+
after_ident=$(_fm_open_decisions_file_ident "$status")
|
|
473
|
+
[ "$after_ident" = "$before_ident" ] || fail "permission recovery changed the catch-all file identity"
|
|
474
|
+
rm -f "$state/.subsuper-last-scan"
|
|
475
|
+
FM_STATE_OVERRIDE="$state" housekeeping "$state"
|
|
476
|
+
out=$(cat "$state/.subsuper-escalations" 2>/dev/null || true)
|
|
477
|
+
case "$out" in *"blocked: release approval required"*) ;;
|
|
478
|
+
*) fail "the catch-all did not surface preserved content after readability recovery: $out" ;;
|
|
479
|
+
esac
|
|
480
|
+
[ "$(status_seen_offset "$state" permission-r8)" = "$(log_size "$status")" ] \
|
|
481
|
+
|| fail "the catch-all did not classify from the unadvanced position"
|
|
482
|
+
pass "daemon catch-all reclassifies permission-recovered status content"
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
# A permanently unclassifiable status must not wedge supervision. The accepted
|
|
486
|
+
# contract is deliberately simple: report it, acknowledge the wake so an unchanged
|
|
487
|
+
# permanent failure cannot re-alarm on every pass, and never advance the
|
|
488
|
+
# classification position, so the log is classified from where it stopped once it
|
|
489
|
+
# becomes readable. The residual risk - no guaranteed automatic retry inside a
|
|
490
|
+
# crash-mid-read window - is accepted and covered by the locked startup replay.
|
|
491
|
+
test_permanent_classification_failure_is_reported_and_acknowledged() {
|
|
492
|
+
local dir state fakebin out
|
|
493
|
+
dir=$(make_supercase durable-symlink); state="$dir/state"; fakebin="$dir/daemon-bin"
|
|
494
|
+
printf 'blocked: first target\n' > "$dir/target-one"
|
|
495
|
+
ln -s "$dir/target-one" "$state/symlink-r9.status"
|
|
496
|
+
mkdir -p "$fakebin"
|
|
497
|
+
cat > "$fakebin/fm-wake-drain.sh" <<EOF
|
|
498
|
+
#!/usr/bin/env bash
|
|
499
|
+
if [ "\${1:-}" = --ack-through ]; then printf '%s\n' ack >> "$dir/acked"; exit 0; fi
|
|
500
|
+
printf '1\t1\tsignal\tsymlink-r9.status\tsignal: $state/symlink-r9.status\n'
|
|
501
|
+
printf 'WAKE_ACK_REQUIRED: bounded --ack-through 1 --recovery-generation gen\n' >&2
|
|
502
|
+
EOF
|
|
503
|
+
chmod +x "$fakebin/fm-wake-drain.sh"
|
|
504
|
+
|
|
505
|
+
FM_DAEMON_DIR="$fakebin" handle_durable_wakes fallback "$state" \
|
|
506
|
+
|| fail "a permanent classification failure left its wake unacknowledged"
|
|
507
|
+
out=$(cat "$state/.subsuper-escalations" 2>/dev/null || true)
|
|
508
|
+
case "$out" in *"unreadable status span"*) ;;
|
|
509
|
+
*) fail "a permanent classification failure did not surface its diagnostic: $out" ;;
|
|
510
|
+
esac
|
|
511
|
+
[ "$(status_seen_offset "$state" symlink-r9)" = 0 ] \
|
|
512
|
+
|| fail "a classification failure advanced its position"
|
|
513
|
+
|
|
514
|
+
rm -f "$state/.subsuper-last-scan"
|
|
515
|
+
FM_STATE_OVERRIDE="$state" housekeeping "$state"
|
|
516
|
+
FM_DAEMON_DIR="$fakebin" handle_durable_wakes fallback "$state" \
|
|
517
|
+
|| fail "a repeated permanent failure retained its wake"
|
|
518
|
+
[ "$(grep -c 'unreadable status span' "$state/.subsuper-escalations")" = 1 ] \
|
|
519
|
+
|| fail "an unchanged failure was reported more than once across daemon paths"
|
|
520
|
+
[ "$(status_seen_offset "$state" symlink-r9)" = 0 ] \
|
|
521
|
+
|| fail "a repeated classification failure advanced its position"
|
|
522
|
+
|
|
523
|
+
printf 'blocked: changed target state with a longer path\n' > "$dir/target-two-longer"
|
|
524
|
+
ln -snf "$dir/target-two-longer" "$state/symlink-r9.status"
|
|
525
|
+
FM_DAEMON_DIR="$fakebin" handle_durable_wakes fallback "$state" \
|
|
526
|
+
|| fail "a changed permanent failure retained its wake"
|
|
527
|
+
[ "$(grep -c 'unreadable status span' "$state/.subsuper-escalations")" = 2 ] \
|
|
528
|
+
|| fail "a changed failure state did not report again exactly once"
|
|
529
|
+
[ "$(status_seen_offset "$state" symlink-r9)" = 0 ] \
|
|
530
|
+
|| fail "a changed classification failure advanced its position"
|
|
531
|
+
|
|
532
|
+
# Once the log is readable, its content is classified from the position that
|
|
533
|
+
# was never advanced, so nothing written before the failure is lost.
|
|
534
|
+
rm -f "$state/symlink-r9.status"
|
|
535
|
+
printf 'blocked: readable replacement\nworking: cleanup\n' > "$state/symlink-r9.status"
|
|
536
|
+
: > "$state/.subsuper-escalations"
|
|
537
|
+
FM_DAEMON_DIR="$fakebin" handle_durable_wakes fallback "$state" \
|
|
538
|
+
|| fail "a readable replacement did not classify normally"
|
|
539
|
+
out=$(cat "$state/.subsuper-escalations" 2>/dev/null || true)
|
|
540
|
+
case "$out" in *"blocked: readable replacement"*) ;;
|
|
541
|
+
*) fail "the readable replacement was not classified from the unadvanced position: $out" ;;
|
|
542
|
+
esac
|
|
543
|
+
[ "$(status_seen_offset "$state" symlink-r9)" = "$(log_size "$state/symlink-r9.status")" ] \
|
|
544
|
+
|| fail "successful recovery did not advance through the readable replacement"
|
|
545
|
+
[ "$(wc -l < "$dir/acked" | tr -d ' ')" = 4 ] \
|
|
546
|
+
|| fail "every durable wake should be acknowledged, including the failures"
|
|
547
|
+
pass "a permanent classification failure is reported, acknowledged, and never advances its position"
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
test_catchall_scan_surfaces_a_masked_event() {
|
|
551
|
+
local dir state
|
|
552
|
+
dir=$(make_supercase catchall-masked)
|
|
553
|
+
state="$dir/state"
|
|
554
|
+
printf 'working: setup\nneeds-decision [key=release]: pick A or B\nfailed: release build broke\nworking: tidying the branch\n' \
|
|
555
|
+
> "$state/catch-m1.status"
|
|
556
|
+
rm -f "$state/.subsuper-last-scan"
|
|
557
|
+
FM_STATE_OVERRIDE="$state" housekeeping "$state"
|
|
558
|
+
[ -s "$state/.subsuper-escalations" ] \
|
|
559
|
+
|| fail "the catch-all scan missed a decision hidden behind a later working: line"
|
|
560
|
+
grep -F "needs-decision [key=release]: pick A or B" "$state/.subsuper-escalations" >/dev/null \
|
|
561
|
+
|| fail "the catch-all scan omitted the decision it found"
|
|
562
|
+
grep -F "failed: release build broke" "$state/.subsuper-escalations" >/dev/null \
|
|
563
|
+
|| fail "the catch-all scan committed past a failure it did not report"
|
|
564
|
+
# And it records progress, so the next scan does not re-fire the same event.
|
|
565
|
+
: > "$state/.subsuper-escalations"
|
|
566
|
+
rm -f "$state/.subsuper-last-scan"
|
|
567
|
+
FM_STATE_OVERRIDE="$state" housekeeping "$state"
|
|
568
|
+
[ ! -s "$state/.subsuper-escalations" ] \
|
|
569
|
+
|| fail "the catch-all scan re-fired an event it had already escalated"
|
|
570
|
+
pass "the away-mode catch-all scan surfaces a masked event once"
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
test_classify_routine_signal_self() {
|
|
574
|
+
local dir state out
|
|
575
|
+
dir=$(make_supercase classify-routine)
|
|
576
|
+
state="$dir/state"
|
|
577
|
+
printf 'working: step 1\nworking: step 2\n' > "$state/foo-x1.status"
|
|
578
|
+
out=$(FM_STATE_OVERRIDE="$state" classify_signal "$state/foo-x1.status" "$state")
|
|
579
|
+
case "$out" in self\|*) pass "routine signal self-handles" ;; *) fail "routine signal did not self-handle: $out" ;; esac
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
test_classify_terminal_signal_escalates() {
|
|
583
|
+
local dir state kw out
|
|
584
|
+
dir=$(make_supercase classify-terminal)
|
|
585
|
+
state="$dir/state"
|
|
586
|
+
for kw in "done: PR https://x/y/pull/1" "needs-decision: pick A" "blocked: no perms" \
|
|
587
|
+
"failed: rc 2" "PR ready https://x/y/pull/2" "checks green" \
|
|
588
|
+
"ready in branch fm/t1" "merged"; do
|
|
589
|
+
printf 'working\n%s\n' "$kw" > "$state/t.status"
|
|
590
|
+
out=$(FM_STATE_OVERRIDE="$state" classify_signal "$state/t.status" "$state")
|
|
591
|
+
case "$out" in escalate\|*) ;; *) fail "captain verb did not escalate ($kw): $out" ;; esac
|
|
592
|
+
done
|
|
593
|
+
pass "captain-relevant status verbs escalate"
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
test_classify_check_and_unknown_escalate() {
|
|
597
|
+
local out
|
|
598
|
+
out=$(classify_check "check: /s/c.check.sh: merged: https://x")
|
|
599
|
+
case "$out" in escalate\|*) ;; *) fail "check did not escalate: $out" ;; esac
|
|
600
|
+
out=$(classify_unknown "frobnicate: weird")
|
|
601
|
+
case "$out" in escalate\|*) ;; *) fail "unknown did not fail-safe escalate: $out" ;; esac
|
|
602
|
+
out=$(classify_heartbeat)
|
|
603
|
+
case "$out" in self\|*) ;; *) fail "heartbeat did not self-handle: $out" ;; esac
|
|
604
|
+
pass "check + unknown escalate; heartbeat self-handles"
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
test_stale_transient_self_records_marker() {
|
|
608
|
+
local dir state out key
|
|
609
|
+
dir=$(make_supercase stale-transient)
|
|
610
|
+
state="$dir/state"
|
|
611
|
+
printf 'working: building\n' > "$state/qux-w4.status"
|
|
612
|
+
stale_marker_record "sess:fm-qux-w4" "$state"
|
|
613
|
+
out=$(FM_STATE_OVERRIDE="$state" classify_stale "sess:fm-qux-w4" "$state")
|
|
614
|
+
case "$out" in self\|*) ;; *) fail "transient stale did not self-handle: $out" ;; esac
|
|
615
|
+
key=$(printf '%s' "$(window_to_task "sess:fm-qux-w4")" | tr ':/.' '___')
|
|
616
|
+
[ -e "$state/.subsuper-stale-$key" ] || fail "stale marker was not recorded"
|
|
617
|
+
pass "transient stale self-handles and records a persistence marker"
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
test_stale_diagnostic_wedge_survives_busy_housekeeping() {
|
|
621
|
+
local case_name dir state fakebin key task win pane reason status_line action_log
|
|
622
|
+
for case_name in working prior-terminal paused; do
|
|
623
|
+
dir=$(make_supercase "stale-diagnostic-$case_name")
|
|
624
|
+
state="$dir/state"
|
|
625
|
+
fakebin="$dir/fakebin"
|
|
626
|
+
task="suffix-$case_name"
|
|
627
|
+
win="sess:fm-$task"
|
|
628
|
+
pane="$dir/pane.txt"
|
|
629
|
+
action_log="$dir/actions.log"
|
|
630
|
+
reason="stale: $win (idle 500s, possible wedge, escalation 3, demand-deep-inspection: same pane has wedge-escalated 3 times in a row - do not re-absorb on the run-step/pane state alone)"
|
|
631
|
+
fm_write_meta "$state/$task.meta" "window=$win" "backend=tmux"
|
|
632
|
+
case "$case_name" in
|
|
633
|
+
working) status_line='working: building' ;;
|
|
634
|
+
prior-terminal) status_line='done: already surfaced' ;;
|
|
635
|
+
paused) status_line='paused: awaiting an external dependency' ;;
|
|
636
|
+
esac
|
|
637
|
+
printf '%s\n' "$status_line" > "$state/$task.status"
|
|
638
|
+
printf 'Working...\n' > "$pane"
|
|
639
|
+
key=$(printf '%s' "$task" | tr ':/.' '___')
|
|
640
|
+
echo $(( $(date +%s) - 500 )) > "$state/.subsuper-stale-$key"
|
|
641
|
+
[ "$case_name" = prior-terminal ] \
|
|
642
|
+
&& seen_through "$state" "$task"
|
|
643
|
+
[ "$case_name" = paused ] \
|
|
644
|
+
&& echo $(( $(date +%s) - 500 )) > "$state/.subsuper-paused-$key"
|
|
645
|
+
|
|
646
|
+
(
|
|
647
|
+
kill() { printf 'kill %s\n' "$*" >> "$action_log"; }
|
|
648
|
+
fm_backend_send_text_submit() { printf 'interrupt %s\n' "$*" >> "$action_log"; }
|
|
649
|
+
LOG="$dir/daemon.log" FM_STATE_OVERRIDE="$state" handle_wake "$reason" "$state"
|
|
650
|
+
PATH="$fakebin:$PATH" FM_FAKE_TMUX_WINDOW="$win" FM_FAKE_TMUX_CAPTURE="$pane" \
|
|
651
|
+
FM_STATE_OVERRIDE="$state" FM_ESCALATE_BATCH_SECS=999999 housekeeping "$state"
|
|
652
|
+
)
|
|
653
|
+
case "$case_name" in
|
|
654
|
+
paused)
|
|
655
|
+
# A current declared wait owns the cadence: the enriched wedge routes to the
|
|
656
|
+
# bounded PAUSE_RESURFACE_SECS recheck instead of escalating on the wedge
|
|
657
|
+
# cadence. test_enriched_wedge_under_declared_wait_uses_pause_cadence pins
|
|
658
|
+
# the full cadence, including the one recheck that still re-surfaces it.
|
|
659
|
+
[ ! -s "$state/.subsuper-escalations" ] \
|
|
660
|
+
|| fail "paused enriched wedge escalated instead of routing to the pause cadence: $(cat "$state/.subsuper-escalations")"
|
|
661
|
+
[ -e "$state/.subsuper-paused-$key" ] \
|
|
662
|
+
|| fail "paused enriched wedge erased ordinary pause tracking" ;;
|
|
663
|
+
*)
|
|
664
|
+
[ "$(wc -l < "$state/.subsuper-escalations" | tr -d ' ')" = 1 ] \
|
|
665
|
+
|| fail "$case_name enriched wedge did not produce exactly one escalation"
|
|
666
|
+
grep -F "${reason#stale: }" "$state/.subsuper-escalations" >/dev/null \
|
|
667
|
+
|| fail "$case_name enriched wedge lost its demand-deep-inspection detail"
|
|
668
|
+
[ ! -e "$state/.subsuper-paused-$key" ] \
|
|
669
|
+
|| fail "$case_name enriched wedge created pause tracking" ;;
|
|
670
|
+
esac
|
|
671
|
+
[ ! -e "$state/.subsuper-stale-$key" ] \
|
|
672
|
+
|| fail "$case_name enriched wedge retained ordinary stale tracking"
|
|
673
|
+
[ ! -s "$action_log" ] \
|
|
674
|
+
|| fail "$case_name enriched wedge interrupted or killed the busy worker"
|
|
675
|
+
done
|
|
676
|
+
pass "enriched stale wedges bypass status absorption except under a declared wait, without disturbing busy workers"
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
# The second half of issue #3149. The watcher's wedge timer emits an enriched
|
|
680
|
+
# "idle Ns, possible wedge, escalation N" reason for any pane it reads as frozen -
|
|
681
|
+
# including one whose crew has a CURRENT declared wait, because the watcher's own
|
|
682
|
+
# provably-working classification and the crew's status line can disagree (a crew
|
|
683
|
+
# that declares `paused:` while its no-mistakes run is still attributed to its code
|
|
684
|
+
# reads `working` to pause_state_class and takes the wedge timer). handle_wake's
|
|
685
|
+
# enriched-wedge override force-escalated every such reason, discarding the `pause`
|
|
686
|
+
# verdict classify_stale had already returned for the same pane, so a healthy
|
|
687
|
+
# declared wait was escalated once per STALE_ESCALATE_SECS for as long as it lasted.
|
|
688
|
+
# A declaration is categorically stronger than the run-step/pane state the enriched
|
|
689
|
+
# reason tells the supervisor not to re-absorb on, so it routes the pane to the long
|
|
690
|
+
# PAUSE_RESURFACE_SECS recheck instead. This drives repeated enriched wedges through
|
|
691
|
+
# the real handle_wake/housekeeping pair and asserts the cadence, not just one wake.
|
|
692
|
+
test_enriched_wedge_under_declared_wait_uses_pause_cadence() {
|
|
693
|
+
local dir state fakebin task win pane key reason i escalations
|
|
694
|
+
dir=$(make_supercase enriched-wedge-declared-wait)
|
|
695
|
+
state="$dir/state"; fakebin="$dir/fakebin"
|
|
696
|
+
task=paused-wedge-w1; win="sess:fm-$task"; pane="$dir/pane.txt"
|
|
697
|
+
key=$(printf '%s' "$task" | tr ':/.' '___')
|
|
698
|
+
fm_write_meta "$state/$task.meta" "window=$win" "backend=tmux"
|
|
699
|
+
printf 'working: dispatching the long audit\npaused: the audit engine is running to completion\n' \
|
|
700
|
+
> "$state/$task.status"
|
|
701
|
+
printf 'idle prompt $\n' > "$pane"
|
|
702
|
+
case "$(FM_STATE_OVERRIDE="$state" classify_stale "$win" "$state")" in
|
|
703
|
+
pause\|*) ;;
|
|
704
|
+
*) fail "the fixture's own classifier verdict is not a pause, so this case pins nothing about the override" ;;
|
|
705
|
+
esac
|
|
706
|
+
|
|
707
|
+
# Four consecutive wedge-cadence deliveries, exactly as the watcher emits them once
|
|
708
|
+
# a pane crosses STALE_ESCALATE_SECS repeatedly.
|
|
709
|
+
for i in 2 3 4 5; do
|
|
710
|
+
if [ "$i" -ge 3 ]; then
|
|
711
|
+
# Past FM_WEDGE_DEMAND_INSPECT_COUNT the watcher enriches the same reason with
|
|
712
|
+
# its demand-deep-inspection marker; a declaration outranks both forms.
|
|
713
|
+
reason="stale: $win (idle 250s, possible wedge, escalation $i, demand-deep-inspection: same pane has wedge-escalated $i times in a row - do not re-absorb on the run-step/pane state alone)"
|
|
714
|
+
else
|
|
715
|
+
reason="stale: $win (idle 250s, possible wedge, escalation $i)"
|
|
716
|
+
fi
|
|
717
|
+
LOG="$dir/daemon.log" FM_STATE_OVERRIDE="$state" handle_wake "$reason" "$state"
|
|
718
|
+
PATH="$fakebin:$PATH" FM_FAKE_TMUX_WINDOW="$win" FM_FAKE_TMUX_CAPTURE="$pane" \
|
|
719
|
+
FM_STATE_OVERRIDE="$state" FM_ESCALATE_BATCH_SECS=999999 \
|
|
720
|
+
FM_STALE_ESCALATE_SECS=240 FM_PAUSE_RESURFACE_SECS=3600 housekeeping "$state"
|
|
721
|
+
done
|
|
722
|
+
|
|
723
|
+
[ ! -s "$state/.subsuper-escalations" ] \
|
|
724
|
+
|| fail "a declared wait escalated inside one PAUSE_RESURFACE_SECS window: $(cat "$state/.subsuper-escalations")"
|
|
725
|
+
[ -e "$state/.subsuper-paused-$key" ] \
|
|
726
|
+
|| fail "an enriched wedge under a declared wait did not record pause tracking"
|
|
727
|
+
[ ! -e "$state/.subsuper-stale-$key" ] \
|
|
728
|
+
|| fail "an enriched wedge under a declared wait left wedge aging in place"
|
|
729
|
+
|
|
730
|
+
# Past PAUSE_RESURFACE_SECS the wait must re-surface exactly once as an
|
|
731
|
+
# awaiting-external recheck (never a wedge) and reset its window.
|
|
732
|
+
echo $(( $(date +%s) - 5000 )) > "$state/.subsuper-paused-$key"
|
|
733
|
+
PATH="$fakebin:$PATH" FM_FAKE_TMUX_WINDOW="$win" FM_FAKE_TMUX_CAPTURE="$pane" \
|
|
734
|
+
FM_STATE_OVERRIDE="$state" FM_ESCALATE_BATCH_SECS=999999 FM_PAUSE_RESURFACE_SECS=3600 \
|
|
735
|
+
housekeeping "$state"
|
|
736
|
+
escalations=0
|
|
737
|
+
[ -s "$state/.subsuper-escalations" ] \
|
|
738
|
+
&& escalations=$(wc -l < "$state/.subsuper-escalations" | tr -d ' ')
|
|
739
|
+
[ "$escalations" = 1 ] || fail "the pause window produced $escalations escalations, expected exactly one recheck"
|
|
740
|
+
grep -F "awaiting external" "$state/.subsuper-escalations" >/dev/null \
|
|
741
|
+
|| fail "the one pause-window escalation was not an awaiting-external recheck"
|
|
742
|
+
grep -F "possible wedge" "$state/.subsuper-escalations" >/dev/null \
|
|
743
|
+
&& fail "the pause-window recheck was mislabeled a possible wedge"
|
|
744
|
+
|
|
745
|
+
# A later status append that stops declaring the wait ends the routing: the same
|
|
746
|
+
# enriched wedge escalates again, unchanged.
|
|
747
|
+
: > "$state/.subsuper-escalations"
|
|
748
|
+
printf 'working: the audit finished, resuming\n' >> "$state/$task.status"
|
|
749
|
+
reason="stale: $win (idle 250s, possible wedge, escalation 6)"
|
|
750
|
+
LOG="$dir/daemon.log" FM_STATE_OVERRIDE="$state" handle_wake "$reason" "$state"
|
|
751
|
+
PATH="$fakebin:$PATH" FM_FAKE_TMUX_WINDOW="$win" FM_FAKE_TMUX_CAPTURE="$pane" \
|
|
752
|
+
FM_STATE_OVERRIDE="$state" FM_ESCALATE_BATCH_SECS=999999 FM_PAUSE_RESURFACE_SECS=3600 \
|
|
753
|
+
housekeeping "$state"
|
|
754
|
+
grep -F "${reason#stale: }" "$state/.subsuper-escalations" >/dev/null \
|
|
755
|
+
|| fail "wedge escalation was not restored after the crew left its declared wait"
|
|
756
|
+
[ ! -e "$state/.subsuper-paused-$key" ] \
|
|
757
|
+
|| fail "pause tracking survived a status append that no longer declares the wait"
|
|
758
|
+
pass "an enriched wedge under a declared wait uses the pause cadence and restores wedge detection on resume"
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
test_stale_terminal_escalates() {
|
|
762
|
+
local dir state out
|
|
763
|
+
dir=$(make_supercase stale-terminal)
|
|
764
|
+
state="$dir/state"
|
|
765
|
+
printf 'done: ready in branch fm/t1\n' > "$state/fin-t5.status"
|
|
766
|
+
out=$(FM_STATE_OVERRIDE="$state" classify_stale "sess:fm-fin-t5" "$state")
|
|
767
|
+
case "$out" in escalate\|*) ;; *) fail "terminal stale did not escalate: $out" ;; esac
|
|
768
|
+
fm_write_meta "$state/herdr-t5.meta" "window=default:w1:p2" "backend=herdr"
|
|
769
|
+
printf 'done: ready in branch fm/herdr\n' > "$state/herdr-t5.status"
|
|
770
|
+
out=$(FM_STATE_OVERRIDE="$state" classify_stale "default:w1:p2" "$state")
|
|
771
|
+
case "$out" in escalate\|*) ;; *) fail "terminal herdr stale did not escalate through metadata: $out" ;; esac
|
|
772
|
+
pass "stale + terminal status escalates immediately"
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
test_stale_actionable_wait_escalates_and_keeps_pause_cadence() {
|
|
776
|
+
local dir state win key out reason resumed_win resumed_key
|
|
777
|
+
dir=$(make_supercase stale-actionable-wait); state="$dir/state"
|
|
778
|
+
win="sess:fm-waiting-r10"; key=$(printf '%s' waiting-r10 | tr ':/.' '___')
|
|
779
|
+
printf 'blocked [key=release]: need captain approval\npaused: waiting for release access\n' \
|
|
780
|
+
> "$state/waiting-r10.status"
|
|
781
|
+
|
|
782
|
+
out=$(FM_STATE_OVERRIDE="$state" classify_stale "$win" "$state")
|
|
783
|
+
case "$out" in escalate\|*"blocked [key=release]: need captain approval"*) ;;
|
|
784
|
+
*) fail "a current wait hid an unreported blocker from stale classification: $out" ;;
|
|
785
|
+
esac
|
|
786
|
+
reason="stale: $win (idle 250s, possible wedge, escalation 3, demand-deep-inspection: inspect the repeated wedge)"
|
|
787
|
+
FM_ESCALATE_BATCH_SECS=999 handle_wake "$reason" "$state"
|
|
788
|
+
out=$(cat "$state/.subsuper-escalations" 2>/dev/null || true)
|
|
789
|
+
case "$out" in *"blocked [key=release]: need captain approval"*) ;;
|
|
790
|
+
*) fail "the stale escalation did not name the blocker behind the current wait: $out" ;;
|
|
791
|
+
esac
|
|
792
|
+
[ -e "$state/.subsuper-paused-$key" ] \
|
|
793
|
+
|| fail "an actionable current wait did not retain its pause cadence"
|
|
794
|
+
[ ! -e "$state/.subsuper-stale-$key" ] \
|
|
795
|
+
|| fail "an actionable current wait was also aged as a wedge"
|
|
796
|
+
|
|
797
|
+
resumed_win="sess:fm-resumed-r10"; resumed_key=$(printf '%s' resumed-r10 | tr ':/.' '___')
|
|
798
|
+
printf 'paused: old wait\nworking: resumed after access arrived\n' > "$state/resumed-r10.status"
|
|
799
|
+
printf '1' > "$state/.subsuper-paused-$resumed_key"
|
|
800
|
+
FM_ESCALATE_BATCH_SECS=999 handle_wake "stale: $resumed_win" "$state"
|
|
801
|
+
[ ! -e "$state/.subsuper-paused-$resumed_key" ] \
|
|
802
|
+
|| fail "an older pause declaration kept a resumed crew on pause cadence"
|
|
803
|
+
[ -e "$state/.subsuper-stale-$resumed_key" ] \
|
|
804
|
+
|| fail "a resumed crew did not return to ordinary stale aging"
|
|
805
|
+
pass "stale escalation and current wait cadence remain independent"
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
# A DECLARED external-wait pause (paused:) is neither a wedge nor a terminal
|
|
809
|
+
# escalation: classify_stale returns the `pause` action so handle_wake records a
|
|
810
|
+
# pause marker (long re-surface cadence) rather than a wedge stale marker.
|
|
811
|
+
test_stale_paused_classifies_pause() {
|
|
812
|
+
local dir state out pause_reason
|
|
813
|
+
dir=$(make_supercase stale-paused)
|
|
814
|
+
state="$dir/state"
|
|
815
|
+
pause_reason='paused: waiting for upstream checks green, merged, and blocked state to clear'
|
|
816
|
+
status_is_captain_relevant "$pause_reason" && fail "pause reason phrases made the status captain-relevant"
|
|
817
|
+
printf '%s\n' "$pause_reason" > "$state/held-w9.status"
|
|
818
|
+
out=$(FM_STATE_OVERRIDE="$state" classify_stale "sess:fm-held-w9" "$state")
|
|
819
|
+
case "$out" in pause\|*) ;; *) fail "declared pause did not classify as pause: $out" ;; esac
|
|
820
|
+
pass "paused reasons with captain phrases remain pause-classified"
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
# A verified captain-held transfer is the other declaration that leaves an idle pane
|
|
824
|
+
# EXPECTED, so it earns the same pause action as paused: rather than being aged as a
|
|
825
|
+
# wedge. The wait itself is already durable in the captain-held backlog task.
|
|
826
|
+
test_stale_captain_held_classifies_pause() {
|
|
827
|
+
local dir state out held_reason
|
|
828
|
+
dir=$(make_supercase stale-captain-held)
|
|
829
|
+
state="$dir/state"
|
|
830
|
+
held_reason='captain-held [key=route]: tracked by task-decision-route'
|
|
831
|
+
status_is_captain_relevant "$held_reason" && fail "a captain-held transfer line was treated as captain-relevant"
|
|
832
|
+
printf '%s\n' "$held_reason" > "$state/held-w9h.status"
|
|
833
|
+
out=$(FM_STATE_OVERRIDE="$state" classify_stale "sess:fm-held-w9h" "$state")
|
|
834
|
+
case "$out" in pause\|*) ;; *) fail "captain-held transfer did not classify as pause: $out" ;; esac
|
|
835
|
+
pass "a captain-held transfer classifies as pause, not as a wedge candidate"
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
# handle_wake on a paused stale records a pause marker, drops any pre-existing wedge
|
|
839
|
+
# marker (so a working->paused pane is not still wedge-aged), and does NOT escalate
|
|
840
|
+
# on the wake itself - the recheck is housekeeping's job on the long cadence.
|
|
841
|
+
test_handle_wake_paused_records_pause_marker() {
|
|
842
|
+
local dir state key win
|
|
843
|
+
dir=$(make_supercase handle-paused)
|
|
844
|
+
state="$dir/state"
|
|
845
|
+
win="sess:fm-held-w10"
|
|
846
|
+
printf 'paused: awaiting the vendor rate-limit reset\n' > "$state/held-w10.status"
|
|
847
|
+
key=$(printf '%s' "held-w10" | tr ':/.' '___')
|
|
848
|
+
date +%s > "$state/.subsuper-stale-$key"
|
|
849
|
+
FM_STATE_OVERRIDE="$state" handle_wake "stale: $win" "$state"
|
|
850
|
+
[ -e "$state/.subsuper-paused-$key" ] || fail "pause marker not recorded by handle_wake"
|
|
851
|
+
[ ! -e "$state/.subsuper-stale-$key" ] || fail "wedge marker not cleared when the crew declared a pause"
|
|
852
|
+
[ ! -s "$state/.subsuper-escalations" ] || fail "a declared pause escalated on the wake itself (should defer to the long recheck)"
|
|
853
|
+
pass "handle_wake on a paused stale records a pause marker, drops the wedge marker, and does not escalate"
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
test_handle_wake_paused_signal_records_pause_marker() {
|
|
857
|
+
local dir state key win
|
|
858
|
+
dir=$(make_supercase handle-paused-signal)
|
|
859
|
+
state="$dir/state"
|
|
860
|
+
win="sess:fm-held-w10-signal"
|
|
861
|
+
printf 'window=%s\nkind=ship\n' "$win" > "$state/held-w10-signal.meta"
|
|
862
|
+
printf 'paused: awaiting the vendor rate-limit reset\n' > "$state/held-w10-signal.status"
|
|
863
|
+
key=$(printf '%s' "held-w10-signal" | tr ':/.' '___')
|
|
864
|
+
date +%s > "$state/.subsuper-stale-$key"
|
|
865
|
+
FM_STATE_OVERRIDE="$state" handle_wake "signal: $state/held-w10-signal.status" "$state"
|
|
866
|
+
[ -e "$state/.subsuper-paused-$key" ] || fail "pause signal did not record a pause marker"
|
|
867
|
+
[ ! -e "$state/.subsuper-stale-$key" ] || fail "pause signal did not clear the wedge marker"
|
|
868
|
+
[ ! -s "$state/.subsuper-escalations" ] || fail "a declared pause signal escalated instead of self-handling"
|
|
869
|
+
pass "handle_wake records a declared pause from a routine signal for long-cadence rechecks"
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
test_handle_wake_terminal_signal_clears_pause_tracking() {
|
|
873
|
+
local dir state key watcher_key win
|
|
874
|
+
dir=$(make_supercase handle-terminal-signal)
|
|
875
|
+
state="$dir/state"
|
|
876
|
+
win="sess:fm-held-w10-terminal"
|
|
877
|
+
printf 'window=%s\nkind=ship\n' "$win" > "$state/held-w10-terminal.meta"
|
|
878
|
+
printf 'done: upstream landed\n' > "$state/held-w10-terminal.status"
|
|
879
|
+
key=$(printf '%s' "held-w10-terminal" | tr '.:/' '___')
|
|
880
|
+
watcher_key=$(printf '%s' "$win" | tr '.:/' '___')
|
|
881
|
+
date +%s > "$state/.subsuper-paused-$key"
|
|
882
|
+
date +%s > "$state/.subsuper-stale-$key"
|
|
883
|
+
: > "$state/.paused-$watcher_key"
|
|
884
|
+
: > "$state/.stale-$watcher_key"
|
|
885
|
+
: > "$state/.wedge-escalations-$watcher_key"
|
|
886
|
+
FM_STATE_OVERRIDE="$state" handle_wake "signal: $state/held-w10-terminal.status" "$state"
|
|
887
|
+
[ ! -e "$state/.subsuper-paused-$key" ] || fail "terminal signal retained the daemon pause marker"
|
|
888
|
+
[ ! -e "$state/.subsuper-stale-$key" ] || fail "terminal signal retained daemon stale tracking"
|
|
889
|
+
[ ! -e "$state/.paused-$watcher_key" ] || fail "terminal signal retained watcher pause tracking"
|
|
890
|
+
[ ! -e "$state/.stale-$watcher_key" ] || fail "terminal signal retained watcher stale tracking"
|
|
891
|
+
[ ! -e "$state/.wedge-escalations-$watcher_key" ] || fail "terminal signal retained watcher wedge tracking"
|
|
892
|
+
FM_STATE_OVERRIDE="$state" handle_wake "stale: $win" "$state"
|
|
893
|
+
[ ! -e "$state/.subsuper-stale-$key" ] || fail "terminal stale dedupe restored daemon stale tracking"
|
|
894
|
+
pass "a terminal signal clears pause and stale tracking across both supervisors"
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
test_housekeeping_migrates_watcher_pause_marker() {
|
|
898
|
+
local dir state key win
|
|
899
|
+
dir=$(make_supercase migrate-watcher-pause)
|
|
900
|
+
state="$dir/state"
|
|
901
|
+
win="sess:fm-held-w10-migrate"
|
|
902
|
+
printf 'window=%s\nkind=ship\n' "$win" > "$state/held-w10-migrate.meta"
|
|
903
|
+
printf 'paused: awaiting the upstream release\n' > "$state/held-w10-migrate.status"
|
|
904
|
+
key=$(printf '%s' "$win" | tr '.:/' '___')
|
|
905
|
+
: > "$state/.paused-$key"
|
|
906
|
+
FM_STATE_OVERRIDE="$state" housekeeping "$state"
|
|
907
|
+
key=$(printf '%s' "held-w10-migrate" | tr '.:/' '___')
|
|
908
|
+
[ -e "$state/.subsuper-paused-$key" ] || fail "watcher pause marker was not migrated into daemon tracking"
|
|
909
|
+
[ ! -e "$state/.subsuper-stale-$key" ] || fail "watcher pause migration left a wedge marker behind"
|
|
910
|
+
pass "housekeeping migrates a normal-watcher's declared pause into daemon tracking"
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
test_housekeeping_migrates_watcher_unpaused_marker_to_clear() {
|
|
914
|
+
local dir state key watcher_key win
|
|
915
|
+
dir=$(make_supercase migrate-watcher-unpaused)
|
|
916
|
+
state="$dir/state"
|
|
917
|
+
win="sess:fm-held-w10-migrate-unpaused"
|
|
918
|
+
printf 'window=%s\nkind=ship\n' "$win" > "$state/held-w10-migrate-unpaused.meta"
|
|
919
|
+
printf 'working: upstream landed, resuming\n' > "$state/held-w10-migrate-unpaused.status"
|
|
920
|
+
watcher_key=$(printf '%s' "$win" | tr '.:/' '___')
|
|
921
|
+
: > "$state/.paused-$watcher_key"
|
|
922
|
+
FM_STATE_OVERRIDE="$state" housekeeping "$state"
|
|
923
|
+
key=$(printf '%s' "held-w10-migrate-unpaused" | tr '.:/' '___')
|
|
924
|
+
[ ! -e "$state/.paused-$watcher_key" ] || fail "stale watcher pause marker was not cleared after resume"
|
|
925
|
+
[ ! -e "$state/.subsuper-paused-$key" ] || fail "unpaused watcher handoff created a daemon pause marker"
|
|
926
|
+
[ ! -e "$state/.subsuper-stale-$key" ] || fail "unpaused watcher handoff retained daemon stale tracking"
|
|
927
|
+
[ ! -e "$state/.stale-$watcher_key" ] || fail "unpaused watcher handoff retained watcher stale tracking"
|
|
928
|
+
pass "housekeeping clears an already-resumed watcher pause across both supervisors"
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
test_housekeeping_seeds_pause_marker_from_status() {
|
|
932
|
+
local dir state key win
|
|
933
|
+
dir=$(make_supercase seed-paused-status)
|
|
934
|
+
state="$dir/state"
|
|
935
|
+
win="sess:fm-held-w10-seed"
|
|
936
|
+
printf 'window=%s\nkind=ship\n' "$win" > "$state/held-w10-seed.meta"
|
|
937
|
+
printf 'paused: awaiting the upstream release\n' > "$state/held-w10-seed.status"
|
|
938
|
+
key=$(printf '%s' "held-w10-seed" | tr '.:/' '___')
|
|
939
|
+
FM_STATE_OVERRIDE="$state" housekeeping "$state"
|
|
940
|
+
[ -e "$state/.subsuper-paused-$key" ] || fail "paused status did not seed daemon pause tracking"
|
|
941
|
+
[ ! -e "$state/.subsuper-stale-$key" ] || fail "paused status seeded wedge tracking"
|
|
942
|
+
pass "housekeeping seeds pause tracking from status without a watcher marker"
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
# housekeeping re-surfaces a stale declared pause only past PAUSE_RESURFACE_SECS,
|
|
946
|
+
# as an awaiting-external recheck (never a wedge), and RESETS the marker so the
|
|
947
|
+
# window repeats rather than firing once.
|
|
948
|
+
test_housekeeping_paused_resurfaces_and_resets() {
|
|
949
|
+
local dir state fakebin win pane key age
|
|
950
|
+
dir=$(make_supercase paused-resurface)
|
|
951
|
+
state="$dir/state"; fakebin="$dir/fakebin"
|
|
952
|
+
win="sess:fm-held-w11"; pane="$dir/pane.txt"
|
|
953
|
+
printf 'paused: holding for the upstream tool release\n' > "$state/held-w11.status"
|
|
954
|
+
printf 'idle prompt $\n' > "$pane"
|
|
955
|
+
key=$(printf '%s' "held-w11" | tr ':/.' '___')
|
|
956
|
+
echo $(( $(date +%s) - 5000 )) > "$state/.subsuper-paused-$key"
|
|
957
|
+
PATH="$fakebin:$PATH" FM_FAKE_TMUX_WINDOW="$win" FM_FAKE_TMUX_CAPTURE="$pane" \
|
|
958
|
+
FM_STATE_OVERRIDE="$state" FM_PAUSE_RESURFACE_SECS=240 housekeeping "$state"
|
|
959
|
+
grep -F "awaiting external" "$state/.subsuper-escalations" >/dev/null 2>&1 || fail "declared pause was not re-surfaced as an awaiting-external recheck"
|
|
960
|
+
grep -F "awaiting the captain" "$state/.subsuper-escalations" >/dev/null 2>&1 && fail "declared pause named the captain instead of its external dependency"
|
|
961
|
+
grep -F "possible wedge" "$state/.subsuper-escalations" >/dev/null 2>&1 && fail "declared pause was mislabeled a possible wedge"
|
|
962
|
+
[ -e "$state/.subsuper-paused-$key" ] || fail "pause marker cleared instead of reset for the next window"
|
|
963
|
+
age=$(( $(date +%s) - $(cat "$state/.subsuper-paused-$key" 2>/dev/null || echo 0) ))
|
|
964
|
+
[ "$age" -lt 60 ] || fail "pause marker was not reset to now on re-surface (age ${age}s)"
|
|
965
|
+
pass "housekeeping re-surfaces a stale declared pause on the long cadence and resets its window"
|
|
966
|
+
}
|
|
967
|
+
|
|
968
|
+
# The other half of quieting a captain-held task: it must NOT be silenced outright.
|
|
969
|
+
# fm-classify-lib.sh's cadence comment is explicit that a forgotten hold cannot rot
|
|
970
|
+
# invisibly, so a held task re-surfaces on the same bounded window as a pause, with
|
|
971
|
+
# its marker reset so the window repeats instead of firing once. The digest the
|
|
972
|
+
# captain reads must also name the captain rather than an external dependency: the
|
|
973
|
+
# hold is waiting on the one person reading the digest, so borrowing the pause verb's
|
|
974
|
+
# awaiting-external wording would point them away from being the blocker.
|
|
975
|
+
test_housekeeping_captain_held_resurfaces_and_resets() {
|
|
976
|
+
local dir state fakebin win pane key age
|
|
977
|
+
dir=$(make_supercase captain-held-resurface)
|
|
978
|
+
state="$dir/state"; fakebin="$dir/fakebin"
|
|
979
|
+
win="sess:fm-held-w11h"; pane="$dir/pane.txt"
|
|
980
|
+
printf 'captain-held [key=route]: tracked by task-decision-route\n' > "$state/held-w11h.status"
|
|
981
|
+
printf 'idle prompt $\n' > "$pane"
|
|
982
|
+
key=$(printf '%s' "held-w11h" | tr ':/.' '___')
|
|
983
|
+
echo $(( $(date +%s) - 5000 )) > "$state/.subsuper-paused-$key"
|
|
984
|
+
PATH="$fakebin:$PATH" FM_FAKE_TMUX_WINDOW="$win" FM_FAKE_TMUX_CAPTURE="$pane" \
|
|
985
|
+
FM_STATE_OVERRIDE="$state" FM_PAUSE_RESURFACE_SECS=240 housekeeping "$state"
|
|
986
|
+
grep -F "awaiting the captain" "$state/.subsuper-escalations" >/dev/null 2>&1 || fail "a captain hold was silenced entirely instead of re-surfacing as a captain-owned recheck: $(cat "$state/.subsuper-escalations" 2>/dev/null || true)"
|
|
987
|
+
grep -F "awaiting external" "$state/.subsuper-escalations" >/dev/null 2>&1 && fail "a captain hold was re-surfaced as an external wait, hiding that the captain is the blocker"
|
|
988
|
+
grep -F "possible wedge" "$state/.subsuper-escalations" >/dev/null 2>&1 && fail "a captain hold was re-surfaced as a possible wedge"
|
|
989
|
+
[ -e "$state/.subsuper-paused-$key" ] || fail "captain-held marker cleared instead of reset for the next window"
|
|
990
|
+
age=$(( $(date +%s) - $(cat "$state/.subsuper-paused-$key" 2>/dev/null || echo 0) ))
|
|
991
|
+
[ "$age" -lt 60 ] || fail "captain-held marker was not reset to now on re-surface (age ${age}s)"
|
|
992
|
+
pass "housekeeping re-surfaces a forgotten captain hold on the long cadence and resets its window"
|
|
993
|
+
}
|
|
994
|
+
|
|
995
|
+
# A crew that RESUMED - whose latest status line no longer declares the wait - drops
|
|
996
|
+
# its pause tracking without escalating. The dimension pinned here is that pane busy
|
|
997
|
+
# state does not GATE that clear: the status append alone ends the wait, on the
|
|
998
|
+
# reconcile path the loop head runs before the pause recheck ever reads a pane, so a
|
|
999
|
+
# crew that resumed into a genuinely busy pane cannot hold a stale window open. The
|
|
1000
|
+
# fixture asserts its own busy verdict first, so it cannot silently decay into an
|
|
1001
|
+
# idle-pane case (already covered by test_housekeeping_paused_unpaused_cleared) and
|
|
1002
|
+
# keep claiming that dimension. The inverse - a busy pane that is STILL declaring the
|
|
1003
|
+
# wait - is test_housekeeping_busy_declared_wait_matures_its_window.
|
|
1004
|
+
test_housekeeping_paused_resumed_cleared() {
|
|
1005
|
+
local dir state fakebin win pane key
|
|
1006
|
+
dir=$(make_supercase paused-resumed)
|
|
1007
|
+
state="$dir/state"; fakebin="$dir/fakebin"
|
|
1008
|
+
win="sess:fm-held-w12"; pane="$dir/pane.txt"
|
|
1009
|
+
printf 'paused: holding for the upstream tool release\nworking: upstream landed, resuming\n' \
|
|
1010
|
+
> "$state/held-w12.status"
|
|
1011
|
+
printf 'Working...\n' > "$pane"
|
|
1012
|
+
fm_write_meta "$state/held-w12.meta" "window=$win" "worktree=$dir/wt" "kind=ship" "harness=pi"
|
|
1013
|
+
local gen; gen=$("$ROOT/bin/fm-busy-event.sh" arm "$state" held-w12)
|
|
1014
|
+
"$ROOT/bin/fm-busy-event.sh" apply "$state" held-w12 busy --gen "$gen" \
|
|
1015
|
+
--source pi-ext --event agent-start
|
|
1016
|
+
key=$(printf '%s' "held-w12" | tr ':/.' '___')
|
|
1017
|
+
echo $(( $(date +%s) - 5000 )) > "$state/.subsuper-paused-$key"
|
|
1018
|
+
PATH="$fakebin:$PATH" FM_FAKE_TMUX_WINDOW="$win" FM_FAKE_TMUX_CAPTURE="$pane" \
|
|
1019
|
+
FM_STATE_OVERRIDE="$state" stale_window_is_busy "$win" "$state" \
|
|
1020
|
+
|| fail "the resumed-pause fixture does not actually read busy, so it pins nothing about busy state"
|
|
1021
|
+
PATH="$fakebin:$PATH" FM_FAKE_TMUX_WINDOW="$win" FM_FAKE_TMUX_CAPTURE="$pane" \
|
|
1022
|
+
FM_STATE_OVERRIDE="$state" FM_PAUSE_RESURFACE_SECS=240 housekeeping "$state"
|
|
1023
|
+
[ -e "$state/.subsuper-paused-$key" ] && fail "resumed (busy, no longer declaring) pause marker was not cleared"
|
|
1024
|
+
[ ! -s "$state/.subsuper-escalations" ] || fail "a resumed pause was escalated"
|
|
1025
|
+
pass "a busy pane cannot gate the pause clear once its crew's status no longer declares the wait"
|
|
1026
|
+
}
|
|
1027
|
+
|
|
1028
|
+
# The inverse of test_housekeeping_paused_resumed_cleared, and the first half of
|
|
1029
|
+
# issue #3149. A declared wait can legitimately hold a pane BUSY - a worker parked on
|
|
1030
|
+
# a long foreground call it keeps live for as long as the wait lasts - so a busy
|
|
1031
|
+
# verdict is not evidence that the crew resumed. Reading it as one dropped the marker
|
|
1032
|
+
# un-escalated, and migrate_watcher_pause_markers recreated it with a fresh timestamp
|
|
1033
|
+
# on the very next tick, so the window restarted forever and the wait never matured
|
|
1034
|
+
# into its one recheck. Away mode makes that terminal: the watcher hands a busy
|
|
1035
|
+
# declared wait to the daemon exactly once per declaration (bin/fm-watch.sh's
|
|
1036
|
+
# busy_turn_bound_check), so this recheck is the only thing left that can re-surface
|
|
1037
|
+
# the pane at all. Both declaration forms take the same 2b arm, so both are pinned.
|
|
1038
|
+
test_housekeeping_busy_declared_wait_matures_its_window() {
|
|
1039
|
+
local case_name dir state fakebin task win pane key gen tick age escalations digest
|
|
1040
|
+
for case_name in paused captain-held; do
|
|
1041
|
+
dir=$(make_supercase "busy-declared-wait-$case_name")
|
|
1042
|
+
state="$dir/state"; fakebin="$dir/fakebin"
|
|
1043
|
+
task="held-w12b-$case_name"; win="sess:fm-$task"; pane="$dir/pane.txt"
|
|
1044
|
+
case "$case_name" in
|
|
1045
|
+
paused) printf 'paused: the audit engine is running to completion\n' > "$state/$task.status"
|
|
1046
|
+
digest="awaiting external" ;;
|
|
1047
|
+
captain-held) printf 'captain-held [key=route]: tracked by task-decision-route\n' > "$state/$task.status"
|
|
1048
|
+
digest="awaiting the captain" ;;
|
|
1049
|
+
esac
|
|
1050
|
+
printf 'Working...\n' > "$pane"
|
|
1051
|
+
fm_write_meta "$state/$task.meta" "window=$win" "worktree=$dir/wt" "kind=ship" "harness=pi"
|
|
1052
|
+
gen=$("$ROOT/bin/fm-busy-event.sh" arm "$state" "$task")
|
|
1053
|
+
"$ROOT/bin/fm-busy-event.sh" apply "$state" "$task" busy --gen "$gen" \
|
|
1054
|
+
--source pi-ext --event agent-start
|
|
1055
|
+
key=$(printf '%s' "$task" | tr ':/.' '___')
|
|
1056
|
+
PATH="$fakebin:$PATH" FM_FAKE_TMUX_WINDOW="$win" FM_FAKE_TMUX_CAPTURE="$pane" \
|
|
1057
|
+
FM_STATE_OVERRIDE="$state" stale_window_is_busy "$win" "$state" \
|
|
1058
|
+
|| fail "the $case_name fixture does not actually read busy, so it pins nothing about busy state"
|
|
1059
|
+
|
|
1060
|
+
# Immature window: ticks inside PAUSE_RESURFACE_SECS neither escalate nor let the
|
|
1061
|
+
# marker the window ages against be recreated with a fresh timestamp.
|
|
1062
|
+
echo $(( $(date +%s) - 100 )) > "$state/.subsuper-paused-$key"
|
|
1063
|
+
for tick in 1 2 3; do
|
|
1064
|
+
PATH="$fakebin:$PATH" FM_FAKE_TMUX_WINDOW="$win" FM_FAKE_TMUX_CAPTURE="$pane" \
|
|
1065
|
+
FM_STATE_OVERRIDE="$state" FM_ESCALATE_BATCH_SECS=999999 FM_PAUSE_RESURFACE_SECS=3600 \
|
|
1066
|
+
housekeeping "$state"
|
|
1067
|
+
[ -e "$state/.subsuper-paused-$key" ] \
|
|
1068
|
+
|| fail "$case_name busy declared wait lost its marker on tick $tick inside the window"
|
|
1069
|
+
age=$(( $(date +%s) - $(cat "$state/.subsuper-paused-$key" 2>/dev/null || echo 0) ))
|
|
1070
|
+
[ "$age" -ge 100 ] \
|
|
1071
|
+
|| fail "$case_name tick $tick restarted the maturing window (age fell to ${age}s)"
|
|
1072
|
+
done
|
|
1073
|
+
[ ! -s "$state/.subsuper-escalations" ] \
|
|
1074
|
+
|| fail "$case_name busy declared wait escalated inside its PAUSE_RESURFACE_SECS window"
|
|
1075
|
+
|
|
1076
|
+
# Matured window: exactly one recheck, named for the right human, never a wedge,
|
|
1077
|
+
# and the window reset so the next one repeats rather than firing once.
|
|
1078
|
+
echo $(( $(date +%s) - 5000 )) > "$state/.subsuper-paused-$key"
|
|
1079
|
+
PATH="$fakebin:$PATH" FM_FAKE_TMUX_WINDOW="$win" FM_FAKE_TMUX_CAPTURE="$pane" \
|
|
1080
|
+
FM_STATE_OVERRIDE="$state" FM_ESCALATE_BATCH_SECS=999999 FM_PAUSE_RESURFACE_SECS=3600 \
|
|
1081
|
+
housekeeping "$state"
|
|
1082
|
+
escalations=0
|
|
1083
|
+
[ -s "$state/.subsuper-escalations" ] \
|
|
1084
|
+
&& escalations=$(wc -l < "$state/.subsuper-escalations" | tr -d ' ')
|
|
1085
|
+
[ "$escalations" = 1 ] \
|
|
1086
|
+
|| fail "$case_name busy declared wait produced $escalations escalations past its window, expected exactly one"
|
|
1087
|
+
grep -F "$digest" "$state/.subsuper-escalations" >/dev/null \
|
|
1088
|
+
|| fail "$case_name busy declared wait was not re-surfaced as a '$digest' recheck: $(cat "$state/.subsuper-escalations")"
|
|
1089
|
+
grep -F "possible wedge" "$state/.subsuper-escalations" >/dev/null \
|
|
1090
|
+
&& fail "$case_name busy declared wait was mislabeled a possible wedge"
|
|
1091
|
+
[ -e "$state/.subsuper-paused-$key" ] \
|
|
1092
|
+
|| fail "$case_name busy declared wait cleared its marker instead of resetting the window"
|
|
1093
|
+
age=$(( $(date +%s) - $(cat "$state/.subsuper-paused-$key" 2>/dev/null || echo 0) ))
|
|
1094
|
+
[ "$age" -lt 60 ] || fail "$case_name busy declared wait did not reset its window to now (age ${age}s)"
|
|
1095
|
+
|
|
1096
|
+
# The next tick, still inside the fresh window, stays silent: one recheck per window.
|
|
1097
|
+
PATH="$fakebin:$PATH" FM_FAKE_TMUX_WINDOW="$win" FM_FAKE_TMUX_CAPTURE="$pane" \
|
|
1098
|
+
FM_STATE_OVERRIDE="$state" FM_ESCALATE_BATCH_SECS=999999 FM_PAUSE_RESURFACE_SECS=3600 \
|
|
1099
|
+
housekeeping "$state"
|
|
1100
|
+
escalations=0
|
|
1101
|
+
[ -s "$state/.subsuper-escalations" ] \
|
|
1102
|
+
&& escalations=$(wc -l < "$state/.subsuper-escalations" | tr -d ' ')
|
|
1103
|
+
[ "$escalations" = 1 ] \
|
|
1104
|
+
|| fail "$case_name busy declared wait re-surfaced again inside its reset window ($escalations escalations)"
|
|
1105
|
+
done
|
|
1106
|
+
pass "housekeeping matures a busy pane's declared-wait window into exactly one recheck per window"
|
|
1107
|
+
}
|
|
1108
|
+
|
|
1109
|
+
# A pane still idle but whose status is no longer a pause (the crew changed state
|
|
1110
|
+
# without becoming busy) drops the marker - the signal path owns the new state, so
|
|
1111
|
+
# the pause recheck must not re-surface a stale pause reason.
|
|
1112
|
+
test_housekeeping_paused_unpaused_cleared() {
|
|
1113
|
+
local dir state fakebin win pane key
|
|
1114
|
+
dir=$(make_supercase paused-unpaused)
|
|
1115
|
+
state="$dir/state"; fakebin="$dir/fakebin"
|
|
1116
|
+
win="sess:fm-held-w13"; pane="$dir/pane.txt"
|
|
1117
|
+
printf 'paused: holding for the upstream release\nworking: resumed, upstream landed\n' > "$state/held-w13.status"
|
|
1118
|
+
printf 'idle prompt $\n' > "$pane"
|
|
1119
|
+
key=$(printf '%s' "held-w13" | tr ':/.' '___')
|
|
1120
|
+
echo $(( $(date +%s) - 5000 )) > "$state/.subsuper-paused-$key"
|
|
1121
|
+
PATH="$fakebin:$PATH" FM_FAKE_TMUX_WINDOW="$win" FM_FAKE_TMUX_CAPTURE="$pane" \
|
|
1122
|
+
FM_STATE_OVERRIDE="$state" FM_PAUSE_RESURFACE_SECS=240 housekeeping "$state"
|
|
1123
|
+
[ -e "$state/.subsuper-paused-$key" ] && fail "no-longer-paused marker was not cleared"
|
|
1124
|
+
[ ! -s "$state/.subsuper-escalations" ] || fail "a crew that left its pause was re-surfaced as a pause"
|
|
1125
|
+
pass "housekeeping clears a paused marker once the crew is no longer declaring the pause"
|
|
1126
|
+
}
|
|
1127
|
+
|
|
1128
|
+
# Once the captain answers, the hold is no longer a declared wait: the resolved line
|
|
1129
|
+
# takes over the last-line read, so the pause cadence must stop claiming the task
|
|
1130
|
+
# rather than keep re-surfacing a settled decision.
|
|
1131
|
+
test_housekeeping_captain_held_resolved_cleared() {
|
|
1132
|
+
local dir state fakebin win pane key
|
|
1133
|
+
dir=$(make_supercase captain-held-resolved)
|
|
1134
|
+
state="$dir/state"; fakebin="$dir/fakebin"
|
|
1135
|
+
win="sess:fm-held-w13h"; pane="$dir/pane.txt"
|
|
1136
|
+
printf 'captain-held [key=route]: tracked by task-decision-route\nresolved [key=route]: captain chose the direct path\n' > "$state/held-w13h.status"
|
|
1137
|
+
printf 'idle prompt $\n' > "$pane"
|
|
1138
|
+
key=$(printf '%s' "held-w13h" | tr ':/.' '___')
|
|
1139
|
+
echo $(( $(date +%s) - 5000 )) > "$state/.subsuper-paused-$key"
|
|
1140
|
+
PATH="$fakebin:$PATH" FM_FAKE_TMUX_WINDOW="$win" FM_FAKE_TMUX_CAPTURE="$pane" \
|
|
1141
|
+
FM_STATE_OVERRIDE="$state" FM_PAUSE_RESURFACE_SECS=240 housekeeping "$state"
|
|
1142
|
+
[ -e "$state/.subsuper-paused-$key" ] && fail "an answered captain hold kept its pause marker"
|
|
1143
|
+
[ ! -s "$state/.subsuper-escalations" ] || fail "an answered captain hold was re-surfaced as a declared wait"
|
|
1144
|
+
pass "housekeeping clears the pause marker once a captain hold is answered"
|
|
1145
|
+
}
|
|
1146
|
+
|
|
1147
|
+
test_housekeeping_stale_marker_transitions_to_pause() {
|
|
1148
|
+
local dir state fakebin win pane key
|
|
1149
|
+
dir=$(make_supercase stale-to-paused)
|
|
1150
|
+
state="$dir/state"; fakebin="$dir/fakebin"; win="sess:fm-held-w14"; pane="$dir/pane.txt"
|
|
1151
|
+
printf 'paused: awaiting the upstream tool release\n' > "$state/held-w14.status"
|
|
1152
|
+
printf 'idle prompt $\n' > "$pane"
|
|
1153
|
+
key=$(printf '%s' "held-w14" | tr ':/.' '___')
|
|
1154
|
+
echo $(( $(date +%s) - 5000 )) > "$state/.subsuper-stale-$key"
|
|
1155
|
+
PATH="$fakebin:$PATH" FM_FAKE_TMUX_WINDOW="$win" FM_FAKE_TMUX_CAPTURE="$pane" \
|
|
1156
|
+
FM_STATE_OVERRIDE="$state" FM_STALE_ESCALATE_SECS=240 housekeeping "$state"
|
|
1157
|
+
[ -e "$state/.subsuper-paused-$key" ] || fail "existing stale marker did not move to paused state"
|
|
1158
|
+
[ ! -e "$state/.subsuper-stale-$key" ] || fail "existing stale marker remained wedge-aged after pause"
|
|
1159
|
+
[ ! -s "$state/.subsuper-escalations" ] || fail "a newly declared pause was escalated as a possible wedge"
|
|
1160
|
+
pass "housekeeping moves an existing stale marker to pause before wedge escalation"
|
|
1161
|
+
}
|
|
1162
|
+
|
|
1163
|
+
# The quieting half for a captain hold. A finished task marked captain-held is idle by
|
|
1164
|
+
# design, so an already-aged wedge marker converts to pause tracking on the next sweep
|
|
1165
|
+
# instead of firing the possible-wedge escalation.
|
|
1166
|
+
test_housekeeping_captain_held_stale_marker_transitions_to_pause() {
|
|
1167
|
+
local dir state fakebin win pane key
|
|
1168
|
+
dir=$(make_supercase stale-to-captain-held)
|
|
1169
|
+
state="$dir/state"; fakebin="$dir/fakebin"; win="sess:fm-held-w14h"; pane="$dir/pane.txt"
|
|
1170
|
+
printf 'captain-held [key=route]: tracked by task-decision-route\n' > "$state/held-w14h.status"
|
|
1171
|
+
printf 'idle prompt $\n' > "$pane"
|
|
1172
|
+
key=$(printf '%s' "held-w14h" | tr ':/.' '___')
|
|
1173
|
+
echo $(( $(date +%s) - 5000 )) > "$state/.subsuper-stale-$key"
|
|
1174
|
+
PATH="$fakebin:$PATH" FM_FAKE_TMUX_WINDOW="$win" FM_FAKE_TMUX_CAPTURE="$pane" \
|
|
1175
|
+
FM_STATE_OVERRIDE="$state" FM_STALE_ESCALATE_SECS=240 housekeeping "$state"
|
|
1176
|
+
[ -e "$state/.subsuper-paused-$key" ] || fail "a captain hold did not move its stale marker to pause tracking"
|
|
1177
|
+
[ ! -e "$state/.subsuper-stale-$key" ] || fail "a captain hold remained wedge-aged"
|
|
1178
|
+
[ ! -s "$state/.subsuper-escalations" ] || fail "a captain hold was escalated as a possible wedge"
|
|
1179
|
+
pass "housekeeping moves a captain hold's existing stale marker to pause before wedge escalation"
|
|
1180
|
+
}
|
|
1181
|
+
|
|
1182
|
+
test_housekeeping_pause_marker_transitions_to_clear() {
|
|
1183
|
+
local dir state fakebin win pane key
|
|
1184
|
+
dir=$(make_supercase paused-to-stale)
|
|
1185
|
+
state="$dir/state"; fakebin="$dir/fakebin"; win="sess:fm-held-w15"; pane="$dir/pane.txt"
|
|
1186
|
+
printf 'working: upstream landed, resuming\n' > "$state/held-w15.status"
|
|
1187
|
+
printf 'idle prompt $\n' > "$pane"
|
|
1188
|
+
key=$(printf '%s' "held-w15" | tr ':/.' '___')
|
|
1189
|
+
date +%s > "$state/.subsuper-paused-$key"
|
|
1190
|
+
PATH="$fakebin:$PATH" FM_FAKE_TMUX_WINDOW="$win" FM_FAKE_TMUX_CAPTURE="$pane" \
|
|
1191
|
+
FM_STATE_OVERRIDE="$state" FM_PAUSE_RESURFACE_SECS=999999 housekeeping "$state"
|
|
1192
|
+
[ ! -e "$state/.subsuper-paused-$key" ] || fail "pause marker remained after the crew resumed"
|
|
1193
|
+
[ ! -e "$state/.subsuper-stale-$key" ] || fail "resume retained normal stale tracking"
|
|
1194
|
+
[ ! -s "$state/.subsuper-escalations" ] || fail "resuming from pause escalated immediately"
|
|
1195
|
+
pass "housekeeping clears tracking when a crew leaves pause"
|
|
1196
|
+
}
|
|
1197
|
+
|
|
1198
|
+
test_housekeeping_persistent_stale_escalates() {
|
|
1199
|
+
local dir state fakebin win pane key
|
|
1200
|
+
dir=$(make_supercase stale-persistent)
|
|
1201
|
+
state="$dir/state"
|
|
1202
|
+
fakebin="$dir/fakebin"
|
|
1203
|
+
win="sess:fm-pers-w5"
|
|
1204
|
+
pane="$dir/pane.txt"
|
|
1205
|
+
printf 'working\n' > "$state/pers-w5.status"
|
|
1206
|
+
printf 'idle prompt $\n' > "$pane"
|
|
1207
|
+
key=$(printf '%s' "pers-w5" | tr ':/.' '___')
|
|
1208
|
+
echo $(( $(date +%s) - 500 )) > "$state/.subsuper-stale-$key"
|
|
1209
|
+
PATH="$fakebin:$PATH" FM_FAKE_TMUX_WINDOW="$win" FM_FAKE_TMUX_CAPTURE="$pane" \
|
|
1210
|
+
FM_STATE_OVERRIDE="$state" FM_STALE_ESCALATE_SECS=240 housekeeping "$state"
|
|
1211
|
+
[ -s "$state/.subsuper-escalations" ] || fail "persistent stale was not escalated"
|
|
1212
|
+
[ ! -e "$state/.subsuper-stale-$key" ] || fail "stale marker not cleared after escalation"
|
|
1213
|
+
pass "persistent stale escalates after threshold and clears its marker"
|
|
1214
|
+
}
|
|
1215
|
+
|
|
1216
|
+
test_housekeeping_resumed_stale_cleared() {
|
|
1217
|
+
local dir state fakebin win pane key
|
|
1218
|
+
dir=$(make_supercase stale-resumed)
|
|
1219
|
+
state="$dir/state"
|
|
1220
|
+
fakebin="$dir/fakebin"
|
|
1221
|
+
win="sess:fm-res-w6"
|
|
1222
|
+
pane="$dir/pane.txt"
|
|
1223
|
+
printf 'working\n' > "$state/res-w6.status"
|
|
1224
|
+
printf 'Working...\n' > "$pane"
|
|
1225
|
+
# A resumed crew proves it is working through its own semantic busy-state
|
|
1226
|
+
# record (bin/fm-busy-lib.sh), not through the pane's rendered footer.
|
|
1227
|
+
fm_write_meta "$state/res-w6.meta" "window=$win" "worktree=$dir/wt" "kind=ship" "harness=pi"
|
|
1228
|
+
local gen; gen=$("$ROOT/bin/fm-busy-event.sh" arm "$state" res-w6)
|
|
1229
|
+
"$ROOT/bin/fm-busy-event.sh" apply "$state" res-w6 busy --gen "$gen" \
|
|
1230
|
+
--source pi-ext --event agent-start
|
|
1231
|
+
key=$(printf '%s' "res-w6" | tr ':/.' '___')
|
|
1232
|
+
echo $(( $(date +%s) - 500 )) > "$state/.subsuper-stale-$key"
|
|
1233
|
+
PATH="$fakebin:$PATH" FM_FAKE_TMUX_WINDOW="$win" FM_FAKE_TMUX_CAPTURE="$pane" \
|
|
1234
|
+
FM_STATE_OVERRIDE="$state" FM_STALE_ESCALATE_SECS=240 housekeeping "$state"
|
|
1235
|
+
[ -e "$state/.subsuper-stale-$key" ] && fail "resumed stale marker was not cleared"
|
|
1236
|
+
[ -s "$state/.subsuper-escalations" ] && fail "resumed stale was escalated"
|
|
1237
|
+
pass "resumed (busy) stale clears its marker without escalating"
|
|
1238
|
+
}
|
|
1239
|
+
|
|
1240
|
+
test_housekeeping_herdr_persistent_stale_resolves_meta() {
|
|
1241
|
+
local dir state key
|
|
1242
|
+
dir=$(make_supercase stale-herdr-persistent)
|
|
1243
|
+
state="$dir/state"
|
|
1244
|
+
fm_write_meta "$state/herdr-w7.meta" "window=default:w1:p2" "backend=herdr"
|
|
1245
|
+
printf 'working\n' > "$state/herdr-w7.status"
|
|
1246
|
+
key=$(printf '%s' "herdr-w7" | tr ':/.' '___')
|
|
1247
|
+
echo $(( $(date +%s) - 500 )) > "$state/.subsuper-stale-$key"
|
|
1248
|
+
(
|
|
1249
|
+
fm_backend_capture() {
|
|
1250
|
+
[ "$1" = herdr ] || fail "expected herdr capture backend, got $1"
|
|
1251
|
+
[ "$2" = "default:w1:p2" ] || fail "expected herdr window target, got $2"
|
|
1252
|
+
printf 'idle prompt\n'
|
|
1253
|
+
}
|
|
1254
|
+
fm_backend_busy_state() {
|
|
1255
|
+
[ "$1" = herdr ] || fail "expected herdr busy backend, got $1"
|
|
1256
|
+
[ "$2" = "default:w1:p2" ] || fail "expected herdr busy target, got $2"
|
|
1257
|
+
printf 'idle'
|
|
1258
|
+
}
|
|
1259
|
+
fm_backend_capture herdr default:w1:p2 40 >/dev/null
|
|
1260
|
+
[ "$(fm_backend_busy_state herdr default:w1:p2)" = idle ] || fail "herdr busy stub did not report idle"
|
|
1261
|
+
FM_STATE_OVERRIDE="$state" FM_STALE_ESCALATE_SECS=240 housekeeping "$state"
|
|
1262
|
+
) || fail "herdr persistent stale housekeeping failed"
|
|
1263
|
+
[ -s "$state/.subsuper-escalations" ] || fail "persistent herdr stale was not escalated"
|
|
1264
|
+
[ ! -e "$state/.subsuper-stale-$key" ] || fail "herdr stale marker not cleared after escalation"
|
|
1265
|
+
pass "persistent herdr stale resolves the target from metadata and escalates"
|
|
1266
|
+
}
|
|
1267
|
+
|
|
1268
|
+
# A herdr crew whose native agent.get reads idle (generation state) but whose
|
|
1269
|
+
# own semantic busy-state record says busy is still working, so its stale
|
|
1270
|
+
# marker clears without escalating. The record - not the pane's rendered
|
|
1271
|
+
# footer - is what proves it.
|
|
1272
|
+
test_housekeeping_herdr_idle_busy_record_clears_stale() {
|
|
1273
|
+
local dir state key gen
|
|
1274
|
+
dir=$(make_supercase stale-herdr-idle-busy-record)
|
|
1275
|
+
state="$dir/state"
|
|
1276
|
+
fm_write_meta "$state/herdr-footer.meta" "window=default:w1:p4" "backend=herdr" "harness=claude"
|
|
1277
|
+
printf 'working\n' > "$state/herdr-footer.status"
|
|
1278
|
+
gen=$("$ROOT/bin/fm-busy-event.sh" arm "$state" herdr-footer)
|
|
1279
|
+
"$ROOT/bin/fm-busy-event.sh" apply "$state" herdr-footer busy --gen "$gen" \
|
|
1280
|
+
--source claude-hook --event user-prompt-submit
|
|
1281
|
+
key=$(printf '%s' "herdr-footer" | tr ':/.' '___')
|
|
1282
|
+
echo $(( $(date +%s) - 500 )) > "$state/.subsuper-stale-$key"
|
|
1283
|
+
(
|
|
1284
|
+
fm_backend_capture() {
|
|
1285
|
+
[ "$1" = herdr ] || fail "expected herdr capture backend, got $1"
|
|
1286
|
+
[ "$2" = "default:w1:p4" ] || fail "expected herdr window target, got $2"
|
|
1287
|
+
printf 'quiet\n'
|
|
1288
|
+
}
|
|
1289
|
+
fm_backend_busy_state() {
|
|
1290
|
+
[ "$1" = herdr ] || fail "expected herdr busy backend, got $1"
|
|
1291
|
+
[ "$2" = "default:w1:p4" ] || fail "expected herdr busy target, got $2"
|
|
1292
|
+
printf 'idle'
|
|
1293
|
+
}
|
|
1294
|
+
fm_backend_capture herdr default:w1:p4 40 >/dev/null
|
|
1295
|
+
[ "$(fm_backend_busy_state herdr default:w1:p4)" = idle ] || fail "herdr busy stub did not report idle"
|
|
1296
|
+
FM_STATE_OVERRIDE="$state" FM_STALE_ESCALATE_SECS=240 housekeeping "$state"
|
|
1297
|
+
) || fail "herdr idle busy-footer housekeeping failed"
|
|
1298
|
+
[ ! -e "$state/.subsuper-stale-$key" ] || fail "idle-native busy-record herdr stale marker was not cleared"
|
|
1299
|
+
[ ! -s "$state/.subsuper-escalations" ] || fail "idle-native busy-record herdr stale was escalated"
|
|
1300
|
+
pass "herdr idle busy-footer stale clears through capture corroboration"
|
|
1301
|
+
}
|
|
1302
|
+
|
|
1303
|
+
test_housekeeping_herdr_resumed_stale_cleared() {
|
|
1304
|
+
local dir state key
|
|
1305
|
+
dir=$(make_supercase stale-herdr-resumed)
|
|
1306
|
+
state="$dir/state"
|
|
1307
|
+
fm_write_meta "$state/herdr-busy.meta" "window=default:w1:p3" "backend=herdr"
|
|
1308
|
+
printf 'working\n' > "$state/herdr-busy.status"
|
|
1309
|
+
key=$(printf '%s' "herdr-busy" | tr ':/.' '___')
|
|
1310
|
+
echo $(( $(date +%s) - 500 )) > "$state/.subsuper-stale-$key"
|
|
1311
|
+
(
|
|
1312
|
+
fm_backend_capture() {
|
|
1313
|
+
[ "$1" = herdr ] || fail "expected herdr capture backend, got $1"
|
|
1314
|
+
[ "$2" = "default:w1:p3" ] || fail "expected herdr window target, got $2"
|
|
1315
|
+
printf 'unchanged pane\n'
|
|
1316
|
+
}
|
|
1317
|
+
fm_backend_busy_state() {
|
|
1318
|
+
[ "$1" = herdr ] || fail "expected herdr busy backend, got $1"
|
|
1319
|
+
[ "$2" = "default:w1:p3" ] || fail "expected herdr busy target, got $2"
|
|
1320
|
+
printf 'busy'
|
|
1321
|
+
}
|
|
1322
|
+
fm_backend_capture herdr default:w1:p3 40 >/dev/null
|
|
1323
|
+
[ "$(fm_backend_busy_state herdr default:w1:p3)" = busy ] || fail "herdr busy stub did not report busy"
|
|
1324
|
+
FM_STATE_OVERRIDE="$state" FM_STALE_ESCALATE_SECS=240 housekeeping "$state"
|
|
1325
|
+
) || fail "herdr resumed stale housekeeping failed"
|
|
1326
|
+
[ ! -e "$state/.subsuper-stale-$key" ] || fail "busy herdr stale marker was not cleared"
|
|
1327
|
+
[ ! -s "$state/.subsuper-escalations" ] || fail "busy herdr stale was escalated"
|
|
1328
|
+
pass "resumed herdr stale clears through backend-aware busy state"
|
|
1329
|
+
}
|
|
1330
|
+
|
|
1331
|
+
test_housekeeping_orca_persistent_stale_resolves_terminal() {
|
|
1332
|
+
local dir state key
|
|
1333
|
+
dir=$(make_supercase stale-orca-persistent)
|
|
1334
|
+
state="$dir/state"
|
|
1335
|
+
fm_write_meta "$state/orca-w8.meta" "window=fm-orca-w8" "terminal=term-orca-w8" "backend=orca"
|
|
1336
|
+
printf 'working\n' > "$state/orca-w8.status"
|
|
1337
|
+
key=$(printf '%s' "orca-w8" | tr ':/.' '___')
|
|
1338
|
+
echo $(( $(date +%s) - 500 )) > "$state/.subsuper-stale-$key"
|
|
1339
|
+
(
|
|
1340
|
+
fm_backend_capture() {
|
|
1341
|
+
[ "$1" = orca ] || fail "expected orca capture backend, got $1"
|
|
1342
|
+
[ "$2" = "term-orca-w8" ] || fail "expected Orca terminal target, got $2"
|
|
1343
|
+
printf 'idle prompt\n'
|
|
1344
|
+
}
|
|
1345
|
+
fm_backend_busy_state() {
|
|
1346
|
+
[ "$1" = orca ] || fail "expected orca busy backend, got $1"
|
|
1347
|
+
[ "$2" = "term-orca-w8" ] || fail "expected Orca busy target, got $2"
|
|
1348
|
+
printf 'idle'
|
|
1349
|
+
}
|
|
1350
|
+
fm_backend_capture orca term-orca-w8 40 >/dev/null
|
|
1351
|
+
[ "$(fm_backend_busy_state orca term-orca-w8)" = idle ] || fail "Orca busy stub did not report idle"
|
|
1352
|
+
FM_STATE_OVERRIDE="$state" FM_STALE_ESCALATE_SECS=240 housekeeping "$state"
|
|
1353
|
+
) || fail "Orca persistent stale housekeeping failed"
|
|
1354
|
+
[ -s "$state/.subsuper-escalations" ] || fail "persistent Orca stale was not escalated"
|
|
1355
|
+
[ ! -e "$state/.subsuper-stale-$key" ] || fail "Orca stale marker not cleared after escalation"
|
|
1356
|
+
pass "persistent Orca stale resolves the terminal from metadata"
|
|
1357
|
+
}
|
|
1358
|
+
|
|
1359
|
+
test_escalate_batches_into_one_digest() {
|
|
1360
|
+
local dir state fakebin sent capture n
|
|
1361
|
+
dir=$(make_supercase batch)
|
|
1362
|
+
state="$dir/state"
|
|
1363
|
+
fakebin="$dir/fakebin"
|
|
1364
|
+
sent="$dir/sent.log"; : > "$sent"
|
|
1365
|
+
capture="$dir/pane.txt"; printf '\342\235\257 \n' > "$capture" # a proven-empty bare claude composer: STRICT injection needs positive proof
|
|
1366
|
+
escalate_add "$state" "event A: done: PR 1"
|
|
1367
|
+
escalate_add "$state" "event B: done: PR 2"
|
|
1368
|
+
afk_enter "$state"
|
|
1369
|
+
PATH="$fakebin:$PATH" FM_FAKE_TMUX_PANE_ALIVE=1 FM_FAKE_TMUX_SENT="$sent" \
|
|
1370
|
+
FM_FAKE_TMUX_CAPTURE="$capture" FM_ESCALATE_BATCH_SECS=0 escalate_flush "$state" \
|
|
1371
|
+
|| fail "escalate_flush failed"
|
|
1372
|
+
grep -F 'FIRSTMATE_OP: v1 away-supervisor: ' "$sent" >/dev/null \
|
|
1373
|
+
|| fail "batch digest lacks the exact current away-supervisor kind"
|
|
1374
|
+
grep -F "event A" "$sent" >/dev/null || fail "batch digest missing event A"
|
|
1375
|
+
grep -F "event B" "$sent" >/dev/null || fail "batch digest missing event B"
|
|
1376
|
+
grep -F 'event A: done: PR 1 | event B: done: PR 2' "$sent" >/dev/null \
|
|
1377
|
+
|| fail "batch digest did not join events with literal ' | '"
|
|
1378
|
+
[ -s "$state/.subsuper-escalations" ] && fail "escalation buffer not cleared after flush"
|
|
1379
|
+
[ -e "$state/.subsuper-escalations.since" ] && fail "first-append sidecar not cleared after flush"
|
|
1380
|
+
n=$(grep -c '\[ENTER\]' "$sent")
|
|
1381
|
+
[ "$n" -eq 1 ] || fail "expected one injected digest, got $n send-keys submits"
|
|
1382
|
+
pass "multiple escalations flush as a single batched digest"
|
|
1383
|
+
}
|
|
1384
|
+
|
|
1385
|
+
test_escalate_batch_age_uses_first_append() {
|
|
1386
|
+
local dir state fakebin sent capture
|
|
1387
|
+
dir=$(make_supercase batch-age)
|
|
1388
|
+
state="$dir/state"
|
|
1389
|
+
fakebin="$dir/fakebin"
|
|
1390
|
+
sent="$dir/sent.log"; : > "$sent"
|
|
1391
|
+
capture="$dir/pane.txt"; printf '\342\235\257 \n' > "$capture" # a proven-empty bare claude composer: STRICT injection needs positive proof
|
|
1392
|
+
escalate_add "$state" "event A: done: PR 1"
|
|
1393
|
+
escalate_add "$state" "event B: done: PR 2"
|
|
1394
|
+
echo $(( $(date +%s) - 100 )) > "$state/.subsuper-escalations.since"
|
|
1395
|
+
afk_enter "$state"
|
|
1396
|
+
PATH="$fakebin:$PATH" FM_FAKE_TMUX_PANE_ALIVE=1 FM_FAKE_TMUX_SENT="$sent" \
|
|
1397
|
+
FM_FAKE_TMUX_CAPTURE="$capture" FM_ESCALATE_BATCH_SECS=90 FM_HOUSEKEEPING_TICK=0 \
|
|
1398
|
+
housekeeping "$state"
|
|
1399
|
+
grep -F 'event A: done: PR 1 | event B: done: PR 2' "$sent" >/dev/null \
|
|
1400
|
+
|| fail "backdated batch did not flush as a joined digest (max-delay measured from last append)"
|
|
1401
|
+
[ -s "$state/.subsuper-escalations" ] && fail "escalation buffer not cleared after backdated flush"
|
|
1402
|
+
[ -e "$state/.subsuper-escalations.since" ] && fail "first-append sidecar not cleared after flush"
|
|
1403
|
+
pass "batch flush measures max-delay from the first append, not the last"
|
|
1404
|
+
}
|
|
1405
|
+
|
|
1406
|
+
test_heartbeat_scan_dedup() {
|
|
1407
|
+
local dir state
|
|
1408
|
+
dir=$(make_supercase scan-dedup)
|
|
1409
|
+
state="$dir/state"
|
|
1410
|
+
printf 'done: ready\n' > "$state/dup-t6.status"
|
|
1411
|
+
rm -f "$state/.subsuper-last-scan"
|
|
1412
|
+
FM_STATE_OVERRIDE="$state" housekeeping "$state"
|
|
1413
|
+
[ -s "$state/.subsuper-escalations" ] || fail "catch-all scan did not escalate a terminal"
|
|
1414
|
+
: > "$state/.subsuper-escalations"
|
|
1415
|
+
echo $(( $(date +%s) - 99999 )) > "$state/.subsuper-last-scan"
|
|
1416
|
+
FM_STATE_OVERRIDE="$state" housekeeping "$state"
|
|
1417
|
+
[ -s "$state/.subsuper-escalations" ] && fail "catch-all scan re-escalated the same terminal (dedup failed)"
|
|
1418
|
+
pass "catch-all scan escalates a missed terminal once, not twice"
|
|
1419
|
+
}
|
|
1420
|
+
|
|
1421
|
+
test_handle_wake_routes_self_and_escalate() {
|
|
1422
|
+
local dir state
|
|
1423
|
+
dir=$(make_supercase handle)
|
|
1424
|
+
state="$dir/state"
|
|
1425
|
+
printf 'working\n' > "$state/h-routine.status"
|
|
1426
|
+
FM_STATE_OVERRIDE="$state" handle_wake "signal: $state/h-routine.status" "$state"
|
|
1427
|
+
[ -s "$state/.subsuper-escalations" ] && fail "routine signal was escalated by handle_wake"
|
|
1428
|
+
printf 'done: PR 1\n' > "$state/h-done.status"
|
|
1429
|
+
FM_STATE_OVERRIDE="$state" handle_wake "signal: $state/h-done.status" "$state"
|
|
1430
|
+
[ -s "$state/.subsuper-escalations" ] || fail "captain signal was not buffered by handle_wake"
|
|
1431
|
+
pass "handle_wake routes routine->self and captain->escalate"
|
|
1432
|
+
}
|
|
1433
|
+
|
|
1434
|
+
test_inject_skip_forces_self() {
|
|
1435
|
+
local dir state
|
|
1436
|
+
dir=$(make_supercase skip)
|
|
1437
|
+
state="$dir/state"
|
|
1438
|
+
printf 'done: PR 1\n' > "$state/s1.status"
|
|
1439
|
+
FM_STATE_OVERRIDE="$state" FM_INJECT_SKIP="signal" handle_wake "signal: $state/s1.status" "$state"
|
|
1440
|
+
[ -s "$state/.subsuper-escalations" ] && fail "INJECT_SKIP=signal did not force self-handle"
|
|
1441
|
+
pass "INJECT_SKIP forces self-handle, bypassing captain-relevant classification"
|
|
1442
|
+
}
|
|
1443
|
+
|
|
1444
|
+
test_is_wake_reason_distinguishes_status_stdout() {
|
|
1445
|
+
# Real wake reasons are recognized; watcher status lines (singleton collision)
|
|
1446
|
+
# are not, so the main loop can idle them without flooding escalations.
|
|
1447
|
+
is_wake_reason "signal: /x/y.status" || fail "signal: not recognized as wake"
|
|
1448
|
+
is_wake_reason "stale: s:fm-x" || fail "stale: not recognized as wake"
|
|
1449
|
+
is_wake_reason "check: /s/c.sh: merged" || fail "check: not recognized as wake"
|
|
1450
|
+
is_wake_reason "heartbeat" || fail "heartbeat not recognized as wake"
|
|
1451
|
+
is_wake_reason "watcher: already running" && fail "singleton status line misclassified as wake"
|
|
1452
|
+
is_wake_reason "watcher: already running pid 123" && fail "singleton status (pid) misclassified as wake"
|
|
1453
|
+
pass "is_wake_reason distinguishes watcher wake reasons from singleton-status stdout"
|
|
1454
|
+
}
|
|
1455
|
+
|
|
1456
|
+
test_terminal_stale_escalate_leaves_no_marker() {
|
|
1457
|
+
local dir state win key
|
|
1458
|
+
dir=$(make_supercase stale-terminal-nomarker)
|
|
1459
|
+
state="$dir/state"
|
|
1460
|
+
win="sess:fm-fin-n7"
|
|
1461
|
+
printf 'done: PR https://x/y/pull/7\n' > "$state/fin-n7.status"
|
|
1462
|
+
key=$(printf '%s' "fin-n7" | tr ':/.' '___')
|
|
1463
|
+
echo $(( $(date +%s) - 500 )) > "$state/.subsuper-stale-$key"
|
|
1464
|
+
FM_STATE_OVERRIDE="$state" handle_wake "stale: $win" "$state"
|
|
1465
|
+
[ -s "$state/.subsuper-escalations" ] || fail "terminal stale was not escalated"
|
|
1466
|
+
[ ! -e "$state/.subsuper-stale-$key" ] || fail "terminal stale left a persistence marker (housekeeping would re-escalate)"
|
|
1467
|
+
: > "$state/.subsuper-escalations"
|
|
1468
|
+
rm -f "$state/.subsuper-last-scan"
|
|
1469
|
+
FM_STATE_OVERRIDE="$state" FM_STALE_ESCALATE_SECS=240 housekeeping "$state"
|
|
1470
|
+
[ ! -s "$state/.subsuper-escalations" ] || fail "housekeeping re-escalated a terminal stale as a wedge"
|
|
1471
|
+
pass "terminal-stale escalate removes its marker so housekeeping does not re-escalate"
|
|
1472
|
+
}
|
|
1473
|
+
|
|
1474
|
+
test_signal_escalate_marks_seen_no_catchall_refire() {
|
|
1475
|
+
local dir state key
|
|
1476
|
+
dir=$(make_supercase signal-seen)
|
|
1477
|
+
state="$dir/state"
|
|
1478
|
+
printf 'done: PR https://x/y/pull/8\n' > "$state/sig-t8.status"
|
|
1479
|
+
FM_STATE_OVERRIDE="$state" handle_wake "signal: $state/sig-t8.status" "$state"
|
|
1480
|
+
[ -s "$state/.subsuper-escalations" ] || fail "captain signal was not escalated"
|
|
1481
|
+
key=$(printf '%s' "sig-t8" | tr ':/.' '___')
|
|
1482
|
+
[ "$(status_seen_offset "$state" sig-t8)" = "$(log_size "$state/sig-t8.status")" ] \
|
|
1483
|
+
|| fail "captain signal escalate did not record the escalated-through offset"
|
|
1484
|
+
: > "$state/.subsuper-escalations"
|
|
1485
|
+
rm -f "$state/.subsuper-last-scan"
|
|
1486
|
+
FM_STATE_OVERRIDE="$state" housekeeping "$state"
|
|
1487
|
+
[ ! -s "$state/.subsuper-escalations" ] || fail "catch-all scan re-fired an already-escalated signal"
|
|
1488
|
+
pass "captain signal escalate marks seen so the catch-all scan does not re-fire"
|
|
1489
|
+
}
|
|
1490
|
+
|
|
1491
|
+
test_collapse_newlines_pure() {
|
|
1492
|
+
local out
|
|
1493
|
+
out=$(_collapse_newlines $'line one\nline two\nline three')
|
|
1494
|
+
[ "$out" = "line one - line two - line three" ] || fail "collapse failed: '$out'"
|
|
1495
|
+
out=$(_collapse_newlines "no newlines here")
|
|
1496
|
+
[ "$out" = "no newlines here" ] || fail "collapse changed no-newline text"
|
|
1497
|
+
out=$(_collapse_newlines $'a\nb')
|
|
1498
|
+
[ "$out" = "a - b" ] || fail "collapse two lines failed: '$out'"
|
|
1499
|
+
pass "_collapse_newlines replaces newlines with literal separator"
|
|
1500
|
+
}
|
|
1501
|
+
|
|
1502
|
+
test_afk_absent_daemon_does_not_inject() {
|
|
1503
|
+
local dir state fakebin sent capture
|
|
1504
|
+
dir=$(make_supercase afk-off)
|
|
1505
|
+
state="$dir/state"
|
|
1506
|
+
fakebin="$dir/fakebin"
|
|
1507
|
+
sent="$dir/sent.log"; : > "$sent"
|
|
1508
|
+
capture="$dir/pane.txt"; printf '\342\235\257 \n' > "$capture" # a proven-empty bare claude composer: STRICT injection needs positive proof
|
|
1509
|
+
escalate_add "$state" "done: PR 1"
|
|
1510
|
+
# afk flag deliberately NOT set
|
|
1511
|
+
if PATH="$fakebin:$PATH" FM_FAKE_TMUX_PANE_ALIVE=1 FM_FAKE_TMUX_SENT="$sent" \
|
|
1512
|
+
FM_FAKE_TMUX_CAPTURE="$capture" FM_ESCALATE_BATCH_SECS=0 escalate_flush "$state"; then
|
|
1513
|
+
fail "escalate_flush succeeded while afk inactive"
|
|
1514
|
+
fi
|
|
1515
|
+
[ -s "$sent" ] && fail "daemon injected while afk inactive"
|
|
1516
|
+
[ -s "$state/.subsuper-escalations" ] || fail "buffer not preserved when afk inactive"
|
|
1517
|
+
pass "afk flag absent: daemon does not inject, buffer preserved"
|
|
1518
|
+
}
|
|
1519
|
+
|
|
1520
|
+
test_busy_guard_defers_when_supervisor_busy() {
|
|
1521
|
+
local dir state fakebin sent capture
|
|
1522
|
+
dir=$(make_supercase busy-guard)
|
|
1523
|
+
state="$dir/state"
|
|
1524
|
+
fakebin="$dir/fakebin"
|
|
1525
|
+
sent="$dir/sent.log"; : > "$sent"
|
|
1526
|
+
capture="$dir/pane.txt"
|
|
1527
|
+
printf 'esc to interrupt\n' > "$capture"
|
|
1528
|
+
escalate_add "$state" "done: PR 1"
|
|
1529
|
+
afk_enter "$state"
|
|
1530
|
+
if PATH="$fakebin:$PATH" FM_FAKE_TMUX_PANE_ALIVE=1 FM_FAKE_TMUX_SENT="$sent" \
|
|
1531
|
+
FM_FAKE_TMUX_CAPTURE="$capture" FM_ESCALATE_BATCH_SECS=0 escalate_flush "$state"; then
|
|
1532
|
+
fail "escalate_flush should defer when supervisor pane busy"
|
|
1533
|
+
fi
|
|
1534
|
+
[ -s "$sent" ] && fail "daemon injected into a busy pane"
|
|
1535
|
+
[ -s "$state/.subsuper-escalations" ] || fail "buffer not preserved when deferred"
|
|
1536
|
+
pass "busy-guard defers injection when supervisor pane is busy"
|
|
1537
|
+
}
|
|
1538
|
+
|
|
1539
|
+
test_marker_detection() {
|
|
1540
|
+
local marker_hex
|
|
1541
|
+
marker_hex=$(printf '%s' "$FM_INJECT_MARK" | od -An -tx1 | tr -d ' \n')
|
|
1542
|
+
[ "$marker_hex" = e281a3 ] \
|
|
1543
|
+
|| fail "FM_INJECT_MARK must use terminal-safe U+2063 bytes, got $marker_hex"
|
|
1544
|
+
[ "$FM_OPERATIONAL_PREFIX" = "${FM_INJECT_MARK}FIRSTMATE_OP: " ] \
|
|
1545
|
+
|| fail "away-mode operational prefix drifted from the shared captain-boundary marker"
|
|
1546
|
+
# message_is_injection: marker present -> injection; absent -> real message
|
|
1547
|
+
message_is_injection "${FM_OPERATIONAL_PREFIX}Supervisor escalate: done" \
|
|
1548
|
+
|| fail "operationally-prefixed message not detected as injection"
|
|
1549
|
+
message_is_injection "${FM_INJECT_MARK}Supervisor escalate: done" \
|
|
1550
|
+
|| fail "legacy marker-prefixed message not detected as injection"
|
|
1551
|
+
message_is_injection "how's it going?" \
|
|
1552
|
+
&& fail "plain message misdetected as injection"
|
|
1553
|
+
message_is_injection "" && fail "empty message misdetected as injection"
|
|
1554
|
+
# should_exit_afk: the full afk-exit contract
|
|
1555
|
+
local dir state
|
|
1556
|
+
dir=$(make_supercase marker-detect)
|
|
1557
|
+
state="$dir/state"
|
|
1558
|
+
afk_enter "$state"
|
|
1559
|
+
should_exit_afk "$state" "${FM_INJECT_MARK}escalate" \
|
|
1560
|
+
&& fail "marker message should not exit afk (internal escalation)"
|
|
1561
|
+
should_exit_afk "$state" "status update please" \
|
|
1562
|
+
|| fail "plain message should exit afk (captain is back)"
|
|
1563
|
+
pass "marker detection: marker -> stay afk, no marker -> exit afk"
|
|
1564
|
+
}
|
|
1565
|
+
|
|
1566
|
+
test_afk_turn_exemption() {
|
|
1567
|
+
local dir state
|
|
1568
|
+
dir=$(make_supercase afk-exempt)
|
|
1569
|
+
state="$dir/state"
|
|
1570
|
+
afk_enter "$state"
|
|
1571
|
+
# /afk while already away must NOT self-cancel (re-entering/extending)
|
|
1572
|
+
should_exit_afk "$state" "/afk" \
|
|
1573
|
+
&& fail "bare /afk should not exit afk"
|
|
1574
|
+
should_exit_afk "$state" "/afk back in an hour" \
|
|
1575
|
+
&& fail "/afk with args should not exit afk"
|
|
1576
|
+
# a non-/afk skill invocation DOES exit (the captain is actively working)
|
|
1577
|
+
should_exit_afk "$state" "/no-mistakes" \
|
|
1578
|
+
|| fail "non-afk skill should exit afk"
|
|
1579
|
+
pass "/afk invocation is exempt from afk exit (no self-cancel)"
|
|
1580
|
+
}
|
|
1581
|
+
|
|
1582
|
+
test_should_exit_afk_when_afk_inactive() {
|
|
1583
|
+
local dir state
|
|
1584
|
+
dir=$(make_supercase no-afk)
|
|
1585
|
+
state="$dir/state"
|
|
1586
|
+
# afk flag absent: should never signal exit (nothing to exit)
|
|
1587
|
+
should_exit_afk "$state" "hello" \
|
|
1588
|
+
&& fail "should_exit_afk true when afk inactive"
|
|
1589
|
+
should_exit_afk "$state" "${FM_INJECT_MARK}test" \
|
|
1590
|
+
&& fail "should_exit_afk true when afk inactive (marker)"
|
|
1591
|
+
pass "should_exit_afk returns false when afk is not active"
|
|
1592
|
+
}
|
|
1593
|
+
|
|
1594
|
+
test_strip_injection_marker() {
|
|
1595
|
+
local encoded stripped
|
|
1596
|
+
fm_operational_input_encode away-supervisor "Supervisor escalate: done" encoded \
|
|
1597
|
+
|| fail "could not encode current away fixture"
|
|
1598
|
+
stripped=$(strip_injection_marker "$encoded")
|
|
1599
|
+
[ "$stripped" = "Supervisor escalate: done" ] \
|
|
1600
|
+
|| fail "current typed operational envelope not stripped: '$stripped'"
|
|
1601
|
+
stripped=$(strip_injection_marker "${FM_OPERATIONAL_PREFIX}Supervisor escalate: done")
|
|
1602
|
+
[ "$stripped" = "Supervisor escalate: done" ] \
|
|
1603
|
+
|| fail "landed untyped operational prefix not stripped: '$stripped'"
|
|
1604
|
+
stripped=$(strip_injection_marker "${FM_INJECT_MARK}Supervisor escalate: done")
|
|
1605
|
+
[ "$stripped" = "Supervisor escalate: done" ] \
|
|
1606
|
+
|| fail "legacy marker not stripped: '$stripped'"
|
|
1607
|
+
# No marker → unchanged.
|
|
1608
|
+
stripped=$(strip_injection_marker "no marker here")
|
|
1609
|
+
[ "$stripped" = "no marker here" ] \
|
|
1610
|
+
|| fail "non-marker text changed: '$stripped'"
|
|
1611
|
+
# Empty → empty.
|
|
1612
|
+
stripped=$(strip_injection_marker "")
|
|
1613
|
+
[ "$stripped" = "" ] || fail "empty text changed: '$stripped'"
|
|
1614
|
+
# Only marker → empty.
|
|
1615
|
+
stripped=$(strip_injection_marker "$FM_INJECT_MARK")
|
|
1616
|
+
[ "$stripped" = "" ] || fail "bare marker not stripped: '$stripped'"
|
|
1617
|
+
pass "strip_injection_marker removes the sentinel marker cleanly"
|
|
1618
|
+
}
|
|
1619
|
+
|
|
1620
|
+
test_pane_input_pending_detects_partial_input() {
|
|
1621
|
+
local dir state fakebin capture
|
|
1622
|
+
dir=$(make_supercase pending-input)
|
|
1623
|
+
state="$dir/state"
|
|
1624
|
+
fakebin="$dir/fakebin"
|
|
1625
|
+
capture="$dir/pane.txt"
|
|
1626
|
+
# Line 3 (cursor_y=2) has human's partial text (no Enter) → pending.
|
|
1627
|
+
printf 'line one\nline two\nhuman draft text\n' > "$capture"
|
|
1628
|
+
PATH="$fakebin:$PATH" FM_FAKE_TMUX_CAPTURE="$capture" FM_FAKE_TMUX_CURSOR_Y=2 \
|
|
1629
|
+
pane_input_pending "fakepane" \
|
|
1630
|
+
|| fail "pane_input_pending should detect non-empty composer (human text)"
|
|
1631
|
+
pass "pane_input_pending detects partial input on the cursor line"
|
|
1632
|
+
}
|
|
1633
|
+
|
|
1634
|
+
test_pane_input_pending_blank_defers_strict() {
|
|
1635
|
+
# THE STRICT BLANK-ROW RULE (captain decision blank-row-injection-posture,
|
|
1636
|
+
# 2026-08-09): a blank cursor row with no positive container proof is
|
|
1637
|
+
# `unknown` and the injector DEFERS. The permissive rule this replaced read
|
|
1638
|
+
# the same row as `empty` and injected - into whatever the blank row really
|
|
1639
|
+
# was (a modal dialog, a dead shell between stale transcript rules, a
|
|
1640
|
+
# mid-redraw pane). This assertion IS the posture divergence: if it ever
|
|
1641
|
+
# reads not-pending again, the permissive rule has silently returned.
|
|
1642
|
+
local dir state fakebin capture
|
|
1643
|
+
dir=$(make_supercase pending-blank)
|
|
1644
|
+
state="$dir/state"
|
|
1645
|
+
fakebin="$dir/fakebin"
|
|
1646
|
+
capture="$dir/pane.txt"
|
|
1647
|
+
printf 'some output\nmore output\n\n' > "$capture"
|
|
1648
|
+
PATH="$fakebin:$PATH" FM_FAKE_TMUX_CAPTURE="$capture" FM_FAKE_TMUX_CURSOR_Y=2 \
|
|
1649
|
+
pane_input_pending "fakepane" \
|
|
1650
|
+
|| fail "a blank unidentified cursor row must defer under the strict rule, not read empty"
|
|
1651
|
+
pass "pane_input_pending: a blank unidentified cursor row defers (strict container-proof rule)"
|
|
1652
|
+
}
|
|
1653
|
+
|
|
1654
|
+
test_pane_input_pending_requires_proven_empty_prompt() {
|
|
1655
|
+
local dir state fakebin capture prompt
|
|
1656
|
+
dir=$(make_supercase pending-prompt)
|
|
1657
|
+
state="$dir/state"
|
|
1658
|
+
fakebin="$dir/fakebin"
|
|
1659
|
+
capture="$dir/pane.txt"
|
|
1660
|
+
for prompt in '$' '>'; do
|
|
1661
|
+
printf 'output\noutput\n%s \n' "$prompt" > "$capture"
|
|
1662
|
+
PATH="$fakebin:$PATH" FM_FAKE_TMUX_CAPTURE="$capture" FM_FAKE_TMUX_CURSOR_Y=2 \
|
|
1663
|
+
pane_input_pending "fakepane" \
|
|
1664
|
+
|| fail "bare shell prompt '$prompt' should defer as unknown"
|
|
1665
|
+
done
|
|
1666
|
+
for prompt in '❯' '›'; do
|
|
1667
|
+
printf 'output\noutput\n%s \n' "$prompt" > "$capture"
|
|
1668
|
+
if PATH="$fakebin:$PATH" FM_FAKE_TMUX_CAPTURE="$capture" FM_FAKE_TMUX_CURSOR_Y=2 \
|
|
1669
|
+
pane_input_pending "fakepane"; then
|
|
1670
|
+
fail "proven empty agent prompt '$prompt' should not defer"
|
|
1671
|
+
fi
|
|
1672
|
+
done
|
|
1673
|
+
pass "pane_input_pending: only proven empty agent prompts pass"
|
|
1674
|
+
}
|
|
1675
|
+
|
|
1676
|
+
# The safety fix at the tmux classifier (task fm-composer-shellglyph-safety): a
|
|
1677
|
+
# bare, unbordered shell prompt is a dead shell (the agent exited to its login
|
|
1678
|
+
# shell), NOT an empty agent composer. It must read `unknown` (unsafe target),
|
|
1679
|
+
# never `empty`. Before this fix a dead-shell pane read `empty` and the away-mode
|
|
1680
|
+
# injector could type (and a shell could execute) an escalation there.
|
|
1681
|
+
test_tmux_composer_state_bare_shell_is_unknown() {
|
|
1682
|
+
local dir fakebin capture g out
|
|
1683
|
+
dir=$(make_supercase composer-bare-shell)
|
|
1684
|
+
fakebin="$dir/fakebin"; capture="$dir/pane.txt"
|
|
1685
|
+
for g in '$' '%' '#' '>'; do
|
|
1686
|
+
printf 'output\noutput\n%s \n' "$g" > "$capture"
|
|
1687
|
+
out=$(PATH="$fakebin:$PATH" FM_FAKE_TMUX_CAPTURE="$capture" FM_FAKE_TMUX_CURSOR_Y=2 \
|
|
1688
|
+
fm_tmux_composer_state "fakepane")
|
|
1689
|
+
[ "$out" = unknown ] \
|
|
1690
|
+
|| fail "bare shell prompt '$g' must classify unknown (dead shell, unsafe), got '$out'"
|
|
1691
|
+
done
|
|
1692
|
+
pass "fm_tmux_composer_state: a bare shell prompt (\$/%/#/>) reads unknown, never empty (dead-shell injection safety)"
|
|
1693
|
+
}
|
|
1694
|
+
|
|
1695
|
+
# The other side of the fix: a bordered composer box (the harness draws its own
|
|
1696
|
+
# prompt glyph inside it) and a bare AGENT prompt glyph (claude ❯, codex ›) are
|
|
1697
|
+
# genuine empty agent composers and must still read `empty`.
|
|
1698
|
+
test_tmux_composer_state_bordered_and_agent_rows_are_empty() {
|
|
1699
|
+
local dir fakebin capture out
|
|
1700
|
+
dir=$(make_supercase composer-empty-agent)
|
|
1701
|
+
fakebin="$dir/fakebin"; capture="$dir/pane.txt"
|
|
1702
|
+
printf '╭────────────────────────╮\n│ > │\n╰────────────────────────╯\n' > "$capture"
|
|
1703
|
+
out=$(PATH="$fakebin:$PATH" FM_FAKE_TMUX_CAPTURE="$capture" FM_FAKE_TMUX_CURSOR_Y=1 \
|
|
1704
|
+
fm_tmux_composer_state "fakepane")
|
|
1705
|
+
[ "$out" = empty ] || fail "a bordered '│ > │' composer should read empty, got '$out'"
|
|
1706
|
+
printf '%s\n' "❯ " > "$capture"
|
|
1707
|
+
out=$(PATH="$fakebin:$PATH" FM_FAKE_TMUX_CAPTURE="$capture" FM_FAKE_TMUX_CURSOR_Y=0 \
|
|
1708
|
+
fm_tmux_composer_state "fakepane")
|
|
1709
|
+
[ "$out" = empty ] || fail "a bare claude '❯' composer should read empty, got '$out'"
|
|
1710
|
+
printf '%s\n' "› " > "$capture"
|
|
1711
|
+
out=$(PATH="$fakebin:$PATH" FM_FAKE_TMUX_CAPTURE="$capture" FM_FAKE_TMUX_CURSOR_Y=0 \
|
|
1712
|
+
fm_tmux_composer_state "fakepane")
|
|
1713
|
+
[ "$out" = empty ] || fail "a bare codex '›' composer should read empty, got '$out'"
|
|
1714
|
+
pass "fm_tmux_composer_state: a bordered composer box and bare agent glyphs (❯/›) still read empty"
|
|
1715
|
+
}
|
|
1716
|
+
|
|
1717
|
+
test_tmux_composer_state_requires_matching_box_borders() {
|
|
1718
|
+
local dir fakebin capture line out
|
|
1719
|
+
dir=$(make_supercase composer-decorated-shell)
|
|
1720
|
+
fakebin="$dir/fakebin"; capture="$dir/pane.txt"
|
|
1721
|
+
for line in '| $ ' '$ |' '│ % ' '# ┃'; do
|
|
1722
|
+
printf '%s\n' "$line" > "$capture"
|
|
1723
|
+
out=$(PATH="$fakebin:$PATH" FM_FAKE_TMUX_CAPTURE="$capture" FM_FAKE_TMUX_CURSOR_Y=0 \
|
|
1724
|
+
fm_tmux_composer_state "fakepane")
|
|
1725
|
+
[ "$out" != empty ] \
|
|
1726
|
+
|| fail "a decorated shell prompt '$line' must not read as an empty composer"
|
|
1727
|
+
done
|
|
1728
|
+
pass "fm_tmux_composer_state: only matching edge borders form a composer box"
|
|
1729
|
+
}
|
|
1730
|
+
|
|
1731
|
+
test_pane_input_pending_preserves_bright_placeholder_like_draft() {
|
|
1732
|
+
local dir fakebin capture
|
|
1733
|
+
dir=$(make_supercase pending-custom-idle)
|
|
1734
|
+
fakebin="$dir/fakebin"
|
|
1735
|
+
capture="$dir/pane.txt"
|
|
1736
|
+
printf '╭────────────────╮\n│ custom idle> │\n╰────────────────╯\n' > "$capture"
|
|
1737
|
+
PATH="$fakebin:$PATH" FM_FAKE_TMUX_CAPTURE="$capture" FM_FAKE_TMUX_CURSOR_Y=1 \
|
|
1738
|
+
FM_COMPOSER_IDLE_RE='^custom idle>$' pane_input_pending "fakepane" \
|
|
1739
|
+
|| fail "bright placeholder-like input must remain pending in a styled capture"
|
|
1740
|
+
pass "pane_input_pending preserves bright placeholder-like drafts in styled captures"
|
|
1741
|
+
}
|
|
1742
|
+
|
|
1743
|
+
test_classify_signal_dedup_against_scan() {
|
|
1744
|
+
# If the catch-all scan already escalated a status (seen marker matches),
|
|
1745
|
+
# classify_signal must self-handle to avoid a duplicate in the digest.
|
|
1746
|
+
local dir state key out
|
|
1747
|
+
dir=$(make_supercase signal-dedup)
|
|
1748
|
+
state="$dir/state"
|
|
1749
|
+
printf 'done: PR https://x/y/pull/9\n' > "$state/dup-s9.status"
|
|
1750
|
+
# Simulate the catch-all scan having already escalated this status.
|
|
1751
|
+
key=$(printf '%s' "dup-s9" | tr ':/.' '___')
|
|
1752
|
+
seen_through "$state" "dup-s9"
|
|
1753
|
+
out=$(FM_STATE_OVERRIDE="$state" classify_signal "$state/dup-s9.status" "$state")
|
|
1754
|
+
case "$out" in self\|*) ;; *) fail "signal not deduped against scan: $out" ;; esac
|
|
1755
|
+
# Without the seen marker, it should escalate.
|
|
1756
|
+
rm -f "$state/.subsuper-seen-status-$key"
|
|
1757
|
+
out=$(FM_STATE_OVERRIDE="$state" classify_signal "$state/dup-s9.status" "$state")
|
|
1758
|
+
case "$out" in escalate\|*) ;; *) fail "signal should escalate when not seen: $out" ;; esac
|
|
1759
|
+
pass "classify_signal dedupes against the catch-all scan seen marker"
|
|
1760
|
+
}
|
|
1761
|
+
|
|
1762
|
+
test_classify_stale_dedup_against_signal() {
|
|
1763
|
+
# If the signal path already escalated a status (seen marker matches),
|
|
1764
|
+
# classify_stale must self-handle to avoid a duplicate in the digest.
|
|
1765
|
+
local dir state key out
|
|
1766
|
+
dir=$(make_supercase stale-dedup)
|
|
1767
|
+
state="$dir/state"
|
|
1768
|
+
printf 'done: PR https://x/y/pull/10\n' > "$state/dup-s10.status"
|
|
1769
|
+
key=$(printf '%s' "dup-s10" | tr ':/.' '___')
|
|
1770
|
+
seen_through "$state" "dup-s10"
|
|
1771
|
+
out=$(FM_STATE_OVERRIDE="$state" classify_stale "sess:fm-dup-s10" "$state")
|
|
1772
|
+
case "$out" in self\|*) ;; *) fail "stale not deduped against signal: $out" ;; esac
|
|
1773
|
+
# Without the seen marker, it should escalate.
|
|
1774
|
+
rm -f "$state/.subsuper-seen-status-$key"
|
|
1775
|
+
out=$(FM_STATE_OVERRIDE="$state" classify_stale "sess:fm-dup-s10" "$state")
|
|
1776
|
+
case "$out" in escalate\|*) ;; *) fail "stale should escalate when not seen: $out" ;; esac
|
|
1777
|
+
pass "classify_stale dedupes against the signal path seen marker"
|
|
1778
|
+
}
|
|
1779
|
+
|
|
1780
|
+
# AFK incident regression: a nonterminal working: line that was already surfaced
|
|
1781
|
+
# (seen marker matches, including free-text "merged") must keep possible-wedge
|
|
1782
|
+
# aging. handle_wake must record the stale marker; housekeeping re-escalates
|
|
1783
|
+
# once at the configured bound.
|
|
1784
|
+
test_afk_nonterminal_working_merged_keeps_wedge_aging() {
|
|
1785
|
+
local dir state key out win pane incident fakebin
|
|
1786
|
+
dir=$(make_supercase afk-working-merged-wedge)
|
|
1787
|
+
state="$dir/state"
|
|
1788
|
+
fakebin="$dir/fakebin"
|
|
1789
|
+
win="sess:fm-wishlist-w1"
|
|
1790
|
+
pane="$dir/pane.txt"
|
|
1791
|
+
incident='working: stage 2 setup complete on PR #74 exact source branch rebased onto merged #76; task dates preserved'
|
|
1792
|
+
printf '%s\n' "$incident" > "$state/wishlist-w1.status"
|
|
1793
|
+
printf 'idle prompt $\n' > "$pane"
|
|
1794
|
+
key=$(printf '%s' "wishlist-w1" | tr ':/.' '___')
|
|
1795
|
+
# Simulate an earlier false-positive escalate that wrote the seen marker.
|
|
1796
|
+
seen_through "$state" "wishlist-w1"
|
|
1797
|
+
out=$(FM_STATE_OVERRIDE="$state" classify_stale "$win" "$state")
|
|
1798
|
+
case "$out" in
|
|
1799
|
+
self\|*transient*) ;;
|
|
1800
|
+
escalate\|*) fail "nonterminal working: escalated as terminal stale: $out" ;;
|
|
1801
|
+
*)
|
|
1802
|
+
case "$out" in
|
|
1803
|
+
*already\ escalated*) fail "nonterminal working: treated as already-escalated terminal: $out" ;;
|
|
1804
|
+
*) fail "nonterminal working: unexpected classify_stale: $out" ;;
|
|
1805
|
+
esac
|
|
1806
|
+
;;
|
|
1807
|
+
esac
|
|
1808
|
+
FM_STATE_OVERRIDE="$state" handle_wake "stale: $win" "$state"
|
|
1809
|
+
[ -e "$state/.subsuper-stale-$key" ] \
|
|
1810
|
+
|| fail "wedge stale marker was not recorded for already-seen nonterminal working:"
|
|
1811
|
+
[ ! -s "$state/.subsuper-escalations" ] \
|
|
1812
|
+
|| fail "nonterminal working: stale incorrectly escalated immediately"
|
|
1813
|
+
# Age the marker past the escalate bound (marker stores first-seen epoch).
|
|
1814
|
+
echo $(( $(date +%s) - 500 )) > "$state/.subsuper-stale-$key"
|
|
1815
|
+
PATH="$fakebin:$PATH" FM_FAKE_TMUX_WINDOW="$win" FM_FAKE_TMUX_CAPTURE="$pane" \
|
|
1816
|
+
FM_STATE_OVERRIDE="$state" FM_STALE_ESCALATE_SECS=240 housekeeping "$state"
|
|
1817
|
+
[ -s "$state/.subsuper-escalations" ] \
|
|
1818
|
+
|| fail "housekeeping did not re-escalate aged nonterminal working: wedge"
|
|
1819
|
+
grep -q 'possible wedge' "$state/.subsuper-escalations" \
|
|
1820
|
+
|| fail "housekeeping escalate was not a possible-wedge: $(cat "$state/.subsuper-escalations")"
|
|
1821
|
+
pass "AFK nonterminal working:+merged keeps wedge aging and re-escalates at bound"
|
|
1822
|
+
}
|
|
1823
|
+
|
|
1824
|
+
test_afk_genuine_done_still_terminal_stale() {
|
|
1825
|
+
local dir state out
|
|
1826
|
+
dir=$(make_supercase afk-genuine-done-stale)
|
|
1827
|
+
state="$dir/state"
|
|
1828
|
+
printf 'done: PR https://example.com/pull/76 checks green; stage 1 of 4 ready for firstmate merge\n' \
|
|
1829
|
+
> "$state/stage1-w2.status"
|
|
1830
|
+
out=$(FM_STATE_OVERRIDE="$state" classify_stale "sess:fm-stage1-w2" "$state")
|
|
1831
|
+
case "$out" in escalate\|*) ;; *) fail "genuine done: stale did not escalate: $out" ;; esac
|
|
1832
|
+
out=$(classify_check "check: /s/t.check.sh: merged")
|
|
1833
|
+
case "$out" in escalate\|*) ;; *) fail "validated merge-check did not escalate: $out" ;; esac
|
|
1834
|
+
pass "genuine done: and merge-check events still escalate"
|
|
1835
|
+
}
|
|
1836
|
+
|
|
1837
|
+
test_pane_input_pending_bordered_idle_not_pending() {
|
|
1838
|
+
# THE regression: an idle claude composer is a bordered box ("│ > … │"). The
|
|
1839
|
+
# old idle regex only matched a BARE prompt, so every idle claude pane read as
|
|
1840
|
+
# pending and the away-mode daemon deferred 100% of escalations for 9.5h.
|
|
1841
|
+
local dir state fakebin capture line
|
|
1842
|
+
dir=$(make_supercase pending-bordered-idle)
|
|
1843
|
+
state="$dir/state"; fakebin="$dir/fakebin"; capture="$dir/pane.txt"
|
|
1844
|
+
for line in '>' '❯' ''; do
|
|
1845
|
+
case "$line" in
|
|
1846
|
+
'>') printf '╭────────────╮\n│ > │\n╰────────────╯\n' > "$capture" ;;
|
|
1847
|
+
'❯') printf '╭────────────╮\n│ ❯ │\n╰────────────╯\n' > "$capture" ;;
|
|
1848
|
+
'') printf '╭────────────╮\n│ │\n╰────────────╯\n' > "$capture" ;;
|
|
1849
|
+
esac
|
|
1850
|
+
if PATH="$fakebin:$PATH" FM_FAKE_TMUX_CAPTURE="$capture" FM_FAKE_TMUX_CURSOR_Y=1 \
|
|
1851
|
+
pane_input_pending "fakepane"; then
|
|
1852
|
+
fail "bordered idle composer falsely detected as pending: <$line>"
|
|
1853
|
+
fi
|
|
1854
|
+
done
|
|
1855
|
+
pass "pane_input_pending: an idle bordered composer is NOT pending (afk-invx-i5)"
|
|
1856
|
+
}
|
|
1857
|
+
|
|
1858
|
+
test_pane_input_pending_bordered_with_text_is_pending() {
|
|
1859
|
+
# Guard against over-broadening: real unsubmitted text inside the box must
|
|
1860
|
+
# still read as pending so the daemon defers (and the captain-return race is
|
|
1861
|
+
# still protected).
|
|
1862
|
+
local dir state fakebin capture
|
|
1863
|
+
dir=$(make_supercase pending-bordered-text)
|
|
1864
|
+
state="$dir/state"; fakebin="$dir/fakebin"; capture="$dir/pane.txt"
|
|
1865
|
+
printf '╭────────────────────────────────────────────────╮\n│ > fix findings 1 and 3, skip 2 │\n╰────────────────────────────────────────────────╯\n' > "$capture"
|
|
1866
|
+
PATH="$fakebin:$PATH" FM_FAKE_TMUX_CAPTURE="$capture" FM_FAKE_TMUX_CURSOR_Y=1 \
|
|
1867
|
+
pane_input_pending "fakepane" \
|
|
1868
|
+
|| fail "real text inside a bordered composer was not detected as pending"
|
|
1869
|
+
pass "pane_input_pending: text inside a bordered composer is still pending"
|
|
1870
|
+
}
|
|
1871
|
+
|
|
1872
|
+
test_submit_ack_confirms_on_bordered_empty_composer() {
|
|
1873
|
+
# RC2: the submit acknowledgement must recognize a bordered-EMPTY composer as
|
|
1874
|
+
# "submitted." The old ACK reused the broken check, so on claude it could never
|
|
1875
|
+
# confirm and always reported a false "Enter swallowed."
|
|
1876
|
+
local dir fakebin sent verdict
|
|
1877
|
+
dir=$(make_bordered_case ack-bordered)
|
|
1878
|
+
fakebin="$dir/fakebin"; sent="$dir/sent.log"; : > "$sent"
|
|
1879
|
+
verdict=$(PATH="$fakebin:$PATH" FM_FAKE_COMPOSER="$dir/composer" FM_FAKE_SENT="$sent" \
|
|
1880
|
+
fm_tmux_submit_core "win" "the digest" 3 0.05 0.05)
|
|
1881
|
+
[ "$verdict" = empty ] || fail "submit-ACK did not confirm on a bordered-empty composer: $verdict"
|
|
1882
|
+
[ "$(grep -cv '\[ENTER\]' "$sent")" -eq 1 ] || fail "digest typed more than once (retype)"
|
|
1883
|
+
[ "$(grep -c '\[ENTER\]' "$sent")" -eq 1 ] || fail "expected exactly one submitted Enter"
|
|
1884
|
+
pass "submit-ACK confirms a submit when the composer returns to a bordered-empty box"
|
|
1885
|
+
}
|
|
1886
|
+
|
|
1887
|
+
test_submit_ack_reports_pending_on_persistent_swallow() {
|
|
1888
|
+
# A genuinely swallowed Enter (text stays in the box across all retries) is
|
|
1889
|
+
# reported as "pending" — the daemon keeps the buffer, fm-send exits non-zero —
|
|
1890
|
+
# and the digest is typed ONCE (Enter-only retries, never a retype).
|
|
1891
|
+
local dir fakebin sent verdict
|
|
1892
|
+
dir=$(make_bordered_case ack-swallow)
|
|
1893
|
+
fakebin="$dir/fakebin"; sent="$dir/sent.log"; : > "$sent"
|
|
1894
|
+
touch "$dir/.swallow"
|
|
1895
|
+
verdict=$(PATH="$fakebin:$PATH" FM_FAKE_COMPOSER="$dir/composer" FM_FAKE_SENT="$sent" \
|
|
1896
|
+
FM_FAKE_SWALLOW="$dir/.swallow" FM_FAKE_PERSIST_SWALLOW=1 \
|
|
1897
|
+
fm_tmux_submit_core "win" "the digest" 3 0.05 0.05)
|
|
1898
|
+
[ "$verdict" = pending ] || fail "persistent swallow not reported as pending: $verdict"
|
|
1899
|
+
[ "$(grep -cv '\[ENTER\]' "$sent")" -eq 1 ] || fail "digest retyped on swallow (expected type-once)"
|
|
1900
|
+
pass "submit-ACK reports pending on a persistently swallowed Enter (type-once)"
|
|
1901
|
+
}
|
|
1902
|
+
|
|
1903
|
+
test_max_defer_empty_swallow_types_once_and_alarms() {
|
|
1904
|
+
local dir state fakebin sent
|
|
1905
|
+
dir=$(make_bordered_case maxdefer-stuck)
|
|
1906
|
+
state="$dir/state"; fakebin="$dir/fakebin"
|
|
1907
|
+
sent="$dir/sent.log"; : > "$sent"
|
|
1908
|
+
printf '╭─────╮\n│ > │\n╰─────╯\n' > "$dir/composer"
|
|
1909
|
+
touch "$dir/.swallow"
|
|
1910
|
+
escalate_add "$state" "needs-decision: pick A"
|
|
1911
|
+
echo $(( $(date +%s) - 600 )) > "$state/.subsuper-escalations.since"
|
|
1912
|
+
afk_enter "$state"
|
|
1913
|
+
PATH="$fakebin:$PATH" FM_FAKE_COMPOSER="$dir/composer" FM_FAKE_SENT="$sent" \
|
|
1914
|
+
FM_FAKE_SWALLOW="$dir/.swallow" FM_FAKE_PERSIST_SWALLOW=1 FM_INJECT_CONFIRM_SLEEP=0.05 \
|
|
1915
|
+
FM_ESCALATE_BATCH_SECS=99999 FM_MAX_DEFER_SECS=60 housekeeping "$state"
|
|
1916
|
+
[ "$(grep -c 'Supervisor escalate' "$sent" 2>/dev/null || true)" -eq 1 ] \
|
|
1917
|
+
|| fail "max-defer typed the digest more than once"
|
|
1918
|
+
[ -s "$state/.subsuper-inject-wedged" ] \
|
|
1919
|
+
|| fail "stuck max-defer inject did not raise a wedge alarm marker"
|
|
1920
|
+
[ -s "$state/.subsuper-escalations" ] \
|
|
1921
|
+
|| fail "buffer lost after a failed max-defer inject (must be preserved)"
|
|
1922
|
+
pass "max-defer on an empty stuck pane types once, alarms, and preserves the buffer"
|
|
1923
|
+
}
|
|
1924
|
+
|
|
1925
|
+
test_max_defer_flushes_empty_idle_pane() {
|
|
1926
|
+
local dir state fakebin sent
|
|
1927
|
+
dir=$(make_bordered_case maxdefer-recover)
|
|
1928
|
+
state="$dir/state"; fakebin="$dir/fakebin"
|
|
1929
|
+
sent="$dir/sent.log"; : > "$sent"
|
|
1930
|
+
printf '╭─────╮\n│ > │\n╰─────╯\n' > "$dir/composer"
|
|
1931
|
+
escalate_add "$state" "done: PR https://x/y/pull/1"
|
|
1932
|
+
echo $(( $(date +%s) - 600 )) > "$state/.subsuper-escalations.since"
|
|
1933
|
+
afk_enter "$state"
|
|
1934
|
+
PATH="$fakebin:$PATH" FM_FAKE_COMPOSER="$dir/composer" FM_FAKE_SENT="$sent" \
|
|
1935
|
+
FM_ESCALATE_BATCH_SECS=99999 FM_MAX_DEFER_SECS=60 FM_INJECT_CONFIRM_SLEEP=0.05 \
|
|
1936
|
+
housekeeping "$state"
|
|
1937
|
+
[ ! -s "$state/.subsuper-escalations" ] || fail "buffer not cleared after a recovered max-defer flush"
|
|
1938
|
+
[ ! -e "$state/.subsuper-inject-wedged" ] || fail "wedge alarm left behind after a successful max-defer flush"
|
|
1939
|
+
pass "max-defer flushes and clears the buffer on an empty bordered pane"
|
|
1940
|
+
}
|
|
1941
|
+
|
|
1942
|
+
test_max_defer_pending_composer_alarms_without_typing() {
|
|
1943
|
+
local dir state fakebin sent
|
|
1944
|
+
dir=$(make_bordered_case maxdefer-pending-digest)
|
|
1945
|
+
state="$dir/state"; fakebin="$dir/fakebin"
|
|
1946
|
+
sent="$dir/sent.log"; : > "$sent"
|
|
1947
|
+
printf '╭─────────────────╮\n│ > human draft │\n╰─────────────────╯\n' > "$dir/composer"
|
|
1948
|
+
escalate_add "$state" "needs-decision: pick B"
|
|
1949
|
+
echo $(( $(date +%s) - 600 )) > "$state/.subsuper-escalations.since"
|
|
1950
|
+
afk_enter "$state"
|
|
1951
|
+
PATH="$fakebin:$PATH" FM_FAKE_COMPOSER="$dir/composer" FM_FAKE_SENT="$sent" \
|
|
1952
|
+
FM_ESCALATE_BATCH_SECS=99999 FM_MAX_DEFER_SECS=60 FM_INJECT_CONFIRM_SLEEP=0.05 \
|
|
1953
|
+
housekeeping "$state"
|
|
1954
|
+
[ ! -s "$sent" ] || fail "max-defer typed into a pending composer"
|
|
1955
|
+
[ -s "$state/.subsuper-inject-wedged" ] || fail "pending composer did not raise a wedge alarm marker"
|
|
1956
|
+
[ -s "$state/.subsuper-escalations" ] || fail "buffer lost while composer was pending"
|
|
1957
|
+
grep -F 'human draft' "$dir/composer" >/dev/null || fail "pending composer content changed"
|
|
1958
|
+
pass "max-defer on a pending composer alarms without typing"
|
|
1959
|
+
}
|
|
1960
|
+
|
|
1961
|
+
test_normal_flush_clears_stale_wedge_marker() {
|
|
1962
|
+
local dir state fakebin sent
|
|
1963
|
+
dir=$(make_bordered_case normal-clears-wedge)
|
|
1964
|
+
state="$dir/state"; fakebin="$dir/fakebin"
|
|
1965
|
+
sent="$dir/sent.log"; : > "$sent"
|
|
1966
|
+
printf 'old wedge\n' > "$state/.subsuper-inject-wedged"
|
|
1967
|
+
escalate_add "$state" "done: PR https://x/y/pull/2"
|
|
1968
|
+
afk_enter "$state"
|
|
1969
|
+
PATH="$fakebin:$PATH" FM_FAKE_COMPOSER="$dir/composer" FM_FAKE_SENT="$sent" \
|
|
1970
|
+
FM_INJECT_CONFIRM_SLEEP=0.05 escalate_flush "$state" \
|
|
1971
|
+
|| fail "normal escalate_flush failed"
|
|
1972
|
+
[ ! -s "$state/.subsuper-escalations" ] || fail "buffer not cleared after normal flush"
|
|
1973
|
+
[ ! -e "$state/.subsuper-inject-wedged" ] || fail "wedge marker survived successful normal flush"
|
|
1974
|
+
pass "normal flush clears a stale wedge marker"
|
|
1975
|
+
}
|
|
1976
|
+
|
|
1977
|
+
test_below_max_defer_does_nothing() {
|
|
1978
|
+
local dir state fakebin sent capture
|
|
1979
|
+
dir=$(make_supercase below-maxdefer)
|
|
1980
|
+
state="$dir/state"; fakebin="$dir/fakebin"
|
|
1981
|
+
sent="$dir/sent.log"; : > "$sent"
|
|
1982
|
+
capture="$dir/pane.txt"; printf 'stuck junk line\n' > "$capture"
|
|
1983
|
+
escalate_add "$state" "needs-decision: pick A"
|
|
1984
|
+
date +%s > "$state/.subsuper-escalations.since" # just now
|
|
1985
|
+
afk_enter "$state"
|
|
1986
|
+
PATH="$fakebin:$PATH" FM_FAKE_TMUX_PANE_ALIVE=1 FM_FAKE_TMUX_SENT="$sent" \
|
|
1987
|
+
FM_FAKE_TMUX_CAPTURE="$capture" FM_FAKE_TMUX_CURSOR_Y=0 \
|
|
1988
|
+
FM_ESCALATE_BATCH_SECS=99999 FM_MAX_DEFER_SECS=300 housekeeping "$state"
|
|
1989
|
+
[ ! -s "$sent" ] || fail "injected before MAX_DEFER elapsed"
|
|
1990
|
+
[ ! -e "$state/.subsuper-inject-wedged" ] || fail "wedge alarm fired before MAX_DEFER"
|
|
1991
|
+
[ -s "$state/.subsuper-escalations" ] || fail "buffer dropped below MAX_DEFER"
|
|
1992
|
+
pass "below MAX_DEFER: no inject, no alarm, buffer preserved"
|
|
1993
|
+
}
|
|
1994
|
+
|
|
1995
|
+
test_max_defer_afk_inactive_does_not_flush_or_alarm() {
|
|
1996
|
+
local dir state fakebin sent
|
|
1997
|
+
dir=$(make_bordered_case maxdefer-inactive)
|
|
1998
|
+
state="$dir/state"; fakebin="$dir/fakebin"
|
|
1999
|
+
sent="$dir/sent.log"; : > "$sent"
|
|
2000
|
+
escalate_add "$state" "needs-decision: pick B"
|
|
2001
|
+
echo $(( $(date +%s) - 600 )) > "$state/.subsuper-escalations.since"
|
|
2002
|
+
PATH="$fakebin:$PATH" FM_FAKE_COMPOSER="$dir/composer" FM_FAKE_SENT="$sent" \
|
|
2003
|
+
FM_ESCALATE_BATCH_SECS=99999 FM_MAX_DEFER_SECS=60 FM_INJECT_CONFIRM_SLEEP=0.05 \
|
|
2004
|
+
housekeeping "$state"
|
|
2005
|
+
[ ! -s "$sent" ] || fail "injected while afk was inactive"
|
|
2006
|
+
[ ! -e "$state/.subsuper-inject-wedged" ] || fail "wedge alarm fired while afk was inactive"
|
|
2007
|
+
[ -s "$state/.subsuper-escalations" ] || fail "buffer dropped while afk was inactive"
|
|
2008
|
+
pass "max-defer does not flush or alarm while afk is inactive"
|
|
2009
|
+
}
|
|
2010
|
+
|
|
2011
|
+
# --- backend-independent active wedge alert ---------------------------------
|
|
2012
|
+
# These cover the 2026-07-10 overnight-incident fix: the max-defer wedge alarm's
|
|
2013
|
+
# ACTIVE alert channel must reach the captain even when the wedged pane and its
|
|
2014
|
+
# backend status-line are unreadable (a claude-on-herdr primary that night).
|
|
2015
|
+
#
|
|
2016
|
+
# NO test here EVER posts a real notification. Every notifier routes through
|
|
2017
|
+
# the FM_WEDGE_ALARM_EXEC seam, which tests/wake-helpers.sh forces to a recorder
|
|
2018
|
+
# ($FM_WEDGE_ALARM_LOG logs "<channel>\t<summary>"); the daemon also defaults
|
|
2019
|
+
# that seam to "discard" whenever it is sourced. Assertions read the recorder
|
|
2020
|
+
# log, so they verify channel SELECTION and summary propagation; the real
|
|
2021
|
+
# osascript/herdr argv is verified once by the bounded manual evidence in
|
|
2022
|
+
# docs/wedge-alarm.md, never from a suite.
|
|
2023
|
+
make_wedge_case() { # <name> -> echoes dir; creates state/, fakebin/{uname,osascript,herdr}, alert.log
|
|
2024
|
+
local name=$1 dir fakebin
|
|
2025
|
+
dir="$TMP_ROOT/$name"; fakebin="$dir/fakebin"
|
|
2026
|
+
mkdir -p "$dir/state" "$fakebin"
|
|
2027
|
+
# Fake uname so `auto` platform resolution is deterministic on any CI host.
|
|
2028
|
+
cat > "$fakebin/uname" <<'SH'
|
|
2029
|
+
#!/usr/bin/env bash
|
|
2030
|
+
printf '%s\n' "${FM_FAKE_UNAME:-Darwin}"
|
|
2031
|
+
SH
|
|
2032
|
+
# Fakes keep command discovery deterministic on any CI host.
|
|
2033
|
+
cat > "$fakebin/osascript" <<'SH'
|
|
2034
|
+
#!/usr/bin/env bash
|
|
2035
|
+
printf '%s\n' osascript >> "${FM_WEDGE_ALARM_REAL_LOG:-/dev/null}"
|
|
2036
|
+
exit 0
|
|
2037
|
+
SH
|
|
2038
|
+
cat > "$fakebin/herdr" <<'SH'
|
|
2039
|
+
#!/usr/bin/env bash
|
|
2040
|
+
printf '%s\n' herdr >> "${FM_WEDGE_ALARM_REAL_LOG:-/dev/null}"
|
|
2041
|
+
exit 0
|
|
2042
|
+
SH
|
|
2043
|
+
chmod +x "$fakebin/uname" "$fakebin/osascript" "$fakebin/herdr"
|
|
2044
|
+
: > "$dir/alert.log"
|
|
2045
|
+
printf '%s\n' "$dir"
|
|
2046
|
+
}
|
|
2047
|
+
|
|
2048
|
+
test_wedge_alarm_library_mode_defaults_to_discard() {
|
|
2049
|
+
# The structural guarantee: sourcing the daemon with NO seam configured defaults
|
|
2050
|
+
# FM_WEDGE_ALARM_EXEC to "discard", so a sourced context (every test) cannot
|
|
2051
|
+
# fire a real notification even if it forgets to stub. Checked in a clean
|
|
2052
|
+
# subshell that first unsets this harness's recorder.
|
|
2053
|
+
local out
|
|
2054
|
+
# shellcheck disable=SC2016 # $1/$FM_WEDGE_ALARM_EXEC must expand in the child, not here
|
|
2055
|
+
out=$(env -u FM_WEDGE_ALARM_EXEC bash -c '. "$1"; printf "%s" "${FM_WEDGE_ALARM_EXEC:-UNSET}"' _ "$DAEMON")
|
|
2056
|
+
[ "$out" = discard ] \
|
|
2057
|
+
|| fail "sourcing the daemon did not default the notifier seam to discard (got: $out)"
|
|
2058
|
+
pass "library mode: sourcing the daemon defaults FM_WEDGE_ALARM_EXEC to discard (no test can fire a real notification)"
|
|
2059
|
+
}
|
|
2060
|
+
|
|
2061
|
+
test_wake_helpers_replace_inherited_notifier_override() {
|
|
2062
|
+
local dir unsafe_log alert_log unsafe
|
|
2063
|
+
dir=$(make_wedge_case wedge-inherited-override)
|
|
2064
|
+
unsafe_log="$dir/unsafe.log"
|
|
2065
|
+
alert_log="$dir/alert.log"
|
|
2066
|
+
unsafe="$dir/unsafe-override"
|
|
2067
|
+
cat > "$unsafe" <<'SH'
|
|
2068
|
+
#!/usr/bin/env bash
|
|
2069
|
+
printf '%s\n' invoked >> "${FM_WEDGE_ALARM_UNSAFE_LOG:?}"
|
|
2070
|
+
SH
|
|
2071
|
+
chmod +x "$unsafe"
|
|
2072
|
+
FM_WEDGE_ALARM_EXEC="$unsafe" FM_WEDGE_ALARM_UNSAFE_LOG="$unsafe_log" \
|
|
2073
|
+
FM_WEDGE_ALARM_LOG="$alert_log" FM_WEDGE_ALARM_CHANNEL=osascript \
|
|
2074
|
+
bash -c '. "$1"; . "$2"; wedge_alarm_notify "away-mode WEDGED 900s" "/s/.marker"' \
|
|
2075
|
+
_ "$ROOT/tests/wake-helpers.sh" "$DAEMON"
|
|
2076
|
+
[ ! -s "$unsafe_log" ] || fail "wake helpers preserved an inherited notifier override"
|
|
2077
|
+
grep -F 'osascript' "$alert_log" >/dev/null \
|
|
2078
|
+
|| fail "wake helpers did not install the safe notifier recorder"
|
|
2079
|
+
pass "wake helpers replace inherited notifier overrides with the safe recorder"
|
|
2080
|
+
}
|
|
2081
|
+
|
|
2082
|
+
test_wedge_alarm_discard_seam_fires_nothing() {
|
|
2083
|
+
local dir log command_output channel
|
|
2084
|
+
dir=$(make_wedge_case wedge-discard); log="$dir/alert.log"
|
|
2085
|
+
command_output="$dir/command-output"
|
|
2086
|
+
channel="command: printf '%s' \"\$1\" > '$command_output'"
|
|
2087
|
+
PATH="$dir/fakebin:$PATH" FM_WEDGE_ALARM_LOG="$log" FM_WEDGE_ALARM_EXEC=discard \
|
|
2088
|
+
FM_WEDGE_ALARM_CHANNEL=$'osascript\nherdr\n'"$channel" \
|
|
2089
|
+
wedge_alarm_notify "away-mode WEDGED 900s" "/s/.marker"
|
|
2090
|
+
[ ! -s "$log" ] || fail "the discard seam still fired a notifier: $(cat "$log")"
|
|
2091
|
+
[ ! -e "$command_output" ] || fail "the discard seam still fired a command: notifier"
|
|
2092
|
+
pass "the discard seam suppresses every notifier, including command: (fires nothing)"
|
|
2093
|
+
}
|
|
2094
|
+
|
|
2095
|
+
test_wedge_alarm_direct_notifiers_honor_discard_seam() {
|
|
2096
|
+
local dir real_log command_output command
|
|
2097
|
+
dir=$(make_wedge_case wedge-direct-discard); real_log="$dir/real.log"
|
|
2098
|
+
command_output="$dir/command-output"
|
|
2099
|
+
command="printf '%s' \"\$1\" > '$command_output'"
|
|
2100
|
+
PATH="$dir/fakebin:$PATH" FM_WEDGE_ALARM_REAL_LOG="$real_log" FM_WEDGE_ALARM_EXEC=discard \
|
|
2101
|
+
wedge_alarm_via_osascript "away-mode WEDGED 900s"
|
|
2102
|
+
PATH="$dir/fakebin:$PATH" FM_WEDGE_ALARM_REAL_LOG="$real_log" FM_WEDGE_ALARM_EXEC=discard \
|
|
2103
|
+
wedge_alarm_via_herdr "away-mode WEDGED 900s"
|
|
2104
|
+
FM_WEDGE_ALARM_EXEC=discard wedge_alarm_via_command "$command" "away-mode WEDGED 900s"
|
|
2105
|
+
[ ! -s "$real_log" ] || fail "direct notifier helpers bypassed the discard seam: $(cat "$real_log")"
|
|
2106
|
+
[ ! -e "$command_output" ] || fail "direct command helper bypassed the discard seam"
|
|
2107
|
+
pass "direct notifier helpers honor the discard seam, including command:"
|
|
2108
|
+
}
|
|
2109
|
+
|
|
2110
|
+
test_wedge_alarm_osascript_channel_selected() {
|
|
2111
|
+
local dir log
|
|
2112
|
+
dir=$(make_wedge_case wedge-osascript); log="$dir/alert.log"
|
|
2113
|
+
FM_WEDGE_ALARM_LOG="$log" FM_WEDGE_ALARM_CHANNEL=osascript \
|
|
2114
|
+
wedge_alarm_notify "away-mode escalations WEDGED 600s undelivered - see /s/.marker" "/s/.marker"
|
|
2115
|
+
grep -F 'osascript' "$log" >/dev/null || fail "osascript channel not routed through the notifier seam: $(cat "$log")"
|
|
2116
|
+
grep -F 'WEDGED 600s undelivered' "$log" >/dev/null || fail "osascript channel did not carry the summary"
|
|
2117
|
+
grep -F 'herdr' "$log" >/dev/null && fail "osascript-only config also selected herdr"
|
|
2118
|
+
pass "osascript channel routes through the notifier seam with the summary (never a real notification)"
|
|
2119
|
+
}
|
|
2120
|
+
|
|
2121
|
+
test_wedge_alarm_herdr_channel_selected() {
|
|
2122
|
+
local dir log
|
|
2123
|
+
dir=$(make_wedge_case wedge-herdr); log="$dir/alert.log"
|
|
2124
|
+
FM_WEDGE_ALARM_LOG="$log" FM_WEDGE_ALARM_CHANNEL=herdr \
|
|
2125
|
+
wedge_alarm_notify "away-mode escalations WEDGED 800s undelivered - see /s/.marker" "/s/.marker"
|
|
2126
|
+
grep -F 'herdr' "$log" >/dev/null || fail "herdr channel not routed through the notifier seam: $(cat "$log")"
|
|
2127
|
+
grep -F 'WEDGED 800s undelivered' "$log" >/dev/null || fail "herdr channel did not carry the summary"
|
|
2128
|
+
grep -F 'osascript' "$log" >/dev/null && fail "herdr-only config also selected osascript"
|
|
2129
|
+
pass "herdr channel routes through the notifier seam with the summary (never a real notification)"
|
|
2130
|
+
}
|
|
2131
|
+
|
|
2132
|
+
test_wedge_alarm_command_channel_receives_summary() {
|
|
2133
|
+
local dir out_argv out_stdin chan
|
|
2134
|
+
dir=$(make_wedge_case wedge-command)
|
|
2135
|
+
out_argv="$dir/argv.txt"; out_stdin="$dir/stdin.txt"
|
|
2136
|
+
chan="command: printf '%s' \"\$1\" > '$out_argv'; cat > '$out_stdin'"
|
|
2137
|
+
FM_WEDGE_ALARM_EXEC='' FM_WEDGE_ALARM_CHANNEL="$chan" \
|
|
2138
|
+
wedge_alarm_notify "away-mode WEDGED 900s" "/s/.marker"
|
|
2139
|
+
[ "$(cat "$out_argv" 2>/dev/null)" = "away-mode WEDGED 900s" ] || fail "command channel did not receive the summary on \$1"
|
|
2140
|
+
grep -F 'away-mode WEDGED 900s' "$out_stdin" >/dev/null || fail "command channel did not receive the summary on stdin"
|
|
2141
|
+
pass "command channel runs the captain command with the summary on \$1 and on stdin"
|
|
2142
|
+
}
|
|
2143
|
+
|
|
2144
|
+
test_wedge_alarm_command_failure_hides_configured_command() {
|
|
2145
|
+
local dir daemon_log secret rc
|
|
2146
|
+
dir=$(make_wedge_case wedge-command-redaction); daemon_log="$dir/daemon.log"
|
|
2147
|
+
secret="https://alerts.example.invalid/hook?token=private-wedge-token"
|
|
2148
|
+
LOG="$daemon_log" FM_WEDGE_ALARM_EXEC='' FM_WEDGE_ALARM_CHANNEL="command:exit 73 # $secret" \
|
|
2149
|
+
wedge_alarm_notify "away-mode WEDGED 900s" "/s/.marker"
|
|
2150
|
+
rc=$?
|
|
2151
|
+
[ "$rc" -eq 0 ] || fail "a failed command channel made wedge_alarm_notify return non-zero ($rc)"
|
|
2152
|
+
grep -F 'command channel exited 73 (command redacted)' "$daemon_log" >/dev/null \
|
|
2153
|
+
|| fail "command channel failure did not log its exit status: $(cat "$daemon_log" 2>/dev/null)"
|
|
2154
|
+
grep -F "$secret" "$daemon_log" >/dev/null \
|
|
2155
|
+
&& fail "command channel failure leaked its configured command: $(cat "$daemon_log")"
|
|
2156
|
+
pass "command channel failures redact configured commands while logging their exit status"
|
|
2157
|
+
}
|
|
2158
|
+
|
|
2159
|
+
test_wedge_alarm_unknown_channel_hides_configured_directive() {
|
|
2160
|
+
local dir daemon_log secret rc
|
|
2161
|
+
dir=$(make_wedge_case wedge-unknown-redaction); daemon_log="$dir/daemon.log"
|
|
2162
|
+
secret="https://alerts.example.invalid/hook?token=private-wedge-token"
|
|
2163
|
+
LOG="$daemon_log" FM_WEDGE_ALARM_CHANNEL="webhook:$secret" \
|
|
2164
|
+
wedge_alarm_notify "away-mode WEDGED 900s" "/s/.marker"
|
|
2165
|
+
rc=$?
|
|
2166
|
+
[ "$rc" -eq 0 ] || fail "an unknown channel made wedge_alarm_notify return non-zero ($rc)"
|
|
2167
|
+
grep -F 'unrecognized active-alert channel directive (redacted); marker still written' "$daemon_log" >/dev/null \
|
|
2168
|
+
|| fail "an unknown channel did not log the redacted directive category: $(cat "$daemon_log" 2>/dev/null)"
|
|
2169
|
+
grep -F "$secret" "$daemon_log" >/dev/null \
|
|
2170
|
+
&& fail "an unknown channel leaked its configured directive: $(cat "$daemon_log")"
|
|
2171
|
+
pass "unknown channel directives are redacted while the alarm keeps running"
|
|
2172
|
+
}
|
|
2173
|
+
|
|
2174
|
+
test_wedge_alarm_off_disables_active_alert_regardless_of_position() {
|
|
2175
|
+
local dir log directives
|
|
2176
|
+
dir=$(make_wedge_case wedge-off); log="$dir/alert.log"
|
|
2177
|
+
for directives in $'osascript\noff' $'off\nosascript'; do
|
|
2178
|
+
: > "$log"
|
|
2179
|
+
FM_WEDGE_ALARM_LOG="$log" FM_WEDGE_ALARM_CHANNEL="$directives" \
|
|
2180
|
+
wedge_alarm_notify "away-mode WEDGED 900s" "/s/.marker"
|
|
2181
|
+
[ ! -s "$log" ] || fail "off did not disable a preceding or following active alert: $(cat "$log")"
|
|
2182
|
+
done
|
|
2183
|
+
pass "off disables every active alert regardless of directive position (marker and tmux flash are unaffected)"
|
|
2184
|
+
}
|
|
2185
|
+
|
|
2186
|
+
test_wedge_alarm_auto_darwin_selects_osascript() {
|
|
2187
|
+
local dir log
|
|
2188
|
+
dir=$(make_wedge_case wedge-auto-darwin); log="$dir/alert.log"
|
|
2189
|
+
PATH="$dir/fakebin:$PATH" FM_WEDGE_ALARM_LOG="$log" FM_FAKE_UNAME=Darwin FM_WEDGE_ALARM_CHANNEL=auto \
|
|
2190
|
+
wedge_alarm_notify "away-mode WEDGED 900s" "/s/.marker"
|
|
2191
|
+
grep -F 'osascript' "$log" >/dev/null || fail "auto did not resolve to osascript on Darwin: $(cat "$log")"
|
|
2192
|
+
pass "auto resolves to the macOS osascript notifier on Darwin (default-on)"
|
|
2193
|
+
}
|
|
2194
|
+
|
|
2195
|
+
test_wedge_alarm_auto_non_darwin_has_no_os_channel() {
|
|
2196
|
+
local dir log
|
|
2197
|
+
dir=$(make_wedge_case wedge-auto-linux); log="$dir/alert.log"
|
|
2198
|
+
PATH="$dir/fakebin:$PATH" FM_WEDGE_ALARM_LOG="$log" FM_FAKE_UNAME=Linux FM_WEDGE_ALARM_CHANNEL=auto \
|
|
2199
|
+
wedge_alarm_notify "away-mode WEDGED 900s" "/s/.marker"
|
|
2200
|
+
[ ! -s "$log" ] || fail "auto selected a built-in OS channel on a non-macOS platform: $(cat "$log")"
|
|
2201
|
+
pass "auto on a non-macOS platform selects no built-in OS channel (the marker or a configured command carries it)"
|
|
2202
|
+
}
|
|
2203
|
+
|
|
2204
|
+
test_wedge_alarm_config_file_multi_channel() {
|
|
2205
|
+
local dir cfgdir log
|
|
2206
|
+
dir=$(make_wedge_case wedge-config); log="$dir/alert.log"
|
|
2207
|
+
cfgdir="$dir/config"; mkdir -p "$cfgdir"
|
|
2208
|
+
printf '# active alert channels\n\nosascript\nherdr\n' > "$cfgdir/wedge-alarm"
|
|
2209
|
+
FM_WEDGE_ALARM_LOG="$log" FM_CONFIG_OVERRIDE="$cfgdir" \
|
|
2210
|
+
wedge_alarm_notify "away-mode WEDGED 700s" "/s/.marker"
|
|
2211
|
+
grep -F 'osascript' "$log" >/dev/null || fail "config/wedge-alarm osascript line was not selected"
|
|
2212
|
+
grep -F 'herdr' "$log" >/dev/null || fail "config/wedge-alarm herdr line was not selected"
|
|
2213
|
+
pass "config/wedge-alarm selects every configured channel and skips comment and blank lines"
|
|
2214
|
+
}
|
|
2215
|
+
|
|
2216
|
+
test_wedge_alarm_failing_channel_degrades_gracefully() {
|
|
2217
|
+
local dir log rc
|
|
2218
|
+
dir=$(make_wedge_case wedge-degrade); log="$dir/alert.log"
|
|
2219
|
+
FM_WEDGE_ALARM_LOG="$log" FM_WEDGE_ALARM_FAIL=osascript \
|
|
2220
|
+
FM_WEDGE_ALARM_CHANNEL=$'osascript\nherdr' \
|
|
2221
|
+
wedge_alarm_notify "away-mode WEDGED 900s" "/s/.marker"
|
|
2222
|
+
rc=$?
|
|
2223
|
+
[ "$rc" -eq 0 ] || fail "a failing channel made wedge_alarm_notify return non-zero ($rc)"
|
|
2224
|
+
grep -F 'osascript' "$log" >/dev/null || fail "the failing osascript channel was not even attempted"
|
|
2225
|
+
grep -F 'herdr' "$log" >/dev/null || fail "a failing earlier channel prevented the herdr channel from firing"
|
|
2226
|
+
pass "a failing channel logs and falls back to the next channel, never crashing the alarm"
|
|
2227
|
+
}
|
|
2228
|
+
|
|
2229
|
+
test_wedge_alarm_hung_channel_times_out_and_falls_through() {
|
|
2230
|
+
local dir daemon_log output channel start elapsed
|
|
2231
|
+
dir=$(make_wedge_case wedge-timeout); daemon_log="$dir/daemon.log"; output="$dir/fallback-output"
|
|
2232
|
+
channel="command: printf '%s' \"\$1\" > '$output'"
|
|
2233
|
+
start=$SECONDS
|
|
2234
|
+
LOG="$daemon_log" FM_WEDGE_ALARM_EXEC='' FM_WEDGE_ALARM_TIMEOUT_SECS=1 \
|
|
2235
|
+
FM_WEDGE_ALARM_CHANNEL=$'command:sleep 30\n'"$channel" \
|
|
2236
|
+
wedge_alarm_notify "away-mode WEDGED 900s" "/s/.marker"
|
|
2237
|
+
elapsed=$((SECONDS - start))
|
|
2238
|
+
[ "$elapsed" -lt 5 ] || fail "a hung wedge notifier blocked the alarm for ${elapsed}s"
|
|
2239
|
+
grep -F 'command notifier timed out' "$daemon_log" >/dev/null \
|
|
2240
|
+
|| fail "a hung wedge notifier did not log its timeout: $(cat "$daemon_log" 2>/dev/null)"
|
|
2241
|
+
[ "$(cat "$output" 2>/dev/null)" = "away-mode WEDGED 900s" ] \
|
|
2242
|
+
|| fail "a timed-out command notifier prevented the next channel"
|
|
2243
|
+
pass "a hung notifier is bounded, logged, and falls through to the next channel"
|
|
2244
|
+
}
|
|
2245
|
+
|
|
2246
|
+
test_wedge_alarm_backgrounded_command_times_out_and_reaps_descendant() {
|
|
2247
|
+
local dir daemon_log child_file child command
|
|
2248
|
+
dir=$(make_wedge_case wedge-backgrounded-timeout)
|
|
2249
|
+
daemon_log="$dir/daemon.log"
|
|
2250
|
+
child_file="$dir/notifier-child"
|
|
2251
|
+
command="sleep 30 & printf '%s' \"\$!\" > '$child_file'"
|
|
2252
|
+
LOG="$daemon_log" FM_WEDGE_ALARM_EXEC='' FM_WEDGE_ALARM_TIMEOUT_SECS=1 \
|
|
2253
|
+
wedge_alarm_via_command "$command" "away-mode WEDGED 900s"
|
|
2254
|
+
[ -s "$child_file" ] || fail "the backgrounded notifier did not record its descendant"
|
|
2255
|
+
child=$(cat "$child_file")
|
|
2256
|
+
grep -F 'command notifier timed out' "$daemon_log" >/dev/null \
|
|
2257
|
+
|| fail "a backgrounded command notifier bypassed its timeout: $(cat "$daemon_log" 2>/dev/null)"
|
|
2258
|
+
if is_live_non_zombie "$child"; then
|
|
2259
|
+
kill -TERM "$child" 2>/dev/null || true
|
|
2260
|
+
fail "a timed-out command notifier left its descendant running (pid $child)"
|
|
2261
|
+
fi
|
|
2262
|
+
pass "a backgrounded command notifier remains bounded until its process group is reaped"
|
|
2263
|
+
}
|
|
2264
|
+
|
|
2265
|
+
test_wedge_alarm_hung_override_times_out_and_falls_through() {
|
|
2266
|
+
local dir blocker daemon_log start elapsed
|
|
2267
|
+
dir=$(make_wedge_case wedge-override-timeout)
|
|
2268
|
+
blocker="$dir/blocker"; daemon_log="$dir/daemon.log"
|
|
2269
|
+
cat > "$blocker" <<'SH'
|
|
2270
|
+
#!/usr/bin/env bash
|
|
2271
|
+
sleep 30
|
|
2272
|
+
SH
|
|
2273
|
+
chmod +x "$blocker"
|
|
2274
|
+
start=$SECONDS
|
|
2275
|
+
LOG="$daemon_log" FM_WEDGE_ALARM_EXEC="$blocker" FM_WEDGE_ALARM_TIMEOUT_SECS=1 \
|
|
2276
|
+
FM_WEDGE_ALARM_CHANNEL=$'osascript\nherdr' \
|
|
2277
|
+
wedge_alarm_notify "away-mode WEDGED 900s" "/s/.marker"
|
|
2278
|
+
elapsed=$((SECONDS - start))
|
|
2279
|
+
[ "$elapsed" -lt 6 ] || fail "a hung wedge notifier override blocked the alarm for ${elapsed}s"
|
|
2280
|
+
grep -F 'osascript notifier timed out' "$daemon_log" >/dev/null \
|
|
2281
|
+
|| fail "a hung notifier override did not log its timeout: $(cat "$daemon_log" 2>/dev/null)"
|
|
2282
|
+
grep -F 'herdr notifier timed out' "$daemon_log" >/dev/null \
|
|
2283
|
+
|| fail "a hung notifier override prevented the next channel: $(cat "$daemon_log" 2>/dev/null)"
|
|
2284
|
+
pass "a hung notifier override is bounded, logged, and proceeds to the next channel"
|
|
2285
|
+
}
|
|
2286
|
+
|
|
2287
|
+
test_wedge_alarm_shutdown_stops_active_notifier_group() {
|
|
2288
|
+
local dir child_file pid child
|
|
2289
|
+
dir=$(make_wedge_case wedge-shutdown)
|
|
2290
|
+
child_file="$dir/notifier-child"
|
|
2291
|
+
(
|
|
2292
|
+
set -m
|
|
2293
|
+
sh -c 'sleep 30 & printf "%s" "$!" > "$1"; wait' sh "$child_file" &
|
|
2294
|
+
pid=$!
|
|
2295
|
+
while [ ! -s "$child_file" ]; do sleep 0.05; done
|
|
2296
|
+
child=$(cat "$child_file")
|
|
2297
|
+
WEDGE_ALARM_NOTIFIER_PID=$pid
|
|
2298
|
+
wedge_alarm_stop_active_notifier
|
|
2299
|
+
if kill -0 "$child" 2>/dev/null; then
|
|
2300
|
+
fail "shutdown left a notifier descendant running (pid $child)"
|
|
2301
|
+
fi
|
|
2302
|
+
) || fail "notifier shutdown cleanup helper failed"
|
|
2303
|
+
pass "daemon shutdown stops and reaps the active notifier process group"
|
|
2304
|
+
}
|
|
2305
|
+
|
|
2306
|
+
test_inject_wedge_alarm_fires_active_alert_on_non_tmux_backend() {
|
|
2307
|
+
# The whole incident: a non-tmux (herdr) primary gets NO tmux status-line
|
|
2308
|
+
# flash, so inject_wedge_alarm must still emit the backend-independent alert
|
|
2309
|
+
# alongside the durable marker.
|
|
2310
|
+
local dir state log
|
|
2311
|
+
dir=$(make_wedge_case wedge-integration); state="$dir/state"; log="$dir/alert.log"
|
|
2312
|
+
escalate_add "$state" "needs-decision: pick A"
|
|
2313
|
+
WEDGE_ALARM_LAST_EPOCH=0
|
|
2314
|
+
FM_WEDGE_ALARM_LOG="$log" FM_STATE_OVERRIDE="$state" \
|
|
2315
|
+
FM_WEDGE_ALARM_CHANNEL=osascript FM_SUPERVISOR_BACKEND=herdr \
|
|
2316
|
+
inject_wedge_alarm "$state" 30600
|
|
2317
|
+
[ -s "$state/.subsuper-inject-wedged" ] || fail "inject_wedge_alarm did not write the durable marker"
|
|
2318
|
+
grep -F 'osascript' "$log" >/dev/null || fail "inject_wedge_alarm did not emit the active alert on a non-tmux backend: $(cat "$log")"
|
|
2319
|
+
grep -F 'WEDGED 30600s' "$log" >/dev/null || fail "active alert missing the age and summary"
|
|
2320
|
+
pass "inject_wedge_alarm writes the marker AND emits the active alert even with no tmux status-line (herdr backend)"
|
|
2321
|
+
}
|
|
2322
|
+
|
|
2323
|
+
test_inject_wedge_alarm_throttles_when_marker_cannot_be_written() {
|
|
2324
|
+
local dir state log daemon_log alerts errors
|
|
2325
|
+
dir=$(make_wedge_case wedge-unwritable-marker)
|
|
2326
|
+
state="$dir/state"; log="$dir/alert.log"; daemon_log="$dir/daemon.log"
|
|
2327
|
+
escalate_add "$state" "needs-decision: pick A"
|
|
2328
|
+
chmod u-w "$state"
|
|
2329
|
+
WEDGE_ALARM_LAST_EPOCH=0
|
|
2330
|
+
LOG="$daemon_log" FM_WEDGE_ALARM_LOG="$log" FM_MAX_DEFER_SECS=600 \
|
|
2331
|
+
FM_WEDGE_ALARM_CHANNEL=osascript FM_SUPERVISOR_BACKEND=herdr \
|
|
2332
|
+
inject_wedge_alarm "$state" 30600
|
|
2333
|
+
LOG="$daemon_log" FM_WEDGE_ALARM_LOG="$log" FM_MAX_DEFER_SECS=600 \
|
|
2334
|
+
FM_WEDGE_ALARM_CHANNEL=osascript FM_SUPERVISOR_BACKEND=herdr \
|
|
2335
|
+
inject_wedge_alarm "$state" 30615
|
|
2336
|
+
chmod u+w "$state"
|
|
2337
|
+
[ ! -e "$state/.subsuper-inject-wedged" ] || fail "wedge marker unexpectedly persisted in an unwritable state directory"
|
|
2338
|
+
alerts=$(grep -c 'osascript' "$log" 2>/dev/null || true)
|
|
2339
|
+
[ "$alerts" -eq 1 ] || fail "unwritable marker emitted $alerts active alerts instead of one"
|
|
2340
|
+
errors=$(grep -c 'ERROR: away-mode escalation undelivered' "$daemon_log" 2>/dev/null || true)
|
|
2341
|
+
[ "$errors" -eq 1 ] || fail "unwritable marker logged $errors wedge errors instead of one"
|
|
2342
|
+
pass "in-process wedge throttle prevents alert spam when the marker cannot persist"
|
|
2343
|
+
}
|
|
2344
|
+
|
|
2345
|
+
test_fm_send_reports_delivered_unconfirmed_submit() {
|
|
2346
|
+
# When typed-plane text was typed and Enter sent but the submit read-back
|
|
2347
|
+
# remains pending, fm-send must return its documented delivered-unconfirmed status and prevent
|
|
2348
|
+
# a duplicate resend reflex. A synchronously confirmed submit remains zero.
|
|
2349
|
+
local dir fakebin err rc
|
|
2350
|
+
dir=$(make_bordered_case send-swallow)
|
|
2351
|
+
fakebin="$dir/fakebin"; err="$dir/send.err"
|
|
2352
|
+
# Clean submit -> exit 0.
|
|
2353
|
+
PATH="$fakebin:$PATH" FM_HOME="$dir" FM_STATE_OVERRIDE="$dir/state" FM_FAKE_COMPOSER="$dir/composer" \
|
|
2354
|
+
FM_SEND_SLEEP=0.05 "$ROOT/bin/fm-send.sh" sess:win 'route this work' >/dev/null 2>"$err" \
|
|
2355
|
+
|| fail "fm-send exited non-zero on a clean submit: $(cat "$err")"
|
|
2356
|
+
# Persistent composer text after Enter -> delivered-unconfirmed exit 3 with
|
|
2357
|
+
# a non-error warning that explicitly tells the operator not to resend.
|
|
2358
|
+
printf '╭─────╮\n│ > │\n╰─────╯\n' > "$dir/composer"
|
|
2359
|
+
touch "$dir/.swallow"
|
|
2360
|
+
if PATH="$fakebin:$PATH" FM_HOME="$dir" FM_STATE_OVERRIDE="$dir/state" FM_FAKE_COMPOSER="$dir/composer" \
|
|
2361
|
+
FM_FAKE_SWALLOW="$dir/.swallow" FM_FAKE_PERSIST_SWALLOW=1 FM_SEND_SLEEP=0.05 \
|
|
2362
|
+
"$ROOT/bin/fm-send.sh" sess:win 'fix findings 1 and 3, skip 2' >/dev/null 2>"$err"; then
|
|
2363
|
+
rc=0
|
|
2364
|
+
else
|
|
2365
|
+
rc=$?
|
|
2366
|
+
fi
|
|
2367
|
+
[ "$rc" -eq 3 ] || fail "fm-send returned $rc instead of delivered-unconfirmed exit 3: $(cat "$err")"
|
|
2368
|
+
grep -F 'submission is unconfirmed' "$err" >/dev/null \
|
|
2369
|
+
|| fail "fm-send did not explain the pending confirmation: $(cat "$err")"
|
|
2370
|
+
grep -F 'do not retype or blindly resend' "$err" >/dev/null \
|
|
2371
|
+
|| fail "fm-send did not prevent a duplicate resend: $(cat "$err")"
|
|
2372
|
+
if grep -F 'error:' "$err" >/dev/null; then
|
|
2373
|
+
fail "fm-send mislabeled delivered-unconfirmed as an error: $(cat "$err")"
|
|
2374
|
+
fi
|
|
2375
|
+
pass "fm-send returns 3 with a non-error no-resend warning when confirmation stays pending"
|
|
2376
|
+
}
|
|
2377
|
+
|
|
2378
|
+
test_fm_send_exits_nonzero_on_initial_send_failure() {
|
|
2379
|
+
local dir fakebin err
|
|
2380
|
+
dir=$(make_bordered_case send-type-failure)
|
|
2381
|
+
fakebin="$dir/fakebin"; err="$dir/send.err"
|
|
2382
|
+
if PATH="$fakebin:$PATH" FM_HOME="$dir" FM_STATE_OVERRIDE="$dir/state" FM_FAKE_COMPOSER="$dir/composer" \
|
|
2383
|
+
FM_FAKE_SEND_FAIL=1 FM_SEND_SLEEP=0.05 \
|
|
2384
|
+
"$ROOT/bin/fm-send.sh" sess:win 'route this work' >/dev/null 2>"$err"; then
|
|
2385
|
+
fail "fm-send exited zero despite initial tmux send-keys failure"
|
|
2386
|
+
fi
|
|
2387
|
+
grep -F 'text not sent' "$err" >/dev/null || fail "fm-send did not explain initial send failure: $(cat "$err")"
|
|
2388
|
+
pass "fm-send exits non-zero when initial text send fails"
|
|
2389
|
+
}
|
|
2390
|
+
|
|
2391
|
+
test_fm_send_exits_nonzero_on_unproven_submit() {
|
|
2392
|
+
local dir fakebin err
|
|
2393
|
+
dir=$(make_bordered_case send-unproven)
|
|
2394
|
+
fakebin="$dir/fakebin"; err="$dir/send.err"
|
|
2395
|
+
touch "$dir/.swallow"
|
|
2396
|
+
if PATH="$fakebin:$PATH" FM_HOME="$dir" FM_STATE_OVERRIDE="$dir/state" FM_FAKE_COMPOSER="$dir/composer" \
|
|
2397
|
+
FM_FAKE_SWALLOW="$dir/.swallow" FM_FAKE_PERSIST_SWALLOW=1 FM_SEND_SLEEP=0.05 \
|
|
2398
|
+
"$ROOT/bin/fm-send.sh" sess:win '修复' >/dev/null 2>"$err"; then
|
|
2399
|
+
fail "fm-send exited zero when submit proof remained pending-unproven"
|
|
2400
|
+
fi
|
|
2401
|
+
grep -F 'verdict=pending-unproven' "$err" >/dev/null \
|
|
2402
|
+
|| fail "fm-send did not preserve the unproven-submit verdict: $(cat "$err")"
|
|
2403
|
+
pass "fm-send exits non-zero unless delivery is proven empty"
|
|
2404
|
+
}
|
|
2405
|
+
|
|
2406
|
+
# --- herdr backend-awareness (fm-turnend-guard-h6-adjacent transport fix) ----
|
|
2407
|
+
# Discovery, busy/pending dispatch, and the full inject_msg guard chain must
|
|
2408
|
+
# work through the herdr backend, not just tmux. Env-var prefix assignments
|
|
2409
|
+
# (e.g. `TMUX_PANE= HERDR_ENV=1 ... discover_supervisor_target`) neutralize
|
|
2410
|
+
# whatever ambient TMUX_PANE/HERDR_ENV the CURRENT dev/CI shell happens to carry
|
|
2411
|
+
# for the duration of that one call only, so these tests are deterministic
|
|
2412
|
+
# regardless of what runtime backend is running this test suite itself.
|
|
2413
|
+
|
|
2414
|
+
test_discover_supervisor_backend_precedence() {
|
|
2415
|
+
local out
|
|
2416
|
+
out=$(FM_SUPERVISOR_BACKEND=herdr TMUX_PANE='%9' HERDR_ENV=1 HERDR_PANE_ID=w1:p1 discover_supervisor_backend)
|
|
2417
|
+
[ "$out" = herdr ] || fail "explicit FM_SUPERVISOR_BACKEND override was not honored: $out"
|
|
2418
|
+
|
|
2419
|
+
out=$(FM_SUPERVISOR_BACKEND='' TMUX_PANE='%9' HERDR_ENV=1 HERDR_PANE_ID=w1:p1 discover_supervisor_backend)
|
|
2420
|
+
[ "$out" = tmux ] || fail "TMUX_PANE should win over HERDR_ENV (tmux nested in herdr resolves to tmux): $out"
|
|
2421
|
+
|
|
2422
|
+
out=$(FM_SUPERVISOR_BACKEND='' TMUX_PANE='' HERDR_ENV=1 HERDR_PANE_ID=w1:p1 discover_supervisor_backend)
|
|
2423
|
+
[ "$out" = herdr ] || fail "HERDR_ENV=1 with HERDR_PANE_ID present should resolve to herdr: $out"
|
|
2424
|
+
|
|
2425
|
+
if out=$(FM_SUPERVISOR_BACKEND='' TMUX_PANE='' HERDR_ENV='' HERDR_PANE_ID='' discover_supervisor_backend); then
|
|
2426
|
+
fail "bare fallback (no override, no TMUX_PANE, no HERDR_ENV) should return non-zero"
|
|
2427
|
+
fi
|
|
2428
|
+
[ "$out" = tmux ] || fail "bare fallback should still print tmux: $out"
|
|
2429
|
+
|
|
2430
|
+
pass "discover_supervisor_backend: override > TMUX_PANE > HERDR_ENV+HERDR_PANE_ID > tmux fallback"
|
|
2431
|
+
}
|
|
2432
|
+
|
|
2433
|
+
test_discover_supervisor_target_herdr() {
|
|
2434
|
+
local out
|
|
2435
|
+
out=$(FM_SUPERVISOR_TARGET=explicit:target TMUX_PANE='' HERDR_ENV=1 HERDR_PANE_ID=w1:p9 discover_supervisor_target)
|
|
2436
|
+
[ "$out" = "explicit:target" ] || fail "explicit FM_SUPERVISOR_TARGET override was not honored: $out"
|
|
2437
|
+
|
|
2438
|
+
out=$(FM_SUPERVISOR_TARGET='' TMUX_PANE='%3' HERDR_ENV=1 HERDR_PANE_ID=w1:p9 discover_supervisor_target)
|
|
2439
|
+
[ "$out" = '%3' ] || fail "TMUX_PANE should win over herdr markers: $out"
|
|
2440
|
+
|
|
2441
|
+
out=$(FM_SUPERVISOR_TARGET='' TMUX_PANE='' HERDR_ENV=1 HERDR_PANE_ID=w1:p9 HERDR_SESSION='' discover_supervisor_target)
|
|
2442
|
+
[ "$out" = "default:w1:p9" ] || fail "herdr target should default HERDR_SESSION to 'default': $out"
|
|
2443
|
+
|
|
2444
|
+
out=$(FM_SUPERVISOR_TARGET='' TMUX_PANE='' HERDR_ENV=1 HERDR_PANE_ID=w1:p9 HERDR_SESSION=iso1 discover_supervisor_target)
|
|
2445
|
+
[ "$out" = "iso1:w1:p9" ] || fail "herdr target should use an explicit HERDR_SESSION: $out"
|
|
2446
|
+
|
|
2447
|
+
if out=$(FM_SUPERVISOR_TARGET='' TMUX_PANE='' HERDR_ENV='' HERDR_PANE_ID='' discover_supervisor_target); then
|
|
2448
|
+
fail "bare fallback should return non-zero"
|
|
2449
|
+
fi
|
|
2450
|
+
[ "$out" = "firstmate:0" ] || fail "bare fallback should still print firstmate:0: $out"
|
|
2451
|
+
|
|
2452
|
+
pass "discover_supervisor_target: override > TMUX_PANE > herdr '<session>:<pane-id>' composition > firstmate:0 fallback"
|
|
2453
|
+
}
|
|
2454
|
+
|
|
2455
|
+
test_pane_is_busy_herdr_native_busy_state() {
|
|
2456
|
+
local dir
|
|
2457
|
+
dir=$(make_supercase primary-herdr-busy)
|
|
2458
|
+
(
|
|
2459
|
+
fm_backend_busy_state() { [ "$1" = herdr ] && [ "$2" = "default:w1:p2" ] || fail "unexpected busy_state args: $1 $2"; printf 'busy'; }
|
|
2460
|
+
fm_backend_capture() { fail "capture should not be consulted when busy_state is conclusive"; }
|
|
2461
|
+
FM_STATE_OVERRIDE="$dir/state" FM_DAEMON_PRIMARY_HARNESS=claude pane_is_busy "default:w1:p2" herdr \
|
|
2462
|
+
|| fail "pane_is_busy should report busy from herdr's native busy_state"
|
|
2463
|
+
) || fail "herdr native-busy pane_is_busy subshell failed"
|
|
2464
|
+
pass "pane_is_busy: herdr native busy_state='busy' short-circuits without a capture fallback"
|
|
2465
|
+
}
|
|
2466
|
+
|
|
2467
|
+
test_primary_busy_guard_is_harness_scoped() {
|
|
2468
|
+
(
|
|
2469
|
+
fm_backend_busy_state() { printf 'unknown'; }
|
|
2470
|
+
fm_backend_capture() { printf 'esc interrupt\n'; }
|
|
2471
|
+
if FM_DAEMON_PRIMARY_HARNESS=claude pane_is_busy "default:w1:p2" herdr; then
|
|
2472
|
+
fail "OpenCode's rendered signature must not classify a Claude primary busy"
|
|
2473
|
+
fi
|
|
2474
|
+
FM_DAEMON_PRIMARY_HARNESS=opencode pane_is_busy "default:w1:p2" herdr \
|
|
2475
|
+
|| fail "OpenCode's rendered signature should classify an OpenCode primary busy"
|
|
2476
|
+
) || fail "harness-scoped primary busy guard subshell failed"
|
|
2477
|
+
pass "primary busy guard isolates rendered signatures by detected harness"
|
|
2478
|
+
}
|
|
2479
|
+
|
|
2480
|
+
test_pane_is_busy_defaults_to_tmux_when_backend_omitted() {
|
|
2481
|
+
local dir fakebin capture
|
|
2482
|
+
dir=$(make_supercase busy-default-backend)
|
|
2483
|
+
fakebin="$dir/fakebin"; capture="$dir/pane.txt"
|
|
2484
|
+
printf 'Ctrl+c:cancel\n' > "$capture"
|
|
2485
|
+
PATH="$fakebin:$PATH" FM_FAKE_TMUX_CAPTURE="$capture" FM_STATE_OVERRIDE="$dir/state" FM_DAEMON_PRIMARY_HARNESS=grok pane_is_busy "fakepane" \
|
|
2486
|
+
|| fail "pane_is_busy with no backend arg should still default to tmux"
|
|
2487
|
+
pass "pane_is_busy: omitted backend defaults to tmux for Grok's isolated fallback"
|
|
2488
|
+
}
|
|
2489
|
+
|
|
2490
|
+
test_pane_input_pending_herdr_dispatch() {
|
|
2491
|
+
(
|
|
2492
|
+
fm_backend_composer_state() { [ "$1" = herdr ] && [ "$2" = "default:w1:p2" ] || fail "unexpected composer_state args: $1 $2"; printf 'pending'; }
|
|
2493
|
+
pane_input_pending "default:w1:p2" herdr || fail "pane_input_pending should report pending from herdr composer_state"
|
|
2494
|
+
) || fail "herdr pane_input_pending (pending case) subshell failed"
|
|
2495
|
+
(
|
|
2496
|
+
fm_backend_composer_state() { printf 'empty'; }
|
|
2497
|
+
if pane_input_pending "default:w1:p2" herdr; then
|
|
2498
|
+
fail "pane_input_pending should report not-pending for an empty herdr composer"
|
|
2499
|
+
fi
|
|
2500
|
+
) || fail "herdr pane_input_pending (empty case) subshell failed"
|
|
2501
|
+
(
|
|
2502
|
+
fm_backend_composer_state() { printf 'future-state'; }
|
|
2503
|
+
pane_input_pending "default:w1:p2" herdr \
|
|
2504
|
+
|| fail "pane_input_pending should defer on an unrecognized composer state"
|
|
2505
|
+
) || fail "herdr pane_input_pending (future-state case) subshell failed"
|
|
2506
|
+
pass "pane_input_pending: dispatches through fm_backend_composer_state for backend=herdr"
|
|
2507
|
+
}
|
|
2508
|
+
|
|
2509
|
+
test_inject_msg_herdr_busy_guard_defers() {
|
|
2510
|
+
local dir state
|
|
2511
|
+
dir=$(make_supercase inject-herdr-busy)
|
|
2512
|
+
state="$dir/state"
|
|
2513
|
+
afk_enter "$state"
|
|
2514
|
+
(
|
|
2515
|
+
fm_backend_target_exists() { [ "$1" = herdr ] && [ "$2" = "default:w1:p2" ] || fail "unexpected target_exists args: $1 $2"; return 0; }
|
|
2516
|
+
pane_is_busy() { return 0; }
|
|
2517
|
+
fm_backend_composer_state() { fail "composer_state should not be consulted once the busy-guard already deferred"; }
|
|
2518
|
+
fm_backend_send_text_submit() { fail "send_text_submit should not run when the busy-guard defers"; }
|
|
2519
|
+
if FM_SUPERVISOR_BACKEND=herdr FM_SUPERVISOR_TARGET="default:w1:p2" inject_msg "hello" "$state"; then
|
|
2520
|
+
fail "inject_msg should defer (return non-zero) when the herdr supervisor pane is busy"
|
|
2521
|
+
fi
|
|
2522
|
+
) || fail "herdr busy-guard inject_msg subshell failed"
|
|
2523
|
+
pass "inject_msg: herdr busy-guard defers before ever attempting a submit"
|
|
2524
|
+
}
|
|
2525
|
+
|
|
2526
|
+
test_inject_msg_herdr_composer_guard_defers() {
|
|
2527
|
+
local dir state
|
|
2528
|
+
dir=$(make_supercase inject-herdr-pending)
|
|
2529
|
+
state="$dir/state"
|
|
2530
|
+
afk_enter "$state"
|
|
2531
|
+
(
|
|
2532
|
+
fm_backend_target_exists() { return 0; }
|
|
2533
|
+
pane_is_busy() { return 1; }
|
|
2534
|
+
fm_backend_composer_state() { [ "$1" = herdr ] && [ "$2" = "default:w1:p2" ] || fail "unexpected composer_state args: $1 $2"; printf 'pending'; }
|
|
2535
|
+
fm_backend_send_text_submit() { fail "send_text_submit should not run when the composer-guard defers"; }
|
|
2536
|
+
if FM_SUPERVISOR_BACKEND=herdr FM_SUPERVISOR_TARGET="default:w1:p2" inject_msg "hello" "$state"; then
|
|
2537
|
+
fail "inject_msg should defer when the herdr composer has pending input"
|
|
2538
|
+
fi
|
|
2539
|
+
) || fail "herdr composer-guard inject_msg subshell failed"
|
|
2540
|
+
pass "inject_msg: herdr composer-guard defers before ever attempting a submit"
|
|
2541
|
+
}
|
|
2542
|
+
|
|
2543
|
+
test_inject_msg_herdr_pane_gone_defers() {
|
|
2544
|
+
local dir state
|
|
2545
|
+
dir=$(make_supercase inject-herdr-gone)
|
|
2546
|
+
state="$dir/state"
|
|
2547
|
+
afk_enter "$state"
|
|
2548
|
+
(
|
|
2549
|
+
fm_backend_target_exists() { return 1; }
|
|
2550
|
+
pane_is_busy() { fail "busy guard should not be consulted once the pane-exists check already failed"; }
|
|
2551
|
+
fm_backend_send_text_submit() { fail "send_text_submit should not run when the pane does not exist"; }
|
|
2552
|
+
if FM_SUPERVISOR_BACKEND=herdr FM_SUPERVISOR_TARGET="default:w1:gone" inject_msg "hello" "$state"; then
|
|
2553
|
+
fail "inject_msg should defer when the herdr target does not exist"
|
|
2554
|
+
fi
|
|
2555
|
+
) || fail "herdr pane-gone inject_msg subshell failed"
|
|
2556
|
+
pass "inject_msg: herdr pane-gone check defers before any busy/composer/submit call"
|
|
2557
|
+
}
|
|
2558
|
+
|
|
2559
|
+
test_inject_msg_herdr_submits_through_backend_dispatch() {
|
|
2560
|
+
local dir state
|
|
2561
|
+
dir=$(make_supercase inject-herdr-submit)
|
|
2562
|
+
state="$dir/state"
|
|
2563
|
+
afk_enter "$state"
|
|
2564
|
+
(
|
|
2565
|
+
fm_backend_target_exists() { return 0; }
|
|
2566
|
+
pane_is_busy() { return 1; }
|
|
2567
|
+
fm_backend_composer_state() { printf 'empty'; }
|
|
2568
|
+
fm_backend_send_text_submit() {
|
|
2569
|
+
[ "$1" = herdr ] && [ "$2" = "default:w1:p2" ] || fail "unexpected send_text_submit args: $1 $2"
|
|
2570
|
+
case "$3" in *"hello"*) : ;; *) fail "digest text missing from send_text_submit: $3" ;; esac
|
|
2571
|
+
printf 'empty'
|
|
2572
|
+
}
|
|
2573
|
+
FM_SUPERVISOR_BACKEND=herdr FM_SUPERVISOR_TARGET="default:w1:p2" inject_msg "hello" "$state" \
|
|
2574
|
+
|| fail "inject_msg should succeed when send_text_submit confirms empty"
|
|
2575
|
+
) || fail "herdr successful-submit inject_msg subshell failed"
|
|
2576
|
+
pass "inject_msg: dispatches busy-guard/composer-guard/submit through the herdr backend and succeeds on a confirmed empty composer"
|
|
2577
|
+
}
|
|
2578
|
+
|
|
2579
|
+
# Safety-critical (task fm-composer-shellglyph-safety): the away-mode injector
|
|
2580
|
+
# must NEVER type an escalation into a dead-shell pane. A bare shell prompt
|
|
2581
|
+
# classifies `unknown` (not `pending`), and inject_msg now defers on anything
|
|
2582
|
+
# that is not affirmatively `empty`, so a dead shell (or an unreadable pane) can
|
|
2583
|
+
# never be mistaken for a safe empty agent composer and typed into.
|
|
2584
|
+
test_inject_msg_defers_on_dead_shell_unknown() {
|
|
2585
|
+
local dir state
|
|
2586
|
+
dir=$(make_supercase inject-dead-shell)
|
|
2587
|
+
state="$dir/state"
|
|
2588
|
+
afk_enter "$state"
|
|
2589
|
+
(
|
|
2590
|
+
fm_backend_target_exists() { return 0; }
|
|
2591
|
+
pane_is_busy() { return 1; }
|
|
2592
|
+
fm_backend_composer_state() { printf 'unknown'; }
|
|
2593
|
+
fm_backend_send_text_submit() { fail "send_text_submit must NOT run when the composer is a dead shell (unknown)"; }
|
|
2594
|
+
if FM_SUPERVISOR_BACKEND=herdr FM_SUPERVISOR_TARGET="default:w1:p2" inject_msg "hello" "$state"; then
|
|
2595
|
+
fail "inject_msg should defer (never inject) when the composer reads unknown (dead shell / unreadable)"
|
|
2596
|
+
fi
|
|
2597
|
+
) || fail "dead-shell inject_msg subshell failed"
|
|
2598
|
+
pass "inject_msg: defers on a dead-shell/unreadable composer (unknown), never typing the escalation into a shell"
|
|
2599
|
+
}
|
|
2600
|
+
|
|
2601
|
+
test_inject_msg_defers_on_unrecognized_composer_state() {
|
|
2602
|
+
local dir state
|
|
2603
|
+
dir=$(make_supercase inject-future-composer-state)
|
|
2604
|
+
state="$dir/state"
|
|
2605
|
+
afk_enter "$state"
|
|
2606
|
+
(
|
|
2607
|
+
fm_backend_target_exists() { return 0; }
|
|
2608
|
+
pane_is_busy() { return 1; }
|
|
2609
|
+
fm_backend_composer_state() { printf 'future-state'; }
|
|
2610
|
+
fm_backend_send_text_submit() { fail "send_text_submit must not run for an unrecognized composer state"; }
|
|
2611
|
+
if FM_SUPERVISOR_BACKEND=herdr FM_SUPERVISOR_TARGET="default:w1:p2" inject_msg "hello" "$state"; then
|
|
2612
|
+
fail "inject_msg should defer on an unrecognized composer state"
|
|
2613
|
+
fi
|
|
2614
|
+
) || fail "unrecognized composer-state inject_msg subshell failed"
|
|
2615
|
+
pass "inject_msg: unrecognized composer states defer by default"
|
|
2616
|
+
}
|
|
2617
|
+
|
|
2618
|
+
test_afk_start_refuses_when_flag_cannot_be_written
|
|
2619
|
+
test_afk_start_ignores_stale_pidfile_without_lock
|
|
2620
|
+
test_afk_start_reclaims_stale_daemon_lock_reused_pid
|
|
2621
|
+
test_daemon_state_root_uses_fm_home
|
|
2622
|
+
test_classify_routine_signal_self
|
|
2623
|
+
test_classify_terminal_signal_escalates
|
|
2624
|
+
test_classify_check_and_unknown_escalate
|
|
2625
|
+
test_stale_transient_self_records_marker
|
|
2626
|
+
test_stale_diagnostic_wedge_survives_busy_housekeeping
|
|
2627
|
+
test_enriched_wedge_under_declared_wait_uses_pause_cadence
|
|
2628
|
+
test_stale_terminal_escalates
|
|
2629
|
+
test_stale_actionable_wait_escalates_and_keeps_pause_cadence
|
|
2630
|
+
test_stale_paused_classifies_pause
|
|
2631
|
+
test_stale_captain_held_classifies_pause
|
|
2632
|
+
test_handle_wake_paused_records_pause_marker
|
|
2633
|
+
test_handle_wake_paused_signal_records_pause_marker
|
|
2634
|
+
test_handle_wake_terminal_signal_clears_pause_tracking
|
|
2635
|
+
test_housekeeping_migrates_watcher_pause_marker
|
|
2636
|
+
test_housekeeping_migrates_watcher_unpaused_marker_to_clear
|
|
2637
|
+
test_housekeeping_seeds_pause_marker_from_status
|
|
2638
|
+
test_housekeeping_persistent_stale_escalates
|
|
2639
|
+
test_housekeeping_resumed_stale_cleared
|
|
2640
|
+
test_housekeeping_paused_resurfaces_and_resets
|
|
2641
|
+
test_housekeeping_captain_held_resurfaces_and_resets
|
|
2642
|
+
test_housekeeping_paused_resumed_cleared
|
|
2643
|
+
test_housekeeping_busy_declared_wait_matures_its_window
|
|
2644
|
+
test_housekeeping_paused_unpaused_cleared
|
|
2645
|
+
test_housekeeping_captain_held_resolved_cleared
|
|
2646
|
+
test_housekeeping_stale_marker_transitions_to_pause
|
|
2647
|
+
test_housekeeping_captain_held_stale_marker_transitions_to_pause
|
|
2648
|
+
test_housekeeping_pause_marker_transitions_to_clear
|
|
2649
|
+
test_housekeeping_herdr_persistent_stale_resolves_meta
|
|
2650
|
+
test_housekeeping_herdr_idle_busy_record_clears_stale
|
|
2651
|
+
test_housekeeping_herdr_resumed_stale_cleared
|
|
2652
|
+
test_housekeeping_orca_persistent_stale_resolves_terminal
|
|
2653
|
+
test_escalate_batches_into_one_digest
|
|
2654
|
+
test_escalate_batch_age_uses_first_append
|
|
2655
|
+
test_heartbeat_scan_dedup
|
|
2656
|
+
test_handle_wake_routes_self_and_escalate
|
|
2657
|
+
test_inject_skip_forces_self
|
|
2658
|
+
test_is_wake_reason_distinguishes_status_stdout
|
|
2659
|
+
test_terminal_stale_escalate_leaves_no_marker
|
|
2660
|
+
test_signal_escalate_marks_seen_no_catchall_refire
|
|
2661
|
+
test_collapse_newlines_pure
|
|
2662
|
+
test_afk_absent_daemon_does_not_inject
|
|
2663
|
+
test_busy_guard_defers_when_supervisor_busy
|
|
2664
|
+
test_marker_detection
|
|
2665
|
+
test_afk_turn_exemption
|
|
2666
|
+
test_should_exit_afk_when_afk_inactive
|
|
2667
|
+
test_strip_injection_marker
|
|
2668
|
+
test_pane_input_pending_detects_partial_input
|
|
2669
|
+
test_pane_input_pending_blank_defers_strict
|
|
2670
|
+
test_pane_input_pending_requires_proven_empty_prompt
|
|
2671
|
+
test_tmux_composer_state_bare_shell_is_unknown
|
|
2672
|
+
test_tmux_composer_state_bordered_and_agent_rows_are_empty
|
|
2673
|
+
test_tmux_composer_state_requires_matching_box_borders
|
|
2674
|
+
test_pane_input_pending_preserves_bright_placeholder_like_draft
|
|
2675
|
+
test_classify_signal_dedup_against_scan
|
|
2676
|
+
test_classify_signal_skips_turn_end_markers
|
|
2677
|
+
test_classify_signal_survives_a_later_routine_append
|
|
2678
|
+
test_classification_commits_its_captured_endpoint
|
|
2679
|
+
test_stale_masked_event_escalates_at_captured_endpoint
|
|
2680
|
+
test_stale_read_failure_surfaces_without_advancing_seen
|
|
2681
|
+
test_recreated_status_rejects_captured_identity
|
|
2682
|
+
test_unverifiable_identity_surfaces_without_marker
|
|
2683
|
+
test_status_read_failure_surfaces_without_advancing_seen
|
|
2684
|
+
test_catchall_advances_routine_then_surfaces_append
|
|
2685
|
+
test_escalation_buffer_failure_retains_wake_and_position
|
|
2686
|
+
test_catchall_buffer_failure_preserves_position
|
|
2687
|
+
test_durable_wake_failure_retains_entire_batch
|
|
2688
|
+
test_missing_status_stale_is_acknowledged_without_diagnostic
|
|
2689
|
+
test_transient_unreadable_signal_recovers_without_advancing
|
|
2690
|
+
test_permission_recovery_reclassifies_catchall_status
|
|
2691
|
+
test_permanent_classification_failure_is_reported_and_acknowledged
|
|
2692
|
+
test_catchall_scan_surfaces_a_masked_event
|
|
2693
|
+
test_classify_stale_dedup_against_signal
|
|
2694
|
+
test_afk_nonterminal_working_merged_keeps_wedge_aging
|
|
2695
|
+
test_afk_genuine_done_still_terminal_stale
|
|
2696
|
+
test_pane_input_pending_bordered_idle_not_pending
|
|
2697
|
+
test_pane_input_pending_bordered_with_text_is_pending
|
|
2698
|
+
test_submit_ack_confirms_on_bordered_empty_composer
|
|
2699
|
+
test_submit_ack_reports_pending_on_persistent_swallow
|
|
2700
|
+
test_max_defer_empty_swallow_types_once_and_alarms
|
|
2701
|
+
test_max_defer_flushes_empty_idle_pane
|
|
2702
|
+
test_max_defer_pending_composer_alarms_without_typing
|
|
2703
|
+
test_normal_flush_clears_stale_wedge_marker
|
|
2704
|
+
test_below_max_defer_does_nothing
|
|
2705
|
+
test_max_defer_afk_inactive_does_not_flush_or_alarm
|
|
2706
|
+
test_wedge_alarm_library_mode_defaults_to_discard
|
|
2707
|
+
test_wake_helpers_replace_inherited_notifier_override
|
|
2708
|
+
test_wedge_alarm_discard_seam_fires_nothing
|
|
2709
|
+
test_wedge_alarm_direct_notifiers_honor_discard_seam
|
|
2710
|
+
test_wedge_alarm_osascript_channel_selected
|
|
2711
|
+
test_wedge_alarm_herdr_channel_selected
|
|
2712
|
+
test_wedge_alarm_command_channel_receives_summary
|
|
2713
|
+
test_wedge_alarm_command_failure_hides_configured_command
|
|
2714
|
+
test_wedge_alarm_unknown_channel_hides_configured_directive
|
|
2715
|
+
test_wedge_alarm_off_disables_active_alert_regardless_of_position
|
|
2716
|
+
test_wedge_alarm_auto_darwin_selects_osascript
|
|
2717
|
+
test_wedge_alarm_auto_non_darwin_has_no_os_channel
|
|
2718
|
+
test_wedge_alarm_config_file_multi_channel
|
|
2719
|
+
test_wedge_alarm_failing_channel_degrades_gracefully
|
|
2720
|
+
test_wedge_alarm_hung_channel_times_out_and_falls_through
|
|
2721
|
+
test_wedge_alarm_backgrounded_command_times_out_and_reaps_descendant
|
|
2722
|
+
test_wedge_alarm_hung_override_times_out_and_falls_through
|
|
2723
|
+
test_wedge_alarm_shutdown_stops_active_notifier_group
|
|
2724
|
+
test_inject_wedge_alarm_fires_active_alert_on_non_tmux_backend
|
|
2725
|
+
test_inject_wedge_alarm_throttles_when_marker_cannot_be_written
|
|
2726
|
+
test_fm_send_reports_delivered_unconfirmed_submit
|
|
2727
|
+
test_fm_send_exits_nonzero_on_initial_send_failure
|
|
2728
|
+
test_fm_send_exits_nonzero_on_unproven_submit
|
|
2729
|
+
test_discover_supervisor_backend_precedence
|
|
2730
|
+
test_discover_supervisor_target_herdr
|
|
2731
|
+
test_pane_is_busy_herdr_native_busy_state
|
|
2732
|
+
test_primary_busy_guard_is_harness_scoped
|
|
2733
|
+
test_pane_is_busy_defaults_to_tmux_when_backend_omitted
|
|
2734
|
+
test_pane_input_pending_herdr_dispatch
|
|
2735
|
+
test_inject_msg_herdr_busy_guard_defers
|
|
2736
|
+
test_inject_msg_herdr_composer_guard_defers
|
|
2737
|
+
test_inject_msg_herdr_pane_gone_defers
|
|
2738
|
+
test_inject_msg_herdr_submits_through_backend_dispatch
|
|
2739
|
+
test_inject_msg_defers_on_dead_shell_unknown
|
|
2740
|
+
test_inject_msg_defers_on_unrecognized_composer_state
|