@zerwiz/ymir 0.1.4 → 0.1.7
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/README.md +74 -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 +360 -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.example +72 -0
- package/.agents/config/crew-dispatch.json +22 -0
- package/.agents/config/cron.yaml +11 -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/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/runes_audit.md +5 -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 +121 -0
- package/.agents/migrations/0004-hoard-and-realms.sh +104 -0
- package/.agents/migrations/0005-roots-out-of-tree.sh +158 -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 +159 -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 +736 -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 +539 -0
- package/.agents/skills/galdr-ymirsystem/assets/installation.md +763 -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 +163 -0
- package/.agents/skills/galdr-ymirsystem/assets/nornir-jobs.md +415 -0
- package/.agents/skills/galdr-ymirsystem/assets/norse-naming.md +325 -0
- package/.agents/skills/galdr-ymirsystem/assets/odrerir-hall.md +133 -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 +101 -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 +180 -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 +503 -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 +90 -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 +57 -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 +35 -0
- package/.agents/skills/ymir-host/assets/desktop.md +36 -0
- package/.agents/skills/ymir-host/assets/install.md +69 -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 +498 -0
- package/CHANGELOG.md +1913 -0
- package/LICENSE +1 -1
- package/NOTICE +63 -0
- package/README.md +114 -60
- 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 +97 -0
- package/RULES/05-platforms.md +67 -0
- package/RULES/06-append-only.md +108 -0
- package/RULES/07-config.md +101 -0
- package/RULES/08-delivery-gate.md +75 -0
- package/RULES/09-electron.md +58 -0
- package/RULES/README.md +22 -0
- package/bin/README.md +107 -0
- package/bin/a2a-mcp.sh +133 -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 +256 -0
- package/bin/apodex-smoke-test.sh +105 -0
- package/bin/bifrost-bridge.sh +131 -0
- package/bin/bifrost-ingress.sh +77 -0
- package/bin/bootstrap-macos.sh +112 -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 +51 -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-assemble.sh +154 -0
- package/bin/changelog-guard.sh +197 -0
- package/bin/crash-sense.sh +173 -0
- package/bin/design-check.sh +80 -0
- package/bin/design-icon.sh +153 -0
- package/bin/desktop-place.sh +224 -0
- package/bin/docs-guard.sh +39 -0
- package/bin/editor-place.sh +225 -0
- package/bin/eindri-acclaim.sh +47 -0
- package/bin/eindri-control.sh +28 -0
- package/bin/eindri-role.sh +83 -0
- package/bin/eindri-seat.sh +83 -0
- package/bin/eindri-seen.sh +54 -0
- package/bin/eindri-send.sh +15 -0
- package/bin/eindri-start.sh +98 -0
- package/bin/eindri-watch.sh +57 -0
- package/bin/einherjar-spawn.sh +576 -0
- package/bin/eir-doctor.sh +158 -0
- package/bin/erindi-brief.sh +271 -0
- package/bin/feature-inventory.sh +73 -0
- package/bin/fleet-apply.sh +81 -0
- package/bin/github-deploy.sh +94 -0
- package/bin/gjallarhorn-expose.sh +136 -0
- package/bin/gjallarhorn-notify.sh +71 -0
- package/bin/gjallarhorn-purge.sh +67 -0
- package/bin/gjallarhorn-tunnel.sh +85 -0
- package/bin/gleipnir-lock-lib.sh +284 -0
- package/bin/groa-update.sh +130 -0
- package/bin/hall-snapshot.sh +111 -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 +452 -0
- package/bin/hermes-ensure.sh +88 -0
- package/bin/hlidskjalf-agents.sh +172 -0
- package/bin/hlidskjalf-usage.sh +196 -0
- package/bin/hoard-guard.sh +135 -0
- package/bin/hoard-lib.sh +105 -0
- package/bin/hodd.sh +102 -0
- package/bin/host-sense.sh +199 -0
- package/bin/huginn-research-worker.sh +121 -0
- package/bin/local-model-lock.sh +70 -0
- package/bin/mimir-bridge.py +259 -0
- package/bin/mimir-bridge.sh +100 -0
- package/bin/mimir-ingest.sh +131 -0
- package/bin/mimir-reflect.sh +27 -0
- package/bin/mimir.sh +113 -0
- package/bin/mjollnir-webhook.sh +72 -0
- package/bin/mjollnir.sh +104 -0
- package/bin/model-hardware.sh +96 -0
- package/bin/model-resolve.sh +160 -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-hall-snapshot.sh +28 -0
- package/bin/nornir-job-memory-housekeeping.sh +114 -0
- package/bin/nornir-job-nsr-compliance.sh +54 -0
- package/bin/nornir-job-observer.sh +166 -0
- package/bin/nornir-job-skillopt-sleep.sh +76 -0
- package/bin/npm-publish.sh +136 -0
- package/bin/omarchy-hook-install.sh +60 -0
- package/bin/omarchy-install.sh +161 -0
- package/bin/omarchy-plugins.sh +187 -0
- package/bin/omarchy-sense.sh +162 -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-model-agnostic.sh +71 -0
- package/bin/pi-seat.sh +63 -0
- package/bin/prereq-ensure.sh +166 -0
- package/bin/private-guard.sh +58 -0
- package/bin/project-git.sh +69 -0
- package/bin/public-guard.sh +71 -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 +104 -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 +94 -0
- package/bin/smidja-observe.sh +86 -0
- package/bin/syn-arm-pretool-check.sh +57 -0
- package/bin/syn-asset-pretool-check.sh +89 -0
- package/bin/syn-cd-pretool-check.sh +28 -0
- package/bin/syn-guard-pretool-check.sh +83 -0
- package/bin/syn-spawn-pretool-check.sh +58 -0
- package/bin/syn-turnend-guard.sh +36 -0
- package/bin/syn-watch-arm.sh +99 -0
- package/bin/tailscale-sync.sh +203 -0
- package/bin/telegram-bot.sh +82 -0
- package/bin/toolchain.sh +74 -0
- package/bin/ui-truth-check.sh +75 -0
- package/bin/utgard.sh +111 -0
- package/bin/valhalla.sh +115 -0
- package/bin/valknut-load.sh +298 -0
- package/bin/vor-crew-state.sh +124 -0
- package/bin/wedge-notify.sh +60 -0
- package/bin/workspace-provision.sh +80 -0
- package/bin/workspace-rag.sh +163 -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 +806 -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-plan.sh +356 -0
- package/bin/ymir-platform.sh +348 -0
- package/bin/ymir-say.sh +110 -0
- package/bin/ymir-setup-auth.sh +119 -0
- package/bin/ymir-validate.sh +193 -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 +1060 -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 +54 -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 +269 -0
- package/scripts/lower.sh +34 -0
- package/scripts/raise.sh +42 -0
- package/scripts/start.sh +217 -0
- package/scripts/stop.sh +113 -0
- package/install.sh +0 -35
|
@@ -0,0 +1,1849 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Shared durable wake queue and portable lock helpers.
|
|
3
|
+
|
|
4
|
+
BROKK_WAKE_LIB_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
5
|
+
BROKK_WAKE_DEFAULT_ROOT="$(cd "$BROKK_WAKE_LIB_DIR/.." && pwd)"
|
|
6
|
+
BROKK_ROOT="${BROKK_ROOT_OVERRIDE:-${BROKK_ROOT:-$BROKK_WAKE_DEFAULT_ROOT}}"
|
|
7
|
+
BROKK_HOME="${BROKK_HOME:-${BROKK_ROOT_OVERRIDE:-$BROKK_ROOT}}"
|
|
8
|
+
STATE="${BROKK_STATE_OVERRIDE:-${STATE:-$BROKK_HOME/state}}"
|
|
9
|
+
BROKK_WAKE_QUEUE="${BROKK_WAKE_QUEUE:-$STATE/.wake-queue}"
|
|
10
|
+
BROKK_WAKE_QUEUE_LOCK="${BROKK_WAKE_QUEUE_LOCK:-$STATE/.wake-queue.lock}"
|
|
11
|
+
BROKK_LOCK_STALE_AFTER="${BROKK_LOCK_STALE_AFTER:-2}"
|
|
12
|
+
# Resolved once at source time: fm_pid_identity and fm_path_mtime run inside 0.2s
|
|
13
|
+
# confirm and 0.5s attach polls, and forking uname per call is a measurable cost on
|
|
14
|
+
# the platform (Git Bash/MSYS) that already pays the highest fork price.
|
|
15
|
+
_BROKK_UNAME=$(uname 2>/dev/null || echo unknown)
|
|
16
|
+
mkdir -p "$STATE"
|
|
17
|
+
|
|
18
|
+
# Most wake-library consumers need only queue and lock primitives, including
|
|
19
|
+
# deliberately minimal recovery fixtures and remote installations.
|
|
20
|
+
# Load the classifier only when a status presentation helper is actually used.
|
|
21
|
+
_fm_wake_require_classify() {
|
|
22
|
+
command -v status_observed_signature >/dev/null 2>&1 && return 0
|
|
23
|
+
# shellcheck source=bin/brokk-classify-lib.sh
|
|
24
|
+
. "$BROKK_WAKE_LIB_DIR/brokk-classify-lib.sh"
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
fm_current_pid() {
|
|
28
|
+
printf '%s\n' "${BASHPID:-$$}"
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
fm_pid_alive() {
|
|
32
|
+
local pid=$1
|
|
33
|
+
case "$pid" in
|
|
34
|
+
''|*[!0-9]*) return 1 ;;
|
|
35
|
+
esac
|
|
36
|
+
kill -0 "$pid" 2>/dev/null
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
fm_pid_identity() {
|
|
40
|
+
local pid=$1 out proc_root stat_line starttime cmdline_hex identity_key
|
|
41
|
+
local -a stat_fields
|
|
42
|
+
case "$pid" in
|
|
43
|
+
''|*[!0-9]*) return 1 ;;
|
|
44
|
+
esac
|
|
45
|
+
proc_root=${BROKK_PROC_ROOT_OVERRIDE:-/proc}
|
|
46
|
+
# Prefer a Linux-compatible /proc when present: stat field 22 (starttime, clock ticks since boot) is
|
|
47
|
+
# immune to the wall-clock steps that re-render the ps lstart fallback's date
|
|
48
|
+
# (observed as WSL2 btime drift) and would evict a live watcher; combining the
|
|
49
|
+
# full NUL-separated cmdline keeps PID reuse a mismatch even on a tick collision.
|
|
50
|
+
# Git Bash/MSYS exposes these compatible files but its Cygwin ps rejects the
|
|
51
|
+
# portable fallback's -o fields, so capability detection must not key on uname.
|
|
52
|
+
if [ -r "$proc_root/$pid/stat" ] && [ -r "$proc_root/$pid/cmdline" ]; then
|
|
53
|
+
stat_line=$(cat "$proc_root/$pid/stat" 2>/dev/null) || return 1
|
|
54
|
+
# After the final comm delimiter, array index 19 is proc stat field 22.
|
|
55
|
+
read -r -a stat_fields <<< "${stat_line##*)}"
|
|
56
|
+
[ "${#stat_fields[@]}" -ge 20 ] || return 1
|
|
57
|
+
starttime=${stat_fields[19]}
|
|
58
|
+
case "$starttime" in
|
|
59
|
+
''|*[!0-9]*) return 1 ;;
|
|
60
|
+
esac
|
|
61
|
+
cmdline_hex=$(od -An -v -tx1 "$proc_root/$pid/cmdline" 2>/dev/null | tr -d '[:space:]') || return 1
|
|
62
|
+
[ -n "$cmdline_hex" ] || return 1
|
|
63
|
+
identity_key=proc-starttime
|
|
64
|
+
[ "$_BROKK_UNAME" != Linux ] || identity_key=linux-starttime
|
|
65
|
+
printf '%s=%s cmdline-hex=%s\n' "$identity_key" "$starttime" "$cmdline_hex"
|
|
66
|
+
return 0
|
|
67
|
+
fi
|
|
68
|
+
# Pin LC_ALL=C so lstart's date format is locale-invariant: the identity is
|
|
69
|
+
# written under one locale but re-read under the machine's ambient locale, which
|
|
70
|
+
# would otherwise mismatch on a non-C locale (e.g. ko_KR) and reject a live watcher.
|
|
71
|
+
out=$(LC_ALL=C ps -p "$pid" -o lstart= -o command= 2>/dev/null) || return 1
|
|
72
|
+
[ -n "$out" ] || return 1
|
|
73
|
+
printf '%s\n' "$out" | sed 's/^[[:space:]]*//'
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
fm_path_mtime() {
|
|
77
|
+
if [ "$_BROKK_UNAME" = Darwin ]; then
|
|
78
|
+
stat -f %m "$1" 2>/dev/null
|
|
79
|
+
else
|
|
80
|
+
stat -c %Y "$1" 2>/dev/null
|
|
81
|
+
fi
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
fm_path_age() {
|
|
85
|
+
local path=$1 m
|
|
86
|
+
m=$(fm_path_mtime "$path") || { echo 999999; return; }
|
|
87
|
+
echo $(( $(date +%s) - m ))
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
# fm_watcher_lock_unheld <state>
|
|
91
|
+
# True when the watcher lock or its symlinked owner directory is absent, or when
|
|
92
|
+
# the existing lock records no pid at all. Any non-empty pid remains held here;
|
|
93
|
+
# its syntax, liveness, ownership metadata, and identity are health concerns.
|
|
94
|
+
fm_watcher_lock_unheld() {
|
|
95
|
+
local state=$1 lockdir pid
|
|
96
|
+
lockdir="$state/.watch.lock"
|
|
97
|
+
[ ! -e "$lockdir" ] && return 0
|
|
98
|
+
[ ! -e "$lockdir/pid" ] && return 0
|
|
99
|
+
pid=$(cat "$lockdir/pid" 2>/dev/null) || return 1
|
|
100
|
+
[ -z "$pid" ]
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
BROKK_WATCHER_MATCHED_IDENTITY=
|
|
104
|
+
fm_watcher_lock_matches_pid() {
|
|
105
|
+
local state=$1 watch_path=$2 pid=$3 home=${4:-$BROKK_HOME} lockdir lock_home lock_path lock_identity current_identity
|
|
106
|
+
BROKK_WATCHER_MATCHED_IDENTITY=
|
|
107
|
+
lockdir="$state/.watch.lock"
|
|
108
|
+
lock_home=$(cat "$lockdir/brokk-home" 2>/dev/null || true)
|
|
109
|
+
lock_path=$(cat "$lockdir/watcher-path" 2>/dev/null || true)
|
|
110
|
+
lock_identity=$(cat "$lockdir/pid-identity" 2>/dev/null || true)
|
|
111
|
+
[ "$lock_home" = "$home" ] || return 1
|
|
112
|
+
[ "$lock_path" = "$watch_path" ] || return 1
|
|
113
|
+
[ -n "$lock_identity" ] || return 1
|
|
114
|
+
current_identity=$(fm_pid_identity "$pid") || return 1
|
|
115
|
+
[ "$current_identity" = "$lock_identity" ] || return 1
|
|
116
|
+
BROKK_WATCHER_MATCHED_IDENTITY=$lock_identity
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
BROKK_WATCHER_HEALTHY_PID=
|
|
120
|
+
BROKK_WATCHER_HEALTHY_IDENTITY=
|
|
121
|
+
fm_watcher_healthy() {
|
|
122
|
+
local state=$1 watch_path=$2 grace=${3:-${BROKK_GUARD_GRACE:-300}} home=${4:-$BROKK_HOME} lockdir beat pid identity age
|
|
123
|
+
BROKK_WATCHER_HEALTHY_PID=
|
|
124
|
+
BROKK_WATCHER_HEALTHY_IDENTITY=
|
|
125
|
+
lockdir="$state/.watch.lock"
|
|
126
|
+
beat="$state/.last-watcher-beat"
|
|
127
|
+
pid=$(cat "$lockdir/pid" 2>/dev/null || true)
|
|
128
|
+
fm_pid_alive "$pid" || return 1
|
|
129
|
+
fm_watcher_lock_matches_pid "$state" "$watch_path" "$pid" "$home" || return 1
|
|
130
|
+
identity=$BROKK_WATCHER_MATCHED_IDENTITY
|
|
131
|
+
age=$(fm_path_age "$beat")
|
|
132
|
+
[ "$age" -lt "$grace" ] || return 1
|
|
133
|
+
# shellcheck disable=SC2034 # Read by callers after fm_watcher_healthy returns.
|
|
134
|
+
BROKK_WATCHER_HEALTHY_PID=$pid
|
|
135
|
+
# shellcheck disable=SC2034 # Read by callers after fm_watcher_healthy returns.
|
|
136
|
+
BROKK_WATCHER_HEALTHY_IDENTITY=$identity
|
|
137
|
+
return 0
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
# fm_watcher_healthy above is the PID-STRICT primitive: true only when a live,
|
|
141
|
+
# identity-matched watcher PROCESS holds this home's lock with a fresh beacon. The
|
|
142
|
+
# arm layer (bin/syn-watch-arm.sh, bin/brokk-claude-stop-autoarm.sh) needs exactly
|
|
143
|
+
# that - it decides whether to start, attach to, or replace a real watcher
|
|
144
|
+
# process, so a leftover beacon must never satisfy it. bin/syn-turnend-guard.sh
|
|
145
|
+
# also keeps this strict check because it fires at the turn boundary where the
|
|
146
|
+
# auto-arm brings a fresh watcher up. The pull warning (bin/brokk-guard.sh) fires
|
|
147
|
+
# mid-turn, where the auto-arm model runs no watcher at all, so it wants a
|
|
148
|
+
# different, model-aware question:
|
|
149
|
+
|
|
150
|
+
# fm_supervision_model
|
|
151
|
+
# Print the supervision model of this home's PRIMARY harness:
|
|
152
|
+
# autoarm Claude's Stop-hook auto-arm and Cursor's stop-hook park: the
|
|
153
|
+
# watcher is armed at each turn end and exits on its wake, so it
|
|
154
|
+
# runs only BETWEEN turns. Mid-turn a fresh beacon with no live
|
|
155
|
+
# watcher process is the healthy state.
|
|
156
|
+
# extension Pi (and pi-signed): .pi/extensions/brokk-primary-pi-watch.ts owns
|
|
157
|
+
# continuity. It tears the watcher down on every actionable wake and
|
|
158
|
+
# spawns the replacement itself, so a genuinely unheld singleton lock
|
|
159
|
+
# is healthy during that hand-off only with extension ownership and a
|
|
160
|
+
# fresh beacon. Any held but unhealthy lock remains down.
|
|
161
|
+
# persistent every other harness (codex foreground checkpoint, opencode/grok
|
|
162
|
+
# background arm, tmux, unknown): the watcher runs as a tracked live
|
|
163
|
+
# process, so a live identity-matched pid is the real liveness signal.
|
|
164
|
+
# BROKK_SUPERVISION_MODEL overrides detection (tests, and callers that already know
|
|
165
|
+
# the harness). Otherwise bin/hamr-harness.sh is the single detection owner, so this
|
|
166
|
+
# stays consistent with the harness-specific repair line the guards already emit.
|
|
167
|
+
fm_supervision_model() {
|
|
168
|
+
local harness
|
|
169
|
+
case "${BROKK_SUPERVISION_MODEL:-}" in
|
|
170
|
+
autoarm|extension|persistent) printf '%s\n' "$BROKK_SUPERVISION_MODEL"; return 0 ;;
|
|
171
|
+
esac
|
|
172
|
+
harness=$("$BROKK_WAKE_LIB_DIR/hamr-harness.sh" 2>/dev/null || printf unknown)
|
|
173
|
+
case "$harness" in
|
|
174
|
+
claude|cursor) printf 'autoarm\n' ;;
|
|
175
|
+
pi|pi-signed) printf 'extension\n' ;;
|
|
176
|
+
*) printf 'persistent\n' ;;
|
|
177
|
+
esac
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
# Pi primary supervision evidence. The Pi extensions record, in their state
|
|
181
|
+
# markers, the exact build they loaded and the session process that loaded it, so
|
|
182
|
+
# "a live Pi session owns supervision" is provable from durable state without a
|
|
183
|
+
# watcher process and without reading any vendor-rendered surface.
|
|
184
|
+
#
|
|
185
|
+
# fm_pi_extension_version <file>
|
|
186
|
+
# Print the marker version string the Pi extensions record for <file>. Must stay
|
|
187
|
+
# byte-identical to the "sha256:<hex>" digest .pi/extensions/brokk-primary-pi-watch.ts
|
|
188
|
+
# and .pi/extensions/brokk-primary-turnend-guard.ts compute for themselves; a host
|
|
189
|
+
# with no SHA-256 tool falls back to a form no marker can match, which keeps every
|
|
190
|
+
# consumer loud rather than silently satisfied.
|
|
191
|
+
fm_pi_extension_version() {
|
|
192
|
+
local file=$1
|
|
193
|
+
[ -f "$file" ] || return 1
|
|
194
|
+
if command -v shasum >/dev/null 2>&1; then
|
|
195
|
+
shasum -a 256 "$file" | awk '{print "sha256:" $1}'
|
|
196
|
+
elif command -v sha256sum >/dev/null 2>&1; then
|
|
197
|
+
sha256sum "$file" | awk '{print "sha256:" $1}'
|
|
198
|
+
else
|
|
199
|
+
cksum "$file" | awk '{print "cksum:" $1 ":" $2}'
|
|
200
|
+
fi
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
# fm_pi_extension_loaded <marker> <expected-version> <session-lock>
|
|
204
|
+
# True when <marker> records <expected-version> and names the session process in
|
|
205
|
+
# <session-lock>, i.e. the session holding this home loaded exactly this build.
|
|
206
|
+
fm_pi_extension_loaded() {
|
|
207
|
+
local marker=$1 expected_version=$2 lock=$3 marker_version marker_pid lock_pid
|
|
208
|
+
[ -f "$marker" ] && [ -f "$lock" ] && [ -n "$expected_version" ] || return 1
|
|
209
|
+
marker_version=$(sed -n '1p' "$marker")
|
|
210
|
+
marker_pid=$(sed -n '2p' "$marker")
|
|
211
|
+
lock_pid=$(sed -n '1p' "$lock")
|
|
212
|
+
[ -n "$marker_pid" ] || return 1
|
|
213
|
+
[ "$marker_version" = "$expected_version" ] && [ "$marker_pid" = "$lock_pid" ]
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
# fm_pi_extension_owns_supervision <state> <root>
|
|
217
|
+
# True when a LIVE Pi session owns supervision continuity for this home: both
|
|
218
|
+
# primary extensions are loaded at their current on-disk builds by the process
|
|
219
|
+
# recorded in this home's session lock, and that process is still alive.
|
|
220
|
+
# Requiring the turn-end guard extension too is deliberate - it is the structural
|
|
221
|
+
# backstop that catches a cycle the watch extension failed to restore, so a home
|
|
222
|
+
# missing it has no benign hand-off to tolerate.
|
|
223
|
+
fm_pi_extension_owns_supervision() {
|
|
224
|
+
local state=$1 root=$2 lock session_pid pair source marker version
|
|
225
|
+
lock="$state/.lock"
|
|
226
|
+
for pair in \
|
|
227
|
+
"brokk-primary-pi-watch.ts:.pi-watch-extension-loaded" \
|
|
228
|
+
"brokk-primary-turnend-guard.ts:.pi-turnend-extension-loaded"; do
|
|
229
|
+
source=${pair%%:*}
|
|
230
|
+
marker=${pair#*:}
|
|
231
|
+
version=$(fm_pi_extension_version "$root/.pi/extensions/$source") || return 1
|
|
232
|
+
fm_pi_extension_loaded "$state/$marker" "$version" "$lock" || return 1
|
|
233
|
+
done
|
|
234
|
+
session_pid=$(sed -n '1p' "$lock" 2>/dev/null)
|
|
235
|
+
fm_pid_alive "$session_pid"
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
# fm_watcher_supervision_verdict <state> <watch-path> [grace] [home] [root]
|
|
239
|
+
# Model-aware "is supervision healthy right now" verdict for the pull warning
|
|
240
|
+
# guard (bin/brokk-guard.sh), NOT the arm layer or the turn-end guard. Sets:
|
|
241
|
+
# BROKK_WATCHER_VERDICT_OK true when supervision is healthy for this model
|
|
242
|
+
# BROKK_WATCHER_VERDICT_REASON when not ok, the true failing condition:
|
|
243
|
+
# no-watcher - a live watcher process is the real
|
|
244
|
+
# signal for this model but none holds
|
|
245
|
+
# the lock (the beacon is still fresh)
|
|
246
|
+
# stale-beacon - the beacon is stale beyond grace or
|
|
247
|
+
# absent (a genuine supervision lapse)
|
|
248
|
+
# autoarm: a fresh beacon within grace is healthy even with no live watcher,
|
|
249
|
+
# because the watcher only runs between turns; only a stale beacon is a lapse.
|
|
250
|
+
# extension: a live identity-matched watcher is the ordinary healthy state, but a
|
|
251
|
+
# genuinely unheld lock is also healthy while the beacon is fresh AND a live Pi
|
|
252
|
+
# session provably owns continuity (fm_pi_extension_owns_supervision) - that is the
|
|
253
|
+
# extension's own tear-down-and-respawn hand-off, which it retries and escalates
|
|
254
|
+
# itself. A lock with any recorded pid remains down if the strict health check fails.
|
|
255
|
+
# Without ownership proof an unheld lock is down exactly as before, so an unloaded,
|
|
256
|
+
# version-drifted, or exited Pi session still alarms immediately, and a cycle the
|
|
257
|
+
# extension never restores still alarms once the beacon passes grace.
|
|
258
|
+
# persistent: require a live identity-matched watcher with a fresh beacon
|
|
259
|
+
# (fm_watcher_healthy); a fresh leftover beacon with no live watcher is still down.
|
|
260
|
+
# shellcheck disable=SC2034 # Read by callers after the function returns.
|
|
261
|
+
BROKK_WATCHER_VERDICT_OK=false
|
|
262
|
+
# shellcheck disable=SC2034 # Read by callers after the function returns.
|
|
263
|
+
BROKK_WATCHER_VERDICT_REASON=stale-beacon
|
|
264
|
+
fm_watcher_supervision_verdict() {
|
|
265
|
+
local state=$1 watch=$2 grace=${3:-${BROKK_GUARD_GRACE:-300}} home=${4:-$BROKK_HOME}
|
|
266
|
+
local root=${5:-$BROKK_ROOT}
|
|
267
|
+
local beat age fresh=false model
|
|
268
|
+
BROKK_WATCHER_VERDICT_OK=false
|
|
269
|
+
BROKK_WATCHER_VERDICT_REASON=stale-beacon
|
|
270
|
+
beat="$state/.last-watcher-beat"
|
|
271
|
+
age=$(fm_path_age "$beat")
|
|
272
|
+
case "$age" in
|
|
273
|
+
''|*[!0-9]*) ;;
|
|
274
|
+
*) [ "$age" -lt "$grace" ] && fresh=true ;;
|
|
275
|
+
esac
|
|
276
|
+
model=$(fm_supervision_model)
|
|
277
|
+
if [ "$model" = autoarm ]; then
|
|
278
|
+
[ "$fresh" = true ] && BROKK_WATCHER_VERDICT_OK=true
|
|
279
|
+
return 0
|
|
280
|
+
fi
|
|
281
|
+
if fm_watcher_healthy "$state" "$watch" "$grace" "$home"; then
|
|
282
|
+
# shellcheck disable=SC2034 # Read by callers after the function returns.
|
|
283
|
+
BROKK_WATCHER_VERDICT_OK=true
|
|
284
|
+
elif [ "$fresh" = true ]; then
|
|
285
|
+
if [ "$model" = extension ] && fm_watcher_lock_unheld "$state" \
|
|
286
|
+
&& fm_pi_extension_owns_supervision "$state" "$root"; then
|
|
287
|
+
# shellcheck disable=SC2034 # Read by callers after the function returns.
|
|
288
|
+
BROKK_WATCHER_VERDICT_OK=true
|
|
289
|
+
else
|
|
290
|
+
# shellcheck disable=SC2034 # Read by callers after the function returns.
|
|
291
|
+
BROKK_WATCHER_VERDICT_REASON=no-watcher
|
|
292
|
+
fi
|
|
293
|
+
fi
|
|
294
|
+
return 0
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
fm_lock_clean_known_files() {
|
|
298
|
+
local lockdir=$1
|
|
299
|
+
rm -f \
|
|
300
|
+
"$lockdir/pid" \
|
|
301
|
+
"$lockdir/brokk-home" \
|
|
302
|
+
"$lockdir/pid-identity" \
|
|
303
|
+
"$lockdir/role" \
|
|
304
|
+
"$lockdir/watcher-path" \
|
|
305
|
+
2>/dev/null || true
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
fm_lock_set_role() {
|
|
309
|
+
local lockdir=$1 role=$2 current pid back
|
|
310
|
+
case "$role" in
|
|
311
|
+
autoarm|terminal-check) : ;;
|
|
312
|
+
*) return 1 ;;
|
|
313
|
+
esac
|
|
314
|
+
current=${BASHPID:-$$}
|
|
315
|
+
pid=$(cat "$lockdir/pid" 2>/dev/null || true)
|
|
316
|
+
[ "$pid" = "$current" ] || return 1
|
|
317
|
+
printf '%s\n' "$role" > "$lockdir/role" 2>/dev/null || return 1
|
|
318
|
+
back=$(cat "$lockdir/role" 2>/dev/null || true)
|
|
319
|
+
[ "$back" = "$role" ]
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
fm_lock_role() {
|
|
323
|
+
cat "$1/role" 2>/dev/null
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
fm_lock_abs_path() {
|
|
327
|
+
local path=$1 dir base
|
|
328
|
+
dir=$(dirname "$path")
|
|
329
|
+
base=$(basename "$path")
|
|
330
|
+
dir=$(cd "$dir" 2>/dev/null && pwd -P) || return 1
|
|
331
|
+
printf '%s/%s\n' "$dir" "$base"
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
fm_lock_owner_dir() {
|
|
335
|
+
local lockdir=$1 lock_abs
|
|
336
|
+
lock_abs=$(fm_lock_abs_path "$lockdir") || return 1
|
|
337
|
+
mktemp -d "${lock_abs}.owner.XXXXXX" 2>/dev/null
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
fm_lock_prepare_owner() {
|
|
341
|
+
local ownerdir=$1 mypid back
|
|
342
|
+
mypid=${BASHPID:-$$}
|
|
343
|
+
printf '%s\n' "$mypid" > "$ownerdir/pid" 2>/dev/null || return 1
|
|
344
|
+
back=$(cat "$ownerdir/pid" 2>/dev/null || true)
|
|
345
|
+
[ "$back" = "$mypid" ]
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
fm_lock_link_owner() {
|
|
349
|
+
local lockdir=$1 owner
|
|
350
|
+
owner=$(readlink "$lockdir" 2>/dev/null) || return 1
|
|
351
|
+
[ -n "$owner" ] || return 1
|
|
352
|
+
case "$owner" in
|
|
353
|
+
/*) printf '%s\n' "$owner" ;;
|
|
354
|
+
*) printf '%s/%s\n' "$(dirname "$lockdir")" "$owner" ;;
|
|
355
|
+
esac
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
fm_lock_points_to_owner() {
|
|
359
|
+
local lockdir=$1 ownerdir=$2 actual
|
|
360
|
+
actual=$(readlink "$lockdir" 2>/dev/null) || return 1
|
|
361
|
+
[ "$actual" = "$ownerdir" ]
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
fm_lock_discard_owner() {
|
|
365
|
+
local ownerdir=$1
|
|
366
|
+
[ -n "$ownerdir" ] || return 0
|
|
367
|
+
fm_lock_clean_known_files "$ownerdir"
|
|
368
|
+
rmdir "$ownerdir" 2>/dev/null || true
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
fm_lock_remove_stray_owner_link() {
|
|
372
|
+
local lockdir=$1 ownerdir=$2 stray
|
|
373
|
+
stray="$lockdir/$(basename "$ownerdir")"
|
|
374
|
+
if [ -L "$stray" ] && [ "$(readlink "$stray" 2>/dev/null || true)" = "$ownerdir" ]; then
|
|
375
|
+
rm -f "$stray" 2>/dev/null || true
|
|
376
|
+
fi
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
fm_lock_claim_blocked_by_steal() {
|
|
380
|
+
local lockdir=$1 allowed_steal_owner=${2:-} steal
|
|
381
|
+
steal="$lockdir.steal"
|
|
382
|
+
[ -e "$steal" ] || [ -L "$steal" ] || return 1
|
|
383
|
+
if [ -n "$allowed_steal_owner" ] && fm_lock_points_to_owner "$steal" "$allowed_steal_owner"; then
|
|
384
|
+
return 1
|
|
385
|
+
fi
|
|
386
|
+
return 0
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
fm_lock_claim() {
|
|
390
|
+
local lockdir=$1 ownerdir=$2 allowed_steal_owner=${3:-} mypid back
|
|
391
|
+
mypid=${BASHPID:-$$}
|
|
392
|
+
if ! { printf '%s\n' "$mypid" > "$ownerdir/pid"; } 2>/dev/null; then
|
|
393
|
+
fm_lock_discard_owner "$ownerdir"
|
|
394
|
+
return 1
|
|
395
|
+
fi
|
|
396
|
+
back=$(cat "$ownerdir/pid" 2>/dev/null || true)
|
|
397
|
+
if [ "$back" != "$mypid" ]; then
|
|
398
|
+
fm_lock_discard_owner "$ownerdir"
|
|
399
|
+
return 1
|
|
400
|
+
fi
|
|
401
|
+
if ! fm_lock_points_to_owner "$lockdir" "$ownerdir"; then
|
|
402
|
+
fm_lock_discard_owner "$ownerdir"
|
|
403
|
+
return 1
|
|
404
|
+
fi
|
|
405
|
+
if fm_lock_claim_blocked_by_steal "$lockdir" "$allowed_steal_owner"; then
|
|
406
|
+
if fm_lock_points_to_owner "$lockdir" "$ownerdir"; then
|
|
407
|
+
rm -f "$lockdir" 2>/dev/null || true
|
|
408
|
+
fi
|
|
409
|
+
fm_lock_discard_owner "$ownerdir"
|
|
410
|
+
return 1
|
|
411
|
+
fi
|
|
412
|
+
return 0
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
fm_lock_try_create() {
|
|
416
|
+
local lockdir=$1 allowed_steal_owner=${2:-} ownerdir
|
|
417
|
+
BROKK_LOCK_OWNER_DIR=
|
|
418
|
+
ownerdir=$(fm_lock_owner_dir "$lockdir") || return 1
|
|
419
|
+
if [ -e "$lockdir" ] || [ -L "$lockdir" ]; then
|
|
420
|
+
fm_lock_discard_owner "$ownerdir"
|
|
421
|
+
return 1
|
|
422
|
+
fi
|
|
423
|
+
if ! fm_lock_prepare_owner "$ownerdir"; then
|
|
424
|
+
fm_lock_discard_owner "$ownerdir"
|
|
425
|
+
return 1
|
|
426
|
+
fi
|
|
427
|
+
if ln -s "$ownerdir" "$lockdir" 2>/dev/null && fm_lock_points_to_owner "$lockdir" "$ownerdir"; then
|
|
428
|
+
if fm_lock_claim "$lockdir" "$ownerdir" "$allowed_steal_owner"; then
|
|
429
|
+
BROKK_LOCK_OWNER_DIR=$ownerdir
|
|
430
|
+
return 0
|
|
431
|
+
fi
|
|
432
|
+
if fm_lock_points_to_owner "$lockdir" "$ownerdir"; then
|
|
433
|
+
rm -f "$lockdir" 2>/dev/null || true
|
|
434
|
+
fi
|
|
435
|
+
else
|
|
436
|
+
fm_lock_remove_stray_owner_link "$lockdir" "$ownerdir"
|
|
437
|
+
fi
|
|
438
|
+
fm_lock_discard_owner "$ownerdir"
|
|
439
|
+
return 1
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
fm_lock_remove_path() {
|
|
443
|
+
local lockdir=$1 ownerdir
|
|
444
|
+
if [ -L "$lockdir" ]; then
|
|
445
|
+
ownerdir=$(fm_lock_link_owner "$lockdir" 2>/dev/null || true)
|
|
446
|
+
rm -f "$lockdir" 2>/dev/null || return 1
|
|
447
|
+
[ -n "$ownerdir" ] && fm_lock_discard_owner "$ownerdir"
|
|
448
|
+
return 0
|
|
449
|
+
fi
|
|
450
|
+
fm_lock_clean_known_files "$lockdir"
|
|
451
|
+
rmdir "$lockdir" 2>/dev/null
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
fm_lock_mid_acquire_is_fresh() {
|
|
455
|
+
local lockdir=$1 pid=$2 mid_acquire_stale
|
|
456
|
+
case "$pid" in
|
|
457
|
+
''|*[!0-9]*)
|
|
458
|
+
mid_acquire_stale=$BROKK_LOCK_STALE_AFTER
|
|
459
|
+
[ "$mid_acquire_stale" -lt 2 ] && mid_acquire_stale=2
|
|
460
|
+
[ "$(fm_path_age "$lockdir")" -lt "$mid_acquire_stale" ]
|
|
461
|
+
return
|
|
462
|
+
;;
|
|
463
|
+
esac
|
|
464
|
+
return 1
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
fm_lock_recheck_stale_owner() {
|
|
468
|
+
local lockdir=$1 expected_owner=$2 expected_pid=$3 actual_pid
|
|
469
|
+
if [ -n "$expected_owner" ]; then
|
|
470
|
+
fm_lock_points_to_owner "$lockdir" "$expected_owner" || return 1
|
|
471
|
+
elif [ -e "$lockdir" ] || [ -L "$lockdir" ]; then
|
|
472
|
+
[ -d "$lockdir" ] && [ ! -L "$lockdir" ] || return 1
|
|
473
|
+
fi
|
|
474
|
+
actual_pid=$(cat "$lockdir/pid" 2>/dev/null || true)
|
|
475
|
+
[ "$actual_pid" = "$expected_pid" ] || return 1
|
|
476
|
+
if fm_pid_alive "$actual_pid"; then
|
|
477
|
+
return 1
|
|
478
|
+
fi
|
|
479
|
+
if fm_lock_mid_acquire_is_fresh "$lockdir" "$actual_pid"; then
|
|
480
|
+
return 1
|
|
481
|
+
fi
|
|
482
|
+
return 0
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
BROKK_RECOVERY_MARKER_TOKEN=
|
|
486
|
+
BROKK_RECOVERY_MARKER_ACTION='none'
|
|
487
|
+
|
|
488
|
+
# Token grammar (one owner): <pending|announced|acked>:<handling|downtime>:<generation>
|
|
489
|
+
# docs/watcher-continuity.md owns the recovery-episode contract, including the
|
|
490
|
+
# once-per-generation announcement rule for unacknowledged downtime.
|
|
491
|
+
fm_recovery_marker_read() {
|
|
492
|
+
local marker=$1 line count
|
|
493
|
+
BROKK_RECOVERY_MARKER_TOKEN=
|
|
494
|
+
[ -f "$marker" ] && [ ! -L "$marker" ] || return 1
|
|
495
|
+
count=$(wc -l < "$marker" 2>/dev/null | tr -d '[:space:]') || return 1
|
|
496
|
+
[ "$count" = 1 ] || return 1
|
|
497
|
+
IFS= read -r line < "$marker" || return 1
|
|
498
|
+
case "$line" in
|
|
499
|
+
pending:handling:*|pending:downtime:*|announced:handling:*|announced:downtime:*|acked:handling:*|acked:downtime:*) ;;
|
|
500
|
+
*) return 1 ;;
|
|
501
|
+
esac
|
|
502
|
+
case "${line##*:}" in
|
|
503
|
+
''|*[!A-Za-z0-9._-]*) return 1 ;;
|
|
504
|
+
esac
|
|
505
|
+
BROKK_RECOVERY_MARKER_TOKEN=$line
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
_fm_atomic_replace() {
|
|
509
|
+
mv -f -- "$1" "$2"
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
_fm_recovery_marker_write_locked() {
|
|
513
|
+
local marker=$1 kind=$2 generation=${3:-} status=${4:-pending} tmp
|
|
514
|
+
case "$kind" in handling|downtime) ;; *) return 1 ;; esac
|
|
515
|
+
case "$status" in pending|announced) ;; *) return 1 ;; esac
|
|
516
|
+
tmp=$(mktemp "${marker}.tmp.XXXXXX") || return 1
|
|
517
|
+
[ -n "$generation" ] || generation="$(fm_current_pid).$(date +%s).${tmp##*.}"
|
|
518
|
+
if ! printf '%s:%s:%s\n' "$status" "$kind" "$generation" > "$tmp" \
|
|
519
|
+
|| ! chmod 0600 "$tmp" \
|
|
520
|
+
|| ! _fm_atomic_replace "$tmp" "$marker"; then
|
|
521
|
+
rm -f -- "$tmp"
|
|
522
|
+
return 1
|
|
523
|
+
fi
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
# Preserve a pending or announced episode's generation across downtime
|
|
527
|
+
# republication so its outstanding acknowledgement remains usable, and keep an
|
|
528
|
+
# already-announced generation announced so it cannot be re-presented until a
|
|
529
|
+
# new down stretch mints a new generation.
|
|
530
|
+
# docs/watcher-continuity.md owns the recovery contract and sequence-safety rationale.
|
|
531
|
+
_fm_recovery_marker_publish() {
|
|
532
|
+
local marker=$1 kind=${2:-downtime} lock saved_token generation='' status=pending
|
|
533
|
+
case "$kind" in handling|downtime) ;; *) return 1 ;; esac
|
|
534
|
+
lock="${marker}.lock"
|
|
535
|
+
fm_lock_acquire_wait "$lock" || return 1
|
|
536
|
+
if [ -d "$marker" ] && [ ! -L "$marker" ]; then
|
|
537
|
+
fm_lock_release "$lock"
|
|
538
|
+
return 1
|
|
539
|
+
fi
|
|
540
|
+
if [ "$kind" = downtime ]; then
|
|
541
|
+
# Read inline rather than in a command substitution: this runs inside the
|
|
542
|
+
# marker-lock critical section, so it must not add a subshell fork there.
|
|
543
|
+
# The token is restored because publishing owns no snapshot of its own.
|
|
544
|
+
saved_token=$BROKK_RECOVERY_MARKER_TOKEN
|
|
545
|
+
if fm_recovery_marker_read "$marker"; then
|
|
546
|
+
case "$BROKK_RECOVERY_MARKER_TOKEN" in
|
|
547
|
+
pending:handling:*|pending:downtime:*)
|
|
548
|
+
generation=${BROKK_RECOVERY_MARKER_TOKEN##*:}
|
|
549
|
+
status=pending
|
|
550
|
+
;;
|
|
551
|
+
announced:handling:*|announced:downtime:*)
|
|
552
|
+
generation=${BROKK_RECOVERY_MARKER_TOKEN##*:}
|
|
553
|
+
status=announced
|
|
554
|
+
;;
|
|
555
|
+
esac
|
|
556
|
+
fi
|
|
557
|
+
BROKK_RECOVERY_MARKER_TOKEN=$saved_token
|
|
558
|
+
fi
|
|
559
|
+
if ! _fm_recovery_marker_write_locked "$marker" "$kind" "$generation" "$status"; then
|
|
560
|
+
fm_lock_release "$lock"
|
|
561
|
+
return 1
|
|
562
|
+
fi
|
|
563
|
+
fm_lock_release "$lock"
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
_fm_recovery_marker_begin_handling() {
|
|
567
|
+
local marker=$1 expected_generation=${2:-} lock line generation
|
|
568
|
+
lock="${marker}.lock"
|
|
569
|
+
fm_lock_acquire_wait "$lock" || return 1
|
|
570
|
+
if ! fm_recovery_marker_read "$marker"; then
|
|
571
|
+
fm_lock_release "$lock"
|
|
572
|
+
return 1
|
|
573
|
+
fi
|
|
574
|
+
line=$BROKK_RECOVERY_MARKER_TOKEN
|
|
575
|
+
generation=${line##*:}
|
|
576
|
+
if [ -n "$expected_generation" ] && [ "$generation" != "$expected_generation" ]; then
|
|
577
|
+
fm_lock_release "$lock"
|
|
578
|
+
return 3
|
|
579
|
+
fi
|
|
580
|
+
case "$line" in
|
|
581
|
+
pending:handling:*|announced:handling:*) ;;
|
|
582
|
+
pending:downtime:*)
|
|
583
|
+
if ! _fm_recovery_marker_write_locked "$marker" handling "$generation"; then
|
|
584
|
+
fm_lock_release "$lock"
|
|
585
|
+
return 1
|
|
586
|
+
fi
|
|
587
|
+
BROKK_RECOVERY_MARKER_TOKEN="pending:handling:$generation"
|
|
588
|
+
;;
|
|
589
|
+
announced:downtime:*)
|
|
590
|
+
if ! _fm_recovery_marker_write_locked "$marker" handling "$generation" announced; then
|
|
591
|
+
fm_lock_release "$lock"
|
|
592
|
+
return 1
|
|
593
|
+
fi
|
|
594
|
+
BROKK_RECOVERY_MARKER_TOKEN="announced:handling:$generation"
|
|
595
|
+
;;
|
|
596
|
+
*) fm_lock_release "$lock"; return 1 ;;
|
|
597
|
+
esac
|
|
598
|
+
fm_lock_release "$lock"
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
fm_recovery_marker_snapshot() {
|
|
602
|
+
local marker=$1 lock
|
|
603
|
+
BROKK_RECOVERY_MARKER_TOKEN=
|
|
604
|
+
lock="${marker}.lock"
|
|
605
|
+
fm_lock_acquire_wait "$lock" || return 1
|
|
606
|
+
fm_recovery_marker_read "$marker" || true
|
|
607
|
+
fm_lock_release "$lock"
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
_fm_recovery_marker_ack() {
|
|
611
|
+
local marker=$1 expected_generation=$2 lock tmp line
|
|
612
|
+
[ -n "$expected_generation" ] || return 2
|
|
613
|
+
lock="${marker}.lock"
|
|
614
|
+
fm_lock_acquire_wait "$lock" || return 1
|
|
615
|
+
if ! fm_recovery_marker_read "$marker" \
|
|
616
|
+
|| [ "${BROKK_RECOVERY_MARKER_TOKEN##*:}" != "$expected_generation" ]; then
|
|
617
|
+
fm_lock_release "$lock"
|
|
618
|
+
return 3
|
|
619
|
+
fi
|
|
620
|
+
line=$BROKK_RECOVERY_MARKER_TOKEN
|
|
621
|
+
case "$line" in
|
|
622
|
+
pending:*|announced:*) line="acked:${line#*:}" ;;
|
|
623
|
+
acked:*) fm_lock_release "$lock"; return 0 ;;
|
|
624
|
+
*) fm_lock_release "$lock"; return 1 ;;
|
|
625
|
+
esac
|
|
626
|
+
tmp=$(mktemp "${marker}.tmp.XXXXXX") || { fm_lock_release "$lock"; return 1; }
|
|
627
|
+
if ! printf '%s\n' "$line" > "$tmp" \
|
|
628
|
+
|| ! chmod 0600 "$tmp" \
|
|
629
|
+
|| ! mv -f -- "$tmp" "$marker"; then
|
|
630
|
+
rm -f -- "$tmp"
|
|
631
|
+
fm_lock_release "$lock"
|
|
632
|
+
return 1
|
|
633
|
+
fi
|
|
634
|
+
fm_lock_release "$lock"
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
_fm_recovery_marker_arm_check() {
|
|
638
|
+
local marker=$1 lock line quarantine
|
|
639
|
+
BROKK_RECOVERY_MARKER_ACTION='none'
|
|
640
|
+
lock="${marker}.lock"
|
|
641
|
+
fm_lock_acquire_wait "$BROKK_WAKE_QUEUE_LOCK" || return 1
|
|
642
|
+
if ! fm_lock_acquire_wait "$lock"; then
|
|
643
|
+
fm_lock_release "$BROKK_WAKE_QUEUE_LOCK"
|
|
644
|
+
return 1
|
|
645
|
+
fi
|
|
646
|
+
if [ ! -e "$marker" ] && [ ! -L "$marker" ]; then
|
|
647
|
+
if [ -s "$BROKK_WAKE_QUEUE" ]; then
|
|
648
|
+
if ! _fm_recovery_marker_write_locked "$marker" downtime "" announced; then
|
|
649
|
+
fm_lock_release "$lock"
|
|
650
|
+
fm_lock_release "$BROKK_WAKE_QUEUE_LOCK"
|
|
651
|
+
return 1
|
|
652
|
+
fi
|
|
653
|
+
BROKK_RECOVERY_MARKER_ACTION='recover'
|
|
654
|
+
fi
|
|
655
|
+
fm_lock_release "$lock"
|
|
656
|
+
fm_lock_release "$BROKK_WAKE_QUEUE_LOCK"
|
|
657
|
+
return 0
|
|
658
|
+
fi
|
|
659
|
+
if ! fm_recovery_marker_read "$marker"; then
|
|
660
|
+
quarantine=$(mktemp -d "${marker}.invalid.XXXXXX") \
|
|
661
|
+
|| {
|
|
662
|
+
fm_lock_release "$lock"
|
|
663
|
+
fm_lock_release "$BROKK_WAKE_QUEUE_LOCK"
|
|
664
|
+
return 1
|
|
665
|
+
}
|
|
666
|
+
if ! mv -- "$marker" "$quarantine/marker" \
|
|
667
|
+
|| ! _fm_recovery_marker_write_locked "$marker" downtime "" announced; then
|
|
668
|
+
rmdir "$quarantine" 2>/dev/null || true
|
|
669
|
+
fm_lock_release "$lock"
|
|
670
|
+
fm_lock_release "$BROKK_WAKE_QUEUE_LOCK"
|
|
671
|
+
return 1
|
|
672
|
+
fi
|
|
673
|
+
BROKK_RECOVERY_MARKER_ACTION='recover'
|
|
674
|
+
fm_lock_release "$lock"
|
|
675
|
+
fm_lock_release "$BROKK_WAKE_QUEUE_LOCK"
|
|
676
|
+
return 0
|
|
677
|
+
fi
|
|
678
|
+
line=$BROKK_RECOVERY_MARKER_TOKEN
|
|
679
|
+
case "$line" in
|
|
680
|
+
pending:handling:*|announced:handling:*|announced:downtime:*)
|
|
681
|
+
BROKK_RECOVERY_MARKER_ACTION='wait'
|
|
682
|
+
fm_lock_release "$lock"
|
|
683
|
+
fm_lock_release "$BROKK_WAKE_QUEUE_LOCK"
|
|
684
|
+
return 0
|
|
685
|
+
;;
|
|
686
|
+
pending:downtime:*)
|
|
687
|
+
if ! _fm_recovery_marker_write_locked "$marker" downtime "${line##*:}" announced; then
|
|
688
|
+
fm_lock_release "$lock"
|
|
689
|
+
fm_lock_release "$BROKK_WAKE_QUEUE_LOCK"
|
|
690
|
+
return 1
|
|
691
|
+
fi
|
|
692
|
+
BROKK_RECOVERY_MARKER_TOKEN="announced:downtime:${line##*:}"
|
|
693
|
+
BROKK_RECOVERY_MARKER_ACTION='recover'
|
|
694
|
+
;;
|
|
695
|
+
acked:*)
|
|
696
|
+
if [ -s "$BROKK_WAKE_QUEUE" ]; then
|
|
697
|
+
if ! _fm_recovery_marker_write_locked "$marker" downtime "" announced; then
|
|
698
|
+
fm_lock_release "$lock"
|
|
699
|
+
fm_lock_release "$BROKK_WAKE_QUEUE_LOCK"
|
|
700
|
+
return 1
|
|
701
|
+
fi
|
|
702
|
+
# shellcheck disable=SC2034 # Output read by callers after this function returns.
|
|
703
|
+
BROKK_RECOVERY_MARKER_ACTION='recover'
|
|
704
|
+
fi
|
|
705
|
+
;;
|
|
706
|
+
esac
|
|
707
|
+
fm_lock_release "$lock"
|
|
708
|
+
fm_lock_release "$BROKK_WAKE_QUEUE_LOCK"
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
# A non-successor watcher start after an announced-but-unacked episode is a new
|
|
712
|
+
# down stretch: mint a fresh pending generation so a still-open decision or
|
|
713
|
+
# buried note can be presented once more. Handling successors must not call
|
|
714
|
+
# this, because Option B re-arm is not a new down stretch.
|
|
715
|
+
_fm_recovery_marker_reopen_announced() {
|
|
716
|
+
local marker=$1 lock
|
|
717
|
+
lock="${marker}.lock"
|
|
718
|
+
fm_lock_acquire_wait "$lock" || return 1
|
|
719
|
+
if ! fm_recovery_marker_read "$marker"; then
|
|
720
|
+
fm_lock_release "$lock"
|
|
721
|
+
return 0
|
|
722
|
+
fi
|
|
723
|
+
case "$BROKK_RECOVERY_MARKER_TOKEN" in
|
|
724
|
+
announced:*)
|
|
725
|
+
if ! _fm_recovery_marker_write_locked "$marker" downtime ""; then
|
|
726
|
+
fm_lock_release "$lock"
|
|
727
|
+
return 1
|
|
728
|
+
fi
|
|
729
|
+
;;
|
|
730
|
+
esac
|
|
731
|
+
fm_lock_release "$lock"
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
fm_recovery_transition() {
|
|
735
|
+
local marker=$1 action=$2 target=${3:-} value=${4:-}
|
|
736
|
+
case "$action" in
|
|
737
|
+
publish)
|
|
738
|
+
_fm_recovery_marker_publish "$marker" "${target:-downtime}"
|
|
739
|
+
;;
|
|
740
|
+
acknowledge)
|
|
741
|
+
_fm_recovery_marker_ack "$marker" "$target"
|
|
742
|
+
;;
|
|
743
|
+
arm-check)
|
|
744
|
+
_fm_recovery_marker_arm_check "$marker"
|
|
745
|
+
;;
|
|
746
|
+
reopen-announced)
|
|
747
|
+
_fm_recovery_marker_reopen_announced "$marker"
|
|
748
|
+
;;
|
|
749
|
+
release-lock)
|
|
750
|
+
[ -n "$target" ] || return 1
|
|
751
|
+
_fm_recovery_marker_publish "$marker" "${value:-downtime}" || return 1
|
|
752
|
+
fm_lock_release "$target"
|
|
753
|
+
;;
|
|
754
|
+
release-lock-existing)
|
|
755
|
+
[ -n "$target" ] || return 1
|
|
756
|
+
local lock="${marker}.lock"
|
|
757
|
+
fm_lock_acquire_wait "$lock" || return 1
|
|
758
|
+
if ! fm_recovery_marker_read "$marker"; then
|
|
759
|
+
fm_lock_release "$lock"
|
|
760
|
+
return 1
|
|
761
|
+
fi
|
|
762
|
+
fm_lock_release "$target"
|
|
763
|
+
fm_lock_release "$lock"
|
|
764
|
+
;;
|
|
765
|
+
clear-stale-lock)
|
|
766
|
+
[ -n "$target" ] || return 1
|
|
767
|
+
_fm_recovery_marker_publish "$marker" "${value:-downtime}" || return 1
|
|
768
|
+
fm_lock_remove_path "$target"
|
|
769
|
+
;;
|
|
770
|
+
*) return 2 ;;
|
|
771
|
+
esac
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
fm_recovery_marker_publish() {
|
|
775
|
+
fm_recovery_transition "$1" publish "${2:-downtime}"
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
fm_recovery_marker_ack() {
|
|
779
|
+
fm_recovery_transition "$1" acknowledge "$2"
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
fm_recovery_marker_begin_handling() {
|
|
783
|
+
_fm_recovery_marker_begin_handling "$1" "${2:-}"
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
fm_recovery_marker_arm_check() {
|
|
787
|
+
fm_recovery_transition "$1" arm-check
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
fm_recovery_marker_reopen_announced() {
|
|
791
|
+
fm_recovery_transition "$1" reopen-announced
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
fm_lock_try_acquire() {
|
|
795
|
+
local lockdir=$1 pid steal cur rc steal_owner primary_owner
|
|
796
|
+
BROKK_LOCK_HELD_PID=
|
|
797
|
+
BROKK_LOCK_OWNER_DIR=
|
|
798
|
+
BROKK_LOCK_RECOVERED_PID=
|
|
799
|
+
|
|
800
|
+
if fm_lock_try_create "$lockdir"; then
|
|
801
|
+
return 0
|
|
802
|
+
fi
|
|
803
|
+
|
|
804
|
+
# Compare against ${BASHPID:-$$} inline, never via a command substitution:
|
|
805
|
+
# $() forks a subshell whose BASHPID is not this frame's pid.
|
|
806
|
+
pid=$(cat "$lockdir/pid" 2>/dev/null || true)
|
|
807
|
+
if [ -n "$pid" ] && [ "$pid" = "${BASHPID:-$$}" ]; then
|
|
808
|
+
# The recorded holder is THIS very process. Single-threaded bash can only
|
|
809
|
+
# observe that when an interrupting trap abandoned the frame that held the
|
|
810
|
+
# lock mid-critical-section (e.g. TERM inside a recovery-marker section,
|
|
811
|
+
# with the EXIT path then re-acquiring the same lock), and every
|
|
812
|
+
# lock-taking trap path in this repo exits rather than resuming the
|
|
813
|
+
# interrupted frame. Spinning here deadlocks the exit path against itself
|
|
814
|
+
# - the hang reproduced by the self-held reclaim regression in
|
|
815
|
+
# tests/brokk-wake-queue.test.sh - so reclaim the abandoned hold instead.
|
|
816
|
+
fm_lock_remove_path "$lockdir" || true
|
|
817
|
+
if fm_lock_try_create "$lockdir"; then
|
|
818
|
+
return 0
|
|
819
|
+
fi
|
|
820
|
+
BROKK_LOCK_HELD_PID=$(cat "$lockdir/pid" 2>/dev/null || true)
|
|
821
|
+
return 1
|
|
822
|
+
fi
|
|
823
|
+
if fm_pid_alive "$pid"; then
|
|
824
|
+
BROKK_LOCK_HELD_PID=$pid
|
|
825
|
+
return 1
|
|
826
|
+
fi
|
|
827
|
+
if fm_lock_mid_acquire_is_fresh "$lockdir" "$pid"; then
|
|
828
|
+
BROKK_LOCK_HELD_PID=$pid
|
|
829
|
+
return 1
|
|
830
|
+
fi
|
|
831
|
+
|
|
832
|
+
steal="$lockdir.steal"
|
|
833
|
+
if ! fm_lock_try_acquire "$steal"; then
|
|
834
|
+
BROKK_LOCK_HELD_PID=$(cat "$lockdir/pid" 2>/dev/null || true)
|
|
835
|
+
BROKK_LOCK_OWNER_DIR=
|
|
836
|
+
return 1
|
|
837
|
+
fi
|
|
838
|
+
steal_owner=${BROKK_LOCK_OWNER_DIR:-}
|
|
839
|
+
|
|
840
|
+
cur=$(cat "$lockdir/pid" 2>/dev/null || true)
|
|
841
|
+
if fm_pid_alive "$cur"; then
|
|
842
|
+
fm_lock_release "$steal"
|
|
843
|
+
BROKK_LOCK_HELD_PID=$cur
|
|
844
|
+
BROKK_LOCK_OWNER_DIR=
|
|
845
|
+
return 1
|
|
846
|
+
fi
|
|
847
|
+
if fm_lock_mid_acquire_is_fresh "$lockdir" "$cur"; then
|
|
848
|
+
fm_lock_release "$steal"
|
|
849
|
+
BROKK_LOCK_HELD_PID=$cur
|
|
850
|
+
BROKK_LOCK_OWNER_DIR=
|
|
851
|
+
return 1
|
|
852
|
+
fi
|
|
853
|
+
if ! fm_lock_points_to_owner "$steal" "$steal_owner"; then
|
|
854
|
+
fm_lock_release "$steal"
|
|
855
|
+
BROKK_LOCK_HELD_PID=$(cat "$lockdir/pid" 2>/dev/null || true)
|
|
856
|
+
BROKK_LOCK_OWNER_DIR=
|
|
857
|
+
return 1
|
|
858
|
+
fi
|
|
859
|
+
|
|
860
|
+
primary_owner=
|
|
861
|
+
if [ -L "$lockdir" ]; then
|
|
862
|
+
primary_owner=$(fm_lock_link_owner "$lockdir" 2>/dev/null || true)
|
|
863
|
+
fi
|
|
864
|
+
cur=$(cat "$lockdir/pid" 2>/dev/null || true)
|
|
865
|
+
if ! fm_lock_recheck_stale_owner "$lockdir" "$primary_owner" "$cur"; then
|
|
866
|
+
fm_lock_release "$steal"
|
|
867
|
+
BROKK_LOCK_HELD_PID=$(cat "$lockdir/pid" 2>/dev/null || true)
|
|
868
|
+
BROKK_LOCK_OWNER_DIR=
|
|
869
|
+
return 1
|
|
870
|
+
fi
|
|
871
|
+
|
|
872
|
+
if [ "$lockdir" = "$STATE/.watch.lock" ] \
|
|
873
|
+
&& ! _fm_recovery_marker_publish "$STATE/.watcher-down" downtime; then
|
|
874
|
+
fm_lock_release "$steal"
|
|
875
|
+
BROKK_LOCK_HELD_PID=$cur
|
|
876
|
+
BROKK_LOCK_OWNER_DIR=
|
|
877
|
+
return 1
|
|
878
|
+
fi
|
|
879
|
+
fm_lock_remove_path "$lockdir" || true
|
|
880
|
+
rc=1
|
|
881
|
+
if fm_lock_try_create "$lockdir" "$steal_owner"; then
|
|
882
|
+
rc=0
|
|
883
|
+
# shellcheck disable=SC2034 # Read by sourcing callers after lock acquisition.
|
|
884
|
+
BROKK_LOCK_RECOVERED_PID=$cur
|
|
885
|
+
fi
|
|
886
|
+
if [ "$rc" -ne 0 ]; then
|
|
887
|
+
# shellcheck disable=SC2034 # Read by callers after fm_lock_try_acquire returns.
|
|
888
|
+
BROKK_LOCK_HELD_PID=$(cat "$lockdir/pid" 2>/dev/null || true)
|
|
889
|
+
BROKK_LOCK_OWNER_DIR=
|
|
890
|
+
fi
|
|
891
|
+
fm_lock_release "$steal"
|
|
892
|
+
return "$rc"
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
fm_lock_acquire_wait() {
|
|
896
|
+
local lockdir=$1
|
|
897
|
+
while ! fm_lock_try_acquire "$lockdir"; do
|
|
898
|
+
sleep 0.1
|
|
899
|
+
done
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
fm_lock_release() {
|
|
903
|
+
local lockdir=$1 pid current ownerdir
|
|
904
|
+
current=${BASHPID:-$$}
|
|
905
|
+
if [ -L "$lockdir" ]; then
|
|
906
|
+
ownerdir=$(fm_lock_link_owner "$lockdir" 2>/dev/null || true)
|
|
907
|
+
[ -n "$ownerdir" ] || return 0
|
|
908
|
+
pid=$(cat "$ownerdir/pid" 2>/dev/null || true)
|
|
909
|
+
[ "$pid" = "$current" ] || return 0
|
|
910
|
+
fm_lock_points_to_owner "$lockdir" "$ownerdir" || return 0
|
|
911
|
+
rm -f "$lockdir" 2>/dev/null || return 0
|
|
912
|
+
fm_lock_discard_owner "$ownerdir"
|
|
913
|
+
return 0
|
|
914
|
+
fi
|
|
915
|
+
pid=$(cat "$lockdir/pid" 2>/dev/null || true)
|
|
916
|
+
[ "$pid" = "$current" ] || return 0
|
|
917
|
+
fm_lock_clean_known_files "$lockdir"
|
|
918
|
+
rmdir "$lockdir" 2>/dev/null || true
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
fm_meta_lock_path() {
|
|
922
|
+
local meta=$1 dir base id
|
|
923
|
+
dir=${meta%/*}
|
|
924
|
+
base=${meta##*/}
|
|
925
|
+
[ "$dir" != "$meta" ] || dir=.
|
|
926
|
+
case "$base" in
|
|
927
|
+
*.meta) id=${base%.meta} ;;
|
|
928
|
+
*) return 1 ;;
|
|
929
|
+
esac
|
|
930
|
+
case "$id" in
|
|
931
|
+
''|*[!A-Za-z0-9._-]*) return 1 ;;
|
|
932
|
+
esac
|
|
933
|
+
printf '%s/.meta-%s.lock\n' "$dir" "$id"
|
|
934
|
+
}
|
|
935
|
+
|
|
936
|
+
# fm_task_set_lock_path: the per-home lock guarding WHICH tasks exist in a home,
|
|
937
|
+
# as opposed to fm_meta_lock_path, which guards one task's record.
|
|
938
|
+
#
|
|
939
|
+
# A per-task lock cannot protect a task that does not exist yet. Forced
|
|
940
|
+
# eindri-home teardown enumerates a home's task set, locks what it found, and
|
|
941
|
+
# then re-enumerates while removing; a fresh spawn publishing a record inside
|
|
942
|
+
# that window is invisible to the first enumeration and visible to the second,
|
|
943
|
+
# so it gets destructively processed while never lifecycle-locked (reproduced
|
|
944
|
+
# with real agents: a record published 0.249s after teardown began was removed
|
|
945
|
+
# and its worktree returned to the pool, with both commands reporting success).
|
|
946
|
+
# Holding this lock from enumeration through cleanup makes the two operations
|
|
947
|
+
# serialize: either the spawn publishes first and the teardown's preflight
|
|
948
|
+
# covers it, or the teardown owns the set and the spawn refuses. Both directions
|
|
949
|
+
# fail closed.
|
|
950
|
+
fm_task_set_lock_path() { # <state-dir>
|
|
951
|
+
local state=$1
|
|
952
|
+
[ -n "$state" ] || return 1
|
|
953
|
+
case "$state" in *[$'\n\r\t']*) return 1 ;; esac
|
|
954
|
+
printf '%s/.task-set.lock\n' "$state"
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
fm_failure_episode_reset() {
|
|
958
|
+
local state=$1 mode=${2:-acquire} lock current pid acquired=0 path
|
|
959
|
+
lock="$state/.turnend-claude-blocks.lock"
|
|
960
|
+
case "$mode" in
|
|
961
|
+
acquire)
|
|
962
|
+
fm_lock_try_acquire "$lock" || return 1
|
|
963
|
+
acquired=1
|
|
964
|
+
;;
|
|
965
|
+
held)
|
|
966
|
+
current=${BASHPID:-$$}
|
|
967
|
+
pid=$(cat "$lock/pid" 2>/dev/null || true)
|
|
968
|
+
[ "$pid" = "$current" ] || return 1
|
|
969
|
+
;;
|
|
970
|
+
*) return 1 ;;
|
|
971
|
+
esac
|
|
972
|
+
for path in \
|
|
973
|
+
"$state/.turnend-claude-blocks" \
|
|
974
|
+
"$state/.claude-autoarm-failure-notified" \
|
|
975
|
+
"$state/.claude-autoarm-failure-alarmed"
|
|
976
|
+
do
|
|
977
|
+
if [ -d "$path" ] && [ ! -L "$path" ]; then
|
|
978
|
+
[ "$acquired" -eq 0 ] || fm_lock_release "$lock"
|
|
979
|
+
return 1
|
|
980
|
+
fi
|
|
981
|
+
done
|
|
982
|
+
if ! rm -f \
|
|
983
|
+
"$state/.turnend-claude-blocks" \
|
|
984
|
+
"$state/.claude-autoarm-failure-notified" \
|
|
985
|
+
"$state/.claude-autoarm-failure-alarmed" \
|
|
986
|
+
2>/dev/null; then
|
|
987
|
+
[ "$acquired" -eq 0 ] || fm_lock_release "$lock"
|
|
988
|
+
return 1
|
|
989
|
+
fi
|
|
990
|
+
[ "$acquired" -eq 0 ] || fm_lock_release "$lock"
|
|
991
|
+
return 0
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
# --- Claude Stop auto-arm generation claims -----------------------------------
|
|
995
|
+
# Both Stop-event participants (bin/brokk-claude-stop-autoarm.sh and
|
|
996
|
+
# bin/syn-turnend-guard.sh --claude) coordinate through the epoch ledger
|
|
997
|
+
# state/.claude-autoarm-epoch, whose monotonic epoch sequence IS the claim
|
|
998
|
+
# generation. This is an optimistic, generation-based single-flight design:
|
|
999
|
+
#
|
|
1000
|
+
# - The CURRENT claim is the ledger's latest entry: line 1 is the classic
|
|
1001
|
+
# "epoch=N owner_pid=P outcome=O updated_at=T" record, and line 2 is the
|
|
1002
|
+
# claiming process's pid-identity, the same identity every other
|
|
1003
|
+
# supervision lock in this repo records (fm_pid_identity above). The
|
|
1004
|
+
# identity is MANDATORY: a claimant that cannot record it does not claim
|
|
1005
|
+
# (continuity falls to the synchronous guard), and the identity is read
|
|
1006
|
+
# from the ledger entry alone - never substituted from any lock - so a
|
|
1007
|
+
# reused pid can never authenticate someone else's stale entry.
|
|
1008
|
+
# - A claim is OPEN (fm_autoarm_claim_open) while its outcome is "arming",
|
|
1009
|
+
# its owner pid is alive, its recorded identity successfully recomputes
|
|
1010
|
+
# and matches that pid, and it is not STUCK - stuck meaning both the
|
|
1011
|
+
# ledger entry and the watcher beacon (state/.last-watcher-beat) are older
|
|
1012
|
+
# than the guard grace, which proves the owner hung mid-arm with nothing
|
|
1013
|
+
# supervising (every legitimate arming phase with no watcher is bounded in
|
|
1014
|
+
# seconds, while a healthy hours-long cycle keeps the beacon beating).
|
|
1015
|
+
# - Every firing DEFERS (exits 0) to an open claim; anything else - a
|
|
1016
|
+
# terminal outcome, a dead or identity-mismatched owner, a stuck owner, an
|
|
1017
|
+
# identityless entry, or no claim at all - lets the next firing take
|
|
1018
|
+
# generation N+1 (fm_autoarm_claim_next). Taking a newer generation IS the
|
|
1019
|
+
# reclaim: a steady-state predecessor is never signalled or revoked.
|
|
1020
|
+
# - NO mutex is ever held across a blocking step. The owner lock
|
|
1021
|
+
# state/.claude-autoarm.lock survives only as a micro-mutex serializing
|
|
1022
|
+
# individual ledger reads-then-writes (a few non-blocking file
|
|
1023
|
+
# operations); a holder that dies inside the hold is reclaimed by
|
|
1024
|
+
# fm_lock_try_acquire's ordinary dead-owner steal.
|
|
1025
|
+
# - A superseded owner goes COMPLETELY silent - cleanup only. Ownership is
|
|
1026
|
+
# re-verified before every side effect: each arm invocation, each
|
|
1027
|
+
# episode-state mutation, each ledger write, and each continuation.
|
|
1028
|
+
# - The irrevocable commit point of a translation is the EXIT STATUS: the
|
|
1029
|
+
# harness delivers the collected stderr banner only on exit 2 and discards
|
|
1030
|
+
# it on exit 0. Markerless outcomes commit with the owned terminal ledger
|
|
1031
|
+
# write. The once-per-episode failure notice commits only when its marker is
|
|
1032
|
+
# created after the winning "failed" write in the same owned critical
|
|
1033
|
+
# section. A superseded generation or failed required-marker creation is
|
|
1034
|
+
# refused and exits 0 silently even after printing; a later generation
|
|
1035
|
+
# supersedes the terminal entry and retries the notice.
|
|
1036
|
+
#
|
|
1037
|
+
# This structurally removes the failure classes the lock-held-across-arm
|
|
1038
|
+
# design produced: a hung owner deferring every later firing forever (observed
|
|
1039
|
+
# 2026-08-26: a hook hung mid-arm with its ledger frozen at "arming" kept the
|
|
1040
|
+
# watcher from ever being auto-re-armed again; and 2026-08-14: a finished
|
|
1041
|
+
# claim whose leftover lock silenced both participants for 40 beacon-less
|
|
1042
|
+
# minutes), a reclaim mutex held across a blocking banner write recreating the
|
|
1043
|
+
# same unreclaimable-live-owner shape, and a reclaimed-but-alive owner racing
|
|
1044
|
+
# its replacement to translate one close twice.
|
|
1045
|
+
#
|
|
1046
|
+
# Two bounded residuals are ACCEPTED INTENT, because closing them absolutely
|
|
1047
|
+
# would require a mutex held across output or steady-state revocation, both
|
|
1048
|
+
# deliberately rejected: (1) an owner that dies between its owned terminal
|
|
1049
|
+
# write and its own process exit leaves a committed outcome whose banner was
|
|
1050
|
+
# never delivered (process-death territory; the durable wake queue retains the
|
|
1051
|
+
# underlying event), and (2) a hung old-build owner that resumes during the
|
|
1052
|
+
# one legacy upgrade window may add one duplicate continuation. Each residual
|
|
1053
|
+
# costs at most one extra exit-2 continuation turn absorbed by the durable
|
|
1054
|
+
# idempotent wake queue. A claim misread as stuck in a pathological race
|
|
1055
|
+
# (e.g. a beacon read right at system wake) likewise yields at most one extra
|
|
1056
|
+
# arm that the watcher singleton dedupes, while the superseded owner still
|
|
1057
|
+
# goes silent.
|
|
1058
|
+
#
|
|
1059
|
+
# fm_autoarm_claim_abandoned / fm_autoarm_release_abandoned below survive as
|
|
1060
|
+
# the LEGACY shim for a lock-holding claim from a pre-generation build (the
|
|
1061
|
+
# lock carries a role file only in that legacy shape, and in the guard's own
|
|
1062
|
+
# short terminal-check hold): a live legacy owner still defers per the legacy
|
|
1063
|
+
# proof, and a proven-abandoned one is reclaimed once through the steal mutex
|
|
1064
|
+
# - with an identity-verified live owner retired via TERM first, because old
|
|
1065
|
+
# code cannot re-check generations - so an upgrade mid-session can neither
|
|
1066
|
+
# double-arm nor deadlock behind a hung legacy hook.
|
|
1067
|
+
_fm_autoarm_epoch_field() { # <epoch-file> <field>
|
|
1068
|
+
local file=$1 field=$2 tok
|
|
1069
|
+
local -a toks=()
|
|
1070
|
+
[ -r "$file" ] || return 1
|
|
1071
|
+
# 2> before <: a failed input redirection reports through whatever stderr is
|
|
1072
|
+
# current when it runs, so the suppression has to be established first.
|
|
1073
|
+
IFS=' ' read -r -a toks 2>/dev/null < "$file" || return 1
|
|
1074
|
+
for tok in ${toks[@]+"${toks[@]}"}; do
|
|
1075
|
+
case "$tok" in
|
|
1076
|
+
"$field="?*) printf '%s\n' "${tok#*=}"; return 0 ;;
|
|
1077
|
+
esac
|
|
1078
|
+
done
|
|
1079
|
+
return 1
|
|
1080
|
+
}
|
|
1081
|
+
|
|
1082
|
+
# Parse the current ledger claim. Sets BROKK_AUTOARM_GEN, BROKK_AUTOARM_OWNER,
|
|
1083
|
+
# BROKK_AUTOARM_OUTCOME, and BROKK_AUTOARM_IDENTITY (line 2 of the entry, and ONLY
|
|
1084
|
+
# line 2 - identity is never substituted from a lock, so a transient
|
|
1085
|
+
# micro-mutex hold or a reused pid can never authenticate a stale entry).
|
|
1086
|
+
fm_autoarm_ledger_read() { # <state-dir>
|
|
1087
|
+
local state=$1 epoch
|
|
1088
|
+
epoch="$state/.claude-autoarm-epoch"
|
|
1089
|
+
BROKK_AUTOARM_GEN=
|
|
1090
|
+
BROKK_AUTOARM_OWNER=
|
|
1091
|
+
BROKK_AUTOARM_OUTCOME=
|
|
1092
|
+
BROKK_AUTOARM_IDENTITY=
|
|
1093
|
+
BROKK_AUTOARM_GEN=$(_fm_autoarm_epoch_field "$epoch" epoch) || return 1
|
|
1094
|
+
BROKK_AUTOARM_OWNER=$(_fm_autoarm_epoch_field "$epoch" owner_pid) || return 1
|
|
1095
|
+
BROKK_AUTOARM_OUTCOME=$(_fm_autoarm_epoch_field "$epoch" outcome) || return 1
|
|
1096
|
+
case "$BROKK_AUTOARM_GEN" in
|
|
1097
|
+
''|*[!0-9]*) return 1 ;;
|
|
1098
|
+
esac
|
|
1099
|
+
BROKK_AUTOARM_IDENTITY=$(sed -n '2p' "$epoch" 2>/dev/null || true)
|
|
1100
|
+
return 0
|
|
1101
|
+
}
|
|
1102
|
+
|
|
1103
|
+
# True while the CURRENT ledger claim is open and healthy - the defer predicate
|
|
1104
|
+
# both Stop participants use. Open means: outcome "arming", a live owner whose
|
|
1105
|
+
# mandatory recorded identity recomputes and matches its pid, and not stuck
|
|
1106
|
+
# (the contract comment above owns the stuck proof). fm_path_age reports an
|
|
1107
|
+
# absent beacon as ancient, which is exactly right: arming for a full grace
|
|
1108
|
+
# window without producing a first beat is the same hang. An identityless
|
|
1109
|
+
# entry is never open: real generation claims always record identity, a legacy
|
|
1110
|
+
# build's entry gets its deference from its held role-carrying lock through
|
|
1111
|
+
# the legacy shim, and anything else must not defer.
|
|
1112
|
+
fm_autoarm_claim_open() { # <state-dir> [grace]
|
|
1113
|
+
local state=$1 grace=${2:-${BROKK_GUARD_GRACE:-300}} epoch current
|
|
1114
|
+
epoch="$state/.claude-autoarm-epoch"
|
|
1115
|
+
case "$grace" in
|
|
1116
|
+
''|*[!0-9]*|0) grace=300 ;;
|
|
1117
|
+
esac
|
|
1118
|
+
fm_autoarm_ledger_read "$state" || return 1
|
|
1119
|
+
[ "$BROKK_AUTOARM_OUTCOME" = arming ] || return 1
|
|
1120
|
+
fm_pid_alive "$BROKK_AUTOARM_OWNER" || return 1
|
|
1121
|
+
[ -n "$BROKK_AUTOARM_IDENTITY" ] || return 1
|
|
1122
|
+
current=$(fm_pid_identity "$BROKK_AUTOARM_OWNER" 2>/dev/null) || return 1
|
|
1123
|
+
[ -n "$current" ] || return 1
|
|
1124
|
+
[ "$current" = "$BROKK_AUTOARM_IDENTITY" ] || return 1
|
|
1125
|
+
if [ "$(fm_path_age "$epoch")" -ge "$grace" ] \
|
|
1126
|
+
&& [ "$(fm_path_age "$state/.last-watcher-beat")" -ge "$grace" ]; then
|
|
1127
|
+
return 1
|
|
1128
|
+
fi
|
|
1129
|
+
return 0
|
|
1130
|
+
}
|
|
1131
|
+
|
|
1132
|
+
# Atomically publish this process as the owner of generation N+1, under one
|
|
1133
|
+
# short micro-mutex hold. Returns 0 with BROKK_AUTOARM_MY_GEN set on success, 2
|
|
1134
|
+
# when a competing claimant won the race (the ledger holds an open claim), and
|
|
1135
|
+
# 1 when the micro-mutex is contended, the mandatory identity cannot be
|
|
1136
|
+
# computed, or the write failed.
|
|
1137
|
+
fm_autoarm_claim_next() { # <state-dir> [grace]
|
|
1138
|
+
local state=$1 grace=${2:-${BROKK_GUARD_GRACE:-300}} lock epoch pid gen identity tmp
|
|
1139
|
+
lock="$state/.claude-autoarm.lock"
|
|
1140
|
+
epoch="$state/.claude-autoarm-epoch"
|
|
1141
|
+
BROKK_AUTOARM_MY_GEN=
|
|
1142
|
+
# Resolve the pid into a variable FIRST: expanding ${BASHPID:-$$} inside a
|
|
1143
|
+
# command substitution would resolve it in that subshell, recording the
|
|
1144
|
+
# identity of a process that exits immediately.
|
|
1145
|
+
pid=${BASHPID:-$$}
|
|
1146
|
+
identity=$(fm_pid_identity "$pid" 2>/dev/null) || return 1
|
|
1147
|
+
[ -n "$identity" ] || return 1
|
|
1148
|
+
fm_lock_try_acquire "$lock" || return 1
|
|
1149
|
+
if fm_autoarm_claim_open "$state" "$grace"; then
|
|
1150
|
+
fm_lock_release "$lock"
|
|
1151
|
+
return 2
|
|
1152
|
+
fi
|
|
1153
|
+
gen=$(_fm_autoarm_epoch_field "$epoch" epoch 2>/dev/null || true)
|
|
1154
|
+
case "$gen" in
|
|
1155
|
+
''|*[!0-9]*) gen=0 ;;
|
|
1156
|
+
esac
|
|
1157
|
+
gen=$((gen + 1))
|
|
1158
|
+
tmp="$epoch.tmp.$pid"
|
|
1159
|
+
if ! printf 'epoch=%s owner_pid=%s outcome=arming updated_at=%s\n%s\n' \
|
|
1160
|
+
"$gen" "$pid" "$(date +%s)" "$identity" > "$tmp" 2>/dev/null \
|
|
1161
|
+
|| ! mv -f "$tmp" "$epoch" 2>/dev/null; then
|
|
1162
|
+
rm -f "$tmp" 2>/dev/null || true
|
|
1163
|
+
fm_lock_release "$lock"
|
|
1164
|
+
return 1
|
|
1165
|
+
fi
|
|
1166
|
+
fm_lock_release "$lock"
|
|
1167
|
+
# shellcheck disable=SC2034 # Read by callers after the claim succeeds.
|
|
1168
|
+
BROKK_AUTOARM_MY_GEN=$gen
|
|
1169
|
+
return 0
|
|
1170
|
+
}
|
|
1171
|
+
|
|
1172
|
+
# Write a new outcome for a generation this process still owns, re-verified
|
|
1173
|
+
# under the micro-mutex so a superseded owner can never clobber a newer claim.
|
|
1174
|
+
# With a fourth argument, create that marker after the ledger rename in the same
|
|
1175
|
+
# owned critical section (the once-per-episode failure notice). A marker failure
|
|
1176
|
+
# refuses the commit even though its terminal ledger entry remains; marker-first
|
|
1177
|
+
# ordering could permanently suppress a notice whose ledger write never won.
|
|
1178
|
+
# Returns 0 committed, 2 refused (superseded or required-marker failure), and 1
|
|
1179
|
+
# unable (bounded contention or ledger-write failure).
|
|
1180
|
+
fm_autoarm_write_owned() { # <state-dir> <gen> <outcome> [marker-file]
|
|
1181
|
+
local state=$1 gen=$2 outcome=$3 marker=${4:-} lock epoch pid identity tmp i
|
|
1182
|
+
lock="$state/.claude-autoarm.lock"
|
|
1183
|
+
epoch="$state/.claude-autoarm-epoch"
|
|
1184
|
+
pid=${BASHPID:-$$}
|
|
1185
|
+
i=0
|
|
1186
|
+
while ! fm_lock_try_acquire "$lock"; do
|
|
1187
|
+
[ "$i" -lt 20 ] || return 1
|
|
1188
|
+
sleep 0.02
|
|
1189
|
+
i=$((i + 1))
|
|
1190
|
+
done
|
|
1191
|
+
if ! fm_autoarm_ledger_read "$state" \
|
|
1192
|
+
|| [ "$BROKK_AUTOARM_GEN" != "$gen" ] || [ "$BROKK_AUTOARM_OWNER" != "$pid" ]; then
|
|
1193
|
+
fm_lock_release "$lock"
|
|
1194
|
+
return 2
|
|
1195
|
+
fi
|
|
1196
|
+
identity=$BROKK_AUTOARM_IDENTITY
|
|
1197
|
+
tmp="$epoch.tmp.$pid"
|
|
1198
|
+
if ! {
|
|
1199
|
+
printf 'epoch=%s owner_pid=%s outcome=%s updated_at=%s\n' \
|
|
1200
|
+
"$gen" "$pid" "$outcome" "$(date +%s)"
|
|
1201
|
+
[ -z "$identity" ] || printf '%s\n' "$identity"
|
|
1202
|
+
} > "$tmp" 2>/dev/null || ! mv -f "$tmp" "$epoch" 2>/dev/null; then
|
|
1203
|
+
rm -f "$tmp" 2>/dev/null || true
|
|
1204
|
+
fm_lock_release "$lock"
|
|
1205
|
+
return 1
|
|
1206
|
+
fi
|
|
1207
|
+
if [ -n "$marker" ] && ! : > "$marker" 2>/dev/null; then
|
|
1208
|
+
fm_lock_release "$lock"
|
|
1209
|
+
return 2
|
|
1210
|
+
fi
|
|
1211
|
+
fm_lock_release "$lock"
|
|
1212
|
+
return 0
|
|
1213
|
+
}
|
|
1214
|
+
|
|
1215
|
+
# Lockless pre-side-effect ownership check: true while the ledger still names
|
|
1216
|
+
# <gen> owned by this process. A superseded owner must go silent instead of
|
|
1217
|
+
# arming, mutating shared state, or emitting.
|
|
1218
|
+
fm_autoarm_still_owner() { # <state-dir> <gen>
|
|
1219
|
+
local state=$1 gen=$2 pid
|
|
1220
|
+
pid=${BASHPID:-$$}
|
|
1221
|
+
fm_autoarm_ledger_read "$state" || return 1
|
|
1222
|
+
[ "$BROKK_AUTOARM_GEN" = "$gen" ] && [ "$BROKK_AUTOARM_OWNER" = "$pid" ]
|
|
1223
|
+
}
|
|
1224
|
+
|
|
1225
|
+
fm_autoarm_reset_owned() { # <state-dir> <gen>
|
|
1226
|
+
local state=$1 gen=$2 lock pid
|
|
1227
|
+
lock="$state/.claude-autoarm.lock"
|
|
1228
|
+
pid=${BASHPID:-$$}
|
|
1229
|
+
fm_lock_try_acquire "$lock" || return 2
|
|
1230
|
+
if ! fm_autoarm_ledger_read "$state" \
|
|
1231
|
+
|| [ "$BROKK_AUTOARM_GEN" != "$gen" ] || [ "$BROKK_AUTOARM_OWNER" != "$pid" ]; then
|
|
1232
|
+
fm_lock_release "$lock"
|
|
1233
|
+
return 2
|
|
1234
|
+
fi
|
|
1235
|
+
if ! fm_failure_episode_reset "$state"; then
|
|
1236
|
+
fm_lock_release "$lock"
|
|
1237
|
+
return 1
|
|
1238
|
+
fi
|
|
1239
|
+
fm_lock_release "$lock"
|
|
1240
|
+
return 0
|
|
1241
|
+
}
|
|
1242
|
+
|
|
1243
|
+
# LEGACY shim (see the contract comment above): the abandonment proof for a
|
|
1244
|
+
# lock-holding claim from a pre-generation build, recognizable by the role
|
|
1245
|
+
# file only such claims and the guard's short terminal-check hold publish.
|
|
1246
|
+
# A live legacy owner defers per this proof; a finished, identity-mismatched,
|
|
1247
|
+
# or stuck one is abandoned:
|
|
1248
|
+
#
|
|
1249
|
+
# 1. the owner lock exists and carries the auto-arm role,
|
|
1250
|
+
# 2. its recorded pid is numeric,
|
|
1251
|
+
# 3. a recorded pid-identity that no longer matches the live pid is
|
|
1252
|
+
# abandonment on its own (pid reuse after a group kill), and otherwise
|
|
1253
|
+
# 4. the ledger's owner_pid is exactly that pid and its outcome is present
|
|
1254
|
+
# and either is not "arming", or is "arming" while both the ledger entry
|
|
1255
|
+
# and the watcher beacon are older than the guard grace (the same stuck
|
|
1256
|
+
# proof as fm_autoarm_claim_open).
|
|
1257
|
+
fm_autoarm_claim_abandoned() { # <state-dir> [grace]
|
|
1258
|
+
local state=$1 grace=${2:-${BROKK_GUARD_GRACE:-300}} epoch lock role pid owner outcome recorded current
|
|
1259
|
+
lock="$state/.claude-autoarm.lock"
|
|
1260
|
+
epoch="$state/.claude-autoarm-epoch"
|
|
1261
|
+
case "$grace" in
|
|
1262
|
+
''|*[!0-9]*|0) grace=300 ;;
|
|
1263
|
+
esac
|
|
1264
|
+
[ -e "$lock" ] || [ -L "$lock" ] || return 1
|
|
1265
|
+
role=$(fm_lock_role "$lock")
|
|
1266
|
+
[ "$role" = autoarm ] || return 1
|
|
1267
|
+
pid=$(cat "$lock/pid" 2>/dev/null || true)
|
|
1268
|
+
case "$pid" in
|
|
1269
|
+
''|*[!0-9]*) return 1 ;;
|
|
1270
|
+
esac
|
|
1271
|
+
recorded=$(cat "$lock/pid-identity" 2>/dev/null || true)
|
|
1272
|
+
if [ -n "$recorded" ] && current=$(fm_pid_identity "$pid" 2>/dev/null) \
|
|
1273
|
+
&& [ -n "$current" ] && [ "$current" != "$recorded" ]; then
|
|
1274
|
+
return 0
|
|
1275
|
+
fi
|
|
1276
|
+
owner=$(_fm_autoarm_epoch_field "$epoch" owner_pid) || return 1
|
|
1277
|
+
[ "$owner" = "$pid" ] || return 1
|
|
1278
|
+
outcome=$(_fm_autoarm_epoch_field "$epoch" outcome) || return 1
|
|
1279
|
+
case "$outcome" in
|
|
1280
|
+
'') return 1 ;;
|
|
1281
|
+
arming)
|
|
1282
|
+
[ "$(fm_path_age "$epoch")" -ge "$grace" ] || return 1
|
|
1283
|
+
[ "$(fm_path_age "$state/.last-watcher-beat")" -ge "$grace" ] || return 1
|
|
1284
|
+
return 0
|
|
1285
|
+
;;
|
|
1286
|
+
esac
|
|
1287
|
+
return 0
|
|
1288
|
+
}
|
|
1289
|
+
|
|
1290
|
+
# Remove a proven-abandoned legacy claim so the next claimant can arm. The
|
|
1291
|
+
# proof is re-verified while holding the lock's steal mutex, the same
|
|
1292
|
+
# serialization fm_lock_try_acquire uses for stale-owner reclaim: while it is
|
|
1293
|
+
# held no other process can publish the primary lock, so the window between
|
|
1294
|
+
# proving abandonment and removing the lock cannot swallow a genuine new
|
|
1295
|
+
# claim.
|
|
1296
|
+
#
|
|
1297
|
+
# Old-build code cannot re-check generations, so a LIVE proven-abandoned
|
|
1298
|
+
# legacy owner whose recorded identity is verified to match its pid is retired
|
|
1299
|
+
# with TERM before the lock is removed: once the TERM is successfully queued
|
|
1300
|
+
# the process can never resume normal execution (delivery precedes any further
|
|
1301
|
+
# user code when it continues), so a short bounded wait for observed exit is a
|
|
1302
|
+
# courtesy, not a requirement. A pid is never signalled without a verified
|
|
1303
|
+
# matching identity; when the kill itself fails or the identity stops matching
|
|
1304
|
+
# mid-procedure (pid reuse), the reclaim refuses. Missing identity evidence
|
|
1305
|
+
# never blocks the reclaim of a proven-abandoned claim - it only disables the
|
|
1306
|
+
# TERM and the ledger graft below, keeping the documented bounded
|
|
1307
|
+
# upgrade-window residual instead of the deadlock.
|
|
1308
|
+
fm_autoarm_release_abandoned() { # <state-dir> [grace]
|
|
1309
|
+
local state=$1 grace=${2:-${BROKK_GUARD_GRACE:-300}} lock steal epoch lock_pid recorded current owner line1 tmp i
|
|
1310
|
+
lock="$state/.claude-autoarm.lock"
|
|
1311
|
+
steal="$lock.steal"
|
|
1312
|
+
epoch="$state/.claude-autoarm-epoch"
|
|
1313
|
+
fm_autoarm_claim_abandoned "$state" "$grace" || return 1
|
|
1314
|
+
fm_lock_try_acquire "$steal" || return 1
|
|
1315
|
+
if ! fm_autoarm_claim_abandoned "$state" "$grace"; then
|
|
1316
|
+
fm_lock_release "$steal"
|
|
1317
|
+
return 1
|
|
1318
|
+
fi
|
|
1319
|
+
lock_pid=$(cat "$lock/pid" 2>/dev/null || true)
|
|
1320
|
+
recorded=$(cat "$lock/pid-identity" 2>/dev/null || true)
|
|
1321
|
+
if [ -n "$recorded" ] && fm_pid_alive "$lock_pid" \
|
|
1322
|
+
&& current=$(fm_pid_identity "$lock_pid" 2>/dev/null) \
|
|
1323
|
+
&& [ -n "$current" ] && [ "$current" = "$recorded" ]; then
|
|
1324
|
+
# A live pid still answering to the recorded identity IS the genuine
|
|
1325
|
+
# legacy owner (proven stuck or blocked after a terminal write): retire it
|
|
1326
|
+
# before removing its lock, because old-build code cannot re-check
|
|
1327
|
+
# generations. A pid the recorded identity does NOT verify - reused,
|
|
1328
|
+
# unverifiable, or never recorded - is NEVER signalled; those shapes are
|
|
1329
|
+
# reclaimed as-is, which is safe exactly because the recorded owner is
|
|
1330
|
+
# gone or was never provably this process.
|
|
1331
|
+
if ! kill -TERM "$lock_pid" 2>/dev/null; then
|
|
1332
|
+
fm_lock_release "$steal"
|
|
1333
|
+
return 1
|
|
1334
|
+
fi
|
|
1335
|
+
i=0
|
|
1336
|
+
while [ "$i" -lt 20 ] && fm_pid_alive "$lock_pid"; do
|
|
1337
|
+
sleep 0.05
|
|
1338
|
+
i=$((i + 1))
|
|
1339
|
+
done
|
|
1340
|
+
fi
|
|
1341
|
+
# Preserve the legacy lock's identity evidence in the ledger before the lock
|
|
1342
|
+
# disappears, keeping the ledger's original mtime so the stuck proof's age
|
|
1343
|
+
# window is not silently reopened. Best effort.
|
|
1344
|
+
if [ -n "$recorded" ] && [ -n "$lock_pid" ] \
|
|
1345
|
+
&& owner=$(_fm_autoarm_epoch_field "$epoch" owner_pid 2>/dev/null) \
|
|
1346
|
+
&& [ "$owner" = "$lock_pid" ] \
|
|
1347
|
+
&& [ -z "$(sed -n '2p' "$epoch" 2>/dev/null)" ]; then
|
|
1348
|
+
line1=$(sed -n '1p' "$epoch" 2>/dev/null || true)
|
|
1349
|
+
tmp="$epoch.tmp.${BASHPID:-$$}"
|
|
1350
|
+
if [ -n "$line1" ] \
|
|
1351
|
+
&& printf '%s\n%s\n' "$line1" "$recorded" > "$tmp" 2>/dev/null \
|
|
1352
|
+
&& touch -r "$epoch" "$tmp" 2>/dev/null \
|
|
1353
|
+
&& mv -f "$tmp" "$epoch" 2>/dev/null; then
|
|
1354
|
+
:
|
|
1355
|
+
fi
|
|
1356
|
+
rm -f "$tmp" 2>/dev/null || true
|
|
1357
|
+
fi
|
|
1358
|
+
fm_lock_remove_path "$lock" || true
|
|
1359
|
+
fm_lock_release "$steal"
|
|
1360
|
+
[ -e "$lock" ] || [ -L "$lock" ] || return 0
|
|
1361
|
+
return 1
|
|
1362
|
+
}
|
|
1363
|
+
|
|
1364
|
+
fm_wake_clean_field() {
|
|
1365
|
+
LC_ALL=C tr '\t\r\n' ' '
|
|
1366
|
+
}
|
|
1367
|
+
|
|
1368
|
+
fm_wake_append() {
|
|
1369
|
+
local kind=$1 key=$2 payload=$3 clean_key clean_payload epoch seq seq_file status
|
|
1370
|
+
local recovery_marker
|
|
1371
|
+
case "$kind" in
|
|
1372
|
+
signal|stale|check|heartbeat) ;;
|
|
1373
|
+
*) printf 'fm_wake_append: invalid wake kind: %s\n' "$kind" >&2; return 2 ;;
|
|
1374
|
+
esac
|
|
1375
|
+
|
|
1376
|
+
clean_key=$(printf '%s' "$key" | fm_wake_clean_field)
|
|
1377
|
+
clean_payload=$(printf '%s' "$payload" | fm_wake_clean_field)
|
|
1378
|
+
epoch=$(date +%s)
|
|
1379
|
+
seq_file="$STATE/.wake-queue.seq"
|
|
1380
|
+
recovery_marker="$STATE/.watcher-down"
|
|
1381
|
+
status=0
|
|
1382
|
+
|
|
1383
|
+
fm_lock_acquire_wait "$BROKK_WAKE_QUEUE_LOCK"
|
|
1384
|
+
_fm_recovery_marker_publish "$recovery_marker" downtime || status=$?
|
|
1385
|
+
if [ "$status" -eq 0 ]; then
|
|
1386
|
+
seq=$(cat "$seq_file" 2>/dev/null || echo 0)
|
|
1387
|
+
case "$seq" in
|
|
1388
|
+
''|*[!0-9]*) seq=0 ;;
|
|
1389
|
+
esac
|
|
1390
|
+
seq=$((seq + 1))
|
|
1391
|
+
printf '%s\n' "$seq" > "$seq_file" || status=$?
|
|
1392
|
+
fi
|
|
1393
|
+
if [ "$status" -eq 0 ]; then
|
|
1394
|
+
printf '%s\t%s\t%s\t%s\t%s\n' "$epoch" "$seq" "$kind" "$clean_key" "$clean_payload" >> "$BROKK_WAKE_QUEUE" || status=$?
|
|
1395
|
+
fi
|
|
1396
|
+
fm_lock_release "$BROKK_WAKE_QUEUE_LOCK"
|
|
1397
|
+
return "$status"
|
|
1398
|
+
}
|
|
1399
|
+
|
|
1400
|
+
# fm_wake_queued_keys <kind>
|
|
1401
|
+
# Print the distinct keys currently queued for <kind>, oldest first. Read under
|
|
1402
|
+
# the append lock so a concurrent append is never observed half-written. The
|
|
1403
|
+
# durable queue stays the authority: a key appears here exactly while a record
|
|
1404
|
+
# for it is queued and unacknowledged, and disappears only after post-handling
|
|
1405
|
+
# acknowledgement consumes it.
|
|
1406
|
+
fm_wake_queued_keys() {
|
|
1407
|
+
local kind=$1
|
|
1408
|
+
case "$kind" in
|
|
1409
|
+
signal|stale|check|heartbeat) ;;
|
|
1410
|
+
*) printf 'fm_wake_queued_keys: invalid wake kind: %s\n' "$kind" >&2; return 2 ;;
|
|
1411
|
+
esac
|
|
1412
|
+
fm_lock_acquire_wait "$BROKK_WAKE_QUEUE_LOCK"
|
|
1413
|
+
fm_wake_queued_keys_locked "$kind"
|
|
1414
|
+
fm_lock_release "$BROKK_WAKE_QUEUE_LOCK"
|
|
1415
|
+
}
|
|
1416
|
+
|
|
1417
|
+
fm_wake_queued_keys_locked() {
|
|
1418
|
+
local kind=$1
|
|
1419
|
+
awk -F '\t' -v kind="$kind" 'NF >= 5 && $3 == kind && !seen[$4]++ { print $4 }' \
|
|
1420
|
+
"$BROKK_WAKE_QUEUE" 2>/dev/null || true
|
|
1421
|
+
}
|
|
1422
|
+
|
|
1423
|
+
fm_wake_eindri-home_stall_marker_write() { # <task> <row-key>
|
|
1424
|
+
local task=$1 row_key=$2 marker tmp
|
|
1425
|
+
case "$task" in ''|*[!A-Za-z0-9._-]*) return 1 ;; esac
|
|
1426
|
+
case "$row_key" in ''|*[!0-9-]*) return 1 ;; esac
|
|
1427
|
+
marker="$STATE/.eindri-home-wake-stall-$task"
|
|
1428
|
+
if [ -e "$marker" ] || [ -L "$marker" ]; then
|
|
1429
|
+
[ -f "$marker" ] && [ ! -L "$marker" ] || return 1
|
|
1430
|
+
fi
|
|
1431
|
+
tmp=$(mktemp "$STATE/.eindri-home-wake-stall.XXXXXX") || return 1
|
|
1432
|
+
if ! printf '%s\n' "$row_key" > "$tmp" || ! chmod 0600 "$tmp" \
|
|
1433
|
+
|| ! _fm_atomic_replace "$tmp" "$marker"; then
|
|
1434
|
+
rm -f -- "$tmp"
|
|
1435
|
+
return 1
|
|
1436
|
+
fi
|
|
1437
|
+
}
|
|
1438
|
+
|
|
1439
|
+
fm_wake_eindri-home_stall_receipt_write() { # <task> <row-key>
|
|
1440
|
+
local task=$1 row_key=$2 root task_dir receipt tmp
|
|
1441
|
+
case "$task" in ''|*[!A-Za-z0-9._-]*) return 1 ;; esac
|
|
1442
|
+
case "$row_key" in ''|*[!0-9-]*) return 1 ;; esac
|
|
1443
|
+
root="$STATE/.eindri-home-wake-stall-receipts"
|
|
1444
|
+
task_dir="$root/$task"
|
|
1445
|
+
if [ -e "$root" ] || [ -L "$root" ]; then
|
|
1446
|
+
[ -d "$root" ] && [ ! -L "$root" ] || return 1
|
|
1447
|
+
else
|
|
1448
|
+
mkdir "$root" || return 1
|
|
1449
|
+
chmod 0700 "$root" || return 1
|
|
1450
|
+
fi
|
|
1451
|
+
if [ -e "$task_dir" ] || [ -L "$task_dir" ]; then
|
|
1452
|
+
[ -d "$task_dir" ] && [ ! -L "$task_dir" ] || return 1
|
|
1453
|
+
else
|
|
1454
|
+
mkdir "$task_dir" || return 1
|
|
1455
|
+
chmod 0700 "$task_dir" || return 1
|
|
1456
|
+
fi
|
|
1457
|
+
receipt="$task_dir/$row_key"
|
|
1458
|
+
[ "$(cat "$receipt" 2>/dev/null || true)" != "$row_key" ] || return 0
|
|
1459
|
+
tmp=$(mktemp "$task_dir/.receipt.XXXXXX") || return 1
|
|
1460
|
+
if ! printf '%s\n' "$row_key" > "$tmp" || ! chmod 0600 "$tmp" \
|
|
1461
|
+
|| ! _fm_atomic_replace "$tmp" "$receipt"; then
|
|
1462
|
+
rm -f -- "$tmp"
|
|
1463
|
+
return 1
|
|
1464
|
+
fi
|
|
1465
|
+
}
|
|
1466
|
+
|
|
1467
|
+
fm_wake_commit_eindri-home_stall_receipts_through() { # <cutoff> [<rows-file>]
|
|
1468
|
+
local cutoff=$1 rows=${2:-} key seq rest epoch task row_key
|
|
1469
|
+
while IFS= read -r key; do
|
|
1470
|
+
seq=${key##*-}
|
|
1471
|
+
rest=${key%-*}
|
|
1472
|
+
epoch=${rest##*-}
|
|
1473
|
+
task=${rest#eindri-home-wake-loop-}
|
|
1474
|
+
task=${task%-"$epoch"}
|
|
1475
|
+
case "$seq" in ''|*[!0-9]*) return 1 ;; esac
|
|
1476
|
+
case "$epoch" in ''|*[!0-9]*) return 1 ;; esac
|
|
1477
|
+
case "$task" in ''|*[!A-Za-z0-9._-]*) return 1 ;; esac
|
|
1478
|
+
row_key="$epoch-$seq"
|
|
1479
|
+
fm_wake_eindri-home_stall_receipt_write "$task" "$row_key" || return 1
|
|
1480
|
+
done < <(awk -F '\t' -v cutoff="$cutoff" -v rows="$rows" '
|
|
1481
|
+
BEGIN { if (rows != "") while ((getline line < rows) > 0) owned[line]=1 }
|
|
1482
|
+
NF >= 5 && $2 ~ /^[0-9]+$/ && $2 <= cutoff \
|
|
1483
|
+
&& (rows == "" || ($2 in owned)) && $3 == "check" \
|
|
1484
|
+
&& $4 ~ /^eindri-home-wake-loop-[A-Za-z0-9._-]+-[0-9]+-[0-9]+$/ { print $4 }
|
|
1485
|
+
' "$BROKK_WAKE_QUEUE" 2>/dev/null)
|
|
1486
|
+
}
|
|
1487
|
+
|
|
1488
|
+
fm_wake_restore_queue() {
|
|
1489
|
+
local drained=$1 restore
|
|
1490
|
+
restore="$STATE/.wake-queue.restore.$(fm_current_pid)"
|
|
1491
|
+
if [ -e "$BROKK_WAKE_QUEUE" ]; then
|
|
1492
|
+
cat "$drained" "$BROKK_WAKE_QUEUE" > "$restore" && mv "$restore" "$BROKK_WAKE_QUEUE"
|
|
1493
|
+
else
|
|
1494
|
+
mv "$drained" "$BROKK_WAKE_QUEUE"
|
|
1495
|
+
fi
|
|
1496
|
+
}
|
|
1497
|
+
|
|
1498
|
+
fm_wake_print_deduped() {
|
|
1499
|
+
local file=$1
|
|
1500
|
+
awk -F '\t' '
|
|
1501
|
+
NF >= 5 {
|
|
1502
|
+
dedupe = $3 SUBSEP $4
|
|
1503
|
+
if ($3 == "heartbeat") {
|
|
1504
|
+
dedupe = "heartbeat"
|
|
1505
|
+
}
|
|
1506
|
+
if (!(dedupe in seen)) {
|
|
1507
|
+
order[++count] = dedupe
|
|
1508
|
+
seen[dedupe] = 1
|
|
1509
|
+
}
|
|
1510
|
+
line[dedupe] = $0
|
|
1511
|
+
}
|
|
1512
|
+
END {
|
|
1513
|
+
for (i = 1; i <= count; i++) {
|
|
1514
|
+
print line[order[i]]
|
|
1515
|
+
}
|
|
1516
|
+
}
|
|
1517
|
+
' "$file"
|
|
1518
|
+
}
|
|
1519
|
+
|
|
1520
|
+
# --- signal announcement signatures -----------------------------------------
|
|
1521
|
+
#
|
|
1522
|
+
# The watcher's per-file signal scan (bin/brokk-watch.sh scan_signals) detects a
|
|
1523
|
+
# status or turn-ended change by comparing a file signature against a persisted
|
|
1524
|
+
# state/.seen-* marker.
|
|
1525
|
+
# brokk-classify-lib.sh's header owns the status marker contract, including its
|
|
1526
|
+
# independent reported signature and classified position.
|
|
1527
|
+
# These helpers own wake-facing marker routing, the legacy turn-ended signature,
|
|
1528
|
+
# drain-time staleness checks, and guarded bookkeeping writes.
|
|
1529
|
+
|
|
1530
|
+
fm_wake_signal_sig() { # <file> -> reported-state signature
|
|
1531
|
+
case "$1" in
|
|
1532
|
+
*.status)
|
|
1533
|
+
_fm_wake_require_classify || return 1
|
|
1534
|
+
status_observed_signature "$1"
|
|
1535
|
+
;;
|
|
1536
|
+
*)
|
|
1537
|
+
if [ "$_BROKK_UNAME" = Darwin ]; then stat -f '%z:%Fm' "$1" 2>/dev/null; else stat -c '%s:%Y' "$1" 2>/dev/null; fi
|
|
1538
|
+
;;
|
|
1539
|
+
esac
|
|
1540
|
+
}
|
|
1541
|
+
|
|
1542
|
+
fm_wake_signal_seen_path() { # <state> <file>
|
|
1543
|
+
local task
|
|
1544
|
+
case "$2" in
|
|
1545
|
+
*.status)
|
|
1546
|
+
task=$(basename "$2"); task=${task%.status}
|
|
1547
|
+
printf '%s/.seen-%s' "$1" "$(printf '%s.status' "$task" | tr '.' '_')"
|
|
1548
|
+
;;
|
|
1549
|
+
*) printf '%s/.seen-%s' "$1" "$(basename "$2" | tr '.' '_')" ;;
|
|
1550
|
+
esac
|
|
1551
|
+
}
|
|
1552
|
+
|
|
1553
|
+
# The byte size recorded in <file>'s seen marker, or 0 when no marker exists, it
|
|
1554
|
+
# cannot be read, or it does not hold the supported presentation-marker format.
|
|
1555
|
+
# That size is the position the watcher has already classified, independently of
|
|
1556
|
+
# the file signature it has already reported. A 0 means "classify the whole
|
|
1557
|
+
# file", which surfaces events rather than losing them.
|
|
1558
|
+
fm_wake_signal_seen_size() { # <state> <file>
|
|
1559
|
+
local marker sig size
|
|
1560
|
+
marker=$(fm_wake_signal_seen_path "$1" "$2")
|
|
1561
|
+
case "$2" in
|
|
1562
|
+
*.status)
|
|
1563
|
+
_fm_wake_require_classify || { printf '0'; return 0; }
|
|
1564
|
+
status_presentation_marker_offset "$marker" "$2"
|
|
1565
|
+
;;
|
|
1566
|
+
*)
|
|
1567
|
+
sig=$(cat "$marker" 2>/dev/null) || { printf '0'; return 0; }
|
|
1568
|
+
case "$sig" in *:*) size=${sig%%:*} ;; *) size=0 ;; esac
|
|
1569
|
+
case "$size" in ''|*[!0-9]*) printf '0' ;; *) printf '%s' "$size" ;; esac
|
|
1570
|
+
;;
|
|
1571
|
+
esac
|
|
1572
|
+
}
|
|
1573
|
+
|
|
1574
|
+
# 0 when <file>'s current signature matches its recorded reported state.
|
|
1575
|
+
# For a status file this means the current state was already reported, not that
|
|
1576
|
+
# every byte was successfully classified; the separate classified position owns
|
|
1577
|
+
# that fact.
|
|
1578
|
+
# A missing marker or unreadable signature is not a match, so uncertainty reads
|
|
1579
|
+
# as an unreported state.
|
|
1580
|
+
fm_wake_signal_seen_current() { # <state> <file>
|
|
1581
|
+
local sig marker
|
|
1582
|
+
sig=$(fm_wake_signal_sig "$2") || return 1
|
|
1583
|
+
[ -n "$sig" ] || return 1
|
|
1584
|
+
marker=$(fm_wake_signal_seen_path "$1" "$2")
|
|
1585
|
+
case "$2" in
|
|
1586
|
+
*.status)
|
|
1587
|
+
_fm_wake_require_classify || return 1
|
|
1588
|
+
status_presentation_marker_reported_matches "$marker" "$sig"
|
|
1589
|
+
;;
|
|
1590
|
+
*) [ "$(cat "$marker" 2>/dev/null)" = "$sig" ] ;;
|
|
1591
|
+
esac
|
|
1592
|
+
}
|
|
1593
|
+
|
|
1594
|
+
fm_wake_status_reported_commit() { # <state> <status-file> <reported-signature>
|
|
1595
|
+
_fm_wake_require_classify || return 1
|
|
1596
|
+
status_presentation_marker_report "$(fm_wake_signal_seen_path "$1" "$2")" "$3"
|
|
1597
|
+
}
|
|
1598
|
+
|
|
1599
|
+
fm_wake_status_seen_commit() { # <state> <status-file> <captured-end> <captured-identity>
|
|
1600
|
+
_fm_wake_require_classify || return 1
|
|
1601
|
+
status_presentation_marker_commit "$(fm_wake_signal_seen_path "$1" "$2")" "$2" "$3" "$4"
|
|
1602
|
+
}
|
|
1603
|
+
|
|
1604
|
+
# Mark the current complete status snapshot as both reported and classified.
|
|
1605
|
+
# This is the public setup primitive for consumers that adopt an existing log.
|
|
1606
|
+
fm_wake_status_mark_current() { # <state> <status-file>
|
|
1607
|
+
local size ident
|
|
1608
|
+
_fm_wake_require_classify || return 1
|
|
1609
|
+
size=$(_fm_status_file_size "$2") || return 1
|
|
1610
|
+
ident=$(_fm_open_decisions_file_ident "$2") || return 1
|
|
1611
|
+
fm_wake_status_seen_commit "$1" "$2" "$size" "$ident"
|
|
1612
|
+
}
|
|
1613
|
+
|
|
1614
|
+
# Guarded self-announced status append - the one dedup primitive for a status
|
|
1615
|
+
# line THIS home's own machinery writes as bookkeeping it has already presented
|
|
1616
|
+
# in the very turn or tick that writes it (an answerer-closes resolved line, a
|
|
1617
|
+
# pending-reply escalation close, a Allfather-held transfer). Such a close must
|
|
1618
|
+
# not wake the session that wrote it, so this appends the line and then
|
|
1619
|
+
# advances the watcher's seen marker to cover exactly the appended bytes and
|
|
1620
|
+
# nothing else. The advance is provenance-gated and fails toward waking:
|
|
1621
|
+
# - the marker advances ONLY when the file's pre-append signature matched the
|
|
1622
|
+
# recorded seen marker (every earlier byte was already announced or
|
|
1623
|
+
# deliberately absorbed), AND the post-append size equals the pre-append
|
|
1624
|
+
# size plus exactly the appended bytes (no foreign write interleaved);
|
|
1625
|
+
# - on ANY other condition - missing marker, pending foreign bytes, an
|
|
1626
|
+
# interleaved writer, an unreadable signature - the line is still appended
|
|
1627
|
+
# but the marker is left alone, so the watcher surfaces the file normally.
|
|
1628
|
+
# A later, different line from any other writer grows the size past the marker
|
|
1629
|
+
# and wakes as before: task identity alone can never suppress new content.
|
|
1630
|
+
# Returns 0 appended and self-announced, 1 appended but left for the watcher
|
|
1631
|
+
# (the safe direction), 2 the append itself failed.
|
|
1632
|
+
fm_wake_status_append_self_announced() { # <state> <status-file> <line>
|
|
1633
|
+
local state=$1 file=$2 line=$3 marker pre_sig='' pre_size='' pre_ident='' post_size post_ident
|
|
1634
|
+
local LC_ALL=C
|
|
1635
|
+
_fm_wake_require_classify || return 1
|
|
1636
|
+
marker=$(fm_wake_signal_seen_path "$state" "$file")
|
|
1637
|
+
if [ -e "$file" ]; then
|
|
1638
|
+
pre_sig=$(fm_wake_signal_sig "$file") || pre_sig=''
|
|
1639
|
+
pre_size=$(_fm_status_file_size "$file") || pre_size=''
|
|
1640
|
+
pre_ident=$(_fm_open_decisions_file_ident "$file") || pre_ident=''
|
|
1641
|
+
fi
|
|
1642
|
+
printf '%s\n' "$line" >> "$file" || return 2
|
|
1643
|
+
[ -n "$pre_sig" ] || return 1
|
|
1644
|
+
status_presentation_marker_reported_matches "$marker" "$pre_sig" || return 1
|
|
1645
|
+
[ "$(status_presentation_marker_offset "$marker" "$file")" = "$pre_size" ] || return 1
|
|
1646
|
+
post_size=$(_fm_status_file_size "$file") || return 1
|
|
1647
|
+
post_ident=$(_fm_open_decisions_file_ident "$file") || return 1
|
|
1648
|
+
case "$pre_size$post_size" in ''|*[!0-9]*) return 1 ;; esac
|
|
1649
|
+
[ -n "$pre_ident" ] && [ "$post_ident" = "$pre_ident" ] || return 1
|
|
1650
|
+
[ "$post_size" -eq $((pre_size + ${#line} + 1)) ] || return 1
|
|
1651
|
+
fm_wake_status_seen_commit "$state" "$file" "$post_size" "$post_ident" || return 1
|
|
1652
|
+
return 0
|
|
1653
|
+
}
|
|
1654
|
+
|
|
1655
|
+
# Map one structurally valid signal key to its home-local status filename.
|
|
1656
|
+
# Queue payload text is intentionally ignored: it is display data, not a path
|
|
1657
|
+
# authority. The caller still verifies the resulting regular file immediately
|
|
1658
|
+
# before its bounded read.
|
|
1659
|
+
BROKK_WAKE_STATUS_KEY=
|
|
1660
|
+
BROKK_WAKE_STATUS_HISTORICAL=false
|
|
1661
|
+
fm_wake_status_key_map() { # <queue-key>
|
|
1662
|
+
local key=$1 id
|
|
1663
|
+
BROKK_WAKE_STATUS_KEY=
|
|
1664
|
+
BROKK_WAKE_STATUS_HISTORICAL=false
|
|
1665
|
+
case "$key" in
|
|
1666
|
+
*.status)
|
|
1667
|
+
id=${key%.status}
|
|
1668
|
+
;;
|
|
1669
|
+
*.turn-ended)
|
|
1670
|
+
id=${key%.turn-ended}
|
|
1671
|
+
BROKK_WAKE_STATUS_HISTORICAL=true
|
|
1672
|
+
;;
|
|
1673
|
+
*)
|
|
1674
|
+
return 1
|
|
1675
|
+
;;
|
|
1676
|
+
esac
|
|
1677
|
+
case "$id" in
|
|
1678
|
+
''|.*|*[!A-Za-z0-9._-]*) return 1 ;;
|
|
1679
|
+
esac
|
|
1680
|
+
[ "${#id}" -le 64 ] || return 1
|
|
1681
|
+
BROKK_WAKE_STATUS_KEY="$id.status"
|
|
1682
|
+
}
|
|
1683
|
+
|
|
1684
|
+
fm_wake_annotation_manifest() { # <deduped-raw-rows>
|
|
1685
|
+
local rows=$1 epoch seq kind key payload
|
|
1686
|
+
while IFS=$(printf '\t') read -r epoch seq kind key payload; do
|
|
1687
|
+
[ "$kind" = signal ] || continue
|
|
1688
|
+
fm_wake_status_key_map "$key" || continue
|
|
1689
|
+
if [ "$BROKK_WAKE_STATUS_HISTORICAL" = true ]; then
|
|
1690
|
+
printf '%s\thistorical\n' "$BROKK_WAKE_STATUS_KEY"
|
|
1691
|
+
else
|
|
1692
|
+
printf '%s\tdirect\n' "$BROKK_WAKE_STATUS_KEY"
|
|
1693
|
+
fi
|
|
1694
|
+
done <<EOF
|
|
1695
|
+
$rows
|
|
1696
|
+
EOF
|
|
1697
|
+
}
|
|
1698
|
+
|
|
1699
|
+
BROKK_WAKE_EVENT_LINE=
|
|
1700
|
+
BROKK_WAKE_UNREAD_LINES=
|
|
1701
|
+
fm_wake_status_cursor_offset() { # <validated-status-path> -> already-presented byte offset
|
|
1702
|
+
local path=$1 offset
|
|
1703
|
+
command -v status_presentation_cursor_offset >/dev/null 2>&1 || return 1
|
|
1704
|
+
offset=$(status_presentation_cursor_offset "$path" 2>/dev/null) || return 1
|
|
1705
|
+
case "$offset" in ''|*[!0-9]*) return 1 ;; esac
|
|
1706
|
+
printf '%s' "$offset"
|
|
1707
|
+
}
|
|
1708
|
+
|
|
1709
|
+
# O_NOFOLLOW read of every still-unread status byte. min-offset is the
|
|
1710
|
+
# already-presented cursor from classify-lib. Lines whose bytes begin before
|
|
1711
|
+
# that offset are not replayed. Prints nothing and returns 1 when no unread
|
|
1712
|
+
# non-blank line exists.
|
|
1713
|
+
fm_wake_unread_events() { # <validated-status-path> <unused-tail-byte-cap> <min-offset> [<end-offset>]
|
|
1714
|
+
local path=$1 min_offset=$3 end_offset=${4:-} result size chunk chunk_start
|
|
1715
|
+
local LC_ALL=C
|
|
1716
|
+
BROKK_WAKE_EVENT_LINE=
|
|
1717
|
+
BROKK_WAKE_UNREAD_LINES=
|
|
1718
|
+
case "$min_offset" in ''|*[!0-9]*) min_offset=0 ;; esac
|
|
1719
|
+
result=$(perl -MFcntl=:DEFAULT -e '
|
|
1720
|
+
my ($path, $start, $end) = @ARGV;
|
|
1721
|
+
sysopen(my $file, $path, O_RDONLY | O_NOFOLLOW) or exit 1;
|
|
1722
|
+
my @stat = stat $file or exit 1;
|
|
1723
|
+
exit 1 unless -f _;
|
|
1724
|
+
my $size = $stat[7];
|
|
1725
|
+
exit 1 unless $size =~ /\A\d+\z/ && $start =~ /\A\d+\z/ && $start <= $size;
|
|
1726
|
+
$end = $size unless length $end;
|
|
1727
|
+
exit 1 unless $end =~ /\A\d+\z/ && $start <= $end && $end <= $size;
|
|
1728
|
+
seek($file, $start, 0) or exit 1;
|
|
1729
|
+
printf "%s\t", $end or exit 1;
|
|
1730
|
+
my $remaining = $end - $start;
|
|
1731
|
+
while ($remaining > 0) {
|
|
1732
|
+
my $read = read($file, my $buffer, $remaining);
|
|
1733
|
+
exit 1 unless defined $read;
|
|
1734
|
+
last unless $read;
|
|
1735
|
+
print $buffer or exit 1;
|
|
1736
|
+
$remaining -= $read;
|
|
1737
|
+
}
|
|
1738
|
+
' "$path" "$min_offset" "$end_offset" 2>/dev/null) || return 1
|
|
1739
|
+
size=${result%%$'\t'*}
|
|
1740
|
+
chunk=${result#*$'\t'}
|
|
1741
|
+
case "$size" in ''|*[!0-9]*) return 1 ;; esac
|
|
1742
|
+
[ -n "$chunk" ] || return 1
|
|
1743
|
+
[ "$min_offset" -lt "$size" ] || return 1
|
|
1744
|
+
chunk_start=$min_offset
|
|
1745
|
+
BROKK_WAKE_UNREAD_LINES=$(printf '%s' "$chunk" | LC_ALL=C awk -v start="$chunk_start" -v min="$min_offset" '
|
|
1746
|
+
BEGIN { pos = start + 0 }
|
|
1747
|
+
{
|
|
1748
|
+
line_start = pos
|
|
1749
|
+
pos += length($0) + 1
|
|
1750
|
+
if ($0 ~ /[^[:space:]]/ && line_start >= min) print $0
|
|
1751
|
+
}
|
|
1752
|
+
') || return 1
|
|
1753
|
+
[ -n "$BROKK_WAKE_UNREAD_LINES" ] || return 1
|
|
1754
|
+
BROKK_WAKE_EVENT_LINE=$(printf '%s\n' "$BROKK_WAKE_UNREAD_LINES" | tail -1)
|
|
1755
|
+
BROKK_WAKE_EVENT_LINE=$(printf '%s' "$BROKK_WAKE_EVENT_LINE" | LC_ALL=C tr '\t\r' ' ')
|
|
1756
|
+
}
|
|
1757
|
+
|
|
1758
|
+
fm_wake_latest_event() { # <validated-status-path> <tail-byte-cap>
|
|
1759
|
+
fm_wake_unread_events "$1" "$2" 0
|
|
1760
|
+
}
|
|
1761
|
+
|
|
1762
|
+
# Print supplemental drain-time context only after the caller has committed the
|
|
1763
|
+
# raw queue consumption and released the append lock.
|
|
1764
|
+
fm_wake_print_annotations() { # <deduped-raw-rows> [<presentation-snapshot>]
|
|
1765
|
+
local rows=$1 snapshot=${2:-} manifest status_key mode path prefix line task endpoint
|
|
1766
|
+
local snapshot_task snapshot_endpoint _snapshot_ident offset last_event event_line
|
|
1767
|
+
local LC_ALL=C
|
|
1768
|
+
|
|
1769
|
+
manifest=$(fm_wake_annotation_manifest "$rows" | awk -F '\t' '
|
|
1770
|
+
{
|
|
1771
|
+
key = $1
|
|
1772
|
+
if (!(key in seen)) {
|
|
1773
|
+
order[++count] = key
|
|
1774
|
+
seen[key] = 1
|
|
1775
|
+
mode[key] = $2
|
|
1776
|
+
} else if ($2 == "direct") {
|
|
1777
|
+
mode[key] = "direct"
|
|
1778
|
+
}
|
|
1779
|
+
}
|
|
1780
|
+
END {
|
|
1781
|
+
for (i = 1; i <= count; i++) print order[i] "\t" mode[order[i]]
|
|
1782
|
+
}
|
|
1783
|
+
') || return 0
|
|
1784
|
+
|
|
1785
|
+
# Test-only latency seam for proving that queue appends remain independent of
|
|
1786
|
+
# a slow best-effort annotation phase.
|
|
1787
|
+
case "${BROKK_WAKE_ENRICH_TEST_DELAY:-0}" in
|
|
1788
|
+
0) ;;
|
|
1789
|
+
''|*[!0-9]*) ;;
|
|
1790
|
+
*) sleep "$BROKK_WAKE_ENRICH_TEST_DELAY" ;;
|
|
1791
|
+
esac
|
|
1792
|
+
|
|
1793
|
+
while IFS=$(printf '\t') read -r status_key mode; do
|
|
1794
|
+
[ -n "$status_key" ] || continue
|
|
1795
|
+
path="$STATE/$status_key"
|
|
1796
|
+
# A turn-ended-only (historical) row's annotation would show unread status
|
|
1797
|
+
# lines even when those bytes are fully covered by the seen marker - already
|
|
1798
|
+
# surfaced to brokk or deliberately absorbed by the signal triage.
|
|
1799
|
+
# Presenting such an already-announced line again makes a bare turn-end look
|
|
1800
|
+
# like fresh progress, so skip the annotation when the status file's
|
|
1801
|
+
# signature still matches its marker (a proven replay). Any uncertainty -
|
|
1802
|
+
# missing marker, unreadable signature - keeps the annotation with its
|
|
1803
|
+
# existing historical caveat. A direct status row is annotated for every
|
|
1804
|
+
# still-unread line since the last drain presentation; already-presented
|
|
1805
|
+
# bytes are not replayed.
|
|
1806
|
+
if [ "$mode" = historical ] && fm_wake_signal_seen_current "$STATE" "$path"; then
|
|
1807
|
+
continue
|
|
1808
|
+
fi
|
|
1809
|
+
offset=$(fm_wake_status_cursor_offset "$path") || return 1
|
|
1810
|
+
endpoint=
|
|
1811
|
+
if [ -n "$snapshot" ]; then
|
|
1812
|
+
task=${status_key%.status}
|
|
1813
|
+
while IFS=$(printf '\t') read -r snapshot_task snapshot_endpoint _snapshot_ident; do
|
|
1814
|
+
if [ "$snapshot_task" = "$task" ]; then endpoint=$snapshot_endpoint; break; fi
|
|
1815
|
+
done <<EOF
|
|
1816
|
+
$snapshot
|
|
1817
|
+
EOF
|
|
1818
|
+
[ -n "$endpoint" ] || continue
|
|
1819
|
+
fi
|
|
1820
|
+
if [ -n "$endpoint" ] && [ "$offset" -ge "$endpoint" ]; then continue; fi
|
|
1821
|
+
if ! fm_wake_unread_events "$path" 0 "$offset" "$endpoint"; then
|
|
1822
|
+
# Annotation enrichment is supplemental to the already-printed durable
|
|
1823
|
+
# wake rows. A file that disappears, rotates, or becomes unreadable after
|
|
1824
|
+
# the snapshot must not suppress annotations for other status files; the
|
|
1825
|
+
# presentation commit will reject a changed snapshot identity.
|
|
1826
|
+
continue
|
|
1827
|
+
fi
|
|
1828
|
+
last_event=$BROKK_WAKE_EVENT_LINE
|
|
1829
|
+
while IFS= read -r event_line || [ -n "$event_line" ]; do
|
|
1830
|
+
[ -n "$event_line" ] || continue
|
|
1831
|
+
event_line=$(printf '%s' "$event_line" | LC_ALL=C tr '\t\r' ' ')
|
|
1832
|
+
prefix="wake annotation: latest wake-EVENT observed at drain, not current state"
|
|
1833
|
+
if [ "$event_line" != "$last_event" ]; then
|
|
1834
|
+
prefix="wake annotation: unread wake-EVENT since last drain, not current state"
|
|
1835
|
+
fi
|
|
1836
|
+
if [ "$mode" = historical ]; then
|
|
1837
|
+
prefix="$prefix; historical / not necessarily the triggering event"
|
|
1838
|
+
fi
|
|
1839
|
+
line="$prefix: $status_key: $event_line"
|
|
1840
|
+
printf '%s\n' "$line" || return 1
|
|
1841
|
+
done <<EOF
|
|
1842
|
+
$BROKK_WAKE_UNREAD_LINES
|
|
1843
|
+
EOF
|
|
1844
|
+
done <<EOF
|
|
1845
|
+
$manifest
|
|
1846
|
+
EOF
|
|
1847
|
+
|
|
1848
|
+
return 0
|
|
1849
|
+
}
|