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
|
@@ -70,9 +70,10 @@
|
|
|
70
70
|
* @module scripts/lib/maintenance-due-banner
|
|
71
71
|
*/
|
|
72
72
|
|
|
73
|
-
import {
|
|
73
|
+
import { existsSync, statSync } from 'node:fs';
|
|
74
74
|
import path from 'node:path';
|
|
75
75
|
|
|
76
|
+
import { SCAN_CHUNK_BYTES, scanEventsBackwards } from './events.mjs';
|
|
76
77
|
import { computeReconcileNudge } from './reconcile-nudge-banner.mjs';
|
|
77
78
|
import { sweepExpiredLearnings } from './learnings/expiry-sweep.mjs';
|
|
78
79
|
import { shouldDispatchAutoDialectic } from './auto-dialectic.mjs';
|
|
@@ -152,104 +153,64 @@ function isoDay(ts) {
|
|
|
152
153
|
}
|
|
153
154
|
|
|
154
155
|
/**
|
|
155
|
-
* Backwards-scan chunk size (#1290 item 2)
|
|
156
|
-
*
|
|
157
|
-
*
|
|
158
|
-
*
|
|
159
|
-
* handful of reads instead of loading the whole 7.9 MB file. The scan is
|
|
160
|
-
* UNBOUNDED in the worst case ON PURPOSE: "never ran" is a claim about every
|
|
161
|
-
* line and cannot be made from a tail, so a repo with no `evolve.completed`
|
|
162
|
-
* record still walks the file to its start — just in chunks, never all at once
|
|
163
|
-
* in one string.
|
|
164
|
-
*
|
|
165
|
-
* REVISIT TRIGGER: the maintenance probe's median passes 1000 ms (half
|
|
166
|
-
* `PROBE_BUDGET_MS`), or one repo's `events.jsonl` passes 50 MB. Either means
|
|
167
|
-
* the "never ran" walk has become the cost that matters and the answer needs an
|
|
168
|
-
* index rather than a scan.
|
|
156
|
+
* Backwards-scan chunk size (#1290 item 2) — the shared reader's constant, kept
|
|
157
|
+
* under this module's own name because it is the number this probe's cost is
|
|
158
|
+
* reasoned about in. NAMED CEILING + revisit trigger live at
|
|
159
|
+
* {@link SCAN_CHUNK_BYTES}.
|
|
169
160
|
*/
|
|
170
|
-
export const TAIL_CHUNK_BYTES =
|
|
161
|
+
export const TAIL_CHUNK_BYTES = SCAN_CHUNK_BYTES;
|
|
171
162
|
|
|
172
163
|
/**
|
|
173
|
-
*
|
|
164
|
+
* Wall-clock budget for the "did /evolve ever run?" walk (#1414).
|
|
165
|
+
*
|
|
166
|
+
* NAMED CEILING (BV-004): 1000 ms is half of `PROBE_BUDGET_MS` (2000 ms in
|
|
167
|
+
* `session-start-probes.mjs`), which this probe shares with six other signals.
|
|
168
|
+
* The walk is UNBOUNDED in principle — "never ran" is a claim about every line
|
|
169
|
+
* of every source — so it needs an own limit now that it spans the archives
|
|
170
|
+
* too, and running out must report `truncated`, never a clean "never".
|
|
174
171
|
*
|
|
175
|
-
*
|
|
176
|
-
*
|
|
172
|
+
* REVISIT TRIGGER: a repo where this reports `truncated` more than rarely. That
|
|
173
|
+
* means the answer needs an index rather than a scan, not a bigger budget
|
|
174
|
+
* (HR-101).
|
|
177
175
|
*/
|
|
178
|
-
|
|
179
|
-
if (buf.length === 0) return null;
|
|
180
|
-
const lines = buf.toString('utf8').split('\n');
|
|
181
|
-
for (let i = lines.length - 1; i >= 0; i -= 1) {
|
|
182
|
-
const line = lines[i];
|
|
183
|
-
if (!line || !line.includes(EVOLVE_EVENT)) continue; // cheap pre-filter before JSON.parse
|
|
184
|
-
try {
|
|
185
|
-
const rec = JSON.parse(line);
|
|
186
|
-
if (rec?.event !== EVOLVE_EVENT) continue;
|
|
187
|
-
return { lastAt: typeof rec.timestamp === 'string' ? rec.timestamp : null };
|
|
188
|
-
} catch {
|
|
189
|
-
continue; // a malformed line is not evidence either way — keep scanning
|
|
190
|
-
}
|
|
191
|
-
}
|
|
192
|
-
return null;
|
|
193
|
-
}
|
|
176
|
+
export const EVOLVE_SCAN_BUDGET_MS = 1000;
|
|
194
177
|
|
|
195
178
|
/**
|
|
196
|
-
* Find the most recent `orchestrator.evolve.completed` record
|
|
197
|
-
*
|
|
198
|
-
* Reads the ledger BACKWARDS in {@link TAIL_CHUNK_BYTES} chunks and stops at
|
|
199
|
-
* the first hit, because the interesting answer is the LAST occurrence. The
|
|
200
|
-
* former implementation `readFileSync`-ed the whole file (7.9 MB here, 30–44 ms)
|
|
201
|
-
* to answer a question the last few kilobytes usually settle.
|
|
179
|
+
* Find the most recent `orchestrator.evolve.completed` record — across ROTATION
|
|
180
|
+
* boundaries (#1414).
|
|
202
181
|
*
|
|
203
|
-
*
|
|
204
|
-
*
|
|
205
|
-
*
|
|
206
|
-
*
|
|
207
|
-
*
|
|
182
|
+
* Walks the ledger backwards through every source, newest first, and stops at
|
|
183
|
+
* the first hit, because the interesting answer is the LAST occurrence. Reading
|
|
184
|
+
* the active file alone was the bug: "never ran" is a claim about EVERY line
|
|
185
|
+
* ever written, and after a rotation the only `evolve.completed` record on the
|
|
186
|
+
* host can sit in `_archive/` — the probe then nags a repo that has run
|
|
187
|
+
* /evolve, which is the HR-101 failure mode this module exists to avoid.
|
|
208
188
|
*
|
|
209
189
|
* @param {string} repoRoot
|
|
210
|
-
* @returns {{ok: boolean, lastAt: string|null}} `ok: false`
|
|
211
|
-
* but could not be read
|
|
190
|
+
* @returns {{ok: boolean, lastAt: string|null, truncated: boolean}} `ok: false`
|
|
191
|
+
* ⇒ a source exists but could not be read; `truncated: true` ⇒ the budget ran
|
|
192
|
+
* out before the walk finished. In BOTH cases the caller must record
|
|
193
|
+
* `undeterminable`, never clean — a walk that did not finish has not proven
|
|
194
|
+
* "never".
|
|
212
195
|
*/
|
|
213
196
|
function readLastEvolveRun(repoRoot) {
|
|
214
197
|
const file = path.join(repoRoot, '.orchestrator', 'metrics', 'events.jsonl');
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
carry = block; // no complete line yet — a line longer than one chunk
|
|
232
|
-
continue;
|
|
233
|
-
}
|
|
234
|
-
const hit = scanEvolveLines(block.subarray(firstNewline + 1));
|
|
235
|
-
if (hit) return { ok: true, lastAt: hit.lastAt };
|
|
236
|
-
carry = block.subarray(0, firstNewline);
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
// pos === 0: the carry is the file's FIRST line, complete by construction.
|
|
240
|
-
const hit = scanEvolveLines(carry);
|
|
241
|
-
return { ok: true, lastAt: hit ? hit.lastAt : null };
|
|
242
|
-
} catch {
|
|
243
|
-
return { ok: false, lastAt: null };
|
|
244
|
-
} finally {
|
|
245
|
-
if (fd !== undefined) {
|
|
246
|
-
try {
|
|
247
|
-
closeSync(fd);
|
|
248
|
-
} catch {
|
|
249
|
-
/* best-effort */
|
|
250
|
-
}
|
|
251
|
-
}
|
|
252
|
-
}
|
|
198
|
+
let lastAt = null;
|
|
199
|
+
const scan = scanEventsBackwards({
|
|
200
|
+
filePath: file,
|
|
201
|
+
chunkBytes: TAIL_CHUNK_BYTES,
|
|
202
|
+
budgetMs: EVOLVE_SCAN_BUDGET_MS,
|
|
203
|
+
// Cheap substring pre-filter before JSON.parse — the property the previous
|
|
204
|
+
// hand-rolled scan was written for, kept here rather than re-derived.
|
|
205
|
+
filter: EVOLVE_EVENT,
|
|
206
|
+
onRecord: (rec) => {
|
|
207
|
+
if (rec?.event !== EVOLVE_EVENT) return false;
|
|
208
|
+
lastAt = typeof rec.timestamp === 'string' ? rec.timestamp : null;
|
|
209
|
+
return true;
|
|
210
|
+
},
|
|
211
|
+
});
|
|
212
|
+
if (scan.unreadable.length > 0) return { ok: false, lastAt: null, truncated: false };
|
|
213
|
+
return { ok: true, lastAt, truncated: scan.truncated };
|
|
253
214
|
}
|
|
254
215
|
|
|
255
216
|
/**
|
|
@@ -335,7 +296,9 @@ export async function computeMaintenanceDue(opts = {}) {
|
|
|
335
296
|
} else {
|
|
336
297
|
// --- evolve (S1) -------------------------------------------------------
|
|
337
298
|
const evolve = readLastEvolveRun(repoRoot);
|
|
338
|
-
|
|
299
|
+
// `truncated` is NOT "not found": a walk that ran out of budget proves
|
|
300
|
+
// nothing about the lines it never reached (#1414).
|
|
301
|
+
if (!evolve.ok || evolve.truncated) {
|
|
339
302
|
undeterminable.push('evolve');
|
|
340
303
|
} else if (evolve.lastAt === null && nudge.activeLearnings >= MAINTENANCE_MIN_LEARNINGS) {
|
|
341
304
|
markDue('evolve', `never, ${nudge.activeLearnings} active learnings`);
|
|
@@ -344,7 +307,9 @@ export async function computeMaintenanceDue(opts = {}) {
|
|
|
344
307
|
// --- reconcile (S3) ----------------------------------------------------
|
|
345
308
|
if (nudge.nudge === true) {
|
|
346
309
|
// HR-106: report what the rule JUDGED. `computeReconcileNudge` nudges on
|
|
347
|
-
// a BACKLOG
|
|
310
|
+
// a BACKLOG — rule-eligible learnings minus those already materialized
|
|
311
|
+
// in the sidecar or `.claude/rules/` (`countReconcileBacklog`, #1380;
|
|
312
|
+
// before #1380 it counted all eligible ones) — never on a date — so
|
|
348
313
|
// printing `lastRunAt` here put today's date next to the word "due" and
|
|
349
314
|
// read as "last run today and already due again". Measured 2026-09-09
|
|
350
315
|
// (learning 013a45ba): with 108 learnings capped under
|
|
@@ -7,7 +7,8 @@
|
|
|
7
7
|
* during a wave, reviewed by the memory-cleanup flow, and either accepted
|
|
8
8
|
* (appended to learnings.jsonl) or discarded.
|
|
9
9
|
*
|
|
10
|
-
* Pure
|
|
10
|
+
* Pure module — imports only node:crypto and the pure learnings/schema.mjs
|
|
11
|
+
* (type registry, repo-relative path predicate). No I/O, no quota enforcement
|
|
11
12
|
* (see store.mjs for those responsibilities).
|
|
12
13
|
*
|
|
13
14
|
* Canonical schema (schema_version: 1) — ALL required fields:
|
|
@@ -30,7 +31,8 @@
|
|
|
30
31
|
*/
|
|
31
32
|
|
|
32
33
|
import { randomUUID } from 'node:crypto';
|
|
33
|
-
|
|
34
|
+
|
|
35
|
+
import { isRepoRelativePathEntry, LEARNING_TYPE_REGISTRY } from '../learnings/schema.mjs';
|
|
34
36
|
|
|
35
37
|
// ---------------------------------------------------------------------------
|
|
36
38
|
// Constants
|
|
@@ -251,24 +253,28 @@ export function validateProposalRecord(record) {
|
|
|
251
253
|
}
|
|
252
254
|
|
|
253
255
|
// file_paths (optional, but if present must be a non-empty array of
|
|
254
|
-
//
|
|
256
|
+
// repo-relative, newline-free, glob-metacharacter-free strings) — issue #900 C,
|
|
255
257
|
// Q3-MED fix pass: a glob metacharacter (* ? [ ] { }) surviving this schema
|
|
256
258
|
// gate would later reach emitter.mjs::globsFromFilePaths verbatim for a
|
|
257
259
|
// top-level (dirname==='.') entry, effectively producing an always-on rule
|
|
258
260
|
// glob (e.g. `file_paths: ['**']`). `createProposalRecord` never sets an
|
|
259
261
|
// empty array (it omits the key), so a present-but-empty `[]` is itself a
|
|
260
262
|
// signal of a malformed/tampered record — reject it explicitly.
|
|
263
|
+
// Repo-relative is the SAME predicate validateLearning applies at write time
|
|
264
|
+
// (isRepoRelativePathEntry): a record read back from proposals.jsonl is not
|
|
265
|
+
// re-validated before writeApproved → appendLearning, so an absolute, home-
|
|
266
|
+
// relative or URL entry must be refused here, not silently at promotion.
|
|
261
267
|
if ('file_paths' in record) {
|
|
262
268
|
const fp = record.file_paths;
|
|
263
269
|
const isValidArray =
|
|
264
270
|
Array.isArray(fp) &&
|
|
265
271
|
fp.length > 0 &&
|
|
266
272
|
fp.every(
|
|
267
|
-
(p) =>
|
|
273
|
+
(p) => isRepoRelativePathEntry(p) && !/[\r\n]/.test(p) && !/[*?[\]{}]/.test(p),
|
|
268
274
|
);
|
|
269
275
|
if (!isValidArray) {
|
|
270
276
|
errors.push(
|
|
271
|
-
'file_paths must be a non-empty array of
|
|
277
|
+
'file_paths must be a non-empty array of repo-relative, newline-free, glob-metacharacter-free strings when present',
|
|
272
278
|
);
|
|
273
279
|
}
|
|
274
280
|
}
|
|
@@ -29,11 +29,21 @@
|
|
|
29
29
|
* also YIELDS to tier 1: when SO_BASELINE_PATH is set (non-blank), resolveNamedBaseline
|
|
30
30
|
* returns the null-fallback ({source:null}) so the caller's env tier wins.
|
|
31
31
|
*
|
|
32
|
+
* ── Other sections (agents/vault#319) ────────────────────────────────────────
|
|
33
|
+
*
|
|
34
|
+
* The same directory-prefix match serves any owner.yaml list of the shape
|
|
35
|
+
* `{ path, match: { path-prefix } }`. `section` picks the list and `envKey` the
|
|
36
|
+
* env var this tier yields to. The live second consumer is `vault-dirs:`
|
|
37
|
+
* (section 'vault-dirs', envKey 'SO_VAULT_DIR'), driven by
|
|
38
|
+
* `resolveVaultDir` in scripts/lib/config/host-paths.mjs. `name` is required
|
|
39
|
+
* only for `baselines:` (the #819 contract); elsewhere it is optional and
|
|
40
|
+
* defaults to `path`, which is what the ambiguity WARN then names.
|
|
41
|
+
*
|
|
32
42
|
* ── Exports ──────────────────────────────────────────────────────────────────
|
|
33
43
|
*
|
|
34
|
-
* parseBaselines(ownerConfig)
|
|
35
|
-
* matchBaselineForPath(absPath, baselines)
|
|
36
|
-
* resolveNamedBaseline({ cwd, ownerConfig, env })
|
|
44
|
+
* parseBaselines(ownerConfig, section?)
|
|
45
|
+
* matchBaselineForPath(absPath, baselines, section?)
|
|
46
|
+
* resolveNamedBaseline({ cwd, ownerConfig, env, section?, envKey? })
|
|
37
47
|
*/
|
|
38
48
|
|
|
39
49
|
import { normalize, sep } from 'node:path';
|
|
@@ -43,6 +53,9 @@ import { expandTilde } from './common.mjs';
|
|
|
43
53
|
// Internal helpers
|
|
44
54
|
// ---------------------------------------------------------------------------
|
|
45
55
|
|
|
56
|
+
/** Sections whose entries MUST carry a `name` (the #819 baselines contract). */
|
|
57
|
+
const NAME_REQUIRED_SECTIONS = new Set(['baselines']);
|
|
58
|
+
|
|
46
59
|
/** @param {unknown} v */
|
|
47
60
|
function isPlainObject(v) {
|
|
48
61
|
return v !== null && typeof v === 'object' && !Array.isArray(v);
|
|
@@ -70,18 +83,23 @@ function _normalizePath(p) {
|
|
|
70
83
|
// ---------------------------------------------------------------------------
|
|
71
84
|
|
|
72
85
|
/**
|
|
73
|
-
* Extract and validate
|
|
86
|
+
* Extract and validate a directory-prefix list (default `baselines:`) from a raw
|
|
87
|
+
* ownerConfig object.
|
|
74
88
|
*
|
|
75
89
|
* Drop-and-WARN on malformed entries; never throw. Returns [] when the section
|
|
76
|
-
* is absent, null, or empty — the backward-compat no-op path.
|
|
90
|
+
* is absent, null, or empty — the backward-compat no-op path. Every WARN names
|
|
91
|
+
* the section it came from.
|
|
77
92
|
*
|
|
78
|
-
* Each valid entry: { name: string, path: string, match: { 'path-prefix': string } }
|
|
93
|
+
* Each valid entry: { name: string, path: string, match: { 'path-prefix': string } }.
|
|
94
|
+
* `name` is required for `baselines`; for other sections an absent name falls
|
|
95
|
+
* back to `path` (a present-but-blank or non-string name is still dropped).
|
|
79
96
|
*
|
|
80
97
|
* @param {object|undefined} ownerConfig — raw parsed owner.yaml (NOT merged with defaults)
|
|
98
|
+
* @param {string} [section='baselines'] — owner.yaml list to read (e.g. 'vault-dirs')
|
|
81
99
|
* @returns {Array<{name:string, path:string, match:{'path-prefix':string}}>}
|
|
82
100
|
*/
|
|
83
|
-
export function parseBaselines(ownerConfig) {
|
|
84
|
-
const raw = ownerConfig?.
|
|
101
|
+
export function parseBaselines(ownerConfig, section = 'baselines') {
|
|
102
|
+
const raw = ownerConfig?.[section];
|
|
85
103
|
|
|
86
104
|
// Absent or explicit null/empty → no-op
|
|
87
105
|
if (raw === undefined || raw === null) return [];
|
|
@@ -89,54 +107,56 @@ export function parseBaselines(ownerConfig) {
|
|
|
89
107
|
|
|
90
108
|
if (!Array.isArray(raw)) {
|
|
91
109
|
process.stderr.write(
|
|
92
|
-
|
|
110
|
+
`WARN named-baseline-resolver: owner.yaml ${section}: must be an array; ignoring\n`,
|
|
93
111
|
);
|
|
94
112
|
return [];
|
|
95
113
|
}
|
|
96
114
|
|
|
115
|
+
const nameRequired = NAME_REQUIRED_SECTIONS.has(section);
|
|
97
116
|
const result = [];
|
|
98
117
|
for (let i = 0; i < raw.length; i++) {
|
|
99
118
|
const entry = raw[i];
|
|
100
119
|
if (!isPlainObject(entry)) {
|
|
101
120
|
process.stderr.write(
|
|
102
|
-
`WARN named-baseline-resolver: owner.yaml
|
|
121
|
+
`WARN named-baseline-resolver: owner.yaml ${section}[${i}] is not an object; dropping\n`,
|
|
103
122
|
);
|
|
104
123
|
continue;
|
|
105
124
|
}
|
|
106
125
|
|
|
107
126
|
const { name, path: entryPath, match } = entry;
|
|
108
127
|
|
|
109
|
-
|
|
128
|
+
const nameAbsent = name === undefined || name === null;
|
|
129
|
+
if ((nameRequired || !nameAbsent) && (typeof name !== 'string' || name.trim() === '')) {
|
|
110
130
|
process.stderr.write(
|
|
111
|
-
`WARN named-baseline-resolver: owner.yaml
|
|
131
|
+
`WARN named-baseline-resolver: owner.yaml ${section}[${i}].name must be a non-empty string; dropping entry\n`,
|
|
112
132
|
);
|
|
113
133
|
continue;
|
|
114
134
|
}
|
|
115
135
|
if (typeof entryPath !== 'string' || entryPath.trim() === '') {
|
|
116
136
|
process.stderr.write(
|
|
117
|
-
`WARN named-baseline-resolver: owner.yaml
|
|
137
|
+
`WARN named-baseline-resolver: owner.yaml ${section}[${i}].path must be a non-empty string; dropping entry\n`,
|
|
118
138
|
);
|
|
119
139
|
continue;
|
|
120
140
|
}
|
|
121
141
|
|
|
122
|
-
// match is REQUIRED (unlike vaults) —
|
|
142
|
+
// match is REQUIRED (unlike vaults) — an entry with no path-prefix can
|
|
123
143
|
// never be selected, so it is dropped rather than kept.
|
|
124
144
|
if (!isPlainObject(match)) {
|
|
125
145
|
process.stderr.write(
|
|
126
|
-
`WARN named-baseline-resolver: owner.yaml
|
|
146
|
+
`WARN named-baseline-resolver: owner.yaml ${section}[${i}].match must be an object; dropping entry\n`,
|
|
127
147
|
);
|
|
128
148
|
continue;
|
|
129
149
|
}
|
|
130
150
|
const pathPrefix = match['path-prefix'];
|
|
131
151
|
if (typeof pathPrefix !== 'string' || pathPrefix.trim() === '') {
|
|
132
152
|
process.stderr.write(
|
|
133
|
-
`WARN named-baseline-resolver: owner.yaml
|
|
153
|
+
`WARN named-baseline-resolver: owner.yaml ${section}[${i}].match.path-prefix must be a non-empty string; dropping entry\n`,
|
|
134
154
|
);
|
|
135
155
|
continue;
|
|
136
156
|
}
|
|
137
157
|
|
|
138
158
|
result.push({
|
|
139
|
-
name: name.trim(),
|
|
159
|
+
name: nameAbsent ? entryPath.trim() : name.trim(),
|
|
140
160
|
path: entryPath.trim(),
|
|
141
161
|
match: { 'path-prefix': pathPrefix.trim() },
|
|
142
162
|
});
|
|
@@ -162,9 +182,10 @@ export function parseBaselines(ownerConfig) {
|
|
|
162
182
|
*
|
|
163
183
|
* @param {string} absPath — the working directory to classify
|
|
164
184
|
* @param {Array<{name:string, path:string, match:{'path-prefix':string}}>} baselines
|
|
185
|
+
* @param {string} [section='baselines'] — owner.yaml list name, used only in the WARN
|
|
165
186
|
* @returns {{name:string, path:string, match:{'path-prefix':string}}|null}
|
|
166
187
|
*/
|
|
167
|
-
export function matchBaselineForPath(absPath, baselines) {
|
|
188
|
+
export function matchBaselineForPath(absPath, baselines, section = 'baselines') {
|
|
168
189
|
if (!absPath || !Array.isArray(baselines) || baselines.length === 0) return null;
|
|
169
190
|
|
|
170
191
|
const target = _normalizePath(absPath);
|
|
@@ -181,7 +202,7 @@ export function matchBaselineForPath(absPath, baselines) {
|
|
|
181
202
|
|
|
182
203
|
if (matched.length > 1) {
|
|
183
204
|
process.stderr.write(
|
|
184
|
-
`WARN named-baseline-resolver: multiple
|
|
205
|
+
`WARN named-baseline-resolver: multiple ${section} match path "${absPath}": ${matched
|
|
185
206
|
.map((b) => b.name)
|
|
186
207
|
.join(', ')}; using first match "${matched[0].name}"\n`,
|
|
187
208
|
);
|
|
@@ -195,13 +216,13 @@ export function matchBaselineForPath(absPath, baselines) {
|
|
|
195
216
|
// ---------------------------------------------------------------------------
|
|
196
217
|
|
|
197
218
|
/**
|
|
198
|
-
* Resolve the
|
|
219
|
+
* Resolve the directory-prefix match tier (default `baselines:`) for a given cwd.
|
|
199
220
|
*
|
|
200
221
|
* Returns:
|
|
201
|
-
* { path, name, source:'match' } when
|
|
222
|
+
* { path, name, source:'match' } when an entry's directory-prefix matches cwd.
|
|
202
223
|
* { path:null, name:null, source:null } (the null-fallback) otherwise — including
|
|
203
|
-
* when
|
|
204
|
-
*
|
|
224
|
+
* when `env[envKey]` is set (this tier yields to the higher env tier the caller
|
|
225
|
+
* owns), when the section is not configured, or when nothing matches.
|
|
205
226
|
*
|
|
206
227
|
* Pure + synchronous — no git calls, no disk reads. Never throws.
|
|
207
228
|
*
|
|
@@ -209,22 +230,26 @@ export function matchBaselineForPath(absPath, baselines) {
|
|
|
209
230
|
* cwd?: string,
|
|
210
231
|
* ownerConfig?: object,
|
|
211
232
|
* env?: Record<string, string|undefined>,
|
|
212
|
-
*
|
|
233
|
+
* section?: string,
|
|
234
|
+
* envKey?: string,
|
|
235
|
+
* }} [opts] — `section` defaults to 'baselines', `envKey` to 'SO_BASELINE_PATH'
|
|
213
236
|
* @returns {{ path: string|null, name: string|null, source: 'match'|null }}
|
|
214
237
|
*/
|
|
215
238
|
export function resolveNamedBaseline({
|
|
216
239
|
cwd = process.cwd(),
|
|
217
240
|
ownerConfig,
|
|
218
241
|
env = process.env,
|
|
242
|
+
section = 'baselines',
|
|
243
|
+
envKey = 'SO_BASELINE_PATH',
|
|
219
244
|
} = {}) {
|
|
220
|
-
// Yield to the higher-precedence env tier the caller owns
|
|
221
|
-
const envVal = env?.
|
|
245
|
+
// Yield to the higher-precedence env tier the caller owns.
|
|
246
|
+
const envVal = env?.[envKey];
|
|
222
247
|
if (typeof envVal === 'string' && envVal.trim() !== '') {
|
|
223
248
|
return { path: null, name: null, source: null };
|
|
224
249
|
}
|
|
225
250
|
|
|
226
|
-
const
|
|
227
|
-
const matched =
|
|
251
|
+
const entries = parseBaselines(ownerConfig, section);
|
|
252
|
+
const matched = entries.length > 0 ? matchBaselineForPath(cwd, entries, section) : null;
|
|
228
253
|
if (matched !== null) {
|
|
229
254
|
return { path: matched.path, name: matched.name, source: 'match' };
|
|
230
255
|
}
|
|
@@ -47,11 +47,14 @@
|
|
|
47
47
|
* resolveCanonicalSuffixes({ ownerConfig, env })
|
|
48
48
|
* findRepoRoot(cwd, { existsSync, realpathSync })
|
|
49
49
|
* resolveNamedVault({ vaultName, cwd, ownerConfig, env, gitRemote, existsSync, realpathSync })
|
|
50
|
+
* normalizeRemote(url)
|
|
51
|
+
* checkCanonicalVault({ vaultDir, ownerConfig, env, readOriginUrl })
|
|
50
52
|
*/
|
|
51
53
|
|
|
52
54
|
import { join, dirname } from 'node:path';
|
|
53
55
|
import { existsSync as nodeExistsSync, realpathSync as nodeRealpathSync } from 'node:fs';
|
|
54
|
-
import {
|
|
56
|
+
import { spawnSync } from 'node:child_process';
|
|
57
|
+
import { resolvePreferredRemote, isQueryFailure, redactUrlCredentials } from './vcs-repo-spec.mjs';
|
|
55
58
|
|
|
56
59
|
// ---------------------------------------------------------------------------
|
|
57
60
|
// Internal helpers
|
|
@@ -255,6 +258,73 @@ export function resolveCanonicalSuffixes({ ownerConfig, env = process.env } = {}
|
|
|
255
258
|
return canonicalSuffixesFromVaults(vaults, env?.VAULT_MIRROR_CANONICAL_SUFFIX);
|
|
256
259
|
}
|
|
257
260
|
|
|
261
|
+
// ---------------------------------------------------------------------------
|
|
262
|
+
// checkCanonicalVault — the canonical-vault write guard (#1450)
|
|
263
|
+
// ---------------------------------------------------------------------------
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* Default origin reader: `git -C <vaultDir> remote get-url origin`. Returns
|
|
267
|
+
* `''` on a spawn error or a non-zero exit, so every failure reads as "no
|
|
268
|
+
* origin" and the guard stays fail-closed.
|
|
269
|
+
* @param {string} vaultDir
|
|
270
|
+
* @returns {string}
|
|
271
|
+
*/
|
|
272
|
+
function _defaultReadOriginUrl(vaultDir) {
|
|
273
|
+
const res = spawnSync('git', ['-C', vaultDir, 'remote', 'get-url', 'origin'], { encoding: 'utf8' });
|
|
274
|
+
if (res.error || res.status !== 0) return '';
|
|
275
|
+
return String(res.stdout ?? '').trim();
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* Decide whether `vaultDir` is a canonical Meta-Vault, i.e. whether its git
|
|
280
|
+
* `origin` ends in one of {@link resolveCanonicalSuffixes}. Shared by every
|
|
281
|
+
* writer into the vault so none of them can write into a foreign vault that
|
|
282
|
+
* `vault-dir` happened to resolve to.
|
|
283
|
+
*
|
|
284
|
+
* Fail-closed: a missing origin, a failing or throwing reader, and a suffix
|
|
285
|
+
* resolution that throws all return `ok: false`. The only bypass is the
|
|
286
|
+
* existing `VAULT_MIRROR_SKIP_CANONICAL_CHECK === '1'` switch, read from the
|
|
287
|
+
* `env` passed in (never from the ambient process.env when `env` is given).
|
|
288
|
+
* It is evaluated before any suffix resolution, so a skipped check neither
|
|
289
|
+
* parses `ownerConfig.vaults` nor prints its WARN lines, and returns
|
|
290
|
+
* `expected: []`.
|
|
291
|
+
*
|
|
292
|
+
* @param {{
|
|
293
|
+
* vaultDir: string,
|
|
294
|
+
* ownerConfig?: object,
|
|
295
|
+
* env?: Record<string, string|undefined>,
|
|
296
|
+
* readOriginUrl?: (vaultDir: string) => string,
|
|
297
|
+
* }} opts
|
|
298
|
+
* @returns {{ ok: boolean, reason?: 'skipped-by-env'|'no-git-origin'|'not-canonical', got?: string, expected: string[] }}
|
|
299
|
+
*/
|
|
300
|
+
export function checkCanonicalVault({ vaultDir, ownerConfig, env = process.env, readOriginUrl = _defaultReadOriginUrl } = {}) {
|
|
301
|
+
if (env?.VAULT_MIRROR_SKIP_CANONICAL_CHECK === '1') {
|
|
302
|
+
// Empty on purpose: suffix resolution is exactly what the switch skips, and
|
|
303
|
+
// callers read `expected` only on `ok: false`.
|
|
304
|
+
return { ok: true, reason: 'skipped-by-env', expected: [] };
|
|
305
|
+
}
|
|
306
|
+
let expected;
|
|
307
|
+
try {
|
|
308
|
+
expected = resolveCanonicalSuffixes({ ownerConfig, env });
|
|
309
|
+
} catch {
|
|
310
|
+
return { ok: false, reason: 'not-canonical', expected: [] };
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
let url;
|
|
314
|
+
try {
|
|
315
|
+
url = String(readOriginUrl(vaultDir) ?? '').trim();
|
|
316
|
+
} catch {
|
|
317
|
+
url = '';
|
|
318
|
+
}
|
|
319
|
+
if (!url) return { ok: false, reason: 'no-git-origin', expected };
|
|
320
|
+
|
|
321
|
+
const normalized = normalizeRemote(url);
|
|
322
|
+
if (!expected.some((s) => normalized.endsWith(s))) {
|
|
323
|
+
return { ok: false, reason: 'not-canonical', got: url, expected };
|
|
324
|
+
}
|
|
325
|
+
return { ok: true, expected };
|
|
326
|
+
}
|
|
327
|
+
|
|
258
328
|
// ---------------------------------------------------------------------------
|
|
259
329
|
// findRepoRoot — injectable IO
|
|
260
330
|
// ---------------------------------------------------------------------------
|
|
@@ -414,7 +484,7 @@ export function resolveNamedVault({
|
|
|
414
484
|
|
|
415
485
|
// Guard: if this repo IS one of the vaults, skip (don't self-mirror)
|
|
416
486
|
const canonicalSuffixes = canonicalSuffixesFromVaults(vaults, env?.VAULT_MIRROR_CANONICAL_SUFFIX);
|
|
417
|
-
const normalized =
|
|
487
|
+
const normalized = normalizeRemote(remoteUrl);
|
|
418
488
|
if (canonicalSuffixes.some((s) => normalized.endsWith(s))) {
|
|
419
489
|
// This is the vault itself — fall through to fallback
|
|
420
490
|
} else {
|
|
@@ -460,11 +530,14 @@ function _resolveEnvSuffix(env) {
|
|
|
460
530
|
}
|
|
461
531
|
|
|
462
532
|
/**
|
|
463
|
-
* Normalize a git remote URL to host/path tail
|
|
533
|
+
* Normalize a git remote URL to a host/path tail for canonical-suffix matching.
|
|
534
|
+
* Strips `.git`, the `git@host:` / scheme prefixes, and trailing slashes —
|
|
535
|
+
* byte-identical to vault-mirror.mjs `_normalizeRemote`, which predates this
|
|
536
|
+
* export and can import it from here.
|
|
464
537
|
* @param {string} url
|
|
465
538
|
* @returns {string}
|
|
466
539
|
*/
|
|
467
|
-
function
|
|
540
|
+
export function normalizeRemote(url) {
|
|
468
541
|
return String(url ?? '')
|
|
469
542
|
.trim()
|
|
470
543
|
.replace(/\.git$/, '')
|
|
@@ -473,6 +546,32 @@ function _normalizeRemoteUrl(url) {
|
|
|
473
546
|
.replace(/\/+$/, '');
|
|
474
547
|
}
|
|
475
548
|
|
|
549
|
+
/**
|
|
550
|
+
* Render a git origin URL for a stderr line (#1479). The vault writers print
|
|
551
|
+
* the origin {@link checkCanonicalVault} rejected, and that stderr lands in CI
|
|
552
|
+
* logs and session transcripts — so a LOCAL origin must not carry its path
|
|
553
|
+
* (the operator's home dir, `.claude/rules/security.md` § Owner-Privacy).
|
|
554
|
+
*
|
|
555
|
+
* - `file:` URL, or a bare/relative path (git's rule: no `://` and no `:`
|
|
556
|
+
* before the first `/`, a Windows drive letter counting as a path)
|
|
557
|
+
* → `file://<local path>`.
|
|
558
|
+
* - Every other form (`https://`, `ssh://`, scp-like `git@host:path`) keeps its
|
|
559
|
+
* host/path and goes through {@link redactUrlCredentials}, so an
|
|
560
|
+
* `https://user:token@host/…` origin cannot leak its credential either.
|
|
561
|
+
*
|
|
562
|
+
* Returns `''` for an empty/absent origin; callers supply their own wording.
|
|
563
|
+
* @param {string|undefined|null} url
|
|
564
|
+
* @returns {string}
|
|
565
|
+
*/
|
|
566
|
+
export function describeOriginForLog(url) {
|
|
567
|
+
const s = String(url ?? '').trim();
|
|
568
|
+
if (!s) return '';
|
|
569
|
+
if (/^file:/i.test(s)) return 'file://<local path>';
|
|
570
|
+
if (s.includes('://')) return redactUrlCredentials(s);
|
|
571
|
+
const scpLike = /^[^/\\]*:/.test(s) && !/^[a-z]:[\\/]/i.test(s);
|
|
572
|
+
return scpLike ? redactUrlCredentials(s) : 'file://<local path>';
|
|
573
|
+
}
|
|
574
|
+
|
|
476
575
|
/**
|
|
477
576
|
* Derive an org/repo slug from a git remote URL.
|
|
478
577
|
* e.g. 'git@github.com:my-org/my-repo.git' → 'my-org/my-repo'
|
|
@@ -480,7 +579,7 @@ function _normalizeRemoteUrl(url) {
|
|
|
480
579
|
* @returns {string}
|
|
481
580
|
*/
|
|
482
581
|
function _deriveSlugFromRemote(url) {
|
|
483
|
-
const normalized =
|
|
582
|
+
const normalized = normalizeRemote(url);
|
|
484
583
|
// normalized is now 'host/path/to/repo' — take the last two segments
|
|
485
584
|
const parts = normalized.split('/').filter(Boolean);
|
|
486
585
|
if (parts.length >= 2) {
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* orphan-reaper/defaults.mjs — the reaper's parameter table and its repo-path join.
|
|
3
|
+
*
|
|
4
|
+
* Imports nothing but `node:path` on purpose: the trigger hooks reach it through
|
|
5
|
+
* `scan-throttle.mjs` on a hot path whose import closure is measured (#1432), and
|
|
6
|
+
* every other reaper module shares these numbers instead of re-spelling them.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import path from 'node:path';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Stufe-1 parameters. Every number carries its provenance from the PRD
|
|
13
|
+
* parameter table (`docs/prd/2026-09-20-prozessgruppen-kill-und-waisen-waechter.md`
|
|
14
|
+
* § 4) — NOT calibrated, so the provenance is what makes a later re-measurement
|
|
15
|
+
* possible ("keine Zahl ohne ihre Population").
|
|
16
|
+
*/
|
|
17
|
+
export const REAPER_DEFAULTS = Object.freeze({
|
|
18
|
+
/** `reaper.min-age-seconds`: DevWatchdogs' hard limit for `tsgo`; the
|
|
19
|
+
* 2026-09-20 orphans were 7-17 min old, comfortably above it. */
|
|
20
|
+
minAgeSeconds: 300,
|
|
21
|
+
/** `reaper.min-scan-interval-seconds`: DevWatchdogs' normal scan cadence —
|
|
22
|
+
* keeps a `PostToolBatch` storm from taxing every tool call. */
|
|
23
|
+
minScanIntervalSeconds: 30,
|
|
24
|
+
/** `reaper.kill-grace-ms`: `DEFAULT_KILL_GRACE_MS` from `process-group.mjs` —
|
|
25
|
+
* a repo convention, not a new number. A literal rather than an import so this
|
|
26
|
+
* module stays out of `process-group.mjs`'s closure. */
|
|
27
|
+
killGraceMs: 10_000,
|
|
28
|
+
/** `reaper.verify-wait-ms`: without it the 2026-09-20 hand-run cleanup
|
|
29
|
+
* reported "still alive" for processes that were already gone. */
|
|
30
|
+
verifyWaitMs: 500,
|
|
31
|
+
/** `reaper.max-hook-latency-ms`: ceiling a scan may delay a hook by. */
|
|
32
|
+
maxHookLatencyMs: 50,
|
|
33
|
+
/** `reaper.false-alarm-window`: last N audit decisions — a ROLLING window
|
|
34
|
+
* rather than calendar time, so a quiet host still has a population (HR-101). */
|
|
35
|
+
falseAlarmWindow: 50,
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
/** @param {string} repoRoot @param {string} relpath @returns {string} */
|
|
39
|
+
export function underRepo(repoRoot, relpath) {
|
|
40
|
+
return path.join(repoRoot, ...relpath.split('/'));
|
|
41
|
+
}
|