session-orchestrator 5.12.0 → 5.13.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/.claude-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +4 -1
- package/.codex-plugin/agents/explorer.toml +1 -1
- package/.codex-plugin/agents/session-reviewer.toml +2 -2
- package/.codex-plugin/agents/wave-worker.toml +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/.cursor-plugin/plugin.json +1 -1
- package/.orchestrator/policy/blocked-commands.json +8 -0
- package/.orchestrator/policy/public-export.json +30 -0
- package/CHANGELOG.md +162 -4
- package/README.md +126 -127
- package/agents/analyst.md +7 -2
- package/agents/architect-reviewer.md +7 -2
- package/agents/code-implementer.md +4 -2
- package/agents/db-specialist.md +9 -2
- package/agents/dialectic-deriver.md +9 -3
- package/agents/docs-writer.md +7 -2
- package/agents/eval-judge.md +7 -1
- package/agents/qa-strategist.md +7 -2
- package/agents/security-reviewer.md +9 -2
- package/agents/session-reviewer.md +18 -2
- package/agents/skill-applied-judge.md +7 -1
- package/agents/test-writer.md +9 -2
- package/agents/ui-developer.md +9 -2
- package/agents/ux-evaluator.md +6 -1
- package/commands/session.md +1 -1
- package/commands/templates-ack.md +89 -18
- package/docs/agent-authoring.md +30 -11
- package/docs/ci-setup.md +38 -2
- package/docs/codex-setup.md +33 -9
- package/docs/components.md +13 -2
- package/docs/cursor-setup.md +3 -3
- package/docs/events-schema.md +5 -4
- package/docs/evidence-candidates.md +63 -0
- package/docs/fleet-checkin-audit.md +7 -4
- package/docs/github-mirror-protection.md +150 -3
- package/docs/install.md +90 -17
- package/docs/instruction-delivery.md +1 -1
- package/docs/migration-v4.md +2 -2
- package/docs/mods.md +118 -0
- package/docs/owner-config-schema.md +1 -0
- package/docs/plugin-architecture-v3.md +1 -1
- package/docs/pm-skills-marketplace.md +4 -3
- package/docs/rule-authoring.md +24 -13
- package/docs/scope-collision-guard.md +2 -1
- package/docs/session-config-reference.md +49 -15
- package/docs/session-config-template.md +30 -22
- package/docs/sessions-ledger-repair.md +137 -0
- package/docs/vault-docs-architecture.md +1 -1
- package/hooks/_lib/hook-import-set.json +406 -73
- package/hooks/_lib/hook-root.mjs +301 -0
- package/hooks/_lib/lock-bootstrap.mjs +434 -4
- package/hooks/_lib/orchestrator-ignore.mjs +3 -0
- package/hooks/_lib/subagent-transcript.mjs +2 -2
- package/hooks/_lib/vcs-create-matcher.mjs +13 -17
- package/hooks/cwd-change-restore.mjs +5 -1
- package/hooks/enforce-commands.mjs +4 -3
- package/hooks/enforce-scope.mjs +11 -1
- package/hooks/hooks-codex.json +48 -2
- package/hooks/hooks.json +31 -31
- package/hooks/on-session-end.mjs +38 -15
- package/hooks/on-session-start.mjs +139 -49
- package/hooks/on-stop.mjs +11 -6
- package/hooks/post-bash-issue-budget-refund.mjs +6 -1
- package/hooks/post-bash-write-verify.mjs +8 -4
- package/hooks/post-edit-validate.mjs +45 -52
- package/hooks/post-subagent-discovery-validator.mjs +2 -2
- package/hooks/post-tool-batch-wave-signal.mjs +8 -4
- package/hooks/post-tool-failure-corrective-context.mjs +5 -1
- package/hooks/pre-bash-destructive-guard.mjs +220 -8
- package/hooks/pre-bash-issue-budget.mjs +6 -1
- package/hooks/pre-bash-sessions-ledger-guard.mjs +691 -123
- package/hooks/pre-bash-staging-fence.mjs +258 -14
- package/hooks/pre-bash-templates-first.mjs +28 -27
- package/hooks/run-node.sh +1 -1
- package/hooks/skill-invocation-telemetry.mjs +15 -8
- package/hooks/subagent-telemetry.mjs +77 -14
- package/hooks/wave-scope-commit-guard.mjs +143 -14
- package/mods/so-hud/.claude-plugin/plugin.json +11 -0
- package/mods/so-hud/README.md +57 -0
- package/mods/so-hud/hooks/beacon-io.mjs +101 -0
- package/mods/so-hud/hooks/heredoc-scan.ts +394 -0
- package/mods/so-hud/hooks/hooks.json +1 -0
- package/mods/so-hud/hooks/logic.ts +182 -0
- package/mods/so-hud/hooks/register.tsx +670 -0
- package/mods/so-hud/hooks/rules.ts +2998 -0
- package/mods/so-hud/tsconfig.json +8 -0
- package/mods/so-hud/types/index.d.ts +36 -0
- package/package.json +4 -2
- package/rules/_index.md +1 -1
- package/rules/always-on/ask-via-tool.md +1 -1
- package/rules/always-on/loop-and-monitor.md +17 -194
- package/rules/always-on/parallel-sessions.md +3 -1
- package/rules/opt-in-stack/backend-data.md +1 -1
- package/rules/opt-in-stack/backend.md +1 -1
- package/rules/opt-in-stack/security-web.md +5 -1
- package/scripts/agent-usage-census.mjs +79 -0
- package/scripts/apply-session-learnings.mjs +34 -6
- package/scripts/archive-closed-prds.mjs +21 -5
- package/scripts/autopilot.mjs +11 -5
- package/scripts/backfill-abandoned-sessions.mjs +13 -3
- package/scripts/backfill-evidence-digest.mjs +2 -1
- package/scripts/backfill-learnings-from-vault.mjs +61 -6
- package/scripts/backfill-sessions.mjs +16 -2
- package/scripts/check-package-manager.mjs +26 -8
- package/scripts/check-sessions-integrity.mjs +4 -1
- package/scripts/ci/assert-vitest-green.mjs +3 -0
- package/scripts/ci/windows-smoke.mjs +201 -0
- package/scripts/close-precheck.mjs +55 -0
- package/scripts/dialectic-deriver.mjs +20 -12
- package/scripts/emit-event.mjs +2 -2
- package/scripts/emit-session.mjs +239 -20
- package/scripts/eval-session.mjs +17 -3
- package/scripts/export-hw-learnings.mjs +29 -2
- package/scripts/fleet-checkin-audit.mjs +8 -4
- package/scripts/fleet-checkin.mjs +39 -27
- package/scripts/gc-stale-worktrees.mjs +6 -1
- package/scripts/github-export.mjs +128 -0
- package/scripts/github-protection-audit.mjs +3 -1
- package/scripts/handover-queue-check.mjs +209 -0
- package/scripts/harness-audit.mjs +6 -4
- package/scripts/lib/agent-frontmatter.mjs +57 -10
- package/scripts/lib/agent-status.mjs +125 -7
- package/scripts/lib/agent-usage-census.mjs +394 -0
- package/scripts/lib/auto-dialectic.mjs +17 -10
- package/scripts/lib/auto-dream.mjs +22 -14
- package/scripts/lib/autopilot/loop.mjs +3 -1
- package/scripts/lib/autopilot/recent-runs.mjs +4 -2
- package/scripts/lib/autopilot/stall-sampler.mjs +2 -1
- package/scripts/lib/autopilot/telemetry.mjs +5 -1
- package/scripts/lib/autopilot/worktree-pipeline.mjs +5 -5
- package/scripts/lib/baseline-archetypes.mjs +10 -2
- package/scripts/lib/blocked-commands-policy.mjs +4 -3
- package/scripts/lib/build-live-signals.mjs +13 -9
- package/scripts/lib/ci-status-banner.mjs +329 -24
- package/scripts/lib/codex/plugin-contract.mjs +8 -0
- package/scripts/lib/cold-start-detector.mjs +5 -2
- package/scripts/lib/command-blocker.mjs +603 -43
- package/scripts/lib/common.mjs +3 -2
- package/scripts/lib/config/cross-repo.mjs +74 -7
- package/scripts/lib/config/host-paths.mjs +6 -2
- package/scripts/lib/config/section-extractor.mjs +65 -11
- package/scripts/lib/config/template-keys.mjs +153 -0
- package/scripts/lib/config/vault-integration.mjs +1 -0
- package/scripts/lib/config.mjs +2 -2
- package/scripts/lib/coordinator-snapshot.mjs +1 -1
- package/scripts/lib/discovery/triage-state.mjs +2 -1
- package/scripts/lib/dispatcher/rank.mjs +5 -7
- package/scripts/lib/ecosystem-health.mjs +2 -2
- package/scripts/lib/eval/engine.mjs +3 -2
- package/scripts/lib/eval/session-resolve.mjs +6 -2
- package/scripts/lib/eval/sink.mjs +4 -3
- package/scripts/lib/events-retention-banner.mjs +3 -2
- package/scripts/lib/events-schema.mjs +10 -0
- package/scripts/lib/events.mjs +23 -12
- package/scripts/lib/evolve/autonomy-verdict.mjs +4 -9
- package/scripts/lib/evolve/autopilot-effectiveness.mjs +8 -18
- package/scripts/lib/fleet-protocol.mjs +4 -0
- package/scripts/lib/gates/gate-full.mjs +172 -11
- package/scripts/lib/gates/gate-helpers.mjs +236 -23
- package/scripts/lib/git-config-drift.mjs +141 -7
- package/scripts/lib/github-export.mjs +447 -0
- package/scripts/lib/gitlab-portfolio/cli.mjs +16 -5
- package/scripts/lib/handover-queue-marks.mjs +159 -0
- package/scripts/lib/hardware-pattern-detector.mjs +103 -13
- package/scripts/lib/harness-audit/categories/category1.mjs +16 -4
- package/scripts/lib/harness-audit/categories/category4.mjs +11 -8
- package/scripts/lib/harness-audit/categories/category6.mjs +25 -5
- package/scripts/lib/harness-audit/categories/category9.mjs +10 -4
- package/scripts/lib/host-identity.mjs +9 -5
- package/scripts/lib/http-request.mjs +64 -0
- package/scripts/lib/instruction-budget-guard.mjs +30 -22
- package/scripts/lib/io.mjs +6 -3
- package/scripts/lib/issue-budget-reconcile.mjs +3 -2
- package/scripts/lib/issue-budget.mjs +36 -7
- package/scripts/lib/learnings/evidence-candidates.mjs +154 -0
- package/scripts/lib/learnings/expiry-sweep.mjs +99 -7
- package/scripts/lib/learnings/io.mjs +289 -15
- package/scripts/lib/learnings/select.mjs +53 -4
- package/scripts/lib/learnings/store-role.mjs +209 -0
- package/scripts/lib/learnings/surface.mjs +46 -7
- package/scripts/lib/ledger-path.mjs +113 -0
- package/scripts/lib/maintenance-due-banner.mjs +63 -12
- package/scripts/lib/memory-banner.mjs +5 -6
- package/scripts/lib/memory-proposals/collector.mjs +5 -2
- package/scripts/lib/memory-proposals/sink.mjs +7 -4
- package/scripts/lib/memory-proposals/store.mjs +22 -5
- package/scripts/lib/mode-selector-accuracy.mjs +2 -1
- package/scripts/lib/no-follow-open.mjs +57 -0
- package/scripts/lib/orphan-reaper/reaper-audit.mjs +4 -2
- package/scripts/lib/own-lock-root.mjs +95 -0
- package/scripts/lib/owner-interview.mjs +2 -1
- package/scripts/lib/owner-yaml.mjs +70 -7
- package/scripts/lib/platform.mjs +161 -9
- package/scripts/lib/plugin-update-banner.mjs +15 -11
- package/scripts/lib/pre-push-local-partial.mjs +117 -0
- package/scripts/lib/pre-push-remote-gate.mjs +151 -25
- package/scripts/lib/process-group.mjs +270 -34
- package/scripts/lib/project-hygiene.mjs +28 -0
- package/scripts/lib/quality-gate.mjs +33 -9
- package/scripts/lib/reconcile/backlog.mjs +29 -15
- package/scripts/lib/reconcile/engine.mjs +26 -2
- package/scripts/lib/reconcile/idempotency.mjs +65 -3
- package/scripts/lib/reconcile/rule-expiry-sweep.mjs +53 -12
- package/scripts/lib/reconcile/verdict-ledger.mjs +224 -0
- package/scripts/lib/reconcile-nudge-banner.mjs +40 -6
- package/scripts/lib/resource-probe/evaluate.mjs +32 -9
- package/scripts/lib/resource-probe/probe-platform.mjs +3 -1
- package/scripts/lib/rule-loader.mjs +20 -10
- package/scripts/lib/scope-baseline.mjs +123 -8
- package/scripts/lib/scope-echo.mjs +5 -2
- package/scripts/lib/scope-gate.mjs +55 -20
- package/scripts/lib/session-close-backfill.mjs +75 -7
- package/scripts/lib/session-end/close-precheck.mjs +151 -0
- package/scripts/lib/session-end/phase-skip.mjs +8 -7
- package/scripts/lib/session-end/tail-runner.mjs +7 -2
- package/scripts/lib/session-id.mjs +9 -1
- package/scripts/lib/session-identity/own-session.mjs +63 -0
- package/scripts/lib/session-lock.mjs +114 -0
- package/scripts/lib/session-record-repair.mjs +245 -35
- package/scripts/lib/session-registry.mjs +72 -9
- package/scripts/lib/session-schema/constants.mjs +9 -0
- package/scripts/lib/session-schema/filters.mjs +56 -1
- package/scripts/lib/session-schema/validator.mjs +15 -5
- package/scripts/lib/session-start-probes.mjs +149 -22
- package/scripts/lib/session-token-rollup.mjs +2 -1
- package/scripts/lib/session-work-trees.mjs +166 -0
- package/scripts/lib/sessions-canonical.mjs +61 -3
- package/scripts/lib/sessions-integrity-banner.mjs +17 -39
- package/scripts/lib/sessions-staleness-banner.mjs +8 -5
- package/scripts/lib/sh-quote.mjs +1 -0
- package/scripts/lib/skill-evidence-window.mjs +5 -1
- package/scripts/lib/skill-health/join.mjs +10 -9
- package/scripts/lib/skill-invocations-schema.mjs +13 -0
- package/scripts/lib/skill-judgments-schema.mjs +6 -2
- package/scripts/lib/snapshot-probe.mjs +414 -0
- package/scripts/lib/so-hud/collect.mjs +49 -5
- package/scripts/lib/so-hud/runtime.mjs +15 -4
- package/scripts/lib/spiral-carryover.mjs +8 -1
- package/scripts/lib/state-md/frontmatter-mutators.mjs +266 -8
- package/scripts/lib/state-md/idle-reset.mjs +14 -5
- package/scripts/lib/state-md/mission-status.mjs +5 -1
- package/scripts/lib/state-md-peer-guard.mjs +2 -0
- package/scripts/lib/state-md.mjs +31 -8
- package/scripts/lib/sunset/walker.mjs +4 -3
- package/scripts/lib/telemetry/consent.mjs +4 -2
- package/scripts/lib/telemetry/queue.mjs +3 -3
- package/scripts/lib/telemetry/sync.mjs +45 -9
- package/scripts/lib/test-runner/artifact-paths.mjs +10 -2
- package/scripts/lib/tmux-layout/layouts.mjs +20 -3
- package/scripts/lib/ux-grill/paths.mjs +5 -1
- package/scripts/lib/validate/check-agents.mjs +37 -6
- package/scripts/lib/validate/check-component-paths.mjs +25 -1
- package/scripts/lib/validate/check-guard-requires-parity.mjs +3 -3
- package/scripts/lib/validate/check-hooks-symmetry.mjs +53 -32
- package/scripts/lib/validate/check-json-files.mjs +3 -1
- package/scripts/lib/validate/check-learning-provenance.mjs +3 -2
- package/scripts/lib/validate/check-learnings-shell-writes.mjs +10 -4
- package/scripts/lib/validate/check-metrics-ledger-root.mjs +234 -0
- package/scripts/lib/validate/check-owner-leakage.mjs +159 -11
- package/scripts/lib/validate/check-test-posix-shell.mjs +246 -0
- package/scripts/lib/validate/check-unwired-features.mjs +4 -2
- package/scripts/lib/validate/check-vcs-repo-flag-positional.mjs +58 -0
- package/scripts/lib/validate/check-vcs-repo-flag.mjs +259 -73
- package/scripts/lib/validate/check-workflow-continue-on-error.mjs +63 -0
- package/scripts/lib/validate/confidential-names.mjs +3 -1
- package/scripts/lib/vault-mirror/migrate-ids.mjs +321 -0
- package/scripts/lib/vault-mirror/namespace.mjs +19 -7
- package/scripts/lib/vault-mirror/process.mjs +56 -27
- package/scripts/lib/vault-mirror/prune.mjs +5 -2
- package/scripts/lib/vault-mirror/render-learnings.mjs +3 -3
- package/scripts/lib/vault-mirror/render-sessions.mjs +36 -53
- package/scripts/lib/vault-mirror/retention.mjs +2 -2
- package/scripts/lib/vault-mirror/utils.mjs +79 -0
- package/scripts/lib/vault-staleness-banner.mjs +4 -7
- package/scripts/lib/vault-status/board-writer.mjs +2 -1
- package/scripts/lib/vault-status/narrative-mirror.mjs +66 -7
- package/scripts/lib/wave-executor/foreign-dispatch.mjs +20 -0
- package/scripts/lib/win-process-tree.mjs +137 -0
- package/scripts/lib/workspace.mjs +1 -1
- package/scripts/lib/worktree/constants.mjs +1 -1
- package/scripts/lib/worktree/lifecycle.mjs +1 -1
- package/scripts/lib/worktree/listing.mjs +1 -1
- package/scripts/lib/zx.mjs +50 -0
- package/scripts/mcp-server.mjs +16 -7
- package/scripts/mcp-server.sh +13 -83
- package/scripts/measure-policy-cache-effectiveness.mjs +7 -5
- package/scripts/memory-propose.mjs +10 -3
- package/scripts/migrate-cold-start-seed.mjs +7 -0
- package/scripts/migrate-legacy-learnings.sh +22 -0
- package/scripts/migrate-sessions-jsonl.mjs +16 -2
- package/scripts/migrate-vault-paths.mjs +29 -13
- package/scripts/parse-config.mjs +71 -14
- package/scripts/print-learnings-index.mjs +25 -2
- package/scripts/release-session-lock.mjs +94 -7
- package/scripts/release.mjs +451 -24
- package/scripts/relocate-vault-corpus.mjs +11 -7
- package/scripts/repair-invalid-sessions.mjs +68 -3
- package/scripts/resolve-session-invocation.mjs +10 -17
- package/scripts/run-migrate-v2-cross-repo.mjs +56 -1
- package/scripts/run-quality-gate.mjs +33 -7
- package/scripts/self-update.mjs +56 -7
- package/scripts/session-commit-files.mjs +133 -0
- package/scripts/site-numbers.mjs +52 -9
- package/scripts/snapshot-probe.mjs +156 -0
- package/scripts/so-hud.mjs +29 -6
- package/scripts/state-md-idle-reset.mjs +2 -1
- package/scripts/sweep-expired-learnings.mjs +54 -6
- package/scripts/sweep-expired-rules.mjs +4 -1
- package/scripts/validate-plugin.mjs +22 -0
- package/scripts/vault-integration-watcher.mjs +54 -6
- package/scripts/vault-mirror.mjs +9 -0
- package/skills/_shared/fleet-protocol.md +142 -1
- package/skills/_shared/instruction-file-resolution.md +1 -1
- package/skills/_shared/monitor-patterns.md +44 -25
- package/skills/_shared/platform-tools.md +7 -0
- package/skills/_shared/state-ownership.md +3 -2
- package/skills/architecture/references/domain-model.md +1 -1
- package/skills/bootstrap/SKILL.md +3 -1
- package/skills/bootstrap/_shared-template.md +4 -4
- package/skills/bootstrap/fast-template.md +1 -1
- package/skills/bootstrap/private-contract.md +1 -1
- package/skills/bootstrap/references/bootstrap-retroactive-flow.md +17 -9
- package/skills/bootstrap/templates/agents/project-code-review.md +5 -1
- package/skills/bootstrap/templates/agents/project-discovery.md +5 -1
- package/skills/bootstrap/templates/agents/project-quality-gate.md +5 -1
- package/skills/claude-md-drift-check/SKILL.md +3 -3
- package/skills/claude-md-drift-check/checker.mjs +61 -101
- package/skills/close/SKILL.md +20 -7
- package/skills/discovery/probes/docs-staleness.mjs +7 -5
- package/skills/discovery/probes/frontend-slop.mjs +7 -3
- package/skills/discovery/probes/ssot-code-diff.mjs +5 -3
- package/skills/discovery/probes/supply-chain-slopcheck.mjs +10 -8
- package/skills/discovery/probes/vault-narrative-staleness.mjs +30 -10
- package/skills/discovery/probes/vault-staleness.mjs +5 -3
- package/skills/discovery/probes-session.md +3 -0
- package/skills/discovery/probes-vault.md +2 -0
- package/skills/dispatcher/SKILL.md +1 -1
- package/skills/eval/SKILL.md +3 -1
- package/skills/evolve/SKILL.md +2 -0
- package/skills/evolve/references/evolve-analyze-mode.md +3 -3
- package/skills/evolve/references/evolve-dialectic-mode.md +1 -1
- package/skills/gitlab-ops/SKILL.md +7 -6
- package/skills/gitlab-portfolio/SKILL.md +2 -2
- package/skills/go/SKILL.md +13 -1
- package/skills/npm-publish/SKILL.md +5 -2
- package/skills/plan/SKILL.md +23 -10
- package/skills/plan/mode-feature.md +13 -8
- package/skills/plan/mode-new.md +6 -14
- package/skills/plan/prd-feature-template.md +19 -0
- package/skills/plan/prd-full-template.md +12 -5
- package/skills/plan/prd-premortem-panel.md +48 -0
- package/skills/plan/prd-reviewer-prompt.md +11 -3
- package/skills/plan/soul.md +1 -1
- package/skills/portfolio/SKILL.md +5 -6
- package/skills/quality-gates/SKILL.md +2 -4
- package/skills/reconcile/SKILL.md +27 -4
- package/skills/release/SKILL.md +16 -8
- package/skills/session-end/SKILL.md +94 -11
- package/skills/session-end/phase-3-2-docs-verification.md +1 -0
- package/skills/session-end/phase-3-6-tail.md +11 -7
- package/skills/session-end/phase-3-7a-recommendations.md +9 -2
- package/skills/session-end/plan-verification.md +16 -1
- package/skills/session-end/references/phase-3-documentation-updates.md +14 -12
- package/skills/session-end/references/phase-5-issue-cleanup.md +1 -1
- package/skills/session-end/references/session-summary-template.md +4 -0
- package/skills/session-end/session-metrics-write.md +7 -4
- package/skills/session-plan/SKILL.md +5 -3
- package/skills/session-start/SKILL.md +9 -3
- package/skills/session-start/phase-4-5-resource-health.md +11 -5
- package/skills/session-start/phase-8-5-express-path.md +4 -3
- package/skills/session-start/references/phase-1-2-session-lock.md +7 -5
- package/skills/session-start/references/phase-1-5-session-continuity.md +2 -2
- 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 +3 -2
- package/skills/tmux-layout/SKILL.md +2 -2
- package/skills/vault-sync/validator.mjs +3 -2
- package/skills/wave-executor/SKILL.md +5 -4
- package/skills/wave-executor/references/wave-executor-state-init.md +3 -3
- package/skills/wave-executor/references/wave-loop-dispatch.md +11 -5
- package/skills/wave-executor/references/wave-loop-review.md +13 -5
- package/skills/wave-executor/references/wave-loop-scope-manifest.md +1 -1
- package/assets/og-card.png +0 -0
- package/assets/og-card.svg +0 -47
- package/assets/wave-lifecycle.svg +0 -98
|
@@ -69,7 +69,7 @@ import { fileURLToPath } from 'node:url';
|
|
|
69
69
|
import { emitEvent, sessionAttribution } from './events.mjs';
|
|
70
70
|
import { resolveWaveNumber } from './quality-gate-wave.mjs';
|
|
71
71
|
import { parsePorcelainZ } from './git-porcelain.mjs';
|
|
72
|
-
import { admitSuiteCounts, extractTestCounts } from './gates/gate-helpers.mjs';
|
|
72
|
+
import { admitSuiteCounts, extractTestCounts, leftoverGroupLine } from './gates/gate-helpers.mjs';
|
|
73
73
|
import {
|
|
74
74
|
DEFAULT_GATE_TIMEOUT_MS,
|
|
75
75
|
buildCommandSignature,
|
|
@@ -78,6 +78,7 @@ import {
|
|
|
78
78
|
import { redactDiagnosticsBundle } from './quality-gate/diagnostics.mjs';
|
|
79
79
|
import { atomicWriteWithBackup, writeJsonAtomicSync } from './io.mjs';
|
|
80
80
|
import { readProcessLocalSessionIds } from './session-identity/own-session.mjs';
|
|
81
|
+
import { resolveOwnLockRoot } from '../../hooks/_lib/hook-root.mjs';
|
|
81
82
|
|
|
82
83
|
export { redactDiagnosticsBundle } from './quality-gate/diagnostics.mjs';
|
|
83
84
|
|
|
@@ -319,7 +320,8 @@ function resolveCommands(override, repoRoot) {
|
|
|
319
320
|
* (`spawnFn`, `killFn`, `isAliveFn`, `sleepFn`, `killGraceMs`, `verifyWaitMs`).
|
|
320
321
|
* Test seam only; production passes nothing and gets the module defaults.
|
|
321
322
|
* @returns {Promise<{ exitCode: number, output: string, timedOut: boolean,
|
|
322
|
-
* overflow: boolean, killSignals: string[], survivors: number[], pgid: number
|
|
323
|
+
* overflow: boolean, killSignals: string[], survivors: number[]|null, error?: string, pgid: number,
|
|
324
|
+
* leftoverGroup?: {pgid: number, signals: string[], survivors: number[]} }>}
|
|
323
325
|
*/
|
|
324
326
|
async function runGate(cmd, cwd, opts = {}) {
|
|
325
327
|
try {
|
|
@@ -349,9 +351,16 @@ async function runGate(cmd, cwd, opts = {}) {
|
|
|
349
351
|
// A process that outlived SIGKILL (one that `setsid`-ed out of the group) is
|
|
350
352
|
// SAID OUT LOUD on the channel the operator and the diagnostics bundle both
|
|
351
353
|
// read. Silence here would be the exact failure the ladder exists to expose.
|
|
352
|
-
const
|
|
354
|
+
const survivorOutput = result.survivors === null
|
|
355
|
+
? `${result.output}\nÜberlebende nicht messbar: ${result.error}`.trim()
|
|
356
|
+
: result.survivors.length > 0
|
|
353
357
|
? `${result.output}\ngate: ${result.survivors.length} process(es) survived SIGKILL: ${result.survivors.join(', ')}`.trim()
|
|
354
358
|
: result.output;
|
|
359
|
+
// #1704: a group still alive after a NORMAL exit was reaped by the ladder.
|
|
360
|
+
// Named here, never a status change (pass/fail stays the exit code's).
|
|
361
|
+
const output = result.leftoverGroup
|
|
362
|
+
? `${survivorOutput}\n${leftoverGroupLine(result.leftoverGroup)}`.trim()
|
|
363
|
+
: survivorOutput;
|
|
355
364
|
|
|
356
365
|
return {
|
|
357
366
|
exitCode: result.exitCode,
|
|
@@ -360,6 +369,8 @@ async function runGate(cmd, cwd, opts = {}) {
|
|
|
360
369
|
overflow: result.overflow,
|
|
361
370
|
killSignals: result.killSignals,
|
|
362
371
|
survivors: result.survivors,
|
|
372
|
+
...(result.survivors === null ? { error: result.error, ok: false } : {}),
|
|
373
|
+
...(result.leftoverGroup ? { leftoverGroup: result.leftoverGroup } : {}),
|
|
363
374
|
pgid: result.pgid,
|
|
364
375
|
};
|
|
365
376
|
} catch (err) {
|
|
@@ -607,16 +618,25 @@ function classifyCurrentSessionOwnership(parsed, ownIds) {
|
|
|
607
618
|
* WARN is not de-duplicated; this function runs at most once per fixer dispatch
|
|
608
619
|
* plus once at abort, and each line marks a distinct decision point.
|
|
609
620
|
*
|
|
621
|
+
* The file is read beside THIS session's own lock (#1630): after /clear in an
|
|
622
|
+
* entered worktree the lock and `current-session.json` live there (#1605), and
|
|
623
|
+
* after ExitWorktree the gate runs with `repoRoot` on the main checkout, where
|
|
624
|
+
* the file belongs to another session. `resolveOwnLockRoot` follows the host
|
|
625
|
+
* registry's `session_root` pointer — the lookup the hook writer
|
|
626
|
+
* (`post-tool-failure-corrective-context.mjs`) uses — and answers `repoRoot`
|
|
627
|
+
* unchanged when no own lock is found elsewhere.
|
|
628
|
+
*
|
|
610
629
|
* @param {string} repoRoot
|
|
611
630
|
* @returns {Array<object>}
|
|
612
631
|
*/
|
|
613
632
|
function readCorrectiveContext(repoRoot) {
|
|
614
633
|
try {
|
|
615
|
-
const
|
|
634
|
+
const ownIds = readOwnSessionIds();
|
|
635
|
+
const ownRoot = resolveOwnLockRoot({ session_id: [...ownIds][0] ?? null, cwd: repoRoot }, repoRoot);
|
|
636
|
+
const p = join(ownRoot, '.orchestrator', 'current-session.json');
|
|
616
637
|
if (!existsSync(p)) return [];
|
|
617
638
|
const raw = readFileSync(p, 'utf8');
|
|
618
639
|
const parsed = JSON.parse(raw);
|
|
619
|
-
const ownIds = readOwnSessionIds();
|
|
620
640
|
const { verdict, fileIds } = classifyCurrentSessionOwnership(parsed, ownIds);
|
|
621
641
|
if (verdict === 'foreign') {
|
|
622
642
|
process.stderr.write(
|
|
@@ -791,13 +811,15 @@ function coerceMaxRetries(n) {
|
|
|
791
811
|
* - `kill_signals` — the signals actually sent, in order (`SIGTERM`, `SIGKILL`).
|
|
792
812
|
* Copied, because `spawnInGroup` records them at send time into a live array.
|
|
793
813
|
*
|
|
794
|
-
* @param {{timedOut?: boolean, killSignals?: string[], survivors?: number[]}|null|undefined} run
|
|
795
|
-
* @returns {{timed_out: boolean, survivors: number, kill_signals: string[]}}
|
|
814
|
+
* @param {{timedOut?: boolean, killSignals?: string[], survivors?: number[]|null, error?: string}|null|undefined} run
|
|
815
|
+
* @returns {{timed_out: boolean, survivors: number|null, error?: string, kill_signals: string[]}}
|
|
796
816
|
*/
|
|
817
|
+
// Explicit null is an unmeasurable cleanup, never a measured zero.
|
|
797
818
|
export function gateKillFields(run) {
|
|
798
819
|
return {
|
|
799
820
|
timed_out: run?.timedOut === true,
|
|
800
|
-
survivors: Array.isArray(run?.survivors) ? run.survivors.length : 0,
|
|
821
|
+
survivors: run?.survivors === null ? null : (Array.isArray(run?.survivors) ? run.survivors.length : 0),
|
|
822
|
+
...(run?.survivors === null ? { error: run.error, ok: false } : {}),
|
|
801
823
|
kill_signals: Array.isArray(run?.killSignals) ? [...run.killSignals] : [],
|
|
802
824
|
};
|
|
803
825
|
}
|
|
@@ -887,7 +909,7 @@ export function gateKillFields(run) {
|
|
|
887
909
|
* @param {number} attempts
|
|
888
910
|
* @param {string|null} gate
|
|
889
911
|
* @param {{passed: number, failed: number, total: number}|null} [counts]
|
|
890
|
-
* @param {{timedOut?: boolean, killSignals?: string[], survivors?: number[]}|null} [killRun]
|
|
912
|
+
* @param {{timedOut?: boolean, killSignals?: string[], survivors?: number[]|null, error?: string}|null} [killRun]
|
|
891
913
|
* the run that decided the verdict — the last attempt's failing gate; `null`
|
|
892
914
|
* on the passing path, where every gate exited 0 and so none was killed
|
|
893
915
|
* (`spawnInGroup` only starts its kill ladder on timeout or overflow, and
|
|
@@ -1036,6 +1058,8 @@ export async function runQualityGateWithRetry(opts) {
|
|
|
1036
1058
|
timedOut: result.timedOut,
|
|
1037
1059
|
killSignals: result.killSignals,
|
|
1038
1060
|
survivors: result.survivors,
|
|
1061
|
+
...(result.survivors === null ? { error: result.error, ok: false } : {}),
|
|
1062
|
+
...(result.leftoverGroup ? { leftoverGroup: result.leftoverGroup } : {}),
|
|
1039
1063
|
command: cmd,
|
|
1040
1064
|
attempt,
|
|
1041
1065
|
};
|
|
@@ -41,11 +41,13 @@
|
|
|
41
41
|
import { readFileSync, readdirSync } from 'node:fs';
|
|
42
42
|
import { isAbsolute, join } from 'node:path';
|
|
43
43
|
|
|
44
|
+
import { resolveLedgerPath } from '../ledger-path.mjs';
|
|
44
45
|
import { learningKeyOf } from '../learnings/kebab.mjs';
|
|
45
46
|
import { migrateLegacyLearning, normalizeLearning } from '../learnings/schema.mjs';
|
|
46
47
|
import { isRuleExpired, parseGlobsFrontmatter } from '../rule-loader.mjs';
|
|
47
48
|
import { filterEligible } from './eligibility.mjs';
|
|
48
49
|
import { isProcessed, loadCandidates as realLoadCandidates } from './idempotency.mjs';
|
|
50
|
+
import { loadVerdictLedger } from './verdict-ledger.mjs';
|
|
49
51
|
|
|
50
52
|
/** Default repo-relative location of the learnings corpus. */
|
|
51
53
|
export const DEFAULT_LEARNINGS_PATH = '.orchestrator/metrics/learnings.jsonl';
|
|
@@ -65,7 +67,7 @@ export function defaultLoadLearnings(repoRoot) {
|
|
|
65
67
|
const root = typeof repoRoot === 'string' && repoRoot.length > 0 ? repoRoot : process.cwd();
|
|
66
68
|
const absPath = isAbsolute(DEFAULT_LEARNINGS_PATH)
|
|
67
69
|
? DEFAULT_LEARNINGS_PATH
|
|
68
|
-
:
|
|
70
|
+
: resolveLedgerPath(DEFAULT_LEARNINGS_PATH, root);
|
|
69
71
|
|
|
70
72
|
let raw;
|
|
71
73
|
try {
|
|
@@ -198,12 +200,15 @@ const BODY_LEARNING_ID_RE = /-\s*learning-id:\s*`([^`]+)`/g;
|
|
|
198
200
|
* unparseable `expires-at` is NOT expired; fail-closed would un-materialize the
|
|
199
201
|
* whole corpus on one typo).
|
|
200
202
|
*
|
|
201
|
-
* KNOWN LIMIT (measured 2026-09-18): this covers only the on-disk
|
|
202
|
-
* {@link partitionMaterialized} (`sidecarTerminal || onDisk`).
|
|
203
|
-
* repo's 92 provenance keys are ALSO terminal in the idempotency
|
|
204
|
-
* only 13 actually become re-proposable; the sidecar
|
|
205
|
-
*
|
|
206
|
-
*
|
|
203
|
+
* KNOWN LIMIT (measured 2026-09-18): this covers only the on-disk third of
|
|
204
|
+
* {@link partitionMaterialized} (`sidecarTerminal || ledgerTerminal || onDisk`).
|
|
205
|
+
* 79 of this repo's 92 provenance keys are ALSO terminal in the idempotency
|
|
206
|
+
* sidecar, so only 13 actually become re-proposable; the sidecar and the
|
|
207
|
+
* verdict ledger (#1710) are left untouched on purpose because they carry
|
|
208
|
+
* operator DECLINES — the ledger holds DECISIONS only (`rejected`,
|
|
209
|
+
* `expired-swept`; never the branch-scoped `written` / `already-on-disk`), so
|
|
210
|
+
* an expiring rule's own `written` verdict never pins it there. See
|
|
211
|
+
* `docs/rule-authoring.md` § Consolidated rules.
|
|
207
212
|
*
|
|
208
213
|
* Gated the same way as {@link defaultLoadCandidatesForDedupe}: an absent
|
|
209
214
|
* `repoRoot` yields empty sets rather than falling back to `process.cwd()`.
|
|
@@ -267,20 +272,22 @@ export function defaultReadMaterializedProvenance(repoRoot) {
|
|
|
267
272
|
* Split rule-eligible learnings into the ones still awaiting a rule and the ones
|
|
268
273
|
* already materialized (#484/#1242 dedupe contract). A learning is materialized
|
|
269
274
|
* when the idempotency sidecar carries a terminal verdict for its `learning_key`
|
|
270
|
-
* (CACHE) OR
|
|
271
|
-
*
|
|
275
|
+
* (CACHE) OR the append-only verdict ledger does (`./verdict-ledger.mjs`, #1710 —
|
|
276
|
+
* survives worktrees and hosts the sidecar does not) OR a `.claude/rules/*.md`
|
|
277
|
+
* provenance marker names its key/id (AUTHORITATIVE).
|
|
272
278
|
*
|
|
273
279
|
* Pure: reads nothing, writes nothing, emits nothing — the engine and the
|
|
274
280
|
* session-start reconcile nudge share it so both judge the same backlog (#1380).
|
|
275
281
|
*
|
|
276
282
|
* @param {Array<Record<string, unknown>>} eligible - output of `filterEligible().eligible`.
|
|
277
|
-
* @param {{ existingCandidates: import('./idempotency.mjs').ReconcileCandidate[], materialized: { keys: Set<string>, ids: Set<string> } }} sources
|
|
283
|
+
* @param {{ existingCandidates: import('./idempotency.mjs').ReconcileCandidate[], materialized: { keys: Set<string>, ids: Set<string> }, ledgerKeys?: { has(key: string): boolean } }} sources
|
|
284
|
+
* `ledgerKeys` — terminal learning keys of the verdict ledger; absent ⇒ none.
|
|
278
285
|
* @returns {{
|
|
279
286
|
* stillEligible: Array<Record<string, unknown>>,
|
|
280
|
-
* materializedItems: Array<{ learning: Record<string, unknown>, learningKey: string|null, sidecarTerminal: boolean, onDisk: boolean }>,
|
|
287
|
+
* materializedItems: Array<{ learning: Record<string, unknown>, learningKey: string|null, sidecarTerminal: boolean, ledgerTerminal: boolean, onDisk: boolean }>,
|
|
281
288
|
* }}
|
|
282
289
|
*/
|
|
283
|
-
export function partitionMaterialized(eligible, { existingCandidates, materialized }) {
|
|
290
|
+
export function partitionMaterialized(eligible, { existingCandidates, materialized, ledgerKeys }) {
|
|
284
291
|
/** @type {Array<Record<string, unknown>>} */
|
|
285
292
|
const stillEligible = [];
|
|
286
293
|
const materializedItems = [];
|
|
@@ -296,14 +303,16 @@ export function partitionMaterialized(eligible, { existingCandidates, materializ
|
|
|
296
303
|
|
|
297
304
|
const sidecarTerminal =
|
|
298
305
|
learningKey !== null && isProcessed({ learning_key: learningKey }, existingCandidates);
|
|
306
|
+
const ledgerTerminal =
|
|
307
|
+
learningKey !== null && !!ledgerKeys && typeof ledgerKeys.has === 'function' && ledgerKeys.has(learningKey);
|
|
299
308
|
const onDisk =
|
|
300
309
|
(learningKey !== null && materialized.keys.has(learningKey)) ||
|
|
301
310
|
(learningId !== null && materialized.ids.has(learningId));
|
|
302
311
|
|
|
303
|
-
if (!sidecarTerminal && !onDisk) {
|
|
312
|
+
if (!sidecarTerminal && !ledgerTerminal && !onDisk) {
|
|
304
313
|
stillEligible.push(learning);
|
|
305
314
|
} else {
|
|
306
|
-
materializedItems.push({ learning, learningKey, sidecarTerminal, onDisk });
|
|
315
|
+
materializedItems.push({ learning, learningKey, sidecarTerminal, ledgerTerminal, onDisk });
|
|
307
316
|
}
|
|
308
317
|
}
|
|
309
318
|
return { stillEligible, materializedItems };
|
|
@@ -312,7 +321,8 @@ export function partitionMaterialized(eligible, { existingCandidates, materializ
|
|
|
312
321
|
/**
|
|
313
322
|
* Count the reconcile BACKLOG the way a real `/reconcile` run would see it:
|
|
314
323
|
* rule-eligible learnings (expiry-gated by `now`) minus those already
|
|
315
|
-
* materialized in the sidecar
|
|
324
|
+
* materialized in the sidecar, the verdict ledger (#1710) or under
|
|
325
|
+
* `.claude/rules/`. This is the quantity
|
|
316
326
|
* `/reconcile` can actually drive to zero — the session-start nudge judges on it
|
|
317
327
|
* (#1380; HR-101: a probe that counts already-materialized learnings can never
|
|
318
328
|
* go green).
|
|
@@ -332,6 +342,8 @@ export function partitionMaterialized(eligible, { existingCandidates, materializ
|
|
|
332
342
|
* already-loaded sidecar records (DI seam for a caller that read the store
|
|
333
343
|
* itself — the session-start nudge does, for the last-run timestamp). Absent
|
|
334
344
|
* ⇒ this function reads the store via {@link defaultLoadCandidatesForDedupe}.
|
|
345
|
+
* @param {{ has(key: string): boolean }} [params.ledgerKeys] - terminal keys of the verdict
|
|
346
|
+
* ledger (DI seam). Absent ⇒ read via `loadVerdictLedger(repoRoot)` (repoRoot-gated).
|
|
335
347
|
* @param {Date|number} [params.now] - clock for the expiry gate (defaults to `Date.now()`).
|
|
336
348
|
* @param {number} [params.minInsightChars] - forwarded to `filterEligible`.
|
|
337
349
|
* @returns {{ eligible: number, alreadyMaterialized: number, backlog: number }}
|
|
@@ -340,6 +352,7 @@ export function countReconcileBacklog({
|
|
|
340
352
|
repoRoot,
|
|
341
353
|
learnings,
|
|
342
354
|
existingCandidates,
|
|
355
|
+
ledgerKeys,
|
|
343
356
|
now,
|
|
344
357
|
minInsightChars,
|
|
345
358
|
} = {}) {
|
|
@@ -356,6 +369,7 @@ export function countReconcileBacklog({
|
|
|
356
369
|
? existingCandidates
|
|
357
370
|
: defaultLoadCandidatesForDedupe(repoRoot).records,
|
|
358
371
|
materialized: defaultReadMaterializedProvenance(repoRoot),
|
|
372
|
+
ledgerKeys: ledgerKeys ?? loadVerdictLedger(repoRoot).terminalKeys,
|
|
359
373
|
});
|
|
360
374
|
return {
|
|
361
375
|
eligible: eligible.length,
|
|
@@ -125,6 +125,7 @@ import {
|
|
|
125
125
|
makeCandidateId,
|
|
126
126
|
mergeCandidates as realMergeCandidates,
|
|
127
127
|
} from './idempotency.mjs';
|
|
128
|
+
import { appendRun, loadVerdictLedger } from './verdict-ledger.mjs';
|
|
128
129
|
|
|
129
130
|
/**
|
|
130
131
|
* Re-exported from the LEAF module `./backlog.mjs` (which now owns the
|
|
@@ -354,6 +355,10 @@ export function resolveEffectiveTargets({ targets, baselineRoot } = {}) {
|
|
|
354
355
|
* @param {(repoRoot?: string) => { keys: Set<string>, ids: Set<string> }} [opts.readMaterializedProvenance]
|
|
355
356
|
* override the `.claude/rules/` provenance scan used by the same dedupe step —
|
|
356
357
|
* defaults to {@link defaultReadMaterializedProvenance} (same repoRoot gate).
|
|
358
|
+
* @param {(repoRoot?: string) => { terminalKeys: { has(key: string): boolean } }} [opts.loadVerdictLedger]
|
|
359
|
+
* override the append-only verdict-ledger read (#1710) used by the same dedupe step —
|
|
360
|
+
* defaults to `loadVerdictLedger` (same repoRoot gate). After a successful non-dryRun
|
|
361
|
+
* merge the engine appends one run record to that ledger (explicit repoRoot only).
|
|
357
362
|
* @param {boolean} [opts.dryRun] - when true, compute proposals but SKIP the merge entirely.
|
|
358
363
|
* @returns {Promise<ReconcileResult>}
|
|
359
364
|
*/
|
|
@@ -381,6 +386,8 @@ async function runReconcileInner(
|
|
|
381
386
|
typeof opts.readMaterializedProvenance === 'function'
|
|
382
387
|
? opts.readMaterializedProvenance
|
|
383
388
|
: defaultReadMaterializedProvenance;
|
|
389
|
+
const readVerdictLedger =
|
|
390
|
+
typeof opts.loadVerdictLedger === 'function' ? opts.loadVerdictLedger : loadVerdictLedger;
|
|
384
391
|
// Volume brake (#900 D) — always active; a missing/invalid override falls
|
|
385
392
|
// back to the same default the Session Config parser uses.
|
|
386
393
|
const maxProposalsPerRun =
|
|
@@ -453,18 +460,24 @@ async function runReconcileInner(
|
|
|
453
460
|
// shape" test in `tests/lib/reconcile/engine.test.mjs`.
|
|
454
461
|
const { records: existingCandidates } = loadCandidatesForDedupe(repoRoot) ?? { records: [] };
|
|
455
462
|
const materialized = readMaterializedProvenance(repoRoot) ?? { keys: new Set(), ids: new Set() };
|
|
463
|
+
// #1710 — the append-only verdict ledger carries terminal verdicts into a
|
|
464
|
+
// fresh worktree / another host, where the checkout-local sidecar is empty.
|
|
465
|
+
const ledgerKeys = readVerdictLedger(repoRoot)?.terminalKeys;
|
|
456
466
|
|
|
457
467
|
const { stillEligible, materializedItems } = partitionMaterialized(eligible, {
|
|
458
468
|
existingCandidates,
|
|
459
469
|
materialized,
|
|
470
|
+
ledgerKeys,
|
|
460
471
|
});
|
|
461
472
|
const alreadyMaterialized = materializedItems.length;
|
|
462
473
|
|
|
463
|
-
for (const { learning, learningKey, sidecarTerminal, onDisk } of materializedItems) {
|
|
474
|
+
for (const { learning, learningKey, sidecarTerminal, ledgerTerminal, onDisk } of materializedItems) {
|
|
464
475
|
const type = learningType(learning);
|
|
465
476
|
const reason = sidecarTerminal
|
|
466
477
|
? 'already processed — the idempotency sidecar already carries a terminal verdict for this learning-key'
|
|
467
|
-
:
|
|
478
|
+
: ledgerTerminal
|
|
479
|
+
? 'already processed — the reconcile verdict ledger already carries a terminal verdict for this learning-key'
|
|
480
|
+
: 'already materialized — a .claude/rules/ file already carries this learning-key/learning-id';
|
|
468
481
|
rejected.push({ learningKey, type, reason, status: 'rejected' });
|
|
469
482
|
|
|
470
483
|
// Freshly discovered on-disk materialization (not yet reflected in the
|
|
@@ -632,6 +645,17 @@ async function runReconcileInner(
|
|
|
632
645
|
if (mergeResult && Number.isFinite(mergeResult.skipped)) {
|
|
633
646
|
skipped = Number(mergeResult.skipped);
|
|
634
647
|
}
|
|
648
|
+
// #1710 — record the run itself, so run recency survives a checkout
|
|
649
|
+
// without the sidecar, with the learnings count it saw (the nudge's
|
|
650
|
+
// "new since last run" baseline). Explicit repoRoot only (never cwd).
|
|
651
|
+
if (written && typeof repoRoot === 'string' && repoRoot.length > 0) {
|
|
652
|
+
const run = appendRun(repoRoot, createdAt, totalLearnings);
|
|
653
|
+
if (run.ok !== true) {
|
|
654
|
+
process.stderr.write(
|
|
655
|
+
`⚠ reconcile: run record not appended to the verdict ledger (${run.error ?? 'unknown error'})\n`,
|
|
656
|
+
);
|
|
657
|
+
}
|
|
658
|
+
}
|
|
635
659
|
} catch {
|
|
636
660
|
// Merge failure is non-fatal; proposals still returned, written stays
|
|
637
661
|
// false and `skipped` stays absent (the store was never inspected).
|
|
@@ -61,6 +61,7 @@ import { readFileSync } from 'node:fs';
|
|
|
61
61
|
import { isAbsolute, join } from 'node:path';
|
|
62
62
|
import { createHash } from 'node:crypto';
|
|
63
63
|
import { atomicWriteWithBackup } from '../io.mjs';
|
|
64
|
+
import { appendVerdicts, isDecisionOutcome, loadVerdictLedger } from './verdict-ledger.mjs';
|
|
64
65
|
|
|
65
66
|
/** Default repo-relative location of the reconcile-candidate work-queue. */
|
|
66
67
|
export const DEFAULT_STORE_PATH = '.orchestrator/runtime/reconcile-candidates.jsonl';
|
|
@@ -317,11 +318,22 @@ export function isProcessed(candidate, existing) {
|
|
|
317
318
|
* (malformed JSON, or a record missing `learning_key`/`created_at`). Because the
|
|
318
319
|
* store is rewritten in full, those lines are DROPPED from disk by this call —
|
|
319
320
|
* the count is what makes that loss attributable instead of silent.
|
|
321
|
+
*
|
|
322
|
+
* Verdict ledger (#1710): after a successful store write, and ONLY when
|
|
323
|
+
* `repoRoot` was passed explicitly, every terminal DECISION of the merged store
|
|
324
|
+
* (`isDecisionOutcome` — not the branch-scoped `written` / `already-on-disk` /
|
|
325
|
+
* absent outcome, see `BRANCH_SCOPED_OUTCOMES`) whose `learning_key` the
|
|
326
|
+
* append-only ledger (`./verdict-ledger.mjs`) does not yet hold is appended
|
|
327
|
+
* there — new stamps AND older sidecar verdicts (a one-time backfill). The
|
|
328
|
+
* ledger outlives the gitignored, checkout-local sidecar, so a fresh worktree
|
|
329
|
+
* keeps an operator decline. A ledger failure never turns a successful store
|
|
330
|
+
* write into a failure; it is reported in the additive `ledger` field (absent
|
|
331
|
+
* when the ledger was not consulted) and as ONE stderr WARN.
|
|
320
332
|
* @param {Object} [params]
|
|
321
333
|
* @param {ReconcileCandidate[]} [params.candidates] - newly minted candidates to merge.
|
|
322
334
|
* @param {string} [params.repoRoot]
|
|
323
335
|
* @param {string} [params.storePath]
|
|
324
|
-
* @returns {{ merged: ReconcileCandidate[], written: boolean, skipped: number }}
|
|
336
|
+
* @returns {{ merged: ReconcileCandidate[], written: boolean, skipped: number, ledger?: { ok: boolean, appended: number, error?: string } }}
|
|
325
337
|
*/
|
|
326
338
|
export function mergeCandidates({ candidates, repoRoot, storePath } = {}) {
|
|
327
339
|
const absPath = resolveStorePath(repoRoot, storePath);
|
|
@@ -353,7 +365,28 @@ export function mergeCandidates({ candidates, repoRoot, storePath } = {}) {
|
|
|
353
365
|
);
|
|
354
366
|
|
|
355
367
|
const result = writeStore(absPath, merged);
|
|
356
|
-
|
|
368
|
+
const written = result.ok === true;
|
|
369
|
+
if (!written || typeof repoRoot !== 'string' || repoRoot.length === 0) {
|
|
370
|
+
return { merged, written, skipped };
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
const ledger = loadVerdictLedger(repoRoot);
|
|
374
|
+
if (ledger.readError) {
|
|
375
|
+
// Unreadable ledger: appending blind would duplicate every verdict.
|
|
376
|
+
process.stderr.write('⚠ mergeCandidates: verdict ledger unreadable — no verdicts mirrored this merge\n');
|
|
377
|
+
return { merged, written, skipped, ledger: { ok: false, appended: 0, error: 'ledger unreadable' } };
|
|
378
|
+
}
|
|
379
|
+
const missing = merged.filter(
|
|
380
|
+
(rec) => isTerminal(rec) && isDecisionOutcome(rec.outcome) && !ledger.terminalKeys.has(rec.learning_key),
|
|
381
|
+
);
|
|
382
|
+
const appended = appendVerdicts(repoRoot, missing);
|
|
383
|
+
if (appended.ok !== true) {
|
|
384
|
+
process.stderr.write(
|
|
385
|
+
`⚠ mergeCandidates: verdict ledger append failed (${appended.error ?? 'unknown error'}) — ` +
|
|
386
|
+
`${missing.length} verdict(s) not mirrored\n`,
|
|
387
|
+
);
|
|
388
|
+
}
|
|
389
|
+
return { merged, written, skipped, ledger: appended };
|
|
357
390
|
}
|
|
358
391
|
|
|
359
392
|
/**
|
|
@@ -376,7 +409,10 @@ export function mergeCandidates({ candidates, repoRoot, storePath } = {}) {
|
|
|
376
409
|
* `mergeCandidates`'s own dedupe rule still applies underneath this call: if
|
|
377
410
|
* the existing record is ALREADY terminal (a prior `processed_at`), the merge
|
|
378
411
|
* keeps the existing verdict and this call is a no-op on disk — a terminal
|
|
379
|
-
* verdict is never regressed, even by a second stamp attempt.
|
|
412
|
+
* verdict is never regressed, even by a second stamp attempt. One exception
|
|
413
|
+
* (#1710): a DECISION (`isDecisionOutcome`) stamped over a kept `written` /
|
|
414
|
+
* `already-on-disk` record is still appended to the verdict ledger (explicit
|
|
415
|
+
* `repoRoot` only), so a dropped or swept rule stays dropped in other checkouts.
|
|
380
416
|
*
|
|
381
417
|
* Never throws (delegates to the never-throwing `loadCandidates`/
|
|
382
418
|
* `mergeCandidates`); on write failure returns `{ written: false, ... }`.
|
|
@@ -460,6 +496,32 @@ export function markCandidateProcessed({
|
|
|
460
496
|
// the caller a verdict that is nowhere on disk. `merged` IS the array that was
|
|
461
497
|
// just written, so reading the record back out of it is the honest answer.
|
|
462
498
|
const persisted = result.merged.find((rec) => rec && rec.learning_key === learningKey);
|
|
499
|
+
|
|
500
|
+
// A DECISION stamped over a kept branch-scoped terminal record (`written` /
|
|
501
|
+
// `already-on-disk`) never reaches the ledger through `mergeCandidates`: the
|
|
502
|
+
// store keeps the old outcome, and only decisions are mirrored. That is the
|
|
503
|
+
// rule-drop recipe and the expiry sweep's delete stamp — append it here so the
|
|
504
|
+
// drop holds in other checkouts too (#1710).
|
|
505
|
+
if (
|
|
506
|
+
result.written === true &&
|
|
507
|
+
typeof repoRoot === 'string' &&
|
|
508
|
+
repoRoot.length > 0 &&
|
|
509
|
+
isDecisionOutcome(outcome) &&
|
|
510
|
+
persisted &&
|
|
511
|
+
!isDecisionOutcome(persisted.outcome)
|
|
512
|
+
) {
|
|
513
|
+
const ledger = loadVerdictLedger(repoRoot);
|
|
514
|
+
if (!ledger.readError && !ledger.terminalKeys.has(learningKey)) {
|
|
515
|
+
const appended = appendVerdicts(repoRoot, [{ learning_key: learningKey, outcome, processed_at: stampAt }]);
|
|
516
|
+
if (appended.ok !== true) {
|
|
517
|
+
process.stderr.write(
|
|
518
|
+
`⚠ markCandidateProcessed: verdict ledger append failed (${appended.error ?? 'unknown error'}) — ` +
|
|
519
|
+
`${outcome} for ${learningKey} not mirrored\n`,
|
|
520
|
+
);
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
|
|
463
525
|
return {
|
|
464
526
|
written: result.written === true,
|
|
465
527
|
stamped: persisted ?? stamped,
|
|
@@ -141,10 +141,13 @@
|
|
|
141
141
|
*/
|
|
142
142
|
|
|
143
143
|
import { lstatSync, readFileSync, unlinkSync } from 'node:fs';
|
|
144
|
+
import { readFile } from 'node:fs/promises';
|
|
144
145
|
import { join, resolve, sep } from 'node:path';
|
|
145
146
|
|
|
146
147
|
import { atomicWriteWithBackup } from '../io.mjs';
|
|
147
|
-
import {
|
|
148
|
+
import { resolveLedgerPath } from '../ledger-path.mjs';
|
|
149
|
+
import { parseLearningsText } from '../learnings/io.mjs';
|
|
150
|
+
import { archivePathFor } from '../learnings/store-role.mjs';
|
|
148
151
|
import { listMachineGeneratedRules } from '../instruction-budget-guard.mjs';
|
|
149
152
|
import { parseGlobsFrontmatter, ruleExpiryInstantMs } from '../rule-loader.mjs';
|
|
150
153
|
import { markCandidateProcessed } from './idempotency.mjs';
|
|
@@ -371,26 +374,57 @@ export function parseConsolidatedRule(content) {
|
|
|
371
374
|
}
|
|
372
375
|
|
|
373
376
|
/**
|
|
374
|
-
* Read `learnings.jsonl` into `id -> expires_at`,
|
|
377
|
+
* Read the archive and live `learnings.jsonl` into `id -> expires_at`,
|
|
378
|
+
* counting malformed lines separately for each source.
|
|
375
379
|
*
|
|
376
380
|
* Malformed lines are COUNTED, never silently dropped: a leniently-skipping
|
|
377
381
|
* JSONL parser turns a partial read into a clean verdict, and this sweep
|
|
378
382
|
* DELETES prose on the strength of that verdict.
|
|
379
383
|
*
|
|
384
|
+
* Boundary (BV-004, #1685): the index reads the live store and archive, never the
|
|
385
|
+
* sibling `learnings-inbox.jsonl` (#1516). An inbox-only id resolves as absent:
|
|
386
|
+
* consolidated pairs stay unresolved/kept, a single-entry file falls back to
|
|
387
|
+
* its own `expires-at`. Including the inbox would not be purely conservative —
|
|
388
|
+
* a resolvable past `expires_at` would let `rewriteContent` remove a pair that
|
|
389
|
+
* is kept today. Ceiling: an inbox record's later `expires_at` cannot delay a
|
|
390
|
+
* single-entry delete. Revisit trigger: the first real inbox host, or a
|
|
391
|
+
* generated rule citing an inbox-only learning-id.
|
|
392
|
+
* The archive is admissible because each learning's own `expires_at` carries
|
|
393
|
+
* the expiry evidence; live records override archived records with the same id.
|
|
394
|
+
* Superseded/merged tombstones stay excluded because their content lives in a
|
|
395
|
+
* winner; revisit pointer resolution if a rule section stays permanently
|
|
396
|
+
* unresolved because it cites only a tombstone.
|
|
397
|
+
*
|
|
380
398
|
* @param {string} absPath
|
|
381
|
-
* @returns {Promise<{expiryById: Map<string, string>, malformedLines: number}>}
|
|
399
|
+
* @returns {Promise<{expiryById: Map<string, string>, malformedLines: number, archiveMalformedLines: number}>}
|
|
382
400
|
*/
|
|
383
401
|
async function loadExpiryIndex(absPath) {
|
|
384
402
|
const expiryById = new Map();
|
|
385
|
-
|
|
386
|
-
|
|
403
|
+
// Unlike the shared tolerant reader, a sweep needs evidence that the store
|
|
404
|
+
// was actually read: an absent store must not become an empty expiry index.
|
|
405
|
+
const { entries, malformed } = parseLearningsText(await readFile(absPath, 'utf8'));
|
|
406
|
+
let archiveText = '';
|
|
407
|
+
try {
|
|
408
|
+
archiveText = await readFile(archivePathFor(absPath), 'utf8');
|
|
409
|
+
} catch (err) {
|
|
410
|
+
if (err?.code !== 'ENOENT') throw err;
|
|
411
|
+
}
|
|
412
|
+
const archive = parseLearningsText(archiveText);
|
|
413
|
+
const archivedLearnings = archive.entries.filter((entry) =>
|
|
414
|
+
!Object.hasOwn(entry, '_superseded_by') &&
|
|
415
|
+
!Object.hasOwn(entry, '_merged_into') &&
|
|
416
|
+
entry._archive_reason !== 'superseded' &&
|
|
417
|
+
entry._archive_reason !== 'merged',
|
|
418
|
+
);
|
|
419
|
+
// Overlay the live store last: a reactivated learning may have a later expiry.
|
|
420
|
+
for (const entry of [...archivedLearnings, ...entries]) {
|
|
387
421
|
const id = entry?.id;
|
|
388
422
|
const expiresAt = entry?.expires_at;
|
|
389
423
|
if (typeof id === 'string' && id.length > 0 && typeof expiresAt === 'string') {
|
|
390
424
|
expiryById.set(id, expiresAt);
|
|
391
425
|
}
|
|
392
426
|
}
|
|
393
|
-
return { expiryById, malformedLines: malformed.length };
|
|
427
|
+
return { expiryById, malformedLines: malformed.length, archiveMalformedLines: archive.malformed.length };
|
|
394
428
|
}
|
|
395
429
|
|
|
396
430
|
/** `now` as epoch-ms, accepting a Date, a number, an ISO string, or nothing. */
|
|
@@ -569,7 +603,7 @@ function keepOrRaisePlan(base, parsed, advisory, raiseTo) {
|
|
|
569
603
|
* swept. Default 0: unlike the learnings sweep, nothing re-stamps a rule
|
|
570
604
|
* file's `expires-at`, so there is no re-stamp window to protect.
|
|
571
605
|
* @param {string} [opts.learningsPath] override the learnings store
|
|
572
|
-
* @returns {Promise<{plans: Array<object>, skipped: Array<{file: string, reason: string}>, malformedLines: number, ok: boolean}>}
|
|
606
|
+
* @returns {Promise<{plans: Array<object>, skipped: Array<{file: string, reason: string}>, malformedLines: number, archiveMalformedLines: number, ok: boolean}>}
|
|
573
607
|
*/
|
|
574
608
|
export async function planRuleExpirySweep(opts = {}) {
|
|
575
609
|
const { repoRoot, now, graceDays = 0, learningsPath } = opts;
|
|
@@ -582,11 +616,18 @@ export async function planRuleExpirySweep(opts = {}) {
|
|
|
582
616
|
|
|
583
617
|
const nowMs = toEpochMs(now);
|
|
584
618
|
const cutoffMs = nowMs - graceDays * MS_PER_DAY;
|
|
585
|
-
const { expiryById, malformedLines } = await loadExpiryIndex(
|
|
586
|
-
join(repoRoot, learningsPath ?? DEFAULT_LEARNINGS_PATH),
|
|
587
|
-
);
|
|
588
|
-
|
|
589
619
|
const listed = listMachineGeneratedRules({ repoRoot });
|
|
620
|
+
// Ohne generierte Regeln gibt es nichts zu pruefen: der Bestand wird dann
|
|
621
|
+
// nicht gelesen, damit ein Repo ohne learnings.jsonl nicht mit ENOENT endet
|
|
622
|
+
// (Housekeeping Schritt 3 ruft die CLI repo-unabhaengig auf).
|
|
623
|
+
const { expiryById, malformedLines, archiveMalformedLines } =
|
|
624
|
+
listed.rules.length === 0
|
|
625
|
+
? { expiryById: new Map(), malformedLines: 0, archiveMalformedLines: 0 }
|
|
626
|
+
: await loadExpiryIndex(
|
|
627
|
+
// #1722: the default store (and its archive beside it) follows the
|
|
628
|
+
// ledger root; an explicit learningsPath stays repo-relative as before.
|
|
629
|
+
learningsPath ? join(repoRoot, learningsPath) : resolveLedgerPath(DEFAULT_LEARNINGS_PATH, repoRoot),
|
|
630
|
+
);
|
|
590
631
|
const rulesDir = join(repoRoot, '.claude', 'rules');
|
|
591
632
|
/** @type {Array<object>} */
|
|
592
633
|
const plans = [];
|
|
@@ -807,7 +848,7 @@ export async function planRuleExpirySweep(opts = {}) {
|
|
|
807
848
|
});
|
|
808
849
|
}
|
|
809
850
|
|
|
810
|
-
return { plans, skipped, malformedLines, ok: listed.ok };
|
|
851
|
+
return { plans, skipped, malformedLines, archiveMalformedLines, ok: listed.ok };
|
|
811
852
|
}
|
|
812
853
|
|
|
813
854
|
/**
|