@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,1770 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Shared wake classifier: the common source of truth for captain-relevant status
|
|
3
|
+
# tests, declared-external-wait vocabulary, and the working/paused absorb
|
|
4
|
+
# classification that makes no-verb signal and stale-pane wakes safe to absorb.
|
|
5
|
+
# Sourced by BOTH the always-on watcher
|
|
6
|
+
# (bin/fm-watch.sh) and the away-mode daemon (bin/fm-supervise-daemon.sh) so the
|
|
7
|
+
# overlapping triage policy lives in one place instead of two copies that can
|
|
8
|
+
# drift apart.
|
|
9
|
+
#
|
|
10
|
+
# Most functions are pure, side-effect-free reads of status files: each takes
|
|
11
|
+
# what it needs as arguments and touches no globals beyond the optional
|
|
12
|
+
# FM_CAPTAIN_RE override. Consumers layer their own dedup/marker state on top (the
|
|
13
|
+
# daemon keeps its escalation-digest seen-markers; the watcher keeps its .seen-*
|
|
14
|
+
# signatures).
|
|
15
|
+
# Status-span classification captures one file endpoint and reports every
|
|
16
|
+
# actionable event through that endpoint before the endpoint may be committed.
|
|
17
|
+
# An absent status file is a successful empty span, while an existing status
|
|
18
|
+
# object that cannot be read or identified is a classification failure with no
|
|
19
|
+
# committable endpoint.
|
|
20
|
+
# A presentation marker independently stores the last reported file signature
|
|
21
|
+
# and the last successfully classified position.
|
|
22
|
+
# Successful classification advances both facts through the captured endpoint;
|
|
23
|
+
# after a failure is reported, only the reported signature advances, so the same
|
|
24
|
+
# observed state alarms once while every unclassified byte remains for recovery.
|
|
25
|
+
# The reported signature includes path type, mode, symlink target, and observable
|
|
26
|
+
# failure kind, so a readability change is a new state that triggers another read.
|
|
27
|
+
# A missing, malformed, identity-mismatched, or past-end classified position reads
|
|
28
|
+
# from byte 0, preferring a bounded duplicate over a lost event.
|
|
29
|
+
#
|
|
30
|
+
# There are three documented exceptions. The absorb classification
|
|
31
|
+
# (crew_absorb_class and its working/paused wrappers) is NOT a pure status-file
|
|
32
|
+
# read: it reuses bin/fm-crew-state.sh, which may make a bounded no-mistakes call,
|
|
33
|
+
# to decide whether a crew that just stopped its turn or went stale is working,
|
|
34
|
+
# deliberately paused, or neither. Callers run it ONLY on no-verb signal handling
|
|
35
|
+
# and first sighting of a stale hash, never on every wake, so the per-wake triage
|
|
36
|
+
# stays cheap. status_open_decisions_incremental (see "incremental (cursor-backed)
|
|
37
|
+
# open-decisions fold" below) also writes: it persists a per-status-file byte
|
|
38
|
+
# cursor and folded open-set as a side effect, so a per-drain fleet-wide scan
|
|
39
|
+
# stays bounded by new appends instead of re-reading each task's whole lifetime
|
|
40
|
+
# log every time. crew_worktree_written_since reads the task's meta file and walks
|
|
41
|
+
# a bounded slice of its worktree instead of a status file, so callers run it only
|
|
42
|
+
# at the moment they would otherwise escalate.
|
|
43
|
+
|
|
44
|
+
# Directory of this library, used to locate the sibling fm-crew-state.sh reader.
|
|
45
|
+
# Resolved at source time from BASH_SOURCE so it works whether sourced by a
|
|
46
|
+
# bin/ script (which sets its own SCRIPT_DIR) or directly by a test.
|
|
47
|
+
_FM_CLASSIFY_LIB_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd 2>/dev/null)" || _FM_CLASSIFY_LIB_DIR="."
|
|
48
|
+
|
|
49
|
+
# The crew current-state reader used for the "provably working" decision.
|
|
50
|
+
# Overridable so tests can stub the run-step/pane verdict without a real worktree
|
|
51
|
+
# or no-mistakes install; absent, it points at the real sibling script.
|
|
52
|
+
FM_CREW_STATE_BIN="${FM_CREW_STATE_BIN:-$_FM_CLASSIFY_LIB_DIR/fm-crew-state.sh}"
|
|
53
|
+
|
|
54
|
+
# fm_run_timed, the shared hard bound the worktree write probe below puts around
|
|
55
|
+
# its one filesystem walk. bin/fm-timeout-lib.sh owns bounded execution for this
|
|
56
|
+
# repo, so nothing here re-derives the coreutils/BSD/perl selection. That library
|
|
57
|
+
# declares `set -u` for its own hygiene, which a sourced sibling must not impose on
|
|
58
|
+
# THIS library's consumers - several of them deliberately run without it - so the
|
|
59
|
+
# caller's setting is restored around the source.
|
|
60
|
+
case $- in *u*) _fm_classify_nounset=on ;; *) _fm_classify_nounset=off ;; esac
|
|
61
|
+
# shellcheck source=bin/fm-timeout-lib.sh
|
|
62
|
+
# shellcheck disable=SC1091
|
|
63
|
+
. "$_FM_CLASSIFY_LIB_DIR/fm-timeout-lib.sh"
|
|
64
|
+
[ "$_fm_classify_nounset" = on ] || set +u
|
|
65
|
+
unset _fm_classify_nounset
|
|
66
|
+
|
|
67
|
+
# Captain-relevant status verbs. A status line carrying any of these is work
|
|
68
|
+
# firstmate must see. Lines without these verbs are no-verb signals: the watcher
|
|
69
|
+
# absorbs them only with positive provably-working evidence, while the daemon uses
|
|
70
|
+
# its away-mode classification. FM_CAPTAIN_RE overrides the whole set when a home
|
|
71
|
+
# needs a custom verb vocabulary; absent, this default applies.
|
|
72
|
+
#
|
|
73
|
+
# Free-text tokens (PR ready, checks green, ready in branch, merged) exist only for
|
|
74
|
+
# legacy lines that lack a standard terminal verb. status_is_captain_relevant is
|
|
75
|
+
# verb-aware: a nonterminal working: or paused: line never becomes captain-relevant
|
|
76
|
+
# merely because its prose contains one of those tokens (for example
|
|
77
|
+
# "working: rebased onto merged #76").
|
|
78
|
+
FM_CLASSIFY_CAPTAIN_RE_DEFAULT='done:|needs-decision:|blocked:|failed:|PR ready|checks green|ready in branch|merged'
|
|
79
|
+
|
|
80
|
+
# The deliberate-external-wait verb. A crew (or firstmate steering it) appends
|
|
81
|
+
# paused: <reason>
|
|
82
|
+
# to declare it is intentionally idling on a KNOWN external dependency - an
|
|
83
|
+
# upstream release, a vendor rate-limit reset, a scheduled window. Unlike
|
|
84
|
+
# `blocked:` (stuck, firstmate must help) an idle `paused:` pane is EXPECTED, so
|
|
85
|
+
# the stale path absorbs it instead of escalating a possible wedge. It is
|
|
86
|
+
# deliberately NOT in the captain-relevant set above: a pause is a "stop
|
|
87
|
+
# wedge-nagging this idle pane" signal, not work to keep surfacing. This constant
|
|
88
|
+
# is the ONE definition of the verb; both the watcher and the daemon read it here
|
|
89
|
+
# (status_is_paused) rather than hardcoding the literal, so the vocabulary cannot
|
|
90
|
+
# drift between the two consumers. FM_CLASSIFY_PAUSED_VERB overrides it.
|
|
91
|
+
FM_CLASSIFY_PAUSED_VERB_DEFAULT='paused'
|
|
92
|
+
|
|
93
|
+
# Bounded re-surface cadence for a declared pause or a verified captain hold.
|
|
94
|
+
# Far longer than the wedge threshold (FM_STALE_ESCALATE_SECS, default 240s), it
|
|
95
|
+
# avoids nagging a deliberate wait while ensuring a forgotten hold cannot rot
|
|
96
|
+
# invisibly - it re-surfaces once for a recheck every window. One hour by default;
|
|
97
|
+
# both consumers read FM_PAUSE_RESURFACE_SECS with this default so the cadence has
|
|
98
|
+
# one owner.
|
|
99
|
+
# shellcheck disable=SC2034 # Read by the watcher and daemon (fm-watch.sh, fm-supervise-daemon.sh), not this lib.
|
|
100
|
+
FM_PAUSE_RESURFACE_SECS_DEFAULT=3600
|
|
101
|
+
|
|
102
|
+
# The resolution verb and durable-backlog-transfer verb that CLOSE a keyed
|
|
103
|
+
# status decision opened by needs-decision or blocked. See status_open_decisions
|
|
104
|
+
# below for the status-fold contract. The transfer verb is written only after
|
|
105
|
+
# fm-captain-hold.sh has verified the corresponding captain-held backlog item.
|
|
106
|
+
FM_CLASSIFY_RESOLVE_VERB_DEFAULT='resolved'
|
|
107
|
+
FM_CLASSIFY_CAPTAIN_HELD_VERB_DEFAULT='captain-held'
|
|
108
|
+
|
|
109
|
+
# Return the last non-blank line of a status file (empty if missing/blank).
|
|
110
|
+
last_status_line() {
|
|
111
|
+
local f=$1
|
|
112
|
+
[ -e "$f" ] || return 0
|
|
113
|
+
grep -v '^[[:space:]]*$' "$f" 2>/dev/null | tail -1
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
# 0 if the given (last) status line's leading verb is a real terminal captain verb
|
|
117
|
+
# (done, needs-decision, blocked, failed). Free-text tokens alone never count here;
|
|
118
|
+
# callers that need legacy free-text matching use status_is_captain_relevant.
|
|
119
|
+
status_is_terminal_verb() {
|
|
120
|
+
local line=$1 verb
|
|
121
|
+
[ -n "$line" ] || return 1
|
|
122
|
+
verb=$(status_line_verb "$line")
|
|
123
|
+
case "$verb" in
|
|
124
|
+
done|needs-decision|blocked|failed) return 0 ;;
|
|
125
|
+
*) return 1 ;;
|
|
126
|
+
esac
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
# 0 if the given (last) status line matches a captain-relevant verb.
|
|
130
|
+
# Verb-aware by default: terminal verbs always match; nonterminal progress verbs
|
|
131
|
+
# (working, resolved, captain-held) and paused never match from free-text prose;
|
|
132
|
+
# only lines without those leading verbs may still match free-text tokens for
|
|
133
|
+
# legacy bare lines such as "merged" or "PR ready".
|
|
134
|
+
status_is_captain_relevant() {
|
|
135
|
+
local line=$1 verb
|
|
136
|
+
[ -n "$line" ] || return 1
|
|
137
|
+
status_is_paused "$line" && return 1
|
|
138
|
+
verb=$(status_line_verb "$line")
|
|
139
|
+
case "$verb" in
|
|
140
|
+
working|resolved|captain-held|"${FM_CLASSIFY_PAUSED_VERB:-$FM_CLASSIFY_PAUSED_VERB_DEFAULT}")
|
|
141
|
+
return 1
|
|
142
|
+
;;
|
|
143
|
+
esac
|
|
144
|
+
if [ -z "${FM_CAPTAIN_RE+x}" ]; then
|
|
145
|
+
case "$verb" in
|
|
146
|
+
done|needs-decision|blocked|failed) return 0 ;;
|
|
147
|
+
esac
|
|
148
|
+
fi
|
|
149
|
+
printf '%s' "$line" | grep -qiE "${FM_CAPTAIN_RE:-$FM_CLASSIFY_CAPTAIN_RE_DEFAULT}"
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
# 0 if a status line's leading verb is the pause verb (paused: <reason>). A pure
|
|
153
|
+
# read of the line itself, so the daemon's classify_stale can reuse the last line
|
|
154
|
+
# it already read without a fm-crew-state.sh call. Matches only the verb before the
|
|
155
|
+
# first colon, so a reason mentioning "paused" elsewhere does not false-match.
|
|
156
|
+
status_is_paused() { # <status-line>
|
|
157
|
+
local line=$1 verb
|
|
158
|
+
[ -n "$line" ] || return 1
|
|
159
|
+
verb=$(status_line_verb "$line")
|
|
160
|
+
[ "$verb" = "${FM_CLASSIFY_PAUSED_VERB:-$FM_CLASSIFY_PAUSED_VERB_DEFAULT}" ]
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
# 0 if a status line's leading verb is the verified captain-held transfer verb.
|
|
164
|
+
# The same pure verb read as status_is_paused, and the discriminator a supervisor
|
|
165
|
+
# needs once a declared wait has already been recognized: the two declarations get
|
|
166
|
+
# the same bounded cadence, but they block on DIFFERENT humans, so a recheck that
|
|
167
|
+
# names an external dependency for a hold points the captain away from the fact
|
|
168
|
+
# that they are the one who can clear it.
|
|
169
|
+
status_is_captain_held() { # <status-line>
|
|
170
|
+
local line=$1 verb
|
|
171
|
+
[ -n "$line" ] || return 1
|
|
172
|
+
verb=$(status_line_verb "$line")
|
|
173
|
+
[ "$verb" = "${FM_CLASSIFY_CAPTAIN_HELD_VERB:-$FM_CLASSIFY_CAPTAIN_HELD_VERB_DEFAULT}" ]
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
# 0 if a status line declares either an external-wait pause or a verified
|
|
177
|
+
# captain-held transfer.
|
|
178
|
+
# Both declarations can intentionally leave a crew's endpoint idle, so both
|
|
179
|
+
# supervisors give them one cadence: the away-mode daemon defers the wedge and
|
|
180
|
+
# ages a pause marker instead, and the watcher applies its bounded pause cadence
|
|
181
|
+
# once pause_state_class has admitted the wait (fm-watch.sh owns which liveness
|
|
182
|
+
# evidence each kind of crew must supply for that).
|
|
183
|
+
status_is_paused_or_captain_held() { # <status-line>
|
|
184
|
+
local line=$1
|
|
185
|
+
status_is_paused "$line" || status_is_captain_held "$line"
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
# --- durable keyed decisions ------------------------------------------------
|
|
189
|
+
#
|
|
190
|
+
# The status stream is an append-only EVENT log. Reading it last-event-wins
|
|
191
|
+
# (last_status_line above) cannot represent "an earlier decision is still open
|
|
192
|
+
# after a later, unrelated event": a subsequent done/paused/working line silently
|
|
193
|
+
# masks a still-open needs-decision. status_open_decisions is the ONE authoritative
|
|
194
|
+
# statement of the status-fold contract that fixes this - a needs-decision/blocked
|
|
195
|
+
# line OPENS a keyed decision, and only an explicit resolution or a verified
|
|
196
|
+
# captain-held backlog transfer referencing that key CLOSES it; a later unrelated
|
|
197
|
+
# terminal line never clears an open captain decision.
|
|
198
|
+
# Who WRITES the closing line is owned elsewhere: the answering firstmate closes
|
|
199
|
+
# at answer time through fm-send's --resolve-key (bin/fm-send.sh header), and a
|
|
200
|
+
# worker self-closes only a blocker that cleared without an answer (bin/fm-brief.sh
|
|
201
|
+
# rule 6), so closure never depends on a busy worker's discipline.
|
|
202
|
+
#
|
|
203
|
+
# Decision key grammar (backward-compatible with the existing "<verb>: <note>"
|
|
204
|
+
# format): an OPTIONAL "[key=<slug>]" token names the decision. Its documented
|
|
205
|
+
# position sits between the verb and the colon, and a complete token at the
|
|
206
|
+
# head of the note is accepted as an EQUIVALENT position, because that
|
|
207
|
+
# misplaced-colon shape is common real worker output whose stated key must
|
|
208
|
+
# never silently collapse into the shared "default" bucket (issue #2109):
|
|
209
|
+
# needs-decision [key=api-shape]: <summary>
|
|
210
|
+
# needs-decision: [key=api-shape] <summary>
|
|
211
|
+
# resolved [key=api-shape]: <how it was decided>
|
|
212
|
+
# Both positions state the same key and yield the same note (a consumed
|
|
213
|
+
# note-head token is key metadata, stripped from the note); when both positions
|
|
214
|
+
# carry a token, the documented before-colon one wins and the note-head token
|
|
215
|
+
# stays note text. A token deeper inside the note is prose, never a stated key,
|
|
216
|
+
# so a summary merely MENTIONING "[key=x]" cannot open or close that decision.
|
|
217
|
+
# A line with no token in either position uses the key "default", preserving
|
|
218
|
+
# the historical one-open-decision-per-task behavior (a bare "resolved:" closes
|
|
219
|
+
# "default"). A stated key whose slug fails the charset below is rejected (the
|
|
220
|
+
# folds skip the line), never rewritten to "default".
|
|
221
|
+
# The parsers are pure reads of a single line. Status metadata may contain any
|
|
222
|
+
# number of "[name=value]" tags before the colon, in any order, so verb parsing
|
|
223
|
+
# ends at the first tag rather than special-casing "[key=...]".
|
|
224
|
+
#
|
|
225
|
+
# Correlation tokens. That bracket rule already covers every BRACKETED tag,
|
|
226
|
+
# including the "[corr=<16 hex>]" form bin/fm-secondmate-report.sh writes. It
|
|
227
|
+
# does not cover the UNBRACKETED token that bin/fm-pending-reply-lib.sh writes
|
|
228
|
+
# (fm_pending_reply_corr_token), which a secondmate answering a marked request
|
|
229
|
+
# echoes on its parent status line ahead of the key tag (bin/fm-brief.sh), so a
|
|
230
|
+
# real transition routinely arrives as
|
|
231
|
+
# needs-decision corr=<16 hex> [key=texte-du-mur]: <summary>
|
|
232
|
+
# resolved corr=<16 hex> [key=texte-du-mur]: <how it was decided>
|
|
233
|
+
# and a recovery turn can leave two such tokens on one line. All of those must
|
|
234
|
+
# read as the bare verb, in BOTH directions: a verb parse that keeps the token
|
|
235
|
+
# glued on matches no arm of _fm_decision_fold_line, so the opener never opens
|
|
236
|
+
# and the closer never closes, and a captain decision goes silently missing.
|
|
237
|
+
# Recognition starts only AFTER the retained leading verb: a token-first line
|
|
238
|
+
# keeps that token, so its following word cannot impersonate a transition and
|
|
239
|
+
# close a decision the captain is owed.
|
|
240
|
+
#
|
|
241
|
+
# The token grammar is OWNED by bin/fm-pending-reply-lib.sh
|
|
242
|
+
# (fm_pending_reply_corr_token, FM_PENDING_REPLY_CORR_RE). That library sources
|
|
243
|
+
# this one, so it cannot be sourced back here; the pattern below is a deliberate
|
|
244
|
+
# second statement of the SHAPE alone, and tests/fm-classify-corr-token.test.sh
|
|
245
|
+
# pins the two together through the real writers so they cannot drift.
|
|
246
|
+
#
|
|
247
|
+
# Recognition is deliberately narrow: EXACTLY the token that writer emits, whole
|
|
248
|
+
# word, and nothing else. An arbitrary "<name>=<value>" token is NOT skipped.
|
|
249
|
+
# Skipping unknown tokens would be the permissive road - it would let any
|
|
250
|
+
# free-text word carrying an equals sign ("resolved x=1 [key=k]: ...") reduce to
|
|
251
|
+
# a bare verb and impersonate a transition, which is the takeover the strict
|
|
252
|
+
# parse and _fm_decision_key_transition_allowed exist to prevent. Recognising
|
|
253
|
+
# only what a firstmate library actually writes costs one more line here each
|
|
254
|
+
# time a real new token shape is introduced, and that is the intended trade: a
|
|
255
|
+
# new shape is a deliberate, reviewed edit rather than a silent widening. A line
|
|
256
|
+
# whose token is malformed, wrong-length, or merely mentioned in prose keeps its
|
|
257
|
+
# extra words and therefore stays a non-transition, exactly as before.
|
|
258
|
+
#
|
|
259
|
+
# The 16 hex classes are written out literally rather than built from a
|
|
260
|
+
# variable, the same way bin/fm-secondmate-report.sh validates the id it is
|
|
261
|
+
# handed: a variable holding a glob is only re-read as a pattern under some
|
|
262
|
+
# shells' expansion rules, and a safety parse must not turn on that.
|
|
263
|
+
#
|
|
264
|
+
# 0 if <word> is, in whole, an unbracketed correlation token this fleet's own
|
|
265
|
+
# tooling writes. The bracketed form never reaches here: the tag rule above has
|
|
266
|
+
# already ended the verb parse at its opening bracket.
|
|
267
|
+
_fm_classify_is_corr_token() { # <word>
|
|
268
|
+
case "$1" in
|
|
269
|
+
corr=[0-9A-Fa-f][0-9A-Fa-f][0-9A-Fa-f][0-9A-Fa-f][0-9A-Fa-f][0-9A-Fa-f][0-9A-Fa-f][0-9A-Fa-f][0-9A-Fa-f][0-9A-Fa-f][0-9A-Fa-f][0-9A-Fa-f][0-9A-Fa-f][0-9A-Fa-f][0-9A-Fa-f][0-9A-Fa-f])
|
|
270
|
+
return 0
|
|
271
|
+
;;
|
|
272
|
+
esac
|
|
273
|
+
return 1
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
status_line_verb() { # <status-line> -> leading verb word
|
|
277
|
+
local v=${1%%:*} out='' word
|
|
278
|
+
v=${v%%\[*}
|
|
279
|
+
v=${v#"${v%%[![:space:]]*}"}
|
|
280
|
+
v=${v%"${v##*[![:space:]]}"}
|
|
281
|
+
# Fast path, and the whole no-regression guarantee: a prefix that cannot
|
|
282
|
+
# contain a correlation token is returned byte-for-byte as before, so every
|
|
283
|
+
# line without one keeps its exact historical verb, spacing included.
|
|
284
|
+
case "$v" in
|
|
285
|
+
*corr=*) ;;
|
|
286
|
+
*) printf '%s' "$v"; return 0 ;;
|
|
287
|
+
esac
|
|
288
|
+
# Retain the first word, then drop only recognised tokens from the remaining
|
|
289
|
+
# whole words. Anything unrecognised stays, so prose still matches no verb.
|
|
290
|
+
word=${v%%[[:space:]]*}
|
|
291
|
+
out=$word
|
|
292
|
+
v=${v#"$word"}
|
|
293
|
+
v=${v#"${v%%[![:space:]]*}"}
|
|
294
|
+
while [ -n "$v" ]; do
|
|
295
|
+
word=${v%%[[:space:]]*}
|
|
296
|
+
v=${v#"$word"}
|
|
297
|
+
v=${v#"${v%%[![:space:]]*}"}
|
|
298
|
+
_fm_classify_is_corr_token "$word" && continue
|
|
299
|
+
out="$out $word"
|
|
300
|
+
done
|
|
301
|
+
printf '%s' "$out"
|
|
302
|
+
}
|
|
303
|
+
# 0 when a complete "[key=...]" token sits in the documented position before
|
|
304
|
+
# the line's first colon (or anywhere on a line that has no colon at all).
|
|
305
|
+
_fm_key_before_colon() { # <status-line>
|
|
306
|
+
case "${1%%:*}" in
|
|
307
|
+
*\[key=*\]*) return 0 ;;
|
|
308
|
+
*) return 1 ;;
|
|
309
|
+
esac
|
|
310
|
+
}
|
|
311
|
+
# Raw slug of a complete "[key=<slug>]" token at the head of the note (the
|
|
312
|
+
# first thing after the line's first colon, ignoring whitespace). Fails when
|
|
313
|
+
# the line has no colon or no complete token there; slug charset validity is
|
|
314
|
+
# the caller's check via _fm_decision_slug_ok, exactly as for the before-colon
|
|
315
|
+
# position.
|
|
316
|
+
_fm_key_at_note_head() { # <status-line> -> raw slug
|
|
317
|
+
local rest
|
|
318
|
+
case "$1" in
|
|
319
|
+
*:*) rest=${1#*:} ;;
|
|
320
|
+
*) return 1 ;;
|
|
321
|
+
esac
|
|
322
|
+
rest=${rest#"${rest%%[![:space:]]*}"}
|
|
323
|
+
case "$rest" in
|
|
324
|
+
\[key=*\]*) rest=${rest#\[key=}; printf '%s' "${rest%%\]*}" ;;
|
|
325
|
+
*) return 1 ;;
|
|
326
|
+
esac
|
|
327
|
+
}
|
|
328
|
+
# 0 when a stated key slug is well-formed: nonempty, A-Za-z0-9._- only.
|
|
329
|
+
_fm_decision_slug_ok() { # <slug>
|
|
330
|
+
case "$1" in
|
|
331
|
+
''|*[!A-Za-z0-9._-]*) return 1 ;;
|
|
332
|
+
*) return 0 ;;
|
|
333
|
+
esac
|
|
334
|
+
}
|
|
335
|
+
status_line_note() { # <status-line> -> text after the first colon, trimmed
|
|
336
|
+
local n k
|
|
337
|
+
case "$1" in
|
|
338
|
+
*:*) n=${1#*:}; n=${n#"${n%%[![:space:]]*}"} ;;
|
|
339
|
+
*) printf '%s' "$1"; return 0 ;;
|
|
340
|
+
esac
|
|
341
|
+
# A note-head token that states this line's key (no before-colon token, valid
|
|
342
|
+
# slug) is key metadata, not note text: strip it so both stated-key positions
|
|
343
|
+
# yield the same note.
|
|
344
|
+
if ! _fm_key_before_colon "$1" && k=$(_fm_key_at_note_head "$1") \
|
|
345
|
+
&& _fm_decision_slug_ok "$k"; then
|
|
346
|
+
n=${n#"[key=$k]"}
|
|
347
|
+
n=${n#"${n%%[![:space:]]*}"}
|
|
348
|
+
fi
|
|
349
|
+
printf '%s' "$n"
|
|
350
|
+
}
|
|
351
|
+
_fm_decision_key() { # <status-line> -> key slug, or "default" when no token
|
|
352
|
+
local k
|
|
353
|
+
if _fm_key_before_colon "$1"; then
|
|
354
|
+
k=${1%%:*}
|
|
355
|
+
k=${k#*\[key=}
|
|
356
|
+
k=${k%%\]*}
|
|
357
|
+
else
|
|
358
|
+
k=$(_fm_key_at_note_head "$1") || { printf 'default'; return 0; }
|
|
359
|
+
fi
|
|
360
|
+
_fm_decision_slug_ok "$k" || return 1
|
|
361
|
+
printf '%s' "$k"
|
|
362
|
+
}
|
|
363
|
+
# Drop the record for <key> from a newline-terminated "<key>\t<verb>\t<note>" set.
|
|
364
|
+
# Portable (no associative arrays) so the fold runs on bash 3.2 as well as 4+.
|
|
365
|
+
_fm_decision_drop() { # <open-set> <key>
|
|
366
|
+
local set=$1 key=$2 line out=''
|
|
367
|
+
while IFS= read -r line; do
|
|
368
|
+
[ -n "$line" ] || continue
|
|
369
|
+
case "$line" in
|
|
370
|
+
"$key"$'\t'*) : ;;
|
|
371
|
+
*) out="${out}${line}"$'\n' ;;
|
|
372
|
+
esac
|
|
373
|
+
done <<EOF
|
|
374
|
+
$set
|
|
375
|
+
EOF
|
|
376
|
+
printf '%s' "$out"
|
|
377
|
+
}
|
|
378
|
+
# Fold ONE status line into an existing "<key>\t<verb>\t<note>\n"-per-line open
|
|
379
|
+
# set, applying the same needs-decision/blocked-opens, resolved/captain-held-closes
|
|
380
|
+
# rule status_open_decisions documents above. Pure text transform, no file I/O.
|
|
381
|
+
# This is the ONE place the per-line open/resolved rule is written; both the
|
|
382
|
+
# whole-file fold (status_open_decisions) and the incremental cursor-backed fold
|
|
383
|
+
# (status_open_decisions_incremental) below call this instead of re-deriving the
|
|
384
|
+
# rule, so the two consumption strategies can never drift apart on semantics.
|
|
385
|
+
# Reserved decision-key namespaces, and the rule that makes them mean something.
|
|
386
|
+
#
|
|
387
|
+
# A key like `pending-reply-<id>` names a decision that one library raises and is
|
|
388
|
+
# the only thing that ever closes it. Every writer reaches this same stream: a
|
|
389
|
+
# local mate appends straight into it, and a remote mate's lines are mirrored
|
|
390
|
+
# into it verbatim. So without a rule here, any writer could claim a reserved
|
|
391
|
+
# key with an unrelated note, take the key over in this fold, and permanently
|
|
392
|
+
# block the owner's close - leaving a decision nothing will ever resolve - or
|
|
393
|
+
# clear the owner's decision with a bare resolution.
|
|
394
|
+
#
|
|
395
|
+
# The rule is deliberately generic, so this fold needs no knowledge of any
|
|
396
|
+
# particular owner: a reserved key may only be opened or closed by a line whose
|
|
397
|
+
# note speaks that namespace's own vocabulary, which its owner states by
|
|
398
|
+
# beginning the note with a `<namespace>...:` token. A line failing that is not a
|
|
399
|
+
# decision transition at all here and is folded as ordinary status. This is a
|
|
400
|
+
# consumer-side rule on purpose - it protects local and remote writers
|
|
401
|
+
# identically, and it can never fail a whole delta or wedge a stream the way a
|
|
402
|
+
# writer-side rejection would.
|
|
403
|
+
FM_CLASSIFY_RESERVED_KEY_PREFIXES_DEFAULT='pending-reply-'
|
|
404
|
+
|
|
405
|
+
# 0 when <key> is not reserved, or is reserved and <note> speaks its vocabulary.
|
|
406
|
+
_fm_decision_key_transition_allowed() { # <key> <note>
|
|
407
|
+
local key=$1 note=$2 prefix
|
|
408
|
+
for prefix in ${FM_CLASSIFY_RESERVED_KEY_PREFIXES:-$FM_CLASSIFY_RESERVED_KEY_PREFIXES_DEFAULT}; do
|
|
409
|
+
case "$key" in
|
|
410
|
+
"$prefix"*)
|
|
411
|
+
case "$note" in
|
|
412
|
+
"$prefix"*:*) return 0 ;;
|
|
413
|
+
*) return 1 ;;
|
|
414
|
+
esac
|
|
415
|
+
;;
|
|
416
|
+
esac
|
|
417
|
+
done
|
|
418
|
+
return 0
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
_fm_decision_fold_line() { # <open-set> <status-line> <resolve-verb> <held-verb>
|
|
422
|
+
local open=$1 line=$2 resolve=$3 held=$4 verb key note
|
|
423
|
+
# Blank-line guard. A `case` glob answers "does this line hold any non-space
|
|
424
|
+
# character" in one pattern match; the equivalent ${line//[[:space:]]/} costs
|
|
425
|
+
# tens of milliseconds per line under bash 3.2's global bracket-class
|
|
426
|
+
# substitution, which is the whole per-line cost of both folds on a status log
|
|
427
|
+
# of ordinary width. Same verdict, bounded cost.
|
|
428
|
+
case "$line" in
|
|
429
|
+
*[![:space:]]*) ;;
|
|
430
|
+
*) printf '%s' "$open"; return 0 ;;
|
|
431
|
+
esac
|
|
432
|
+
verb=$(status_line_verb "$line")
|
|
433
|
+
key=$(_fm_decision_key "$line") || { printf '%s' "$open"; return 0; }
|
|
434
|
+
_fm_decision_key_transition_allowed "$key" "$(status_line_note "$line")" \
|
|
435
|
+
|| { printf '%s' "$open"; return 0; }
|
|
436
|
+
case "$verb" in
|
|
437
|
+
needs-decision|blocked)
|
|
438
|
+
note=$(status_line_note "$line")
|
|
439
|
+
open=$(_fm_decision_drop "$open" "$key")
|
|
440
|
+
[ -n "$open" ] && open="${open}"$'\n'
|
|
441
|
+
open="${open}${key}"$'\t'"${verb}"$'\t'"${note}"$'\n'
|
|
442
|
+
;;
|
|
443
|
+
"$resolve"|"$held")
|
|
444
|
+
open=$(_fm_decision_drop "$open" "$key")
|
|
445
|
+
[ -n "$open" ] && open="${open}"$'\n'
|
|
446
|
+
;;
|
|
447
|
+
esac
|
|
448
|
+
printf '%s' "$open"
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
# Fold the WHOLE status stream into the set of decisions still open. Prints one
|
|
452
|
+
# TAB-separated "<key>\t<verb>\t<summary>" line per still-open decision, in
|
|
453
|
+
# most-recently-opened-last order; prints nothing when none are open. Pure read of
|
|
454
|
+
# the file, no globals beyond the optional FM_CLASSIFY_RESOLVE_VERB override. This
|
|
455
|
+
# is the durable open-set the fleet snapshot and any point-in-time consumer must use
|
|
456
|
+
# instead of trusting the last status line.
|
|
457
|
+
# The scan_open_decisions wrapper below enumerates a whole directory rather than
|
|
458
|
+
# a single caller-chosen path, so a status file that is itself a symlink (e.g.
|
|
459
|
+
# escaping the state directory) is rejected outright with a plain [ -L ] check
|
|
460
|
+
# before any read - a cheap builtin, unlike fm_wake_latest_event's O_NOFOLLOW
|
|
461
|
+
# subprocess read, which exists for that function's much narrower payload-driven
|
|
462
|
+
# path resolution rather than this directory-local glob.
|
|
463
|
+
status_open_decisions() { # <status-file>
|
|
464
|
+
local f=$1 line resolve held open=''
|
|
465
|
+
[ -f "$f" ] && [ -r "$f" ] && [ ! -L "$f" ] || return 0
|
|
466
|
+
resolve=${FM_CLASSIFY_RESOLVE_VERB:-$FM_CLASSIFY_RESOLVE_VERB_DEFAULT}
|
|
467
|
+
held=${FM_CLASSIFY_CAPTAIN_HELD_VERB:-$FM_CLASSIFY_CAPTAIN_HELD_VERB_DEFAULT}
|
|
468
|
+
while IFS= read -r line || [ -n "$line" ]; do
|
|
469
|
+
open=$(_fm_decision_fold_line "$open" "$line" "$resolve" "$held")
|
|
470
|
+
done < "$f"
|
|
471
|
+
printf '%s' "$open"
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
# 0 when <key> has a record in a folded "<key>\t<verb>\t<note>" open set.
|
|
475
|
+
_fm_open_set_has() { # <open-set> <key>
|
|
476
|
+
case "$1" in
|
|
477
|
+
"$2"$'\t'*|*$'\n'"$2"$'\t'*) return 0 ;;
|
|
478
|
+
*) return 1 ;;
|
|
479
|
+
esac
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
# The verb stored for <key> in a folded open set (empty when it has no record).
|
|
483
|
+
_fm_open_set_verb() { # <open-set> <key>
|
|
484
|
+
local line
|
|
485
|
+
while IFS= read -r line; do
|
|
486
|
+
case "$line" in
|
|
487
|
+
"$2"$'\t'*) line=${line#*$'\t'}; printf '%s' "${line%%$'\t'*}"; return 0 ;;
|
|
488
|
+
esac
|
|
489
|
+
done <<EOF
|
|
490
|
+
$1
|
|
491
|
+
EOF
|
|
492
|
+
return 0
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
# The verb that last moved <key> in a status stream, which is what tells a
|
|
496
|
+
# consumer HOW the status side currently reads that key. Prints the opening verb
|
|
497
|
+
# (needs-decision or blocked) while the key is still open, the closing verb
|
|
498
|
+
# (resolved, or the captain-held durable-transfer verb) once it is closed, and
|
|
499
|
+
# nothing at all when no line in the stream ever stated a transition for it.
|
|
500
|
+
#
|
|
501
|
+
# The distinction between the two closing verbs is the whole point: a
|
|
502
|
+
# `captain-held` close is the VERIFIED handoff to a durable captain-held task
|
|
503
|
+
# (fm-captain-hold.sh complete writes it only after verifying that task), so the
|
|
504
|
+
# structured row staying open afterwards is correct. A `resolved` close claims
|
|
505
|
+
# the question is settled outright, so a structured row still open behind it is a
|
|
506
|
+
# contradiction between the two records - see fm-captain-hold.sh's `diverged`.
|
|
507
|
+
#
|
|
508
|
+
# Semantics are not re-derived here: every line goes through the same
|
|
509
|
+
# _fm_decision_fold_line rule the two folds use, and the reported verb is read
|
|
510
|
+
# off the transitions that rule produces. Only lines whose parsed key equals the
|
|
511
|
+
# requested one can move that key, so a caller-supplied key other than "default"
|
|
512
|
+
# lets the scan pre-filter the stream to lines carrying its token and stay cheap
|
|
513
|
+
# on a long log.
|
|
514
|
+
status_key_closing_verb() { # <status-file> <key>
|
|
515
|
+
local f=$1 want=$2 line resolve held open='' was verb='' stream
|
|
516
|
+
[ -f "$f" ] && [ -r "$f" ] && [ ! -L "$f" ] || return 0
|
|
517
|
+
[ -n "$want" ] || return 0
|
|
518
|
+
resolve=${FM_CLASSIFY_RESOLVE_VERB:-$FM_CLASSIFY_RESOLVE_VERB_DEFAULT}
|
|
519
|
+
held=${FM_CLASSIFY_CAPTAIN_HELD_VERB:-$FM_CLASSIFY_CAPTAIN_HELD_VERB_DEFAULT}
|
|
520
|
+
if [ "$want" = default ]; then
|
|
521
|
+
stream=$(cat "$f") || return 0
|
|
522
|
+
else
|
|
523
|
+
stream=$(grep -F "[key=$want]" "$f") || stream=''
|
|
524
|
+
fi
|
|
525
|
+
[ -n "$stream" ] || return 0
|
|
526
|
+
while IFS= read -r line || [ -n "$line" ]; do
|
|
527
|
+
was=0
|
|
528
|
+
_fm_open_set_has "$open" "$want" && was=1
|
|
529
|
+
open=$(_fm_decision_fold_line "$open" "$line" "$resolve" "$held")
|
|
530
|
+
if [ "$was" = 1 ] && ! _fm_open_set_has "$open" "$want"; then
|
|
531
|
+
verb=$(status_line_verb "$line")
|
|
532
|
+
fi
|
|
533
|
+
done <<EOF
|
|
534
|
+
$stream
|
|
535
|
+
EOF
|
|
536
|
+
if _fm_open_set_has "$open" "$want"; then
|
|
537
|
+
_fm_open_set_verb "$open" "$want"
|
|
538
|
+
return 0
|
|
539
|
+
fi
|
|
540
|
+
printf '%s' "$verb"
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
# Fleet-wide wrapper around status_open_decisions: scans every task's status
|
|
544
|
+
# log under <state> and prefixes each still-open decision with its owning task
|
|
545
|
+
# id, so a per-wake or per-session surface can print the consolidated open set
|
|
546
|
+
# without re-walking the fold itself. A thin directory scan only - the fold
|
|
547
|
+
# above remains the ONE place the open/resolved semantics are decided. Prints
|
|
548
|
+
# one "<task>\t<key>\t<verb>\t<note>" line per open decision, in glob (task id)
|
|
549
|
+
# order; prints nothing when none are open.
|
|
550
|
+
scan_open_decisions() { # <state>
|
|
551
|
+
local state=$1 f task open line
|
|
552
|
+
for f in "$state"/*.status; do
|
|
553
|
+
[ -e "$f" ] || continue
|
|
554
|
+
task=$(basename "$f"); task="${task%.status}"
|
|
555
|
+
open=$(status_open_decisions "$f") || continue
|
|
556
|
+
[ -n "$open" ] || continue
|
|
557
|
+
while IFS= read -r line; do
|
|
558
|
+
[ -n "$line" ] || continue
|
|
559
|
+
printf '%s\t%s\n' "$task" "$line"
|
|
560
|
+
done <<EOF
|
|
561
|
+
$open
|
|
562
|
+
EOF
|
|
563
|
+
done
|
|
564
|
+
return 0
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
# --- incremental (cursor-backed) open-decisions fold ------------------------
|
|
568
|
+
#
|
|
569
|
+
# status_open_decisions above re-reads and re-folds a status file's ENTIRE
|
|
570
|
+
# lifetime on every call, so its cost grows with total log size. A per-drain
|
|
571
|
+
# fleet-wide scan using that whole-file function would pay that cost for every
|
|
572
|
+
# task on every wake, which grows unbounded as tasks run longer and accumulate
|
|
573
|
+
# status history. status_open_decisions_incremental and scan_open_decisions_incremental
|
|
574
|
+
# below are the bounded-cost siblings used for that per-drain path: each call
|
|
575
|
+
# reads only the bytes appended to a status file since its own last call (a
|
|
576
|
+
# persisted per-file byte cursor) and folds just those new lines into a
|
|
577
|
+
# persisted running open-set, via the exact same _fm_decision_fold_line rule
|
|
578
|
+
# status_open_decisions uses - so the two strategies can never disagree on what
|
|
579
|
+
# is open. Cost is bounded by NEW appends since the last drain, not by the
|
|
580
|
+
# status file's total lifetime size.
|
|
581
|
+
#
|
|
582
|
+
# Correctness invariant (unchanged from the whole-file fold): an open decision
|
|
583
|
+
# is dropped ONLY by an explicit resolved/captain-held line for its exact key,
|
|
584
|
+
# never by cursor advancement, age, or being buried under later appends - the
|
|
585
|
+
# persisted open-set carries every still-open key forward across calls
|
|
586
|
+
# regardless of how much new unrelated log content has since been folded in.
|
|
587
|
+
#
|
|
588
|
+
# The cursor format is `version`, `offset`, `ident`, then the folded open set.
|
|
589
|
+
# FM_OPEN_DECISIONS_FOLD_VERSION must be bumped whenever
|
|
590
|
+
# _fm_decision_fold_line semantics change, so persisted state from an older
|
|
591
|
+
# interpretation is discarded and rebuilt from byte 0.
|
|
592
|
+
#
|
|
593
|
+
# Cursor invalidation is deliberately minimal, matching how status files are
|
|
594
|
+
# ACTUALLY used in this repo: every one is created once (`>`) and only ever
|
|
595
|
+
# appended to (`>>`) - never replaced, renamed, or rewritten in place. So the
|
|
596
|
+
# ways a cursor can go stale are a fold-version mismatch, a shrink (truncated),
|
|
597
|
+
# or the file at this path being a different file than before
|
|
598
|
+
# (replaced/rotated/recreated), which a changed device+inode makes an O(1) check
|
|
599
|
+
# via a single `stat` call - no content hashing, no re-reading the consumed
|
|
600
|
+
# prefix. Any signal falls back to a full re-fold of the whole current file from
|
|
601
|
+
# byte 0 - byte for byte what status_open_decisions itself would compute - and
|
|
602
|
+
# rewrites the cursor from that clean baseline. A same-inode, same-size,
|
|
603
|
+
# in-place byte edit is NOT detected; that is a deliberately accepted gap
|
|
604
|
+
# because no code path in this repo ever does that to a status file.
|
|
605
|
+
#
|
|
606
|
+
# The other real failure mode is OUR OWN read failing (a stat/wc/tail I/O
|
|
607
|
+
# error), not a malformed writer: every such read here is checked, and on
|
|
608
|
+
# failure this reports the already-trusted persisted set unchanged rather than
|
|
609
|
+
# risking a silent invalidation that would wipe it - never a bare "empty" as if
|
|
610
|
+
# nothing were open.
|
|
611
|
+
#
|
|
612
|
+
# Not a pure status-file read: this writes/rewrites the sibling cursor file as a
|
|
613
|
+
# side effect (state/.<task>.open-decisions-cursor), the library's second
|
|
614
|
+
# documented exception to the pure-read rule after crew_absorb_class. The write
|
|
615
|
+
# is atomic (temp file + rename), so a crash between calls leaves either the
|
|
616
|
+
# prior cursor or the new one, never a partial one. bin/fm-wake-drain.sh calls
|
|
617
|
+
# this only after releasing the wake-queue lock, so a hypothetical race between
|
|
618
|
+
# two overlapping drains can at worst redo a little folding work twice - never
|
|
619
|
+
# drop an open decision - because a losing writer's offset can only ever be
|
|
620
|
+
# equal to or behind an already-recorded byte position, and the next call
|
|
621
|
+
# re-derives from whatever offset actually landed on disk.
|
|
622
|
+
_fm_open_decisions_cursor_path() { # <status-file>
|
|
623
|
+
local f=$1 dir base
|
|
624
|
+
dir=$(dirname "$f")
|
|
625
|
+
base=$(basename "$f")
|
|
626
|
+
printf '%s/.%s.open-decisions-cursor' "$dir" "${base%.status}"
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
# 4: verb parsing ends at the first "[name=value]" tag rather than only at a
|
|
630
|
+
# "[key=...]" one, so lines carrying another bracketed tag first became opens
|
|
631
|
+
# and closes.
|
|
632
|
+
# 5: status_line_verb now also reads through an UNBRACKETED correlation token,
|
|
633
|
+
# so lines that previously folded as ordinary status become opens and closes.
|
|
634
|
+
# Version 4 was already spent on the bracketed-tag parser change above, and a
|
|
635
|
+
# cursor persisted under that reading predates this one, so it must still be
|
|
636
|
+
# discarded and rebuilt from byte 0 under the new reading.
|
|
637
|
+
FM_OPEN_DECISIONS_FOLD_VERSION=5
|
|
638
|
+
|
|
639
|
+
# Portable device:inode identity for the rotation/recreation check below.
|
|
640
|
+
_fm_open_decisions_file_ident() { # <file> -> strongest available identity
|
|
641
|
+
local f=$1 epoch birth ident
|
|
642
|
+
if [ -n "${FM_STATUS_IDENTITY_READER:-}" ]; then
|
|
643
|
+
"$FM_STATUS_IDENTITY_READER" "$f"
|
|
644
|
+
return
|
|
645
|
+
fi
|
|
646
|
+
if [ "$(uname -s 2>/dev/null)" = Darwin ]; then
|
|
647
|
+
ident=$(LC_ALL=C stat -f '%d:%i' "$f" 2>/dev/null) || return 1
|
|
648
|
+
epoch=$(LC_ALL=C stat -f '%B' "$f" 2>/dev/null) || epoch=0
|
|
649
|
+
if [ "$epoch" != 0 ]; then birth=$(LC_ALL=C stat -f '%FB' "$f" 2>/dev/null) || birth=''; else birth=''; fi
|
|
650
|
+
else
|
|
651
|
+
ident=$(LC_ALL=C stat -c '%d:%i' "$f" 2>/dev/null) || return 1
|
|
652
|
+
epoch=$(LC_ALL=C stat -c '%W' "$f" 2>/dev/null) || epoch=0
|
|
653
|
+
if [ "$epoch" != 0 ]; then birth=$(LC_ALL=C stat -c '%w' "$f" 2>/dev/null) || birth=''; else birth=''; fi
|
|
654
|
+
fi
|
|
655
|
+
case "$ident$birth" in *$'\t'*|*$'\n'*|'') return 1 ;; esac
|
|
656
|
+
if [ -n "$birth" ]; then printf 'strong:%s:%s' "$ident" "$birth"; else printf 'weak:%s' "$ident"; fi
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
_fm_status_file_size() { # <status-file>
|
|
660
|
+
local f=$1
|
|
661
|
+
if [ -n "${FM_STATUS_SIZE_READER:-}" ]; then
|
|
662
|
+
"$FM_STATUS_SIZE_READER" "$f"
|
|
663
|
+
return
|
|
664
|
+
fi
|
|
665
|
+
if [ "$(uname -s 2>/dev/null)" = Darwin ]; then
|
|
666
|
+
LC_ALL=C stat -f '%z' "$f" 2>/dev/null
|
|
667
|
+
else
|
|
668
|
+
LC_ALL=C stat -c '%s' "$f" 2>/dev/null
|
|
669
|
+
fi
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
# Private scratch path for a one-shot span read, alongside the status file the
|
|
673
|
+
# same way the cursor above is, and PID-scoped so concurrent readers of one log
|
|
674
|
+
# (the watcher and the away-mode daemon both classify the same stream) never
|
|
675
|
+
# truncate each other's chunk.
|
|
676
|
+
_fm_status_span_scratch() { # <status-file>
|
|
677
|
+
printf '%s.span.%s' "$(_fm_open_decisions_cursor_path "$1")" "$$"
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
_fm_status_read_span() { # <status-file> <start-offset> <byte-length>
|
|
681
|
+
local f=$1 start=$2 length=$3
|
|
682
|
+
if [ -n "${FM_STATUS_SPAN_READER:-}" ]; then
|
|
683
|
+
"$FM_STATUS_SPAN_READER" "$f" "$start" "$length"
|
|
684
|
+
return
|
|
685
|
+
fi
|
|
686
|
+
perl -MFcntl=:DEFAULT -e '
|
|
687
|
+
my ($path, $start, $length) = @ARGV;
|
|
688
|
+
sysopen(my $file, $path, O_RDONLY | O_NOFOLLOW) or exit 1;
|
|
689
|
+
sysseek($file, $start, 0) == $start or exit 1;
|
|
690
|
+
while ($length > 0) {
|
|
691
|
+
my $want = $length > 65536 ? 65536 : $length;
|
|
692
|
+
my $read = sysread($file, my $chunk, $want);
|
|
693
|
+
defined($read) && $read > 0 or exit 1;
|
|
694
|
+
print $chunk or exit 1;
|
|
695
|
+
$length -= $read;
|
|
696
|
+
}
|
|
697
|
+
' "$f" "$start" "$length"
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
status_open_decisions_incremental() { # <status-file> [<captured-end-offset>]
|
|
701
|
+
local f=$1 captured_end=${2:-} cf offset ident open='' trusted_open='' cursor_data first rest offset_line ident_line
|
|
702
|
+
local version='' size actual_size cur_ident resolve held chunk_file chunk_size line cursor_dirty=0
|
|
703
|
+
local target_cursor
|
|
704
|
+
[ -f "$f" ] && [ -r "$f" ] && [ ! -L "$f" ] || return 0
|
|
705
|
+
cf=$(_fm_open_decisions_cursor_path "$f")
|
|
706
|
+
offset=0
|
|
707
|
+
ident=''
|
|
708
|
+
if [ -f "$cf" ] && [ -r "$cf" ] && [ ! -L "$cf" ]; then
|
|
709
|
+
cursor_data=$(LC_ALL=C command cat "$cf" 2>/dev/null) || cursor_data=''
|
|
710
|
+
fi
|
|
711
|
+
if [ -n "${cursor_data:-}" ]; then
|
|
712
|
+
first=${cursor_data%%$'\n'*}
|
|
713
|
+
case "$first" in
|
|
714
|
+
version=*)
|
|
715
|
+
version=${first#version=}
|
|
716
|
+
[ "$version" = "$FM_OPEN_DECISIONS_FOLD_VERSION" ] || version=''
|
|
717
|
+
rest=${cursor_data#*$'\n'}
|
|
718
|
+
offset_line=${rest%%$'\n'*}
|
|
719
|
+
case "$offset_line" in
|
|
720
|
+
offset=*) offset=${offset_line#offset=} ;;
|
|
721
|
+
*) offset=0; version='' ;;
|
|
722
|
+
esac
|
|
723
|
+
case "$offset" in
|
|
724
|
+
''|*[!0-9]*) offset=0; version='' ;;
|
|
725
|
+
*)
|
|
726
|
+
case "$rest" in
|
|
727
|
+
*$'\n'*)
|
|
728
|
+
rest=${rest#*$'\n'}
|
|
729
|
+
ident_line=${rest%%$'\n'*}
|
|
730
|
+
case "$ident_line" in
|
|
731
|
+
ident=*)
|
|
732
|
+
ident=${ident_line#ident=}
|
|
733
|
+
case "$rest" in
|
|
734
|
+
*$'\n'*) open=${rest#*$'\n'} ;;
|
|
735
|
+
esac
|
|
736
|
+
if [ -n "$version" ] && [ -n "$ident" ]; then trusted_open=$open; fi
|
|
737
|
+
;;
|
|
738
|
+
*) offset=0; version='' ;;
|
|
739
|
+
esac
|
|
740
|
+
;;
|
|
741
|
+
*) offset=0; version='' ;;
|
|
742
|
+
esac
|
|
743
|
+
;;
|
|
744
|
+
esac
|
|
745
|
+
;;
|
|
746
|
+
esac
|
|
747
|
+
fi
|
|
748
|
+
|
|
749
|
+
# A stat/size-read failure is a genuine I/O error, not "the file is empty" -
|
|
750
|
+
# report the already-trusted persisted set unchanged rather than risking a
|
|
751
|
+
# silent invalidation that would wipe it.
|
|
752
|
+
cur_ident=$(_fm_open_decisions_file_ident "$f") || { printf '%s' "$trusted_open"; return 0; }
|
|
753
|
+
[ -n "$cur_ident" ] || { printf '%s' "$trusted_open"; return 0; }
|
|
754
|
+
actual_size=$(_fm_status_file_size "$f") \
|
|
755
|
+
|| { printf '%s' "$trusted_open"; return 0; }
|
|
756
|
+
actual_size=${actual_size//[[:space:]]/}
|
|
757
|
+
case "$actual_size" in ''|*[!0-9]*) printf '%s' "$trusted_open"; return 0 ;; esac
|
|
758
|
+
if [ -n "$captured_end" ]; then
|
|
759
|
+
case "$captured_end" in
|
|
760
|
+
''|*[!0-9]*) printf '%s' "$trusted_open"; return 0 ;;
|
|
761
|
+
esac
|
|
762
|
+
[ "$captured_end" -le "$actual_size" ] || { printf '%s' "$trusted_open"; return 0; }
|
|
763
|
+
size=$captured_end
|
|
764
|
+
else
|
|
765
|
+
size=$actual_size
|
|
766
|
+
fi
|
|
767
|
+
|
|
768
|
+
if [ -z "$version" ] || [ -z "$ident" ] || [ "$ident" != "$cur_ident" ] || [ "$offset" -gt "$actual_size" ]; then
|
|
769
|
+
offset=0
|
|
770
|
+
open=''
|
|
771
|
+
trusted_open=''
|
|
772
|
+
cursor_dirty=1
|
|
773
|
+
fi
|
|
774
|
+
|
|
775
|
+
if [ "$offset" -lt "$size" ]; then
|
|
776
|
+
chunk_file="$cf.read.$$"
|
|
777
|
+
_fm_status_read_span "$f" "$offset" "$((size - offset))" > "$chunk_file" 2>/dev/null \
|
|
778
|
+
|| { rm -f "$chunk_file"; printf '%s' "$trusted_open"; return 0; }
|
|
779
|
+
chunk_size=$(LC_ALL=C wc -c < "$chunk_file" 2>/dev/null) \
|
|
780
|
+
|| { rm -f "$chunk_file"; printf '%s' "$trusted_open"; return 0; }
|
|
781
|
+
chunk_size=${chunk_size//[[:space:]]/}
|
|
782
|
+
case "$chunk_size" in
|
|
783
|
+
''|*[!0-9]*) rm -f "$chunk_file"; printf '%s' "$trusted_open"; return 0 ;;
|
|
784
|
+
esac
|
|
785
|
+
# Test-only observability seam (off by default, no production behavior
|
|
786
|
+
# change): when set, records exactly how many bytes THIS call folded, so a
|
|
787
|
+
# test can assert the incremental path stays bounded by new appends rather
|
|
788
|
+
# than re-reading the whole file, without relying on timing or source text.
|
|
789
|
+
[ -n "${FM_OPEN_DECISIONS_READ_PROBE:-}" ] \
|
|
790
|
+
&& printf '%s\t%s\n' "$f" "$chunk_size" >> "$FM_OPEN_DECISIONS_READ_PROBE"
|
|
791
|
+
resolve=${FM_CLASSIFY_RESOLVE_VERB:-$FM_CLASSIFY_RESOLVE_VERB_DEFAULT}
|
|
792
|
+
held=${FM_CLASSIFY_CAPTAIN_HELD_VERB:-$FM_CLASSIFY_CAPTAIN_HELD_VERB_DEFAULT}
|
|
793
|
+
while IFS= read -r line || [ -n "$line" ]; do
|
|
794
|
+
open=$(_fm_decision_fold_line "$open" "$line" "$resolve" "$held")
|
|
795
|
+
done < "$chunk_file"
|
|
796
|
+
rm -f "$chunk_file"
|
|
797
|
+
offset=$size
|
|
798
|
+
cursor_dirty=1
|
|
799
|
+
fi
|
|
800
|
+
if [ "$cursor_dirty" -eq 1 ]; then
|
|
801
|
+
target_cursor="$cf.tmp.$$"
|
|
802
|
+
{
|
|
803
|
+
printf 'version=%s\n' "$FM_OPEN_DECISIONS_FOLD_VERSION"
|
|
804
|
+
printf 'offset=%s\n' "$offset"
|
|
805
|
+
printf 'ident=%s\n' "$cur_ident"
|
|
806
|
+
if [ -n "$open" ]; then printf '%s' "$open"; fi
|
|
807
|
+
} > "$target_cursor" || return 1
|
|
808
|
+
mv -f "$target_cursor" "$cf" || return 1
|
|
809
|
+
fi
|
|
810
|
+
printf '%s' "$open"
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
# Incremental sibling of scan_open_decisions: same fleet-wide directory walk and
|
|
814
|
+
# output shape ("<task>\t<key>\t<verb>\t<note>" per open decision), but folds
|
|
815
|
+
# each task's status log through status_open_decisions_incremental instead of
|
|
816
|
+
# the whole-file status_open_decisions, so a fleet-wide per-drain scan stays
|
|
817
|
+
# bounded by new appends rather than total lifetime log size across every task.
|
|
818
|
+
scan_open_decisions_incremental() { # <state>
|
|
819
|
+
local state=$1 f task open line
|
|
820
|
+
for f in "$state"/*.status; do
|
|
821
|
+
[ -e "$f" ] || continue
|
|
822
|
+
task=$(basename "$f"); task="${task%.status}"
|
|
823
|
+
open=$(status_open_decisions_incremental "$f") || continue
|
|
824
|
+
[ -n "$open" ] || continue
|
|
825
|
+
while IFS= read -r line; do
|
|
826
|
+
[ -n "$line" ] || continue
|
|
827
|
+
printf '%s\t%s\n' "$task" "$line"
|
|
828
|
+
done <<EOF
|
|
829
|
+
$open
|
|
830
|
+
EOF
|
|
831
|
+
done
|
|
832
|
+
return 0
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
status_presentation_snapshot() { # <state>
|
|
836
|
+
local state=$1 f task size ident
|
|
837
|
+
for f in "$state"/*.status; do
|
|
838
|
+
[ -e "$f" ] || continue
|
|
839
|
+
[ -f "$f" ] && [ -r "$f" ] && [ ! -L "$f" ] || continue
|
|
840
|
+
task=$(basename "$f"); task="${task%.status}"
|
|
841
|
+
size=$(_fm_status_file_size "$f") || return 1
|
|
842
|
+
size=${size//[[:space:]]/}
|
|
843
|
+
ident=$(_fm_open_decisions_file_ident "$f") || return 1
|
|
844
|
+
case "$size" in ''|*[!0-9]*) return 1 ;; esac
|
|
845
|
+
[ -n "$ident" ] || return 1
|
|
846
|
+
printf '%s\t%s\t%s\n' "$task" "$size" "$ident" || return 1
|
|
847
|
+
done
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
status_presentation_cursor_offset() { # <status-file>
|
|
851
|
+
local f=$1 state task manifest data row_task offset ident extra cur_ident size legacy
|
|
852
|
+
[ -f "$f" ] && [ -r "$f" ] && [ ! -L "$f" ] || return 1
|
|
853
|
+
state=${f%/*}
|
|
854
|
+
task=${f##*/}; task=${task%.status}
|
|
855
|
+
manifest="$state/.status-presentation-cursor"
|
|
856
|
+
if [ -e "$manifest" ] || [ -L "$manifest" ]; then
|
|
857
|
+
[ -f "$manifest" ] && [ -r "$manifest" ] && [ ! -L "$manifest" ] || return 1
|
|
858
|
+
data=$(LC_ALL=C command cat "$manifest" 2>/dev/null) || return 1
|
|
859
|
+
offset=
|
|
860
|
+
while IFS=$(printf '\t') read -r row_task ident legacy extra; do
|
|
861
|
+
[ -n "$row_task" ] || continue
|
|
862
|
+
[ -z "$extra" ] || return 1
|
|
863
|
+
case "$legacy" in ''|*[!0-9]*) return 1 ;; esac
|
|
864
|
+
[ -n "$ident" ] || return 1
|
|
865
|
+
if [ "$row_task" = "$task" ]; then
|
|
866
|
+
[ -z "$offset" ] || return 1
|
|
867
|
+
offset=$legacy
|
|
868
|
+
cur_ident=$ident
|
|
869
|
+
fi
|
|
870
|
+
done <<EOF
|
|
871
|
+
$data
|
|
872
|
+
EOF
|
|
873
|
+
if [ -z "$offset" ]; then
|
|
874
|
+
printf '0'
|
|
875
|
+
return 0
|
|
876
|
+
fi
|
|
877
|
+
ident=$cur_ident
|
|
878
|
+
else
|
|
879
|
+
legacy=$(_fm_open_decisions_cursor_path "$f")
|
|
880
|
+
if [ -e "$legacy" ] || [ -L "$legacy" ]; then
|
|
881
|
+
status_open_decisions_cursor_offset "$f"
|
|
882
|
+
return
|
|
883
|
+
fi
|
|
884
|
+
offset=0
|
|
885
|
+
ident=$(_fm_open_decisions_file_ident "$f") || return 1
|
|
886
|
+
fi
|
|
887
|
+
cur_ident=$(_fm_open_decisions_file_ident "$f") || return 1
|
|
888
|
+
size=$(_fm_status_file_size "$f") || return 1
|
|
889
|
+
size=${size//[[:space:]]/}
|
|
890
|
+
case "$size:$offset" in *[!0-9:]*) return 1 ;; esac
|
|
891
|
+
if [ "$ident" != "$cur_ident" ] || [ "$offset" -gt "$size" ]; then offset=0; fi
|
|
892
|
+
printf '%s' "$offset"
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
status_signal_seen_marker_path() { # <state> <task-id>
|
|
896
|
+
printf '%s/.seen-%s' "$1" "$(printf '%s.status' "$2" | tr '.' '_')"
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
status_heartbeat_seen_marker_path() { # <state> <task-id>
|
|
900
|
+
printf '%s/.hb-surfaced-%s' "$1" "$(printf '%s' "$2" | tr ':/.' '___')"
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
status_daemon_seen_marker_path() { # <state> <task-id>
|
|
904
|
+
printf '%s/.subsuper-seen-status-%s' "$1" "$(printf '%s' "$2" | tr ':/.' '___')"
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
_status_presentation_signature_valid() {
|
|
908
|
+
local value=$1 size ident encoded
|
|
909
|
+
[ "$value" = unverifiable ] && return 0
|
|
910
|
+
case "$value" in
|
|
911
|
+
r1:*)
|
|
912
|
+
encoded=${value#r1:}
|
|
913
|
+
case "$encoded" in ''|*[!0-9a-f]*) return 1 ;; esac
|
|
914
|
+
return 0
|
|
915
|
+
;;
|
|
916
|
+
esac
|
|
917
|
+
case "$value" in *@*) size=${value%%@*}; ident=${value#*@} ;; *) return 1 ;; esac
|
|
918
|
+
case "$size" in ''|*[!0-9]*) return 1 ;; esac
|
|
919
|
+
case "$ident" in ''|*$'\t'*|*$'\n'*) return 1 ;; esac
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
STATUS_PRESENTATION_REPORTED=
|
|
923
|
+
STATUS_PRESENTATION_CLASSIFIED=
|
|
924
|
+
status_presentation_marker_parse() {
|
|
925
|
+
local raw=$1 rest reported classified
|
|
926
|
+
STATUS_PRESENTATION_REPORTED=
|
|
927
|
+
STATUS_PRESENTATION_CLASSIFIED=
|
|
928
|
+
case "$raw" in
|
|
929
|
+
v2$'\t'*)
|
|
930
|
+
rest=${raw#v2$'\t'}
|
|
931
|
+
case "$rest" in *$'\t'*) reported=${rest%%$'\t'*}; classified=${rest#*$'\t'} ;; *) return 1 ;; esac
|
|
932
|
+
case "$classified" in *$'\t'*) return 1 ;; esac
|
|
933
|
+
_status_presentation_signature_valid "$reported" || return 1
|
|
934
|
+
if [ "$classified" != - ]; then
|
|
935
|
+
_status_presentation_signature_valid "$classified" || return 1
|
|
936
|
+
case "$classified" in unverifiable|r1:*) return 1 ;; esac
|
|
937
|
+
fi
|
|
938
|
+
;;
|
|
939
|
+
*)
|
|
940
|
+
_status_presentation_signature_valid "$raw" || return 1
|
|
941
|
+
case "$raw" in unverifiable|r1:*) return 1 ;; esac
|
|
942
|
+
reported=$raw
|
|
943
|
+
classified=$raw
|
|
944
|
+
;;
|
|
945
|
+
esac
|
|
946
|
+
STATUS_PRESENTATION_REPORTED=$reported
|
|
947
|
+
STATUS_PRESENTATION_CLASSIFIED=$classified
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
_status_observed_path_state() {
|
|
951
|
+
if [ "$(uname -s 2>/dev/null)" = Darwin ]; then
|
|
952
|
+
LC_ALL=C stat -f '%HT:%p' "$1" 2>/dev/null
|
|
953
|
+
else
|
|
954
|
+
LC_ALL=C stat -c '%F:%f' "$1" 2>/dev/null
|
|
955
|
+
fi
|
|
956
|
+
}
|
|
957
|
+
|
|
958
|
+
status_observed_signature() {
|
|
959
|
+
local f=$1 size=${2-} ident=${3-} path_state link_target=- access kind encoded
|
|
960
|
+
path_state=$(_status_observed_path_state "$f") || path_state=stat-error
|
|
961
|
+
if [ -L "$f" ]; then
|
|
962
|
+
link_target=$(readlink "$f" 2>/dev/null) || link_target=readlink-error
|
|
963
|
+
kind=symlink
|
|
964
|
+
elif [ ! -e "$f" ]; then
|
|
965
|
+
kind=absent
|
|
966
|
+
elif [ ! -f "$f" ]; then
|
|
967
|
+
kind=nonregular
|
|
968
|
+
elif [ -r "$f" ]; then
|
|
969
|
+
kind=readable
|
|
970
|
+
else
|
|
971
|
+
kind=unreadable
|
|
972
|
+
fi
|
|
973
|
+
if [ -z "$size" ]; then
|
|
974
|
+
size=$(_fm_status_file_size "$f") || size='size-error'
|
|
975
|
+
size=${size//[[:space:]]/}
|
|
976
|
+
case "$size" in ''|*[!0-9]*) size='size-error' ;; esac
|
|
977
|
+
fi
|
|
978
|
+
if [ -z "$ident" ]; then
|
|
979
|
+
ident=$(_fm_open_decisions_file_ident "$f") || ident=identity-error
|
|
980
|
+
[ -n "$ident" ] || ident=identity-error
|
|
981
|
+
fi
|
|
982
|
+
if [ -r "$f" ]; then access=readable; else access=unreadable; fi
|
|
983
|
+
encoded=$(printf '%s\0%s\0%s\0%s\0%s\0%s' \
|
|
984
|
+
"$size" "$ident" "$path_state" "$link_target" "$access" "$kind" \
|
|
985
|
+
| LC_ALL=C od -An -v -tx1 | tr -d ' \n') || return 1
|
|
986
|
+
printf 'r1:%s' "$encoded"
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
status_presentation_marker_reported_matches() {
|
|
990
|
+
local raw
|
|
991
|
+
raw=$(cat "$1" 2>/dev/null) || return 1
|
|
992
|
+
status_presentation_marker_parse "$raw" || return 1
|
|
993
|
+
[ "$STATUS_PRESENTATION_REPORTED" = "$2" ]
|
|
994
|
+
}
|
|
995
|
+
|
|
996
|
+
status_presentation_marker_offset() {
|
|
997
|
+
local raw classified offset ident current
|
|
998
|
+
raw=$(cat "$1" 2>/dev/null) || { printf '0'; return 0; }
|
|
999
|
+
status_presentation_marker_parse "$raw" || { printf '0'; return 0; }
|
|
1000
|
+
classified=$STATUS_PRESENTATION_CLASSIFIED
|
|
1001
|
+
[ "$classified" != - ] || { printf '0'; return 0; }
|
|
1002
|
+
offset=${classified%%@*}; ident=${classified#*@}
|
|
1003
|
+
current=$(_fm_open_decisions_file_ident "$2") || { printf '0'; return 0; }
|
|
1004
|
+
[ "$ident" = "$current" ] || { printf '0'; return 0; }
|
|
1005
|
+
printf '%s' "$offset"
|
|
1006
|
+
}
|
|
1007
|
+
|
|
1008
|
+
status_presentation_marker_report() {
|
|
1009
|
+
local marker=$1 reported=$2 raw classified=-
|
|
1010
|
+
_status_presentation_signature_valid "$reported" || return 1
|
|
1011
|
+
if raw=$(cat "$marker" 2>/dev/null) && status_presentation_marker_parse "$raw"; then
|
|
1012
|
+
classified=$STATUS_PRESENTATION_CLASSIFIED
|
|
1013
|
+
fi
|
|
1014
|
+
printf 'v2\t%s\t%s' "$reported" "$classified" > "$marker"
|
|
1015
|
+
}
|
|
1016
|
+
|
|
1017
|
+
status_presentation_marker_commit() {
|
|
1018
|
+
local marker=$1 file=$2 endpoint=$3 ident=$4 current reported classified
|
|
1019
|
+
case "$endpoint" in ''|*[!0-9]*) return 1 ;; esac
|
|
1020
|
+
current=$(_fm_open_decisions_file_ident "$file") || return 1
|
|
1021
|
+
[ -n "$ident" ] && [ "$ident" = "$current" ] || return 1
|
|
1022
|
+
reported=$(status_observed_signature "$file" "$endpoint" "$ident") || return 1
|
|
1023
|
+
classified="${endpoint}@${ident}"
|
|
1024
|
+
printf 'v2\t%s\t%s' "$reported" "$classified" > "$marker"
|
|
1025
|
+
}
|
|
1026
|
+
|
|
1027
|
+
status_retire_presentation_task() { # <state> <task-id>
|
|
1028
|
+
local state=$1 task=$2 lock manifest tmp data row_task ident offset extra rc=0 found=0
|
|
1029
|
+
local signal_marker heartbeat_marker daemon_marker
|
|
1030
|
+
lock="$state/.status-presentation-lock"
|
|
1031
|
+
manifest="$state/.status-presentation-cursor"
|
|
1032
|
+
tmp="$manifest.tmp.$$"
|
|
1033
|
+
signal_marker=$(status_signal_seen_marker_path "$state" "$task")
|
|
1034
|
+
heartbeat_marker=$(status_heartbeat_seen_marker_path "$state" "$task")
|
|
1035
|
+
daemon_marker=$(status_daemon_seen_marker_path "$state" "$task")
|
|
1036
|
+
|
|
1037
|
+
# A remote-home teardown can legitimately retire an endpoint ID that has no
|
|
1038
|
+
# status log in that home. Do not contend with that home's unrelated status
|
|
1039
|
+
# presenter in this no-op case. A concurrent presenter cannot add this task
|
|
1040
|
+
# without its status file, so a valid manifest with no matching row is a
|
|
1041
|
+
# durable proof that there is nothing to retire.
|
|
1042
|
+
if [ ! -e "$state/$task.status" ] && [ ! -L "$state/$task.status" ] \
|
|
1043
|
+
&& [ ! -e "$state/.$task.open-decisions-cursor" ] \
|
|
1044
|
+
&& [ ! -L "$state/.$task.open-decisions-cursor" ] \
|
|
1045
|
+
&& [ ! -e "$signal_marker" ] && [ ! -L "$signal_marker" ] \
|
|
1046
|
+
&& [ ! -e "$heartbeat_marker" ] && [ ! -L "$heartbeat_marker" ] \
|
|
1047
|
+
&& [ ! -e "$daemon_marker" ] && [ ! -L "$daemon_marker" ]; then
|
|
1048
|
+
if [ ! -e "$manifest" ] && [ ! -L "$manifest" ]; then
|
|
1049
|
+
return 0
|
|
1050
|
+
fi
|
|
1051
|
+
if [ -f "$manifest" ] && [ -r "$manifest" ] && [ ! -L "$manifest" ] \
|
|
1052
|
+
&& data=$(LC_ALL=C command cat "$manifest" 2>/dev/null); then
|
|
1053
|
+
while IFS=$(printf '\t') read -r row_task ident offset extra; do
|
|
1054
|
+
[ -n "$row_task" ] || continue
|
|
1055
|
+
if [ -n "$extra" ] || [ -z "$ident" ]; then rc=1; break; fi
|
|
1056
|
+
case "$offset" in ''|*[!0-9]*) rc=1; break ;; esac
|
|
1057
|
+
[ "$row_task" != "$task" ] || found=1
|
|
1058
|
+
done <<EOF
|
|
1059
|
+
$data
|
|
1060
|
+
EOF
|
|
1061
|
+
[ "$rc" -ne 0 ] || [ "$found" -ne 0 ] || return 0
|
|
1062
|
+
rc=0
|
|
1063
|
+
fi
|
|
1064
|
+
fi
|
|
1065
|
+
|
|
1066
|
+
fm_lock_acquire_wait "$lock" || return 1
|
|
1067
|
+
if [ -e "$manifest" ] || [ -L "$manifest" ]; then
|
|
1068
|
+
if [ ! -f "$manifest" ] || [ ! -r "$manifest" ] || [ -L "$manifest" ]; then
|
|
1069
|
+
rc=1
|
|
1070
|
+
elif ! data=$(LC_ALL=C command cat "$manifest" 2>/dev/null); then
|
|
1071
|
+
rc=1
|
|
1072
|
+
elif ! : > "$tmp"; then
|
|
1073
|
+
rc=1
|
|
1074
|
+
else
|
|
1075
|
+
while IFS=$(printf '\t') read -r row_task ident offset extra; do
|
|
1076
|
+
[ -n "$row_task" ] || continue
|
|
1077
|
+
if [ -n "$extra" ] || [ -z "$ident" ]; then rc=1; break; fi
|
|
1078
|
+
case "$offset" in ''|*[!0-9]*) rc=1; break ;; esac
|
|
1079
|
+
if [ "$row_task" != "$task" ]; then
|
|
1080
|
+
printf '%s\t%s\t%s\n' "$row_task" "$ident" "$offset" >> "$tmp" \
|
|
1081
|
+
|| { rc=1; break; }
|
|
1082
|
+
fi
|
|
1083
|
+
done <<EOF
|
|
1084
|
+
$data
|
|
1085
|
+
EOF
|
|
1086
|
+
if [ "$rc" -eq 0 ]; then mv -f "$tmp" "$manifest" || rc=1; fi
|
|
1087
|
+
[ "$rc" -eq 0 ] || rm -f "$tmp"
|
|
1088
|
+
fi
|
|
1089
|
+
fi
|
|
1090
|
+
if [ "$rc" -eq 0 ]; then
|
|
1091
|
+
rm -f -- "$state/$task.status" "$state/.$task.open-decisions-cursor" \
|
|
1092
|
+
"$signal_marker" "$heartbeat_marker" "$daemon_marker" || rc=1
|
|
1093
|
+
fi
|
|
1094
|
+
fm_lock_release "$lock" || rc=1
|
|
1095
|
+
return "$rc"
|
|
1096
|
+
}
|
|
1097
|
+
|
|
1098
|
+
status_acknowledge_presented_snapshot() { # <state> <snapshot> [<fully-presented-task-ids>]
|
|
1099
|
+
local state=$1 snapshot=$2 fully_presented=${3:-} task endpoint ident f offset lines line safe
|
|
1100
|
+
while IFS=$(printf '\t') read -r task endpoint ident; do
|
|
1101
|
+
[ -n "$task" ] || continue
|
|
1102
|
+
safe=false
|
|
1103
|
+
case "
|
|
1104
|
+
$fully_presented
|
|
1105
|
+
" in *$'\n'"$task"$'\n'*) safe=true ;; esac
|
|
1106
|
+
if [ "$safe" = false ]; then
|
|
1107
|
+
f="$state/$task.status"
|
|
1108
|
+
offset=$(status_presentation_cursor_offset "$f") || return 1
|
|
1109
|
+
lines=$(status_new_lines_since_cursor "$f" "$endpoint") || return 1
|
|
1110
|
+
# Once any informational line in this span is presented fleet-wide, the
|
|
1111
|
+
# contiguous cursor may advance through the captured endpoint. Routine
|
|
1112
|
+
# lines remain unacknowledged only while they are the sole unread content,
|
|
1113
|
+
# preserving delayed signal annotations without replaying a handled note
|
|
1114
|
+
# that happened to follow a routine line.
|
|
1115
|
+
while IFS= read -r line || [ -n "$line" ]; do
|
|
1116
|
+
case "$line" in
|
|
1117
|
+
*[![:space:]]*)
|
|
1118
|
+
if status_line_is_unread_surface "$line"; then safe=true; break; fi
|
|
1119
|
+
;;
|
|
1120
|
+
esac
|
|
1121
|
+
done <<EOF
|
|
1122
|
+
$lines
|
|
1123
|
+
EOF
|
|
1124
|
+
if [ "$safe" = false ]; then endpoint=$offset; fi
|
|
1125
|
+
fi
|
|
1126
|
+
printf '%s\t%s\t%s\n' "$task" "$endpoint" "$ident" || return 1
|
|
1127
|
+
done <<EOF
|
|
1128
|
+
$snapshot
|
|
1129
|
+
EOF
|
|
1130
|
+
}
|
|
1131
|
+
|
|
1132
|
+
status_commit_presentation_snapshot() { # <state> <snapshot>
|
|
1133
|
+
local state=$1 snapshot=$2 task endpoint ident f cur_ident size tmp
|
|
1134
|
+
tmp="$state/.status-presentation-cursor.tmp.$$"
|
|
1135
|
+
: > "$tmp" || return 1
|
|
1136
|
+
while IFS=$(printf '\t') read -r task endpoint ident; do
|
|
1137
|
+
[ -n "$task" ] || continue
|
|
1138
|
+
case "$endpoint" in ''|*[!0-9]*) rm -f "$tmp"; return 1 ;; esac
|
|
1139
|
+
[ -n "$ident" ] || { rm -f "$tmp"; return 1; }
|
|
1140
|
+
f="$state/$task.status"
|
|
1141
|
+
[ -f "$f" ] && [ -r "$f" ] && [ ! -L "$f" ] || { rm -f "$tmp"; return 1; }
|
|
1142
|
+
cur_ident=$(_fm_open_decisions_file_ident "$f") || { rm -f "$tmp"; return 1; }
|
|
1143
|
+
size=$(_fm_status_file_size "$f") || { rm -f "$tmp"; return 1; }
|
|
1144
|
+
size=${size//[[:space:]]/}
|
|
1145
|
+
case "$size" in ''|*[!0-9]*) rm -f "$tmp"; return 1 ;; esac
|
|
1146
|
+
[ "$cur_ident" = "$ident" ] && [ "$endpoint" -le "$size" ] \
|
|
1147
|
+
|| { rm -f "$tmp"; return 1; }
|
|
1148
|
+
printf '%s\t%s\t%s\n' "$task" "$ident" "$endpoint" >> "$tmp" \
|
|
1149
|
+
|| { rm -f "$tmp"; return 1; }
|
|
1150
|
+
done <<EOF
|
|
1151
|
+
$snapshot
|
|
1152
|
+
EOF
|
|
1153
|
+
mv -f "$tmp" "$state/.status-presentation-cursor" || { rm -f "$tmp"; return 1; }
|
|
1154
|
+
}
|
|
1155
|
+
|
|
1156
|
+
scan_open_decisions_snapshot() { # <state> <task-and-endpoint-snapshot>
|
|
1157
|
+
local state=$1 snapshot=$2 task endpoint ident f open line
|
|
1158
|
+
while IFS=$(printf '\t') read -r task endpoint ident; do
|
|
1159
|
+
[ -n "$task" ] || continue
|
|
1160
|
+
f="$state/$task.status"
|
|
1161
|
+
open=$(status_open_decisions_incremental "$f" "$endpoint") || return 1
|
|
1162
|
+
[ -n "$open" ] || continue
|
|
1163
|
+
while IFS= read -r line; do
|
|
1164
|
+
[ -n "$line" ] || continue
|
|
1165
|
+
printf '%s\t%s\n' "$task" "$line"
|
|
1166
|
+
done <<EOF
|
|
1167
|
+
$open
|
|
1168
|
+
EOF
|
|
1169
|
+
done <<EOF
|
|
1170
|
+
$snapshot
|
|
1171
|
+
EOF
|
|
1172
|
+
}
|
|
1173
|
+
|
|
1174
|
+
# --- unread status lines since the presentation cursor ----------------------
|
|
1175
|
+
#
|
|
1176
|
+
# The drain annotation historically printed only the newest status line, so a
|
|
1177
|
+
# substantive `note:` answer immediately followed by a routine `note:` (or a
|
|
1178
|
+
# pending-reply resolution buried under a later unrelated append) never reached
|
|
1179
|
+
# the supervisor. Those verbs also never enter the OPEN DECISIONS fold, so they
|
|
1180
|
+
# had no other surfacing path.
|
|
1181
|
+
# These helpers are the ONE owner of "what is still unread since the last drain
|
|
1182
|
+
# presentation": one fleet manifest records each status identity and last-
|
|
1183
|
+
# presented byte offset, and one atomic replacement commits only the contiguous
|
|
1184
|
+
# status spans that were successfully presented. A quiet fleet scan leaves
|
|
1185
|
+
# routine working/done bytes unacknowledged so a subsequently published signal
|
|
1186
|
+
# can still annotate them. A missing manifest row or changed file identity is
|
|
1187
|
+
# offset 0 for the current file, while malformed or unreadable cursor state
|
|
1188
|
+
# aborts presentation without advancing any offset. A trusted cursor at EOF
|
|
1189
|
+
# prints nothing, so already-presented bytes are not replayed as new. Teardown
|
|
1190
|
+
# retires a task's manifest row with its status file, so reusing a task ID starts
|
|
1191
|
+
# the replacement log unread at byte 0. Informational `note:` lines and
|
|
1192
|
+
# reserved-key pending-reply resolutions are the fleet-wide unread surface;
|
|
1193
|
+
# they are not open decisions and are not persisted in the folded open-set.
|
|
1194
|
+
|
|
1195
|
+
# Read the legacy per-task open-decisions cursor used to seed the presentation
|
|
1196
|
+
# offset before the fleet manifest exists. A fold-version mismatch, identity
|
|
1197
|
+
# mismatch, or offset past the current size falls back to 0. Never writes unless
|
|
1198
|
+
# a caller explicitly requests a migration snapshot.
|
|
1199
|
+
status_open_decisions_cursor_offset() { # <status-file>
|
|
1200
|
+
local f=$1 cf offset=0 ident='' version='' cursor_data first rest open=''
|
|
1201
|
+
local offset_line ident_line cur_ident size
|
|
1202
|
+
[ -f "$f" ] && [ -r "$f" ] && [ ! -L "$f" ] || return 1
|
|
1203
|
+
cf=$(_fm_open_decisions_cursor_path "$f")
|
|
1204
|
+
if [ -e "$cf" ] || [ -L "$cf" ]; then
|
|
1205
|
+
[ -f "$cf" ] && [ -r "$cf" ] && [ ! -L "$cf" ] || return 1
|
|
1206
|
+
if cursor_data=$(LC_ALL=C command cat "$cf" 2>/dev/null); then
|
|
1207
|
+
first=${cursor_data%%$'\n'*}
|
|
1208
|
+
case "$first" in
|
|
1209
|
+
version=*)
|
|
1210
|
+
version=${first#version=}
|
|
1211
|
+
[ "$version" = "$FM_OPEN_DECISIONS_FOLD_VERSION" ] || version=''
|
|
1212
|
+
rest=${cursor_data#*$'\n'}
|
|
1213
|
+
offset_line=${rest%%$'\n'*}
|
|
1214
|
+
case "$offset_line" in
|
|
1215
|
+
offset=*) offset=${offset_line#offset=} ;;
|
|
1216
|
+
*) offset=0; version='' ;;
|
|
1217
|
+
esac
|
|
1218
|
+
case "$offset" in
|
|
1219
|
+
''|*[!0-9]*) offset=0; version='' ;;
|
|
1220
|
+
*)
|
|
1221
|
+
case "$rest" in
|
|
1222
|
+
*$'\n'*)
|
|
1223
|
+
rest=${rest#*$'\n'}
|
|
1224
|
+
ident_line=${rest%%$'\n'*}
|
|
1225
|
+
case "$ident_line" in
|
|
1226
|
+
ident=*)
|
|
1227
|
+
ident=${ident_line#ident=}
|
|
1228
|
+
case "$rest" in *$'\n'*) open=${rest#*$'\n'} ;; esac
|
|
1229
|
+
;;
|
|
1230
|
+
*) offset=0; version='' ;;
|
|
1231
|
+
esac
|
|
1232
|
+
;;
|
|
1233
|
+
*) offset=0; version='' ;;
|
|
1234
|
+
esac
|
|
1235
|
+
;;
|
|
1236
|
+
esac
|
|
1237
|
+
;;
|
|
1238
|
+
esac
|
|
1239
|
+
else
|
|
1240
|
+
return 1
|
|
1241
|
+
fi
|
|
1242
|
+
fi
|
|
1243
|
+
cur_ident=$(_fm_open_decisions_file_ident "$f") || return 1
|
|
1244
|
+
[ -n "$cur_ident" ] || return 1
|
|
1245
|
+
size=$(_fm_status_file_size "$f") || return 1
|
|
1246
|
+
size=${size//[[:space:]]/}
|
|
1247
|
+
case "$size" in ''|*[!0-9]*) return 1 ;; esac
|
|
1248
|
+
if [ -z "$version" ] || [ -z "$ident" ] || [ "$ident" != "$cur_ident" ] || [ "$offset" -gt "$size" ]; then
|
|
1249
|
+
offset=0
|
|
1250
|
+
open=''
|
|
1251
|
+
fi
|
|
1252
|
+
if [ -n "${FM_STATUS_CURSOR_SNAPSHOT_FILE:-}" ]; then
|
|
1253
|
+
{
|
|
1254
|
+
printf 'version=%s\n' "$FM_OPEN_DECISIONS_FOLD_VERSION"
|
|
1255
|
+
printf 'offset=%s\n' "$offset"
|
|
1256
|
+
printf 'ident=%s\n' "$cur_ident"
|
|
1257
|
+
if [ -n "$open" ]; then printf '%s' "$open"; fi
|
|
1258
|
+
} > "$FM_STATUS_CURSOR_SNAPSHOT_FILE" || return 1
|
|
1259
|
+
fi
|
|
1260
|
+
printf '%s' "$offset"
|
|
1261
|
+
}
|
|
1262
|
+
|
|
1263
|
+
# Print every non-blank status line whose bytes begin at or after the persisted
|
|
1264
|
+
# presentation offset. Does not write the cursor. A missing manifest row or
|
|
1265
|
+
# changed status identity reads the current file from offset 0; malformed or
|
|
1266
|
+
# unreadable cursor state fails the scan. Symlinks and unreadable status files
|
|
1267
|
+
# print nothing.
|
|
1268
|
+
status_new_lines_since_cursor() { # <status-file> [<captured-end-offset>]
|
|
1269
|
+
local f=$1 captured_end=${2:-} cf offset size actual_size chunk_file line rc=0
|
|
1270
|
+
[ -f "$f" ] && [ -r "$f" ] && [ ! -L "$f" ] || return 0
|
|
1271
|
+
cf=$(_fm_open_decisions_cursor_path "$f")
|
|
1272
|
+
chunk_file="$cf.unread.$$"
|
|
1273
|
+
offset=$(status_presentation_cursor_offset "$f") || return 1
|
|
1274
|
+
case "$offset" in ''|*[!0-9]*) return 1 ;; esac
|
|
1275
|
+
actual_size=$(_fm_status_file_size "$f") || return 1
|
|
1276
|
+
actual_size=${actual_size//[[:space:]]/}
|
|
1277
|
+
case "$actual_size" in ''|*[!0-9]*) return 1 ;; esac
|
|
1278
|
+
if [ -n "$captured_end" ]; then
|
|
1279
|
+
case "$captured_end" in ''|*[!0-9]*) return 1 ;; esac
|
|
1280
|
+
[ "$captured_end" -le "$actual_size" ] || return 1
|
|
1281
|
+
size=$captured_end
|
|
1282
|
+
else
|
|
1283
|
+
size=$actual_size
|
|
1284
|
+
fi
|
|
1285
|
+
[ "$offset" -lt "$size" ] || return 0
|
|
1286
|
+
_fm_status_read_span "$f" "$offset" "$((size - offset))" > "$chunk_file" 2>/dev/null \
|
|
1287
|
+
|| { rm -f "$chunk_file"; return 1; }
|
|
1288
|
+
while IFS= read -r line || [ -n "$line" ]; do
|
|
1289
|
+
case "$line" in
|
|
1290
|
+
*[![:space:]]*) printf '%s\n' "$line" || { rc=1; break; } ;;
|
|
1291
|
+
esac
|
|
1292
|
+
done < "$chunk_file"
|
|
1293
|
+
rm -f "$chunk_file"
|
|
1294
|
+
return "$rc"
|
|
1295
|
+
}
|
|
1296
|
+
|
|
1297
|
+
# 0 when a status line is an informational `note:` or a reserved-key
|
|
1298
|
+
# pending-reply resolution. Those lines never fold into OPEN DECISIONS, so the
|
|
1299
|
+
# drain's unread-status surface is their only guaranteed presentation.
|
|
1300
|
+
status_line_is_unread_surface() { # <status-line>
|
|
1301
|
+
local line=$1 verb key note resolve held prefix
|
|
1302
|
+
[ -n "$line" ] || return 1
|
|
1303
|
+
verb=$(status_line_verb "$line")
|
|
1304
|
+
[ "$verb" = note ] && return 0
|
|
1305
|
+
resolve=${FM_CLASSIFY_RESOLVE_VERB:-$FM_CLASSIFY_RESOLVE_VERB_DEFAULT}
|
|
1306
|
+
held=${FM_CLASSIFY_CAPTAIN_HELD_VERB:-$FM_CLASSIFY_CAPTAIN_HELD_VERB_DEFAULT}
|
|
1307
|
+
case "$verb" in
|
|
1308
|
+
"$resolve"|"$held") ;;
|
|
1309
|
+
*) return 1 ;;
|
|
1310
|
+
esac
|
|
1311
|
+
key=$(_fm_decision_key "$line") || return 1
|
|
1312
|
+
note=$(status_line_note "$line")
|
|
1313
|
+
for prefix in ${FM_CLASSIFY_RESERVED_KEY_PREFIXES:-$FM_CLASSIFY_RESERVED_KEY_PREFIXES_DEFAULT}; do
|
|
1314
|
+
case "$key" in
|
|
1315
|
+
"$prefix"*)
|
|
1316
|
+
_fm_decision_key_transition_allowed "$key" "$note"
|
|
1317
|
+
return
|
|
1318
|
+
;;
|
|
1319
|
+
esac
|
|
1320
|
+
done
|
|
1321
|
+
return 1
|
|
1322
|
+
}
|
|
1323
|
+
|
|
1324
|
+
# Fleet-wide unread informational lines: one "<task>\t<status-line>" row per
|
|
1325
|
+
# still-unread `note:` or pending-reply resolution, in glob (task id) order.
|
|
1326
|
+
# Prints nothing when none are unread. Directory scan rejects status symlinks
|
|
1327
|
+
# the same way scan_open_decisions does.
|
|
1328
|
+
scan_unread_surface_lines() { # <state>
|
|
1329
|
+
local state=$1 f task lines line
|
|
1330
|
+
for f in "$state"/*.status; do
|
|
1331
|
+
[ -e "$f" ] || continue
|
|
1332
|
+
task=$(basename "$f"); task="${task%.status}"
|
|
1333
|
+
lines=$(status_new_lines_since_cursor "$f") || return 1
|
|
1334
|
+
[ -n "$lines" ] || continue
|
|
1335
|
+
while IFS= read -r line; do
|
|
1336
|
+
[ -n "$line" ] || continue
|
|
1337
|
+
status_line_is_unread_surface "$line" || continue
|
|
1338
|
+
printf '%s\t%s\n' "$task" "$line"
|
|
1339
|
+
done <<EOF
|
|
1340
|
+
$lines
|
|
1341
|
+
EOF
|
|
1342
|
+
done
|
|
1343
|
+
return 0
|
|
1344
|
+
}
|
|
1345
|
+
|
|
1346
|
+
scan_unread_surface_snapshot() { # <state> <task-and-endpoint-snapshot>
|
|
1347
|
+
local state=$1 snapshot=$2 task endpoint ident f lines line
|
|
1348
|
+
while IFS=$(printf '\t') read -r task endpoint ident; do
|
|
1349
|
+
[ -n "$task" ] || continue
|
|
1350
|
+
f="$state/$task.status"
|
|
1351
|
+
lines=$(status_new_lines_since_cursor "$f" "$endpoint") || return 1
|
|
1352
|
+
[ -n "$lines" ] || continue
|
|
1353
|
+
while IFS= read -r line; do
|
|
1354
|
+
[ -n "$line" ] || continue
|
|
1355
|
+
status_line_is_unread_surface "$line" || continue
|
|
1356
|
+
printf '%s\t%s\n' "$task" "$line"
|
|
1357
|
+
done <<EOF
|
|
1358
|
+
$lines
|
|
1359
|
+
EOF
|
|
1360
|
+
done <<EOF
|
|
1361
|
+
$snapshot
|
|
1362
|
+
EOF
|
|
1363
|
+
}
|
|
1364
|
+
|
|
1365
|
+
# Fold material routed-work phases in the same keyed event stream.
|
|
1366
|
+
# A working or declared-pause event opens or replaces one phase for its key.
|
|
1367
|
+
# A later done, failed, needs-decision, blocked, or resolved event carrying that
|
|
1368
|
+
# key closes the phase, because it has moved to a terminal or separately tracked
|
|
1369
|
+
# state.
|
|
1370
|
+
# A bare legacy event uses the default key, preserving one-phase behavior.
|
|
1371
|
+
# This fold is evidence about whether a parent event was explicitly superseded.
|
|
1372
|
+
# It is never authoritative current crew state, and consumers must not let an open
|
|
1373
|
+
# phase outrank a structured home snapshot or fm-crew-state result.
|
|
1374
|
+
_fm_status_open_activities_stream() {
|
|
1375
|
+
local line verb key note resolve held open='' pause
|
|
1376
|
+
resolve=${FM_CLASSIFY_RESOLVE_VERB:-$FM_CLASSIFY_RESOLVE_VERB_DEFAULT}
|
|
1377
|
+
held=${FM_CLASSIFY_CAPTAIN_HELD_VERB:-$FM_CLASSIFY_CAPTAIN_HELD_VERB_DEFAULT}
|
|
1378
|
+
pause=${FM_CLASSIFY_PAUSED_VERB:-$FM_CLASSIFY_PAUSED_VERB_DEFAULT}
|
|
1379
|
+
while IFS= read -r line || [ -n "$line" ]; do
|
|
1380
|
+
# Blank-line guard; see _fm_decision_fold_line for why this is a glob.
|
|
1381
|
+
case "$line" in
|
|
1382
|
+
*[![:space:]]*) ;;
|
|
1383
|
+
*) continue ;;
|
|
1384
|
+
esac
|
|
1385
|
+
verb=$(status_line_verb "$line")
|
|
1386
|
+
key=$(_fm_decision_key "$line") || continue
|
|
1387
|
+
case "$verb" in
|
|
1388
|
+
working|"$pause")
|
|
1389
|
+
note=$(status_line_note "$line")
|
|
1390
|
+
open=$(_fm_decision_drop "$open" "$key")
|
|
1391
|
+
[ -n "$open" ] && open="${open}"$'\n'
|
|
1392
|
+
open="${open}${key}"$'\t'"${verb}"$'\t'"${note}"$'\n'
|
|
1393
|
+
;;
|
|
1394
|
+
done|failed|needs-decision|blocked|"$resolve"|"$held")
|
|
1395
|
+
open=$(_fm_decision_drop "$open" "$key")
|
|
1396
|
+
[ -n "$open" ] && open="${open}"$'\n'
|
|
1397
|
+
;;
|
|
1398
|
+
esac
|
|
1399
|
+
done
|
|
1400
|
+
printf '%s' "$open"
|
|
1401
|
+
}
|
|
1402
|
+
|
|
1403
|
+
status_open_activities() { # <status-file-or-dash>
|
|
1404
|
+
local f=$1
|
|
1405
|
+
if [ "$f" = - ]; then
|
|
1406
|
+
_fm_status_open_activities_stream
|
|
1407
|
+
return 0
|
|
1408
|
+
fi
|
|
1409
|
+
[ -f "$f" ] || return 0
|
|
1410
|
+
_fm_status_open_activities_stream < "$f"
|
|
1411
|
+
}
|
|
1412
|
+
|
|
1413
|
+
# task id from a recorded window target, falling back to the tmux-shaped
|
|
1414
|
+
# "<session>:fm-<id>" form when no metadata state is available.
|
|
1415
|
+
window_to_task() {
|
|
1416
|
+
local w=$1 state=${2:-${STATE:-${FM_STATE_OVERRIDE:-}}} meta mw mt t
|
|
1417
|
+
if [ -n "$state" ]; then
|
|
1418
|
+
for meta in "$state"/*.meta; do
|
|
1419
|
+
[ -e "$meta" ] || continue
|
|
1420
|
+
mw=$(grep '^window=' "$meta" 2>/dev/null | tail -1 | cut -d= -f2- || true)
|
|
1421
|
+
mt=$(grep '^terminal=' "$meta" 2>/dev/null | tail -1 | cut -d= -f2- || true)
|
|
1422
|
+
[ "$mw" = "$w" ] || [ "$mt" = "$w" ] || continue
|
|
1423
|
+
t=$(basename "$meta")
|
|
1424
|
+
t=${t%.meta}
|
|
1425
|
+
printf '%s' "$t"
|
|
1426
|
+
return 0
|
|
1427
|
+
done
|
|
1428
|
+
fi
|
|
1429
|
+
t="${w##*:}"; t="${t#fm-}"; printf '%s' "$t"
|
|
1430
|
+
}
|
|
1431
|
+
|
|
1432
|
+
# Capture the bytes of an append-only status log at or after <start-offset> under
|
|
1433
|
+
# one size-and-identity snapshot.
|
|
1434
|
+
# The record form prints `<endpoint>\t<identity>\t<events>` and returns 0 when
|
|
1435
|
+
# the span has actionable events, joining every such event in source order with
|
|
1436
|
+
# ` ; ` so callers report the complete captured span before committing it.
|
|
1437
|
+
# It returns 1 after a successful classification with no actionable event; an
|
|
1438
|
+
# existing log still prints its committable endpoint and identity, while an absent
|
|
1439
|
+
# log is the ordinary empty case and prints no record.
|
|
1440
|
+
# It returns 2 with no committable endpoint when an existing status object cannot
|
|
1441
|
+
# be classified.
|
|
1442
|
+
# The simpler wrapper prints only the event field, and the predicate discards the
|
|
1443
|
+
# record; all three inherit the library-header contract above.
|
|
1444
|
+
#
|
|
1445
|
+
# A keyed `needs-decision` or `blocked` transition accepted by the whole-file
|
|
1446
|
+
# fold is included only when that fold still names the exact opening as live.
|
|
1447
|
+
# A transition rejected by the reserved-key vocabulary is surfaced instead as a
|
|
1448
|
+
# reconciliation signal and never treated here as an open decision.
|
|
1449
|
+
# status_open_decisions remains the single owner of open/closed semantics,
|
|
1450
|
+
# including same-key reopening and reserved-key handling.
|
|
1451
|
+
# Every other captain-relevant event is terminal and always actionable.
|
|
1452
|
+
_fm_decision_origin_drop() { # <origins> <key>
|
|
1453
|
+
local origin
|
|
1454
|
+
while IFS= read -r origin; do
|
|
1455
|
+
case "$origin" in "$2"$'\t'*) ;; *) [ -n "$origin" ] && printf '%s\n' "$origin" ;; esac
|
|
1456
|
+
done <<EOF
|
|
1457
|
+
$1
|
|
1458
|
+
EOF
|
|
1459
|
+
}
|
|
1460
|
+
|
|
1461
|
+
_fm_status_open_decision_origins() { # <status-file>
|
|
1462
|
+
local f=$1 line open='' after key verb note number=0 origins=''
|
|
1463
|
+
local resolve held
|
|
1464
|
+
resolve=${FM_CLASSIFY_RESOLVE_VERB:-$FM_CLASSIFY_RESOLVE_VERB_DEFAULT}
|
|
1465
|
+
held=${FM_CLASSIFY_CAPTAIN_HELD_VERB:-$FM_CLASSIFY_CAPTAIN_HELD_VERB_DEFAULT}
|
|
1466
|
+
while IFS= read -r line || [ -n "$line" ]; do
|
|
1467
|
+
number=$((number + 1))
|
|
1468
|
+
after=$(_fm_decision_fold_line "$open" "$line" "$resolve" "$held")
|
|
1469
|
+
key=$(_fm_decision_key "$line") || { open=$after; continue; }
|
|
1470
|
+
verb=$(status_line_verb "$line")
|
|
1471
|
+
note=$(status_line_note "$line")
|
|
1472
|
+
case "$verb" in
|
|
1473
|
+
needs-decision|blocked)
|
|
1474
|
+
if _fm_open_set_has "$after" "$key" \
|
|
1475
|
+
&& [ "$(_fm_open_set_verb "$after" "$key")" = "$verb" ]; then
|
|
1476
|
+
case "$after" in
|
|
1477
|
+
"$key"$'\t'"$verb"$'\t'"$note"|*$'\n'"$key"$'\t'"$verb"$'\t'"$note")
|
|
1478
|
+
origins=$(_fm_decision_origin_drop "$origins" "$key")
|
|
1479
|
+
[ -n "$origins" ] && origins="${origins}"$'\n'
|
|
1480
|
+
origins="${origins}${key}"$'\t'"${number}"
|
|
1481
|
+
;;
|
|
1482
|
+
esac
|
|
1483
|
+
fi
|
|
1484
|
+
;;
|
|
1485
|
+
"$resolve"|"$held")
|
|
1486
|
+
_fm_open_set_has "$after" "$key" || origins=$(_fm_decision_origin_drop "$origins" "$key")
|
|
1487
|
+
;;
|
|
1488
|
+
esac
|
|
1489
|
+
open=$after
|
|
1490
|
+
done < "$f"
|
|
1491
|
+
printf '%s' "$origins"
|
|
1492
|
+
}
|
|
1493
|
+
|
|
1494
|
+
status_span_first_actionable_record() { # <status-file> <start-offset>
|
|
1495
|
+
local f=$1 start=${2:-0} size ident cur_ident scratch chunk_file full_file prefix_file
|
|
1496
|
+
local line verb key origins='' folded=0 rc=1 failed=0 prefix_lines=0 line_number=0 live_line='' events='' _line _key
|
|
1497
|
+
[ -e "$f" ] || { [ -L "$f" ] && return 2; return 1; }
|
|
1498
|
+
[ -f "$f" ] && [ -r "$f" ] && [ ! -L "$f" ] || return 2
|
|
1499
|
+
ident=$(_fm_open_decisions_file_ident "$f") || return 2
|
|
1500
|
+
size=$(_fm_status_file_size "$f") || return 2
|
|
1501
|
+
size=${size//[[:space:]]/}
|
|
1502
|
+
case "$size" in ''|*[!0-9]*) return 2 ;; esac
|
|
1503
|
+
case "$start" in ''|*[!0-9]*) start=0 ;; esac
|
|
1504
|
+
[ "$start" -le "$size" ] || start=0
|
|
1505
|
+
[ "$start" -lt "$size" ] || { printf '%s\t%s' "$size" "$ident"; return 1; }
|
|
1506
|
+
scratch=$(_fm_status_span_scratch "$f") || return 2
|
|
1507
|
+
chunk_file="${scratch}.span"; full_file="${scratch}.full"; prefix_file="${scratch}.prefix"
|
|
1508
|
+
_fm_status_read_span "$f" "$start" "$((size - start))" > "$chunk_file" 2>/dev/null \
|
|
1509
|
+
|| { rm -f "$chunk_file" "$full_file" "$prefix_file"; return 2; }
|
|
1510
|
+
cur_ident=$(_fm_open_decisions_file_ident "$f") || {
|
|
1511
|
+
rm -f "$chunk_file" "$full_file" "$prefix_file"; return 2;
|
|
1512
|
+
}
|
|
1513
|
+
[ "$cur_ident" = "$ident" ] || { rm -f "$chunk_file" "$full_file" "$prefix_file"; return 2; }
|
|
1514
|
+
while IFS= read -r line || [ -n "$line" ]; do
|
|
1515
|
+
line_number=$((line_number + 1))
|
|
1516
|
+
case "$line" in *[![:space:]]*) ;; *) continue ;; esac
|
|
1517
|
+
status_is_captain_relevant "$line" || continue
|
|
1518
|
+
verb=$(status_line_verb "$line")
|
|
1519
|
+
case "$verb" in
|
|
1520
|
+
needs-decision|blocked)
|
|
1521
|
+
key=$(_fm_decision_key "$line") || {
|
|
1522
|
+
[ -n "$events" ] && events="${events} ; "
|
|
1523
|
+
events="${events}${line}"
|
|
1524
|
+
rc=0
|
|
1525
|
+
continue
|
|
1526
|
+
}
|
|
1527
|
+
_fm_decision_key_transition_allowed "$key" "$(status_line_note "$line")" || {
|
|
1528
|
+
[ -n "$events" ] && events="${events} ; "
|
|
1529
|
+
events="${events}reconciliation-required: ${line}"
|
|
1530
|
+
rc=0
|
|
1531
|
+
continue
|
|
1532
|
+
}
|
|
1533
|
+
if [ "$folded" -eq 0 ]; then
|
|
1534
|
+
_fm_status_read_span "$f" 0 "$size" > "$full_file" 2>/dev/null \
|
|
1535
|
+
|| { failed=1; break; }
|
|
1536
|
+
if [ "$start" -gt 0 ]; then
|
|
1537
|
+
_fm_status_read_span "$full_file" 0 "$start" > "$prefix_file" 2>/dev/null \
|
|
1538
|
+
|| { failed=1; break; }
|
|
1539
|
+
while IFS= read -r _line || [ -n "$_line" ]; do prefix_lines=$((prefix_lines + 1)); done < "$prefix_file"
|
|
1540
|
+
fi
|
|
1541
|
+
origins=$(_fm_status_open_decision_origins "$full_file") || { failed=1; break; }
|
|
1542
|
+
folded=1
|
|
1543
|
+
fi
|
|
1544
|
+
live_line=$(while IFS=$(printf '\t') read -r _key _line; do
|
|
1545
|
+
[ "$_key" = "$key" ] && { printf '%s' "$_line"; break; }
|
|
1546
|
+
done <<EOF
|
|
1547
|
+
$origins
|
|
1548
|
+
EOF
|
|
1549
|
+
)
|
|
1550
|
+
[ -n "$live_line" ] && [ "$((prefix_lines + line_number))" -eq "$live_line" ] || continue
|
|
1551
|
+
[ -n "$events" ] && events="${events} ; "
|
|
1552
|
+
events="${events}${line}"
|
|
1553
|
+
rc=0
|
|
1554
|
+
;;
|
|
1555
|
+
*)
|
|
1556
|
+
[ -n "$events" ] && events="${events} ; "
|
|
1557
|
+
events="${events}${line}"
|
|
1558
|
+
rc=0
|
|
1559
|
+
;;
|
|
1560
|
+
esac
|
|
1561
|
+
done < "$chunk_file"
|
|
1562
|
+
rm -f "$chunk_file" "$full_file" "$prefix_file"
|
|
1563
|
+
[ "$failed" -eq 0 ] || return 2
|
|
1564
|
+
if [ "$rc" -eq 0 ]; then printf '%s\t%s\t%s' "$size" "$ident" "$events"; else printf '%s\t%s' "$size" "$ident"; fi
|
|
1565
|
+
return "$rc"
|
|
1566
|
+
}
|
|
1567
|
+
|
|
1568
|
+
status_span_first_actionable() { # <status-file> <start-offset>
|
|
1569
|
+
local record rc rest
|
|
1570
|
+
record=$(status_span_first_actionable_record "$1" "${2:-0}")
|
|
1571
|
+
rc=$?
|
|
1572
|
+
if [ "$rc" -eq 0 ]; then
|
|
1573
|
+
rest=${record#*$'\t'}
|
|
1574
|
+
printf '%s' "${rest#*$'\t'}"
|
|
1575
|
+
fi
|
|
1576
|
+
return "$rc"
|
|
1577
|
+
}
|
|
1578
|
+
|
|
1579
|
+
status_span_has_actionable() { # <status-file> <start-offset>
|
|
1580
|
+
status_span_first_actionable_record "$1" "${2:-0}" > /dev/null
|
|
1581
|
+
}
|
|
1582
|
+
|
|
1583
|
+
# Classify WHY an idle/stale crew MIGHT be safely absorbed instead of surfaced,
|
|
1584
|
+
# from bin/fm-crew-state.sh's one authoritative current-state line
|
|
1585
|
+
# ("state: <s> · source: <src> · <detail>"). Prints exactly one token:
|
|
1586
|
+
# working - an actively-running no-mistakes step (running/fixing/ci) or a busy
|
|
1587
|
+
# pane; the crew is legitimately mid-work on a static-looking pane
|
|
1588
|
+
# (e.g. waiting on CI);
|
|
1589
|
+
# paused - the crew's authoritative current state is a declared external-wait
|
|
1590
|
+
# pause (paused:), which is EXPECTED to idle;
|
|
1591
|
+
# none - neither, so the wake must surface (a stopped/finished/parked/failed/
|
|
1592
|
+
# torn-down/unknown crew, or an unreadable verdict).
|
|
1593
|
+
# One fm-crew-state.sh read serves BOTH absorb reasons at once. Reading the state
|
|
1594
|
+
# authoritatively (not the status log) is what keeps run-step precedence: a crew
|
|
1595
|
+
# that appended paused: but then STARTED a run reports working, never paused.
|
|
1596
|
+
# NOT a pure read: fm-crew-state.sh may make a bounded no-mistakes call, so callers
|
|
1597
|
+
# run it only on no-verb signal and first-sighting stale paths, never every wake.
|
|
1598
|
+
# FM_CREW_STATE_BIN lets tests stub the verdict.
|
|
1599
|
+
crew_absorb_class() { # <id>
|
|
1600
|
+
local id=$1 line state src
|
|
1601
|
+
[ -n "$id" ] || { printf 'none'; return; }
|
|
1602
|
+
line=$("$FM_CREW_STATE_BIN" "$id" 2>/dev/null) || true
|
|
1603
|
+
case "$line" in state:*) ;; *) printf 'none'; return ;; esac
|
|
1604
|
+
state=${line#state: }; state=${state%% *}
|
|
1605
|
+
if [ "$state" = paused ]; then printf 'paused'; return; fi
|
|
1606
|
+
if [ "$state" = working ]; then
|
|
1607
|
+
src=${line#*source: }; src=${src%% *}
|
|
1608
|
+
case "$src" in run-step|pane) printf 'working'; return ;; esac
|
|
1609
|
+
fi
|
|
1610
|
+
printf 'none'
|
|
1611
|
+
}
|
|
1612
|
+
|
|
1613
|
+
# 0 if crew <id> shows POSITIVE evidence it is still working (crew_absorb_class
|
|
1614
|
+
# reports `working`). This is the "provably working" predicate at the heart of
|
|
1615
|
+
# absorb-only-on-positive-evidence. This is the sole proof for stale wakes and the
|
|
1616
|
+
# shared authoritative proof for no-verb signals. Where a home opts in, fm-watch.sh
|
|
1617
|
+
# may additionally absorb a bare turn-end on bounded pane churn, while every other
|
|
1618
|
+
# failed verdict surfaces
|
|
1619
|
+
# because the crew may be done, waiting on a decision, or wedged. For stale panes
|
|
1620
|
+
# it is checked before trusting the status log so a pre-validation captain-relevant
|
|
1621
|
+
# line does not override an active run. See crew_absorb_class for the exact
|
|
1622
|
+
# working/paused/none decision.
|
|
1623
|
+
crew_is_provably_working() { # <id>
|
|
1624
|
+
[ "$(crew_absorb_class "$1")" = working ]
|
|
1625
|
+
}
|
|
1626
|
+
|
|
1627
|
+
# 0 if crew <id>'s authoritative current state is a declared external-wait pause.
|
|
1628
|
+
# The stale path absorbs such a crew (on a long re-surface cadence) instead of
|
|
1629
|
+
# escalating a possible wedge.
|
|
1630
|
+
crew_is_paused() { # <id>
|
|
1631
|
+
[ "$(crew_absorb_class "$1")" = paused ]
|
|
1632
|
+
}
|
|
1633
|
+
|
|
1634
|
+
# Directories excluded from the worktree write probe below, and the depth it walks.
|
|
1635
|
+
# The excluded set is everything a supervisor read or a package manager can write
|
|
1636
|
+
# without the crew doing any work - .git first, so firstmate's own read-only git
|
|
1637
|
+
# commands against the worktree can never make the probe self-fulfilling - plus the
|
|
1638
|
+
# large generated trees that would make the walk expensive. Both are overridable so
|
|
1639
|
+
# a home with an unusual layout can widen or narrow the probe. The list is a skip
|
|
1640
|
+
# list, so clearing it skips nothing and widens the walk to the whole depth-bounded
|
|
1641
|
+
# tree; it never disables the probe, which would quietly cost the wedge detector a
|
|
1642
|
+
# liveness input on a home that meant to widen it. Defaulted with the plain form so
|
|
1643
|
+
# an explicitly empty value stays empty: clearing the knob in the environment is the
|
|
1644
|
+
# documented way to ask for that wider walk, and treating empty as unset would hand
|
|
1645
|
+
# the default skip list back to exactly the home that asked for more coverage.
|
|
1646
|
+
FM_WORKTREE_WRITE_PRUNE=${FM_WORKTREE_WRITE_PRUNE-'.git node_modules .venv venv __pycache__ .mypy_cache .pytest_cache .ruff_cache .tox target dist build .next .cache vendor'}
|
|
1647
|
+
FM_WORKTREE_WRITE_MAXDEPTH=${FM_WORKTREE_WRITE_MAXDEPTH:-6}
|
|
1648
|
+
|
|
1649
|
+
# Wall-clock seconds the probe's single walk may take. The walk runs synchronously
|
|
1650
|
+
# inside the caller's poll loop at the exact moment an escalation would otherwise
|
|
1651
|
+
# fire, and -xdev keeps it out of a nested mount but cannot help when the worktree
|
|
1652
|
+
# root ITSELF sits on a hung network or container mount; unbounded, such a walk
|
|
1653
|
+
# would wedge the very supervisor that exists to notice a wedge, stalling its
|
|
1654
|
+
# heartbeat instead of escalating. Hitting the bound is a negative outcome like
|
|
1655
|
+
# every other: it reads as no evidence, so the caller's escalation schedule is
|
|
1656
|
+
# untouched and a stall that writes nothing still escalates on the existing
|
|
1657
|
+
# schedule. A value that is not a positive integer is not a bound at all (`timeout
|
|
1658
|
+
# 0` and the perl fallback's `alarm 0` both disable the deadline), so the default
|
|
1659
|
+
# applies instead; the check lives at the point of use so an in-process override
|
|
1660
|
+
# gets it too.
|
|
1661
|
+
FM_WORKTREE_WRITE_TIMEOUT=${FM_WORKTREE_WRITE_TIMEOUT:-10}
|
|
1662
|
+
|
|
1663
|
+
# 0 when some regular file under <id>'s recorded worktree is newer than
|
|
1664
|
+
# <anchor-file>: positive evidence the crew is still producing work even though its
|
|
1665
|
+
# rendered pane has gone quiet. This is the third liveness input the wedge detector
|
|
1666
|
+
# has, after pane quietness and the run step, and it exists because neither of
|
|
1667
|
+
# those can see a crew that is writing source, then tests, then documentation
|
|
1668
|
+
# behind a static pane - the 2026-08-14 case of eight consecutive possible-wedge
|
|
1669
|
+
# escalations against a crew that was demonstrably working the whole time.
|
|
1670
|
+
#
|
|
1671
|
+
# 1 for every other outcome, including an id with no recorded worktree, a worktree
|
|
1672
|
+
# that is gone, a missing anchor, and a walk that fails or finds nothing. Absence of
|
|
1673
|
+
# evidence therefore always leaves the caller's existing escalation schedule
|
|
1674
|
+
# untouched, so a crew that writes nothing still escalates exactly as before.
|
|
1675
|
+
#
|
|
1676
|
+
# A kind=secondmate task records a provisioned firstmate home, not a code tree, and
|
|
1677
|
+
# such a home runs its OWN supervision inside it: its state/ directory churns a
|
|
1678
|
+
# watcher beacon, pane hashes, and heartbeats whether or not the mate is producing
|
|
1679
|
+
# anything, so a walk there would report liveness for a mate that has done nothing.
|
|
1680
|
+
# Those homes are excluded outright rather than by pruning "state", which would also
|
|
1681
|
+
# hide a legitimate source directory of that name in an ordinary worktree. The
|
|
1682
|
+
# exclusion is a negative outcome like any other, so an unproductive mate keeps
|
|
1683
|
+
# escalating on the caller's unchanged schedule.
|
|
1684
|
+
#
|
|
1685
|
+
# The anchor is the caller's own idle-window timer file, whose mtime already marks
|
|
1686
|
+
# when the quiet window opened, so `-newer` needs no clock arithmetic, no temp
|
|
1687
|
+
# file, and no portable mtime-setting. Not a pure status-file read (see the header):
|
|
1688
|
+
# one pruned, depth-bounded, wall-clock-bounded walk per call, which callers must
|
|
1689
|
+
# reach only when they are otherwise about to escalate, never on every poll. A walk
|
|
1690
|
+
# that outlives FM_WORKTREE_WRITE_TIMEOUT is killed and reported as no evidence, so
|
|
1691
|
+
# a hung mount costs the escalation nothing but the bound. -xdev holds that walk to the
|
|
1692
|
+
# worktree's own filesystem rather than descending into a nested network or container
|
|
1693
|
+
# mount, so a write that lands only under such a mount is one more negative outcome.
|
|
1694
|
+
crew_worktree_written_since() { # <id> <state> <anchor-file>
|
|
1695
|
+
local id=$1 state=$2 anchor=$3 wt kind name hit bound
|
|
1696
|
+
local -a names=() prune=()
|
|
1697
|
+
[ -n "$id" ] || return 1
|
|
1698
|
+
[ -f "$anchor" ] || return 1
|
|
1699
|
+
wt=$(grep '^worktree=' "$state/$id.meta" 2>/dev/null | tail -1 | cut -d= -f2- || true)
|
|
1700
|
+
[ -n "$wt" ] && [ -d "$wt" ] || return 1
|
|
1701
|
+
kind=$(grep '^kind=' "$state/$id.meta" 2>/dev/null | tail -1 | cut -d= -f2- || true)
|
|
1702
|
+
[ "$kind" != secondmate ] || return 1
|
|
1703
|
+
if [ -e "$wt/.fm-secondmate-home" ] || [ -L "$wt/.fm-secondmate-home" ]; then
|
|
1704
|
+
return 1
|
|
1705
|
+
fi
|
|
1706
|
+
read -r -a names <<< "$FM_WORKTREE_WRITE_PRUNE"
|
|
1707
|
+
for name in ${names[@]+"${names[@]}"}; do
|
|
1708
|
+
[ "${#prune[@]}" -eq 0 ] || prune+=( -o )
|
|
1709
|
+
prune+=( -name "$name" )
|
|
1710
|
+
done
|
|
1711
|
+
bound=$FM_WORKTREE_WRITE_TIMEOUT
|
|
1712
|
+
case "$bound" in ''|*[!0-9]*|0) bound=10 ;; esac
|
|
1713
|
+
if [ "${#prune[@]}" -gt 0 ]; then
|
|
1714
|
+
hit=$(fm_run_timed "$bound" find "$wt" -xdev -maxdepth "$FM_WORKTREE_WRITE_MAXDEPTH" \
|
|
1715
|
+
\( "${prune[@]}" \) -prune -o -type f -newer "$anchor" -print -quit 2>/dev/null || true)
|
|
1716
|
+
else
|
|
1717
|
+
hit=$(fm_run_timed "$bound" find "$wt" -xdev -maxdepth "$FM_WORKTREE_WRITE_MAXDEPTH" \
|
|
1718
|
+
-type f -newer "$anchor" -print -quit 2>/dev/null || true)
|
|
1719
|
+
fi
|
|
1720
|
+
[ -n "$hit" ]
|
|
1721
|
+
}
|
|
1722
|
+
|
|
1723
|
+
# 0 (benign/absorb) if EVERY task referenced by a no-verb "signal:" wake is provably
|
|
1724
|
+
# working; 1 (actionable/surface) if any is not, or no task can be resolved. Pass the
|
|
1725
|
+
# same space-separated file list the caller classified with the span read above.
|
|
1726
|
+
# Files are mapped to task ids by stripping the .status / .turn-ended suffix;
|
|
1727
|
+
# a no-verb wake with nothing
|
|
1728
|
+
# provably working must surface, so an empty/unresolvable list returns 1.
|
|
1729
|
+
# A kind=secondmate task's .status signal is never absorbable here regardless of
|
|
1730
|
+
# busy evidence: that stream is the mate's routed-reply channel, so every append
|
|
1731
|
+
# is parent-directed content the supervisor must read (a routed reply, a newly
|
|
1732
|
+
# raised decision, a mirrored remote line), and a busy mate agent makes its note
|
|
1733
|
+
# more current, not less deliverable. Scoped to .status files - a mate's bare
|
|
1734
|
+
# turn-ended ping still uses the ordinary provably-working absorb.
|
|
1735
|
+
signal_crew_provably_working() { # <file> ...
|
|
1736
|
+
local f base dir task seen=""
|
|
1737
|
+
for f in "$@"; do
|
|
1738
|
+
base=${f##*/}
|
|
1739
|
+
dir=${f%/*}
|
|
1740
|
+
[ "$dir" != "$f" ] || dir=.
|
|
1741
|
+
case "$base" in
|
|
1742
|
+
*.status) task=${base%.status} ;;
|
|
1743
|
+
*.turn-ended) task=${base%.turn-ended} ;;
|
|
1744
|
+
*) continue ;;
|
|
1745
|
+
esac
|
|
1746
|
+
[ -n "$task" ] || continue
|
|
1747
|
+
case "$base" in
|
|
1748
|
+
*.status)
|
|
1749
|
+
if [ "$(grep '^kind=' "$dir/$task.meta" 2>/dev/null | tail -1 | cut -d= -f2-)" = secondmate ]; then
|
|
1750
|
+
return 1
|
|
1751
|
+
fi
|
|
1752
|
+
;;
|
|
1753
|
+
esac
|
|
1754
|
+
case " $seen " in *" $task "*) continue ;; esac
|
|
1755
|
+
seen="$seen $task"
|
|
1756
|
+
crew_is_provably_working "$task" || return 1
|
|
1757
|
+
done
|
|
1758
|
+
[ -n "$seen" ] || return 1
|
|
1759
|
+
return 0
|
|
1760
|
+
}
|
|
1761
|
+
|
|
1762
|
+
# 0 (terminal/actionable) if a stale window's last status line is
|
|
1763
|
+
# captain-relevant; 1 otherwise, including the no-status case. A 1 only means
|
|
1764
|
+
# "non-terminal"; the always-on watcher then applies crew_is_provably_working,
|
|
1765
|
+
# while the away-mode daemon applies its persistence recheck.
|
|
1766
|
+
stale_is_terminal() { # <window> <state>
|
|
1767
|
+
local win=$1 state=$2 last
|
|
1768
|
+
last=$(last_status_line "$state/$(window_to_task "$win" "$state").status")
|
|
1769
|
+
[ -n "$last" ] && status_is_captain_relevant "$last"
|
|
1770
|
+
}
|