@zerwiz/ymir 0.1.4 → 0.1.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.agents/README.md +291 -0
- package/.agents/agents/bragi-marketer.md +111 -0
- package/.agents/agents/brokk.md +352 -0
- package/.agents/agents/forseti-reviewer.md +93 -0
- package/.agents/agents/frigg-consent.md +77 -0
- package/.agents/agents/groa-update.md +77 -0
- package/.agents/agents/hnoss-designer.md +69 -0
- package/.agents/agents/huginn-researcher.md +99 -0
- package/.agents/agents/jord-projects.md +78 -0
- package/.agents/agents/kvasir-scout.md +91 -0
- package/.agents/agents/mimir-planner.md +92 -0
- package/.agents/agents/muninn-stow.md +77 -0
- package/.agents/agents/saga-bearings.md +78 -0
- package/.agents/agents/sindri-developer.md +114 -0
- package/.agents/agents/snotra-documenter.md +105 -0
- package/.agents/agents/syn-recovery.md +78 -0
- package/.agents/agents/tyr-check.md +78 -0
- package/.agents/agents/urdh-hold.md +77 -0
- package/.agents/agents/volundr-smith.md +80 -0
- package/.agents/agents/vor-diagnostics.md +77 -0
- package/.agents/assets/agents/naming.md +50 -0
- package/.agents/assets/agents/registry.md +93 -0
- package/.agents/assets/agents/runtime.md +36 -0
- package/.agents/assets/agents/toon-tasks-cli.md +42 -0
- package/.agents/assets/templates/company_entity.template.md +37 -0
- package/.agents/backend/backends/cmux.sh +649 -0
- package/.agents/backend/backends/herdr-eventwait.py +157 -0
- package/.agents/backend/backends/herdr-workspace-move.py +114 -0
- package/.agents/backend/backends/herdr.sh +3386 -0
- package/.agents/backend/backends/orca.sh +290 -0
- package/.agents/backend/backends/tmux.sh +357 -0
- package/.agents/backend/backends/zellij.sh +700 -0
- package/.agents/backend/fm-afk-launch.sh +654 -0
- package/.agents/backend/fm-afk-return.sh +241 -0
- package/.agents/backend/fm-afk-start.sh +172 -0
- package/.agents/backend/fm-arm-command-policy.mjs +960 -0
- package/.agents/backend/fm-arm-pretool-check.sh +200 -0
- package/.agents/backend/fm-backend-hometag-lib.sh +52 -0
- package/.agents/backend/fm-backend.sh +981 -0
- package/.agents/backend/fm-backlog-handoff.sh +909 -0
- package/.agents/backend/fm-backlog-receive.sh +187 -0
- package/.agents/backend/fm-backlog-transition-lib.sh +779 -0
- package/.agents/backend/fm-bearings-board.sh +204 -0
- package/.agents/backend/fm-bearings-snapshot.sh +546 -0
- package/.agents/backend/fm-bootstrap.sh +1568 -0
- package/.agents/backend/fm-branch-outcome.sh +203 -0
- package/.agents/backend/fm-branch-prompt.sh +101 -0
- package/.agents/backend/fm-brief.sh +452 -0
- package/.agents/backend/fm-busy-event.sh +234 -0
- package/.agents/backend/fm-busy-lib.sh +986 -0
- package/.agents/backend/fm-captain-hold.sh +1001 -0
- package/.agents/backend/fm-cd-command-policy.mjs +153 -0
- package/.agents/backend/fm-cd-pretool-check.sh +190 -0
- package/.agents/backend/fm-check-lib.sh +74 -0
- package/.agents/backend/fm-check-register.sh +42 -0
- package/.agents/backend/fm-check-unregister.sh +52 -0
- package/.agents/backend/fm-classify-lib.sh +1770 -0
- package/.agents/backend/fm-claude-stop-autoarm.sh +334 -0
- package/.agents/backend/fm-composer-lib.sh +1416 -0
- package/.agents/backend/fm-config-inherit-lib.sh +1202 -0
- package/.agents/backend/fm-config-push.sh +254 -0
- package/.agents/backend/fm-control-lib.sh +251 -0
- package/.agents/backend/fm-control.sh +878 -0
- package/.agents/backend/fm-crew-state.sh +625 -0
- package/.agents/backend/fm-cursor-lib.sh +243 -0
- package/.agents/backend/fm-decision-hold.sh +232 -0
- package/.agents/backend/fm-doc-audience-check.sh +269 -0
- package/.agents/backend/fm-dod-lib.sh +67 -0
- package/.agents/backend/fm-ensure-agents-md.sh +253 -0
- package/.agents/backend/fm-extension-launch-barrier.mjs +129 -0
- package/.agents/backend/fm-extension.mjs +2577 -0
- package/.agents/backend/fm-extension.sh +16 -0
- package/.agents/backend/fm-ff-lib.sh +420 -0
- package/.agents/backend/fm-fleet-snapshot.sh +1463 -0
- package/.agents/backend/fm-fleet-sync.sh +458 -0
- package/.agents/backend/fm-fleet-view.sh +96 -0
- package/.agents/backend/fm-gate-refuse-lib.sh +102 -0
- package/.agents/backend/fm-guard.sh +242 -0
- package/.agents/backend/fm-harness.sh +195 -0
- package/.agents/backend/fm-herdr-ci-cleanup.sh +115 -0
- package/.agents/backend/fm-herdr-lab.sh +346 -0
- package/.agents/backend/fm-herdr-session-cleanup.sh +337 -0
- package/.agents/backend/fm-home-seed.sh +984 -0
- package/.agents/backend/fm-home-summary-refresh.sh +255 -0
- package/.agents/backend/fm-hook-host-lib.sh +36 -0
- package/.agents/backend/fm-inactive-reconcile.sh +531 -0
- package/.agents/backend/fm-inbox.sh +399 -0
- package/.agents/backend/fm-install-actionlint.sh +84 -0
- package/.agents/backend/fm-install-herdr.sh +96 -0
- package/.agents/backend/fm-install-shellcheck.sh +84 -0
- package/.agents/backend/fm-install-treehouse.sh +95 -0
- package/.agents/backend/fm-kimi-turnend-hook.sh +276 -0
- package/.agents/backend/fm-lease-lib.sh +218 -0
- package/.agents/backend/fm-lease.sh +189 -0
- package/.agents/backend/fm-line-cap-lib.sh +51 -0
- package/.agents/backend/fm-lint-workflows.sh +137 -0
- package/.agents/backend/fm-lint.sh +591 -0
- package/.agents/backend/fm-lock-lib.sh +104 -0
- package/.agents/backend/fm-lock.sh +107 -0
- package/.agents/backend/fm-marker-lib.sh +12 -0
- package/.agents/backend/fm-merge-local.sh +74 -0
- package/.agents/backend/fm-merge-outcome-lib.sh +138 -0
- package/.agents/backend/fm-nm-run-lib.sh +124 -0
- package/.agents/backend/fm-on.sh +125 -0
- package/.agents/backend/fm-operational-input.sh +253 -0
- package/.agents/backend/fm-peek.sh +46 -0
- package/.agents/backend/fm-pending-reply-lib.sh +1409 -0
- package/.agents/backend/fm-pr-check.sh +135 -0
- package/.agents/backend/fm-pr-lib.sh +1016 -0
- package/.agents/backend/fm-pr-merge.sh +707 -0
- package/.agents/backend/fm-pr-poll.sh +110 -0
- package/.agents/backend/fm-primary-scope-lib.sh +33 -0
- package/.agents/backend/fm-procevent-extension-capture.pl +259 -0
- package/.agents/backend/fm-procevent-lavish.sh +628 -0
- package/.agents/backend/fm-procevent-lib.sh +892 -0
- package/.agents/backend/fm-procevent-quota.sh +290 -0
- package/.agents/backend/fm-procevent-remote-reply.sh +577 -0
- package/.agents/backend/fm-procevent-when.sh +504 -0
- package/.agents/backend/fm-procevent.sh +1466 -0
- package/.agents/backend/fm-project-mode.sh +92 -0
- package/.agents/backend/fm-project-origin-lib.sh +180 -0
- package/.agents/backend/fm-promote.sh +282 -0
- package/.agents/backend/fm-public-followup-emit.sh +260 -0
- package/.agents/backend/fm-public-followup-lib.sh +398 -0
- package/.agents/backend/fm-public-followup.sh +1330 -0
- package/.agents/backend/fm-push-transition-lib.sh +168 -0
- package/.agents/backend/fm-quota-axi-lib.sh +93 -0
- package/.agents/backend/fm-quota-choose.sh +384 -0
- package/.agents/backend/fm-remote-delta-read.sh +189 -0
- package/.agents/backend/fm-remote-doctor.sh +800 -0
- package/.agents/backend/fm-remote-entrypoint.sh +193 -0
- package/.agents/backend/fm-remote-file.sh +234 -0
- package/.agents/backend/fm-remote-home-provision.sh +259 -0
- package/.agents/backend/fm-remote-home-seed.sh +259 -0
- package/.agents/backend/fm-remote-inherit-push.sh +90 -0
- package/.agents/backend/fm-remote-inherit.sh +179 -0
- package/.agents/backend/fm-remote-job-lib.sh +1241 -0
- package/.agents/backend/fm-remote-job-reap-orphans.sh +137 -0
- package/.agents/backend/fm-remote-job-worker.sh +1124 -0
- package/.agents/backend/fm-remote-readiness-lib.sh +44 -0
- package/.agents/backend/fm-remote-secondmate-control.sh +340 -0
- package/.agents/backend/fm-review-diff.sh +158 -0
- package/.agents/backend/fm-secondmate-charter-lib.sh +44 -0
- package/.agents/backend/fm-secondmate-nudge-lib.sh +68 -0
- package/.agents/backend/fm-secondmate-parent-lib.sh +70 -0
- package/.agents/backend/fm-secondmate-reconcile.sh +371 -0
- package/.agents/backend/fm-secondmate-registry-lib.sh +311 -0
- package/.agents/backend/fm-secondmate-report.sh +88 -0
- package/.agents/backend/fm-send.sh +1059 -0
- package/.agents/backend/fm-session-lock-lib.sh +176 -0
- package/.agents/backend/fm-session-start.sh +970 -0
- package/.agents/backend/fm-sessionstart-cursor.sh +40 -0
- package/.agents/backend/fm-sessionstart-nudge.sh +45 -0
- package/.agents/backend/fm-sessionstart-run.sh +145 -0
- package/.agents/backend/fm-spawn.sh +3153 -0
- package/.agents/backend/fm-startup-memory-budget-lib.sh +224 -0
- package/.agents/backend/fm-startup-memory-budget.sh +94 -0
- package/.agents/backend/fm-startup-network.sh +642 -0
- package/.agents/backend/fm-stow-cascade.sh +251 -0
- package/.agents/backend/fm-subagent-pretool-check.sh +207 -0
- package/.agents/backend/fm-supervise-daemon.sh +1732 -0
- package/.agents/backend/fm-supervision-instructions.sh +215 -0
- package/.agents/backend/fm-supervision-lib.sh +90 -0
- package/.agents/backend/fm-supervisor-target-lib.sh +78 -0
- package/.agents/backend/fm-tangle-lib.sh +53 -0
- package/.agents/backend/fm-task-inbox-lib.sh +402 -0
- package/.agents/backend/fm-tasks-axi-lib.sh +123 -0
- package/.agents/backend/fm-teardown.sh +2922 -0
- package/.agents/backend/fm-test-isolation-proof.sh +588 -0
- package/.agents/backend/fm-test-run.sh +2191 -0
- package/.agents/backend/fm-timeout-lib.sh +141 -0
- package/.agents/backend/fm-timing-lib.sh +179 -0
- package/.agents/backend/fm-tmux-lib.sh +291 -0
- package/.agents/backend/fm-tool-update-check.sh +898 -0
- package/.agents/backend/fm-trace-context-lib.sh +227 -0
- package/.agents/backend/fm-transition-lib.sh +103 -0
- package/.agents/backend/fm-turnend-guard-cursor.sh +391 -0
- package/.agents/backend/fm-turnend-guard-grok.sh +90 -0
- package/.agents/backend/fm-turnend-guard.sh +432 -0
- package/.agents/backend/fm-update.sh +110 -0
- package/.agents/backend/fm-vendor-auth-probe.sh +190 -0
- package/.agents/backend/fm-voice-client.py +1373 -0
- package/.agents/backend/fm-voice-relay.py +1256 -0
- package/.agents/backend/fm-wake-drain.sh +592 -0
- package/.agents/backend/fm-wake-grant.sh +127 -0
- package/.agents/backend/fm-wake-lib.sh +1849 -0
- package/.agents/backend/fm-watch-arm.sh +602 -0
- package/.agents/backend/fm-watch-checkpoint.sh +109 -0
- package/.agents/backend/fm-watch.sh +1962 -0
- package/.agents/backend/fm-x-dismiss.sh +116 -0
- package/.agents/backend/fm-x-followup.sh +286 -0
- package/.agents/backend/fm-x-lib.sh +1000 -0
- package/.agents/backend/fm-x-link.sh +234 -0
- package/.agents/backend/fm-x-poll.sh +185 -0
- package/.agents/backend/fm-x-reply.sh +385 -0
- package/.agents/backend/fm_voice_frame.py +166 -0
- package/.agents/backend/fm_voice_records.py +574 -0
- package/.agents/backend/model-bridge.py +290 -0
- package/.agents/backend/opencode-go-bridge.py +30 -0
- package/.agents/bus/README.md +12 -0
- package/.agents/bus/messages.json +1 -0
- package/.agents/config/agents.machine.example.yaml +16 -0
- package/.agents/config/agents.yaml +50 -0
- package/.agents/config/agents.yaml.example +72 -0
- package/.agents/config/crew-dispatch.json +22 -0
- package/.agents/config/cron.yaml +9 -0
- package/.agents/config/eindri-dispatch.json +67 -0
- package/.agents/config/eindri-harness +1 -0
- package/.agents/config/startup-memory-budget +1 -0
- package/.agents/config/tailscale-sync.yaml +27 -0
- package/.agents/filebrowser/.filebrowser.json +6 -0
- package/.agents/filebrowser/README.md +11 -0
- package/.agents/gateway/README.md +12 -0
- package/.agents/github/README.md +15 -0
- package/.agents/github/webhooks/.gitkeep +0 -0
- package/.agents/github/workflows/.gitkeep +0 -0
- package/.agents/harness/opencode/plugins/lib/rodd-operational-input.js +37 -0
- package/.agents/harness/opencode/plugins/package.json +4 -0
- package/.agents/harness/opencode/plugins/saga-sessionstart.js +77 -0
- package/.agents/harness/opencode/plugins/syn-cd-check.js +61 -0
- package/.agents/harness/opencode/plugins/syn-pretool-check.js +68 -0
- package/.agents/harness/opencode/plugins/syn-turnend-guard.js +105 -0
- package/.agents/harness/opencode/plugins/syn-watch-arm.js +521 -0
- package/.agents/memory/README.md +46 -0
- package/.agents/memory/kaia.engram +0 -0
- package/.agents/memory/kaia.engram-shm +0 -0
- package/.agents/memory/kaia.engram-wal +0 -0
- package/.agents/memory/runes_audit.md +5 -0
- package/.agents/memory/well/episodes.jsonl +367 -0
- package/.agents/memory/well/workspace.jsonl +27 -0
- package/.agents/migrations/0001-hodd-layout.sh +38 -0
- package/.agents/migrations/0002-a2a-mcp.sh +27 -0
- package/.agents/migrations/0003-private-data-separation.sh +111 -0
- package/.agents/migrations/0004-hoard-and-realms.sh +104 -0
- package/.agents/sandbox/Dockerfile.utgard +19 -0
- package/.agents/sandbox/README.md +14 -0
- package/.agents/sandbox/utgard.config.json +14 -0
- package/.agents/skills/README.md +59 -0
- package/.agents/skills/bragi-marketing/README.md +3 -0
- package/.agents/skills/bragi-marketing/SKILL.md +82 -0
- package/.agents/skills/eindri-homes/README.md +3 -0
- package/.agents/skills/eindri-homes/SKILL.md +291 -0
- package/.agents/skills/eindri-homes/assets/control-plane.md +122 -0
- package/.agents/skills/eindri-homes/assets/dispatch.md +80 -0
- package/.agents/skills/frigg-consent/README.md +3 -0
- package/.agents/skills/frigg-consent/SKILL.md +56 -0
- package/.agents/skills/galdr-ymirsystem/README.md +3 -0
- package/.agents/skills/galdr-ymirsystem/SKILL.md +158 -0
- package/.agents/skills/galdr-ymirsystem/assets/README.md +74 -0
- package/.agents/skills/galdr-ymirsystem/assets/brokk-distro-runtime.md +390 -0
- package/.agents/skills/galdr-ymirsystem/assets/build-method.md +113 -0
- package/.agents/skills/galdr-ymirsystem/assets/build-tool-categories.md +178 -0
- package/.agents/skills/galdr-ymirsystem/assets/eindri-orchestration.md +518 -0
- package/.agents/skills/galdr-ymirsystem/assets/eindri-profiles.md +136 -0
- package/.agents/skills/galdr-ymirsystem/assets/harness-integration/README.md +673 -0
- package/.agents/skills/galdr-ymirsystem/assets/harness-integration/claude-code.md +197 -0
- package/.agents/skills/galdr-ymirsystem/assets/harness-integration/codex.md +191 -0
- package/.agents/skills/galdr-ymirsystem/assets/harness-integration/cursor.md +193 -0
- package/.agents/skills/galdr-ymirsystem/assets/harness-integration/opencode.md +262 -0
- package/.agents/skills/galdr-ymirsystem/assets/harness-integration/pi.md +295 -0
- package/.agents/skills/galdr-ymirsystem/assets/hlidskjalf-ui.md +496 -0
- package/.agents/skills/galdr-ymirsystem/assets/installation.md +571 -0
- package/.agents/skills/galdr-ymirsystem/assets/local-models/HOST-DATA.example.md +72 -0
- package/.agents/skills/galdr-ymirsystem/assets/local-models.md +274 -0
- package/.agents/skills/galdr-ymirsystem/assets/memory-well.md +161 -0
- package/.agents/skills/galdr-ymirsystem/assets/nornir-jobs.md +377 -0
- package/.agents/skills/galdr-ymirsystem/assets/norse-naming.md +325 -0
- package/.agents/skills/galdr-ymirsystem/assets/odrerir-hall.md +129 -0
- package/.agents/skills/galdr-ymirsystem/assets/pi-boot/einherjar-spawn.schema.json +81 -0
- package/.agents/skills/galdr-ymirsystem/assets/pi-boot/herdr-profile.toml +75 -0
- package/.agents/skills/galdr-ymirsystem/assets/pi-boot/pi-profile.yml +96 -0
- package/.agents/skills/galdr-ymirsystem/assets/pi-boot/supervision-tree.yml +163 -0
- package/.agents/skills/galdr-ymirsystem/assets/pi-boot-guide.md +58 -0
- package/.agents/skills/galdr-ymirsystem/assets/porting-upstream-to-norse.md +301 -0
- package/.agents/skills/galdr-ymirsystem/assets/principles.md +385 -0
- package/.agents/skills/galdr-ymirsystem/assets/reference-adoption.md +99 -0
- package/.agents/skills/galdr-ymirsystem/assets/registry.md +379 -0
- package/.agents/skills/galdr-ymirsystem/assets/runtime-compliance.md +463 -0
- package/.agents/skills/galdr-ymirsystem/assets/runtime-components.md +553 -0
- package/.agents/skills/galdr-ymirsystem/assets/smidja.md +175 -0
- package/.agents/skills/galdr-ymirsystem/schemas/toon-schemas.md +190 -0
- package/.agents/skills/galdr-ymirsystem/scripts/README.md +45 -0
- package/.agents/skills/galdr-ymirsystem/scripts/bench-one.sh +248 -0
- package/.agents/skills/galdr-ymirsystem/scripts/compliance-check.sh +360 -0
- package/.agents/skills/galdr-ymirsystem/scripts/gpu-sample.sh +43 -0
- package/.agents/skills/galdr-ymirsystem/scripts/stop-all.sh +34 -0
- package/.agents/skills/galdr-ymirsystem/scripts/toon-check.py +188 -0
- package/.agents/skills/gjallarhorn-relay/README.md +3 -0
- package/.agents/skills/gjallarhorn-relay/SKILL.md +283 -0
- package/.agents/skills/groa-update/SKILL.md +70 -0
- package/.agents/skills/hamr-adapters/README.md +3 -0
- package/.agents/skills/hamr-adapters/SKILL.md +123 -0
- package/.agents/skills/hamr-adapters/references/common/control-and-recovery.md +37 -0
- package/.agents/skills/hamr-adapters/references/common/dispatch.md +32 -0
- package/.agents/skills/hamr-adapters/references/common/model-and-effort.md +42 -0
- package/.agents/skills/hamr-adapters/references/common/primary-hooks.md +40 -0
- package/.agents/skills/hamr-adapters/references/harness/claude.md +55 -0
- package/.agents/skills/hamr-adapters/references/harness/codex.md +43 -0
- package/.agents/skills/hamr-adapters/references/harness/cursor.md +75 -0
- package/.agents/skills/hamr-adapters/references/harness/grok.md +69 -0
- package/.agents/skills/hamr-adapters/references/harness/kimi.md +51 -0
- package/.agents/skills/hamr-adapters/references/harness/muse.md +70 -0
- package/.agents/skills/hamr-adapters/references/harness/opencode.md +42 -0
- package/.agents/skills/hamr-adapters/references/harness/pi.md +56 -0
- package/.agents/skills/herdr-panes/README.md +3 -0
- package/.agents/skills/herdr-panes/SKILL.md +92 -0
- package/.agents/skills/herdr-panes/assets/herdr-backend.md +346 -0
- package/.agents/skills/herdr-panes/assets/tmux-backend.md +115 -0
- package/.agents/skills/hnoss-design/DESIGN.md +19 -0
- package/.agents/skills/hnoss-design/README.md +3 -0
- package/.agents/skills/hnoss-design/SKILL.md +83 -0
- package/.agents/skills/hvild-afk/README.md +3 -0
- package/.agents/skills/hvild-afk/SKILL.md +241 -0
- package/.agents/skills/jord-projects/README.md +3 -0
- package/.agents/skills/jord-projects/SKILL.md +92 -0
- package/.agents/skills/lifecycle/SKILL.md +56 -0
- package/.agents/skills/lifecycle/smoke_test.sh +52 -0
- package/.agents/skills/lifecycle/start.sh +21 -0
- package/.agents/skills/lifecycle/status.sh +35 -0
- package/.agents/skills/lifecycle/stop.sh +16 -0
- package/.agents/skills/muninn-stow/README.md +3 -0
- package/.agents/skills/muninn-stow/SKILL.md +309 -0
- package/.agents/skills/no-mistakes/SKILL.md +379 -0
- package/.agents/skills/nornir-schedule/README.md +3 -0
- package/.agents/skills/nornir-schedule/SKILL.md +25 -0
- package/.agents/skills/nornir-schedule/assets/events.md +137 -0
- package/.agents/skills/nornir-schedule/assets/quota.md +131 -0
- package/.agents/skills/nsr-compliance/README.md +3 -0
- package/.agents/skills/nsr-compliance/SKILL.md +29 -0
- package/.agents/skills/nsr-compliance/assets/nsr/README.md +40 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/RULES/README.md +15 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/RULES/git-workflow.md +18 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/RULES/security.md +22 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/RULES/tech-stack.md +8 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/SKILL.md +71 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/config/core_four.yaml +16 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/gates/check_danger.sh +102 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/gates/check_wiring.sh +69 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/gates/validate_code.sh +29 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/gates/verify_docs.py +66 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/generate.sh +109 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/harness/envelopes/result_envelope.json +14 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/harness/envelopes/task_envelope.json +20 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/harness/runner.py +303 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/telemetry/logger.py +112 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/templates/compliance/README.md +18 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/templates/compliance/config/README.md +10 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/templates/compliance/gates/README.md +12 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/templates/compliance/gates/check_env.sh +23 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/templates/compliance/gates/check_paths.sh +20 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/templates/compliance/gates/check_platform.sh +23 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/templates/compliance/harness/README.md +11 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/templates/compliance/harness/envelopes/README.md +10 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/templates/compliance/installer/README.md +10 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/templates/compliance/installer/install.sh +125 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/NSRcompliance/templates/compliance/telemetry/README.md +11 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/README.md +20 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/features/README.md +36 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/features/SKILL-template.md +14 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/git_ops/README.md +25 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/git_ops/SKILL.md +13 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/git_ops/create_branch.sh +6 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/git_ops/safe_commit.sh +9 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/git_ops/sync_upstream.sh +6 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/lifecycle/README.md +27 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/lifecycle/SKILL.md +15 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/lifecycle/smoke_test.sh +12 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/lifecycle/start.sh +19 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/lifecycle/status.sh +12 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/agents_skills/lifecycle/stop.sh +15 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/README.md +18 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/config/README.md +10 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/config/core_four.yaml +13 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/gates/README.md +12 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/gates/check_danger.sh +102 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/gates/check_env.sh +23 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/gates/check_paths.sh +20 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/gates/check_platform.sh +23 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/gates/check_wiring.sh +69 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/harness/README.md +11 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/harness/envelopes/README.md +10 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/harness/envelopes/result_envelope.json +14 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/harness/envelopes/task_envelope.json +20 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/harness/runner.py +303 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/installer/README.md +10 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/installer/install.sh +125 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/telemetry/README.md +11 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/compliance/telemetry/logger.py +112 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/BEST_PRACTICES/README.md +14 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/BEST_PRACTICES/api.md +17 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/BEST_PRACTICES/compliance-wiring.md +155 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/BEST_PRACTICES/frontend.md +15 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/CI_CD/README.md +11 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/CI_CD/deployment/README.md +23 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/CI_CD/deployment/clients/README.md +16 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/CI_CD/deployment/clients/template-README.md +11 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/CI_CD/deployment/deploy.sh +49 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/CI_CD/deployment/envs/README.md +12 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/CI_CD/deployment/envs/development.env.example +17 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/CI_CD/deployment/envs/production.env.example +17 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/CI_CD/deployment/envs/staging.env.example +17 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/CI_CD/deployment/tenants/README.md +11 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/CI_CD/deployment/tenants/template.env.example +8 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/DEVELOPER_SETUP/README.md +26 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/DEVELOPER_SETUP/agent.md +7 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/DEVELOPER_SETUP/developers/dev-1.md +20 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/DEVELOPER_SETUP/linux.md +9 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/DEVELOPER_SETUP/macos.md +10 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/DEVELOPER_SETUP/shared.md +19 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/DEVELOPER_SETUP/windows.md +14 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/HOSTING/README.md +25 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/HOSTING/_template.md +41 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/HOSTING/hosting-1.md +41 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/README.md +21 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/RUNBOOK/README.md +15 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/RUNBOOK/deployment.md +19 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/RUNBOOK/disaster-recovery.md +15 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/RUNBOOK/ops-tasks.md +14 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/features/README.md +16 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/project-skills.md +146 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/research/README.md +11 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/research/software-compliance-research.md +269 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/docs/working-with-agents.md +260 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/root/AGENTS.md +38 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/root/ARCHITECTURE.md +36 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/root/BEST_PRACTICES.md +27 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/root/CI_CD.md +23 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/root/FEATURES.md +15 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/root/README.md +22 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/root/RULES.md +37 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/root/STRUCTURE.md +45 -0
- package/.agents/skills/nsr-compliance/assets/nsr/assets/root/TECH_STACK.md +28 -0
- package/.agents/skills/nsr-compliance/assets/nsr/scaffold-spec.md +63 -0
- package/.agents/skills/nsr-compliance/assets/nsr/scripts/scaffold.sh +103 -0
- package/.agents/skills/nsr-compliance/assets/nsr/scripts/validate.sh +52 -0
- package/.agents/skills/nsr-compliance/assets/nsrcompliance/SKILL.md +71 -0
- package/.agents/skills/nsr-compliance/assets/nsrcompliance/config/core_four.yaml +16 -0
- package/.agents/skills/nsr-compliance/assets/nsrcompliance/gates/check_danger.sh +102 -0
- package/.agents/skills/nsr-compliance/assets/nsrcompliance/gates/check_wiring.sh +69 -0
- package/.agents/skills/nsr-compliance/assets/nsrcompliance/gates/validate_code.sh +29 -0
- package/.agents/skills/nsr-compliance/assets/nsrcompliance/gates/verify_docs.py +66 -0
- package/.agents/skills/nsr-compliance/assets/nsrcompliance/generate.sh +109 -0
- package/.agents/skills/nsr-compliance/assets/nsrcompliance/harness/envelopes/result_envelope.json +14 -0
- package/.agents/skills/nsr-compliance/assets/nsrcompliance/harness/envelopes/task_envelope.json +20 -0
- package/.agents/skills/nsr-compliance/assets/nsrcompliance/harness/runner.py +303 -0
- package/.agents/skills/nsr-compliance/assets/nsrcompliance/telemetry/logger.py +112 -0
- package/.agents/skills/nsr-compliance/assets/nsrcompliance/templates/compliance/README.md +18 -0
- package/.agents/skills/nsr-compliance/assets/nsrcompliance/templates/compliance/config/README.md +10 -0
- package/.agents/skills/nsr-compliance/assets/nsrcompliance/templates/compliance/gates/README.md +12 -0
- package/.agents/skills/nsr-compliance/assets/nsrcompliance/templates/compliance/gates/check_env.sh +23 -0
- package/.agents/skills/nsr-compliance/assets/nsrcompliance/templates/compliance/gates/check_paths.sh +20 -0
- package/.agents/skills/nsr-compliance/assets/nsrcompliance/templates/compliance/gates/check_platform.sh +23 -0
- package/.agents/skills/nsr-compliance/assets/nsrcompliance/templates/compliance/harness/README.md +11 -0
- package/.agents/skills/nsr-compliance/assets/nsrcompliance/templates/compliance/harness/envelopes/README.md +10 -0
- package/.agents/skills/nsr-compliance/assets/nsrcompliance/templates/compliance/installer/README.md +10 -0
- package/.agents/skills/nsr-compliance/assets/nsrcompliance/templates/compliance/installer/install.sh +125 -0
- package/.agents/skills/nsr-compliance/assets/nsrcompliance/templates/compliance/telemetry/README.md +11 -0
- package/.agents/skills/pr-ops/README.md +3 -0
- package/.agents/skills/pr-ops/SKILL.md +109 -0
- package/.agents/skills/ratatoskr-a2a/README.md +3 -0
- package/.agents/skills/ratatoskr-a2a/SKILL.md +148 -0
- package/.agents/skills/rules-check-drift/SKILL.md +81 -0
- package/.agents/skills/saga-bearings/README.md +3 -0
- package/.agents/skills/saga-bearings/SKILL.md +26 -0
- package/.agents/skills/saga-bearings/assets/bearings.md +161 -0
- package/.agents/skills/saga-bearings/assets/board-template.html +731 -0
- package/.agents/skills/saga-bearings/assets/recap.md +56 -0
- package/.agents/skills/syn-recovery/README.md +3 -0
- package/.agents/skills/syn-recovery/SKILL.md +54 -0
- package/.agents/skills/tyr-check/README.md +3 -0
- package/.agents/skills/tyr-check/SKILL.md +218 -0
- package/.agents/skills/urdh-hold/README.md +3 -0
- package/.agents/skills/urdh-hold/SKILL.md +25 -0
- package/.agents/skills/urdh-hold/assets/decisions.md +15 -0
- package/.agents/skills/urdh-hold/assets/hold.md +54 -0
- package/.agents/skills/vor-diagnostics/README.md +3 -0
- package/.agents/skills/vor-diagnostics/SKILL.md +68 -0
- package/.agents/skills/ymir-host/README.md +3 -0
- package/.agents/skills/ymir-host/SKILL.md +34 -0
- package/.agents/skills/ymir-host/assets/desktop.md +36 -0
- package/.agents/skills/ymir-host/assets/omarchy.md +297 -0
- package/.agents/skills/ymir-host/assets/runbooks.md +15 -0
- package/.agents/skills/ymir-host/assets/thjazi.md +220 -0
- package/.agents/skills/ymir-host/assets/update.md +64 -0
- package/.agents/tests/assets/board-render-harness.mjs +117 -0
- package/.agents/tests/cmux-test-safety.sh +51 -0
- package/.agents/tests/fixtures.sh +291 -0
- package/.agents/tests/fm-afk-inject-e2e.test.sh +428 -0
- package/.agents/tests/fm-afk-inject-herdr-e2e.test.sh +536 -0
- package/.agents/tests/fm-afk-launch.test.sh +961 -0
- package/.agents/tests/fm-afk-pi-herdr-return-e2e.test.sh +297 -0
- package/.agents/tests/fm-afk-return.test.sh +283 -0
- package/.agents/tests/fm-arm-pretool-check.test.sh +482 -0
- package/.agents/tests/fm-ask-user-authority.test.sh +42 -0
- package/.agents/tests/fm-backend-autodetect-smoke.test.sh +176 -0
- package/.agents/tests/fm-backend-cmux-smoke.test.sh +188 -0
- package/.agents/tests/fm-backend-cmux.test.sh +1166 -0
- package/.agents/tests/fm-backend-herdr-eventwait-smoke.test.sh +136 -0
- package/.agents/tests/fm-backend-herdr-eventwait.test.py +106 -0
- package/.agents/tests/fm-backend-herdr-focus-flash-e2e.test.sh +409 -0
- package/.agents/tests/fm-backend-herdr-launcher-workspace-e2e.test.sh +437 -0
- package/.agents/tests/fm-backend-herdr-presentation-e2e.test.sh +1424 -0
- package/.agents/tests/fm-backend-herdr-prune-safety-e2e.test.sh +180 -0
- package/.agents/tests/fm-backend-herdr-respawn-idem-e2e.test.sh +182 -0
- package/.agents/tests/fm-backend-herdr-smoke.test.sh +355 -0
- package/.agents/tests/fm-backend-herdr-workspace-per-home-e2e.test.sh +251 -0
- package/.agents/tests/fm-backend-herdr.test.sh +4606 -0
- package/.agents/tests/fm-backend-orca.test.sh +1357 -0
- package/.agents/tests/fm-backend-tmux-smoke.test.sh +173 -0
- package/.agents/tests/fm-backend-zellij-smoke.test.sh +205 -0
- package/.agents/tests/fm-backend-zellij.test.sh +1360 -0
- package/.agents/tests/fm-backend.test.sh +1142 -0
- package/.agents/tests/fm-backlog-atomicity.test.sh +2302 -0
- package/.agents/tests/fm-backlog-handoff.test.sh +1360 -0
- package/.agents/tests/fm-bearings-board-render.test.sh +135 -0
- package/.agents/tests/fm-bearings-board.test.sh +414 -0
- package/.agents/tests/fm-bearings-snapshot.test.sh +1990 -0
- package/.agents/tests/fm-bootstrap-network-parallel.test.sh +316 -0
- package/.agents/tests/fm-bootstrap.test.sh +1178 -0
- package/.agents/tests/fm-branch-supervision.test.sh +552 -0
- package/.agents/tests/fm-brief.test.sh +785 -0
- package/.agents/tests/fm-busy-adapter-wiring.test.sh +324 -0
- package/.agents/tests/fm-busy-state.test.sh +465 -0
- package/.agents/tests/fm-calm-pi-extension.test.sh +3979 -0
- package/.agents/tests/fm-captain-hold-lifecycle.test.sh +1187 -0
- package/.agents/tests/fm-cd-pretool-check.test.sh +400 -0
- package/.agents/tests/fm-check-unregister.test.sh +198 -0
- package/.agents/tests/fm-classify-corr-token.test.sh +543 -0
- package/.agents/tests/fm-classify-decision-key.test.sh +340 -0
- package/.agents/tests/fm-claude-stop-autoarm-live-e2e.test.sh +164 -0
- package/.agents/tests/fm-claude-stop-autoarm.test.sh +1190 -0
- package/.agents/tests/fm-cmux-claude-composer-live-e2e.test.sh +110 -0
- package/.agents/tests/fm-codex-continuity-live-e2e.test.sh +55 -0
- package/.agents/tests/fm-composer-ghost.test.sh +716 -0
- package/.agents/tests/fm-composer-lib.test.sh +666 -0
- package/.agents/tests/fm-composer-matrix-live-e2e.test.sh +220 -0
- package/.agents/tests/fm-control-herdr-smoke.test.sh +149 -0
- package/.agents/tests/fm-control-relaunch.test.sh +1530 -0
- package/.agents/tests/fm-control.test.sh +910 -0
- package/.agents/tests/fm-crew-state.test.sh +1610 -0
- package/.agents/tests/fm-cursor-harness.test.sh +404 -0
- package/.agents/tests/fm-cursor-primary-live-e2e.test.sh +217 -0
- package/.agents/tests/fm-cursor-primary.test.sh +708 -0
- package/.agents/tests/fm-daemon.test.sh +2740 -0
- package/.agents/tests/fm-documentation-audiences.test.sh +141 -0
- package/.agents/tests/fm-ensure-agents-md.test.sh +371 -0
- package/.agents/tests/fm-extension-binding.test.sh +2187 -0
- package/.agents/tests/fm-fleet-snapshot-view.test.sh +816 -0
- package/.agents/tests/fm-fleet-sync.test.sh +721 -0
- package/.agents/tests/fm-gate-refuse.test.sh +356 -0
- package/.agents/tests/fm-gitignore-config.test.sh +89 -0
- package/.agents/tests/fm-gotmp.test.sh +230 -0
- package/.agents/tests/fm-grok-continuity-live-e2e.test.sh +112 -0
- package/.agents/tests/fm-grok-harness.test.sh +117 -0
- package/.agents/tests/fm-grok-stop-live-e2e.test.sh +219 -0
- package/.agents/tests/fm-guard-stale-banner.test.sh +709 -0
- package/.agents/tests/fm-harness-adapter-instructions-live-e2e.test.sh +134 -0
- package/.agents/tests/fm-harness-adapter-references.test.sh +30 -0
- package/.agents/tests/fm-harness-liveness-drift-live-e2e.test.sh +151 -0
- package/.agents/tests/fm-herdr-lab.test.sh +243 -0
- package/.agents/tests/fm-herdr-session-cleanup-e2e.test.sh +146 -0
- package/.agents/tests/fm-herdr-session-cleanup.test.sh +329 -0
- package/.agents/tests/fm-herdr-submit-confirm-live-e2e.test.sh +125 -0
- package/.agents/tests/fm-herdr-version-floor-live-e2e.test.sh +144 -0
- package/.agents/tests/fm-home-summary-refresh.test.sh +971 -0
- package/.agents/tests/fm-inactive-reconcile.test.sh +485 -0
- package/.agents/tests/fm-kimi-harness.test.sh +687 -0
- package/.agents/tests/fm-lint-workflows.test.sh +532 -0
- package/.agents/tests/fm-lint.test.sh +1023 -0
- package/.agents/tests/fm-muse-harness.test.sh +932 -0
- package/.agents/tests/fm-muse-signals-live-e2e.test.sh +205 -0
- package/.agents/tests/fm-no-mistakes-required.test.sh +72 -0
- package/.agents/tests/fm-on.test.sh +507 -0
- package/.agents/tests/fm-opencode-primary-live-e2e.test.sh +357 -0
- package/.agents/tests/fm-operational-input.test.sh +160 -0
- package/.agents/tests/fm-peek-remote.test.sh +110 -0
- package/.agents/tests/fm-pending-reply.test.sh +1288 -0
- package/.agents/tests/fm-pi-branch-extension.test.sh +3182 -0
- package/.agents/tests/fm-pi-branch-live-e2e.test.sh +527 -0
- package/.agents/tests/fm-pi-primary-live-e2e.test.sh +345 -0
- package/.agents/tests/fm-pi-primary-types.test.sh +68 -0
- package/.agents/tests/fm-pi-watch-extension.test.sh +2843 -0
- package/.agents/tests/fm-pr-check-security.test.sh +2140 -0
- package/.agents/tests/fm-pr-merge.test.sh +2135 -0
- package/.agents/tests/fm-procevent-quota.test.sh +225 -0
- package/.agents/tests/fm-procevent-when.test.sh +406 -0
- package/.agents/tests/fm-procevent.test.sh +1745 -0
- package/.agents/tests/fm-project-origin.test.sh +110 -0
- package/.agents/tests/fm-public-followup.test.sh +2272 -0
- package/.agents/tests/fm-quota-array-dispatch-live-e2e.test.sh +526 -0
- package/.agents/tests/fm-quota-choose.test.sh +626 -0
- package/.agents/tests/fm-remote-backlog-handoff.test.sh +453 -0
- package/.agents/tests/fm-remote-doctor.test.sh +625 -0
- package/.agents/tests/fm-remote-entrypoint.test.sh +59 -0
- package/.agents/tests/fm-remote-job-orphan-reap.test.sh +205 -0
- package/.agents/tests/fm-remote-job.test.sh +768 -0
- package/.agents/tests/fm-remote-reply.test.sh +520 -0
- package/.agents/tests/fm-remote-secondmate-lifecycle-e2e.test.sh +1256 -0
- package/.agents/tests/fm-remote-secondmate-parent-binding.test.sh +326 -0
- package/.agents/tests/fm-remote-secondmate-trace-context.test.sh +310 -0
- package/.agents/tests/fm-remote-transport-lanes.test.sh +431 -0
- package/.agents/tests/fm-review-diff.test.sh +176 -0
- package/.agents/tests/fm-secondmate-harness.test.sh +2600 -0
- package/.agents/tests/fm-secondmate-lifecycle-e2e.test.sh +259 -0
- package/.agents/tests/fm-secondmate-liveness.test.sh +559 -0
- package/.agents/tests/fm-secondmate-reconcile.test.sh +732 -0
- package/.agents/tests/fm-secondmate-safety.test.sh +2972 -0
- package/.agents/tests/fm-secondmate-sync.test.sh +913 -0
- package/.agents/tests/fm-send-inbox-doorbell-live-e2e.test.sh +206 -0
- package/.agents/tests/fm-send-inbox.test.sh +352 -0
- package/.agents/tests/fm-send-popup-settle.test.sh +167 -0
- package/.agents/tests/fm-send-remote-delivery.test.sh +796 -0
- package/.agents/tests/fm-send-resolve-key.test.sh +551 -0
- package/.agents/tests/fm-send-secondmate-marker-herdr-e2e.test.sh +187 -0
- package/.agents/tests/fm-send-secondmate-marker.test.sh +278 -0
- package/.agents/tests/fm-send-settle.test.sh +147 -0
- package/.agents/tests/fm-send-strict.test.sh +241 -0
- package/.agents/tests/fm-session-lock-ancestry.test.sh +365 -0
- package/.agents/tests/fm-session-start.test.sh +2515 -0
- package/.agents/tests/fm-sessionstart-hook-live-e2e.test.sh +631 -0
- package/.agents/tests/fm-sessionstart-instruction-refresh-live-e2e.test.sh +230 -0
- package/.agents/tests/fm-sessionstart-nudge.test.sh +1039 -0
- package/.agents/tests/fm-shared-captain-inheritance.test.sh +404 -0
- package/.agents/tests/fm-spawn-batch.test.sh +148 -0
- package/.agents/tests/fm-spawn-dispatch-profile.test.sh +827 -0
- package/.agents/tests/fm-spawn-pool-base-freshen.test.sh +440 -0
- package/.agents/tests/fm-spawn-worktree-settle.test.sh +147 -0
- package/.agents/tests/fm-startup-memory-budget.test.sh +333 -0
- package/.agents/tests/fm-startup-network.test.sh +712 -0
- package/.agents/tests/fm-stow-cascade.test.sh +370 -0
- package/.agents/tests/fm-subagent-pretool-check.test.sh +291 -0
- package/.agents/tests/fm-supervision-events.test.sh +156 -0
- package/.agents/tests/fm-supervision-instructions.test.sh +189 -0
- package/.agents/tests/fm-tangle-guard.test.sh +277 -0
- package/.agents/tests/fm-task-delivery.test.sh +420 -0
- package/.agents/tests/fm-task-inbox.test.sh +513 -0
- package/.agents/tests/fm-teardown-endpoint-safety.test.sh +374 -0
- package/.agents/tests/fm-teardown.test.sh +2665 -0
- package/.agents/tests/fm-test-fixture-cleanup.test.sh +172 -0
- package/.agents/tests/fm-test-fixtures.test.sh +132 -0
- package/.agents/tests/fm-test-isolation-proof.test.sh +278 -0
- package/.agents/tests/fm-test-run.test.sh +1196 -0
- package/.agents/tests/fm-tmux-agent-liveness.test.sh +354 -0
- package/.agents/tests/fm-tmux-submit-busy.test.sh +340 -0
- package/.agents/tests/fm-tool-update-check.test.sh +1044 -0
- package/.agents/tests/fm-trace-context-lib.test.sh +253 -0
- package/.agents/tests/fm-trace-context-spawn.test.sh +581 -0
- package/.agents/tests/fm-transition-lib.test.sh +54 -0
- package/.agents/tests/fm-turnend-guard.test.sh +1822 -0
- package/.agents/tests/fm-update.test.sh +304 -0
- package/.agents/tests/fm-vendor-auth-probe.test.sh +395 -0
- package/.agents/tests/fm-voice-relay.test.sh +4741 -0
- package/.agents/tests/fm-wake-daemon-lifecycle-e2e.test.sh +171 -0
- package/.agents/tests/fm-wake-drain-open-decisions-cursor.test.sh +356 -0
- package/.agents/tests/fm-wake-drain-open-decisions.test.sh +226 -0
- package/.agents/tests/fm-wake-drain-unread-status.test.sh +379 -0
- package/.agents/tests/fm-wake-queue.test.sh +1224 -0
- package/.agents/tests/fm-watch-arm.test.sh +821 -0
- package/.agents/tests/fm-watch-checkpoint.test.sh +87 -0
- package/.agents/tests/fm-watch-recovery-loop.test.sh +223 -0
- package/.agents/tests/fm-watch-triage.test.sh +3889 -0
- package/.agents/tests/fm-watcher-lock.test.sh +1133 -0
- package/.agents/tests/fm-x-mode.test.sh +3042 -0
- package/.agents/tests/gleipnir-guard-pretool-check.test.sh +51 -0
- package/.agents/tests/gleipnir-machine-lock.test.sh +130 -0
- package/.agents/tests/herdr-test-safety.sh +42 -0
- package/.agents/tests/lib.sh +315 -0
- package/.agents/tests/remote-herdr-fixture.sh +125 -0
- package/.agents/tests/secondmate-helpers.sh +193 -0
- package/.agents/tests/smoke.test.sh +93 -0
- package/.agents/tests/wake-helpers.sh +338 -0
- package/.agents/tests/zellij-test-safety.sh +60 -0
- package/.agents/tools/README.md +10 -0
- package/.agents/tools/bin/tasks-cli +267 -0
- package/.agents/tools/tasks-cli.ts +216 -0
- package/AGENTS.md +454 -0
- package/CHANGELOG.md +881 -0
- package/LICENSE +1 -1
- package/NOTICE +63 -0
- package/README.md +113 -59
- package/RULES/01-domains.md +51 -0
- package/RULES/02-agents.md +78 -0
- package/RULES/03-houses.md +61 -0
- package/RULES/04-hoard.md +65 -0
- package/RULES/05-platforms.md +67 -0
- package/RULES/06-append-only.md +58 -0
- package/RULES/07-config.md +45 -0
- package/RULES/08-delivery-gate.md +75 -0
- package/RULES/README.md +20 -0
- package/bin/README.md +107 -0
- package/bin/a2a-mcp.sh +98 -0
- package/bin/a2a-serve.py +103 -0
- package/bin/a2a-serve.sh +8 -0
- package/bin/a2a-talk.sh +50 -0
- package/bin/a2abridge-ensure.sh +108 -0
- package/bin/agent-run.sh +43 -0
- package/bin/agents-config.sh +233 -0
- package/bin/apodex-smoke-test.sh +92 -0
- package/bin/bifrost-bridge.sh +112 -0
- package/bin/bifrost-ingress.sh +77 -0
- package/bin/bootstrap-macos.sh +107 -0
- package/bin/bootstrap-windows.ps1 +112 -0
- package/bin/branch-guard.sh +64 -0
- package/bin/brokk +73 -0
- package/bin/brokk-classify-lib.sh +1770 -0
- package/bin/brokk-lease-lib.sh +222 -0
- package/bin/brokk-lease.sh +193 -0
- package/bin/brokk-lint.sh +65 -0
- package/bin/brokk-send.sh +38 -0
- package/bin/brokk-update.sh +9 -0
- package/bin/brokk-wake-grant.sh +127 -0
- package/bin/brokk-wake-lib.sh +1849 -0
- package/bin/changelog-guard.sh +165 -0
- package/bin/crash-sense.sh +160 -0
- package/bin/desktop-place.sh +224 -0
- package/bin/docs-guard.sh +39 -0
- package/bin/editor-place.sh +207 -0
- package/bin/eindri-acclaim.sh +34 -0
- package/bin/eindri-control.sh +28 -0
- package/bin/eindri-role.sh +83 -0
- package/bin/eindri-seen.sh +41 -0
- package/bin/eindri-send.sh +15 -0
- package/bin/eindri-start.sh +85 -0
- package/bin/eindri-watch.sh +57 -0
- package/bin/einherjar-spawn.sh +576 -0
- package/bin/eir-doctor.sh +112 -0
- package/bin/erindi-brief.sh +271 -0
- package/bin/fleet-apply.sh +68 -0
- package/bin/github-deploy.sh +81 -0
- package/bin/gjallarhorn-expose.sh +102 -0
- package/bin/gjallarhorn-notify.sh +58 -0
- package/bin/gjallarhorn-purge.sh +54 -0
- package/bin/gjallarhorn-tunnel.sh +72 -0
- package/bin/gleipnir-lock-lib.sh +284 -0
- package/bin/groa-update.sh +122 -0
- package/bin/hall-snapshot.sh +92 -0
- package/bin/hamr-harness.sh +246 -0
- package/bin/heimdall-ssh-keys.sh +374 -0
- package/bin/herdr-agents.py +44 -0
- package/bin/herdr-ensure.sh +104 -0
- package/bin/herdr-run.sh +431 -0
- package/bin/hermes-ensure.sh +88 -0
- package/bin/hoard-lib.sh +25 -0
- package/bin/hodd.sh +78 -0
- package/bin/host-sense.sh +199 -0
- package/bin/huginn-research-worker.sh +108 -0
- package/bin/local-model-lock.sh +51 -0
- package/bin/mimir-bridge.py +226 -0
- package/bin/mimir-bridge.sh +87 -0
- package/bin/mimir-ingest.sh +131 -0
- package/bin/mimir.sh +113 -0
- package/bin/mjollnir-webhook.sh +59 -0
- package/bin/mjollnir.sh +85 -0
- package/bin/model-hardware.sh +82 -0
- package/bin/model-resolve.sh +147 -0
- package/bin/models-detect.sh +178 -0
- package/bin/nornir-cron-start.sh +162 -0
- package/bin/nornir-job-daily-briefing.sh +191 -0
- package/bin/nornir-job-git-sync.sh +152 -0
- package/bin/nornir-job-memory-housekeeping.sh +114 -0
- package/bin/nornir-job-observer.sh +166 -0
- package/bin/nornir-job-skillopt-sleep.sh +76 -0
- package/bin/omarchy-hook-install.sh +60 -0
- package/bin/omarchy-install.sh +148 -0
- package/bin/omarchy-plugins.sh +174 -0
- package/bin/omarchy-sense.sh +149 -0
- package/bin/perm-guard.sh +30 -0
- package/bin/pi-agent.sh +68 -0
- package/bin/pi-ensure.sh +77 -0
- package/bin/pi-local.sh +37 -0
- package/bin/pi-seat.sh +63 -0
- package/bin/prereq-ensure.sh +166 -0
- package/bin/private-guard.sh +38 -0
- package/bin/project-git.sh +55 -0
- package/bin/public-guard.sh +70 -0
- package/bin/ratatoskr.sh +49 -0
- package/bin/realm-lib.sh +32 -0
- package/bin/repo-scrub.sh +62 -0
- package/bin/rodd-operational-input.sh +250 -0
- package/bin/runes-append.sh +198 -0
- package/bin/saga-session-start.sh +172 -0
- package/bin/saga-sessionstart-run.sh +79 -0
- package/bin/saga-wake-drain.sh +41 -0
- package/bin/secret-guard.sh +63 -0
- package/bin/sessrumnir-ensure.sh +100 -0
- package/bin/sessrumnir-sync.sh +237 -0
- package/bin/sessrumnir.sh +91 -0
- package/bin/skillopt-setup.sh +76 -0
- package/bin/skuld-branch-outcome.sh +203 -0
- package/bin/skuld-branch-prompt.sh +101 -0
- package/bin/smidja-bootstrap.sh +90 -0
- package/bin/smidja-observe.sh +86 -0
- package/bin/syn-arm-pretool-check.sh +57 -0
- package/bin/syn-asset-pretool-check.sh +76 -0
- package/bin/syn-cd-pretool-check.sh +28 -0
- package/bin/syn-guard-pretool-check.sh +83 -0
- package/bin/syn-turnend-guard.sh +36 -0
- package/bin/syn-watch-arm.sh +99 -0
- package/bin/tailscale-sync.sh +190 -0
- package/bin/telegram-bot.sh +69 -0
- package/bin/toolchain.sh +74 -0
- package/bin/utgard.sh +111 -0
- package/bin/valhalla.sh +115 -0
- package/bin/valknut-load.sh +260 -0
- package/bin/vor-crew-state.sh +124 -0
- package/bin/wedge-notify.sh +47 -0
- package/bin/workspace-provision.sh +80 -0
- package/bin/workspace-rag.sh +150 -0
- package/bin/wyrd-db.sh +47 -0
- package/bin/yggdrasil.sh +152 -0
- package/bin/ymir-edit.sh +145 -0
- package/bin/ymir-install.sh +641 -0
- package/bin/ymir-invite.sh +93 -0
- package/bin/ymir-isolation.sh +88 -0
- package/bin/ymir-migrate.sh +61 -0
- package/bin/ymir-platform.sh +348 -0
- package/bin/ymir-say.sh +97 -0
- package/bin/ymir-setup-auth.sh +108 -0
- package/bin/ymir-validate.sh +173 -0
- package/bin/ymir.js +74 -16
- package/docs/Architecture.md +339 -0
- package/docs/design.md +351 -0
- package/docs/installations/README.md +63 -0
- package/docs/installations/ymir-home.md +93 -0
- package/docs/integration.md +105 -0
- package/docs/lore.md +1056 -0
- package/docs/research/agent-harnesses.md +290 -0
- package/docs/runbooks/README.md +19 -0
- package/docs/runbooks/a2a.md +91 -0
- package/docs/runbooks/agent-permissions.md +99 -0
- package/docs/runbooks/agents.md +57 -0
- package/docs/runbooks/models.md +69 -0
- package/docs/runbooks/realm-onboarding.md +60 -0
- package/docs/runbooks/secrets-and-hoard.md +60 -0
- package/docs/runbooks/updates-and-migrations.md +110 -0
- package/docs/runbooks/worktrees-isolation.md +72 -0
- package/docs/session-start.md +105 -0
- package/docs/skillopt-integration.md +321 -0
- package/docs/sync-system.md +66 -0
- package/docs/workspaces.md +104 -0
- package/docs/ymir-rut.md +260 -0
- package/package.json +44 -14
- package/packaging/build.sh +74 -0
- package/packaging/macos/Ymir Installer.command +43 -0
- package/packaging/windows/ymir-setup.nsi +74 -0
- package/scripts/README.md +22 -0
- package/scripts/electron.sh +210 -0
- package/scripts/start.sh +167 -0
- package/scripts/stop.sh +113 -0
- package/install.sh +0 -35
|
@@ -0,0 +1,1409 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# fm-pending-reply-lib.sh - parent-owned secondmate missed-report guards.
|
|
3
|
+
#
|
|
4
|
+
# When the main firstmate delivers a reply-bearing marked from-firstmate request
|
|
5
|
+
# to a secondmate, this library records a durable parent-owned pending-reply
|
|
6
|
+
# expectation BEFORE delivery, embeds a privacy-safe correlation id in the
|
|
7
|
+
# outbound message, and later resolves that expectation only from a correlated
|
|
8
|
+
# parent status line or status-pointed document - never from transport success,
|
|
9
|
+
# chat content, or unrelated status activity.
|
|
10
|
+
#
|
|
11
|
+
# Safety property (captain direction 2026-07-22): a secondmate agent may ignore
|
|
12
|
+
# the marker and answer only in its visible conversation. The parent must notice
|
|
13
|
+
# the missing correlated report without scraping that conversation, send exactly
|
|
14
|
+
# one automatic recovery request asking for a repost through the parent channel,
|
|
15
|
+
# and escalate once if the recovery turn also completes without a correlated
|
|
16
|
+
# report. Never loop, never repeatedly inject, never silently expire unresolved
|
|
17
|
+
# records, and never treat wrong-home or structured-home heuristics as
|
|
18
|
+
# acknowledgement.
|
|
19
|
+
#
|
|
20
|
+
# Record location (parent FM_HOME):
|
|
21
|
+
# state/pending-replies/<corr_id>
|
|
22
|
+
# One more durable input, owned by bin/fm-procevent-remote-reply.sh and read
|
|
23
|
+
# here: state/remote-replies/<task_id>.caught-up, the remote reply mirror's
|
|
24
|
+
# watermark (see the remote reply-channel freshness section below).
|
|
25
|
+
# Each record is a key=value file owned by this library. Schema:
|
|
26
|
+
# schema=fm-pending-reply.v1
|
|
27
|
+
# corr_id= privacy-safe correlation token
|
|
28
|
+
# task_id= secondmate task id in the parent home
|
|
29
|
+
# parent_home= absolute parent FM_HOME
|
|
30
|
+
# parent_status= absolute path of parent state/<task_id>.status
|
|
31
|
+
# parent_status_scan_signature=
|
|
32
|
+
# request_summary= short sanitized summary (no secrets by design)
|
|
33
|
+
# created_epoch= when the expectation was created
|
|
34
|
+
# delivered_epoch= when the marked request was confirmed delivered
|
|
35
|
+
# (empty until delivery; delivery never resolves)
|
|
36
|
+
# phase= awaiting_report | delivery_unknown | recovery_sending |
|
|
37
|
+
# recovery_sent | recovery_failed | recovery_unknown |
|
|
38
|
+
# escalated | resolved
|
|
39
|
+
# turn_seen_busy= 0|1 after delivery for the original request turn
|
|
40
|
+
# request_turn_completed_epoch=
|
|
41
|
+
# recovery_attempted_epoch=
|
|
42
|
+
# recovery_sender_pid=
|
|
43
|
+
# recovery_sender_identity=
|
|
44
|
+
# recovery_sent_epoch=
|
|
45
|
+
# recovery_delivery_outcome=
|
|
46
|
+
# recovery_turn_seen_busy=
|
|
47
|
+
# recovery_turn_completed_epoch=
|
|
48
|
+
# escalated_epoch=
|
|
49
|
+
# escalation_closed_epoch=
|
|
50
|
+
# when the durable status decision opened by that
|
|
51
|
+
# escalation was closed again (see the escalation
|
|
52
|
+
# lifecycle note below); empty until then
|
|
53
|
+
# resolved_epoch=
|
|
54
|
+
# resolved_via= status | document | helper | empty
|
|
55
|
+
# wrong_home_hits= count of corr sightings under the secondmate home
|
|
56
|
+
# wrong_home_sightings= comma-separated identities of counted sightings
|
|
57
|
+
# wrong_home_scan_signature=
|
|
58
|
+
# grace_secs= bounded grace before recovery is eligible
|
|
59
|
+
#
|
|
60
|
+
# Escalation lifecycle: an escalation is not just a message, it OPENS a durable
|
|
61
|
+
# keyed decision in the parent status log, and bin/fm-classify-lib.sh's fold is
|
|
62
|
+
# the one owner of what closes it. So this library owns both ends of that
|
|
63
|
+
# decision: fm_pending_reply_maybe_escalate opens it under a per-request key, and
|
|
64
|
+
# fm_pending_reply_close_escalation closes it once the record resolves. Resolving
|
|
65
|
+
# the record alone would leave the decision open forever, re-surfacing a settled
|
|
66
|
+
# request in every later OPEN DECISIONS fold.
|
|
67
|
+
# That per-request key lives in a namespace the fold reserves to this library, so
|
|
68
|
+
# no other writer into the same status stream - a local mate appending directly,
|
|
69
|
+
# or a remote mate's mirrored line - can take the key over or clear it; see the
|
|
70
|
+
# reserved-key rule in bin/fm-classify-lib.sh.
|
|
71
|
+
#
|
|
72
|
+
# Sourced by bin/fm-send.sh, bin/fm-watch.sh, bin/fm-secondmate-report.sh, and
|
|
73
|
+
# tests. No side effects on source. set -u / set -e safe.
|
|
74
|
+
#
|
|
75
|
+
# Tunables (env):
|
|
76
|
+
# FM_PENDING_REPLY_GRACE_SECS default 120
|
|
77
|
+
# FM_PENDING_REPLY_DIR_OVERRIDE override the pending-replies directory (tests)
|
|
78
|
+
# FM_PENDING_REPLY_SEND_HOOK optional command template for recovery delivery
|
|
79
|
+
# (tests); receives task_id and full message as args
|
|
80
|
+
# FM_PENDING_REPLY_NOW optional fixed epoch for deterministic tests
|
|
81
|
+
|
|
82
|
+
# shellcheck source=bin/fm-marker-lib.sh
|
|
83
|
+
_FM_PENDING_REPLY_LIB_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd 2>/dev/null)" || _FM_PENDING_REPLY_LIB_DIR="."
|
|
84
|
+
# shellcheck source=bin/fm-marker-lib.sh
|
|
85
|
+
. "$_FM_PENDING_REPLY_LIB_DIR/fm-marker-lib.sh"
|
|
86
|
+
# shellcheck source=bin/fm-backend.sh
|
|
87
|
+
. "$_FM_PENDING_REPLY_LIB_DIR/fm-backend.sh"
|
|
88
|
+
# shellcheck source=bin/fm-tmux-lib.sh
|
|
89
|
+
. "$_FM_PENDING_REPLY_LIB_DIR/fm-tmux-lib.sh"
|
|
90
|
+
# shellcheck source=bin/fm-classify-lib.sh
|
|
91
|
+
. "$_FM_PENDING_REPLY_LIB_DIR/fm-classify-lib.sh"
|
|
92
|
+
|
|
93
|
+
FM_PENDING_REPLY_SCHEMA='fm-pending-reply.v1'
|
|
94
|
+
FM_PENDING_REPLY_CORR_RE='corr=[A-Fa-f0-9]{16}'
|
|
95
|
+
FM_PENDING_REPLY_GRACE_DEFAULT=120
|
|
96
|
+
|
|
97
|
+
fm_pending_reply_now() {
|
|
98
|
+
if [ -n "${FM_PENDING_REPLY_NOW:-}" ]; then
|
|
99
|
+
printf '%s' "$FM_PENDING_REPLY_NOW"
|
|
100
|
+
return 0
|
|
101
|
+
fi
|
|
102
|
+
date +%s
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
fm_pending_reply_grace_secs() {
|
|
106
|
+
local g=${FM_PENDING_REPLY_GRACE_SECS:-$FM_PENDING_REPLY_GRACE_DEFAULT}
|
|
107
|
+
case "$g" in
|
|
108
|
+
''|*[!0-9]*) g=$FM_PENDING_REPLY_GRACE_DEFAULT ;;
|
|
109
|
+
esac
|
|
110
|
+
printf '%s' "$g"
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
# Directory holding durable pending-reply records for <state-dir>.
|
|
114
|
+
fm_pending_reply_dir() { # <state-dir>
|
|
115
|
+
local state=$1
|
|
116
|
+
if [ -n "${FM_PENDING_REPLY_DIR_OVERRIDE:-}" ]; then
|
|
117
|
+
printf '%s' "$FM_PENDING_REPLY_DIR_OVERRIDE"
|
|
118
|
+
return 0
|
|
119
|
+
fi
|
|
120
|
+
printf '%s/pending-replies' "$state"
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
fm_pending_reply_path() { # <state-dir> <corr_id>
|
|
124
|
+
printf '%s/%s' "$(fm_pending_reply_dir "$1")" "$2"
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
# Privacy-safe correlation id: 16 lowercase hex chars (64 bits of entropy).
|
|
128
|
+
fm_pending_reply_new_id() {
|
|
129
|
+
local raw hex
|
|
130
|
+
if command -v openssl >/dev/null 2>&1; then
|
|
131
|
+
raw=$(openssl rand -hex 8 2>/dev/null || true)
|
|
132
|
+
fi
|
|
133
|
+
if [ -z "$raw" ]; then
|
|
134
|
+
raw=$(printf '%s' "$$-$(date +%s%N 2>/dev/null || date +%s)-$RANDOM$RANDOM" | cksum 2>/dev/null | awk '{print $1}')
|
|
135
|
+
hex=$(printf '%s' "$raw$RANDOM$RANDOM" | shasum -a 256 2>/dev/null | awk '{print $1}')
|
|
136
|
+
raw=${hex:0:16}
|
|
137
|
+
fi
|
|
138
|
+
printf '%s' "$(printf '%s' "$raw" | tr 'A-F' 'a-f' | tr -cd 'a-f0-9' | cut -c1-16)"
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
fm_pending_reply_corr_token() { # <corr_id>
|
|
142
|
+
printf 'corr=%s' "$1"
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
# Extract the first corr=<16hex> token from free text, or empty.
|
|
146
|
+
fm_pending_reply_extract_corr() { # <text>
|
|
147
|
+
local text=$1
|
|
148
|
+
printf '%s' "$text" | grep -oE "$FM_PENDING_REPLY_CORR_RE" 2>/dev/null | head -1 | cut -d= -f2- | tr 'A-F' 'a-f' || true
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
# 0 if <text> carries the exact correlation token for <corr_id>.
|
|
152
|
+
fm_pending_reply_text_has_corr() { # <text> <corr_id>
|
|
153
|
+
local text=$1 corr=$2 token
|
|
154
|
+
token=$(fm_pending_reply_corr_token "$corr")
|
|
155
|
+
case "$text" in
|
|
156
|
+
*"$token"*) return 0 ;;
|
|
157
|
+
esac
|
|
158
|
+
return 1
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
# Sanitize a short request summary: single line, bounded, no control chars.
|
|
162
|
+
fm_pending_reply_summarize() { # <text>
|
|
163
|
+
local text=$1 cleaned
|
|
164
|
+
cleaned=$(printf '%s' "$text" | tr '\t\r\n' ' ' | tr -cd '\11\12\15\40-\176' | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')
|
|
165
|
+
# Drop an already-present marker/corr prefix so the durable summary stays short.
|
|
166
|
+
cleaned=${cleaned#"$FM_FROMFIRST_MARK"}
|
|
167
|
+
cleaned=$(printf '%s' "$cleaned" | sed -E "s/^corr=[A-Fa-f0-9]{16}[[:space:]]*//")
|
|
168
|
+
if [ "${#cleaned}" -gt 120 ]; then
|
|
169
|
+
cleaned="${cleaned:0:117}..."
|
|
170
|
+
fi
|
|
171
|
+
printf '%s' "$cleaned"
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
fm_pending_reply_get() { # <record-path> <key>
|
|
175
|
+
local rec=$1 key=$2
|
|
176
|
+
[ -f "$rec" ] || return 0
|
|
177
|
+
grep "^${key}=" "$rec" 2>/dev/null | tail -1 | cut -d= -f2- || true
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
fm_pending_reply_corr_reusable() { # <state-dir> <corr_id> <task_id>
|
|
181
|
+
local state=$1 corr=$2 task_id=$3 rec phase delivered
|
|
182
|
+
printf '%s' "$corr" | grep -Eq '^[A-Fa-f0-9]{16}$' || return 1
|
|
183
|
+
rec=$(fm_pending_reply_path "$state" "$corr")
|
|
184
|
+
[ -f "$rec" ] || return 1
|
|
185
|
+
[ "$(fm_pending_reply_get "$rec" task_id)" = "$task_id" ] || return 1
|
|
186
|
+
phase=$(fm_pending_reply_get "$rec" phase)
|
|
187
|
+
case "$phase" in
|
|
188
|
+
awaiting_report|recovery_sending|recovery_sent) return 0 ;;
|
|
189
|
+
delivery_unknown)
|
|
190
|
+
delivered=$(fm_pending_reply_get "$rec" delivered_epoch)
|
|
191
|
+
[ -z "$delivered" ]
|
|
192
|
+
return $?
|
|
193
|
+
;;
|
|
194
|
+
esac
|
|
195
|
+
return 1
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
# Rewrite one key in a pending-reply record atomically. Other keys preserved.
|
|
199
|
+
fm_pending_reply_set() { # <record-path> <key> <value>
|
|
200
|
+
local rec=$1 key=$2 value=$3 dir base tmp line
|
|
201
|
+
[ -f "$rec" ] || return 1
|
|
202
|
+
dir=$(dirname "$rec")
|
|
203
|
+
base=$(basename "$rec")
|
|
204
|
+
tmp="$dir/.${base}.tmp.$$"
|
|
205
|
+
: > "$tmp" || return 1
|
|
206
|
+
while IFS= read -r line || [ -n "$line" ]; do
|
|
207
|
+
case "$line" in
|
|
208
|
+
"${key}="*) continue ;;
|
|
209
|
+
esac
|
|
210
|
+
printf '%s\n' "$line" >> "$tmp" || return 1
|
|
211
|
+
done < "$rec"
|
|
212
|
+
printf '%s=%s\n' "$key" "$value" >> "$tmp" || return 1
|
|
213
|
+
mv -f "$tmp" "$rec"
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
# Embed or replace a correlation token after the from-firstmate marker.
|
|
217
|
+
# Idempotent for the same corr; replaces a different leading corr token.
|
|
218
|
+
# Result is assigned to <result-var>.
|
|
219
|
+
# Trailing newlines in the request body are preserved: never strip via bare
|
|
220
|
+
# $(...) on the body (command substitution removes trailing newlines).
|
|
221
|
+
fm_pending_reply_embed_corr() { # <message> <corr_id> <result-var>
|
|
222
|
+
local message=$1 corr=$2 result_var=$3 body token marked existing
|
|
223
|
+
[ -n "$result_var" ] || return 2
|
|
224
|
+
token=$(fm_pending_reply_corr_token "$corr")
|
|
225
|
+
fm_message_mark_from_firstmate "$message" marked
|
|
226
|
+
body=${marked#"$FM_FROMFIRST_MARK"}
|
|
227
|
+
# Strip a leading corr=<16hex> plus following blanks (space/tab only).
|
|
228
|
+
existing=${body:0:21}
|
|
229
|
+
case "$existing" in
|
|
230
|
+
corr=[a-fA-F0-9][a-fA-F0-9][a-fA-F0-9][a-fA-F0-9][a-fA-F0-9][a-fA-F0-9][a-fA-F0-9][a-fA-F0-9][a-fA-F0-9][a-fA-F0-9][a-fA-F0-9][a-fA-F0-9][a-fA-F0-9][a-fA-F0-9][a-fA-F0-9][a-fA-F0-9])
|
|
231
|
+
body=${body:21}
|
|
232
|
+
while [ "${body# }" != "$body" ]; do body=${body# }; done
|
|
233
|
+
while [ "${body#$'\t'}" != "$body" ]; do body=${body#$'\t'}; done
|
|
234
|
+
;;
|
|
235
|
+
esac
|
|
236
|
+
printf -v "$result_var" '%s' "${FM_FROMFIRST_MARK}${token} ${body}"
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
# Create a durable pending-reply expectation. Prints corr_id on success.
|
|
240
|
+
# Does not deliver anything. Fails if parent paths cannot be prepared.
|
|
241
|
+
fm_pending_reply_create() { # <parent-home> <state-dir> <task_id> <request-text>
|
|
242
|
+
local parent_home=$1 state=$2 task_id=$3 request_text=$4
|
|
243
|
+
local dir rec corr now summary status_path tmp
|
|
244
|
+
[ -n "$parent_home" ] && [ -n "$state" ] && [ -n "$task_id" ] || return 2
|
|
245
|
+
dir=$(fm_pending_reply_dir "$state")
|
|
246
|
+
mkdir -p "$dir" || return 1
|
|
247
|
+
chmod 700 "$dir" 2>/dev/null || true
|
|
248
|
+
corr=$(fm_pending_reply_new_id)
|
|
249
|
+
[ "${#corr}" -eq 16 ] || return 1
|
|
250
|
+
rec=$(fm_pending_reply_path "$state" "$corr")
|
|
251
|
+
# Extremely unlikely collision; regenerate once.
|
|
252
|
+
if [ -e "$rec" ]; then
|
|
253
|
+
corr=$(fm_pending_reply_new_id)
|
|
254
|
+
rec=$(fm_pending_reply_path "$state" "$corr")
|
|
255
|
+
[ ! -e "$rec" ] || return 1
|
|
256
|
+
fi
|
|
257
|
+
now=$(fm_pending_reply_now)
|
|
258
|
+
summary=$(fm_pending_reply_summarize "$request_text")
|
|
259
|
+
status_path="$state/${task_id}.status"
|
|
260
|
+
# Prefer absolute parent_status when parent_home/state resolve.
|
|
261
|
+
case "$status_path" in
|
|
262
|
+
/*) ;;
|
|
263
|
+
*) status_path="$(cd "$state" 2>/dev/null && pwd)/${task_id}.status" ;;
|
|
264
|
+
esac
|
|
265
|
+
case "$parent_home" in
|
|
266
|
+
/*) ;;
|
|
267
|
+
*) parent_home=$(cd "$parent_home" 2>/dev/null && pwd) || parent_home=$1 ;;
|
|
268
|
+
esac
|
|
269
|
+
tmp="$dir/.${corr}.tmp.$$"
|
|
270
|
+
cat > "$tmp" <<EOF
|
|
271
|
+
schema=$FM_PENDING_REPLY_SCHEMA
|
|
272
|
+
corr_id=$corr
|
|
273
|
+
task_id=$task_id
|
|
274
|
+
parent_home=$parent_home
|
|
275
|
+
parent_status=$status_path
|
|
276
|
+
parent_status_scan_signature=
|
|
277
|
+
request_summary=$summary
|
|
278
|
+
created_epoch=$now
|
|
279
|
+
delivered_epoch=
|
|
280
|
+
phase=awaiting_report
|
|
281
|
+
turn_seen_busy=0
|
|
282
|
+
request_turn_completed_epoch=
|
|
283
|
+
recovery_attempted_epoch=
|
|
284
|
+
recovery_sender_pid=
|
|
285
|
+
recovery_sender_identity=
|
|
286
|
+
recovery_sent_epoch=
|
|
287
|
+
recovery_delivery_outcome=
|
|
288
|
+
recovery_turn_seen_busy=0
|
|
289
|
+
recovery_turn_completed_epoch=
|
|
290
|
+
escalated_epoch=
|
|
291
|
+
resolved_epoch=
|
|
292
|
+
resolved_via=
|
|
293
|
+
wrong_home_hits=0
|
|
294
|
+
wrong_home_sightings=
|
|
295
|
+
wrong_home_scan_signature=
|
|
296
|
+
grace_secs=$(fm_pending_reply_grace_secs)
|
|
297
|
+
EOF
|
|
298
|
+
chmod 600 "$tmp" 2>/dev/null || true
|
|
299
|
+
mv -f "$tmp" "$rec" || return 1
|
|
300
|
+
printf '%s' "$corr"
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
# Mark delivery success for an existing expectation. Never resolves.
|
|
304
|
+
fm_pending_reply_mark_delivered() { # <state-dir> <corr_id> [confirmed-epoch]
|
|
305
|
+
local state=$1 corr=$2 confirmed_epoch=${3-} rec phase delivered now
|
|
306
|
+
rec=$(fm_pending_reply_path "$state" "$corr")
|
|
307
|
+
[ -f "$rec" ] || return 1
|
|
308
|
+
phase=$(fm_pending_reply_get "$rec" phase)
|
|
309
|
+
case "$phase" in
|
|
310
|
+
awaiting_report|delivery_unknown|recovery_sending|recovery_sent|escalated|resolved) ;;
|
|
311
|
+
*) return 1 ;;
|
|
312
|
+
esac
|
|
313
|
+
delivered=$(fm_pending_reply_get "$rec" delivered_epoch)
|
|
314
|
+
if [ -z "$delivered" ]; then
|
|
315
|
+
now=${confirmed_epoch:-$(fm_pending_reply_now)}
|
|
316
|
+
fm_pending_reply_set "$rec" delivered_epoch "$now" || return 1
|
|
317
|
+
fi
|
|
318
|
+
if [ "$phase" = delivery_unknown ]; then
|
|
319
|
+
fm_pending_reply_set "$rec" phase awaiting_report || return 1
|
|
320
|
+
fi
|
|
321
|
+
return 0
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
fm_pending_reply_delivery_confirmation_path() { # <state-dir> <corr_id>
|
|
325
|
+
printf '%s/.delivery-confirmed-%s' "$(fm_pending_reply_dir "$1")" "$2"
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
fm_pending_reply_write_delivery_confirmation() { # <state-dir> <corr_id> <state> <value>
|
|
329
|
+
local pending_state=$1 corr=$2 delivery_state=$3 value=$4 marker dir tmp
|
|
330
|
+
marker=$(fm_pending_reply_delivery_confirmation_path "$pending_state" "$corr")
|
|
331
|
+
dir=$(dirname "$marker")
|
|
332
|
+
mkdir -p "$dir" || return 1
|
|
333
|
+
tmp="$marker.tmp.$$"
|
|
334
|
+
printf '%s=%s\n' "$delivery_state" "$value" > "$tmp" || return 1
|
|
335
|
+
chmod 600 "$tmp" 2>/dev/null || true
|
|
336
|
+
mv -f "$tmp" "$marker"
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
fm_pending_reply_prepare_delivery() { # <state-dir> <corr_id>
|
|
340
|
+
local state=$1 corr=$2 rec delivered marker now
|
|
341
|
+
rec=$(fm_pending_reply_path "$state" "$corr")
|
|
342
|
+
[ -f "$rec" ] || return 1
|
|
343
|
+
delivered=$(fm_pending_reply_get "$rec" delivered_epoch)
|
|
344
|
+
[ -z "$delivered" ] || return 0
|
|
345
|
+
marker=$(fm_pending_reply_delivery_confirmation_path "$state" "$corr")
|
|
346
|
+
[ -f "$marker" ] && return 0
|
|
347
|
+
now=$(fm_pending_reply_now)
|
|
348
|
+
fm_pending_reply_write_delivery_confirmation "$state" "$corr" attempted "$now"
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
fm_pending_reply_confirm_delivery() { # <state-dir> <corr_id>
|
|
352
|
+
local state=$1 corr=$2 lock rc=0
|
|
353
|
+
local STATE FM_WAKE_QUEUE FM_WAKE_QUEUE_LOCK
|
|
354
|
+
STATE=$state
|
|
355
|
+
lock="$state/.pending-reply-$corr.lock"
|
|
356
|
+
. "$_FM_PENDING_REPLY_LIB_DIR/fm-wake-lib.sh"
|
|
357
|
+
fm_lock_acquire_wait "$lock" || return 1
|
|
358
|
+
_fm_pending_reply_confirm_delivery_locked "$@" || rc=$?
|
|
359
|
+
fm_lock_release "$lock"
|
|
360
|
+
return "$rc"
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
_fm_pending_reply_confirm_delivery_locked() { # <state-dir> <corr_id>
|
|
364
|
+
local state=$1 corr=$2 now marker
|
|
365
|
+
marker=$(fm_pending_reply_delivery_confirmation_path "$state" "$corr")
|
|
366
|
+
if ! fm_pending_reply_prepare_delivery "$state" "$corr"; then
|
|
367
|
+
return 1
|
|
368
|
+
fi
|
|
369
|
+
now=$(fm_pending_reply_now)
|
|
370
|
+
fm_pending_reply_write_delivery_confirmation "$state" "$corr" confirmed "$now" || return 1
|
|
371
|
+
if fm_pending_reply_mark_delivered "$state" "$corr" "$now"; then
|
|
372
|
+
rm -f "$marker" 2>/dev/null || true
|
|
373
|
+
return 0
|
|
374
|
+
fi
|
|
375
|
+
return 2
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
# Preserve an expectation when a remote transport disconnect makes delivery
|
|
379
|
+
# completion unknowable. This never resolves or retries the request; it moves
|
|
380
|
+
# the existing prepared record to the owner's explicit unknown-delivery path.
|
|
381
|
+
fm_pending_reply_mark_delivery_unknown() { # <state-dir> <corr_id>
|
|
382
|
+
local state=$1 corr=$2 rec phase
|
|
383
|
+
rec=$(fm_pending_reply_path "$state" "$corr")
|
|
384
|
+
[ -f "$rec" ] || return 1
|
|
385
|
+
phase=$(fm_pending_reply_get "$rec" phase)
|
|
386
|
+
case "$phase" in awaiting_report|delivery_unknown) ;;
|
|
387
|
+
*) return 1 ;;
|
|
388
|
+
esac
|
|
389
|
+
fm_pending_reply_set "$rec" phase delivery_unknown
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
_fm_pending_reply_reconcile_delivery_locked() { # <state-dir> <corr_id>
|
|
393
|
+
local state=$1 corr=$2 rec delivered marker entry delivery_state value epoch
|
|
394
|
+
local grace now age phase
|
|
395
|
+
rec=$(fm_pending_reply_path "$state" "$corr")
|
|
396
|
+
[ -f "$rec" ] || return 1
|
|
397
|
+
marker=$(fm_pending_reply_delivery_confirmation_path "$state" "$corr")
|
|
398
|
+
delivered=$(fm_pending_reply_get "$rec" delivered_epoch)
|
|
399
|
+
if [ -n "$delivered" ]; then
|
|
400
|
+
rm -f "$marker" 2>/dev/null || true
|
|
401
|
+
return 0
|
|
402
|
+
fi
|
|
403
|
+
[ -f "$marker" ] || return 1
|
|
404
|
+
entry=$(cat "$marker" 2>/dev/null || true)
|
|
405
|
+
delivery_state=${entry%%=*}
|
|
406
|
+
value=${entry#*=}
|
|
407
|
+
case "$delivery_state" in
|
|
408
|
+
confirmed)
|
|
409
|
+
epoch=$value
|
|
410
|
+
case "$epoch" in ''|*[!0-9]*) return 1 ;; esac
|
|
411
|
+
fm_pending_reply_mark_delivered "$state" "$corr" "$epoch" || return 1
|
|
412
|
+
rm -f "$marker" 2>/dev/null || true
|
|
413
|
+
return 0
|
|
414
|
+
;;
|
|
415
|
+
attempted)
|
|
416
|
+
epoch=$value
|
|
417
|
+
case "$epoch" in ''|*[!0-9]*) return 1 ;; esac
|
|
418
|
+
grace=$(fm_pending_reply_get "$rec" grace_secs)
|
|
419
|
+
case "$grace" in ''|*[!0-9]*) grace=$(fm_pending_reply_grace_secs) ;; esac
|
|
420
|
+
now=$(fm_pending_reply_now)
|
|
421
|
+
age=$((now - epoch))
|
|
422
|
+
[ "$age" -ge "$grace" ] || return 1
|
|
423
|
+
phase=$(fm_pending_reply_get "$rec" phase)
|
|
424
|
+
[ "$phase" = awaiting_report ] || return 1
|
|
425
|
+
fm_pending_reply_set "$rec" phase delivery_unknown || return 1
|
|
426
|
+
return 0
|
|
427
|
+
;;
|
|
428
|
+
esac
|
|
429
|
+
return 1
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
fm_pending_reply_reconcile_delivery() { # <state-dir> <corr_id>
|
|
433
|
+
local state=$1 corr=$2 lock rc=0
|
|
434
|
+
local STATE FM_WAKE_QUEUE FM_WAKE_QUEUE_LOCK
|
|
435
|
+
STATE=$state
|
|
436
|
+
lock="$state/.pending-reply-$corr.lock"
|
|
437
|
+
. "$_FM_PENDING_REPLY_LIB_DIR/fm-wake-lib.sh"
|
|
438
|
+
fm_lock_acquire_wait "$lock" || return 1
|
|
439
|
+
_fm_pending_reply_reconcile_delivery_locked "$@" || rc=$?
|
|
440
|
+
fm_lock_release "$lock"
|
|
441
|
+
return "$rc"
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
fm_pending_reply_delivery_attempt_unresolved() { # <state-dir> <corr_id>
|
|
445
|
+
local state=$1 corr=$2 rec delivered marker entry
|
|
446
|
+
rec=$(fm_pending_reply_path "$state" "$corr")
|
|
447
|
+
[ -f "$rec" ] && [ ! -L "$rec" ] || return 1
|
|
448
|
+
delivered=$(fm_pending_reply_get "$rec" delivered_epoch)
|
|
449
|
+
[ -z "$delivered" ] || return 1
|
|
450
|
+
marker=$(fm_pending_reply_delivery_confirmation_path "$state" "$corr")
|
|
451
|
+
[ -f "$marker" ] && [ ! -L "$marker" ] || return 1
|
|
452
|
+
entry=$(cat "$marker" 2>/dev/null || true)
|
|
453
|
+
case "$entry" in attempted=*) return 0 ;; esac
|
|
454
|
+
return 1
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
# A definitive backend rejection makes the existing correlation retryable again.
|
|
458
|
+
# Reconciliation may have aged the same attempted sidecar to delivery_unknown
|
|
459
|
+
# while the backend call was in flight, so both undelivered phases converge here
|
|
460
|
+
# under the per-correlation lock; a confirmed delivery can never be reset.
|
|
461
|
+
fm_pending_reply_reset_known_undelivered() { # <state-dir> <corr_id>
|
|
462
|
+
local state=$1 corr=$2 lock rc=0
|
|
463
|
+
local STATE FM_WAKE_QUEUE FM_WAKE_QUEUE_LOCK
|
|
464
|
+
STATE=$state
|
|
465
|
+
lock="$state/.pending-reply-$corr.lock"
|
|
466
|
+
. "$_FM_PENDING_REPLY_LIB_DIR/fm-wake-lib.sh"
|
|
467
|
+
fm_lock_acquire_wait "$lock" || return 1
|
|
468
|
+
_fm_pending_reply_reset_known_undelivered_locked "$@" || rc=$?
|
|
469
|
+
fm_lock_release "$lock"
|
|
470
|
+
return "$rc"
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
_fm_pending_reply_reset_known_undelivered_locked() { # <state-dir> <corr_id>
|
|
474
|
+
local state=$1 corr=$2 rec delivered phase marker entry
|
|
475
|
+
rec=$(fm_pending_reply_path "$state" "$corr")
|
|
476
|
+
[ -f "$rec" ] && [ ! -L "$rec" ] || return 1
|
|
477
|
+
delivered=$(fm_pending_reply_get "$rec" delivered_epoch)
|
|
478
|
+
[ -z "$delivered" ] || return 1
|
|
479
|
+
phase=$(fm_pending_reply_get "$rec" phase)
|
|
480
|
+
case "$phase" in awaiting_report|delivery_unknown) ;; *) return 1 ;; esac
|
|
481
|
+
marker=$(fm_pending_reply_delivery_confirmation_path "$state" "$corr")
|
|
482
|
+
[ -e "$marker" ] || [ -L "$marker" ] || {
|
|
483
|
+
[ "$phase" = awaiting_report ]
|
|
484
|
+
return $?
|
|
485
|
+
}
|
|
486
|
+
[ -f "$marker" ] && [ ! -L "$marker" ] || return 1
|
|
487
|
+
entry=$(cat "$marker" 2>/dev/null || true)
|
|
488
|
+
case "$entry" in attempted=*) ;; *) return 1 ;; esac
|
|
489
|
+
[ "$phase" = awaiting_report ] \
|
|
490
|
+
|| fm_pending_reply_set "$rec" phase awaiting_report || return 1
|
|
491
|
+
rm -f -- "$marker"
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
# Drop an undelivered expectation after a failed send so transport failure does
|
|
495
|
+
# not masquerade as a missed report later.
|
|
496
|
+
fm_pending_reply_discard_undelivered() { # <state-dir> <corr_id>
|
|
497
|
+
local state=$1 corr=$2 rec delivered marker
|
|
498
|
+
rec=$(fm_pending_reply_path "$state" "$corr")
|
|
499
|
+
[ -f "$rec" ] || return 0
|
|
500
|
+
delivered=$(fm_pending_reply_get "$rec" delivered_epoch)
|
|
501
|
+
[ -z "$delivered" ] || return 1
|
|
502
|
+
marker=$(fm_pending_reply_delivery_confirmation_path "$state" "$corr")
|
|
503
|
+
rm -f "$marker" 2>/dev/null || true
|
|
504
|
+
rm -f "$rec"
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
# 0 if a status line is a correlated acknowledgement for <corr_id>.
|
|
508
|
+
# Accepts short status replies and status lines that point at a document.
|
|
509
|
+
# Unrelated verbs without the token never match. Stale/wrong corr never match.
|
|
510
|
+
# The parent's own pending-reply-missed escalation line must not self-resolve:
|
|
511
|
+
# it names the request with pending-reply-id= rather than corr=.
|
|
512
|
+
fm_pending_reply_line_resolves() { # <line> <corr_id>
|
|
513
|
+
local line=$1 corr=$2
|
|
514
|
+
[ -n "$line" ] && [ -n "$corr" ] || return 1
|
|
515
|
+
case "$line" in
|
|
516
|
+
*pending-reply-missed*) return 1 ;;
|
|
517
|
+
esac
|
|
518
|
+
fm_pending_reply_text_has_corr "$line" "$corr"
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
# Scan a status file for a correlated resolve. Prints the matching line or empty.
|
|
522
|
+
fm_pending_reply_find_resolve_line() { # <status-file> <corr_id>
|
|
523
|
+
local status_file=$1 corr=$2 line
|
|
524
|
+
[ -f "$status_file" ] || return 0
|
|
525
|
+
while IFS= read -r line || [ -n "$line" ]; do
|
|
526
|
+
[ -n "$line" ] || continue
|
|
527
|
+
if fm_pending_reply_line_resolves "$line" "$corr"; then
|
|
528
|
+
printf '%s' "$line"
|
|
529
|
+
return 0
|
|
530
|
+
fi
|
|
531
|
+
done < "$status_file"
|
|
532
|
+
return 0
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
fm_pending_reply_file_signature() { # <path>
|
|
536
|
+
local path=$1
|
|
537
|
+
[ -f "$path" ] || { printf 'missing'; return 0; }
|
|
538
|
+
if [ "$(uname -s 2>/dev/null)" = Darwin ]; then
|
|
539
|
+
LC_ALL=C stat -f '%d:%i:%z:%m:%c' "$path" 2>/dev/null || printf 'unreadable'
|
|
540
|
+
else
|
|
541
|
+
LC_ALL=C stat -c '%d:%i:%s:%Y:%Z' "$path" 2>/dev/null || printf 'unreadable'
|
|
542
|
+
fi
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
fm_pending_reply_status_set_signature() { # <status-dir>
|
|
546
|
+
local status_dir=$1 status_file signature
|
|
547
|
+
{
|
|
548
|
+
for status_file in "$status_dir"/*.status; do
|
|
549
|
+
[ -f "$status_file" ] || continue
|
|
550
|
+
signature=$(fm_pending_reply_file_signature "$status_file")
|
|
551
|
+
printf '%s:%s:%s\n' "${#status_file}" "$status_file" "$signature"
|
|
552
|
+
done
|
|
553
|
+
} | cksum 2>/dev/null | awk '{printf "%s-%s", $1, $2}'
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
# Classify how a resolving line acknowledged the request.
|
|
557
|
+
fm_pending_reply_resolve_via_of_line() { # <line>
|
|
558
|
+
local line=$1
|
|
559
|
+
case "$line" in
|
|
560
|
+
*data/*report*|*report.md*|*document*|*pointer*)
|
|
561
|
+
printf 'document'
|
|
562
|
+
;;
|
|
563
|
+
*via-helper*|*fm-secondmate-report*)
|
|
564
|
+
printf 'helper'
|
|
565
|
+
;;
|
|
566
|
+
*)
|
|
567
|
+
printf 'status'
|
|
568
|
+
;;
|
|
569
|
+
esac
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
# Idempotently resolve an expectation from a correlated parent report.
|
|
573
|
+
# Returns 0 when the record is resolved after the call (already or newly).
|
|
574
|
+
fm_pending_reply_try_resolve() { # <state-dir> <corr_id> [status-file-override]
|
|
575
|
+
# Serialized per correlation so a resolution and an escalation cannot interleave.
|
|
576
|
+
# bin/fm-wake-lib.sh owns the lock primitives but assigns its own globals when
|
|
577
|
+
# sourced, so they are declared local here: that contains them to this call
|
|
578
|
+
# instead of leaking into every script that sources this library, without the
|
|
579
|
+
# subshell that would make every later use of them read as a lost write.
|
|
580
|
+
# The lock is released explicitly rather than from an EXIT trap, because a trap
|
|
581
|
+
# in a plain function would clobber the caller's own.
|
|
582
|
+
local state=$1 corr=$2 lock rc=0
|
|
583
|
+
local STATE FM_WAKE_QUEUE FM_WAKE_QUEUE_LOCK
|
|
584
|
+
STATE=$state
|
|
585
|
+
lock="$state/.pending-reply-$corr.lock"
|
|
586
|
+
# shellcheck source=bin/fm-wake-lib.sh
|
|
587
|
+
. "$_FM_PENDING_REPLY_LIB_DIR/fm-wake-lib.sh"
|
|
588
|
+
fm_lock_acquire_wait "$lock" || return 1
|
|
589
|
+
_fm_pending_reply_try_resolve_locked "$@" || rc=$?
|
|
590
|
+
fm_lock_release "$lock"
|
|
591
|
+
return "$rc"
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
_fm_pending_reply_try_resolve_locked() { # <state-dir> <corr_id> [status-file-override]
|
|
595
|
+
local state=$1 corr=$2 status_override=${3-}
|
|
596
|
+
local rec phase delivered marker delivery_entry delivery_state status_file signature previous line via now
|
|
597
|
+
local unconfirmed=0
|
|
598
|
+
rec=$(fm_pending_reply_path "$state" "$corr")
|
|
599
|
+
[ -f "$rec" ] || return 1
|
|
600
|
+
phase=$(fm_pending_reply_get "$rec" phase)
|
|
601
|
+
if [ "$phase" = resolved ]; then
|
|
602
|
+
_fm_pending_reply_close_escalation_locked "$state" "$corr" || true
|
|
603
|
+
return 0
|
|
604
|
+
fi
|
|
605
|
+
delivered=$(fm_pending_reply_get "$rec" delivered_epoch)
|
|
606
|
+
if [ -z "$delivered" ]; then
|
|
607
|
+
marker=$(fm_pending_reply_delivery_confirmation_path "$state" "$corr")
|
|
608
|
+
[ -f "$marker" ] || return 1
|
|
609
|
+
delivery_entry=$(cat "$marker" 2>/dev/null || true)
|
|
610
|
+
delivery_state=${delivery_entry%%=*}
|
|
611
|
+
case "$delivery_state" in attempted|confirmed) ;; *) return 1 ;; esac
|
|
612
|
+
unconfirmed=1
|
|
613
|
+
fi
|
|
614
|
+
status_file=${status_override:-$(fm_pending_reply_get "$rec" parent_status)}
|
|
615
|
+
if [ -z "$status_override" ] && [ "$unconfirmed" = 0 ]; then
|
|
616
|
+
signature=$(fm_pending_reply_file_signature "$status_file")
|
|
617
|
+
previous=$(fm_pending_reply_get "$rec" parent_status_scan_signature)
|
|
618
|
+
[ "$signature" != "$previous" ] || return 1
|
|
619
|
+
fi
|
|
620
|
+
line=$(fm_pending_reply_find_resolve_line "$status_file" "$corr")
|
|
621
|
+
if [ -z "$line" ]; then
|
|
622
|
+
if [ -z "$status_override" ] && [ "$unconfirmed" = 0 ]; then
|
|
623
|
+
fm_pending_reply_set "$rec" parent_status_scan_signature "$signature" || return 1
|
|
624
|
+
fi
|
|
625
|
+
return 1
|
|
626
|
+
fi
|
|
627
|
+
via=$(fm_pending_reply_resolve_via_of_line "$line")
|
|
628
|
+
now=$(fm_pending_reply_now)
|
|
629
|
+
fm_pending_reply_set "$rec" phase resolved || return 1
|
|
630
|
+
if [ -z "$delivered" ]; then
|
|
631
|
+
fm_pending_reply_mark_delivered "$state" "$corr" "$now" || return 1
|
|
632
|
+
rm -f "$marker" 2>/dev/null || true
|
|
633
|
+
fi
|
|
634
|
+
fm_pending_reply_set "$rec" resolved_epoch "$now" || return 1
|
|
635
|
+
fm_pending_reply_set "$rec" resolved_via "$via" || return 1
|
|
636
|
+
# The record is resolved either way; a failed close stays retryable from the
|
|
637
|
+
# watcher tick rather than turning a settled request back into a failure.
|
|
638
|
+
_fm_pending_reply_close_escalation_locked "$state" "$corr" || true
|
|
639
|
+
return 0
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
# Observe backend busy/idle evidence for the active turn without reading chat.
|
|
643
|
+
# busy_state must be one of: busy | idle | unknown.
|
|
644
|
+
fm_pending_reply_observe_busy() { # <state-dir> <corr_id> <busy_state>
|
|
645
|
+
local state=$1 corr=$2 busy_state=$3
|
|
646
|
+
local rec phase delivered now seen completed field_seen field_completed
|
|
647
|
+
rec=$(fm_pending_reply_path "$state" "$corr")
|
|
648
|
+
[ -f "$rec" ] || return 1
|
|
649
|
+
phase=$(fm_pending_reply_get "$rec" phase)
|
|
650
|
+
case "$phase" in
|
|
651
|
+
awaiting_report|recovery_sent) ;;
|
|
652
|
+
*) return 0 ;;
|
|
653
|
+
esac
|
|
654
|
+
delivered=$(fm_pending_reply_get "$rec" delivered_epoch)
|
|
655
|
+
[ -n "$delivered" ] || return 0
|
|
656
|
+
if [ "$phase" = awaiting_report ]; then
|
|
657
|
+
field_seen=turn_seen_busy
|
|
658
|
+
field_completed=request_turn_completed_epoch
|
|
659
|
+
else
|
|
660
|
+
field_seen=recovery_turn_seen_busy
|
|
661
|
+
field_completed=recovery_turn_completed_epoch
|
|
662
|
+
fi
|
|
663
|
+
seen=$(fm_pending_reply_get "$rec" "$field_seen")
|
|
664
|
+
completed=$(fm_pending_reply_get "$rec" "$field_completed")
|
|
665
|
+
case "$busy_state" in
|
|
666
|
+
busy)
|
|
667
|
+
if [ "$seen" != 1 ]; then
|
|
668
|
+
fm_pending_reply_set "$rec" "$field_seen" 1 || return 1
|
|
669
|
+
fi
|
|
670
|
+
;;
|
|
671
|
+
idle)
|
|
672
|
+
if [ -z "$completed" ]; then
|
|
673
|
+
# Prefer a busy->idle transition. Also accept a pure idle after delivery
|
|
674
|
+
# when the first observation already missed the busy window (fast turns).
|
|
675
|
+
if [ "$seen" = 1 ] || [ "$seen" = 0 ]; then
|
|
676
|
+
now=$(fm_pending_reply_now)
|
|
677
|
+
fm_pending_reply_set "$rec" "$field_completed" "$now" || return 1
|
|
678
|
+
fi
|
|
679
|
+
fi
|
|
680
|
+
;;
|
|
681
|
+
unknown)
|
|
682
|
+
# No independent proof; leave completion unset.
|
|
683
|
+
;;
|
|
684
|
+
*)
|
|
685
|
+
return 2
|
|
686
|
+
;;
|
|
687
|
+
esac
|
|
688
|
+
return 0
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
fm_pending_reply_fallback_idle_eligible() { # <record-path>
|
|
692
|
+
local rec=$1 phase start seen grace now age
|
|
693
|
+
phase=$(fm_pending_reply_get "$rec" phase)
|
|
694
|
+
case "$phase" in
|
|
695
|
+
awaiting_report)
|
|
696
|
+
start=$(fm_pending_reply_get "$rec" delivered_epoch)
|
|
697
|
+
seen=$(fm_pending_reply_get "$rec" turn_seen_busy)
|
|
698
|
+
;;
|
|
699
|
+
recovery_sent)
|
|
700
|
+
start=$(fm_pending_reply_get "$rec" recovery_sent_epoch)
|
|
701
|
+
seen=$(fm_pending_reply_get "$rec" recovery_turn_seen_busy)
|
|
702
|
+
;;
|
|
703
|
+
*) return 1 ;;
|
|
704
|
+
esac
|
|
705
|
+
[ "$seen" = 1 ] && return 0
|
|
706
|
+
grace=$(fm_pending_reply_get "$rec" grace_secs)
|
|
707
|
+
case "$start" in ''|*[!0-9]*) return 1 ;; esac
|
|
708
|
+
case "$grace" in ''|*[!0-9]*) grace=$(fm_pending_reply_grace_secs) ;; esac
|
|
709
|
+
now=$(fm_pending_reply_now)
|
|
710
|
+
age=$((now - start))
|
|
711
|
+
[ "$age" -ge "$grace" ]
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
# fm_pending_reply_backend_observation: one busy/idle observation of a
|
|
715
|
+
# SECONDMATE endpoint, without ever reading its conversation.
|
|
716
|
+
#
|
|
717
|
+
# Deliberately NOT the semantic busy-state contract (bin/fm-busy-lib.sh).
|
|
718
|
+
# That contract covers ordinary task workers, whose turn lifecycle firstmate
|
|
719
|
+
# wires at spawn; a secondmate has no such wiring because an idle secondmate
|
|
720
|
+
# pane is healthy and it runs no supervised turn sequence of its own. This
|
|
721
|
+
# observation exists only to notice a busy-then-idle transition around one
|
|
722
|
+
# delivered request, so it is a delivery-confirmation signal in the same
|
|
723
|
+
# category as the submit acknowledgement matcher in bin/fm-composer-lib.sh - never task
|
|
724
|
+
# state, and never a source consumers can confuse with semantic state.
|
|
725
|
+
#
|
|
726
|
+
# It stays harness-scoped (fm_busy_lines_match with the recorded harness, no
|
|
727
|
+
# global OR of every vendor signature), so one harness's output cannot make
|
|
728
|
+
# another read busy, and a weak rendered idle degrades to `fallback-idle`,
|
|
729
|
+
# which the caller accepts as idle only after its grace window.
|
|
730
|
+
fm_pending_reply_backend_observation() { # <backend> <target> [expected-label] [harness]
|
|
731
|
+
local backend=$1 target=$2 expected_label=${3-} harness=${4-} native tail40
|
|
732
|
+
native=$(fm_backend_busy_state "$backend" "$target" 2>/dev/null || printf 'unknown')
|
|
733
|
+
case "$native" in
|
|
734
|
+
busy|idle) printf '%s' "$native"; return 0 ;;
|
|
735
|
+
esac
|
|
736
|
+
tail40=$(fm_backend_capture "$backend" "$target" 40 "$expected_label" 2>/dev/null) \
|
|
737
|
+
|| { printf 'unknown'; return 0; }
|
|
738
|
+
if printf '%s' "$tail40" | grep -v '^[[:space:]]*$' | tail -6 \
|
|
739
|
+
| fm_busy_lines_match "$harness"; then
|
|
740
|
+
printf 'busy'
|
|
741
|
+
else
|
|
742
|
+
printf 'fallback-idle'
|
|
743
|
+
fi
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
fm_pending_reply_busy_state_from_observation() { # <record-path> <observation>
|
|
747
|
+
local rec=$1 observation=$2
|
|
748
|
+
case "$observation" in
|
|
749
|
+
busy|idle|unknown) printf '%s' "$observation" ;;
|
|
750
|
+
fallback-idle)
|
|
751
|
+
if fm_pending_reply_fallback_idle_eligible "$rec"; then
|
|
752
|
+
printf 'idle'
|
|
753
|
+
else
|
|
754
|
+
printf 'unknown'
|
|
755
|
+
fi
|
|
756
|
+
;;
|
|
757
|
+
*) printf 'unknown' ;;
|
|
758
|
+
esac
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
# Explicit turn-completion proof (for tests and turn-end backends that surface
|
|
762
|
+
# a completion event without a busy/idle pair).
|
|
763
|
+
fm_pending_reply_mark_turn_completed() { # <state-dir> <corr_id> [which: request|recovery]
|
|
764
|
+
local state=$1 corr=$2 which=${3:-request}
|
|
765
|
+
local rec phase field now
|
|
766
|
+
rec=$(fm_pending_reply_path "$state" "$corr")
|
|
767
|
+
[ -f "$rec" ] || return 1
|
|
768
|
+
phase=$(fm_pending_reply_get "$rec" phase)
|
|
769
|
+
case "$which" in
|
|
770
|
+
request) field=request_turn_completed_epoch ;;
|
|
771
|
+
recovery) field=recovery_turn_completed_epoch ;;
|
|
772
|
+
*) return 2 ;;
|
|
773
|
+
esac
|
|
774
|
+
now=$(fm_pending_reply_now)
|
|
775
|
+
fm_pending_reply_set "$rec" "$field" "$now" || return 1
|
|
776
|
+
# Keep phase consistent with which turn completed.
|
|
777
|
+
if [ "$which" = recovery ] && [ "$phase" = awaiting_report ]; then
|
|
778
|
+
: # recovery completion only meaningful after recovery_sent
|
|
779
|
+
fi
|
|
780
|
+
return 0
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
# --- remote reply-channel freshness -----------------------------------------
|
|
784
|
+
#
|
|
785
|
+
# A LOCAL secondmate appends its report straight into the parent's
|
|
786
|
+
# state/<id>.status, so an absent correlated line there is immediate evidence
|
|
787
|
+
# that no report was written. A REMOTE mate's reports reach that same file only
|
|
788
|
+
# through the asynchronous mirror in bin/fm-procevent-remote-reply.sh, so the
|
|
789
|
+
# same absence proves nothing until that mirror has actually been read past the
|
|
790
|
+
# turn that should have produced the report. Without this distinction the guard
|
|
791
|
+
# nags a REPOST REQUIRED for a reply the mate did write and the parent simply
|
|
792
|
+
# had not received yet - the common case, because the mirror's poll window is
|
|
793
|
+
# comparable to the recovery grace.
|
|
794
|
+
#
|
|
795
|
+
# The mirror therefore publishes one watermark: the epoch at which it last knew
|
|
796
|
+
# it had read the remote log through its end. Only that adapter writes it (it
|
|
797
|
+
# owns the channel), and only this library reads it. A channel that is behind,
|
|
798
|
+
# unarmed, or broken simply never advances the watermark, so the request stays
|
|
799
|
+
# durably open and un-nagged; the mirror escalates its own continuity failures.
|
|
800
|
+
fm_pending_reply_remote_channel_watermark_path() { # <state-dir> <task_id>
|
|
801
|
+
printf '%s/remote-replies/%s.caught-up' "$1" "$2"
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
# Record that the mirrored remote reply log for <task_id> was read through its
|
|
805
|
+
# end at <epoch> (default now). Called only by the remote reply adapter.
|
|
806
|
+
fm_pending_reply_note_remote_channel_caught_up() { # <state-dir> <task_id> [epoch]
|
|
807
|
+
local state=$1 task_id=$2 epoch=${3-} path dir tmp
|
|
808
|
+
[ -n "$state" ] && [ -n "$task_id" ] || return 2
|
|
809
|
+
case "$epoch" in ''|*[!0-9]*) epoch=$(fm_pending_reply_now) ;; esac
|
|
810
|
+
path=$(fm_pending_reply_remote_channel_watermark_path "$state" "$task_id")
|
|
811
|
+
dir=$(dirname "$path")
|
|
812
|
+
mkdir -p "$dir" || return 1
|
|
813
|
+
chmod 700 "$dir" 2>/dev/null || true
|
|
814
|
+
[ ! -L "$path" ] || return 1
|
|
815
|
+
tmp="$dir/.caught-up.$task_id.$$"
|
|
816
|
+
printf 'caught_up_epoch=%s\n' "$epoch" > "$tmp" || { rm -f -- "$tmp"; return 1; }
|
|
817
|
+
chmod 600 "$tmp" 2>/dev/null || true
|
|
818
|
+
mv -f -- "$tmp" "$path"
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
# Print the watermark epoch, or nothing when the channel never reported itself
|
|
822
|
+
# caught up. Never invents a value.
|
|
823
|
+
fm_pending_reply_remote_channel_epoch() { # <state-dir> <task_id>
|
|
824
|
+
local path epoch
|
|
825
|
+
path=$(fm_pending_reply_remote_channel_watermark_path "$1" "$2")
|
|
826
|
+
[ -f "$path" ] && [ ! -L "$path" ] || return 0
|
|
827
|
+
epoch=$(sed -n 's/^caught_up_epoch=//p' "$path" 2>/dev/null | head -1)
|
|
828
|
+
case "$epoch" in ''|*[!0-9]*) return 0 ;; esac
|
|
829
|
+
printf '%s' "$epoch"
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
# 0 when <task_id> is a secondmate whose reports cross a machine boundary.
|
|
833
|
+
fm_pending_reply_target_is_remote() { # <state-dir> <task_id>
|
|
834
|
+
local meta="$1/$2.meta"
|
|
835
|
+
[ -f "$meta" ] || return 1
|
|
836
|
+
[ -n "$(fm_meta_get "$meta" remote_host)" ]
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
# 0 when "no correlated report in the parent status log" is admissible evidence
|
|
840
|
+
# that the mate never reported: always for a local target, and for a remote one
|
|
841
|
+
# only once the mirror has been read through its end at or after <since-epoch>.
|
|
842
|
+
fm_pending_reply_missing_report_is_evidence() { # <state-dir> <task_id> <since-epoch>
|
|
843
|
+
local state=$1 task_id=$2 since=$3 caught
|
|
844
|
+
fm_pending_reply_target_is_remote "$state" "$task_id" || return 0
|
|
845
|
+
case "$since" in ''|*[!0-9]*) return 1 ;; esac
|
|
846
|
+
caught=$(fm_pending_reply_remote_channel_epoch "$state" "$task_id")
|
|
847
|
+
[ -n "$caught" ] || return 1
|
|
848
|
+
[ "$caught" -ge "$since" ]
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
# Build the one automatic recovery message for a pending record.
|
|
852
|
+
fm_pending_reply_recovery_message() { # <record-path>
|
|
853
|
+
local rec=$1 corr summary token msg
|
|
854
|
+
corr=$(fm_pending_reply_get "$rec" corr_id)
|
|
855
|
+
summary=$(fm_pending_reply_get "$rec" request_summary)
|
|
856
|
+
token=$(fm_pending_reply_corr_token "$corr")
|
|
857
|
+
msg="REPOST REQUIRED: previous marked request had no correlated parent report. Reply on the parent status channel including ${token}. Original request: ${summary}"
|
|
858
|
+
fm_pending_reply_embed_corr "$msg" "$corr" msg
|
|
859
|
+
printf '%s' "$msg"
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
# Deliver the recovery message once. Caller must hold phase awaiting_report with
|
|
863
|
+
# turn completed and grace elapsed. Uses FM_PENDING_REPLY_SEND_HOOK when set
|
|
864
|
+
# (tests), otherwise invokes fm-send with FM_PENDING_REPLY_EXISTING_CORR so a
|
|
865
|
+
# second expectation is not created.
|
|
866
|
+
fm_pending_reply_send_recovery() { # <state-dir> <corr_id>
|
|
867
|
+
local state=$1 corr=$2
|
|
868
|
+
local rec phase completed delivered attempted grace now age task_id msg parent_home send_status=0
|
|
869
|
+
local sender_pid sender_identity
|
|
870
|
+
rec=$(fm_pending_reply_path "$state" "$corr")
|
|
871
|
+
[ -f "$rec" ] || return 1
|
|
872
|
+
phase=$(fm_pending_reply_get "$rec" phase)
|
|
873
|
+
[ "$phase" = awaiting_report ] || return 1
|
|
874
|
+
attempted=$(fm_pending_reply_get "$rec" recovery_attempted_epoch)
|
|
875
|
+
if [ -n "$attempted" ]; then
|
|
876
|
+
fm_pending_reply_reconcile_recovery "$state" "$corr" || true
|
|
877
|
+
return 1
|
|
878
|
+
fi
|
|
879
|
+
completed=$(fm_pending_reply_get "$rec" request_turn_completed_epoch)
|
|
880
|
+
[ -n "$completed" ] || return 1
|
|
881
|
+
delivered=$(fm_pending_reply_get "$rec" delivered_epoch)
|
|
882
|
+
[ -n "$delivered" ] || return 1
|
|
883
|
+
grace=$(fm_pending_reply_get "$rec" grace_secs)
|
|
884
|
+
case "$grace" in ''|*[!0-9]*) grace=$(fm_pending_reply_grace_secs) ;; esac
|
|
885
|
+
now=$(fm_pending_reply_now)
|
|
886
|
+
age=$((now - delivered))
|
|
887
|
+
[ "$age" -ge "$grace" ] || return 1
|
|
888
|
+
task_id=$(fm_pending_reply_get "$rec" task_id)
|
|
889
|
+
# A remote mate's report may exist and simply not have been mirrored yet.
|
|
890
|
+
fm_pending_reply_missing_report_is_evidence "$state" "$task_id" "$completed" || return 1
|
|
891
|
+
parent_home=$(fm_pending_reply_get "$rec" parent_home)
|
|
892
|
+
msg=$(fm_pending_reply_recovery_message "$rec")
|
|
893
|
+
sender_pid=${BASHPID:-$$}
|
|
894
|
+
sender_identity=$(fm_pending_reply_pid_identity "$sender_pid") || return 1
|
|
895
|
+
fm_pending_reply_set "$rec" recovery_sender_pid "$sender_pid" || return 1
|
|
896
|
+
fm_pending_reply_set "$rec" recovery_sender_identity "$sender_identity" || return 1
|
|
897
|
+
fm_pending_reply_set "$rec" recovery_attempted_epoch "$now" || return 1
|
|
898
|
+
fm_pending_reply_set "$rec" phase recovery_sending || return 1
|
|
899
|
+
if [ -n "${FM_PENDING_REPLY_SEND_HOOK:-}" ]; then
|
|
900
|
+
# Hook receives: task_id message
|
|
901
|
+
# shellcheck disable=SC2086
|
|
902
|
+
if ! eval "$FM_PENDING_REPLY_SEND_HOOK" "$(printf '%q' "$task_id")" "$(printf '%q' "$msg")"; then
|
|
903
|
+
send_status=1
|
|
904
|
+
fi
|
|
905
|
+
else
|
|
906
|
+
if [ -z "$parent_home" ] || [ ! -d "$parent_home" ]; then
|
|
907
|
+
send_status=1
|
|
908
|
+
elif ! env FM_HOME="$parent_home" FM_PENDING_REPLY_EXISTING_CORR="$corr" \
|
|
909
|
+
"$_FM_PENDING_REPLY_LIB_DIR/fm-send.sh" "$task_id" "$msg"; then
|
|
910
|
+
send_status=1
|
|
911
|
+
fi
|
|
912
|
+
fi
|
|
913
|
+
if [ "$send_status" = 0 ]; then
|
|
914
|
+
fm_pending_reply_finish_recovery "$state" "$corr" confirmed
|
|
915
|
+
return $?
|
|
916
|
+
fi
|
|
917
|
+
fm_pending_reply_finish_recovery "$state" "$corr" failed || return 1
|
|
918
|
+
return 1
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
fm_pending_reply_pid_identity() { # <pid>
|
|
922
|
+
local pid=$1 identity
|
|
923
|
+
case "$pid" in ''|*[!0-9]*) return 1 ;; esac
|
|
924
|
+
identity=$(COLUMNS=10000 LC_ALL=C ps -p "$pid" -o lstart= -o command= 2>/dev/null) || return 1
|
|
925
|
+
[ -n "$identity" ] || return 1
|
|
926
|
+
printf '%s' "$identity"
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
fm_pending_reply_sender_alive() { # <record-path>
|
|
930
|
+
local rec=$1 pid expected actual
|
|
931
|
+
pid=$(fm_pending_reply_get "$rec" recovery_sender_pid)
|
|
932
|
+
expected=$(fm_pending_reply_get "$rec" recovery_sender_identity)
|
|
933
|
+
[ -n "$expected" ] || return 1
|
|
934
|
+
actual=$(fm_pending_reply_pid_identity "$pid") || return 1
|
|
935
|
+
[ "$actual" = "$expected" ]
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
fm_pending_reply_finish_recovery() { # <state-dir> <corr_id> <confirmed|failed>
|
|
939
|
+
local state=$1 corr=$2 outcome=$3 rec phase now sent
|
|
940
|
+
rec=$(fm_pending_reply_path "$state" "$corr")
|
|
941
|
+
[ -f "$rec" ] || return 1
|
|
942
|
+
phase=$(fm_pending_reply_get "$rec" phase)
|
|
943
|
+
[ "$phase" = recovery_sending ] || return 1
|
|
944
|
+
fm_pending_reply_set "$rec" recovery_delivery_outcome "$outcome" || return 1
|
|
945
|
+
if [ "$outcome" = confirmed ]; then
|
|
946
|
+
sent=$(fm_pending_reply_get "$rec" recovery_sent_epoch)
|
|
947
|
+
if [ -z "$sent" ]; then
|
|
948
|
+
now=$(fm_pending_reply_now)
|
|
949
|
+
fm_pending_reply_set "$rec" recovery_sent_epoch "$now" || return 1
|
|
950
|
+
fi
|
|
951
|
+
fm_pending_reply_set "$rec" recovery_turn_seen_busy 0 || return 1
|
|
952
|
+
fm_pending_reply_set "$rec" recovery_turn_completed_epoch "" || return 1
|
|
953
|
+
fm_pending_reply_set "$rec" phase recovery_sent || return 1
|
|
954
|
+
else
|
|
955
|
+
[ "$outcome" = failed ] || return 1
|
|
956
|
+
fm_pending_reply_set "$rec" phase recovery_failed || return 1
|
|
957
|
+
fi
|
|
958
|
+
}
|
|
959
|
+
|
|
960
|
+
fm_pending_reply_reconcile_recovery() { # <state-dir> <corr_id>
|
|
961
|
+
local state=$1 corr=$2 rec phase attempted outcome
|
|
962
|
+
rec=$(fm_pending_reply_path "$state" "$corr")
|
|
963
|
+
[ -f "$rec" ] || return 1
|
|
964
|
+
phase=$(fm_pending_reply_get "$rec" phase)
|
|
965
|
+
case "$phase" in awaiting_report|recovery_sending) ;; *) return 1 ;; esac
|
|
966
|
+
attempted=$(fm_pending_reply_get "$rec" recovery_attempted_epoch)
|
|
967
|
+
[ -n "$attempted" ] || return 1
|
|
968
|
+
case "$attempted" in *[!0-9]*) return 1 ;; esac
|
|
969
|
+
outcome=$(fm_pending_reply_get "$rec" recovery_delivery_outcome)
|
|
970
|
+
case "$outcome" in
|
|
971
|
+
confirmed) fm_pending_reply_finish_recovery "$state" "$corr" confirmed; return $? ;;
|
|
972
|
+
failed) fm_pending_reply_finish_recovery "$state" "$corr" failed; return $? ;;
|
|
973
|
+
unknown)
|
|
974
|
+
fm_pending_reply_set "$rec" phase recovery_unknown || return 1
|
|
975
|
+
return 0
|
|
976
|
+
;;
|
|
977
|
+
esac
|
|
978
|
+
fm_pending_reply_sender_alive "$rec" && return 1
|
|
979
|
+
fm_pending_reply_set "$rec" recovery_delivery_outcome unknown || return 1
|
|
980
|
+
fm_pending_reply_set "$rec" phase recovery_unknown || return 1
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
# The decision key an escalation for <corr_id> opens in the parent status log.
|
|
984
|
+
# Per-request rather than the bare default key, so one request's escalation
|
|
985
|
+
# neither masks nor is masked by an unrelated decision on the same task.
|
|
986
|
+
fm_pending_reply_escalation_key() { # <corr_id>
|
|
987
|
+
printf 'pending-reply-%s' "$1"
|
|
988
|
+
}
|
|
989
|
+
|
|
990
|
+
fm_pending_reply_escalation_payload() { # <record-path> <kind>
|
|
991
|
+
local rec=$1 kind=$2 task_id corr summary outcome token
|
|
992
|
+
task_id=$(fm_pending_reply_get "$rec" task_id)
|
|
993
|
+
corr=$(fm_pending_reply_get "$rec" corr_id)
|
|
994
|
+
summary=$(fm_pending_reply_get "$rec" request_summary)
|
|
995
|
+
[ -n "$task_id" ] && [ -n "$corr" ] || return 1
|
|
996
|
+
case "$kind" in
|
|
997
|
+
missed)
|
|
998
|
+
token=pending-reply-missed
|
|
999
|
+
;;
|
|
1000
|
+
delivery-unknown)
|
|
1001
|
+
token=pending-reply-delivery-unknown
|
|
1002
|
+
;;
|
|
1003
|
+
recovery-delivery)
|
|
1004
|
+
outcome=$(fm_pending_reply_get "$rec" recovery_delivery_outcome)
|
|
1005
|
+
case "$outcome" in failed|unknown) ;; *) return 1 ;; esac
|
|
1006
|
+
token="pending-reply-recovery-delivery-$outcome"
|
|
1007
|
+
;;
|
|
1008
|
+
*) return 1 ;;
|
|
1009
|
+
esac
|
|
1010
|
+
printf '%s: task=%s pending-reply-id=%s request=%s' "$token" "$task_id" "$corr" "$summary"
|
|
1011
|
+
}
|
|
1012
|
+
|
|
1013
|
+
# The escalation line this library published for <corr_id>, or empty. A legacy
|
|
1014
|
+
# unkeyed escalation is matched and closed under the shared default key while
|
|
1015
|
+
# that exact escalation remains open. If an unrelated decision has since taken
|
|
1016
|
+
# over that key, the close is withheld so the unrelated decision is not cleared.
|
|
1017
|
+
fm_pending_reply_escalation_line() { # <status-file> <record-path> <corr_id>
|
|
1018
|
+
local status_file=$1 rec=$2 corr=$3 line found='' kind payload own_key
|
|
1019
|
+
[ -f "$status_file" ] || return 0
|
|
1020
|
+
[ "$(fm_pending_reply_get "$rec" corr_id)" = "$corr" ] || return 0
|
|
1021
|
+
own_key=$(fm_pending_reply_escalation_key "$corr")
|
|
1022
|
+
while IFS= read -r line || [ -n "$line" ]; do
|
|
1023
|
+
[ "$(status_line_verb "$line")" = blocked ] || continue
|
|
1024
|
+
for kind in missed delivery-unknown recovery-delivery; do
|
|
1025
|
+
payload=$(fm_pending_reply_escalation_payload "$rec" "$kind") || continue
|
|
1026
|
+
case "$line" in
|
|
1027
|
+
"blocked [key=$own_key]: $payload"|"blocked: $payload") found=$line; break ;;
|
|
1028
|
+
esac
|
|
1029
|
+
done
|
|
1030
|
+
done < "$status_file"
|
|
1031
|
+
printf '%s' "$found"
|
|
1032
|
+
}
|
|
1033
|
+
|
|
1034
|
+
# Close the durable status decision a previous escalation opened for <corr_id>.
|
|
1035
|
+
# Idempotent, and safe to retry until it succeeds: it appends the closing line
|
|
1036
|
+
# only while that exact keyed decision is still open in
|
|
1037
|
+
# bin/fm-classify-lib.sh's fold. Records that never escalated are left untouched.
|
|
1038
|
+
fm_pending_reply_close_escalation() { # <state-dir> <corr_id>
|
|
1039
|
+
# Serialized per correlation so a resolution and an escalation cannot interleave.
|
|
1040
|
+
# bin/fm-wake-lib.sh owns the lock primitives but assigns its own globals when
|
|
1041
|
+
# sourced, so they are declared local here: that contains them to this call
|
|
1042
|
+
# instead of leaking into every script that sources this library, without the
|
|
1043
|
+
# subshell that would make every later use of them read as a lost write.
|
|
1044
|
+
# The lock is released explicitly rather than from an EXIT trap, because a trap
|
|
1045
|
+
# in a plain function would clobber the caller's own.
|
|
1046
|
+
local state=$1 corr=$2 lock rc=0
|
|
1047
|
+
local STATE FM_WAKE_QUEUE FM_WAKE_QUEUE_LOCK
|
|
1048
|
+
STATE=$state
|
|
1049
|
+
lock="$state/.pending-reply-$corr.lock"
|
|
1050
|
+
# shellcheck source=bin/fm-wake-lib.sh
|
|
1051
|
+
. "$_FM_PENDING_REPLY_LIB_DIR/fm-wake-lib.sh"
|
|
1052
|
+
fm_lock_acquire_wait "$lock" || return 1
|
|
1053
|
+
_fm_pending_reply_close_escalation_locked "$@" || rc=$?
|
|
1054
|
+
fm_lock_release "$lock"
|
|
1055
|
+
return "$rc"
|
|
1056
|
+
}
|
|
1057
|
+
|
|
1058
|
+
_fm_pending_reply_close_escalation_locked() { # <state-dir> <corr_id>
|
|
1059
|
+
local state=$1 corr=$2 rec escalated closed parent_status escalation key note
|
|
1060
|
+
local open_line open_key open_note now close_line close_rc
|
|
1061
|
+
rec=$(fm_pending_reply_path "$state" "$corr")
|
|
1062
|
+
[ -f "$rec" ] || return 1
|
|
1063
|
+
[ "$(fm_pending_reply_get "$rec" phase)" = resolved ] || return 0
|
|
1064
|
+
escalated=$(fm_pending_reply_get "$rec" escalated_epoch)
|
|
1065
|
+
[ -n "$escalated" ] || return 0
|
|
1066
|
+
closed=$(fm_pending_reply_get "$rec" escalation_closed_epoch)
|
|
1067
|
+
[ -z "$closed" ] || return 0
|
|
1068
|
+
parent_status=$(fm_pending_reply_get "$rec" parent_status)
|
|
1069
|
+
[ -n "$parent_status" ] || return 1
|
|
1070
|
+
escalation=$(fm_pending_reply_escalation_line "$parent_status" "$rec" "$corr")
|
|
1071
|
+
if [ -n "$escalation" ]; then
|
|
1072
|
+
key=$(_fm_decision_key "$escalation") || key=''
|
|
1073
|
+
note=$(status_line_note "$escalation")
|
|
1074
|
+
while IFS= read -r open_line; do
|
|
1075
|
+
[ -n "$open_line" ] || continue
|
|
1076
|
+
open_key=${open_line%%$'\t'*}
|
|
1077
|
+
[ "$open_key" = "$key" ] || continue
|
|
1078
|
+
open_note=${open_line#*$'\t'}
|
|
1079
|
+
open_note=${open_note#*$'\t'}
|
|
1080
|
+
[ "$open_note" = "$note" ] || continue
|
|
1081
|
+
# This close is the home's own bookkeeping, written by the same resolve
|
|
1082
|
+
# or tick that already consumed the reply, so it uses the guarded
|
|
1083
|
+
# self-announced append (bin/fm-wake-lib.sh, sourced by this function's
|
|
1084
|
+
# wrappers) and does not wake the home that wrote it; the escalation
|
|
1085
|
+
# OPEN above stays a plain append because a new blocker must wake.
|
|
1086
|
+
close_line=$(printf 'resolved [key=%s]: pending-reply-resolved: task=%s pending-reply-id=%s via=%s' \
|
|
1087
|
+
"$key" "$(fm_pending_reply_get "$rec" task_id)" "$corr" \
|
|
1088
|
+
"$(fm_pending_reply_get "$rec" resolved_via)")
|
|
1089
|
+
close_rc=0
|
|
1090
|
+
fm_wake_status_append_self_announced "${parent_status%/*}" "$parent_status" "$close_line" \
|
|
1091
|
+
2>/dev/null || close_rc=$?
|
|
1092
|
+
[ "$close_rc" -ne 2 ] || return 1
|
|
1093
|
+
break
|
|
1094
|
+
done <<EOF
|
|
1095
|
+
$(status_open_decisions "$parent_status")
|
|
1096
|
+
EOF
|
|
1097
|
+
fi
|
|
1098
|
+
now=$(fm_pending_reply_now)
|
|
1099
|
+
fm_pending_reply_set "$rec" escalation_closed_epoch "$now"
|
|
1100
|
+
}
|
|
1101
|
+
|
|
1102
|
+
# Escalate once after a missed recovery report or failed delivery outcome.
|
|
1103
|
+
# Retains the durable unresolved record. Never loops.
|
|
1104
|
+
fm_pending_reply_maybe_escalate() { # <state-dir> <corr_id>
|
|
1105
|
+
# Serialized per correlation so a resolution and an escalation cannot interleave.
|
|
1106
|
+
# bin/fm-wake-lib.sh owns the lock primitives but assigns its own globals when
|
|
1107
|
+
# sourced, so they are declared local here: that contains them to this call
|
|
1108
|
+
# instead of leaking into every script that sources this library, without the
|
|
1109
|
+
# subshell that would make every later use of them read as a lost write.
|
|
1110
|
+
# The lock is released explicitly rather than from an EXIT trap, because a trap
|
|
1111
|
+
# in a plain function would clobber the caller's own.
|
|
1112
|
+
local state=$1 corr=$2 lock rc=0
|
|
1113
|
+
local STATE FM_WAKE_QUEUE FM_WAKE_QUEUE_LOCK
|
|
1114
|
+
STATE=$state
|
|
1115
|
+
lock="$state/.pending-reply-$corr.lock"
|
|
1116
|
+
# shellcheck source=bin/fm-wake-lib.sh
|
|
1117
|
+
. "$_FM_PENDING_REPLY_LIB_DIR/fm-wake-lib.sh"
|
|
1118
|
+
fm_lock_acquire_wait "$lock" || return 1
|
|
1119
|
+
_fm_pending_reply_maybe_escalate_locked "$@" || rc=$?
|
|
1120
|
+
fm_lock_release "$lock"
|
|
1121
|
+
return "$rc"
|
|
1122
|
+
}
|
|
1123
|
+
|
|
1124
|
+
_fm_pending_reply_maybe_escalate_locked() { # <state-dir> <corr_id>
|
|
1125
|
+
local state=$1 corr=$2
|
|
1126
|
+
local rec phase completed now payload parent_status line kind
|
|
1127
|
+
rec=$(fm_pending_reply_path "$state" "$corr")
|
|
1128
|
+
[ -f "$rec" ] || return 1
|
|
1129
|
+
phase=$(fm_pending_reply_get "$rec" phase)
|
|
1130
|
+
if [ "$phase" = delivery_unknown ]; then
|
|
1131
|
+
_fm_pending_reply_reconcile_delivery_locked "$state" "$corr" || true
|
|
1132
|
+
phase=$(fm_pending_reply_get "$rec" phase)
|
|
1133
|
+
[ "$phase" = delivery_unknown ] || return 0
|
|
1134
|
+
fi
|
|
1135
|
+
case "$phase" in
|
|
1136
|
+
recovery_sent)
|
|
1137
|
+
completed=$(fm_pending_reply_get "$rec" recovery_turn_completed_epoch)
|
|
1138
|
+
[ -n "$completed" ] || return 1
|
|
1139
|
+
# Same reply-channel evidence rule the recovery repost obeys: a missing
|
|
1140
|
+
# correlated report is not a missed report until the mirror caught up.
|
|
1141
|
+
fm_pending_reply_missing_report_is_evidence "$state" \
|
|
1142
|
+
"$(fm_pending_reply_get "$rec" task_id)" "$completed" || return 1
|
|
1143
|
+
;;
|
|
1144
|
+
delivery_unknown|recovery_failed|recovery_unknown) ;;
|
|
1145
|
+
*) return 1 ;;
|
|
1146
|
+
esac
|
|
1147
|
+
# Resolve wins if a late report arrived between completion and this call.
|
|
1148
|
+
if _fm_pending_reply_try_resolve_locked "$state" "$corr"; then
|
|
1149
|
+
return 0
|
|
1150
|
+
fi
|
|
1151
|
+
parent_status=$(fm_pending_reply_get "$rec" parent_status)
|
|
1152
|
+
case "$phase" in
|
|
1153
|
+
delivery_unknown) kind=delivery-unknown ;;
|
|
1154
|
+
recovery_failed|recovery_unknown) kind=recovery-delivery ;;
|
|
1155
|
+
*) kind=missed ;;
|
|
1156
|
+
esac
|
|
1157
|
+
payload=$(fm_pending_reply_escalation_payload "$rec" "$kind") || return 1
|
|
1158
|
+
[ -n "$parent_status" ] || return 1
|
|
1159
|
+
mkdir -p "$(dirname "$parent_status")" 2>/dev/null || return 1
|
|
1160
|
+
line="blocked [key=$(fm_pending_reply_escalation_key "$corr")]: $payload"
|
|
1161
|
+
if ! grep -Fqx "$line" "$parent_status" 2>/dev/null; then
|
|
1162
|
+
printf '%s\n' "$line" >> "$parent_status" 2>/dev/null || return 1
|
|
1163
|
+
fi
|
|
1164
|
+
now=$(fm_pending_reply_now)
|
|
1165
|
+
fm_pending_reply_set "$rec" escalated_epoch "$now" || return 1
|
|
1166
|
+
fm_pending_reply_set "$rec" phase escalated || return 1
|
|
1167
|
+
return 0
|
|
1168
|
+
}
|
|
1169
|
+
|
|
1170
|
+
# Detect a correlated report written under the secondmate home (wrong home)
|
|
1171
|
+
# without treating it as acknowledgement.
|
|
1172
|
+
fm_pending_reply_detect_wrong_home() { # <state-dir> <corr_id> <secondmate-home>
|
|
1173
|
+
local state=$1 corr=$2 sm_home=$3
|
|
1174
|
+
local rec delivered hits sightings snapshot previous status_file line line_no sighting_id phase changed=0
|
|
1175
|
+
rec=$(fm_pending_reply_path "$state" "$corr")
|
|
1176
|
+
[ -f "$rec" ] || return 1
|
|
1177
|
+
[ -n "$sm_home" ] && [ -d "$sm_home" ] || return 0
|
|
1178
|
+
phase=$(fm_pending_reply_get "$rec" phase)
|
|
1179
|
+
[ "$phase" != resolved ] || return 0
|
|
1180
|
+
delivered=$(fm_pending_reply_get "$rec" delivered_epoch)
|
|
1181
|
+
[ -n "$delivered" ] || return 0
|
|
1182
|
+
snapshot=$(fm_pending_reply_status_set_signature "$sm_home/state")
|
|
1183
|
+
previous=$(fm_pending_reply_get "$rec" wrong_home_scan_signature)
|
|
1184
|
+
[ "$snapshot" != "$previous" ] || return 0
|
|
1185
|
+
hits=$(fm_pending_reply_get "$rec" wrong_home_hits)
|
|
1186
|
+
case "$hits" in ''|*[!0-9]*) hits=0 ;; esac
|
|
1187
|
+
sightings=$(fm_pending_reply_get "$rec" wrong_home_sightings)
|
|
1188
|
+
for status_file in "$sm_home"/state/*.status; do
|
|
1189
|
+
[ -e "$status_file" ] || continue
|
|
1190
|
+
line_no=0
|
|
1191
|
+
while IFS= read -r line || [ -n "$line" ]; do
|
|
1192
|
+
line_no=$((line_no + 1))
|
|
1193
|
+
fm_pending_reply_line_resolves "$line" "$corr" || continue
|
|
1194
|
+
sighting_id=$(printf '%s:%s:%s:%s' "${#status_file}" "$status_file" "$line_no" "$line" \
|
|
1195
|
+
| cksum 2>/dev/null | awk '{printf "%s-%s", $1, $2}')
|
|
1196
|
+
[ -n "$sighting_id" ] || continue
|
|
1197
|
+
case ",$sightings," in
|
|
1198
|
+
*",$sighting_id,"*) continue ;;
|
|
1199
|
+
esac
|
|
1200
|
+
if [ -n "$sightings" ]; then
|
|
1201
|
+
sightings="$sightings,$sighting_id"
|
|
1202
|
+
else
|
|
1203
|
+
sightings=$sighting_id
|
|
1204
|
+
fi
|
|
1205
|
+
hits=$((hits + 1))
|
|
1206
|
+
changed=1
|
|
1207
|
+
done < "$status_file"
|
|
1208
|
+
done
|
|
1209
|
+
if [ "$changed" = 1 ]; then
|
|
1210
|
+
fm_pending_reply_set "$rec" wrong_home_sightings "$sightings" || return 1
|
|
1211
|
+
fm_pending_reply_set "$rec" wrong_home_hits "$hits" || return 1
|
|
1212
|
+
fi
|
|
1213
|
+
fm_pending_reply_set "$rec" wrong_home_scan_signature "$snapshot" || return 1
|
|
1214
|
+
return 0
|
|
1215
|
+
}
|
|
1216
|
+
|
|
1217
|
+
# One reconciliation tick for a single record: resolve, observe, recover, escalate.
|
|
1218
|
+
# busy_state is busy|idle|unknown for the secondmate endpoint.
|
|
1219
|
+
# secondmate_home may be empty when unknown.
|
|
1220
|
+
fm_pending_reply_tick_one() { # <state-dir> <corr_id> <busy_state> [secondmate-home]
|
|
1221
|
+
local state=$1 corr=$2 busy_state=$3 sm_home=${4-}
|
|
1222
|
+
local rec phase delivered
|
|
1223
|
+
rec=$(fm_pending_reply_path "$state" "$corr")
|
|
1224
|
+
[ -f "$rec" ] || return 1
|
|
1225
|
+
fm_pending_reply_reconcile_delivery "$state" "$corr" || true
|
|
1226
|
+
phase=$(fm_pending_reply_get "$rec" phase)
|
|
1227
|
+
delivered=$(fm_pending_reply_get "$rec" delivered_epoch)
|
|
1228
|
+
if [ -z "$delivered" ]; then
|
|
1229
|
+
case "$phase" in
|
|
1230
|
+
delivery_unknown) fm_pending_reply_maybe_escalate "$state" "$corr" 2>/dev/null || true ;;
|
|
1231
|
+
escalated) fm_pending_reply_try_resolve "$state" "$corr" >/dev/null 2>&1 || true ;;
|
|
1232
|
+
esac
|
|
1233
|
+
return 0
|
|
1234
|
+
fi
|
|
1235
|
+
# Correlated parent report always wins and is idempotent.
|
|
1236
|
+
if fm_pending_reply_try_resolve "$state" "$corr"; then
|
|
1237
|
+
return 0
|
|
1238
|
+
fi
|
|
1239
|
+
phase=$(fm_pending_reply_get "$rec" phase)
|
|
1240
|
+
case "$phase" in
|
|
1241
|
+
awaiting_report|recovery_sending)
|
|
1242
|
+
if [ -n "$(fm_pending_reply_get "$rec" recovery_attempted_epoch)" ]; then
|
|
1243
|
+
fm_pending_reply_reconcile_recovery "$state" "$corr" || true
|
|
1244
|
+
phase=$(fm_pending_reply_get "$rec" phase)
|
|
1245
|
+
fi
|
|
1246
|
+
;;
|
|
1247
|
+
esac
|
|
1248
|
+
case "$phase" in
|
|
1249
|
+
resolved) return 0 ;;
|
|
1250
|
+
escalated)
|
|
1251
|
+
# Unresolved durable record retained; never auto-delete.
|
|
1252
|
+
if [ -n "$sm_home" ]; then
|
|
1253
|
+
fm_pending_reply_detect_wrong_home "$state" "$corr" "$sm_home" || true
|
|
1254
|
+
fi
|
|
1255
|
+
return 0
|
|
1256
|
+
;;
|
|
1257
|
+
recovery_sending) return 0 ;;
|
|
1258
|
+
recovery_failed|recovery_unknown)
|
|
1259
|
+
fm_pending_reply_maybe_escalate "$state" "$corr" 2>/dev/null || true
|
|
1260
|
+
return 0
|
|
1261
|
+
;;
|
|
1262
|
+
esac
|
|
1263
|
+
if [ -n "$sm_home" ]; then
|
|
1264
|
+
fm_pending_reply_detect_wrong_home "$state" "$corr" "$sm_home" || true
|
|
1265
|
+
fi
|
|
1266
|
+
fm_pending_reply_observe_busy "$state" "$corr" "$busy_state" || true
|
|
1267
|
+
# Re-check resolve after observation in case a concurrent status write landed.
|
|
1268
|
+
if fm_pending_reply_try_resolve "$state" "$corr"; then
|
|
1269
|
+
return 0
|
|
1270
|
+
fi
|
|
1271
|
+
phase=$(fm_pending_reply_get "$rec" phase)
|
|
1272
|
+
if [ "$phase" = awaiting_report ]; then
|
|
1273
|
+
fm_pending_reply_send_recovery "$state" "$corr" 2>/dev/null || true
|
|
1274
|
+
fi
|
|
1275
|
+
phase=$(fm_pending_reply_get "$rec" phase)
|
|
1276
|
+
case "$phase" in
|
|
1277
|
+
recovery_sent|recovery_failed|recovery_unknown)
|
|
1278
|
+
fm_pending_reply_maybe_escalate "$state" "$corr" 2>/dev/null || true
|
|
1279
|
+
;;
|
|
1280
|
+
esac
|
|
1281
|
+
return 0
|
|
1282
|
+
}
|
|
1283
|
+
|
|
1284
|
+
# Scan every pending record for this parent state. Safe to call every poll.
|
|
1285
|
+
# Never scrapes secondmate conversation; uses only parent status, backend busy
|
|
1286
|
+
# state, and optional secondmate-home wrong-home path checks.
|
|
1287
|
+
fm_pending_reply_tick() { # <state-dir>
|
|
1288
|
+
local state=$1 dir rec corr task_id phase delivered meta backend target label busy sm_home harness remote_host
|
|
1289
|
+
local observation observation_task found i
|
|
1290
|
+
local -a observation_tasks=() observation_values=()
|
|
1291
|
+
dir=$(fm_pending_reply_dir "$state")
|
|
1292
|
+
[ -d "$dir" ] || return 0
|
|
1293
|
+
for rec in "$dir"/*; do
|
|
1294
|
+
[ -f "$rec" ] || continue
|
|
1295
|
+
case "$(basename "$rec")" in
|
|
1296
|
+
.*) continue ;;
|
|
1297
|
+
esac
|
|
1298
|
+
corr=$(fm_pending_reply_get "$rec" corr_id)
|
|
1299
|
+
[ -n "$corr" ] || corr=$(basename "$rec")
|
|
1300
|
+
task_id=$(fm_pending_reply_get "$rec" task_id)
|
|
1301
|
+
phase=$(fm_pending_reply_get "$rec" phase)
|
|
1302
|
+
if [ "$phase" = resolved ]; then
|
|
1303
|
+
# Cheap no-op unless an escalation for this record is still open; this is
|
|
1304
|
+
# the retry that makes the close converge after a transient write failure.
|
|
1305
|
+
fm_pending_reply_close_escalation "$state" "$corr" || true
|
|
1306
|
+
continue
|
|
1307
|
+
fi
|
|
1308
|
+
fm_pending_reply_reconcile_delivery "$state" "$corr" || true
|
|
1309
|
+
phase=$(fm_pending_reply_get "$rec" phase)
|
|
1310
|
+
delivered=$(fm_pending_reply_get "$rec" delivered_epoch)
|
|
1311
|
+
if [ -z "$delivered" ]; then
|
|
1312
|
+
case "$phase" in
|
|
1313
|
+
delivery_unknown|escalated)
|
|
1314
|
+
fm_pending_reply_tick_one "$state" "$corr" unknown "" || true
|
|
1315
|
+
;;
|
|
1316
|
+
esac
|
|
1317
|
+
continue
|
|
1318
|
+
fi
|
|
1319
|
+
case "$phase" in
|
|
1320
|
+
awaiting_report|recovery_sending)
|
|
1321
|
+
if [ -n "$(fm_pending_reply_get "$rec" recovery_attempted_epoch)" ]; then
|
|
1322
|
+
fm_pending_reply_reconcile_recovery "$state" "$corr" || true
|
|
1323
|
+
phase=$(fm_pending_reply_get "$rec" phase)
|
|
1324
|
+
fi
|
|
1325
|
+
;;
|
|
1326
|
+
esac
|
|
1327
|
+
meta="$state/${task_id}.meta"
|
|
1328
|
+
if [ "$phase" = escalated ]; then
|
|
1329
|
+
if fm_pending_reply_try_resolve "$state" "$corr"; then
|
|
1330
|
+
continue
|
|
1331
|
+
fi
|
|
1332
|
+
if [ -f "$meta" ]; then
|
|
1333
|
+
sm_home=$(fm_meta_get "$meta" home)
|
|
1334
|
+
if [ -n "$sm_home" ]; then
|
|
1335
|
+
fm_pending_reply_detect_wrong_home "$state" "$corr" "$sm_home" || true
|
|
1336
|
+
fi
|
|
1337
|
+
fi
|
|
1338
|
+
continue
|
|
1339
|
+
fi
|
|
1340
|
+
case "$phase" in
|
|
1341
|
+
recovery_failed|recovery_unknown)
|
|
1342
|
+
fm_pending_reply_tick_one "$state" "$corr" unknown "" || true
|
|
1343
|
+
continue
|
|
1344
|
+
;;
|
|
1345
|
+
esac
|
|
1346
|
+
case "$phase" in
|
|
1347
|
+
awaiting_report|recovery_sent) ;;
|
|
1348
|
+
*) continue ;;
|
|
1349
|
+
esac
|
|
1350
|
+
backend=tmux
|
|
1351
|
+
target=
|
|
1352
|
+
busy=unknown
|
|
1353
|
+
sm_home=
|
|
1354
|
+
harness=
|
|
1355
|
+
if [ -f "$meta" ]; then
|
|
1356
|
+
remote_host=$(fm_meta_get "$meta" remote_host)
|
|
1357
|
+
backend=$(fm_backend_of_meta "$meta")
|
|
1358
|
+
target=$(fm_backend_target_of_meta "$meta")
|
|
1359
|
+
sm_home=$(fm_meta_get "$meta" home)
|
|
1360
|
+
harness=$(fm_meta_get "$meta" harness)
|
|
1361
|
+
if [ -n "$remote_host" ]; then
|
|
1362
|
+
target="remote:$task_id"
|
|
1363
|
+
sm_home=
|
|
1364
|
+
fi
|
|
1365
|
+
if [ -n "$target" ]; then
|
|
1366
|
+
label="fm-$task_id"
|
|
1367
|
+
observation=
|
|
1368
|
+
found=0
|
|
1369
|
+
for ((i = 0; i < ${#observation_tasks[@]}; i++)); do
|
|
1370
|
+
observation_task=${observation_tasks[$i]}
|
|
1371
|
+
[ "$observation_task" = "$task_id" ] || continue
|
|
1372
|
+
observation=${observation_values[$i]}
|
|
1373
|
+
found=1
|
|
1374
|
+
break
|
|
1375
|
+
done
|
|
1376
|
+
if [ "$found" = 0 ]; then
|
|
1377
|
+
if [ -n "$remote_host" ]; then
|
|
1378
|
+
observation=$("$_FM_PENDING_REPLY_LIB_DIR/fm-on.sh" "$task_id" \
|
|
1379
|
+
fm-remote-secondmate-control.sh observe "$task_id" < /dev/null 2>/dev/null || printf 'unknown')
|
|
1380
|
+
case "$observation" in busy|idle|fallback-idle|unknown) ;; *) observation=unknown ;; esac
|
|
1381
|
+
else
|
|
1382
|
+
observation=$(fm_pending_reply_backend_observation "$backend" "$target" "$label" "$harness")
|
|
1383
|
+
fi
|
|
1384
|
+
observation_tasks+=("$task_id")
|
|
1385
|
+
observation_values+=("$observation")
|
|
1386
|
+
fi
|
|
1387
|
+
busy=$(fm_pending_reply_busy_state_from_observation "$rec" "$observation")
|
|
1388
|
+
fi
|
|
1389
|
+
fi
|
|
1390
|
+
fm_pending_reply_tick_one "$state" "$corr" "$busy" "$sm_home" || true
|
|
1391
|
+
done
|
|
1392
|
+
return 0
|
|
1393
|
+
}
|
|
1394
|
+
|
|
1395
|
+
# True when any open (non-resolved) pending reply exists for a task.
|
|
1396
|
+
fm_pending_reply_task_has_open() { # <state-dir> <task_id>
|
|
1397
|
+
local state=$1 task_id=$2 dir rec phase tid
|
|
1398
|
+
dir=$(fm_pending_reply_dir "$state")
|
|
1399
|
+
[ -d "$dir" ] || return 1
|
|
1400
|
+
for rec in "$dir"/*; do
|
|
1401
|
+
[ -f "$rec" ] || continue
|
|
1402
|
+
tid=$(fm_pending_reply_get "$rec" task_id)
|
|
1403
|
+
[ "$tid" = "$task_id" ] || continue
|
|
1404
|
+
phase=$(fm_pending_reply_get "$rec" phase)
|
|
1405
|
+
[ "$phase" != resolved ] || continue
|
|
1406
|
+
return 0
|
|
1407
|
+
done
|
|
1408
|
+
return 1
|
|
1409
|
+
}
|