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,506 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* orphan-reaper/scan.mjs — one orphan scan end to end: `ps` → decide → (unless
|
|
3
|
+
* `dryRun`) re-verify identity before every signal → kill the group → audit.
|
|
4
|
+
*
|
|
5
|
+
* The IMPURE half of the reaper. Everything impure reaches the pure decision
|
|
6
|
+
* (`reaper-decide.mjs`) through `deps` ({@link resolveDeps}), the
|
|
7
|
+
* `lock-reaper.mjs` split of `evaluateRepo()` / `archiveLock()`.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import {
|
|
11
|
+
killProcessGroup,
|
|
12
|
+
pruneGateProcessLedger,
|
|
13
|
+
readGateProcessLedger,
|
|
14
|
+
verifyProcessIdentity,
|
|
15
|
+
} from '../process-group.mjs';
|
|
16
|
+
import { REAPER_DEFAULTS } from './defaults.mjs';
|
|
17
|
+
import { parsePsSnapshot, parsePsSnapshotDetailed, runPs, runPsPid } from './ps-snapshot.mjs';
|
|
18
|
+
import {
|
|
19
|
+
FALSE_ALARM_SUSPECT_RATE,
|
|
20
|
+
appendAuditRecord,
|
|
21
|
+
buildAuditRecord,
|
|
22
|
+
falseAlarmRate,
|
|
23
|
+
pruneReaperAudit,
|
|
24
|
+
readAuditRecords,
|
|
25
|
+
} from './reaper-audit.mjs';
|
|
26
|
+
import { decideReapCandidates, verifyGroupMemberIdentity } from './reaper-decide.mjs';
|
|
27
|
+
|
|
28
|
+
/** Name const for the one event this module emits. @see docs/events-schema.md */
|
|
29
|
+
export const REAPER_SCAN_EVENT = 'orchestrator.reaper.scan_completed';
|
|
30
|
+
|
|
31
|
+
/** Default own-session-id reader. Lazily imported so the static import closure
|
|
32
|
+
* of this module stays small — it is destined for a hot-path hook (#1432).
|
|
33
|
+
* @param {string} repoRoot @returns {Promise<string|null>} */
|
|
34
|
+
async function defaultReadOwnSessionId(repoRoot) {
|
|
35
|
+
try {
|
|
36
|
+
const mod = await import('../session-identity/own-session.mjs');
|
|
37
|
+
const ids = mod.readOwnSessionIds(repoRoot);
|
|
38
|
+
for (const id of ids) return id;
|
|
39
|
+
return null;
|
|
40
|
+
} catch {
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Default live-peer probe. Lazily imported for the same reason as
|
|
47
|
+
* {@link defaultReadOwnSessionId}.
|
|
48
|
+
*
|
|
49
|
+
* Returns `null` — never `[]` — when the registry cannot be read. The two mean
|
|
50
|
+
* opposite things to `decideReapCandidates`: `[]` is a MEASUREMENT ("no
|
|
51
|
+
* peer is alive", so a foreign session's leftovers are reapable), `null` is the
|
|
52
|
+
* absence of one ("report it, do not touch it").
|
|
53
|
+
*
|
|
54
|
+
* @param {string|null} sessionId
|
|
55
|
+
* @returns {Promise<string[]|null>}
|
|
56
|
+
*/
|
|
57
|
+
async function defaultDetectPeers(sessionId) {
|
|
58
|
+
try {
|
|
59
|
+
const mod = await import('../session-registry.mjs');
|
|
60
|
+
const peers = await mod.detectPeers({ sessionId: sessionId ?? undefined });
|
|
61
|
+
if (!Array.isArray(peers)) return null;
|
|
62
|
+
return peers.map((p) => p?.session_id).filter((v) => typeof v === 'string' && v.length > 0);
|
|
63
|
+
} catch {
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Default event emitter. Lazily imported for the same reason as
|
|
70
|
+
* {@link defaultReadOwnSessionId}: `events.mjs` pulls in the schema validator,
|
|
71
|
+
* the attribution chain and the webhook client, and this module is destined for
|
|
72
|
+
* a hot-path hook (#1432) whose static import closure is measured.
|
|
73
|
+
*
|
|
74
|
+
* Never throws and never rejects — telemetry that can fail a scan would fail the
|
|
75
|
+
* hook the scan runs in.
|
|
76
|
+
*
|
|
77
|
+
* @param {string} type
|
|
78
|
+
* @param {object} payload
|
|
79
|
+
* @param {{repoRoot?: string}} [opts]
|
|
80
|
+
* @returns {Promise<boolean>} whether the record was written
|
|
81
|
+
*/
|
|
82
|
+
async function defaultEmitEvent(type, payload, opts = {}) {
|
|
83
|
+
try {
|
|
84
|
+
const mod = await import('../events.mjs');
|
|
85
|
+
await mod.emitEvent(type, payload, opts);
|
|
86
|
+
return true;
|
|
87
|
+
} catch {
|
|
88
|
+
return false;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** @param {number} ms @returns {Promise<void>} */
|
|
93
|
+
function defaultSleep(ms) {
|
|
94
|
+
return new Promise((resolve) => { setTimeout(resolve, ms); });
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Resolve the adapter, defaulting every seam to the real implementation — the
|
|
99
|
+
* `resolveDeps()` pattern from `lock-reaper.mjs:77-100`.
|
|
100
|
+
*
|
|
101
|
+
* `killProcessGroup` HAS a real default, and that is safe only because
|
|
102
|
+
* {@link runOrphanScan} defaults `dryRun: true`. Tests ALWAYS inject it
|
|
103
|
+
* (`.claude/rules/testing.md`: never a real process as a kill target).
|
|
104
|
+
*
|
|
105
|
+
* @param {object} [deps]
|
|
106
|
+
* @returns {object}
|
|
107
|
+
*/
|
|
108
|
+
export function resolveDeps(deps = {}) {
|
|
109
|
+
const d = deps ?? {};
|
|
110
|
+
return {
|
|
111
|
+
runPs: d.runPs ?? runPs,
|
|
112
|
+
runPsPid: d.runPsPid ?? runPsPid,
|
|
113
|
+
readLedger: d.readLedger ?? readGateProcessLedger,
|
|
114
|
+
verifyIdentity: d.verifyIdentity ?? verifyProcessIdentity,
|
|
115
|
+
killProcessGroup: d.killProcessGroup ?? killProcessGroup,
|
|
116
|
+
detectPeers: d.detectPeers ?? defaultDetectPeers,
|
|
117
|
+
readOwnSessionId: d.readOwnSessionId ?? defaultReadOwnSessionId,
|
|
118
|
+
appendAudit: d.appendAudit ?? appendAuditRecord,
|
|
119
|
+
readAuditRecords: d.readAuditRecords ?? readAuditRecords,
|
|
120
|
+
emitEvent: d.emitEvent ?? defaultEmitEvent,
|
|
121
|
+
now: d.now ?? Date.now,
|
|
122
|
+
sleep: d.sleep ?? defaultSleep,
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Map an identity verdict's reason to the audit's `reason` vocabulary.
|
|
128
|
+
*
|
|
129
|
+
* Three outcomes, never two: `gone` (measured, the process is not there),
|
|
130
|
+
* `signature-mismatch` / `identity-mismatch` (measured, it is a DIFFERENT
|
|
131
|
+
* process) and `unmeasured` (the probe itself could not answer). The third is
|
|
132
|
+
* the one a two-state mapping loses, and losing it is how an absent measurement
|
|
133
|
+
* starts reading like a clean verdict.
|
|
134
|
+
*
|
|
135
|
+
* @param {string|null|undefined} identityReason
|
|
136
|
+
* @returns {'gone'|'signature-mismatch'|'identity-mismatch'|'unmeasured'}
|
|
137
|
+
*/
|
|
138
|
+
function rejectReasonFor(identityReason) {
|
|
139
|
+
if (identityReason === 'gone') return 'gone';
|
|
140
|
+
if (identityReason === 'unmeasured') return 'unmeasured';
|
|
141
|
+
if (identityReason === 'signature-mismatch') return 'signature-mismatch';
|
|
142
|
+
return 'identity-mismatch';
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Run one orphan scan: `ps` → decide → (unless `dryRun`) re-verify identity
|
|
147
|
+
* against a FRESH snapshot → kill the group → audit.
|
|
148
|
+
*
|
|
149
|
+
* NO-THROW contract: every failure degrades into `skipped: '<reason>'` or a
|
|
150
|
+
* `rejected` entry, because this runs from a hook and must never fail it
|
|
151
|
+
* (PRD FA4: "bei jedem Fehler degradiert er lautlos").
|
|
152
|
+
*
|
|
153
|
+
* The pre-signal re-check is the TOCTOU defence and the whole point of FA3: a
|
|
154
|
+
* PID recycled between decision and signal must NOT be signalled. It runs a
|
|
155
|
+
* TARGETED `ps` (`psPidArgs`, `ps-snapshot.mjs`) through `deps.runPsPid` and feeds the row to
|
|
156
|
+
* {@link verifyProcessIdentity} — immediately before SIGTERM **and** immediately
|
|
157
|
+
* before SIGKILL, via `killProcessGroup`'s `beforeSignal` gate, because the
|
|
158
|
+
* ladder's 10 s grace is itself a recycling window. On any mismatch no further
|
|
159
|
+
* signal is sent and the candidate is recorded as `decision: 'reject'` with the
|
|
160
|
+
* reason that withdrew it. Such a candidate stays in `candidates` (that WAS the
|
|
161
|
+
* decision) and additionally appears in `rejected` (that is the withdrawal) —
|
|
162
|
+
* the two arrays answer different questions and collapsing them would lose the
|
|
163
|
+
* TOCTOU event.
|
|
164
|
+
*
|
|
165
|
+
* Success is proven the same way, never from an exit code: after the ladder the
|
|
166
|
+
* scan waits `verifyWaitMs` and re-measures (B6). `ok` is true only for a
|
|
167
|
+
* process that is GONE; one that outlived SIGKILL is booked
|
|
168
|
+
* `survivedSigkill: true`, and one the probe could not measure is `verified:
|
|
169
|
+
* 'unmeasured'` — neither is ever a success.
|
|
170
|
+
*
|
|
171
|
+
* `dryRun` defaults to TRUE: arming happens at the CALL SITE (#1432), so a
|
|
172
|
+
* caller that forgets the flag scans and reports instead of killing.
|
|
173
|
+
*
|
|
174
|
+
* @param {object} opts
|
|
175
|
+
* @param {string} opts.repoRoot
|
|
176
|
+
* @param {number} [opts.now]
|
|
177
|
+
* @param {boolean} [opts.dryRun]
|
|
178
|
+
* @param {object} [opts.deps]
|
|
179
|
+
* @param {number} [opts.minAgeSeconds]
|
|
180
|
+
* @param {number} [opts.killGraceMs]
|
|
181
|
+
* @param {number} [opts.verifyWaitMs]
|
|
182
|
+
* @param {number} [opts.falseAlarmWindow] Rolling window of audit DECISIONS the
|
|
183
|
+
* HR-101 rate is judged over (`reaper.false-alarm-window`).
|
|
184
|
+
* @returns {Promise<{scanned: number, candidates: object[], reported: object[],
|
|
185
|
+
* rejected: object[], killed: object[], unattributed: number,
|
|
186
|
+
* peerLiveness: 'measured'|'unmeasured'|null, skipped?: string, malformed: number,
|
|
187
|
+
* durationMs: number, instrumentSuspect: boolean|null, falseAlarmRate: number|null,
|
|
188
|
+
* falseAlarmWindowN: number}>}
|
|
189
|
+
* `instrumentSuspect`/`falseAlarmRate` are `null` on a degraded (`skipped`) scan
|
|
190
|
+
* and `falseAlarmRate` is `null` below the 10-decision floor — in both cases a
|
|
191
|
+
* measurement that does not exist, never a measured zero. `peerLiveness` is the
|
|
192
|
+
* same distinction for the peer probe, and `null` only on a degraded scan.
|
|
193
|
+
*/
|
|
194
|
+
export async function runOrphanScan({
|
|
195
|
+
repoRoot,
|
|
196
|
+
now,
|
|
197
|
+
dryRun = true,
|
|
198
|
+
deps,
|
|
199
|
+
minAgeSeconds = REAPER_DEFAULTS.minAgeSeconds,
|
|
200
|
+
killGraceMs = REAPER_DEFAULTS.killGraceMs,
|
|
201
|
+
verifyWaitMs = REAPER_DEFAULTS.verifyWaitMs,
|
|
202
|
+
falseAlarmWindow = REAPER_DEFAULTS.falseAlarmWindow,
|
|
203
|
+
} = {}) {
|
|
204
|
+
const d = resolveDeps(deps);
|
|
205
|
+
const startedAt = typeof now === 'number' ? now : d.now();
|
|
206
|
+
// A degraded scan measured NOTHING — including the instrument's own health.
|
|
207
|
+
// `null` rather than `false`/`0` for both instrument fields: a scan that never
|
|
208
|
+
// ran must not report a healthy instrument it never looked at.
|
|
209
|
+
const empty = (skipped, malformed = 0) => ({
|
|
210
|
+
scanned: 0,
|
|
211
|
+
candidates: [],
|
|
212
|
+
reported: [],
|
|
213
|
+
rejected: [],
|
|
214
|
+
killed: [],
|
|
215
|
+
unattributed: 0,
|
|
216
|
+
// Nothing was measured, so peer liveness was not measured either — `null`
|
|
217
|
+
// rather than the string, because "unmeasured" is a MEASUREMENT OUTCOME and
|
|
218
|
+
// a degraded scan never got as far as the probe.
|
|
219
|
+
peerLiveness: null,
|
|
220
|
+
skipped,
|
|
221
|
+
malformed,
|
|
222
|
+
durationMs: Math.max(0, d.now() - startedAt),
|
|
223
|
+
instrumentSuspect: null,
|
|
224
|
+
falseAlarmRate: null,
|
|
225
|
+
falseAlarmWindowN: 0,
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
if (typeof repoRoot !== 'string' || repoRoot.length === 0) return empty('no-repo-root');
|
|
229
|
+
|
|
230
|
+
let text;
|
|
231
|
+
try {
|
|
232
|
+
text = await d.runPs();
|
|
233
|
+
} catch {
|
|
234
|
+
return empty('ps-failed');
|
|
235
|
+
}
|
|
236
|
+
if (text === null || text === undefined) return empty('ps-failed');
|
|
237
|
+
|
|
238
|
+
const { rows, malformed } = parsePsSnapshotDetailed(text);
|
|
239
|
+
|
|
240
|
+
let ledger;
|
|
241
|
+
try {
|
|
242
|
+
ledger = d.readLedger(repoRoot, { nowMs: startedAt });
|
|
243
|
+
} catch {
|
|
244
|
+
return empty('ledger-unreadable', malformed);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
let ownSessionId = null;
|
|
248
|
+
try {
|
|
249
|
+
ownSessionId = await d.readOwnSessionId(repoRoot);
|
|
250
|
+
} catch {
|
|
251
|
+
ownSessionId = null;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/** @type {string[]|null} */
|
|
255
|
+
let livePeerSessionIds;
|
|
256
|
+
try {
|
|
257
|
+
livePeerSessionIds = await d.detectPeers(ownSessionId);
|
|
258
|
+
} catch {
|
|
259
|
+
// null = unmeasured, NOT "no peers" — see decideReapCandidates.
|
|
260
|
+
livePeerSessionIds = null;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
const { candidates, reported, rejected } = decideReapCandidates(
|
|
264
|
+
rows,
|
|
265
|
+
ledger?.records ?? [],
|
|
266
|
+
startedAt,
|
|
267
|
+
{ ownSessionId, livePeerSessionIds, minAgeSeconds },
|
|
268
|
+
);
|
|
269
|
+
|
|
270
|
+
const timestamp = new Date(startedAt).toISOString();
|
|
271
|
+
const audit = (entry, decision, extra = {}) => {
|
|
272
|
+
try {
|
|
273
|
+
d.appendAudit(repoRoot, buildAuditRecord(entry, decision, {
|
|
274
|
+
timestamp, sessionId: ownSessionId, ...extra,
|
|
275
|
+
}));
|
|
276
|
+
} catch {
|
|
277
|
+
/* the audit is an aid, never a precondition */
|
|
278
|
+
}
|
|
279
|
+
};
|
|
280
|
+
|
|
281
|
+
for (const entry of reported) audit(entry, 'report');
|
|
282
|
+
|
|
283
|
+
/** @type {object[]} */
|
|
284
|
+
const killed = [];
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* Close the scan: measure the instrument's own health (HR-101), emit at most
|
|
288
|
+
* one event, return the result. Both real exits go through here so the rate
|
|
289
|
+
* and the event can never be computed twice or forgotten once.
|
|
290
|
+
* @returns {Promise<object>}
|
|
291
|
+
*/
|
|
292
|
+
const complete = async () => {
|
|
293
|
+
let auditRecords;
|
|
294
|
+
try {
|
|
295
|
+
auditRecords = (await d.readAuditRecords(repoRoot, falseAlarmWindow)) ?? [];
|
|
296
|
+
} catch {
|
|
297
|
+
auditRecords = [];
|
|
298
|
+
}
|
|
299
|
+
const fa = falseAlarmRate(auditRecords, falseAlarmWindow);
|
|
300
|
+
// HR-101: the rate re-aims the instrument, it never re-thresholds it — so
|
|
301
|
+
// this flag is REPORTED and nothing here branches on it.
|
|
302
|
+
const instrumentSuspect = typeof fa.rate === 'number' && fa.rate > FALSE_ALARM_SUSPECT_RATE;
|
|
303
|
+
const survivedSigkill = killed.filter((k) => k.survivedSigkill === true).length;
|
|
304
|
+
const durationMs = Math.max(0, d.now() - startedAt);
|
|
305
|
+
// Two counts that must not hide inside `reported`: an unattributed record is
|
|
306
|
+
// a PRODUCER defect (the gate runner passed no session id), and an
|
|
307
|
+
// unmeasured peer probe is an INSTRUMENT gap. Folded into the generic
|
|
308
|
+
// `reported` number, both are invisible — which is how the foreign-session
|
|
309
|
+
// guard stayed inert across 377 of 377 records without anything saying so.
|
|
310
|
+
const unattributed = reported.filter((r) => r.reason === 'unattributed').length;
|
|
311
|
+
const peerLiveness = livePeerSessionIds === null ? 'unmeasured' : 'measured';
|
|
312
|
+
|
|
313
|
+
// HR-101 again, in the other direction: a signal that fires on every hook
|
|
314
|
+
// is noise nobody reads. A scan that found nothing emits nothing — the
|
|
315
|
+
// absence of a record IS the healthy state, and `instrumentSuspect` is the
|
|
316
|
+
// one finding that must surface even from an empty scan.
|
|
317
|
+
if (candidates.length + reported.length + killed.length > 0 || instrumentSuspect) {
|
|
318
|
+
try {
|
|
319
|
+
await d.emitEvent(REAPER_SCAN_EVENT, {
|
|
320
|
+
scanned: rows.length,
|
|
321
|
+
candidates: candidates.length,
|
|
322
|
+
reported: reported.length,
|
|
323
|
+
rejected: rejected.length,
|
|
324
|
+
killed: killed.length,
|
|
325
|
+
unattributed,
|
|
326
|
+
peer_liveness: peerLiveness,
|
|
327
|
+
survived_sigkill: survivedSigkill,
|
|
328
|
+
dry_run: dryRun === true,
|
|
329
|
+
duration_ms: durationMs,
|
|
330
|
+
instrument_suspect: instrumentSuspect,
|
|
331
|
+
// OMITTED below the 10-decision floor, never 0: absence means "no
|
|
332
|
+
// population yet", and a fabricated zero would read as a clean
|
|
333
|
+
// instrument (`falseAlarmRate` returns `{rate: null}` there).
|
|
334
|
+
...(typeof fa.rate === 'number' ? { false_alarm_rate: fa.rate } : {}),
|
|
335
|
+
}, { repoRoot });
|
|
336
|
+
} catch {
|
|
337
|
+
/* telemetry that can fail a scan would fail the hook the scan runs in */
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
// Housekeeping, last and best-effort: the ledger is append-only and grew
|
|
342
|
+
// monotonically (measured 2026-09-22: 369 lines/day) because nothing in
|
|
343
|
+
// production ever called the pruner.
|
|
344
|
+
try {
|
|
345
|
+
pruneGateProcessLedger(repoRoot);
|
|
346
|
+
// The audit is pruned AFTER it was read for the rate above, so a prune
|
|
347
|
+
// can never shrink the population this scan judged itself on.
|
|
348
|
+
pruneReaperAudit(repoRoot);
|
|
349
|
+
} catch {
|
|
350
|
+
/* housekeeping, never a precondition of a scan */
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
return {
|
|
354
|
+
scanned: rows.length,
|
|
355
|
+
candidates,
|
|
356
|
+
reported,
|
|
357
|
+
rejected,
|
|
358
|
+
killed,
|
|
359
|
+
unattributed,
|
|
360
|
+
peerLiveness,
|
|
361
|
+
malformed,
|
|
362
|
+
durationMs,
|
|
363
|
+
instrumentSuspect,
|
|
364
|
+
falseAlarmRate: fa.rate,
|
|
365
|
+
falseAlarmWindowN: fa.n,
|
|
366
|
+
};
|
|
367
|
+
};
|
|
368
|
+
|
|
369
|
+
if (dryRun) {
|
|
370
|
+
for (const c of candidates) audit(c, 'dry-run');
|
|
371
|
+
return complete();
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
/**
|
|
375
|
+
* ONE fresh, targeted identity measurement for one candidate (B3).
|
|
376
|
+
*
|
|
377
|
+
* There is deliberately no second, bulk `ps` pass any more: two TOCTOU
|
|
378
|
+
* instruments measuring the same property at different freshness is how the
|
|
379
|
+
* stale one silently wins. This is the only pre-signal measurement, and it is
|
|
380
|
+
* taken immediately before each signal rather than once for the whole pass.
|
|
381
|
+
*
|
|
382
|
+
* @param {object} c
|
|
383
|
+
* @returns {Promise<{match: boolean, reason: string, observed?: object}>}
|
|
384
|
+
*/
|
|
385
|
+
const freshIdentity = async (c) => {
|
|
386
|
+
let text;
|
|
387
|
+
try {
|
|
388
|
+
text = await d.runPsPid(c.pid);
|
|
389
|
+
} catch {
|
|
390
|
+
text = null;
|
|
391
|
+
}
|
|
392
|
+
// `ps -p` cannot distinguish "gone" from "could not run"; both must refuse
|
|
393
|
+
// the signal, and only the second is an instrument gap worth its own reason.
|
|
394
|
+
if (text === null || text === undefined) return { match: false, reason: 'unmeasured' };
|
|
395
|
+
const row = parsePsSnapshot(text).find((r) => r.pid === c.pid) ?? null;
|
|
396
|
+
try {
|
|
397
|
+
// A group MEMBER is re-verified the way it was decided — by the leader's
|
|
398
|
+
// seam it would fail every time (its own start time, its own command),
|
|
399
|
+
// and a pre-signal check that always refuses is a disarmed feature, not a
|
|
400
|
+
// strict one.
|
|
401
|
+
if (c.isLeader === false) {
|
|
402
|
+
return verifyGroupMemberIdentity(row, c.ledgerRecord, { nowMs: d.now() });
|
|
403
|
+
}
|
|
404
|
+
return d.verifyIdentity(
|
|
405
|
+
c.pid,
|
|
406
|
+
{ startTime: c.ledgerRecord.startTime, commandSignature: c.ledgerRecord.commandSignature },
|
|
407
|
+
{ snapshotLine: row, nowMs: d.now() },
|
|
408
|
+
);
|
|
409
|
+
} catch {
|
|
410
|
+
return { match: false, reason: 'gone' };
|
|
411
|
+
}
|
|
412
|
+
};
|
|
413
|
+
|
|
414
|
+
// ONE ladder per GROUP, not per row: with the pgid join a single group can
|
|
415
|
+
// contribute several candidate rows (leader + descendants), and they all name
|
|
416
|
+
// the same kill target. Leaders first, so the target of a group is the process
|
|
417
|
+
// the ledger actually recorded whenever it is still alive.
|
|
418
|
+
const killTargets = [];
|
|
419
|
+
const coveredPgids = new Set();
|
|
420
|
+
for (const c of [...candidates.filter((x) => x.isLeader), ...candidates.filter((x) => !x.isLeader)]) {
|
|
421
|
+
if (coveredPgids.has(c.pgid)) continue;
|
|
422
|
+
coveredPgids.add(c.pgid);
|
|
423
|
+
killTargets.push(c);
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
for (const c of killTargets) {
|
|
427
|
+
/** Set by the gate below when it refuses; `null` means every signal was permitted. */
|
|
428
|
+
let withdrawal = null;
|
|
429
|
+
const beforeSignal = async (signal) => {
|
|
430
|
+
const identity = await freshIdentity(c);
|
|
431
|
+
if (identity?.match === true) return true;
|
|
432
|
+
withdrawal = { signal, identity, reason: rejectReasonFor(identity?.reason) };
|
|
433
|
+
return false;
|
|
434
|
+
};
|
|
435
|
+
|
|
436
|
+
let result;
|
|
437
|
+
try {
|
|
438
|
+
result = await d.killProcessGroup(c.pgid, {
|
|
439
|
+
killGraceMs, verifyWaitMs, sleepFn: d.sleep, beforeSignal,
|
|
440
|
+
});
|
|
441
|
+
} catch (err) {
|
|
442
|
+
result = { ok: false, signalsSent: [], survivors: [c.pgid], error: err?.code ?? null, aborted: null };
|
|
443
|
+
}
|
|
444
|
+
const signalsSent = Array.isArray(result?.signalsSent) ? result.signalsSent : [];
|
|
445
|
+
|
|
446
|
+
if (withdrawal !== null) {
|
|
447
|
+
// The candidate was withdrawn between decision and signal. It stays in
|
|
448
|
+
// `candidates` (that WAS the decision) and appears in `rejected` (that is
|
|
449
|
+
// the withdrawal) — collapsing the two would lose the TOCTOU event.
|
|
450
|
+
// `signalsSent` is carried even here: a withdrawal before the ESCALATION
|
|
451
|
+
// still means a SIGTERM went out, and an audit that hid it would
|
|
452
|
+
// under-report what this reaper did to the host.
|
|
453
|
+
rejected.push({
|
|
454
|
+
...c, reason: withdrawal.reason, identity: withdrawal.identity, signalsSent,
|
|
455
|
+
});
|
|
456
|
+
audit(c, 'reject', {
|
|
457
|
+
reason: withdrawal.reason,
|
|
458
|
+
result: {
|
|
459
|
+
ok: false,
|
|
460
|
+
signalsSent,
|
|
461
|
+
survivors: [],
|
|
462
|
+
survivedSigkill: false,
|
|
463
|
+
verifiedAfterMs: 0,
|
|
464
|
+
verified: 'withdrawn',
|
|
465
|
+
},
|
|
466
|
+
});
|
|
467
|
+
continue;
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
// B6: prove the EFFECT, after a wait, from a fresh measurement of the
|
|
471
|
+
// process table. An exit code and a sent signal prove nothing — on
|
|
472
|
+
// 2026-09-20 a probe with no wait reported "still alive" for dead processes.
|
|
473
|
+
let verified = 'unverified';
|
|
474
|
+
if (signalsSent.length > 0) {
|
|
475
|
+
await d.sleep(verifyWaitMs);
|
|
476
|
+
const after = await freshIdentity(c);
|
|
477
|
+
if (after?.match === true) verified = 'alive';
|
|
478
|
+
else if (after?.reason === 'unmeasured') verified = 'unmeasured';
|
|
479
|
+
else verified = 'gone';
|
|
480
|
+
}
|
|
481
|
+
const ok = verified === 'gone';
|
|
482
|
+
const survivedSigkill = verified === 'alive' && signalsSent.includes('SIGKILL');
|
|
483
|
+
const survivors = ok ? [] : [c.pgid];
|
|
484
|
+
const verifiedAfterMs = signalsSent.length > 0 ? verifyWaitMs : 0;
|
|
485
|
+
|
|
486
|
+
killed.push({
|
|
487
|
+
pid: c.pid,
|
|
488
|
+
pgid: c.pgid,
|
|
489
|
+
// Every candidate row this one ladder covers — the group join means one
|
|
490
|
+
// signal can end several candidates, and a `killed` list that named only
|
|
491
|
+
// the target would under-report what the reaper did to the host.
|
|
492
|
+
groupMemberPids: candidates.filter((x) => x.pgid === c.pgid).map((x) => x.pid),
|
|
493
|
+
ok,
|
|
494
|
+
signalsSent,
|
|
495
|
+
survivors,
|
|
496
|
+
survivedSigkill,
|
|
497
|
+
verified,
|
|
498
|
+
verifiedAfterMs,
|
|
499
|
+
});
|
|
500
|
+
audit(c, 'kill', {
|
|
501
|
+
result: { ok, signalsSent, survivors, survivedSigkill, verifiedAfterMs, verified },
|
|
502
|
+
});
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
return complete();
|
|
506
|
+
}
|