session-orchestrator 5.2.0 → 5.4.0
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/skills/architecture/SKILL.md +3 -1
- package/.agents/skills/autopilot/SKILL.md +5 -1
- package/.agents/skills/autopilot/agents/openai.yaml +5 -0
- package/.agents/skills/bootstrap/SKILL.md +5 -1
- package/.agents/skills/bootstrap/agents/openai.yaml +5 -0
- package/.agents/skills/brainstorm/SKILL.md +5 -1
- package/.agents/skills/brainstorm/agents/openai.yaml +5 -0
- package/.agents/skills/claude-md-drift-check/SKILL.md +4 -2
- package/.agents/skills/close/SKILL.md +5 -1
- package/.agents/skills/close/agents/openai.yaml +5 -0
- package/.agents/skills/convergence-monitoring/SKILL.md +4 -2
- package/.agents/skills/debug/SKILL.md +5 -1
- package/.agents/skills/debug/agents/openai.yaml +5 -0
- package/.agents/skills/discovery/SKILL.md +5 -1
- package/.agents/skills/discovery/agents/openai.yaml +5 -0
- package/.agents/skills/dispatcher/SKILL.md +5 -1
- package/.agents/skills/dispatcher/agents/openai.yaml +5 -0
- package/.agents/skills/docs-orchestrator/SKILL.md +3 -1
- package/.agents/skills/ecosystem-health/SKILL.md +3 -1
- package/.agents/skills/eli5/SKILL.md +5 -1
- package/.agents/skills/eli5/agents/openai.yaml +5 -0
- package/.agents/skills/eval/SKILL.md +6 -2
- package/.agents/skills/eval/agents/openai.yaml +5 -0
- package/.agents/skills/evolve/SKILL.md +6 -2
- package/.agents/skills/evolve/agents/openai.yaml +5 -0
- package/.agents/skills/frontmatter-guard/SKILL.md +3 -1
- package/.agents/skills/gitlab-ops/SKILL.md +3 -1
- package/.agents/skills/gitlab-portfolio/SKILL.md +3 -1
- package/.agents/skills/go/SKILL.md +5 -1
- package/.agents/skills/go/agents/openai.yaml +5 -0
- package/.agents/skills/grill/SKILL.md +5 -1
- package/.agents/skills/grill/agents/openai.yaml +5 -0
- package/.agents/skills/harness-audit/SKILL.md +5 -1
- package/.agents/skills/harness-audit/agents/openai.yaml +5 -0
- package/.agents/skills/hook-development/SKILL.md +3 -1
- package/.agents/skills/mcp-builder/SKILL.md +3 -1
- package/.agents/skills/memory-cleanup/SKILL.md +5 -1
- package/.agents/skills/memory-cleanup/agents/openai.yaml +5 -0
- package/.agents/skills/mode-selector/SKILL.md +3 -1
- package/.agents/skills/navigator/SKILL.md +22 -0
- package/.agents/skills/navigator/agents/openai.yaml +5 -0
- package/.agents/skills/npm-publish/SKILL.md +4 -2
- package/.agents/skills/peekaboo-driver/SKILL.md +3 -1
- package/.agents/skills/persona-panel/SKILL.md +5 -1
- package/.agents/skills/persona-panel/agents/openai.yaml +5 -0
- package/.agents/skills/plan/SKILL.md +5 -1
- package/.agents/skills/plan/agents/openai.yaml +5 -0
- package/.agents/skills/playwright-driver/SKILL.md +3 -1
- package/.agents/skills/portfolio/SKILL.md +5 -1
- package/.agents/skills/portfolio/agents/openai.yaml +5 -0
- package/.agents/skills/quality-gates/SKILL.md +3 -1
- package/.agents/skills/reconcile/SKILL.md +5 -1
- package/.agents/skills/reconcile/agents/openai.yaml +5 -0
- package/.agents/skills/release/SKILL.md +5 -1
- package/.agents/skills/release/agents/openai.yaml +5 -0
- package/.agents/skills/remote-offload/SKILL.md +3 -1
- package/.agents/skills/repo-audit/SKILL.md +5 -1
- package/.agents/skills/repo-audit/agents/openai.yaml +5 -0
- package/.agents/skills/session/SKILL.md +21 -0
- package/.agents/skills/session/agents/openai.yaml +5 -0
- package/.agents/skills/session-end/SKILL.md +3 -1
- package/.agents/skills/session-plan/SKILL.md +3 -1
- package/.agents/skills/session-start/SKILL.md +3 -1
- package/.agents/skills/spinout/SKILL.md +5 -1
- package/.agents/skills/spinout/agents/openai.yaml +5 -0
- package/.agents/skills/sunset-review/SKILL.md +5 -1
- package/.agents/skills/sunset-review/agents/openai.yaml +5 -0
- package/.agents/skills/templates-ack/SKILL.md +21 -0
- package/.agents/skills/templates-ack/agents/openai.yaml +5 -0
- package/.agents/skills/test/SKILL.md +5 -1
- package/.agents/skills/test/agents/openai.yaml +5 -0
- package/.agents/skills/test-audit/SKILL.md +21 -0
- package/.agents/skills/test-audit/agents/openai.yaml +5 -0
- package/.agents/skills/test-runner/SKILL.md +3 -1
- package/.agents/skills/tmux-layout/SKILL.md +3 -1
- package/.agents/skills/using-orchestrator/SKILL.md +3 -1
- package/.agents/skills/ux-grill/SKILL.md +5 -1
- package/.agents/skills/ux-grill/agents/openai.yaml +5 -0
- package/.agents/skills/vault-mirror/SKILL.md +3 -1
- package/.agents/skills/vault-sync/SKILL.md +3 -1
- package/.agents/skills/wave-executor/SKILL.md +3 -1
- package/.agents/skills/write-executable-plan/SKILL.md +3 -1
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +4 -4
- package/.codex-plugin/skills/claude-md-drift-check/SKILL.md +1 -1
- package/.codex-plugin/skills/convergence-monitoring/SKILL.md +1 -3
- package/.codex-plugin/skills/eval/SKILL.md +1 -1
- package/.codex-plugin/skills/evolve/SKILL.md +1 -1
- package/.codex-plugin/skills/navigator/SKILL.md +22 -0
- package/.codex-plugin/skills/{convergence-monitoring → navigator}/agents/openai.yaml +2 -2
- package/.codex-plugin/skills/npm-publish/SKILL.md +1 -3
- package/.codex-plugin/skills/session/SKILL.md +1 -1
- package/.codex-plugin/skills/test-audit/SKILL.md +21 -0
- package/.codex-plugin/skills/{npm-publish → test-audit}/agents/openai.yaml +1 -1
- package/.cursor/commands/eval.md +1 -1
- package/.cursor/commands/navigator.md +15 -0
- package/.cursor/commands/session.md +1 -1
- package/.cursor/commands/test-audit.md +14 -0
- package/.cursor/rules/000-session-orchestrator.mdc +0 -2
- package/.cursor/rules/050-plan.mdc +1 -1
- package/.cursor/rules/060-evolve.mdc +27 -12
- package/.cursor/skills/claude-md-drift-check/SKILL.md +1 -1
- package/.cursor/skills/convergence-monitoring/SKILL.md +1 -0
- package/.cursor/skills/eval/SKILL.md +1 -1
- package/.cursor/skills/navigator/SKILL.md +13 -0
- package/.cursor/skills/npm-publish/SKILL.md +1 -0
- package/.cursor/skills/test-audit/SKILL.md +12 -0
- package/.cursor-plugin/plugin.json +1 -1
- package/.orchestrator/policy/blocked-commands.json +12 -3
- package/AGENTS.md +9 -30
- package/CHANGELOG.md +206 -0
- package/README.md +13 -9
- package/SECURITY.md +12 -0
- package/agents/dialectic-deriver.md +13 -10
- package/agents/eval-judge.md +67 -45
- package/agents/skill-applied-judge.md +34 -19
- package/commands/session.md +7 -3
- package/docs/agent-authoring.md +2 -2
- package/docs/baseline.md +22 -7
- package/docs/ci-setup.md +57 -4
- package/docs/codex-setup.md +14 -2
- package/docs/components.md +12 -10
- package/docs/events-schema.md +65 -15
- package/docs/persona-panel.md +5 -4
- package/docs/rule-authoring.md +84 -17
- package/docs/scope-collision-guard.md +26 -4
- package/docs/session-config-reference.md +154 -12
- package/docs/session-config-template.md +41 -3
- package/docs/telemetry.md +2 -0
- package/hooks/_lib/hook-import-set.json +87 -6
- package/hooks/_lib/subagent-transcript.mjs +582 -31
- package/hooks/config-protection.mjs +11 -3
- package/hooks/cwd-change-restore.mjs +11 -3
- package/hooks/enforce-commands.mjs +70 -23
- package/hooks/enforce-scope.mjs +143 -33
- package/hooks/hooks-codex.json +1 -1
- package/hooks/hooks-cursor.json +5 -0
- package/hooks/hooks-pi.json +5 -0
- package/hooks/hooks.json +6 -1
- package/hooks/loop-guard.mjs +11 -3
- package/hooks/on-session-end.mjs +58 -23
- package/hooks/on-session-start.mjs +101 -11
- package/hooks/on-stop.mjs +170 -22
- package/hooks/operator-steer.mjs +11 -3
- package/hooks/post-bash-issue-budget-refund.mjs +18 -8
- package/hooks/post-bash-write-verify.mjs +3 -2
- package/hooks/post-edit-import-probe.mjs +17 -9
- package/hooks/post-edit-validate.mjs +13 -5
- package/hooks/post-subagent-discovery-validator.mjs +98 -13
- package/hooks/post-tool-batch-wave-signal.mjs +202 -38
- package/hooks/post-tool-failure-corrective-context.mjs +11 -5
- package/hooks/post-tooluse-frontend-slop.mjs +10 -4
- package/hooks/pre-auq-clarity.mjs +15 -2
- package/hooks/pre-bash-destructive-guard.mjs +80 -9
- package/hooks/pre-bash-issue-budget.mjs +16 -11
- package/hooks/pre-bash-memory-propose-audit.mjs +137 -56
- package/hooks/pre-bash-sessions-ledger-guard.mjs +419 -22
- package/hooks/pre-bash-staging-fence.mjs +335 -31
- package/hooks/pre-bash-templates-first.mjs +19 -14
- package/hooks/pre-edit-sessions-ledger-guard.mjs +162 -0
- package/hooks/pre-task-scope-disjoint.mjs +792 -74
- package/hooks/subagent-telemetry.mjs +111 -38
- package/hooks/wave-scope-commit-guard.mjs +197 -100
- package/monitors/monitors.json +1 -1
- package/output-styles/wave-summary.md +1 -1
- package/package.json +3 -3
- package/pi/prompts/eval.md +1 -1
- package/pi/prompts/navigator.md +12 -0
- package/pi/prompts/session.md +1 -1
- package/pi/prompts/test-audit.md +12 -0
- package/rules/README.md +7 -2
- package/rules/_index.md +3 -3
- package/rules/always-on/bash-harness-pitfalls.md +7 -1
- package/rules/opt-in-domain/prompt-caching.md +6 -2
- package/rules/opt-in-stack/backend-data.md +10 -2
- package/rules/opt-in-stack/backend.md +10 -4
- package/rules/opt-in-stack/frontend.md +7 -2
- package/rules/opt-in-stack/security-web.md +10 -4
- package/rules/opt-in-stack/swift.md +6 -2
- package/scripts/apply-session-learnings.mjs +492 -0
- package/scripts/archive-closed-prds.mjs +2 -1
- package/scripts/autopilot.mjs +70 -53
- package/scripts/backfill-abandoned-sessions.mjs +118 -16
- package/scripts/baseline-archetypes.mjs +1 -1
- package/scripts/check-package-manager.mjs +62 -5
- package/scripts/check-sessions-integrity.mjs +300 -0
- package/scripts/cursor-install.mjs +1 -4
- package/scripts/dialectic-deriver.mjs +50 -13
- package/scripts/emit-session.mjs +371 -31
- package/scripts/eval-session.mjs +65 -3
- package/scripts/export-hw-learnings.mjs +22 -3
- package/scripts/fleet-checkin.mjs +147 -0
- package/scripts/generate-agents-skills.mjs +102 -29
- package/scripts/generate-cursor-adapter.mjs +61 -16
- package/scripts/lib/agent-frontmatter.mjs +3 -3
- package/scripts/lib/agent-status.mjs +2 -31
- package/scripts/lib/auq/clarity.mjs +10 -2
- package/scripts/lib/auq/parse.mjs +12 -31
- package/scripts/lib/auq/schema.mjs +56 -41
- package/scripts/lib/auto-dialectic.mjs +304 -15
- package/scripts/lib/auto-dream.mjs +7 -6
- package/scripts/lib/autopilot/flags.mjs +18 -2
- package/scripts/lib/autopilot/kill-switches.mjs +6 -3
- package/scripts/lib/autopilot/loop.mjs +19 -2
- package/scripts/lib/autopilot/stall-sampler.mjs +80 -23
- package/scripts/lib/baseline-archetypes.mjs +41 -22
- package/scripts/lib/ci-status-banner.mjs +376 -16
- package/scripts/lib/command-blocker.mjs +275 -28
- package/scripts/lib/config/dialectic.mjs +12 -3
- package/scripts/lib/config/gate.mjs +74 -0
- package/scripts/lib/config/host-paths.mjs +79 -0
- package/scripts/lib/config/persona-gate-wave.mjs +3 -3
- package/scripts/lib/config/reaper.mjs +163 -0
- package/scripts/lib/config-schema.mjs +13 -0
- package/scripts/lib/config.mjs +85 -10
- package/scripts/lib/convergence-monitor.mjs +74 -11
- package/scripts/lib/ecosystem-health.mjs +11 -0
- package/scripts/lib/eval/engine.mjs +421 -53
- package/scripts/lib/eval/judge.mjs +463 -40
- package/scripts/lib/eval/schema.mjs +10 -1
- package/scripts/lib/events-rotation.mjs +221 -25
- package/scripts/lib/events-schema.mjs +114 -0
- package/scripts/lib/events.mjs +545 -7
- package/scripts/lib/fleet-protocol.mjs +270 -0
- package/scripts/lib/frontmatter-guard.mjs +21 -10
- package/scripts/lib/gates/gate-baseline.mjs +40 -5
- package/scripts/lib/gates/gate-full.mjs +35 -5
- package/scripts/lib/gates/gate-helpers.mjs +336 -23
- package/scripts/lib/gates/gate-incremental.mjs +49 -23
- package/scripts/lib/gates/gate-per-file.mjs +42 -8
- package/scripts/lib/gitlab-portfolio/markdown-writer.mjs +6 -1
- package/scripts/lib/instruction-budget-guard.mjs +208 -8
- package/scripts/lib/io.mjs +42 -8
- package/scripts/lib/issue-close-strip-labels.mjs +207 -49
- package/scripts/lib/js-mask.mjs +197 -0
- package/scripts/lib/learnings/evolve-telemetry.mjs +11 -7
- package/scripts/lib/learnings/expiry-sweep.mjs +34 -3
- package/scripts/lib/learnings/io.mjs +154 -26
- package/scripts/lib/learnings/schema.mjs +163 -15
- package/scripts/lib/learnings/sizing-subject.mjs +2 -2
- package/scripts/lib/maintenance-due-banner.mjs +53 -88
- package/scripts/lib/memory-proposals/schema.mjs +11 -5
- package/scripts/lib/named-baseline-resolver.mjs +53 -28
- package/scripts/lib/named-vault-resolver.mjs +104 -5
- package/scripts/lib/orphan-reaper/defaults.mjs +41 -0
- package/scripts/lib/orphan-reaper/ps-snapshot.mjs +181 -0
- package/scripts/lib/orphan-reaper/reaper-audit.mjs +291 -0
- package/scripts/lib/orphan-reaper/reaper-decide.mjs +395 -0
- package/scripts/lib/orphan-reaper/scan-throttle.mjs +92 -0
- package/scripts/lib/orphan-reaper/scan.mjs +506 -0
- package/scripts/lib/orphan-reaper.mjs +316 -0
- package/scripts/lib/owner-yaml.mjs +104 -6
- package/scripts/lib/peer-cards/merger.mjs +48 -10
- package/scripts/lib/peer-cards/reader.mjs +78 -2
- package/scripts/lib/persona-panel/catalog-loader.mjs +1 -1
- package/scripts/lib/process-group.mjs +950 -0
- package/scripts/lib/project-hygiene.mjs +21 -1
- package/scripts/lib/quality-gate.mjs +152 -33
- package/scripts/lib/reconcile/backlog.mjs +368 -0
- package/scripts/lib/reconcile/engine.mjs +55 -188
- package/scripts/lib/reconcile/rule-expiry-sweep.mjs +302 -60
- package/scripts/lib/reconcile/sanitize.mjs +69 -3
- package/scripts/lib/reconcile-nudge-banner.mjs +138 -45
- package/scripts/lib/resource-probe/parsers.mjs +31 -0
- package/scripts/lib/resource-probe.mjs +1 -1
- package/scripts/lib/rule-loader.mjs +141 -74
- package/scripts/lib/rules-sync.mjs +27 -12
- package/scripts/lib/scope-baseline.mjs +3 -4
- package/scripts/lib/scope-echo.mjs +39 -2
- package/scripts/lib/scope-gate.mjs +605 -1
- package/scripts/lib/session-close-backfill.mjs +33 -6
- package/scripts/lib/session-id.mjs +9 -20
- package/scripts/lib/session-invocation.mjs +20 -0
- package/scripts/lib/session-record-repair.mjs +183 -26
- package/scripts/lib/session-registry.mjs +27 -2
- package/scripts/lib/session-schema/constants.mjs +56 -2
- package/scripts/lib/session-schema/normalizer.mjs +56 -4
- package/scripts/lib/session-schema/validator.mjs +28 -1
- package/scripts/lib/session-schema.mjs +8 -3
- package/scripts/lib/session-start-probes.mjs +95 -10
- package/scripts/lib/session-token-rollup.mjs +245 -52
- package/scripts/lib/sessions-canonical.mjs +118 -4
- package/scripts/lib/sessions-integrity-banner.mjs +7 -1
- package/scripts/lib/sessions-staleness-banner.mjs +193 -51
- package/scripts/lib/sh-quote.mjs +29 -0
- package/scripts/lib/skill-evidence-window.mjs +891 -0
- package/scripts/lib/skill-evolution/candidate-intake.mjs +133 -12
- package/scripts/lib/skill-evolution/engine.mjs +18 -9
- package/scripts/lib/skill-judge.mjs +45 -3
- package/scripts/lib/subagents-schema.mjs +30 -3
- package/scripts/lib/tail-window.mjs +56 -0
- package/scripts/lib/telemetry/pricing.mjs +118 -71
- package/scripts/lib/telemetry/schema.mjs +30 -0
- package/scripts/lib/telemetry/sync.mjs +61 -6
- package/scripts/lib/telemetry-flush-health-banner.mjs +4 -22
- package/scripts/lib/test-runner/issue-reconcile.mjs +48 -16
- package/scripts/lib/tests-src-ratio.mjs +140 -23
- package/scripts/lib/tmux-layout/layouts.mjs +1 -11
- package/scripts/lib/tmux-layout/telemetry-stats.mjs +72 -13
- package/scripts/lib/tmux-layout/tmux-shell.mjs +2 -11
- package/scripts/lib/user-invocable-skills.mjs +23 -3
- package/scripts/lib/ux-grill/reconcile.mjs +48 -22
- package/scripts/lib/validate/check-agents-skills.mjs +26 -15
- package/scripts/lib/validate/check-agents.mjs +1 -1
- package/scripts/lib/validate/check-cursor-adapter.mjs +1 -0
- package/scripts/lib/validate/check-entry-guard.mjs +13 -50
- package/scripts/lib/validate/check-hook-entry-guards.mjs +636 -0
- package/scripts/lib/validate/check-hooks-symmetry.mjs +5 -0
- package/scripts/lib/validate/check-learnings-shell-writes.mjs +214 -0
- package/scripts/lib/validate/check-pi-prompts.mjs +1 -0
- package/scripts/lib/validate/check-rules.mjs +7 -5
- package/scripts/lib/validate/check-skill-links.mjs +9 -1
- package/scripts/lib/validate/check-skill-script-paths.mjs +239 -27
- package/scripts/lib/validate/check-test-git-config-target.mjs +24 -34
- package/scripts/lib/validate/check-test-value-bans.mjs +87 -14
- package/scripts/lib/validate/check-unicode-safety.mjs +1 -0
- package/scripts/lib/validate/check-untracked-test-deps.mjs +7 -102
- package/scripts/lib/validate/check-unwired-features.mjs +130 -27
- package/scripts/lib/validate/check-validator-registration.mjs +34 -10
- package/scripts/lib/validate/confidential-names.mjs +10 -0
- package/scripts/lib/validate-vendored-rules.mjs +210 -84
- package/scripts/lib/vault-mirror/namespace.mjs +46 -8
- package/scripts/lib/vault-mirror/process.mjs +10 -3
- package/scripts/lib/vault-mirror/render-sessions.mjs +12 -2
- package/scripts/lib/vault-mirror/telemetry.mjs +8 -1
- package/scripts/lib/vault-status/board-writer.mjs +48 -11
- package/scripts/lib/vault-status/narrative-mirror.mjs +71 -16
- package/scripts/lib/vault-yaml.mjs +118 -0
- package/scripts/lib/wave-executor/dispatch-common.mjs +10 -18
- package/scripts/lib/wave-executor/foreign-dispatch.mjs +1 -1
- package/scripts/lib/wave-executor/remote-dispatch.mjs +1 -1
- package/scripts/lib/wave-resource-gate.mjs +34 -8
- package/scripts/lib/worktree/lifecycle.mjs +153 -1
- package/scripts/memory-propose.mjs +18 -5
- package/scripts/release-session-lock.mjs +306 -0
- package/scripts/release.mjs +40 -8
- package/scripts/repair-invalid-sessions.mjs +37 -4
- package/scripts/resolve-session-invocation.mjs +59 -0
- package/scripts/run-migrate-v2-cross-repo.mjs +158 -30
- package/scripts/run-quality-gate.mjs +357 -197
- package/scripts/site-numbers.mjs +163 -5
- package/scripts/sweep-expired-learnings.mjs +56 -5
- package/scripts/sweep-expired-rules.mjs +16 -4
- package/scripts/validate-plugin.mjs +19 -0
- package/scripts/validate-wave-scope.mjs +32 -105
- package/scripts/vault-integration-watcher.mjs +2 -1
- package/scripts/vault-mirror.mjs +81 -47
- package/scripts/wave-scope-binding.mjs +2 -1
- package/skills/_shared/config-reading.md +3 -3
- package/skills/_shared/fleet-protocol.md +131 -0
- package/skills/_shared/platform-tools.md +23 -11
- package/skills/_shared/state-ownership.md +5 -0
- package/skills/autopilot/SKILL.md +47 -18
- package/skills/bootstrap/SKILL.md +1 -1
- package/skills/bootstrap/_shared-template.md +1 -1
- package/skills/bootstrap/fast-template.md +1 -1
- package/skills/bootstrap/private-contract.md +2 -1
- package/skills/bootstrap/public-fallback.md +6 -3
- package/skills/bootstrap/references/bootstrap-retroactive-flow.md +2 -1
- package/skills/claude-md-drift-check/SKILL.md +10 -10
- package/skills/claude-md-drift-check/checker.mjs +26 -18
- package/skills/convergence-monitoring/README.md +8 -1
- package/skills/convergence-monitoring/SIGNALS.md +50 -6
- package/skills/convergence-monitoring/SKILL.md +15 -6
- package/skills/eval/SKILL.md +39 -24
- package/skills/eval/rubric-v1.md +1 -0
- package/skills/eval/rubric-v2.md +457 -0
- package/skills/evolve/SKILL.md +3 -3
- package/skills/evolve/references/evolve-analyze-mode.md +10 -6
- package/skills/evolve/references/evolve-dialectic-mode.md +42 -25
- package/skills/gitlab-ops/SKILL.md +3 -2
- package/skills/memory-cleanup/SKILL.md +1 -1
- package/skills/navigator/SKILL.md +193 -0
- package/skills/navigator/references/account-check.sh +87 -0
- package/skills/navigator/references/fleet-decisions.mjs +231 -0
- package/skills/navigator/references/slot-check.sh +114 -0
- package/skills/npm-publish/SKILL.md +1 -1
- package/skills/persona-panel/SKILL.md +2 -2
- package/skills/persona-panel/persona-format.md +2 -2
- package/skills/persona-panel/presets/designer-lens.md +1 -1
- package/skills/persona-panel/presets/engineer-lens.md +1 -1
- package/skills/persona-panel/presets/pm-lens.md +1 -1
- package/skills/plan/mode-retro.md +19 -4
- package/skills/quality-gates/SKILL.md +6 -4
- package/skills/reconcile/SKILL.md +11 -0
- package/skills/remote-offload/SKILL.md +4 -2
- package/skills/session-end/SKILL.md +13 -16
- package/skills/session-end/discovery-scan.md +1 -1
- package/skills/session-end/learning-patterns.md +22 -12
- package/skills/session-end/metrics-collection.md +5 -6
- package/skills/session-end/phase-3-6-tail.md +55 -9
- package/skills/session-end/references/phase-3-documentation-updates.md +2 -2
- package/skills/session-end/references/phase-4a-worktree-cleanup.md +2 -0
- package/skills/session-end/references/phase-5-issue-cleanup.md +9 -14
- package/skills/session-end/references/session-summary-template.md +1 -1
- package/skills/session-end/session-metrics-write.md +32 -60
- package/skills/session-plan/SKILL.md +17 -8
- package/skills/session-plan/references/session-plan-task-classification.md +2 -2
- package/skills/session-start/SKILL.md +6 -0
- package/skills/session-start/references/operations-contract.md +4 -0
- package/skills/session-start/references/phase-1-7-vault-status-board.md +1 -1
- package/skills/session-start/references/phase-4-ssot-environment-check.md +4 -3
- package/skills/test-audit/SKILL.md +152 -0
- package/skills/test-audit/references/campaign.md +111 -0
- package/skills/test-audit/references/mutate.sh +50 -0
- package/skills/ux-grill/SKILL.md +1 -1
- package/skills/wave-executor/SKILL.md +9 -5
- package/skills/wave-executor/circuit-breaker.md +2 -0
- package/skills/wave-executor/references/wave-executor-state-init.md +8 -3
- package/skills/wave-executor/references/wave-loop-dispatch.md +5 -2
- package/skills/wave-executor/references/wave-loop-review.md +3 -5
- package/templates/personas/README.md +5 -5
- package/templates/personas/accounting-compliance.v1.md +1 -1
- package/templates/personas/accounting-tax-advisor.v1.md +1 -1
- package/templates/personas/buyer-p1-cto.v1.md +1 -1
- package/templates/personas/buyer-p2-kanzlei.v1.md +1 -1
- package/templates/personas/buyer-p3-build.v1.md +1 -1
- package/templates/personas/buyer-p4-tech-veto.v1.md +1 -1
- package/templates/personas/buyer-p5-solo.v1.md +1 -1
- package/templates/personas/buyer-p6-ld.v1.md +1 -1
- package/templates/personas/klima-ai-expert.v1.md +1 -1
- package/templates/personas/klima-physicist.v1.md +1 -1
- package/.cursor/commands/convergence-monitoring.md +0 -13
- package/.cursor/commands/npm-publish.md +0 -13
- package/pi/prompts/convergence-monitoring.md +0 -11
- package/pi/prompts/npm-publish.md +0 -11
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* orphan-reaper/ps-snapshot.mjs — the binding `ps` format: how the reaper
|
|
3
|
+
* measures the process table, and how it reads what it measured.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { spawn } from 'node:child_process';
|
|
7
|
+
|
|
8
|
+
import { parseEtimeToSeconds } from '../resource-probe/parsers.mjs';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* The binding `ps` invocation (Discovery d-2, 2026-09-21, Darwin 25.6.0).
|
|
12
|
+
*
|
|
13
|
+
* Headerless (`=` per field) and six NUMERIC fields before `args`, because on
|
|
14
|
+
* macOS `comm` is a 16-character-truncated PATH that may contain spaces (68 of
|
|
15
|
+
* 784 processes carried a space, 13 of them survived the truncation) — appending
|
|
16
|
+
* `args` after `comm` breaks whitespace splitting outright. `-ww` disables column
|
|
17
|
+
* truncation; `ps` escapes control characters, so one process is one line.
|
|
18
|
+
* `rss` is in KiB. Roundtrip measured at ~47 ms for 287 KB / 784 processes.
|
|
19
|
+
*
|
|
20
|
+
* `pgid=` is the sixth column and is what lets a row join the ledger as a GROUP
|
|
21
|
+
* MEMBER rather than only as the recorded leader (see `reaper-decide.mjs`).
|
|
22
|
+
* Availability measured 2026-09-22 on Darwin 25.6.0 — `ps -Aww -o
|
|
23
|
+
* pid=,ppid=,pgid=,rss=,etime=,%cpu=,args=` exits 0 and prints the column; it is
|
|
24
|
+
* POSIX (`pgid` is a standard `-o` keyword) and present on Linux `procps` too.
|
|
25
|
+
*
|
|
26
|
+
* A targeted call, NOT the full `probe()` — that one spawns up to five
|
|
27
|
+
* subprocesses and has no caching (PRD § B4).
|
|
28
|
+
*/
|
|
29
|
+
export const PS_ARGS = Object.freeze(['-Aww', '-o', 'pid=,ppid=,pgid=,rss=,etime=,%cpu=,args=']);
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* The TARGETED variant of {@link PS_ARGS}: one pid, same seven columns.
|
|
33
|
+
*
|
|
34
|
+
* Used immediately before every signal and once after the ladder (B3/B6). It is
|
|
35
|
+
* a separate call rather than a re-run of the full `-A` scan because the whole
|
|
36
|
+
* point is freshness at the moment of signalling — a 784-row snapshot taken for
|
|
37
|
+
* the population is already stale by the time the ladder's 10 s grace elapses,
|
|
38
|
+
* and re-taking it per signal would cost 287 KB to learn one row.
|
|
39
|
+
*
|
|
40
|
+
* @param {number} pid
|
|
41
|
+
* @returns {string[]}
|
|
42
|
+
*/
|
|
43
|
+
export function psPidArgs(pid) {
|
|
44
|
+
return ['-ww', '-p', String(pid), '-o', 'pid=,ppid=,pgid=,rss=,etime=,%cpu=,args='];
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Wall-clock ceiling for one `ps` call. Same 2 s as `runPsDetailed()` in
|
|
48
|
+
* `resource-probe/probe-platform.mjs`, whose spawn/timeout shape this mirrors. */
|
|
49
|
+
const PS_TIMEOUT_MS = 2000;
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* One parsed `ps` row.
|
|
53
|
+
* @typedef {object} PsRow
|
|
54
|
+
* @property {number} pid
|
|
55
|
+
* @property {number} ppid
|
|
56
|
+
* @property {number} pgid Process-group id — the ledger's group join key.
|
|
57
|
+
* @property {number} rssKb Resident set size in KiB.
|
|
58
|
+
* @property {number} etimeSeconds Elapsed seconds since exec.
|
|
59
|
+
* @property {number} cpuPct
|
|
60
|
+
* @property {string} args Full command line.
|
|
61
|
+
*/
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Parse `ps` output in the {@link PS_ARGS} format, tolerantly — and COUNT what
|
|
65
|
+
* it drops.
|
|
66
|
+
*
|
|
67
|
+
* A silently skipping parser turns a partial read into a clean verdict, which is
|
|
68
|
+
* the exact failure mode a reaper must not have; hence the `malformed` count
|
|
69
|
+
* ({@link parsePsSnapshotDetailed}). A row whose `etime` does not parse is
|
|
70
|
+
* treated as malformed rather than returned with a null age: every returned row
|
|
71
|
+
* must carry a usable age, because age is a load-bearing gate here.
|
|
72
|
+
*
|
|
73
|
+
* @param {string|null|undefined} text
|
|
74
|
+
* @returns {{rows: PsRow[], malformed: number}}
|
|
75
|
+
*/
|
|
76
|
+
export function parsePsSnapshotDetailed(text) {
|
|
77
|
+
if (text === null || text === undefined) return { rows: [], malformed: 0 };
|
|
78
|
+
/** @type {PsRow[]} */
|
|
79
|
+
const rows = [];
|
|
80
|
+
let malformed = 0;
|
|
81
|
+
// Six whitespace-free fields, then `args` as the ENTIRE rest of the line —
|
|
82
|
+
// args legitimately contains spaces, so it must never be split.
|
|
83
|
+
const rowRe = /^\s*(\d+)\s+(\d+)\s+(\d+)\s+(\d+)\s+(\S+)\s+(\S+)(?:\s+(.*))?$/;
|
|
84
|
+
for (const raw of String(text).split(/\r?\n/)) {
|
|
85
|
+
if (raw.trim().length === 0) continue;
|
|
86
|
+
const m = rowRe.exec(raw);
|
|
87
|
+
if (!m) { malformed += 1; continue; }
|
|
88
|
+
const etimeSeconds = parseEtimeToSeconds(m[5]);
|
|
89
|
+
const cpuPct = parseFloat(m[6]);
|
|
90
|
+
if (etimeSeconds === null || Number.isNaN(cpuPct)) { malformed += 1; continue; }
|
|
91
|
+
rows.push({
|
|
92
|
+
pid: parseInt(m[1], 10),
|
|
93
|
+
ppid: parseInt(m[2], 10),
|
|
94
|
+
pgid: parseInt(m[3], 10),
|
|
95
|
+
rssKb: parseInt(m[4], 10),
|
|
96
|
+
etimeSeconds,
|
|
97
|
+
cpuPct,
|
|
98
|
+
args: m[7] ?? '',
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
return { rows, malformed };
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* {@link parsePsSnapshotDetailed} without the drop count.
|
|
106
|
+
* @param {string|null|undefined} text
|
|
107
|
+
* @returns {PsRow[]}
|
|
108
|
+
*/
|
|
109
|
+
export function parsePsSnapshot(text) {
|
|
110
|
+
return parsePsSnapshotDetailed(text).rows;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Default `ps` runner: the {@link PS_ARGS} call with a hard 2 s deadline,
|
|
115
|
+
* SIGKILL on overrun, and `null` on any failure. Never throws.
|
|
116
|
+
*
|
|
117
|
+
* Mirrors `runPsDetailed()` in `resource-probe/probe-platform.mjs` — the same
|
|
118
|
+
* spawn/settle/timeout shape, a different column set.
|
|
119
|
+
*
|
|
120
|
+
* @param {number} [timeoutMs]
|
|
121
|
+
* @returns {Promise<string|null>}
|
|
122
|
+
*/
|
|
123
|
+
export function runPs(timeoutMs = PS_TIMEOUT_MS) {
|
|
124
|
+
return runPsArgs([...PS_ARGS], timeoutMs);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Default TARGETED `ps` runner (B3/B6): {@link psPidArgs} with the same hard
|
|
129
|
+
* deadline and the same `null`-on-any-failure contract as {@link runPs}.
|
|
130
|
+
*
|
|
131
|
+
* `ps -p <gone-pid>` exits NON-ZERO with empty stdout on macOS, which
|
|
132
|
+
* {@link runPsArgs} maps to `null` — so "gone" and "could not measure" arrive
|
|
133
|
+
* here as the same value. That is why the caller treats a `null` as UNMEASURED
|
|
134
|
+
* and refuses the signal, rather than reading it as proof of death: refusing on
|
|
135
|
+
* an absent measurement costs one skipped reap, believing it costs a bystander.
|
|
136
|
+
*
|
|
137
|
+
* @param {number} pid
|
|
138
|
+
* @param {number} [timeoutMs]
|
|
139
|
+
* @returns {Promise<string|null>}
|
|
140
|
+
*/
|
|
141
|
+
export function runPsPid(pid, timeoutMs = PS_TIMEOUT_MS) {
|
|
142
|
+
if (!Number.isInteger(pid) || pid <= 0) return Promise.resolve(null);
|
|
143
|
+
return runPsArgs(psPidArgs(pid), timeoutMs);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Spawn `ps` with `args`, capped at `timeoutMs`, `null` on any failure.
|
|
148
|
+
* Never throws. Mirrors `runPsDetailed()` in `resource-probe/probe-platform.mjs`.
|
|
149
|
+
*
|
|
150
|
+
* @param {string[]} args
|
|
151
|
+
* @param {number} timeoutMs
|
|
152
|
+
* @returns {Promise<string|null>}
|
|
153
|
+
*/
|
|
154
|
+
function runPsArgs(args, timeoutMs) {
|
|
155
|
+
return new Promise((resolve) => {
|
|
156
|
+
if (process.platform === 'win32') { resolve(null); return; }
|
|
157
|
+
let settled = false;
|
|
158
|
+
const finish = (value) => {
|
|
159
|
+
if (settled) return;
|
|
160
|
+
settled = true;
|
|
161
|
+
resolve(value);
|
|
162
|
+
};
|
|
163
|
+
try {
|
|
164
|
+
const child = spawn('ps', args, { stdio: ['ignore', 'pipe', 'ignore'] });
|
|
165
|
+
const chunks = [];
|
|
166
|
+
child.stdout.on('data', (c) => chunks.push(c));
|
|
167
|
+
child.on('error', () => finish(null));
|
|
168
|
+
child.on('close', (code) => {
|
|
169
|
+
if (code !== 0) return finish(null);
|
|
170
|
+
finish(Buffer.concat(chunks).toString('utf8'));
|
|
171
|
+
});
|
|
172
|
+
const timer = setTimeout(() => {
|
|
173
|
+
try { child.kill('SIGKILL'); } catch { /* already gone */ }
|
|
174
|
+
finish(null);
|
|
175
|
+
}, timeoutMs);
|
|
176
|
+
timer.unref?.();
|
|
177
|
+
} catch {
|
|
178
|
+
finish(null);
|
|
179
|
+
}
|
|
180
|
+
});
|
|
181
|
+
}
|
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* orphan-reaper/reaper-audit.mjs — the B5 kill audit: its record shape, its
|
|
3
|
+
* writer, its bounded reader and pruner, and the HR-101 false-alarm rate the
|
|
4
|
+
* reader exists for.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
appendFileSync,
|
|
9
|
+
lstatSync,
|
|
10
|
+
mkdirSync,
|
|
11
|
+
renameSync,
|
|
12
|
+
statSync,
|
|
13
|
+
unlinkSync,
|
|
14
|
+
writeFileSync,
|
|
15
|
+
} from 'node:fs';
|
|
16
|
+
import path from 'node:path';
|
|
17
|
+
|
|
18
|
+
import { readTailWindow } from '../tail-window.mjs';
|
|
19
|
+
import { REAPER_DEFAULTS, underRepo } from './defaults.mjs';
|
|
20
|
+
|
|
21
|
+
/** Relative path of the JSONL kill audit (B5). Gitignored via `.gitignore:55`
|
|
22
|
+
* (`.orchestrator/metrics/*.jsonl`), verified with `git check-ignore --no-index -v`.
|
|
23
|
+
* Under the `ledger-delete-protected` policy rule: in-process fs only, never a
|
|
24
|
+
* shell `rm`/`mv`. */
|
|
25
|
+
export const REAPER_AUDIT_RELPATH = '.orchestrator/metrics/reaper-audit.jsonl';
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* HR-101 ceiling: above this false-alarm rate the INSTRUMENT is suspect, and
|
|
29
|
+
* the answer is to re-aim it — never to raise the threshold it fires on. Surfaced
|
|
30
|
+
* as `instrumentSuspect` in the scan result; nothing in the reaper acts on it,
|
|
31
|
+
* because acting on a broken instrument is the failure it names.
|
|
32
|
+
*/
|
|
33
|
+
export const FALSE_ALARM_SUSPECT_RATE = 0.1;
|
|
34
|
+
|
|
35
|
+
/** Characters of `args` copied into an audit record — and ONLY for a command
|
|
36
|
+
* that already matched the read-only allowlist, so a foreign process's command
|
|
37
|
+
* line (which may carry paths or tokens) never reaches the audit. */
|
|
38
|
+
const ARGS_HEAD_CHARS = 80;
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Size ceiling of the audit file and what a prune keeps. Named ceiling (BV-004):
|
|
42
|
+
* a `kill` record with `args_head` and `result` serialises to 505 bytes, a
|
|
43
|
+
* `report` record to 285 (measured on {@link buildAuditRecord} output 2026-10-01;
|
|
44
|
+
* no real audit file existed on this host), so the kept 512 KiB hold ~1,000-1,800
|
|
45
|
+
* decisions against the 50 the false-alarm window reads. Revisit if
|
|
46
|
+
* `reaper.false-alarm-window` is ever configured above ~1,000.
|
|
47
|
+
*/
|
|
48
|
+
const REAPER_AUDIT_MAX_BYTES = 1024 * 1024;
|
|
49
|
+
const REAPER_AUDIT_KEEP_BYTES = 512 * 1024;
|
|
50
|
+
|
|
51
|
+
/** First tail window {@link readAuditRecords} tries; it grows ×4 only while the
|
|
52
|
+
* window holds fewer complete lines than requested. */
|
|
53
|
+
const AUDIT_TAIL_START_BYTES = 64 * 1024;
|
|
54
|
+
|
|
55
|
+
/** Absolute path of the kill audit for a repo. One constant per path.
|
|
56
|
+
* @param {string} repoRoot @returns {string} */
|
|
57
|
+
export function auditPath(repoRoot) {
|
|
58
|
+
return underRepo(repoRoot, REAPER_AUDIT_RELPATH);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Build one B5 audit record.
|
|
63
|
+
*
|
|
64
|
+
* Exactly ONE `trigger` per record, taken from the single verdict
|
|
65
|
+
* `decideReapCandidates` assigned at its fixed priority — a kill carries
|
|
66
|
+
* `orphan-ppid1`, a report carries the reason it was reported for. That is what
|
|
67
|
+
* makes "why exactly was this killed" answerable afterwards; a record listing
|
|
68
|
+
* two triggers would answer it with a shrug.
|
|
69
|
+
*
|
|
70
|
+
* `reason` is separate from `trigger` and present on every WITHDRAWAL: the
|
|
71
|
+
* trigger says what made this a candidate, the reason says what took it back.
|
|
72
|
+
* `args_head` is present ONLY for a command that cleared the read-only
|
|
73
|
+
* allowlist — see {@link ARGS_HEAD_CHARS}.
|
|
74
|
+
*
|
|
75
|
+
* @param {object} entry A candidate or a reported/rejected entry.
|
|
76
|
+
* @param {'kill'|'report'|'reject'|'dry-run'} decision
|
|
77
|
+
* @param {object} [extra]
|
|
78
|
+
* @param {string} extra.timestamp
|
|
79
|
+
* @param {string|null} [extra.sessionId]
|
|
80
|
+
* @param {object} [extra.result]
|
|
81
|
+
* @param {string} [extra.reason]
|
|
82
|
+
* @returns {object}
|
|
83
|
+
*/
|
|
84
|
+
export function buildAuditRecord(entry, decision, {
|
|
85
|
+
timestamp, sessionId = null, result, reason,
|
|
86
|
+
} = {}) {
|
|
87
|
+
/** @type {Record<string, unknown>} */
|
|
88
|
+
const record = {
|
|
89
|
+
timestamp,
|
|
90
|
+
session_id: sessionId,
|
|
91
|
+
pid: entry.pid,
|
|
92
|
+
pgid: entry.pgid ?? null,
|
|
93
|
+
trigger: entry.trigger ?? entry.reason ?? null,
|
|
94
|
+
threshold: entry.threshold ?? null,
|
|
95
|
+
actual: entry.actual ?? (typeof entry.ageSeconds === 'number' ? { ageSeconds: entry.ageSeconds } : null),
|
|
96
|
+
unit: 'seconds',
|
|
97
|
+
command_signature: entry.commandSignature ?? null,
|
|
98
|
+
decision,
|
|
99
|
+
};
|
|
100
|
+
if (typeof entry.args === 'string' && entry.args.length > 0) {
|
|
101
|
+
record.args_head = entry.args.slice(0, ARGS_HEAD_CHARS);
|
|
102
|
+
}
|
|
103
|
+
if (result !== undefined) record.result = result;
|
|
104
|
+
const effectiveReason = reason ?? (decision === 'report' ? entry.reason : undefined);
|
|
105
|
+
if (effectiveReason !== undefined && effectiveReason !== null) record.reason = effectiveReason;
|
|
106
|
+
return record;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Default audit sink: append one JSONL line to {@link REAPER_AUDIT_RELPATH}.
|
|
111
|
+
* In-process fs only (the path sits under the `ledger-delete-protected` policy
|
|
112
|
+
* rule). Best-effort — an audit write must never fail a scan — but a failure
|
|
113
|
+
* prints one WARN line rather than vanishing.
|
|
114
|
+
*
|
|
115
|
+
* @param {string} repoRoot
|
|
116
|
+
* @param {object} record
|
|
117
|
+
* @returns {void}
|
|
118
|
+
*/
|
|
119
|
+
export function appendAuditRecord(repoRoot, record) {
|
|
120
|
+
const target = auditPath(repoRoot);
|
|
121
|
+
try {
|
|
122
|
+
mkdirSync(path.dirname(target), { recursive: true });
|
|
123
|
+
appendFileSync(target, `${JSON.stringify(record)}\n`, 'utf8');
|
|
124
|
+
} catch (err) {
|
|
125
|
+
process.stderr.write(
|
|
126
|
+
`orphan-reaper: could not append to ${REAPER_AUDIT_RELPATH}: ${err?.message ?? String(err)}\n`,
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Default audit READER — the other half of {@link appendAuditRecord}, and the
|
|
133
|
+
* data source {@link falseAlarmRate} needs (B5: "Dieselbe Datei ist die
|
|
134
|
+
* Datenquelle für `reaper.false-alarm-window`").
|
|
135
|
+
*
|
|
136
|
+
* Returns the parsed records among the LAST `limit` non-empty lines, oldest
|
|
137
|
+
* first — the same population a whole-file read sliced to `limit` lines yields,
|
|
138
|
+
* read from a tail window instead. The window grows only while it holds fewer
|
|
139
|
+
* complete lines than `limit`, so records larger than expected cannot shrink the
|
|
140
|
+
* population; {@link pruneReaperAudit} bounds the worst case at the whole file.
|
|
141
|
+
* The first line of a window that does not start at byte 0 is a fragment and is
|
|
142
|
+
* dropped before anything is parsed.
|
|
143
|
+
*
|
|
144
|
+
* A malformed line is dropped from the rate's population rather than counted,
|
|
145
|
+
* because an unreadable record carries no decision to classify — and the rate
|
|
146
|
+
* reports its own `n`, so a shrinking population is visible in the number's
|
|
147
|
+
* denominator.
|
|
148
|
+
*
|
|
149
|
+
* @param {string} repoRoot
|
|
150
|
+
* @param {number} [limit]
|
|
151
|
+
* @returns {object[]} parsed records in chronological order; `[]` on any failure
|
|
152
|
+
*/
|
|
153
|
+
export function readAuditRecords(repoRoot, limit = REAPER_DEFAULTS.falseAlarmWindow) {
|
|
154
|
+
const target = auditPath(repoRoot);
|
|
155
|
+
const want = Math.max(1, limit);
|
|
156
|
+
let lines;
|
|
157
|
+
try {
|
|
158
|
+
let windowBytes = AUDIT_TAIL_START_BYTES;
|
|
159
|
+
for (;;) {
|
|
160
|
+
const { text, cut } = readTailWindow(target, windowBytes);
|
|
161
|
+
const all = text.split('\n');
|
|
162
|
+
if (cut) all.shift();
|
|
163
|
+
lines = all.filter((l) => l.trim().length > 0);
|
|
164
|
+
if (!cut || lines.length >= want) break;
|
|
165
|
+
windowBytes *= 4;
|
|
166
|
+
}
|
|
167
|
+
} catch {
|
|
168
|
+
return [];
|
|
169
|
+
}
|
|
170
|
+
const records = [];
|
|
171
|
+
for (const line of lines.slice(-want)) {
|
|
172
|
+
try {
|
|
173
|
+
const parsed = JSON.parse(line);
|
|
174
|
+
if (parsed && typeof parsed === 'object') records.push(parsed);
|
|
175
|
+
} catch {
|
|
176
|
+
/* an unreadable line carries no decision to classify */
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
return records;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Replace the audit atomically: write `<target>.tmp-<pid>` beside it, then
|
|
184
|
+
* rename over it. Throws instead of writing when the target is a symlink or not
|
|
185
|
+
* a regular file. `writeFileSync(target)` would truncate first — a scan child
|
|
186
|
+
* killed between truncate and write left the "why was this killed" record empty
|
|
187
|
+
* — and would write THROUGH a symlink, cutting e.g. a linked `events.jsonl` to
|
|
188
|
+
* 512 KiB (CWE-59, reproduced 2026-10-01). `rename` replaces the link itself,
|
|
189
|
+
* and `wx` refuses a pre-planted file or link at the tmp name. The same few
|
|
190
|
+
* lines as `process-group.mjs`'s `replaceRegularFile`, kept local because that
|
|
191
|
+
* module is not this one's to depend on for a file write.
|
|
192
|
+
*
|
|
193
|
+
* @param {string} target
|
|
194
|
+
* @param {string} body
|
|
195
|
+
*/
|
|
196
|
+
function replaceRegularFile(target, body) {
|
|
197
|
+
if (!lstatSync(target).isFile()) {
|
|
198
|
+
throw new Error('not a regular file (a symlink is never written through) — left untouched');
|
|
199
|
+
}
|
|
200
|
+
const tmp = `${target}.tmp-${process.pid}`;
|
|
201
|
+
try {
|
|
202
|
+
writeFileSync(tmp, body, { encoding: 'utf8', flag: 'wx' });
|
|
203
|
+
renameSync(tmp, target);
|
|
204
|
+
} catch (err) {
|
|
205
|
+
// EEXIST: the tmp name belongs to someone else — never remove it.
|
|
206
|
+
if (err?.code !== 'EEXIST') {
|
|
207
|
+
try { unlinkSync(tmp); } catch { /* nothing was created */ }
|
|
208
|
+
}
|
|
209
|
+
throw err;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Size-bounded prune of the audit: once the file passes `maxBytes`, rewrite it
|
|
215
|
+
* atomically with its newest `keepBytes`, cut at a line boundary. The same
|
|
216
|
+
* rewrite shape as `pruneGateProcessLedger` (`process-group.mjs`), the other
|
|
217
|
+
* ledger the scan's housekeeping prunes; the audit is pruned by SIZE rather than
|
|
218
|
+
* age because its population is a rolling window of decisions, not of days.
|
|
219
|
+
*
|
|
220
|
+
* Never throws; a failure — including a symlinked or non-regular audit, which
|
|
221
|
+
* is refused rather than written through — prints one WARN line and leaves the
|
|
222
|
+
* file as it was.
|
|
223
|
+
* Named ceiling (BV-004): an append that lands between the read and the write
|
|
224
|
+
* is lost — a window of one read + one write, once per ~0.5 MB of audit.
|
|
225
|
+
*
|
|
226
|
+
* @param {string} repoRoot
|
|
227
|
+
* @param {object} [opts]
|
|
228
|
+
* @param {number} [opts.maxBytes]
|
|
229
|
+
* @param {number} [opts.keepBytes]
|
|
230
|
+
* @returns {number} bytes removed; 0 when nothing was pruned
|
|
231
|
+
*/
|
|
232
|
+
export function pruneReaperAudit(repoRoot, {
|
|
233
|
+
maxBytes = REAPER_AUDIT_MAX_BYTES,
|
|
234
|
+
keepBytes = REAPER_AUDIT_KEEP_BYTES,
|
|
235
|
+
} = {}) {
|
|
236
|
+
const target = auditPath(repoRoot);
|
|
237
|
+
let size;
|
|
238
|
+
try {
|
|
239
|
+
size = statSync(target).size;
|
|
240
|
+
} catch {
|
|
241
|
+
return 0;
|
|
242
|
+
}
|
|
243
|
+
if (size <= maxBytes) return 0;
|
|
244
|
+
try {
|
|
245
|
+
const { text, cut } = readTailWindow(target, Math.min(keepBytes, maxBytes));
|
|
246
|
+
// A cut window starts mid-record; with no newline at all it is ONE record
|
|
247
|
+
// larger than the window, and keeping any part of it would keep a fragment.
|
|
248
|
+
const newline = text.indexOf('\n');
|
|
249
|
+
const kept = !cut ? text : (newline === -1 ? '' : text.slice(newline + 1));
|
|
250
|
+
replaceRegularFile(target, kept);
|
|
251
|
+
return size - Buffer.byteLength(kept, 'utf8');
|
|
252
|
+
} catch (err) {
|
|
253
|
+
process.stderr.write(
|
|
254
|
+
`orphan-reaper: could not prune ${REAPER_AUDIT_RELPATH}: ${err?.message ?? String(err)}\n`,
|
|
255
|
+
);
|
|
256
|
+
return 0;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* False-alarm rate over the last `windowSize` audit decisions — the HR-101
|
|
262
|
+
* instrument-health check ("a signal may only warn if it is rare"; above ~10%
|
|
263
|
+
* the instrument is broken and gets re-aimed, never re-thresholded).
|
|
264
|
+
*
|
|
265
|
+
* POPULATION (the number's denominator, stated because a rate without one is a
|
|
266
|
+
* claim): audit records where the reaper judged a process a reapable orphan —
|
|
267
|
+
* `decision` in `kill` | `dry-run` | `reject`. `report` records are excluded:
|
|
268
|
+
* reporting a foreign or non-read-only process is the correct outcome, not a
|
|
269
|
+
* firing of the kill signal.
|
|
270
|
+
*
|
|
271
|
+
* FALSE ALARM: a record where that judgement was refuted afterwards —
|
|
272
|
+
* `decision: 'reject'` (the identity re-check withdrew the candidate) or a kill
|
|
273
|
+
* whose `result.ok` is false (the signal did not take effect).
|
|
274
|
+
*
|
|
275
|
+
* Returns `{rate: null}` below 10 records: a rate over a handful of decisions
|
|
276
|
+
* says nothing, and `null` is distinguishable from a measured 0 (a missing
|
|
277
|
+
* measurement must never look like a zero).
|
|
278
|
+
*
|
|
279
|
+
* @param {object[]} auditRecords In chronological order; the LAST `windowSize` are used.
|
|
280
|
+
* @param {number} [windowSize]
|
|
281
|
+
* @returns {{rate: number|null, n: number}}
|
|
282
|
+
*/
|
|
283
|
+
export function falseAlarmRate(auditRecords, windowSize = REAPER_DEFAULTS.falseAlarmWindow) {
|
|
284
|
+
const all = Array.isArray(auditRecords) ? auditRecords : [];
|
|
285
|
+
const firings = all.filter((r) => r && (r.decision === 'kill' || r.decision === 'dry-run' || r.decision === 'reject'));
|
|
286
|
+
const window = windowSize > 0 ? firings.slice(-windowSize) : firings;
|
|
287
|
+
const n = window.length;
|
|
288
|
+
if (n < 10) return { rate: null, n };
|
|
289
|
+
const falseAlarms = window.filter((r) => r.decision === 'reject' || r?.result?.ok === false).length;
|
|
290
|
+
return { rate: falseAlarms / n, n };
|
|
291
|
+
}
|