@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,1732 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# fm-supervise-daemon.sh — presence-gated sub-supervisor (closes #27's P2).
|
|
3
|
+
#
|
|
4
|
+
# Wraps bin/fm-watch.sh: runs it as a child, presents and classifies every
|
|
5
|
+
# durable wake after an actionable close, acknowledges only after routing, and
|
|
6
|
+
# either SELF-HANDLES the routine majority in bash (no firstmate turn) or
|
|
7
|
+
# ESCALATES a batched, distilled digest to the supervisor pane on
|
|
8
|
+
# captain-relevant events plus bounded declared-wait rechecks. This is the
|
|
9
|
+
# token-efficient replacement for the prior always-inject daemon: routine
|
|
10
|
+
# signal/stale/heartbeat wakes cost zero firstmate context; only done/
|
|
11
|
+
# needs-decision/blocked/failed/persistent-wedge/check-output events and a
|
|
12
|
+
# declared-wait recheck reach the LLM, and even then as one pre-read digest per
|
|
13
|
+
# batch window.
|
|
14
|
+
#
|
|
15
|
+
# PRESENCE-GATING (the /afk contract). The daemon is the away-mode engine: it
|
|
16
|
+
# injects ONLY when the durable away-mode flag state/.afk is present. Invoking
|
|
17
|
+
# the /afk skill sets that flag and starts this daemon; any real (unmarked)
|
|
18
|
+
# user message clears it and firstmate resumes full responsiveness.
|
|
19
|
+
# When afk is off, normal fm-watch.sh always-on triage is the active mechanism.
|
|
20
|
+
# Any buffered daemon escalations that remain while afk is off survive in
|
|
21
|
+
# state/.subsuper-escalations and are flushed on the next "while you were out"
|
|
22
|
+
# catch-up or when afk is re-entered.
|
|
23
|
+
#
|
|
24
|
+
# IN-BAND OPERATIONAL INPUT. bin/fm-operational-input.sh constructs every
|
|
25
|
+
# current daemon injection as the typed away-supervisor kind after the stable
|
|
26
|
+
# FM_OPERATIONAL_PREFIX. A human cannot type its leading U+2063 from a normal
|
|
27
|
+
# keyboard at the start of a message, and Herdr transports it as text.
|
|
28
|
+
# Firstmate's contract: a message that starts with the current prefix, or a
|
|
29
|
+
# legacy bare-marker daemon escalation, is internal (stay afk); an unmarked
|
|
30
|
+
# message means the captain is back (exit afk, flush catch-up, resume per-wake
|
|
31
|
+
# responsiveness). The prefix and busy-guard solve the same problem - the
|
|
32
|
+
# daemon and the human share one input channel - so they live together under
|
|
33
|
+
# /afk.
|
|
34
|
+
#
|
|
35
|
+
# Reliability model (see the /afk skill):
|
|
36
|
+
# - Nothing is lost in away mode: while state/.afk exists, the watcher reverts
|
|
37
|
+
# to daemon-owned one-shot behavior and enqueues every wake to
|
|
38
|
+
# state/.wake-queue BEFORE advancing its suppression markers, so a
|
|
39
|
+
# crash/restart/missed injection is recovered on the next fm-wake-drain.sh.
|
|
40
|
+
# After a watcher cycle, the daemon handles every durable row through that
|
|
41
|
+
# drain and acknowledges it only after routing completes.
|
|
42
|
+
# - Fail-safe-to-escalate: any wake the classifier cannot confidently mark
|
|
43
|
+
# routine is escalated.
|
|
44
|
+
# - Bounded wedge latency: a stale pane without a declared wait is escalated
|
|
45
|
+
# only after it has been idle for STALE_ESCALATE_SECS
|
|
46
|
+
# (configurable), rechecked once. A wedged crewmate is therefore detected
|
|
47
|
+
# within STALE_ESCALATE_SECS + a tick, never lost. A declared wait - either a
|
|
48
|
+
# paused: external wait or a verified captain-held transfer, per
|
|
49
|
+
# fm-classify-lib.sh's combined predicate - instead gets its own longer
|
|
50
|
+
# PAUSE_RESURFACE_SECS recheck, never a wedge escalation, whether its pane
|
|
51
|
+
# reads idle or busy; only a status append that stops declaring the wait
|
|
52
|
+
# ends that routing.
|
|
53
|
+
# Crewmates are autonomous, so a delayed stale response does not stall a
|
|
54
|
+
# healthy crewmate's own progress.
|
|
55
|
+
# Buffered escalation delivery also has a max-defer alarm: if a digest stays
|
|
56
|
+
# undelivered past FM_MAX_DEFER_SECS, the daemon retries a normal flush and
|
|
57
|
+
# writes state/.subsuper-inject-wedged and attempts a configurable active
|
|
58
|
+
# alert if submit still cannot be confirmed.
|
|
59
|
+
# - Cheap heartbeat catch-all: every HEARTBEAT_SCAN_SECS the daemon greps all
|
|
60
|
+
# state/*.status for a captain-relevant line the per-wake classifier might
|
|
61
|
+
# have missed (e.g. a status verb outside CAPTAIN_RE) and escalates it.
|
|
62
|
+
#
|
|
63
|
+
# The robustness shell from the prior always-inject version is preserved:
|
|
64
|
+
# single-instance lock (portable helper, no flock dependency), crash-loop
|
|
65
|
+
# backoff, pane-gone guard, and a signal-trapped shutdown that flushes buffered
|
|
66
|
+
# escalations before exit.
|
|
67
|
+
#
|
|
68
|
+
# Usage: fm-supervise-daemon.sh
|
|
69
|
+
# Long-lived background loop. Normally started by the /afk skill, which
|
|
70
|
+
# sets state/.afk first. Env knobs:
|
|
71
|
+
# FM_SUPERVISOR_TARGET supervisor pane target (override; otherwise
|
|
72
|
+
# auto-discovered per backend - $TMUX_PANE
|
|
73
|
+
# under tmux, "<session>:<pane-id>" from
|
|
74
|
+
# $HERDR_PANE_ID under herdr - then
|
|
75
|
+
# firstmate:0 fallback). Accepts either a
|
|
76
|
+
# tmux target or a herdr "<session>:<pane-id>"
|
|
77
|
+
# target; which one it's read as is decided by
|
|
78
|
+
# FM_SUPERVISOR_BACKEND (below), independently.
|
|
79
|
+
# FM_SUPERVISOR_BACKEND supervisor pane BACKEND (tmux|herdr;
|
|
80
|
+
# override; otherwise auto-discovered the same
|
|
81
|
+
# way bin/fm-backend.sh's fm_backend_detect
|
|
82
|
+
# resolves the runtime firstmate itself is
|
|
83
|
+
# executing inside - $TMUX_PANE selects tmux,
|
|
84
|
+
# $HERDR_ENV=1 selects herdr - falling back to
|
|
85
|
+
# tmux). zellij, orca, and cmux are not yet
|
|
86
|
+
# supported as supervisor backends; the daemon
|
|
87
|
+
# refuses loudly at startup rather than trying
|
|
88
|
+
# tmux primitives against a non-tmux pane.
|
|
89
|
+
# FM_INJECT_SKIP |-prefixes force-self-handle bypassing
|
|
90
|
+
# classification (default "heartbeat"); empty
|
|
91
|
+
# disables. Use sparingly: it overrides the
|
|
92
|
+
# captain-relevant escalation for matching
|
|
93
|
+
# kinds.
|
|
94
|
+
# FM_STALE_ESCALATE_SECS idle seconds before a stale pane escalates
|
|
95
|
+
# as a possible wedge (default 240)
|
|
96
|
+
# FM_PAUSE_RESURFACE_SECS seconds a declared wait (external or
|
|
97
|
+
# captain-held) stays declared, idle or busy,
|
|
98
|
+
# before it re-surfaces as a recheck
|
|
99
|
+
# (default 3600)
|
|
100
|
+
# FM_ESCALATE_BATCH_SECS buffer window for batched escalation
|
|
101
|
+
# digests; 0 = flush immediately (default 90)
|
|
102
|
+
# FM_HEARTBEAT_SCAN_SECS cadence for the catch-all status scan
|
|
103
|
+
# (default 300)
|
|
104
|
+
# FM_HOUSEKEEPING_TICK seconds between housekeeping passes while
|
|
105
|
+
# the watcher is mid-cycle (default 15)
|
|
106
|
+
# FM_BUSY_REGEX optional rendered busy-signature override
|
|
107
|
+
# for delivery guards and Grok's fallback
|
|
108
|
+
# FM_COMPOSER_IDLE_RE optional shared classifier override; see
|
|
109
|
+
# docs/configuration.md for its safety gates
|
|
110
|
+
# FM_MAX_DEFER_SECS max seconds a buffered escalation may sit
|
|
111
|
+
# undelivered before one normal flush attempt;
|
|
112
|
+
# if that cannot confirm a submit, a wedge
|
|
113
|
+
# alarm fires (default 300; 0 disables)
|
|
114
|
+
# FM_WEDGE_ALARM_CHANNEL override config/wedge-alarm with a single
|
|
115
|
+
# active-alert directive for that wedge alarm
|
|
116
|
+
# (off|auto|osascript|herdr|command:<cmd>). An
|
|
117
|
+
# absent file/var means auto: on macOS that is
|
|
118
|
+
# an OS-level notification, so the alarm is
|
|
119
|
+
# never silent. See wedge_alarm_notify below
|
|
120
|
+
# and docs/configuration.md.
|
|
121
|
+
# FM_WEDGE_ALARM_EXEC notifier seam: when set, every notifier
|
|
122
|
+
# channel routes through this command as
|
|
123
|
+
# `<cmd> <channel> <summary>` instead of
|
|
124
|
+
# invoking its real notifier; "discard" fires
|
|
125
|
+
# nothing. Unset in production. When SOURCED the
|
|
126
|
+
# daemon defaults this to "discard" so no test
|
|
127
|
+
# can post a real notification (wedge_alarm_emit
|
|
128
|
+
# and the library-mode guard at the foot).
|
|
129
|
+
# FM_WEDGE_ALARM_TIMEOUT_SECS seconds allowed for each notifier before
|
|
130
|
+
# its watchdog terminates it and continues to the
|
|
131
|
+
# next channel (default 10; invalid/zero uses the
|
|
132
|
+
# default).
|
|
133
|
+
# FM_INJECT_CONFIRM_RETRIES Enter-retry attempts on a swallowed Enter
|
|
134
|
+
# (default 3); the digest is typed once, only
|
|
135
|
+
# Enter is retried. Composer-empty detection is
|
|
136
|
+
# structural and style-aware (bin/fm-tmux-lib.sh):
|
|
137
|
+
# it drops dim/faint ghost text and strips the
|
|
138
|
+
# harness's box borders before deciding, so a
|
|
139
|
+
# ghost-only or bordered-but-empty composer is
|
|
140
|
+
# not misread as pending input.
|
|
141
|
+
# FM_INJECT_CONFIRM_SLEEP seconds between daemon submit checks
|
|
142
|
+
# (default 0.5)
|
|
143
|
+
# FM_LOG_MAX_BYTES / FM_LOG_KEEP_LINES / FM_CRASH_* log + crash guards
|
|
144
|
+
# FM_STATE_OVERRIDE alternate state dir (testing)
|
|
145
|
+
# Logs each wake to state/.supervise-daemon.log (size-capped). Single
|
|
146
|
+
# instance via portable lock on state/.supervise-daemon.lock. Trapped
|
|
147
|
+
# SIGTERM/SIGINT shut down within ~1s, flush escalations, release the
|
|
148
|
+
# lock. A crashing fm-watch.sh is logged and restarted, never killing
|
|
149
|
+
# the daemon; a tight crash-restart spin is detected and backed off.
|
|
150
|
+
set -u
|
|
151
|
+
|
|
152
|
+
FM_DAEMON_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
153
|
+
FM_ROOT="${FM_ROOT_OVERRIDE:-$(cd "$FM_DAEMON_DIR/.." && pwd)}"
|
|
154
|
+
FM_HOME="${FM_HOME:-${FM_ROOT_OVERRIDE:-$FM_ROOT}}"
|
|
155
|
+
|
|
156
|
+
# Shared tmux pane primitives for supervisor injection (busy/composer detection
|
|
157
|
+
# + verify-retry submit). Sourced at top level so BOTH the executed daemon and
|
|
158
|
+
# the unit tests (which source this file for its pure functions) get the
|
|
159
|
+
# corrected composer detection. Stale task rechecks use fm-backend.sh below.
|
|
160
|
+
# shellcheck source=bin/fm-tmux-lib.sh
|
|
161
|
+
. "$FM_DAEMON_DIR/fm-tmux-lib.sh"
|
|
162
|
+
|
|
163
|
+
# shellcheck source=bin/fm-backend.sh
|
|
164
|
+
. "$FM_DAEMON_DIR/fm-backend.sh"
|
|
165
|
+
|
|
166
|
+
# Canonical construction and parsing for every Firstmate operational input.
|
|
167
|
+
# shellcheck source=bin/fm-operational-input.sh
|
|
168
|
+
. "$FM_DAEMON_DIR/fm-operational-input.sh"
|
|
169
|
+
|
|
170
|
+
# Shared wake classifier (last_status_line, status_is_captain_relevant,
|
|
171
|
+
# window_to_task, and the status-span reader). The SAME library backs the
|
|
172
|
+
# always-on watcher's triage, so the captain-relevant verb set and the
|
|
173
|
+
# classification predicates have exactly one definition.
|
|
174
|
+
# shellcheck source=bin/fm-classify-lib.sh
|
|
175
|
+
. "$FM_DAEMON_DIR/fm-classify-lib.sh"
|
|
176
|
+
|
|
177
|
+
# Supervisor-pane discovery (FM_SUPERVISOR_TARGET_DEFAULT,
|
|
178
|
+
# FM_SUPERVISOR_BACKEND_DEFAULT, discover_supervisor_target,
|
|
179
|
+
# discover_supervisor_backend). Shared with the script-owned away launcher
|
|
180
|
+
# (bin/fm-afk-launch.sh) so the captain-pane resolution has exactly one owner.
|
|
181
|
+
# shellcheck source=bin/fm-supervisor-target-lib.sh
|
|
182
|
+
. "$FM_DAEMON_DIR/fm-supervisor-target-lib.sh"
|
|
183
|
+
|
|
184
|
+
# The single owner of semantic busy state for recorded tasks
|
|
185
|
+
# (fm_busy_classify).
|
|
186
|
+
# shellcheck source=bin/fm-busy-lib.sh
|
|
187
|
+
. "$FM_DAEMON_DIR/fm-busy-lib.sh"
|
|
188
|
+
|
|
189
|
+
# --- tunables ---------------------------------------------------------------
|
|
190
|
+
# Supervisor backends this daemon knows how to inject into today. zellij, orca,
|
|
191
|
+
# and cmux are real backends elsewhere in firstmate (bin/fm-backend.sh) but this
|
|
192
|
+
# daemon has no verified composer/busy primitives wired up for them yet - see
|
|
193
|
+
# docs/herdr-backend.md and AGENTS.md section 4's
|
|
194
|
+
# harness-verification discipline. Selecting one refuses loudly at startup
|
|
195
|
+
# instead of silently running tmux primitives against a pane that is not a tmux
|
|
196
|
+
# pane.
|
|
197
|
+
FM_SUPERVISOR_SUPPORTED_BACKENDS="tmux herdr"
|
|
198
|
+
INJECT_SKIP_DEFAULT="heartbeat"
|
|
199
|
+
STALE_ESCALATE_SECS_DEFAULT=240
|
|
200
|
+
ESCALATE_BATCH_SECS_DEFAULT=90
|
|
201
|
+
HEARTBEAT_SCAN_SECS_DEFAULT=300
|
|
202
|
+
HOUSEKEEPING_TICK_DEFAULT=15
|
|
203
|
+
# Max time a buffered escalation may sit undelivered before the daemon retries
|
|
204
|
+
# the normal flush path and, if that cannot confirm a submit, raises a loud wedge
|
|
205
|
+
# alarm. The escape hatch makes a guard false-positive visible instead of silent.
|
|
206
|
+
MAX_DEFER_SECS_DEFAULT=300
|
|
207
|
+
WEDGE_ALARM_TIMEOUT_SECS_DEFAULT=10
|
|
208
|
+
WEDGE_ALARM_LAST_EPOCH=0
|
|
209
|
+
WEDGE_ALARM_NOTIFIER_PID=
|
|
210
|
+
# The captain-relevant verb set and the status classifiers (last_status_line,
|
|
211
|
+
# status_is_captain_relevant, window_to_task, and the status-span reader) now
|
|
212
|
+
# live in bin/fm-classify-lib.sh, shared with the always-on watcher.
|
|
213
|
+
# Composer-empty detection, submit acknowledgement, and the harness-scoped
|
|
214
|
+
# supervisor-pane busy guard live in bin/fm-tmux-lib.sh.
|
|
215
|
+
# FM_BUSY_REGEX also overrides Grok's isolated task-state fallback.
|
|
216
|
+
INJECT_FAIL_SLEEP_DEFAULT=30
|
|
217
|
+
INJECT_CONFIRM_RETRIES_DEFAULT=3
|
|
218
|
+
INJECT_CONFIRM_SLEEP_DEFAULT=0.5
|
|
219
|
+
CRASH_THRESHOLD_DEFAULT=10
|
|
220
|
+
CRASH_WINDOW_DEFAULT=60
|
|
221
|
+
CRASH_BACKOFF_DEFAULT=60
|
|
222
|
+
CRASH_NORMAL_SLEEP_DEFAULT=5
|
|
223
|
+
LOG_MAX_BYTES_DEFAULT=1048576
|
|
224
|
+
LOG_KEEP_LINES_DEFAULT=2000
|
|
225
|
+
|
|
226
|
+
# --- presence-gating --------------------------------------------------------
|
|
227
|
+
# bin/fm-operational-input.sh owns the U+2063 FIRSTMATE_OP bytes and typed
|
|
228
|
+
# away-supervisor construction. The away-exit predicate intentionally retains
|
|
229
|
+
# its landed leading-U+2063 compatibility behavior.
|
|
230
|
+
AFK_FLAG_NAME=".afk"
|
|
231
|
+
|
|
232
|
+
# Resolve the effective state dir. FM_STATE_OVERRIDE wins (testing); otherwise
|
|
233
|
+
# $FM_HOME/state. Kept as a function so the pure
|
|
234
|
+
# classifiers can take an explicit state arg without depending on globals.
|
|
235
|
+
_state_root() { printf '%s' "${FM_STATE_OVERRIDE:-$FM_HOME/state}"; }
|
|
236
|
+
|
|
237
|
+
# --- portable stat (same trap as fm-watch.sh: no `stat -f || stat -c`) -------
|
|
238
|
+
if [ "$(uname)" = Darwin ]; then
|
|
239
|
+
_stat_file_mtime() { stat -f %m "$1" 2>/dev/null; }
|
|
240
|
+
else
|
|
241
|
+
_stat_file_mtime() { stat -c %Y "$1" 2>/dev/null; }
|
|
242
|
+
fi
|
|
243
|
+
_now() { date +%s; }
|
|
244
|
+
_file_age() { # seconds since mtime; very large if missing
|
|
245
|
+
local f=$1 m
|
|
246
|
+
m=$(_stat_file_mtime "$f") || { echo 999999; return; }
|
|
247
|
+
echo $(( $(_now) - m ))
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
_hash_text() {
|
|
251
|
+
if command -v md5 >/dev/null 2>&1; then printf '%s' "$1" | md5 -q
|
|
252
|
+
else printf '%s' "$1" | md5sum | cut -d ' ' -f1; fi
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
# --- presence-gating helpers (PURE-ish: side-effect-free reads of state) -----
|
|
256
|
+
# afk_active: 0 if the durable away-mode flag exists, 1 otherwise.
|
|
257
|
+
afk_active() { # <state>
|
|
258
|
+
[ -e "$1/$AFK_FLAG_NAME" ]
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
# afk_enter / afk_exit: write/clear the away-mode flag. Called by the /afk
|
|
262
|
+
# skill (enter) and by firstmate on user return (exit). Durable: a plain file,
|
|
263
|
+
# so recovery (§5) re-enters afk if it is present after a restart.
|
|
264
|
+
afk_enter() { # <state>
|
|
265
|
+
mkdir -p "$1"
|
|
266
|
+
date '+%s' > "$1/$AFK_FLAG_NAME"
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
afk_exit() { # <state>
|
|
270
|
+
rm -f "$1/$AFK_FLAG_NAME"
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
# should_exit_afk: encodes firstmate's afk-exit contract as a testable function.
|
|
274
|
+
# afk inactive -> 1 (nothing to exit)
|
|
275
|
+
# message has marker -> 1 (internal escalation; stay afk)
|
|
276
|
+
# message is /afk command -> 1 (re-entering/extending afk; stay afk)
|
|
277
|
+
# anything else -> 0 (captain is back; exit afk)
|
|
278
|
+
# Bias toward exit: only the marker and an explicit /afk invocation keep afk
|
|
279
|
+
# alive. A false exit is self-correcting (the captain re-runs /afk).
|
|
280
|
+
should_exit_afk() { # <state> <message-text>
|
|
281
|
+
local state=$1 msg=$2
|
|
282
|
+
afk_active "$state" || return 1
|
|
283
|
+
message_is_injection "$msg" && return 1
|
|
284
|
+
case "$msg" in
|
|
285
|
+
/afk*) return 1 ;;
|
|
286
|
+
esac
|
|
287
|
+
return 0
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
# message_is_injection: 0 if the given message text starts with the sentinel
|
|
291
|
+
# marker (a daemon escalation), 1 otherwise (a real user message). Firstmate's
|
|
292
|
+
# afk-exit contract uses this: marker present -> stay afk; absent -> captain is
|
|
293
|
+
# back. Bias ambiguous cases toward exit (a false exit is self-correcting).
|
|
294
|
+
message_is_injection() { # <message-text>
|
|
295
|
+
local msg=$1
|
|
296
|
+
[ -n "$msg" ] || return 1
|
|
297
|
+
case "$msg" in
|
|
298
|
+
"$FM_INJECT_MARK"*) return 0 ;;
|
|
299
|
+
esac
|
|
300
|
+
return 1
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
# strip_injection_marker: remove a current typed away envelope, the landed
|
|
304
|
+
# untyped FIRSTMATE_OP prefix, or the legacy bare sentinel. Current grammar is
|
|
305
|
+
# delegated to its owner rather than reimplemented here.
|
|
306
|
+
strip_injection_marker() { # <message-text>
|
|
307
|
+
local msg=$1 body
|
|
308
|
+
if fm_operational_input_body "$msg" body; then
|
|
309
|
+
printf '%s' "$body"
|
|
310
|
+
return
|
|
311
|
+
fi
|
|
312
|
+
case "$msg" in
|
|
313
|
+
"$FM_OPERATIONAL_PREFIX"*) msg=${msg#"$FM_OPERATIONAL_PREFIX"} ;;
|
|
314
|
+
"$FM_INJECT_MARK"*) msg=${msg#"$FM_INJECT_MARK"} ;;
|
|
315
|
+
esac
|
|
316
|
+
printf '%s' "$msg"
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
# Collapse all newlines to a literal " - " separator so the injected digest is
|
|
320
|
+
# a single line. Submission via send-keys + Enter is then unambiguous regardless
|
|
321
|
+
# of how the target TUI handles embedded newlines in its composer.
|
|
322
|
+
_collapse_newlines() { # <text>
|
|
323
|
+
local s=$1
|
|
324
|
+
s=${s//$'\n'/ - }
|
|
325
|
+
printf '%s' "$s"
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
# discover_supervisor_target / discover_supervisor_backend are owned by
|
|
329
|
+
# bin/fm-supervisor-target-lib.sh (sourced above). fm_super_main below calls
|
|
330
|
+
# them exactly as before; the away launcher reuses the identical resolution to
|
|
331
|
+
# pass the captain pane in as FM_SUPERVISOR_TARGET.
|
|
332
|
+
|
|
333
|
+
# --- classification helpers (PURE: no side effects, testable) ---------------
|
|
334
|
+
# last_status_line, status_is_captain_relevant, window_to_task, and the
|
|
335
|
+
# status-span reader come from bin/fm-classify-lib.sh (sourced above),
|
|
336
|
+
# the single classifier shared with bin/fm-watch.sh. The decision-string wrappers
|
|
337
|
+
# and dedup state below layer the daemon's escalation-digest concerns on top.
|
|
338
|
+
#
|
|
339
|
+
# Decision protocol: every classifier prints exactly one line on stdout of the
|
|
340
|
+
# form "<action>|<distilled>" where action is "self" or "escalate". The distilled
|
|
341
|
+
# field for "self" is informational (logged); for "escalate" it is the pre-read
|
|
342
|
+
# summary firstmate would otherwise have to re-read.
|
|
343
|
+
|
|
344
|
+
classify_signal() { # <reason-after-colon> <state>
|
|
345
|
+
local reason=$1 state=$2 f last event record rest endpoint ident rc distilled="" rel="" seen_rel="" task sig marker
|
|
346
|
+
for f in $reason; do
|
|
347
|
+
case "$f" in *.status) ;; *) continue ;; esac
|
|
348
|
+
[ -e "$f" ] || [ -L "$f" ] || continue
|
|
349
|
+
task=$(basename "$f"); task="${task%.status}"
|
|
350
|
+
record=$(status_span_first_actionable_record "$f" \
|
|
351
|
+
"$(status_seen_offset "$state" "$task")")
|
|
352
|
+
rc=$?
|
|
353
|
+
[ "$rc" -eq 1 ] && [ -z "$record" ] && continue
|
|
354
|
+
if [ "$rc" -eq 2 ]; then
|
|
355
|
+
sig=$(status_observed_signature "$f")
|
|
356
|
+
marker=$(_seen_status_path "$state" "$task")
|
|
357
|
+
status_presentation_marker_reported_matches "$marker" "$sig" && continue
|
|
358
|
+
distilled="${distilled}$(basename "$f"): unreadable status span | "
|
|
359
|
+
[ -n "${FM_STATUS_SPAN_ENDPOINT_FILE:-}" ] \
|
|
360
|
+
&& printf 'ERROR\t%s\t%s\n' "$task" "$sig" >> "$FM_STATUS_SPAN_ENDPOINT_FILE"
|
|
361
|
+
rel=1
|
|
362
|
+
continue
|
|
363
|
+
fi
|
|
364
|
+
endpoint=${record%%$'\t'*}
|
|
365
|
+
rest=${record#*$'\t'}; ident=${rest%%$'\t'*}
|
|
366
|
+
[ -n "${FM_STATUS_SPAN_ENDPOINT_FILE:-}" ] \
|
|
367
|
+
&& printf '%s\t%s\t%s\n' "$task" "$endpoint" "$ident" >> "$FM_STATUS_SPAN_ENDPOINT_FILE"
|
|
368
|
+
if [ "$rc" -eq 0 ]; then
|
|
369
|
+
event=${rest#*$'\t'}
|
|
370
|
+
distilled="${distilled}$(basename "$f"): ${event} | "
|
|
371
|
+
rel=1
|
|
372
|
+
continue
|
|
373
|
+
fi
|
|
374
|
+
last=$(last_status_line "$f")
|
|
375
|
+
[ -n "$last" ] || continue
|
|
376
|
+
distilled="${distilled}$(basename "$f"): ${last} | "
|
|
377
|
+
# Nothing captain-relevant is left ahead of the recorded offset. When the log
|
|
378
|
+
# nonetheless ends on a captain-relevant line, this signal is a re-notification
|
|
379
|
+
# of something already escalated, not a routine one; position is the whole
|
|
380
|
+
# dedupe, so no separate seen-marker comparison is needed.
|
|
381
|
+
status_is_captain_relevant "$last" && seen_rel=1
|
|
382
|
+
done
|
|
383
|
+
# strip a trailing " | " separator so the distilled line is clean
|
|
384
|
+
distilled="${distilled% | }"
|
|
385
|
+
if [ -n "$rel" ]; then
|
|
386
|
+
printf 'escalate|%s' "$distilled"
|
|
387
|
+
elif [ -n "$seen_rel" ]; then
|
|
388
|
+
# Already escalated by the per-wake path or the catch-all scan; self-handle
|
|
389
|
+
# to avoid a duplicate entry in the digest.
|
|
390
|
+
printf 'self|signal already escalated (catch-all scan): %s' "$distilled"
|
|
391
|
+
else
|
|
392
|
+
printf 'self|routine signal: %s' "$distilled"
|
|
393
|
+
fi
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
# classify_stale decides the WAKE itself (one-shot per distinct hash). On a
|
|
397
|
+
# first sight of a non-terminal stale it returns "self" and the caller records a
|
|
398
|
+
# timestamp marker; persistence is escalated by housekeeping's recheck, not here.
|
|
399
|
+
classify_stale() { # <window> <state> [<span-record> <span-status>]
|
|
400
|
+
local win=$1 state=$2 record=${3-} rc=${4-} task last event rest
|
|
401
|
+
task=$(window_to_task "$win" "$state")
|
|
402
|
+
if [ -z "$rc" ]; then
|
|
403
|
+
record=$(status_span_first_actionable_record "$state/$task.status" \
|
|
404
|
+
"$(status_seen_offset "$state" "$task")")
|
|
405
|
+
rc=$?
|
|
406
|
+
fi
|
|
407
|
+
last=$(last_status_line "$state/$task.status")
|
|
408
|
+
if [ "$rc" -eq 2 ]; then
|
|
409
|
+
printf 'escalate|unreadable status span for %s' "$task"
|
|
410
|
+
return
|
|
411
|
+
fi
|
|
412
|
+
if [ "$rc" -eq 0 ]; then
|
|
413
|
+
rest=${record#*$'\t'}
|
|
414
|
+
event=${rest#*$'\t'}
|
|
415
|
+
printf 'escalate|stale + actionable status: %s' "$event"
|
|
416
|
+
return
|
|
417
|
+
fi
|
|
418
|
+
if [ -n "$last" ] && status_is_paused_or_captain_held "$last"; then
|
|
419
|
+
# A DECLARED external-wait pause or a verified captain-held transfer
|
|
420
|
+
# (fm-classify-lib.sh owns which declarations qualify): an idle pane is
|
|
421
|
+
# EXPECTED, so this is not a wedge. The caller records a pause marker (long
|
|
422
|
+
# re-surface cadence in housekeeping) rather than a wedge stale marker. Cheap:
|
|
423
|
+
# reuses the status line already read, no fm-crew-state.sh call, mirroring the
|
|
424
|
+
# daemon's existing status-log classification.
|
|
425
|
+
printf 'pause|paused (awaiting external), rechecked on a long cadence: %s' "$last"
|
|
426
|
+
return
|
|
427
|
+
fi
|
|
428
|
+
if [ -n "$last" ] && status_is_captain_relevant "$last"; then
|
|
429
|
+
# Independent of free-text captain-relevant matching: a nonterminal progress
|
|
430
|
+
# verb (working:) must never take the terminal stale path. Seen-status dedupe
|
|
431
|
+
# must not permanently suppress or clear possible-wedge aging merely because
|
|
432
|
+
# prose once looked captain-relevant. Real terminal verbs and legacy free-text
|
|
433
|
+
# captain lines without those verbs keep the terminal escalate/dedupe path.
|
|
434
|
+
if ! status_is_terminal_verb "$last"; then
|
|
435
|
+
case "$(status_line_verb "$last")" in
|
|
436
|
+
working|resolved|captain-held)
|
|
437
|
+
printf 'self|transient stale (%s): %s' "$win" "$last"
|
|
438
|
+
return
|
|
439
|
+
;;
|
|
440
|
+
esac
|
|
441
|
+
fi
|
|
442
|
+
printf 'self|stale + terminal (already escalated by signal): %s' "$last"
|
|
443
|
+
return
|
|
444
|
+
fi
|
|
445
|
+
# Non-terminal (or no status): defer to the persistence recheck. The caller
|
|
446
|
+
# records/refreshes the stale marker so housekeeping can age it.
|
|
447
|
+
printf 'self|transient stale (%s): %s' "$win" "${last:-no status}"
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
classify_check() { # <full reason> — check scripts print only when firstmate should wake
|
|
451
|
+
printf 'escalate|%s' "$1"
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
classify_heartbeat() {
|
|
455
|
+
# The wake itself is routine; the catch-all scan runs separately in
|
|
456
|
+
# housekeeping on the HEARTBEAT_SCAN_SECS cadence.
|
|
457
|
+
printf 'self|heartbeat (catch-all scan runs in housekeeping)'
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
# Anything unrecognized is escalated (fail-safe).
|
|
461
|
+
classify_unknown() { # <reason>
|
|
462
|
+
printf 'escalate|unknown wake: %s' "$1"
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
# --- stale marker + escalation buffer (stateful, but via explicit state dir) -
|
|
466
|
+
# Marker: state/.subsuper-stale-<key> contains the epoch first seen idle.
|
|
467
|
+
# Buffer: state/.subsuper-escalations one distilled line per escalation.
|
|
468
|
+
# Seen: state/.subsuper-seen-status-<task> last reported file signature and
|
|
469
|
+
# classified byte offset, so failures and events do not re-fire while
|
|
470
|
+
# unread bytes remain recoverable.
|
|
471
|
+
|
|
472
|
+
_stale_key() { printf '%s' "$1" | tr ':/.' '___'; }
|
|
473
|
+
|
|
474
|
+
stale_marker_record() { # <window> <state> — create if absent
|
|
475
|
+
local win=$1 state=$2 key marker
|
|
476
|
+
key=$(_stale_key "$(window_to_task "$win" "$state")")
|
|
477
|
+
marker="$state/.subsuper-stale-$key"
|
|
478
|
+
[ -e "$marker" ] || _now > "$marker"
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
stale_marker_remove() { # <window> <state>
|
|
482
|
+
local win=$1 state=$2 key
|
|
483
|
+
key=$(_stale_key "$(window_to_task "$win" "$state")")
|
|
484
|
+
rm -f "$state/.subsuper-stale-$key"
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
# Pause marker: state/.subsuper-paused-<key> holds the epoch a declared wait (a
|
|
488
|
+
# paused: external wait or a verified captain-held transfer) was first observed
|
|
489
|
+
# declared, whether its pane read idle or busy. Housekeeping ages it against
|
|
490
|
+
# PAUSE_RESURFACE_SECS (much longer than a wedge) and re-surfaces the wait once
|
|
491
|
+
# per window. Recording is create-if-absent so the timestamp is stable across a
|
|
492
|
+
# churny pane (many distinct stale hashes map to one marker), keeping the cadence
|
|
493
|
+
# hash-immune.
|
|
494
|
+
pause_marker_record() { # <window> <state> - create if absent
|
|
495
|
+
local win=$1 state=$2 key marker
|
|
496
|
+
key=$(_stale_key "$(window_to_task "$win" "$state")")
|
|
497
|
+
marker="$state/.subsuper-paused-$key"
|
|
498
|
+
[ -e "$marker" ] || _now > "$marker"
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
pause_marker_remove() { # <window> <state>
|
|
502
|
+
local win=$1 state=$2 key
|
|
503
|
+
key=$(_stale_key "$(window_to_task "$win" "$state")")
|
|
504
|
+
rm -f "$state/.subsuper-paused-$key"
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
clear_pause_tracking() { # <window> <state>
|
|
508
|
+
local win=$1 state=$2 task key watcher_key
|
|
509
|
+
task=$(window_to_task "$win" "$state")
|
|
510
|
+
key=$(_stale_key "$task")
|
|
511
|
+
watcher_key=$(_stale_key "$win")
|
|
512
|
+
rm -f "$state/.subsuper-paused-$key" "$state/.subsuper-stale-$key" \
|
|
513
|
+
"$state/.paused-$watcher_key" "$state/.paused-rechecked-$watcher_key" "$state/.paused-resurfaced-$watcher_key" \
|
|
514
|
+
"$state/.stale-$watcher_key" "$state/.stale-since-$watcher_key" "$state/.wedge-escalations-$watcher_key" \
|
|
515
|
+
"$state/.writing-since-$watcher_key" "$state/.writing-resurfaced-$watcher_key"
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
reconcile_pause_tracking() { # <window> <state> <last-status-line>
|
|
519
|
+
local win=$1 state=$2 last=$3 task key marker watcher_key
|
|
520
|
+
task=$(window_to_task "$win" "$state")
|
|
521
|
+
key=$(_stale_key "$task")
|
|
522
|
+
marker="$state/.subsuper-paused-$key"
|
|
523
|
+
watcher_key=$(_stale_key "$win")
|
|
524
|
+
if status_is_paused_or_captain_held "$last"; then
|
|
525
|
+
stale_marker_remove "$win" "$state"
|
|
526
|
+
pause_marker_record "$win" "$state"
|
|
527
|
+
elif [ -e "$marker" ] || [ -e "$state/.paused-$watcher_key" ]; then
|
|
528
|
+
clear_pause_tracking "$win" "$state"
|
|
529
|
+
fi
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
migrate_watcher_pause_markers() { # <state>
|
|
533
|
+
local state=$1 meta win task key last watcher_key
|
|
534
|
+
for meta in "$state"/*.meta; do
|
|
535
|
+
[ -e "$meta" ] || continue
|
|
536
|
+
win=$(fm_backend_target_of_meta "$meta")
|
|
537
|
+
[ -n "$win" ] || continue
|
|
538
|
+
task=$(basename "$meta"); task=${task%.meta}
|
|
539
|
+
key=$(_stale_key "$task")
|
|
540
|
+
watcher_key=$(_stale_key "$win")
|
|
541
|
+
last=$(last_status_line "$state/$task.status")
|
|
542
|
+
if status_is_paused_or_captain_held "$last" || [ -e "$state/.subsuper-paused-$key" ] || [ -e "$state/.paused-$watcher_key" ]; then
|
|
543
|
+
reconcile_pause_tracking "$win" "$state" "$last"
|
|
544
|
+
fi
|
|
545
|
+
done
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
sync_pause_markers_from_signal() { # <state> <signal files>
|
|
549
|
+
local state=$1 paths=$2 f last task win
|
|
550
|
+
local -a files
|
|
551
|
+
read -r -a files <<<"$paths"
|
|
552
|
+
for f in "${files[@]}"; do
|
|
553
|
+
case "$f" in *.status) ;; *) continue ;; esac
|
|
554
|
+
[ -e "$f" ] || continue
|
|
555
|
+
last=$(last_status_line "$f")
|
|
556
|
+
task=$(basename "$f"); task=${task%.status}
|
|
557
|
+
win=$(window_for_task "$task" "$state" 2>/dev/null || true)
|
|
558
|
+
[ -n "$win" ] || continue
|
|
559
|
+
reconcile_pause_tracking "$win" "$state" "$last"
|
|
560
|
+
done
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
_seen_status_path() { # <state> <task>
|
|
564
|
+
status_daemon_seen_marker_path "$1" "$2"
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
# The byte offset in <task>'s status log through which this daemon has
|
|
568
|
+
# successfully classified content, or 0 when it has no usable position.
|
|
569
|
+
# A position rather than an event line prevents both a later routine append from
|
|
570
|
+
# hiding earlier events and repeated event text from suppressing a new occurrence.
|
|
571
|
+
# An absent, malformed, identity-mismatched, or legacy marker reads 0, so the
|
|
572
|
+
# whole log is classified and uncertainty prefers a duplicate over event loss.
|
|
573
|
+
status_seen_offset() { # <state> <task>
|
|
574
|
+
status_presentation_marker_offset "$(_seen_status_path "$1" "$2")" "$1/$2.status"
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
# Commit <task>'s successfully classified endpoint, so the heartbeat catch-all
|
|
578
|
+
# scan does not re-read events already handled by the per-wake or scan path.
|
|
579
|
+
mark_status_seen() { # <state> <task> <captured-end-offset> <captured-identity>
|
|
580
|
+
status_presentation_marker_commit "$(_seen_status_path "$1" "$2")" \
|
|
581
|
+
"$1/$2.status" "$3" "$4"
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
# Advance the offset for every task a per-wake classification escalated, so the
|
|
585
|
+
# catch-all scan does not re-escalate the same events within HEARTBEAT_SCAN_SECS.
|
|
586
|
+
# An ERROR row names a task whose log could not be classified and carries the
|
|
587
|
+
# observed file signature.
|
|
588
|
+
# Recording that signature bounds the report while leaving its classification
|
|
589
|
+
# position unchanged, so readable recovery resumes from the last proven byte.
|
|
590
|
+
mark_escalated_seen() { # <state> <captured-endpoint-file>
|
|
591
|
+
local state=$1 capture=$2 task endpoint ident rc=0
|
|
592
|
+
[ -f "$capture" ] || return 1
|
|
593
|
+
while IFS=$(printf '\t') read -r task endpoint ident; do
|
|
594
|
+
[ -n "$task" ] || continue
|
|
595
|
+
if [ "$task" = ERROR ]; then
|
|
596
|
+
status_presentation_marker_report "$(_seen_status_path "$state" "$endpoint")" "$ident" || rc=1
|
|
597
|
+
continue
|
|
598
|
+
fi
|
|
599
|
+
mark_status_seen "$state" "$task" "$endpoint" "$ident" || rc=1
|
|
600
|
+
done < "$capture"
|
|
601
|
+
return "$rc"
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
# Busy and composer-empty detection form the injection boundary.
|
|
605
|
+
# These thin wrappers keep the daemon's call sites and unit tests stable.
|
|
606
|
+
#
|
|
607
|
+
# pane_input_pending returns 0 unless the composer is positively proven empty.
|
|
608
|
+
# This includes real unsubmitted text, ambiguous structure, unreadable state,
|
|
609
|
+
# blank or otherwise unidentified rows (the strict container-proof rule owned
|
|
610
|
+
# by bin/fm-composer-lib.sh), and future verdicts. The detector drops
|
|
611
|
+
# dim/faint ghost text and strips the harness's composer box borders, so an
|
|
612
|
+
# aligned ghost-only or idle bordered claude composer ("│ > … │") is correctly
|
|
613
|
+
# proven empty while a modal dialog or dead shell never is.
|
|
614
|
+
# pane_is_busy / pane_input_pending: BACKEND-AWARE (dispatch goes through
|
|
615
|
+
# bin/fm-backend.sh's generic per-backend primitives rather than a hand-rolled
|
|
616
|
+
# case statement here). <backend> defaults to tmux when omitted, so every
|
|
617
|
+
# existing caller/test that passes only <target> is unaffected.
|
|
618
|
+
#
|
|
619
|
+
# This rendered reader applies only to the supervisor pane during away-mode
|
|
620
|
+
# injection. It never classifies a recorded worker task. The detected primary
|
|
621
|
+
# harness selects exactly one signature, so output from another harness cannot
|
|
622
|
+
# make the primary read busy.
|
|
623
|
+
#
|
|
624
|
+
# Resolved lazily and memoized: harness detection walks process ancestry, which
|
|
625
|
+
# is too heavy to pay on every source of this library (the unit tests and the
|
|
626
|
+
# launcher source it purely for its pure functions).
|
|
627
|
+
fm_daemon_primary_harness() {
|
|
628
|
+
if [ -z "${FM_DAEMON_PRIMARY_HARNESS:-}" ]; then
|
|
629
|
+
FM_DAEMON_PRIMARY_HARNESS=$("$FM_DAEMON_DIR/fm-harness.sh" 2>/dev/null || printf 'unknown')
|
|
630
|
+
[ -n "$FM_DAEMON_PRIMARY_HARNESS" ] || FM_DAEMON_PRIMARY_HARNESS=unknown
|
|
631
|
+
fi
|
|
632
|
+
printf '%s' "$FM_DAEMON_PRIMARY_HARNESS"
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
pane_is_busy() { # <target> [backend]
|
|
636
|
+
local target=$1 backend=${2:-tmux} native tail40 harness
|
|
637
|
+
harness=$(fm_daemon_primary_harness)
|
|
638
|
+
native=$(fm_backend_busy_state "$backend" "$target" 2>/dev/null)
|
|
639
|
+
case "$native" in
|
|
640
|
+
busy) return 0 ;;
|
|
641
|
+
esac
|
|
642
|
+
tail40=$(fm_backend_capture "$backend" "$target" 40 2>/dev/null) || return 1
|
|
643
|
+
printf '%s' "$tail40" | grep -v '^[[:space:]]*$' | tail -12 \
|
|
644
|
+
| fm_busy_lines_match "$harness"
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
# pane_input_pending dispatches through fm_backend_composer_state and treats
|
|
648
|
+
# every verdict except exact empty as unsafe. inject_msg reads the full verdict
|
|
649
|
+
# directly and applies the same positive-proof boundary.
|
|
650
|
+
pane_input_pending() { # <target> [backend]
|
|
651
|
+
local target=$1 backend=${2:-tmux}
|
|
652
|
+
[ "$(fm_backend_composer_state "$backend" "$target" 2>/dev/null)" != empty ]
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
task_window_backend() { # <window> <state>
|
|
656
|
+
local win=$1 state=$2 task meta
|
|
657
|
+
task=$(window_to_task "$win" "$state")
|
|
658
|
+
meta="$state/$task.meta"
|
|
659
|
+
fm_backend_of_meta "$meta"
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
task_window_harness() { # <window> <state>
|
|
663
|
+
local win=$1 state=$2 task meta
|
|
664
|
+
task=$(window_to_task "$win" "$state")
|
|
665
|
+
meta="$state/$task.meta"
|
|
666
|
+
grep '^harness=' "$meta" 2>/dev/null | cut -d= -f2- || true
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
# stale_window_is_busy: 0 when the task is PROVABLY working through the
|
|
670
|
+
# semantic busy-state contract (bin/fm-busy-lib.sh), 1 when it is not, and 2
|
|
671
|
+
# when the endpoint could not be read at all. Only an exact busy verdict is
|
|
672
|
+
# working: unknown semantic state never becomes busy and never becomes a
|
|
673
|
+
# silent idle, so a stale pane whose state cannot be proven surfaces.
|
|
674
|
+
stale_window_is_busy() { # <window> <state>
|
|
675
|
+
local win=$1 state=$2 backend harness label task tail40 verdict
|
|
676
|
+
backend=$(task_window_backend "$win" "$state")
|
|
677
|
+
harness=$(task_window_harness "$win" "$state")
|
|
678
|
+
task=$(window_to_task "$win" "$state")
|
|
679
|
+
label="fm-$task"
|
|
680
|
+
tail40=$(fm_backend_capture "$backend" "$win" 40 "$label" 2>/dev/null) || return 2
|
|
681
|
+
verdict=$(fm_busy_classify "$backend" "$win" "$harness" "$task" "$state" "$tail40")
|
|
682
|
+
[ "${verdict%% *}" = busy ]
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
escalate_add() { # <state> <distilled-item>
|
|
686
|
+
local state=$1 item=$2 buf
|
|
687
|
+
buf="$state/.subsuper-escalations"
|
|
688
|
+
[ -s "$buf" ] || _now > "${buf}.since"
|
|
689
|
+
printf '%s\n' "$item" >> "$buf"
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
# Flush the escalation buffer as ONE batched, single-line digest to the
|
|
693
|
+
# supervisor pane. Returns 0 on successful inject (or empty buffer), non-zero on
|
|
694
|
+
# inject failure (buffer preserved for retry / catch-up).
|
|
695
|
+
escalate_flush() { # <state>
|
|
696
|
+
local state=$1 buf item n msg
|
|
697
|
+
buf="$state/.subsuper-escalations"
|
|
698
|
+
[ -s "$buf" ] || return 0
|
|
699
|
+
n=$(wc -l < "$buf" 2>/dev/null || echo 0)
|
|
700
|
+
# Join buffered items with the literal " | " separator into one digest line.
|
|
701
|
+
msg=$(awk 'NR>1{printf " | "} {printf "%s",$0} END{print ""}' "$buf" 2>/dev/null)
|
|
702
|
+
# Single-line wrapper: no embedded newlines (inject_msg also collapses as a
|
|
703
|
+
# safety net, but keeping the source single-line makes the intent explicit).
|
|
704
|
+
msg=$(printf 'Supervisor escalate (%s event(s)): %s (pre-read; re-arm not needed — watcher daemon-managed)' "$n" "$msg")
|
|
705
|
+
if inject_msg "$msg" "$state"; then : > "$buf"; rm -f "${buf}.since" "$state/.subsuper-inject-wedged"; return 0; fi
|
|
706
|
+
return 1
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
# --- backend-independent active wedge alert ---------------------------------
|
|
710
|
+
# The tmux status-line flash in inject_wedge_alarm below is a cosmetic,
|
|
711
|
+
# client-side OSD with no cross-backend equivalent, so a wedged non-tmux primary
|
|
712
|
+
# (the 2026-07-10 overnight incident: a claude-on-herdr primary) got NO active
|
|
713
|
+
# signal - only the passive state/.subsuper-inject-wedged marker, which nothing
|
|
714
|
+
# surfaces until the next fleet action (that night, 20 escalations sat buffered
|
|
715
|
+
# for 8.5h). These helpers add a configurable active alert that does not depend
|
|
716
|
+
# on any pane or its backend status-line: an OS-level macOS notification, a
|
|
717
|
+
# herdr notification, or a captain-supplied command (push to a phone, etc.).
|
|
718
|
+
# Every channel is best-effort - a missing or failing channel logs and is
|
|
719
|
+
# skipped, never crashing the daemon loop - and the durable marker plus the tmux
|
|
720
|
+
# flash stay exactly as before.
|
|
721
|
+
#
|
|
722
|
+
# Config: config/wedge-alarm (local, gitignored), one channel directive per
|
|
723
|
+
# non-empty, non-comment line. FM_WEDGE_ALARM_CHANNEL overrides the file with a
|
|
724
|
+
# single directive. Directives:
|
|
725
|
+
# off disable the active alert entirely, regardless of position
|
|
726
|
+
# (marker + flash remain)
|
|
727
|
+
# auto | default platform default: macOS -> osascript; otherwise none
|
|
728
|
+
# osascript macOS Notification Center banner (backend-independent)
|
|
729
|
+
# herdr herdr UI notification (herdr notification show)
|
|
730
|
+
# command:<cmd> run <cmd> via `sh -c`, summary on $1 and on stdin
|
|
731
|
+
# An absent config means auto, i.e. default-ON on macOS: the alarm's whole
|
|
732
|
+
# purpose is to never be silent, so the reachable OS channel fires unless the
|
|
733
|
+
# captain explicitly disables it.
|
|
734
|
+
|
|
735
|
+
# Print the configured channel directives, one per line. FM_WEDGE_ALARM_CHANNEL
|
|
736
|
+
# wins (a single directive); else each non-empty, non-comment line of
|
|
737
|
+
# config/wedge-alarm; else "auto".
|
|
738
|
+
wedge_alarm_configured_channels() {
|
|
739
|
+
local cfg line found=
|
|
740
|
+
if [ -n "${FM_WEDGE_ALARM_CHANNEL:-}" ]; then
|
|
741
|
+
printf '%s\n' "$FM_WEDGE_ALARM_CHANNEL"
|
|
742
|
+
return 0
|
|
743
|
+
fi
|
|
744
|
+
cfg="${FM_CONFIG_OVERRIDE:-$FM_HOME/config}/wedge-alarm"
|
|
745
|
+
if [ -f "$cfg" ]; then
|
|
746
|
+
while IFS= read -r line || [ -n "$line" ]; do
|
|
747
|
+
line="${line#"${line%%[![:space:]]*}"}"
|
|
748
|
+
line="${line%"${line##*[![:space:]]}"}"
|
|
749
|
+
[ -n "$line" ] || continue
|
|
750
|
+
case "$line" in '#'*) continue ;; esac
|
|
751
|
+
printf '%s\n' "$line"
|
|
752
|
+
found=1
|
|
753
|
+
done < "$cfg"
|
|
754
|
+
fi
|
|
755
|
+
[ -n "$found" ] || printf 'auto\n'
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
# Resolve the platform's default OS-level channel for `auto`. macOS reaches the
|
|
759
|
+
# captain via an osascript Notification Center banner; other platforms have no
|
|
760
|
+
# built-in OS channel (the captain wires a command: directive), so this prints
|
|
761
|
+
# nothing and wedge_alarm_notify logs that the marker is the only signal.
|
|
762
|
+
wedge_alarm_platform_default() {
|
|
763
|
+
case "$(uname)" in
|
|
764
|
+
Darwin) command -v osascript >/dev/null 2>&1 && printf 'osascript' ;;
|
|
765
|
+
*) : ;;
|
|
766
|
+
esac
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
wedge_alarm_run_bounded() {
|
|
770
|
+
local channel=$1 timeout monitor_was_on=0 pid start elapsed rc
|
|
771
|
+
shift
|
|
772
|
+
timeout=${FM_WEDGE_ALARM_TIMEOUT_SECS:-$WEDGE_ALARM_TIMEOUT_SECS_DEFAULT}
|
|
773
|
+
case "$timeout" in
|
|
774
|
+
''|*[!0-9]*) timeout=$WEDGE_ALARM_TIMEOUT_SECS_DEFAULT ;;
|
|
775
|
+
*) [ "$timeout" -gt 0 ] 2>/dev/null || timeout=$WEDGE_ALARM_TIMEOUT_SECS_DEFAULT ;;
|
|
776
|
+
esac
|
|
777
|
+
case $- in *m*) monitor_was_on=1 ;; esac
|
|
778
|
+
set -m 2>/dev/null || true
|
|
779
|
+
case $- in
|
|
780
|
+
*m*) ;;
|
|
781
|
+
*) log "wedge alarm: ${channel} notifier skipped because its watchdog could not start"; return 125 ;;
|
|
782
|
+
esac
|
|
783
|
+
"$@" &
|
|
784
|
+
pid=$!
|
|
785
|
+
WEDGE_ALARM_NOTIFIER_PID=$pid
|
|
786
|
+
start=$SECONDS
|
|
787
|
+
while kill -0 "-$pid" 2>/dev/null; do
|
|
788
|
+
elapsed=$((SECONDS - start))
|
|
789
|
+
if [ "$elapsed" -ge "$timeout" ]; then
|
|
790
|
+
wedge_alarm_stop_active_notifier
|
|
791
|
+
[ "$monitor_was_on" -eq 1 ] || set +m 2>/dev/null || true
|
|
792
|
+
log "wedge alarm: ${channel} notifier timed out after ${elapsed}s (limit ${timeout}s)"
|
|
793
|
+
return 124
|
|
794
|
+
fi
|
|
795
|
+
sleep 0.1
|
|
796
|
+
done
|
|
797
|
+
if wait "$pid"; then rc=0; else rc=$?; fi
|
|
798
|
+
WEDGE_ALARM_NOTIFIER_PID=
|
|
799
|
+
[ "$monitor_was_on" -eq 1 ] || set +m 2>/dev/null || true
|
|
800
|
+
return "$rc"
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
wedge_alarm_stop_active_notifier() {
|
|
804
|
+
local pid=${WEDGE_ALARM_NOTIFIER_PID:-}
|
|
805
|
+
[ -n "$pid" ] || return 0
|
|
806
|
+
WEDGE_ALARM_NOTIFIER_PID=
|
|
807
|
+
kill -TERM "-$pid" 2>/dev/null || kill -TERM "$pid" 2>/dev/null || true
|
|
808
|
+
sleep 0.2
|
|
809
|
+
kill -KILL "-$pid" 2>/dev/null || kill -KILL "$pid" 2>/dev/null || true
|
|
810
|
+
wait "$pid" 2>/dev/null || true
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
# The single execution seam for every configured notifier channel.
|
|
814
|
+
# FM_WEDGE_ALARM_EXEC, when set, REPLACES the real notifier: the resolved channel
|
|
815
|
+
# name and summary are handed to that command instead of ever invoking osascript
|
|
816
|
+
# or herdr or a captain-supplied command. This is the one injection point the test harness forces to a recorder
|
|
817
|
+
# so no test can post a real desktop notification - the library-mode guard at the
|
|
818
|
+
# foot of this file defaults it to "discard" whenever the daemon is SOURCED
|
|
819
|
+
# rather than executed, which is the only way a test reaches these functions. The
|
|
820
|
+
# special value "discard" fires nothing; unset means production (the executed
|
|
821
|
+
# daemon), so the real channels fire.
|
|
822
|
+
wedge_alarm_os_notifier_override() { # <channel> <summary>
|
|
823
|
+
local channel=$1 summary=$2 rc exec_override=${FM_WEDGE_ALARM_EXEC:-}
|
|
824
|
+
case "$exec_override" in
|
|
825
|
+
'') return 2 ;;
|
|
826
|
+
discard) return 0 ;;
|
|
827
|
+
*)
|
|
828
|
+
wedge_alarm_run_bounded "$channel" "$exec_override" "$channel" "$summary" >/dev/null 2>&1
|
|
829
|
+
rc=$?
|
|
830
|
+
[ "$rc" -eq 0 ] && return 0
|
|
831
|
+
log "wedge alarm: notifier override exited $rc for channel '$channel'"
|
|
832
|
+
return 1 ;;
|
|
833
|
+
esac
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
# Post a macOS Notification Center banner. `display notification` is OS-level,
|
|
837
|
+
# independent of any terminal pane or multiplexer status-line. The summary is
|
|
838
|
+
# passed as an argv item (never interpolated into the AppleScript source) so its
|
|
839
|
+
# text can never break the script. Best-effort: logs and returns 1 on failure.
|
|
840
|
+
wedge_alarm_via_osascript() { # <summary>
|
|
841
|
+
local summary=$1 rc
|
|
842
|
+
wedge_alarm_os_notifier_override osascript "$summary"
|
|
843
|
+
rc=$?
|
|
844
|
+
case "$rc" in
|
|
845
|
+
0) return 0 ;;
|
|
846
|
+
1) return 1 ;;
|
|
847
|
+
esac
|
|
848
|
+
command -v osascript >/dev/null 2>&1 || {
|
|
849
|
+
log "wedge alarm: osascript not found; cannot post a macOS notification"; return 1; }
|
|
850
|
+
wedge_alarm_run_bounded osascript osascript -e 'on run argv' \
|
|
851
|
+
-e 'display notification (item 1 of argv) with title "firstmate: away-mode escalations WEDGED" sound name "Basso"' \
|
|
852
|
+
-e 'end run' "$summary" >/dev/null 2>&1 && return 0
|
|
853
|
+
log "wedge alarm: osascript notification failed"
|
|
854
|
+
return 1
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
# Post a herdr UI notification - herdr's own surface, separate from the pane and
|
|
858
|
+
# its status-line. Best-effort: logs and returns 1 on failure.
|
|
859
|
+
wedge_alarm_via_herdr() { # <summary>
|
|
860
|
+
local summary=$1 rc
|
|
861
|
+
wedge_alarm_os_notifier_override herdr "$summary"
|
|
862
|
+
rc=$?
|
|
863
|
+
case "$rc" in
|
|
864
|
+
0) return 0 ;;
|
|
865
|
+
1) return 1 ;;
|
|
866
|
+
esac
|
|
867
|
+
command -v herdr >/dev/null 2>&1 || {
|
|
868
|
+
log "wedge alarm: herdr not found; cannot post a herdr notification"; return 1; }
|
|
869
|
+
wedge_alarm_run_bounded herdr herdr notification show "firstmate: away-mode escalations WEDGED" \
|
|
870
|
+
--body "$summary" --sound request >/dev/null 2>&1 && return 0
|
|
871
|
+
log "wedge alarm: herdr notification failed"
|
|
872
|
+
return 1
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
# Run a captain-supplied command with the summary on $1 and on stdin, so an
|
|
876
|
+
# alert can reach a phone/pager (ntfy, Slack, SMS) even when the captain is away
|
|
877
|
+
# from the machine entirely. Best-effort: logs and returns 1 on failure.
|
|
878
|
+
wedge_alarm_via_command() { # <cmd> <summary>
|
|
879
|
+
local cmd=$1 summary=$2 rc
|
|
880
|
+
if [ "${WEDGE_ALARM_EMIT_ACTIVE:-}" != 1 ]; then
|
|
881
|
+
wedge_alarm_emit command "$summary" "$cmd"
|
|
882
|
+
return $?
|
|
883
|
+
fi
|
|
884
|
+
[ -n "$cmd" ] || { log "wedge alarm: empty command: channel; nothing to run"; return 1; }
|
|
885
|
+
wedge_alarm_run_bounded command sh -c "$cmd" fm-wedge-alarm "$summary" \
|
|
886
|
+
<<< "$summary" >/dev/null 2>&1
|
|
887
|
+
rc=$?
|
|
888
|
+
[ "$rc" -eq 0 ] && return 0
|
|
889
|
+
log "wedge alarm: command channel exited $rc (command redacted)"
|
|
890
|
+
return 1
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
wedge_alarm_emit() { # <channel> <summary>
|
|
894
|
+
local channel=$1 summary=$2 cmd=${3:-} rc exec_override=${FM_WEDGE_ALARM_EXEC:-} WEDGE_ALARM_EMIT_ACTIVE=1
|
|
895
|
+
case "$exec_override" in
|
|
896
|
+
'') ;;
|
|
897
|
+
discard) return 0 ;;
|
|
898
|
+
*)
|
|
899
|
+
wedge_alarm_run_bounded "$channel" "$exec_override" "$channel" "$summary" >/dev/null 2>&1
|
|
900
|
+
rc=$?
|
|
901
|
+
[ "$rc" -eq 0 ] && return 0
|
|
902
|
+
log "wedge alarm: notifier override exited $rc for channel '$channel'"
|
|
903
|
+
return 1 ;;
|
|
904
|
+
esac
|
|
905
|
+
case "$channel" in
|
|
906
|
+
osascript) wedge_alarm_via_osascript "$summary" ;;
|
|
907
|
+
herdr) wedge_alarm_via_herdr "$summary" ;;
|
|
908
|
+
command) wedge_alarm_via_command "$cmd" "$summary" ;;
|
|
909
|
+
esac
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
# Fire every configured active-alert channel, best-effort. Always returns 0: a
|
|
913
|
+
# channel failure can never abort inject_wedge_alarm or the daemon loop. Any
|
|
914
|
+
# `off` directive disables the alert, regardless of position; an unresolvable
|
|
915
|
+
# `auto` (no OS channel on this platform) logs that the durable marker is the
|
|
916
|
+
# only signal. Every notifier routes through the test-forced recorder seam.
|
|
917
|
+
wedge_alarm_notify() { # <summary> <marker>
|
|
918
|
+
local summary=$1 marker=$2 ch
|
|
919
|
+
local -a channels=()
|
|
920
|
+
while IFS= read -r ch; do
|
|
921
|
+
[ -n "$ch" ] || continue
|
|
922
|
+
channels+=("$ch")
|
|
923
|
+
done < <(wedge_alarm_configured_channels)
|
|
924
|
+
for ch in "${channels[@]}"; do
|
|
925
|
+
[ "$ch" = off ] && return 0
|
|
926
|
+
done
|
|
927
|
+
for ch in "${channels[@]}"; do
|
|
928
|
+
case "$ch" in auto|default) ch=$(wedge_alarm_platform_default) ;; esac
|
|
929
|
+
case "$ch" in
|
|
930
|
+
'') log "wedge alarm: no OS-level alert channel on $(uname); durable marker $marker is the only signal - set config/wedge-alarm (e.g. a command: directive)" ;;
|
|
931
|
+
osascript|herdr) wedge_alarm_emit "$ch" "$summary" || true ;;
|
|
932
|
+
command:*) wedge_alarm_emit command "$summary" "${ch#command:}" || true ;;
|
|
933
|
+
*) log "wedge alarm: unrecognized active-alert channel directive (redacted); marker still written" ;;
|
|
934
|
+
esac
|
|
935
|
+
done
|
|
936
|
+
return 0
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
# Raise a loud, rate-limited alarm when escalations cannot be delivered after
|
|
940
|
+
# max-defer (the supervisor pane is genuinely busy/wedged, or the submit's Enter
|
|
941
|
+
# is swallowed). The daemon must NEVER silently wedge: this logs
|
|
942
|
+
# an ERROR, drops a durable marker firstmate/recovery can surface, flashes
|
|
943
|
+
# the tmux supervisor client's status line when applicable, and attempts a
|
|
944
|
+
# configurable backend-independent active alert (wedge_alarm_notify). Nothing
|
|
945
|
+
# is lost - the buffer and the
|
|
946
|
+
# wake-queue both survive - but the stall stops being invisible.
|
|
947
|
+
inject_wedge_alarm() { # <state> <age-seconds>
|
|
948
|
+
local state=$1 age=$2 marker target backend max_defer now notify=1
|
|
949
|
+
marker="$state/.subsuper-inject-wedged"
|
|
950
|
+
max_defer="${FM_MAX_DEFER_SECS:-$MAX_DEFER_SECS_DEFAULT}"
|
|
951
|
+
# Re-alarm at most once per max-defer window so a long wedge does not spam.
|
|
952
|
+
if [ "$(_file_age "$marker")" -lt "$max_defer" ]; then
|
|
953
|
+
return 0
|
|
954
|
+
fi
|
|
955
|
+
now=$(_now)
|
|
956
|
+
if [ "$WEDGE_ALARM_LAST_EPOCH" -gt 0 ] && [ $((now - WEDGE_ALARM_LAST_EPOCH)) -lt "$max_defer" ]; then
|
|
957
|
+
notify=0
|
|
958
|
+
else
|
|
959
|
+
WEDGE_ALARM_LAST_EPOCH=$now
|
|
960
|
+
log "ERROR: away-mode escalation undelivered ${age}s; inject could not confirm a submit (supervisor pane busy or wedged). Buffer + wake-queue preserved; alarm marker written."
|
|
961
|
+
fi
|
|
962
|
+
{
|
|
963
|
+
printf 'fm away-mode inject WEDGED: %ss undelivered as of %s\n' "$age" "$(date '+%Y-%m-%dT%H:%M:%S%z')"
|
|
964
|
+
printf 'The supervisor pane could not accept an escalation. Buffered items:\n'
|
|
965
|
+
cat "$state/.subsuper-escalations" 2>/dev/null
|
|
966
|
+
} 2>/dev/null > "$marker" || true
|
|
967
|
+
target="${FM_SUPERVISOR_TARGET:-$FM_SUPERVISOR_TARGET_DEFAULT}"
|
|
968
|
+
backend="${FM_SUPERVISOR_BACKEND:-$FM_SUPERVISOR_BACKEND_DEFAULT}"
|
|
969
|
+
# Best-effort status-line flash. tmux's display-message is a client-side OSD
|
|
970
|
+
# with no herdr equivalent; the log line + durable marker above are already
|
|
971
|
+
# the primary, backend-independent signal, so a non-tmux backend just skips
|
|
972
|
+
# this cosmetic extra rather than attempting an unsupported call.
|
|
973
|
+
if [ "$backend" = tmux ]; then
|
|
974
|
+
tmux display-message -t "$target" "fm: away-mode escalations WEDGED ${age}s — see $marker" 2>/dev/null || true
|
|
975
|
+
fi
|
|
976
|
+
# Backend-independent active alert. Unlike the tmux flash above (skipped on
|
|
977
|
+
# every non-tmux backend), this can reach the captain even when every pane and
|
|
978
|
+
# its backend status-line is unreadable - the gap the 2026-07-10 overnight
|
|
979
|
+
# incident fell through. Configurable and best-effort; the marker above stays
|
|
980
|
+
# the durable record whether or not any channel fires.
|
|
981
|
+
if [ "$notify" -eq 1 ]; then
|
|
982
|
+
wedge_alarm_notify "away-mode escalations WEDGED ${age}s undelivered - see $marker" "$marker"
|
|
983
|
+
fi
|
|
984
|
+
}
|
|
985
|
+
|
|
986
|
+
_oldest_line_age() { # <buf> -> seconds since the oldest buffered item first arrived (sidecar epoch)
|
|
987
|
+
local f=$1 since
|
|
988
|
+
[ -s "$f" ] || { echo 999999; return; }
|
|
989
|
+
since="${f}.since"
|
|
990
|
+
if [ -r "$since" ]; then
|
|
991
|
+
echo $(( $(_now) - $(cat "$since" 2>/dev/null || echo 0) ))
|
|
992
|
+
else
|
|
993
|
+
echo 999999
|
|
994
|
+
fi
|
|
995
|
+
}
|
|
996
|
+
|
|
997
|
+
# --- housekeeping (runs every tick while the watcher is mid-cycle) ----------
|
|
998
|
+
# Four cheap jobs, each guarded so an empty/quiet fleet costs near zero:
|
|
999
|
+
# 1) batch flush: if the escalation buffer's oldest content is older than
|
|
1000
|
+
# ESCALATE_BATCH_SECS (or batching is disabled), inject one digest.
|
|
1001
|
+
# 1b) max-defer escape: if the buffer is STILL undelivered past MAX_DEFER_SECS,
|
|
1002
|
+
# attempt one normal delivery; if it cannot confirm, raise the wedge alarm.
|
|
1003
|
+
# Never silently defer forever.
|
|
1004
|
+
# 2) stale recheck: for each pending stale marker past STALE_ESCALATE_SECS,
|
|
1005
|
+
# re-peek the pane; still idle -> escalate (wedge); resumed -> clear marker.
|
|
1006
|
+
# 2b) pause re-surface: for each declared-wait marker past PAUSE_RESURFACE_SECS,
|
|
1007
|
+
# re-peek; gone -> clear; still declaring the wait, on an idle OR a busy pane
|
|
1008
|
+
# -> escalate a recheck digest naming which human the wait is on, and reset
|
|
1009
|
+
# the window (repeating bounded re-surface, never a wedge).
|
|
1010
|
+
# 3) heartbeat scan: every HEARTBEAT_SCAN_SECS, grep state/*.status for a
|
|
1011
|
+
# captain-relevant line the per-wake classifier missed and escalate it.
|
|
1012
|
+
housekeeping() { # <state>
|
|
1013
|
+
local state=$1 now due f key task win marker age last max_defer oldest pause_secs
|
|
1014
|
+
now=$(_now)
|
|
1015
|
+
migrate_watcher_pause_markers "$state"
|
|
1016
|
+
|
|
1017
|
+
# (1) batch flush
|
|
1018
|
+
if [ "${FM_ESCALATE_BATCH_SECS:-$ESCALATE_BATCH_SECS_DEFAULT}" -le 0 ]; then
|
|
1019
|
+
escalate_flush "$state" || true
|
|
1020
|
+
else
|
|
1021
|
+
due=$(_oldest_line_age "$state/.subsuper-escalations")
|
|
1022
|
+
if [ "$due" -ge "${FM_ESCALATE_BATCH_SECS:-$ESCALATE_BATCH_SECS_DEFAULT}" ]; then
|
|
1023
|
+
escalate_flush "$state" || true
|
|
1024
|
+
fi
|
|
1025
|
+
fi
|
|
1026
|
+
|
|
1027
|
+
# (1b) max-defer escape. If anything is still buffered past MAX_DEFER_SECS,
|
|
1028
|
+
# retry the normal delivery path. If that still cannot confirm, raise a loud
|
|
1029
|
+
# wedge alarm while preserving the buffer.
|
|
1030
|
+
max_defer=${FM_MAX_DEFER_SECS:-$MAX_DEFER_SECS_DEFAULT}
|
|
1031
|
+
if afk_active "$state" && [ "$max_defer" -gt 0 ] && [ -s "$state/.subsuper-escalations" ]; then
|
|
1032
|
+
oldest=$(_oldest_line_age "$state/.subsuper-escalations")
|
|
1033
|
+
# Throttle the alarm to once per max-defer window (the wedge marker doubles
|
|
1034
|
+
# as the throttle). A successful flush clears the buffer; a failed one alarms
|
|
1035
|
+
# and waits.
|
|
1036
|
+
if [ "$oldest" -ge "$max_defer" ] \
|
|
1037
|
+
&& [ "$(_file_age "$state/.subsuper-inject-wedged")" -ge "$max_defer" ]; then
|
|
1038
|
+
if escalate_flush "$state"; then
|
|
1039
|
+
log "inject recovered: max-defer flush succeeded after ${oldest}s undelivered"
|
|
1040
|
+
rm -f "$state/.subsuper-inject-wedged"
|
|
1041
|
+
else
|
|
1042
|
+
inject_wedge_alarm "$state" "$oldest"
|
|
1043
|
+
fi
|
|
1044
|
+
fi
|
|
1045
|
+
fi
|
|
1046
|
+
|
|
1047
|
+
# (2) stale persistence recheck
|
|
1048
|
+
for marker in "$state"/.subsuper-stale-*; do
|
|
1049
|
+
[ -e "$marker" ] || continue
|
|
1050
|
+
key="${marker##*.subsuper-stale-}"
|
|
1051
|
+
# Reconstruct the backend target from metadata, with the live tmux list as the
|
|
1052
|
+
# legacy fallback for old markers that predate meta lookup.
|
|
1053
|
+
win=$(window_for_task "$key" "$state" 2>/dev/null || true)
|
|
1054
|
+
if [ -z "$win" ]; then
|
|
1055
|
+
# Window gone (task torn down): drop the marker, nothing to escalate.
|
|
1056
|
+
rm -f "$marker"; continue
|
|
1057
|
+
fi
|
|
1058
|
+
task=$(window_to_task "$win" "$state")
|
|
1059
|
+
last=$(last_status_line "$state/$task.status")
|
|
1060
|
+
if [ -n "$last" ] && status_is_paused_or_captain_held "$last"; then
|
|
1061
|
+
reconcile_pause_tracking "$win" "$state" "$last"
|
|
1062
|
+
continue
|
|
1063
|
+
fi
|
|
1064
|
+
age=$(( now - $(cat "$marker" 2>/dev/null || echo "$now") ))
|
|
1065
|
+
[ "$age" -ge "${FM_STALE_ESCALATE_SECS:-$STALE_ESCALATE_SECS_DEFAULT}" ] || continue
|
|
1066
|
+
stale_window_is_busy "$win" "$state"
|
|
1067
|
+
case "$?" in
|
|
1068
|
+
0) rm -f "$marker" ;;
|
|
1069
|
+
2) rm -f "$marker" ;;
|
|
1070
|
+
*) if escalate_add "$state" "stale persisted ${age}s (possible wedge): $win"; then
|
|
1071
|
+
stale_marker_remove "$win" "$state"
|
|
1072
|
+
fi ;;
|
|
1073
|
+
esac
|
|
1074
|
+
done
|
|
1075
|
+
|
|
1076
|
+
# (2b) pause re-surface recheck. A declared wait is waiting, not wedged (fm-classify-lib.sh's
|
|
1077
|
+
# status_is_paused_or_captain_held owns which declarations qualify), so it is
|
|
1078
|
+
# rechecked on a much longer cadence than a wedge (PAUSE_RESURFACE_SECS) and never
|
|
1079
|
+
# escalated as one - but it MUST re-surface, so neither a forgotten pause nor a
|
|
1080
|
+
# forgotten captain hold can rot invisibly. Past the window: gone -> drop; still
|
|
1081
|
+
# declaring the wait -> escalate a recheck digest and reset the marker so the window
|
|
1082
|
+
# repeats. The digest names WHICH human the wait is on, because the captain is the
|
|
1083
|
+
# one reading it: an external dependency for a paused: declaration, and the captain
|
|
1084
|
+
# themself for a verified hold transfer.
|
|
1085
|
+
# Pane busy state does NOT end the wait. A declared wait can legitimately hold a
|
|
1086
|
+
# pane busy - a worker parked on a long foreground call it keeps live for as long
|
|
1087
|
+
# as the wait lasts - so reading busy as "the crew resumed" retires the window of
|
|
1088
|
+
# exactly the declaration that needs it. The crew's own latest status line is the
|
|
1089
|
+
# authority, and the loop head above already drops the marker the moment that line
|
|
1090
|
+
# stops declaring the wait.
|
|
1091
|
+
pause_secs=${FM_PAUSE_RESURFACE_SECS:-$FM_PAUSE_RESURFACE_SECS_DEFAULT}
|
|
1092
|
+
for marker in "$state"/.subsuper-paused-*; do
|
|
1093
|
+
[ -e "$marker" ] || continue
|
|
1094
|
+
key="${marker##*.subsuper-paused-}"
|
|
1095
|
+
win=$(window_for_task "$key" "$state" 2>/dev/null || true)
|
|
1096
|
+
if [ -z "$win" ]; then
|
|
1097
|
+
rm -f "$marker"; continue
|
|
1098
|
+
fi
|
|
1099
|
+
task=$(window_to_task "$win" "$state")
|
|
1100
|
+
last=$(last_status_line "$state/$task.status")
|
|
1101
|
+
if [ -z "$last" ] || ! status_is_paused_or_captain_held "$last"; then
|
|
1102
|
+
reconcile_pause_tracking "$win" "$state" "$last"
|
|
1103
|
+
continue
|
|
1104
|
+
fi
|
|
1105
|
+
age=$(( now - $(cat "$marker" 2>/dev/null || echo "$now") ))
|
|
1106
|
+
[ "$age" -ge "$pause_secs" ] || continue
|
|
1107
|
+
# Endpoint-readability probe only: exit code 2 means the capture failed, so the
|
|
1108
|
+
# endpoint is gone and there is nothing left to re-surface. The busy/idle verdict
|
|
1109
|
+
# is deliberately discarded here. Do NOT reinstate a `0)` arm dropping the marker
|
|
1110
|
+
# on busy: migrate_watcher_pause_markers recreates it with a fresh timestamp on
|
|
1111
|
+
# the very next tick while the declaration still stands, so the window would
|
|
1112
|
+
# restart forever and the wait would never mature into its one recheck.
|
|
1113
|
+
stale_window_is_busy "$win" "$state"
|
|
1114
|
+
case "$?" in
|
|
1115
|
+
2) rm -f "$marker" ;;
|
|
1116
|
+
*)
|
|
1117
|
+
last=$(last_status_line "$state/$task.status")
|
|
1118
|
+
if [ -n "$last" ] && status_is_captain_held "$last"; then
|
|
1119
|
+
if escalate_add "$state" "captain-held ${age}s (awaiting the captain, answer the held decision or release the hold): $win"; then
|
|
1120
|
+
_now > "$marker"
|
|
1121
|
+
fi
|
|
1122
|
+
elif [ -n "$last" ] && status_is_paused "$last"; then
|
|
1123
|
+
if escalate_add "$state" "paused ${age}s (awaiting external, recheck whether the wait still holds): $win"; then
|
|
1124
|
+
_now > "$marker"
|
|
1125
|
+
fi
|
|
1126
|
+
else
|
|
1127
|
+
rm -f "$marker"
|
|
1128
|
+
fi
|
|
1129
|
+
;;
|
|
1130
|
+
esac
|
|
1131
|
+
done
|
|
1132
|
+
|
|
1133
|
+
# (3) heartbeat scan (catch-all for a captain-relevant status the per-wake
|
|
1134
|
+
# classifier may have missed). Cheap: status files only, no tmux. It walks
|
|
1135
|
+
# every log rather than only those whose LAST line looks captain-relevant,
|
|
1136
|
+
# because the event this backstop most needs to catch is precisely one a
|
|
1137
|
+
# later routine append has already moved past; fm-classify-lib.sh's span
|
|
1138
|
+
# read decides relevance, and the classified-through offset is the dedup.
|
|
1139
|
+
if [ "$(_file_age "$state/.subsuper-last-scan")" -ge "${FM_HEARTBEAT_SCAN_SECS:-$HEARTBEAT_SCAN_SECS_DEFAULT}" ]; then
|
|
1140
|
+
_now > "$state/.subsuper-last-scan"
|
|
1141
|
+
local event record rest endpoint ident rc
|
|
1142
|
+
for f in "$state"/*.status; do
|
|
1143
|
+
[ -e "$f" ] || [ -L "$f" ] || continue
|
|
1144
|
+
task=$(basename "$f"); task="${task%.status}"
|
|
1145
|
+
record=$(status_span_first_actionable_record "$f" \
|
|
1146
|
+
"$(status_seen_offset "$state" "$task")")
|
|
1147
|
+
rc=$?
|
|
1148
|
+
if [ "$rc" -eq 2 ]; then
|
|
1149
|
+
ident=$(status_observed_signature "$f")
|
|
1150
|
+
status_presentation_marker_reported_matches "$(_seen_status_path "$state" "$task")" "$ident" \
|
|
1151
|
+
&& continue
|
|
1152
|
+
if escalate_add "$state" "$(basename "$f"): unreadable status span (catch-all scan)"; then
|
|
1153
|
+
status_presentation_marker_report "$(_seen_status_path "$state" "$task")" "$ident" || true
|
|
1154
|
+
fi
|
|
1155
|
+
continue
|
|
1156
|
+
fi
|
|
1157
|
+
[ "$rc" -eq 1 ] && [ -z "$record" ] && continue
|
|
1158
|
+
endpoint=${record%%$'\t'*}
|
|
1159
|
+
rest=${record#*$'\t'}; ident=${rest%%$'\t'*}
|
|
1160
|
+
if [ "$rc" -eq 0 ]; then
|
|
1161
|
+
event=${rest#*$'\t'}
|
|
1162
|
+
if escalate_add "$state" "$(basename "$f"): $event (catch-all scan)"; then
|
|
1163
|
+
mark_status_seen "$state" "$task" "$endpoint" "$ident" || true
|
|
1164
|
+
fi
|
|
1165
|
+
elif ! mark_status_seen "$state" "$task" "$endpoint" "$ident"; then
|
|
1166
|
+
escalate_add "$state" "$(basename "$f"): status position commit failed (catch-all scan)"
|
|
1167
|
+
fi
|
|
1168
|
+
done
|
|
1169
|
+
fi
|
|
1170
|
+
}
|
|
1171
|
+
|
|
1172
|
+
# Find a recorded or live window target whose task id matches the marker key.
|
|
1173
|
+
window_for_task() { # <task-key> [state]
|
|
1174
|
+
local key=$1 state=${2:-$(_state_root)} meta task w t
|
|
1175
|
+
for meta in "$state"/*.meta; do
|
|
1176
|
+
[ -e "$meta" ] || continue
|
|
1177
|
+
task=$(basename "$meta"); task=${task%.meta}
|
|
1178
|
+
[ "$(_stale_key "$task")" = "$key" ] || continue
|
|
1179
|
+
w=$(fm_backend_target_of_meta "$meta")
|
|
1180
|
+
[ -n "$w" ] && { printf '%s' "$w"; return 0; }
|
|
1181
|
+
done
|
|
1182
|
+
for w in $(tmux list-windows -a -F '#{session_name}:#{window_name}' 2>/dev/null | grep ':fm-' || true); do
|
|
1183
|
+
t=$(window_to_task "$w" "$state")
|
|
1184
|
+
[ "$(_stale_key "$t")" = "$key" ] && { printf '%s' "$w"; return 0; }
|
|
1185
|
+
done
|
|
1186
|
+
return 1
|
|
1187
|
+
}
|
|
1188
|
+
|
|
1189
|
+
# --- injection --------------------------------------------------------------
|
|
1190
|
+
# inject_msg: send one escalation digest to the supervisor pane.
|
|
1191
|
+
# Returns 0 on successful inject (or empty buffer), non-zero if the pane is
|
|
1192
|
+
# gone, the supervisor is busy, afk is inactive, or the verified submit cannot
|
|
1193
|
+
# be confirmed after bounded retries. On non-zero the caller preserves
|
|
1194
|
+
# the buffer so the escalation survives for the next cycle or the catch-up flush.
|
|
1195
|
+
#
|
|
1196
|
+
# Submit model:
|
|
1197
|
+
# - TYPE ONCE, then submit with Enter. Never retype the digest: a swallowed
|
|
1198
|
+
# Enter leaves our text in the composer, and retyping would concatenate two
|
|
1199
|
+
# sentinel-prefixed digests into one corrupted turn.
|
|
1200
|
+
# - SUBMIT ACK = the backend submit primitive reports `empty` after Enter.
|
|
1201
|
+
# For tmux that means a cleared composer; for herdr's normal idle-baseline
|
|
1202
|
+
# path it means native agent-state observed a real turn start.
|
|
1203
|
+
# Pending means Enter was swallowed; unknown is treated as undelivered by
|
|
1204
|
+
# this strict daemon path.
|
|
1205
|
+
# - COMPOSER GUARD before typing: if the cursor line already has real content
|
|
1206
|
+
# after dim/faint ghost text and borders are ignored (a human's half-typed
|
|
1207
|
+
# line, or a previous injection's unsent text), defer entirely - injecting
|
|
1208
|
+
# would merge with the human's text.
|
|
1209
|
+
inject_msg() { # <message> [state]
|
|
1210
|
+
local msg=$1 state target backend retries sleep_s verdict composer encoded
|
|
1211
|
+
state="${2:-$(_state_root)}"
|
|
1212
|
+
# (1) Presence-gate: inject ONLY when afk is active. When afk is off, the
|
|
1213
|
+
# daemon self-handles and stays quiet; firstmate drives the normal always-on
|
|
1214
|
+
# watcher triage. Escalations buffer and survive for the next catch-up flush.
|
|
1215
|
+
afk_active "$state" || { log "inject deferred: afk inactive"; return 1; }
|
|
1216
|
+
# (2) Single-line digest: collapse any embedded newlines so submission via
|
|
1217
|
+
# send-keys + Enter is unambiguous regardless of how the TUI composer treats
|
|
1218
|
+
# them. Then use the canonical typed envelope so downstream consumers retain
|
|
1219
|
+
# the exact away-supervisor kind without interpreting this payload's prose.
|
|
1220
|
+
msg=$(_collapse_newlines "$msg")
|
|
1221
|
+
fm_operational_input_encode away-supervisor "$msg" encoded || return 1
|
|
1222
|
+
msg=$encoded
|
|
1223
|
+
target="${FM_SUPERVISOR_TARGET:-$FM_SUPERVISOR_TARGET_DEFAULT}"
|
|
1224
|
+
# BACKEND-AWARE (previously a raw `tmux display-message` pane-exists probe):
|
|
1225
|
+
# dispatches through bin/fm-backend.sh so a herdr supervisor pane is checked
|
|
1226
|
+
# via the herdr adapter instead of always assuming tmux. Falls back to tmux
|
|
1227
|
+
# when unset (sourced/test contexts that never ran fm_super_main's startup
|
|
1228
|
+
# discovery), matching this function's pre-existing default assumption.
|
|
1229
|
+
backend="${FM_SUPERVISOR_BACKEND:-tmux}"
|
|
1230
|
+
fm_backend_target_exists "$backend" "$target" || return 1
|
|
1231
|
+
# (3) Busy-guard: never inject into an in-use supervisor pane.
|
|
1232
|
+
if pane_is_busy "$target" "$backend"; then
|
|
1233
|
+
log "inject deferred: supervisor pane busy (agent mid-turn)"
|
|
1234
|
+
return 1
|
|
1235
|
+
fi
|
|
1236
|
+
# b) Composer-guard: inject ONLY into a confirmed-empty GENUINE agent
|
|
1237
|
+
# composer. The shared classifier (fm_backend_composer_state ->
|
|
1238
|
+
# fm_composer_classify_content, bin/fm-composer-lib.sh) reports 'pending'
|
|
1239
|
+
# for real unsubmitted text (a human's half-typed line, or a swallowed
|
|
1240
|
+
# prior injection) and 'unknown' for a bare dead-shell prompt (the agent
|
|
1241
|
+
# exited to its login shell) or an unreadable pane. Neither is a safe
|
|
1242
|
+
# target - typing the escalation into a shell could execute it - so defer
|
|
1243
|
+
# on anything that is not affirmatively 'empty'. A deferred escalation
|
|
1244
|
+
# stays buffered for the next cycle or the catch-up flush.
|
|
1245
|
+
composer=$(fm_backend_composer_state "$backend" "$target" 2>/dev/null)
|
|
1246
|
+
if [ "$composer" != empty ]; then
|
|
1247
|
+
log "inject deferred: supervisor composer not confirmed-empty (state=${composer:-unknown}: pending input, dead-shell prompt, or unreadable pane)"
|
|
1248
|
+
return 1
|
|
1249
|
+
fi
|
|
1250
|
+
# (4) Type the digest ONCE, then submit with Enter (retry Enter only, never
|
|
1251
|
+
# retype) via the shared submit primitive. Success = the backend confirms
|
|
1252
|
+
# submit. An unconfirmed/unknown pane does NOT count as delivered, so the
|
|
1253
|
+
# buffer is preserved (strict) rather than cleared.
|
|
1254
|
+
# Dispatches through fm_backend_send_text_submit (bin/fm-backend.sh): for
|
|
1255
|
+
# backend=tmux this calls fm_backend_tmux_send_text_submit, a verbatim
|
|
1256
|
+
# re-export of fm_tmux_submit_core - byte-identical to calling it directly.
|
|
1257
|
+
retries=${FM_INJECT_CONFIRM_RETRIES:-$INJECT_CONFIRM_RETRIES_DEFAULT}
|
|
1258
|
+
sleep_s=${FM_INJECT_CONFIRM_SLEEP:-$INJECT_CONFIRM_SLEEP_DEFAULT}
|
|
1259
|
+
verdict=$(fm_backend_send_text_submit "$backend" "$target" "$msg" "$retries" "$sleep_s" "$sleep_s")
|
|
1260
|
+
if [ "$verdict" = empty ]; then
|
|
1261
|
+
return 0 # Backend confirmed the submit.
|
|
1262
|
+
fi
|
|
1263
|
+
log "inject failed: submit unconfirmed after $retries retries (verdict=$verdict, text may be in composer)"
|
|
1264
|
+
return 1
|
|
1265
|
+
}
|
|
1266
|
+
|
|
1267
|
+
# --- INJECT_SKIP prefix match (literal prefixes, no regex) ------------------
|
|
1268
|
+
should_force_self() { # <reason>
|
|
1269
|
+
local reason=$1 skip="${FM_INJECT_SKIP:-$INJECT_SKIP_DEFAULT}" prefix
|
|
1270
|
+
[ -n "$skip" ] || return 1
|
|
1271
|
+
local -a prefixes
|
|
1272
|
+
IFS='|' read -ra prefixes <<<"$skip"
|
|
1273
|
+
for prefix in "${prefixes[@]}"; do
|
|
1274
|
+
[ -n "$prefix" ] || continue
|
|
1275
|
+
[ "$reason" != "${reason#"$prefix"}" ] && return 0
|
|
1276
|
+
done
|
|
1277
|
+
return 1
|
|
1278
|
+
}
|
|
1279
|
+
|
|
1280
|
+
# A real watcher WAKE reason starts with one of these prefixes. Anything else on
|
|
1281
|
+
# the watcher child's stdout (e.g. "watcher: already running" on a singleton-lock
|
|
1282
|
+
# collision, reachable if the daemon was SIGKILL'd and its orphaned watcher child
|
|
1283
|
+
# still holds the #29 singleton lock) is a STATUS line, not a wake: handling it
|
|
1284
|
+
# as an unknown wake would flood the escalation buffer and restart the child with
|
|
1285
|
+
# no crash backoff. The main loop treats a non-wake line as idle (log + sleep +
|
|
1286
|
+
# continue), so a singleton collision cannot hot-loop escalations.
|
|
1287
|
+
is_wake_reason() { # <reason>
|
|
1288
|
+
local reason=$1
|
|
1289
|
+
case "$reason" in
|
|
1290
|
+
signal:*|stale:*|check:*|heartbeat|heartbeat:*) return 0 ;;
|
|
1291
|
+
esac
|
|
1292
|
+
return 1
|
|
1293
|
+
}
|
|
1294
|
+
|
|
1295
|
+
# --- dispatch one wake reason to self-handle or escalate --------------------
|
|
1296
|
+
# Side effects: logging, marker records, escalation buffer appends.
|
|
1297
|
+
handle_wake() { # <reason> <state>
|
|
1298
|
+
local reason=$1 state=$2 decision action distilled task last stale_detail
|
|
1299
|
+
local capture="$state/.subsuper-classified-end.$$" span_record='' span_rc='' endpoint ident rest sig marker
|
|
1300
|
+
local kind="" arg="" classification_failed=0 span_failure_repeat=0
|
|
1301
|
+
: > "$capture" || return 1
|
|
1302
|
+
if should_force_self "$reason"; then
|
|
1303
|
+
log "wake force-self (FM_INJECT_SKIP): $reason"
|
|
1304
|
+
rm -f "$capture"
|
|
1305
|
+
return
|
|
1306
|
+
fi
|
|
1307
|
+
case "$reason" in
|
|
1308
|
+
signal:*) kind=signal; arg="${reason#signal: }"
|
|
1309
|
+
decision=$(FM_STATUS_SPAN_ENDPOINT_FILE="$capture" classify_signal "$arg" "$state") ;;
|
|
1310
|
+
stale:*) kind=stale; arg="${reason#stale: }"; stale_detail="${arg#"$arg"}"
|
|
1311
|
+
case "$arg" in *" ("*) stale_detail="${arg#*" ("}"; arg="${arg%% \(*}" ;; esac
|
|
1312
|
+
task=$(window_to_task "$arg" "$state")
|
|
1313
|
+
if [ -n "$task" ]; then
|
|
1314
|
+
span_record=$(status_span_first_actionable_record "$state/$task.status" \
|
|
1315
|
+
"$(status_seen_offset "$state" "$task")")
|
|
1316
|
+
span_rc=$?
|
|
1317
|
+
case "$span_rc" in
|
|
1318
|
+
0|1)
|
|
1319
|
+
if [ -n "$span_record" ]; then endpoint=${span_record%%$'\t'*}; rest=${span_record#*$'\t'}; ident=${rest%%$'\t'*}; printf '%s\t%s\t%s\n' "$task" "$endpoint" "$ident" > "$capture"; fi
|
|
1320
|
+
;;
|
|
1321
|
+
*)
|
|
1322
|
+
sig=$(status_observed_signature "$state/$task.status")
|
|
1323
|
+
marker=$(_seen_status_path "$state" "$task")
|
|
1324
|
+
if status_presentation_marker_reported_matches "$marker" "$sig"; then
|
|
1325
|
+
span_failure_repeat=1
|
|
1326
|
+
else
|
|
1327
|
+
printf 'ERROR\t%s\t%s\n' "$task" "$sig" > "$capture"
|
|
1328
|
+
fi
|
|
1329
|
+
;;
|
|
1330
|
+
esac
|
|
1331
|
+
else
|
|
1332
|
+
span_rc=2
|
|
1333
|
+
printf 'ERROR\t%s\n' "$arg" > "$capture"
|
|
1334
|
+
fi
|
|
1335
|
+
if [ "$span_failure_repeat" -eq 1 ]; then
|
|
1336
|
+
decision="self|unreadable status span already reported for $task"
|
|
1337
|
+
else
|
|
1338
|
+
decision=$(classify_stale "$arg" "$state" "$span_record" "$span_rc")
|
|
1339
|
+
fi
|
|
1340
|
+
# An enriched wedge reason carries the watcher's own escalation count
|
|
1341
|
+
# and its "do not re-absorb on the run-step/pane state alone" demand,
|
|
1342
|
+
# so it outranks this daemon's cheaper status-log absorption - EXCEPT
|
|
1343
|
+
# under a current declared wait. A `pause` verdict is not run-step or
|
|
1344
|
+
# pane state at all: it is the crew's own declaration that this pane
|
|
1345
|
+
# waits by design, which is the one question the wedge timer cannot
|
|
1346
|
+
# answer for itself. Overriding it escalated healthy declared waits
|
|
1347
|
+
# once per STALE_ESCALATE_SECS for as long as the wait lasted.
|
|
1348
|
+
# Housekeeping (2b) then owns the re-surface, so the wait is still
|
|
1349
|
+
# bounded - by one recheck per PAUSE_RESURFACE_SECS instead.
|
|
1350
|
+
case "${decision%%|*}" in
|
|
1351
|
+
pause) : ;;
|
|
1352
|
+
*) case "$stale_detail" in
|
|
1353
|
+
idle\ *s,\ possible\ wedge,\ escalation\ *)
|
|
1354
|
+
last=$(last_status_line "$state/$task.status")
|
|
1355
|
+
status_is_paused_or_captain_held "$last" \
|
|
1356
|
+
|| decision="escalate|${reason#stale: }"
|
|
1357
|
+
;;
|
|
1358
|
+
esac ;;
|
|
1359
|
+
esac ;;
|
|
1360
|
+
check:*) decision=$(classify_check "$reason") ;;
|
|
1361
|
+
heartbeat|heartbeat:*) decision=$(classify_heartbeat) ;;
|
|
1362
|
+
*) decision=$(classify_unknown "$reason") ;;
|
|
1363
|
+
esac
|
|
1364
|
+
action=${decision%%|*}
|
|
1365
|
+
distilled=${decision#*|}
|
|
1366
|
+
[ "$kind" = signal ] && sync_pause_markers_from_signal "$state" "$arg"
|
|
1367
|
+
if [ "$kind" = stale ] && [ "$action" = escalate ]; then
|
|
1368
|
+
task=$(window_to_task "$arg" "$state")
|
|
1369
|
+
last=$(last_status_line "$state/$task.status")
|
|
1370
|
+
reconcile_pause_tracking "$arg" "$state" "$last"
|
|
1371
|
+
fi
|
|
1372
|
+
case "$action" in
|
|
1373
|
+
escalate)
|
|
1374
|
+
log "escalate: $reason -> $distilled"
|
|
1375
|
+
if escalate_add "$state" "$distilled"; then
|
|
1376
|
+
# A terminal-stale escalate must not leave a persistence marker behind, or
|
|
1377
|
+
# housekeeping re-escalates the same pane as a false wedge later.
|
|
1378
|
+
[ "$kind" = "stale" ] && stale_marker_remove "$arg" "$state"
|
|
1379
|
+
mark_escalated_seen "$state" "$capture" || classification_failed=1
|
|
1380
|
+
[ "${FM_ESCALATE_BATCH_SECS:-$ESCALATE_BATCH_SECS_DEFAULT}" -le 0 ] && { escalate_flush "$state" || true; }
|
|
1381
|
+
else
|
|
1382
|
+
classification_failed=1
|
|
1383
|
+
fi
|
|
1384
|
+
;;
|
|
1385
|
+
pause)
|
|
1386
|
+
# Declared wait, an external-wait pause or a verified captain-held transfer:
|
|
1387
|
+
# record a pause marker (long re-surface cadence in housekeeping) and drop any
|
|
1388
|
+
# wedge stale marker, so a pane that transitioned working->declared-wait is not
|
|
1389
|
+
# still wedge-aged. Only stale produces this action.
|
|
1390
|
+
if [ "$kind" = "stale" ]; then
|
|
1391
|
+
stale_marker_remove "$arg" "$state"
|
|
1392
|
+
pause_marker_record "$arg" "$state"
|
|
1393
|
+
fi
|
|
1394
|
+
log "self-handle (paused): $reason -> $distilled"
|
|
1395
|
+
;;
|
|
1396
|
+
*)
|
|
1397
|
+
# Transient (non-terminal) stale: record/refresh the wedge marker so
|
|
1398
|
+
# housekeeping can age it, and drop any pause marker (a crew that left its
|
|
1399
|
+
# pause reverts to normal wedge aging). The persistence recheck, not this
|
|
1400
|
+
# wake, escalates a wedge.
|
|
1401
|
+
if [ "$kind" = "stale" ]; then
|
|
1402
|
+
task=$(window_to_task "$arg" "$state")
|
|
1403
|
+
last=$(last_status_line "$state/$task.status")
|
|
1404
|
+
# Clear wedge aging only for terminal (or legacy free-text) captain lines.
|
|
1405
|
+
# Nonterminal progress verbs keep possible-wedge markers even if free text
|
|
1406
|
+
# once looked captain-relevant or was written into a seen marker.
|
|
1407
|
+
_clear_wedge=0
|
|
1408
|
+
if [ -n "$last" ] && status_is_captain_relevant "$last"; then
|
|
1409
|
+
if status_is_terminal_verb "$last"; then
|
|
1410
|
+
_clear_wedge=1
|
|
1411
|
+
else
|
|
1412
|
+
case "$(status_line_verb "$last")" in
|
|
1413
|
+
working|resolved|captain-held) _clear_wedge=0 ;;
|
|
1414
|
+
*) _clear_wedge=1 ;;
|
|
1415
|
+
esac
|
|
1416
|
+
fi
|
|
1417
|
+
fi
|
|
1418
|
+
if [ "$_clear_wedge" = 1 ]; then
|
|
1419
|
+
stale_marker_remove "$arg" "$state"
|
|
1420
|
+
else
|
|
1421
|
+
pause_marker_remove "$arg" "$state"
|
|
1422
|
+
stale_marker_record "$arg" "$state"
|
|
1423
|
+
fi
|
|
1424
|
+
fi
|
|
1425
|
+
log "self-handle: $reason -> $distilled"
|
|
1426
|
+
;;
|
|
1427
|
+
esac
|
|
1428
|
+
if [ "$action" = self ] && { [ "$kind" = signal ] || [ "$kind" = stale ]; }; then
|
|
1429
|
+
mark_escalated_seen "$state" "$capture" || classification_failed=1
|
|
1430
|
+
fi
|
|
1431
|
+
rm -f "$capture"
|
|
1432
|
+
[ "$classification_failed" -eq 0 ]
|
|
1433
|
+
}
|
|
1434
|
+
|
|
1435
|
+
handle_durable_wakes() { # <watcher-reason> <state>
|
|
1436
|
+
local fallback_reason=$1 state=$2 out err tab epoch sequence kind key payload rest
|
|
1437
|
+
local handled=0 failed=0 ack_through ack_generation
|
|
1438
|
+
out=$(mktemp "$state/.subsuper-wake-drain.XXXXXX") || return 1
|
|
1439
|
+
err=$(mktemp "$state/.subsuper-wake-drain.XXXXXX") || { rm -f "$out"; return 1; }
|
|
1440
|
+
if ! "$FM_DAEMON_DIR/fm-wake-drain.sh" > "$out" 2> "$err"; then
|
|
1441
|
+
cat "$err" >&2
|
|
1442
|
+
rm -f "$out" "$err"
|
|
1443
|
+
return 1
|
|
1444
|
+
fi
|
|
1445
|
+
|
|
1446
|
+
tab=$(printf '\t')
|
|
1447
|
+
while IFS="$tab" read -r epoch sequence kind key payload rest; do
|
|
1448
|
+
case "$epoch" in ''|*[!0-9]*) continue ;; esac
|
|
1449
|
+
case "$sequence" in ''|*[!0-9]*) continue ;; esac
|
|
1450
|
+
case "$kind" in signal|stale|check|heartbeat) ;; *) continue ;; esac
|
|
1451
|
+
handle_wake "$payload" "$state" || failed=1
|
|
1452
|
+
handled=$((handled + 1))
|
|
1453
|
+
done < "$out"
|
|
1454
|
+
if [ "$handled" -eq 0 ]; then handle_wake "$fallback_reason" "$state" || failed=1; fi
|
|
1455
|
+
|
|
1456
|
+
ack_through=$(sed -n 's/^WAKE_ACK_REQUIRED:.*--ack-through \([0-9][0-9]*\) --recovery-generation [A-Za-z0-9._-][A-Za-z0-9._-]*$/\1/p' "$err" | tail -1)
|
|
1457
|
+
ack_generation=$(sed -n 's/^WAKE_ACK_REQUIRED:.*--ack-through [0-9][0-9]* --recovery-generation \([A-Za-z0-9._-][A-Za-z0-9._-]*\)$/\1/p' "$err" | tail -1)
|
|
1458
|
+
grep -v '^WAKE_ACK_REQUIRED:' "$err" >&2 || true
|
|
1459
|
+
rm -f "$out" "$err"
|
|
1460
|
+
if [ "$failed" -ne 0 ]; then
|
|
1461
|
+
log "wake classification failed; retaining durable wakes"
|
|
1462
|
+
return 1
|
|
1463
|
+
fi
|
|
1464
|
+
if [ -z "$ack_through" ] || [ -z "$ack_generation" ]; then
|
|
1465
|
+
log "wake drain omitted its generation-bound acknowledgement; retaining durable wakes"
|
|
1466
|
+
return 1
|
|
1467
|
+
fi
|
|
1468
|
+
"$FM_DAEMON_DIR/fm-wake-drain.sh" --ack-through "$ack_through" \
|
|
1469
|
+
--recovery-generation "$ack_generation"
|
|
1470
|
+
}
|
|
1471
|
+
|
|
1472
|
+
# --- log --------------------------------------------------------------------
|
|
1473
|
+
# Uses LOG set by fm_super_main; harmless no-op-ish if unset (tests source fns
|
|
1474
|
+
# directly and pass state explicitly, so they do not call log).
|
|
1475
|
+
log() { [ -n "${LOG:-}" ] && printf '[%s] %s\n' "$(date '+%Y-%m-%dT%H:%M:%S%z')" "$*" >> "$LOG"; }
|
|
1476
|
+
|
|
1477
|
+
trim_log() {
|
|
1478
|
+
local sz tmp
|
|
1479
|
+
[ -n "${LOG:-}" ] || return 0
|
|
1480
|
+
sz=$(wc -c < "$LOG" 2>/dev/null) || return 0
|
|
1481
|
+
[ "$sz" -ge "${FM_LOG_MAX_BYTES:-$LOG_MAX_BYTES_DEFAULT}" ] || return 0
|
|
1482
|
+
tmp=$(mktemp "${TMPDIR:-/tmp}/fm-daemon-log.XXXXXX") || return 0
|
|
1483
|
+
tail -n "${FM_LOG_KEEP_LINES:-$LOG_KEEP_LINES_DEFAULT}" "$LOG" >"$tmp" 2>/dev/null && mv -f "$tmp" "$LOG"
|
|
1484
|
+
}
|
|
1485
|
+
|
|
1486
|
+
# ============================================================================
|
|
1487
|
+
# Everything below runs only when the script is EXECUTED, not sourced. The pure
|
|
1488
|
+
# classifiers above are sourceable for unit tests (tests/fm-daemon.test.sh).
|
|
1489
|
+
# ============================================================================
|
|
1490
|
+
|
|
1491
|
+
fm_super_main() {
|
|
1492
|
+
local STATE
|
|
1493
|
+
STATE="$(_state_root)"
|
|
1494
|
+
mkdir -p "$STATE"
|
|
1495
|
+
|
|
1496
|
+
# Source the portable lock helpers (works on macOS where flock is absent).
|
|
1497
|
+
# Export FM_STATE_OVERRIDE so the lib resolves the same state dir.
|
|
1498
|
+
# shellcheck source=bin/fm-wake-lib.sh
|
|
1499
|
+
FM_STATE_OVERRIDE="$STATE" . "$FM_DAEMON_DIR/fm-wake-lib.sh"
|
|
1500
|
+
|
|
1501
|
+
local WATCH="$FM_DAEMON_DIR/fm-watch.sh"
|
|
1502
|
+
local LOG="$STATE/.supervise-daemon.log"
|
|
1503
|
+
local WATCH_ERR="$STATE/.supervise-daemon.watcher.err"
|
|
1504
|
+
local LOCK="$STATE/.supervise-daemon.lock"
|
|
1505
|
+
local PIDFILE="$STATE/.supervise-daemon.pid"
|
|
1506
|
+
local INJECT_FAIL_SLEEP=${FM_INJECT_FAIL_SLEEP:-$INJECT_FAIL_SLEEP_DEFAULT}
|
|
1507
|
+
local CRASH_THRESHOLD=${FM_CRASH_THRESHOLD:-$CRASH_THRESHOLD_DEFAULT}
|
|
1508
|
+
local CRASH_WINDOW=${FM_CRASH_WINDOW:-$CRASH_WINDOW_DEFAULT}
|
|
1509
|
+
local CRASH_BACKOFF=${FM_CRASH_BACKOFF:-$CRASH_BACKOFF_DEFAULT}
|
|
1510
|
+
local CRASH_NORMAL_SLEEP=${FM_CRASH_NORMAL_SLEEP:-$CRASH_NORMAL_SLEEP_DEFAULT}
|
|
1511
|
+
|
|
1512
|
+
[ -x "$WATCH" ] || { echo "error: watcher not found or not executable: $WATCH" >&2; exit 1; }
|
|
1513
|
+
|
|
1514
|
+
# --- single instance (portable lock, no flock dependency) ------------------
|
|
1515
|
+
if ! fm_lock_try_acquire "$LOCK"; then
|
|
1516
|
+
if [ -n "${FM_LOCK_HELD_PID:-}" ]; then
|
|
1517
|
+
echo "error: another fm-supervise-daemon is already running (pid $FM_LOCK_HELD_PID, lock $LOCK held)" >&2
|
|
1518
|
+
else
|
|
1519
|
+
echo "error: another fm-supervise-daemon is already running (lock $LOCK held)" >&2
|
|
1520
|
+
fi
|
|
1521
|
+
exit 1
|
|
1522
|
+
fi
|
|
1523
|
+
echo "$$" > "$PIDFILE"
|
|
1524
|
+
fm_pid_identity "${BASHPID:-$$}" > "$LOCK/pid-identity" 2>/dev/null || true
|
|
1525
|
+
|
|
1526
|
+
# --- auto-discover the supervisor BACKEND (tmux vs herdr) first -----------
|
|
1527
|
+
# Priority: FM_SUPERVISOR_BACKEND override > $TMUX_PANE (tmux) > $HERDR_ENV=1
|
|
1528
|
+
# (herdr) > tmux fallback. Resolved before the target below, since target
|
|
1529
|
+
# discovery composes a herdr "<session>:<pane-id>" string using the same
|
|
1530
|
+
# $HERDR_PANE_ID/$HERDR_SESSION markers this checks. Exporting the result
|
|
1531
|
+
# into FM_SUPERVISOR_BACKEND makes inject_msg/pane_is_busy/pane_input_pending
|
|
1532
|
+
# (which read that env var) dispatch through the right backend without an
|
|
1533
|
+
# extra global thread-through.
|
|
1534
|
+
local discovered_backend backend_source
|
|
1535
|
+
backend_source="FM_SUPERVISOR_BACKEND"
|
|
1536
|
+
if [ -z "${FM_SUPERVISOR_BACKEND:-}" ]; then
|
|
1537
|
+
if [ -n "${TMUX_PANE:-}" ]; then
|
|
1538
|
+
backend_source="TMUX_PANE"
|
|
1539
|
+
elif [ "${HERDR_ENV:-}" = "1" ] && [ -n "${HERDR_PANE_ID:-}" ]; then
|
|
1540
|
+
backend_source="HERDR_ENV"
|
|
1541
|
+
else
|
|
1542
|
+
backend_source="FALLBACK($FM_SUPERVISOR_BACKEND_DEFAULT)"
|
|
1543
|
+
fi
|
|
1544
|
+
fi
|
|
1545
|
+
discovered_backend=$(discover_supervisor_backend) || true
|
|
1546
|
+
FM_SUPERVISOR_BACKEND="$discovered_backend"
|
|
1547
|
+
local BACKEND="$FM_SUPERVISOR_BACKEND"
|
|
1548
|
+
|
|
1549
|
+
# --- refuse an unsupported supervisor backend loudly, before ever trying a
|
|
1550
|
+
# tmux/herdr-specific call against it (zellij, orca, and cmux have no verified
|
|
1551
|
+
# composer/busy primitives wired up for this daemon yet - AGENTS.md section 4
|
|
1552
|
+
# harness-verification discipline). This is the clear refusal the task calls
|
|
1553
|
+
# for, instead of a confusing "does not resolve to a tmux pane" error.
|
|
1554
|
+
if ! fm_backend_list_contains "$FM_SUPERVISOR_SUPPORTED_BACKENDS" "$BACKEND"; then
|
|
1555
|
+
echo "error: away-mode daemon does not support supervisor backend '$BACKEND' yet (supported: $FM_SUPERVISOR_SUPPORTED_BACKENDS); set FM_SUPERVISOR_BACKEND=tmux|herdr and FM_SUPERVISOR_TARGET to run firstmate's own pane under a supported backend" >&2
|
|
1556
|
+
log "startup failed: unsupported supervisor backend '$BACKEND' (source=$backend_source)"
|
|
1557
|
+
fm_lock_release "$LOCK" 2>/dev/null || true
|
|
1558
|
+
rm -f "$PIDFILE" 2>/dev/null || true
|
|
1559
|
+
exit 1
|
|
1560
|
+
fi
|
|
1561
|
+
|
|
1562
|
+
# --- auto-discover the supervisor target (the pane running firstmate) -----
|
|
1563
|
+
# Priority: FM_SUPERVISOR_TARGET override > $TMUX_PANE (tmux; inherited from
|
|
1564
|
+
# the pane that launched the daemon, normally firstmate's own) >
|
|
1565
|
+
# $HERDR_PANE_ID (herdr, composed into "<session>:<pane-id>") > firstmate:0
|
|
1566
|
+
# fallback. Exporting the result into FM_SUPERVISOR_TARGET makes inject_msg
|
|
1567
|
+
# (which reads that env var) use the discovered pane without an extra global.
|
|
1568
|
+
local discovered target_source
|
|
1569
|
+
target_source="FM_SUPERVISOR_TARGET"
|
|
1570
|
+
if [ -z "${FM_SUPERVISOR_TARGET:-}" ]; then
|
|
1571
|
+
if [ -n "${TMUX_PANE:-}" ]; then
|
|
1572
|
+
target_source="TMUX_PANE"
|
|
1573
|
+
elif [ "${HERDR_ENV:-}" = "1" ] && [ -n "${HERDR_PANE_ID:-}" ]; then
|
|
1574
|
+
target_source="HERDR_ENV(HERDR_PANE_ID)"
|
|
1575
|
+
else
|
|
1576
|
+
target_source="FALLBACK(firstmate:0)"
|
|
1577
|
+
fi
|
|
1578
|
+
fi
|
|
1579
|
+
if discovered=$(discover_supervisor_target); then
|
|
1580
|
+
: # resolved cleanly
|
|
1581
|
+
else
|
|
1582
|
+
echo "warn: could not auto-discover supervisor pane (no FM_SUPERVISOR_TARGET, TMUX_PANE, or HERDR_ENV/HERDR_PANE_ID); falling back to '$discovered' — verify this is firstmate's pane" >&2
|
|
1583
|
+
fi
|
|
1584
|
+
FM_SUPERVISOR_TARGET="$discovered"
|
|
1585
|
+
local TARGET="$FM_SUPERVISOR_TARGET"
|
|
1586
|
+
|
|
1587
|
+
# --- validate supervisor target at startup (a missing target is a typo) ---
|
|
1588
|
+
# Dispatches through bin/fm-backend.sh instead of a raw `tmux display-message`
|
|
1589
|
+
# probe, so a herdr supervisor pane is checked via the herdr adapter; for
|
|
1590
|
+
# backend=tmux this runs the exact same `tmux display-message -p -t "$TARGET"
|
|
1591
|
+
# '#{pane_id}'` call as before.
|
|
1592
|
+
if ! fm_backend_target_exists "$BACKEND" "$TARGET"; then
|
|
1593
|
+
echo "error: supervisor target '$TARGET' does not resolve to a $BACKEND pane; set FM_SUPERVISOR_TARGET" >&2
|
|
1594
|
+
log "startup failed: target '$TARGET' not found (backend=$BACKEND)"
|
|
1595
|
+
fm_lock_release "$LOCK" 2>/dev/null || true
|
|
1596
|
+
rm -f "$PIDFILE" 2>/dev/null || true
|
|
1597
|
+
exit 1
|
|
1598
|
+
fi
|
|
1599
|
+
|
|
1600
|
+
local afk_status="off"
|
|
1601
|
+
afk_active "$STATE" && afk_status="on"
|
|
1602
|
+
log "daemon starting (pid $$); target=$TARGET; target_source=$target_source; backend=$BACKEND; backend_source=$backend_source; afk=$afk_status; inject_skip='${FM_INJECT_SKIP:-$INJECT_SKIP_DEFAULT}'; stale_escalate=${FM_STALE_ESCALATE_SECS:-$STALE_ESCALATE_SECS_DEFAULT}s; batch=${FM_ESCALATE_BATCH_SECS:-$ESCALATE_BATCH_SECS_DEFAULT}s"
|
|
1603
|
+
migrate_watcher_pause_markers "$STATE"
|
|
1604
|
+
|
|
1605
|
+
# --- shutdown: flush buffered escalations, reap child, release lock -------
|
|
1606
|
+
local WATCHER_PID="" CUR_TMP=""
|
|
1607
|
+
cleanup() {
|
|
1608
|
+
trap - TERM INT
|
|
1609
|
+
wedge_alarm_stop_active_notifier
|
|
1610
|
+
escalate_flush "$STATE" 2>/dev/null || true
|
|
1611
|
+
if [ -n "${WATCHER_PID:-}" ]; then
|
|
1612
|
+
kill "$WATCHER_PID" 2>/dev/null || true
|
|
1613
|
+
wait "$WATCHER_PID" 2>/dev/null || true
|
|
1614
|
+
fi
|
|
1615
|
+
if [ -n "${CUR_TMP:-}" ]; then
|
|
1616
|
+
rm -f "$CUR_TMP" 2>/dev/null || true
|
|
1617
|
+
fi
|
|
1618
|
+
fm_lock_release "$LOCK" 2>/dev/null || true
|
|
1619
|
+
rm -f "$PIDFILE" 2>/dev/null || true
|
|
1620
|
+
log "daemon shutting down"
|
|
1621
|
+
exit 0
|
|
1622
|
+
}
|
|
1623
|
+
trap cleanup TERM INT
|
|
1624
|
+
|
|
1625
|
+
# --- crash-loop guard -----------------------------------------------------
|
|
1626
|
+
local crash_times=() backoff_secs=$CRASH_NORMAL_SLEEP
|
|
1627
|
+
record_crash() {
|
|
1628
|
+
local now t
|
|
1629
|
+
now=$(_now)
|
|
1630
|
+
local -a keep=()
|
|
1631
|
+
for t in "${crash_times[@]:-}"; do
|
|
1632
|
+
[ -n "$t" ] && [ $((now - t)) -lt "$CRASH_WINDOW" ] && keep+=("$t")
|
|
1633
|
+
done
|
|
1634
|
+
keep+=("$now")
|
|
1635
|
+
crash_times=("${keep[@]}")
|
|
1636
|
+
if [ "${#crash_times[@]}" -gt "$CRASH_THRESHOLD" ]; then
|
|
1637
|
+
log "ERROR: watcher crashed ${#crash_times[@]} times within ${CRASH_WINDOW}s; backing off ${CRASH_BACKOFF}s"
|
|
1638
|
+
crash_times=()
|
|
1639
|
+
backoff_secs=$CRASH_BACKOFF
|
|
1640
|
+
else
|
|
1641
|
+
backoff_secs=$CRASH_NORMAL_SLEEP
|
|
1642
|
+
fi
|
|
1643
|
+
}
|
|
1644
|
+
|
|
1645
|
+
start_watcher() {
|
|
1646
|
+
CUR_TMP=$(mktemp "${TMPDIR:-/tmp}/fm-watch.XXXXXX") || { log "error: mktemp failed; retrying in 5s"; sleep 5; return 1; }
|
|
1647
|
+
"$WATCH" >"$CUR_TMP" 2>>"$WATCH_ERR" &
|
|
1648
|
+
WATCHER_PID=$!
|
|
1649
|
+
}
|
|
1650
|
+
|
|
1651
|
+
local rc reason
|
|
1652
|
+
while true; do
|
|
1653
|
+
# --- pane-gone guard (preserved) ---------------------------------------
|
|
1654
|
+
# With the #29 watcher's enqueue-before-suppress, a wake is no longer
|
|
1655
|
+
# swallowed by running the watcher with no injection target. We still back
|
|
1656
|
+
# off while the pane is gone: self-handling needs no pane, but escalation
|
|
1657
|
+
# has nowhere to go, and firstmate itself is the consumer of escalations.
|
|
1658
|
+
# Catch-up signals persist in state/*.status and flow on the next run, so
|
|
1659
|
+
# this delays rather than loses work.
|
|
1660
|
+
if ! fm_backend_target_exists "$BACKEND" "$TARGET"; then
|
|
1661
|
+
log "warn: supervisor target '$TARGET' gone; backing off ${INJECT_FAIL_SLEEP}s, will retry"
|
|
1662
|
+
# Flush is pointless with no pane; preserve any buffered escalations.
|
|
1663
|
+
sleep "$INJECT_FAIL_SLEEP"
|
|
1664
|
+
continue
|
|
1665
|
+
fi
|
|
1666
|
+
|
|
1667
|
+
# --- (re)start watcher if it has exited --------------------------------
|
|
1668
|
+
if [ -z "${WATCHER_PID:-}" ] || ! kill -0 "${WATCHER_PID:-}" 2>/dev/null; then
|
|
1669
|
+
if [ -n "${WATCHER_PID:-}" ]; then
|
|
1670
|
+
# child exited: reap + classify its wake reason
|
|
1671
|
+
if wait "${WATCHER_PID}"; then rc=0; else rc=$?; fi
|
|
1672
|
+
reason=""
|
|
1673
|
+
if [ -n "${CUR_TMP:-}" ] && [ -e "${CUR_TMP:-}" ]; then
|
|
1674
|
+
reason=$(<"${CUR_TMP}")
|
|
1675
|
+
fi
|
|
1676
|
+
if [ -n "${CUR_TMP:-}" ]; then
|
|
1677
|
+
rm -f "${CUR_TMP}" 2>/dev/null || true
|
|
1678
|
+
fi
|
|
1679
|
+
CUR_TMP=""
|
|
1680
|
+
if [ "$rc" -ne 0 ] || [ -z "$reason" ]; then
|
|
1681
|
+
record_crash
|
|
1682
|
+
log "watcher exited rc=$rc reason='$reason'; restarting after ${backoff_secs}s"
|
|
1683
|
+
WATCHER_PID=""
|
|
1684
|
+
sleep "$backoff_secs"
|
|
1685
|
+
continue
|
|
1686
|
+
fi
|
|
1687
|
+
# Non-wake stdout (e.g. a watcher singleton-collision "already running"
|
|
1688
|
+
# status line) is NOT a wake: idling here prevents an escalation flood
|
|
1689
|
+
# and a backoff-less child restart. record_crash is intentionally
|
|
1690
|
+
# skipped (rc=0, this is normal idle, not a crash).
|
|
1691
|
+
if ! is_wake_reason "$reason"; then
|
|
1692
|
+
log "watcher non-wake stdout, idling: $reason"
|
|
1693
|
+
WATCHER_PID=""
|
|
1694
|
+
sleep "${HOUSEKEEPING_TICK:-$HOUSEKEEPING_TICK_DEFAULT}"
|
|
1695
|
+
continue
|
|
1696
|
+
fi
|
|
1697
|
+
log "wake: $reason"
|
|
1698
|
+
if ! handle_durable_wakes "$reason" "$STATE"; then
|
|
1699
|
+
log "durable wake handling was not acknowledged; restarting for recovery"
|
|
1700
|
+
fi
|
|
1701
|
+
trim_log
|
|
1702
|
+
fi
|
|
1703
|
+
start_watcher || continue
|
|
1704
|
+
fi
|
|
1705
|
+
|
|
1706
|
+
# --- one housekeeping tick (gated to HOUSEKEEPING_TICK), then poll -------
|
|
1707
|
+
# The watcher child runs on its own FM_POLL cadence internally; we only need
|
|
1708
|
+
# to detect its exit (the kill -0 above) promptly and run housekeeping often
|
|
1709
|
+
# enough that batch flushes, stale rechecks, and the catch-all scan fire on
|
|
1710
|
+
# cadence. Gating keeps a large fleet cheap between ticks.
|
|
1711
|
+
sleep 1
|
|
1712
|
+
if [ "$(_file_age "$STATE/.subsuper-last-housekeep")" -ge "${FM_HOUSEKEEPING_TICK:-$HOUSEKEEPING_TICK_DEFAULT}" ]; then
|
|
1713
|
+
_now > "$STATE/.subsuper-last-housekeep"
|
|
1714
|
+
housekeeping "$STATE"
|
|
1715
|
+
fi
|
|
1716
|
+
done
|
|
1717
|
+
}
|
|
1718
|
+
|
|
1719
|
+
# Run only when executed, not when sourced (tests source the classifiers).
|
|
1720
|
+
if [ "${BASH_SOURCE[0]}" = "${0}" ]; then
|
|
1721
|
+
fm_super_main "$@"
|
|
1722
|
+
else
|
|
1723
|
+
# Library mode: these functions were SOURCED (only tests do this - production
|
|
1724
|
+
# execs the daemon, see bin/fm-afk-start.sh). Make it structurally impossible
|
|
1725
|
+
# for a sourced context to fire a real desktop notification from the wedge
|
|
1726
|
+
# alarm: default the FM_WEDGE_ALARM_EXEC notifier seam to "discard" unless the
|
|
1727
|
+
# embedder already wired one (e.g. a recorder in tests/wake-helpers.sh). It is
|
|
1728
|
+
# exported so a real daemon a test later spawns inherits the safe default too.
|
|
1729
|
+
# The executed branch above never runs this, so production is untouched.
|
|
1730
|
+
: "${FM_WEDGE_ALARM_EXEC:=discard}"
|
|
1731
|
+
export FM_WEDGE_ALARM_EXEC
|
|
1732
|
+
fi
|