session-orchestrator 3.23.0 → 4.0.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 +18 -0
- package/.agents/skills/autopilot/SKILL.md +17 -0
- package/.agents/skills/bootstrap/SKILL.md +20 -0
- package/.agents/skills/brainstorm/SKILL.md +22 -0
- package/.agents/skills/claude-md-drift-check/SKILL.md +15 -0
- package/.agents/skills/convergence-monitoring/SKILL.md +22 -0
- package/.agents/skills/debug/SKILL.md +22 -0
- package/.agents/skills/discovery/SKILL.md +20 -0
- package/.agents/skills/dispatcher/SKILL.md +15 -0
- package/.agents/skills/docs-orchestrator/SKILL.md +18 -0
- package/.agents/skills/ecosystem-health/SKILL.md +20 -0
- package/.agents/skills/eli5/SKILL.md +20 -0
- package/.agents/skills/eval/SKILL.md +21 -0
- package/.agents/skills/evolve/SKILL.md +21 -0
- package/.agents/skills/frontmatter-guard/SKILL.md +15 -0
- package/.agents/skills/gitlab-ops/SKILL.md +20 -0
- package/.agents/skills/gitlab-portfolio/SKILL.md +15 -0
- package/.agents/skills/grill/SKILL.md +22 -0
- package/.agents/skills/hook-development/SKILL.md +15 -0
- package/.agents/skills/mcp-builder/SKILL.md +15 -0
- package/.agents/skills/memory-cleanup/SKILL.md +21 -0
- package/.agents/skills/mode-selector/SKILL.md +17 -0
- package/.agents/skills/npm-publish/SKILL.md +16 -0
- package/.agents/skills/peekaboo-driver/SKILL.md +18 -0
- package/.agents/skills/persona-panel/SKILL.md +17 -0
- package/.agents/skills/plan/SKILL.md +20 -0
- package/.agents/skills/playwright-driver/SKILL.md +20 -0
- package/.agents/skills/quality-gates/SKILL.md +20 -0
- package/.agents/skills/reconcile/SKILL.md +21 -0
- package/.agents/skills/remote-offload/SKILL.md +20 -0
- package/.agents/skills/repo-audit/SKILL.md +16 -0
- package/.agents/skills/session-end/SKILL.md +20 -0
- package/.agents/skills/session-plan/SKILL.md +20 -0
- package/.agents/skills/session-start/SKILL.md +20 -0
- package/.agents/skills/spinout/SKILL.md +16 -0
- package/.agents/skills/sunset-review/SKILL.md +16 -0
- package/.agents/skills/test-runner/SKILL.md +20 -0
- package/.agents/skills/tmux-layout/SKILL.md +21 -0
- package/.agents/skills/using-orchestrator/SKILL.md +17 -0
- package/.agents/skills/vault-mirror/SKILL.md +15 -0
- package/.agents/skills/vault-sync/SKILL.md +15 -0
- package/.agents/skills/wave-executor/SKILL.md +20 -0
- package/.agents/skills/write-executable-plan/SKILL.md +22 -0
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/.cursor/commands/autopilot.md +2 -2
- package/.cursor/commands/bootstrap.md +1 -1
- package/.cursor/commands/brainstorm.md +1 -1
- package/.cursor/commands/debug.md +1 -1
- package/.cursor/commands/discovery.md +1 -1
- package/.cursor/commands/dispatcher.md +2 -2
- package/.cursor/commands/eli5.md +2 -2
- package/.cursor/commands/eval.md +2 -2
- package/.cursor/commands/evolve.md +1 -1
- package/.cursor/commands/go.md +1 -1
- package/.cursor/commands/grill.md +2 -2
- package/.cursor/commands/memory-cleanup.md +2 -2
- package/.cursor/commands/persona-panel.md +1 -1
- package/.cursor/commands/plan.md +1 -1
- package/.cursor/commands/portfolio.md +1 -1
- package/.cursor/commands/reconcile.md +2 -2
- package/.cursor/commands/release.md +2 -2
- package/.cursor/commands/session.md +2 -2
- package/.cursor/commands/spinout.md +2 -2
- package/.cursor/commands/sunset-review.md +2 -2
- package/.cursor/commands/templates-ack.md +2 -2
- package/.cursor/commands/test.md +2 -2
- package/.cursor/skills/brainstorm/SKILL.md +1 -1
- package/.cursor/skills/eval/SKILL.md +1 -1
- package/.cursor/skills/quality-gates/SKILL.md +1 -1
- package/.cursor/skills/remote-offload/SKILL.md +13 -0
- package/.orchestrator/policy/blocked-commands.json +121 -0
- package/.orchestrator/policy/ecosystem.schema.json +66 -0
- package/.orchestrator/policy/quality-gates.example.json +16 -0
- package/.orchestrator/policy/quality-gates.schema.json +38 -0
- package/.orchestrator/policy/templates-policy.json +27 -0
- package/.orchestrator/policy/test-profiles.json +47 -0
- package/AGENTS.md +225 -0
- package/CHANGELOG.md +1401 -0
- package/NOTICE +11 -6
- package/README.md +127 -92
- package/agents/db-specialist.md +0 -1
- package/agents/eval-judge.md +1 -1
- package/agents/skill-applied-judge.md +1 -1
- package/assets/wave-lifecycle.svg +98 -0
- package/commands/release.md +6 -3
- package/commands/session.md +18 -3
- package/docs/README.md +4 -0
- package/{agents/AGENTS.md → docs/agent-authoring.md} +19 -26
- package/docs/baseline.md +67 -0
- package/docs/ci-setup.md +249 -48
- package/docs/codex-setup.md +66 -22
- package/docs/components.md +37 -16
- package/docs/cursor-setup.md +6 -2
- package/docs/events-schema.md +51 -10
- package/docs/instruction-delivery.md +62 -0
- package/{agents/memory-proposal-collector.md → docs/memory-proposal-flow.md} +1 -8
- package/docs/migration-v4.md +341 -0
- package/docs/pi-setup.md +6 -1
- package/docs/plugin-architecture-v3.md +1 -1
- package/docs/rule-authoring.md +85 -19
- package/docs/scope-collision-guard.md +8 -8
- package/docs/session-config-reference.md +120 -61
- package/docs/session-config-template.md +40 -33
- package/docs/telemetry/telemetry-claims.md +11 -10
- package/docs/telemetry.md +187 -4
- package/docs/vault-docs-architecture.md +50 -11
- package/hooks/_lib/atomic-json.mjs +111 -0
- package/hooks/_lib/hook-import-set.json +1487 -0
- package/hooks/_lib/subagent-paths.mjs +143 -0
- package/hooks/_lib/subagent-transcript.mjs +562 -0
- package/hooks/config-protection.mjs +2 -2
- package/hooks/cwd-change-restore.mjs +11 -31
- package/hooks/enforce-commands.mjs +69 -0
- package/hooks/enforce-scope.mjs +35 -6
- package/hooks/hooks-codex.json +1 -1
- package/hooks/hooks-cursor.json +10 -0
- package/hooks/hooks-pi.json +5 -0
- package/hooks/hooks.json +6 -1
- package/hooks/loop-guard.mjs +3 -3
- package/hooks/on-session-end.mjs +280 -14
- package/hooks/on-session-start.mjs +153 -4
- package/hooks/on-stop.mjs +371 -17
- package/hooks/operator-steer.mjs +2 -2
- package/hooks/post-bash-write-verify.mjs +189 -4
- package/hooks/post-edit-import-probe.mjs +344 -0
- package/hooks/post-subagent-discovery-validator.mjs +278 -392
- package/hooks/post-tool-batch-wave-signal.mjs +272 -44
- package/hooks/post-tool-failure-corrective-context.mjs +11 -34
- package/hooks/post-tooluse-frontend-slop.mjs +3 -3
- package/hooks/pre-bash-destructive-guard.mjs +39 -13
- package/hooks/pre-bash-memory-propose-audit.mjs +13 -7
- package/hooks/skill-invocation-telemetry.mjs +17 -5
- package/hooks/subagent-telemetry.mjs +24 -30
- package/monitors/monitors.json +3 -3
- package/package.json +9 -1
- package/pi/prompts/session.md +2 -2
- package/plugin.json +27 -0
- package/scripts/autopilot.mjs +26 -12
- package/scripts/backfill-abandoned-sessions.mjs +130 -15
- package/scripts/backfill-learnings-from-vault.mjs +9 -3
- package/scripts/dialectic-deriver.mjs +73 -8
- package/scripts/emit-event.mjs +10 -2
- package/scripts/export-hw-learnings.mjs +113 -1
- package/scripts/generate-agents-skills.mjs +378 -0
- package/scripts/generate-cursor-adapter.mjs +45 -8
- package/scripts/generate-hook-import-set.mjs +249 -0
- package/scripts/lib/agent-status.mjs +13 -2
- package/scripts/lib/auq/parse.mjs +5 -29
- package/scripts/lib/auto-dialectic.mjs +68 -0
- package/scripts/lib/auto-dream.mjs +38 -36
- package/scripts/lib/autonomy/suitability.mjs +6 -0
- package/scripts/lib/autopilot/loop.mjs +2 -2
- package/scripts/lib/autopilot/worktree-pipeline.mjs +82 -6
- package/scripts/lib/build-live-signals.mjs +25 -22
- package/scripts/lib/ci-status-banner.mjs +220 -75
- package/scripts/lib/codex/plugin-contract.mjs +82 -6
- package/scripts/lib/cold-start-detector.mjs +23 -14
- package/scripts/lib/config/auto-dream.mjs +2 -1
- package/scripts/lib/config/block-header.mjs +63 -0
- package/scripts/lib/config/block-preprocess.mjs +177 -0
- package/scripts/lib/config/broken-window.mjs +2 -1
- package/scripts/lib/config/cold-start.mjs +2 -1
- package/scripts/lib/config/config-protection.mjs +22 -2
- package/scripts/lib/config/context-coverage.mjs +2 -1
- package/scripts/lib/config/cross-repo.mjs +2 -1
- package/scripts/lib/config/custom-phases.mjs +2 -1
- package/scripts/lib/config/dialectic.mjs +2 -1
- package/scripts/lib/config/discovery-validator.mjs +9 -3
- package/scripts/lib/config/dispatcher-autonomy-capture.mjs +24 -1
- package/scripts/lib/config/dispatcher-autonomy.mjs +2 -1
- package/scripts/lib/config/docs-orchestrator.mjs +2 -1
- package/scripts/lib/config/docs-staleness.mjs +2 -1
- package/scripts/lib/config/drift-check.mjs +2 -1
- package/scripts/lib/config/eval.mjs +2 -1
- package/scripts/lib/config/events-rotation.mjs +2 -1
- package/scripts/lib/config/evolve.mjs +8 -2
- package/scripts/lib/config/frontend-slop-hook.mjs +7 -3
- package/scripts/lib/config/gitlab-portfolio.mjs +2 -1
- package/scripts/lib/config/handover-gate.mjs +2 -1
- package/scripts/lib/config/health-endpoints.mjs +388 -0
- package/scripts/lib/config/issue-budget.mjs +2 -1
- package/scripts/lib/config/loop-guard.mjs +2 -1
- package/scripts/lib/config/memory.mjs +2 -1
- package/scripts/lib/config/moc-staleness.mjs +2 -1
- package/scripts/lib/config/persona-gate-wave.mjs +2 -1
- package/scripts/lib/config/private-config-dir.mjs +67 -0
- package/scripts/lib/config/reconcile.mjs +2 -1
- package/scripts/lib/config/remote-hosts.mjs +234 -0
- package/scripts/lib/config/section-extractor.mjs +7 -1
- package/scripts/lib/config/skill-evolution.mjs +2 -1
- package/scripts/lib/config/slopcheck.mjs +2 -1
- package/scripts/lib/config/state-md-lock.mjs +2 -1
- package/scripts/lib/config/templates-first.mjs +2 -1
- package/scripts/lib/config/test.mjs +2 -1
- package/scripts/lib/config/vault-integration.mjs +7 -1
- package/scripts/lib/config/vault-mirror-quality.mjs +2 -1
- package/scripts/lib/config/vault-staleness.mjs +2 -1
- package/scripts/lib/config/vault-sync.mjs +2 -1
- package/scripts/lib/config/verification-auto-fix.mjs +2 -1
- package/scripts/lib/config/wave-reviewers.mjs +2 -1
- package/scripts/lib/config/worktree-orphans.mjs +2 -1
- package/scripts/lib/config.mjs +31 -3
- package/scripts/lib/convergence-monitor.mjs +82 -16
- package/scripts/lib/dispatcher/enumerate.mjs +2 -17
- package/scripts/lib/dispatcher/rank.mjs +124 -48
- package/scripts/lib/ecosystem-health.mjs +16 -2
- package/scripts/lib/eval/engine.mjs +9 -1
- package/scripts/lib/eval/session-resolve.mjs +23 -4
- package/scripts/lib/events-schema.mjs +48 -0
- package/scripts/lib/events.mjs +256 -7
- package/scripts/lib/evolve/autonomy-verdict.mjs +9 -4
- package/scripts/lib/evolve/autopilot-effectiveness.mjs +18 -1
- package/scripts/lib/frontmatter-guard.mjs +131 -13
- package/scripts/lib/gates/gate-full.mjs +26 -0
- package/scripts/lib/gates/gate-helpers.mjs +76 -0
- package/scripts/lib/gitlab-portfolio/cli.mjs +3 -15
- package/scripts/lib/hardware-pattern-detector.mjs +18 -1
- package/scripts/lib/harness-audit/categories/category1.mjs +17 -6
- package/scripts/lib/harness-audit/categories/category4.mjs +31 -11
- package/scripts/lib/host-identity.mjs +50 -11
- package/scripts/lib/instruction-budget-guard.mjs +171 -5
- package/scripts/lib/learnings/evolve-telemetry.mjs +178 -0
- package/scripts/lib/learnings/io.mjs +60 -6
- package/scripts/lib/memory-banner.mjs +20 -8
- package/scripts/lib/memory-proposals/store.mjs +30 -22
- package/scripts/lib/owner-config-banner.mjs +43 -6
- package/scripts/lib/owner-config-loader.mjs +21 -10
- package/scripts/lib/owner-interview.mjs +3 -3
- package/scripts/lib/owner-yaml.mjs +207 -14
- package/scripts/lib/peer-discovery.mjs +20 -2
- package/scripts/lib/platform.mjs +108 -15
- package/scripts/lib/plugin-update-banner.mjs +406 -0
- package/scripts/lib/project-hygiene.mjs +38 -2
- package/scripts/lib/qg-command-drift-banner.mjs +50 -12
- package/scripts/lib/quality-gate.mjs +133 -44
- package/scripts/lib/reconcile/emitter.mjs +68 -6
- package/scripts/lib/reconcile/engine.mjs +249 -9
- package/scripts/lib/reconcile/idempotency.mjs +37 -4
- package/scripts/lib/reconcile/writer.mjs +40 -18
- package/scripts/lib/scope-gate.mjs +36 -0
- package/scripts/lib/session-close-backfill.mjs +125 -18
- package/scripts/lib/session-discovery.mjs +57 -3
- package/scripts/lib/session-end/phase-skip.mjs +2 -2
- package/scripts/lib/session-id.mjs +12 -23
- package/scripts/lib/session-identity/own-session.mjs +187 -11
- package/scripts/lib/session-lock-shape.mjs +43 -0
- package/scripts/lib/session-lock.mjs +5 -10
- package/scripts/lib/session-registry.mjs +25 -9
- package/scripts/lib/session-schema/constants.mjs +36 -2
- package/scripts/lib/session-schema/validator.mjs +38 -4
- package/scripts/lib/session-start-probes.mjs +18 -1
- package/scripts/lib/session-transition.mjs +1 -1
- package/scripts/lib/sessions-canonical.mjs +446 -0
- package/scripts/lib/sessions-staleness-banner.mjs +18 -11
- package/scripts/lib/skill-health/join.mjs +17 -4
- package/scripts/lib/state-md.mjs +78 -0
- package/scripts/lib/sunset/walker.mjs +6 -0
- package/scripts/lib/telemetry/schema.mjs +255 -17
- package/scripts/lib/telemetry/sync.mjs +417 -24
- package/scripts/lib/tmux-layout/telemetry.mjs +14 -2
- package/scripts/lib/validate/check-agents-skills.mjs +327 -0
- package/scripts/lib/validate/check-agents.mjs +3 -3
- package/scripts/lib/validate/check-cursor-adapter.mjs +234 -72
- package/scripts/lib/validate/check-doc-cli-commands.mjs +9 -33
- package/scripts/lib/validate/check-hooks-emit-event-guard.mjs +370 -0
- package/scripts/lib/validate/check-hooks-symmetry.mjs +45 -16
- package/scripts/lib/validate/check-owner-leakage.mjs +281 -20
- package/scripts/lib/validate/check-skill-links.mjs +163 -0
- package/scripts/lib/validate/check-skill-script-paths.mjs +455 -0
- package/scripts/lib/validate/check-untracked-test-deps.mjs +10 -0
- package/scripts/lib/validate/check-unwired-features.mjs +0 -9
- package/scripts/lib/validate/check-validator-registration.mjs +254 -0
- package/scripts/lib/validate/check-vcs-repo-flag.mjs +6 -28
- package/scripts/lib/validate/enumerate-repo-files.mjs +317 -0
- package/scripts/lib/validate/markdown-fences.mjs +196 -0
- package/scripts/lib/vault-backfill/template.mjs +63 -6
- package/scripts/lib/vault-mirror/process.mjs +165 -42
- package/scripts/lib/vault-mirror/telemetry.mjs +2 -2
- package/scripts/lib/vault-status/board-lock.mjs +185 -0
- package/scripts/lib/vault-status/board-writer.mjs +174 -135
- package/scripts/lib/vault-status/narrative-mirror.mjs +129 -37
- package/scripts/lib/wave-executor/dispatch-common.mjs +164 -0
- package/scripts/lib/wave-executor/foreign-dispatch.mjs +7 -142
- package/scripts/lib/wave-executor/remote-dispatch.mjs +502 -0
- package/scripts/lib/wave-resource-gate.mjs +133 -7
- package/scripts/lib/wave-sizing.mjs +4 -1
- package/scripts/lib/wave-transcript-tail.mjs +142 -8
- package/scripts/materialize-wave-scope.mjs +32 -9
- package/scripts/memory-propose.mjs +146 -8
- package/scripts/migrate-cold-start-seed.mjs +4 -1
- package/scripts/parse-config.mjs +60 -3
- package/scripts/promote-vault-strict.mjs +4 -15
- package/scripts/release.mjs +337 -29
- package/scripts/repair-invalid-sessions.mjs +3 -3
- package/scripts/run-quality-gate.mjs +128 -11
- package/scripts/site-numbers.mjs +36 -4
- package/scripts/sweep-expired-learnings.mjs +90 -0
- package/scripts/sync-vault-schema.mjs +3 -1
- package/scripts/telemetry.mjs +2 -2
- package/scripts/validate-plugin.mjs +187 -0
- package/scripts/validate-wave-scope.mjs +28 -8
- package/scripts/vault-consolidate.mjs +3 -11
- package/scripts/vault-integration-watcher.mjs +2 -4
- package/scripts/vault-mirror.mjs +111 -26
- package/scripts/wave-scope-binding.mjs +215 -0
- package/skills/_shared/instruction-file-resolution.md +10 -0
- package/skills/_shared/parallel-aware-auq.md +31 -2
- package/skills/_shared/parallel-aware-preamble.md +18 -4
- package/skills/_shared/platform-tools.md +1 -1
- package/skills/_shared/state-ownership.md +1 -1
- package/skills/architecture/SKILL.md +7 -5
- package/skills/{domain-model/SKILL.md → architecture/references/domain-model.md} +9 -9
- package/skills/autopilot/SKILL.md +4 -18
- package/skills/claude-md-drift-check/SKILL.md +5 -1
- package/skills/claude-md-drift-check/checker.mjs +62 -2
- package/skills/convergence-monitoring/SIGNALS.md +55 -0
- package/skills/discovery/probes/vault-staleness.mjs +37 -13
- package/skills/discovery/probes-arch.md +20 -18
- package/skills/dispatcher/SKILL.md +3 -2
- package/skills/ecosystem-health/SKILL.md +4 -1
- package/skills/ecosystem-health/wizard.md +5 -0
- package/skills/evolve/SKILL.md +87 -11
- package/skills/frontmatter-guard/SKILL.md +11 -5
- package/skills/npm-publish/SKILL.md +1 -1
- package/skills/reconcile/SKILL.md +38 -2
- package/skills/remote-offload/SKILL.md +89 -0
- package/skills/session-end/SKILL.md +18 -905
- package/skills/session-end/phase-3-6-tail.md +19 -9
- package/skills/session-end/plan-verification.md +221 -155
- package/skills/session-end/references/phase-2-quality-gate.md +93 -0
- package/skills/session-end/references/phase-3-documentation-updates.md +229 -0
- package/skills/session-end/references/phase-4a-worktree-cleanup.md +120 -0
- package/skills/session-end/references/phase-4b-worktree-orphan-sweep.md +58 -0
- package/skills/session-end/references/phase-5-issue-cleanup.md +104 -0
- package/skills/session-end/references/session-summary-template.md +62 -0
- package/skills/session-plan/SKILL.md +49 -0
- package/skills/session-start/SKILL.md +41 -900
- package/skills/session-start/phase-8-5-express-path.md +1 -1
- package/skills/session-start/references/phase-1-1-dispatcher-autonomy-capture.md +55 -0
- package/skills/session-start/references/phase-1-2-session-lock.md +140 -0
- package/skills/session-start/references/phase-1-5-session-continuity.md +254 -0
- package/skills/session-start/references/phase-1-7-vault-status-board.md +53 -0
- package/skills/session-start/references/phase-2-7-portfolio-snapshot.md +75 -0
- package/skills/session-start/references/phase-4-ssot-environment-check.md +155 -0
- package/skills/session-start/references/phase-6-5-forced-reads.md +75 -0
- package/skills/session-start/references/phase-6-6-project-intelligence.md +81 -0
- package/skills/session-start/references/phase-6-7-memory-banner-telemetry-consent.md +103 -0
- package/skills/vault-sync/validator.mjs +21 -27
- package/skills/wave-executor/SKILL.md +16 -2
- package/skills/wave-executor/references/wave-loop-dispatch.md +612 -0
- package/skills/wave-executor/references/wave-loop-review.md +570 -0
- package/skills/wave-executor/references/wave-loop-scope-manifest.md +162 -0
- package/skills/wave-executor/wave-loop.md +14 -1271
- package/templates/_shared/journey-manifest.md +10 -6
- package/.cursor/commands/autopilot-multi.md +0 -14
- package/.cursor/commands/contract-version-bump.md +0 -14
- package/.cursor/commands/journey-audit.md +0 -14
- package/.cursor/skills/contract-version-bump/SKILL.md +0 -12
- package/.cursor/skills/daily/SKILL.md +0 -12
- package/.cursor/skills/domain-model/SKILL.md +0 -13
- package/.cursor/skills/journey-audit/SKILL.md +0 -13
- package/.cursor/skills/skill-creator/SKILL.md +0 -13
- package/.cursor/skills/ubiquitous-language/SKILL.md +0 -13
- package/commands/autopilot-multi.md +0 -74
- package/commands/contract-version-bump.md +0 -28
- package/commands/journey-audit.md +0 -43
- package/pi/prompts/autopilot-multi.md +0 -12
- package/pi/prompts/contract-version-bump.md +0 -12
- package/pi/prompts/journey-audit.md +0 -12
- package/scripts/autopilot-multi.mjs +0 -885
- package/scripts/backfill-learnings-expires.mjs +0 -196
- package/scripts/backfill-learnings.mjs +0 -203
- package/scripts/fleet-instruction-scan.mjs +0 -141
- package/scripts/lib/autopilot/dep-graph.mjs +0 -417
- package/scripts/lib/autopilot/multi-killswitch.mjs +0 -184
- package/scripts/lib/webhook-url.mjs +0 -105
- package/scripts/lifecycle-sim-v6.mjs +0 -347
- package/scripts/migrate-learnings-jsonl.mjs +0 -189
- package/scripts/migrate-subagents-jsonl.mjs +0 -196
- package/scripts/upload-social-preview.mjs +0 -316
- package/skills/_shared/model-selection.md +0 -64
- package/skills/contract-version-bump/SKILL.md +0 -219
- package/skills/daily/SKILL.md +0 -222
- package/skills/daily/generate.sh +0 -92
- package/skills/daily/templates/daily.md.tpl +0 -36
- package/skills/journey-audit/SKILL.md +0 -269
- package/skills/skill-creator/SKILL.md +0 -168
- package/skills/ubiquitous-language/SKILL.md +0 -97
- package/skills/vault-sync/package-lock.json +0 -40
- /package/skills/{domain-model → architecture/references}/ADR-FORMAT.md +0 -0
- /package/skills/{domain-model → architecture/references}/CONTEXT-FORMAT.md +0 -0
|
@@ -67,7 +67,7 @@ import { fileURLToPath } from 'node:url';
|
|
|
67
67
|
import { emitEvent, sessionAttribution } from './events.mjs';
|
|
68
68
|
import { admitSuiteCounts, extractTestCounts } from './gates/gate-helpers.mjs';
|
|
69
69
|
import { redactDiagnosticsBundle } from './quality-gate/diagnostics.mjs';
|
|
70
|
-
import {
|
|
70
|
+
import { readProcessLocalSessionIds } from './session-identity/own-session.mjs';
|
|
71
71
|
|
|
72
72
|
export { redactDiagnosticsBundle } from './quality-gate/diagnostics.mjs';
|
|
73
73
|
|
|
@@ -114,30 +114,99 @@ function resolveRepoRoot(explicit) {
|
|
|
114
114
|
}
|
|
115
115
|
|
|
116
116
|
/**
|
|
117
|
-
*
|
|
118
|
-
*
|
|
119
|
-
*
|
|
117
|
+
* Closed set of `degraded` reasons for {@link loadCommandsFromSessionConfigDetailed}.
|
|
118
|
+
*
|
|
119
|
+
* Its own enum, deliberately not shared with `ci-status-banner.mjs`'s
|
|
120
|
+
* `DEGRADED_REASONS`: the members below are exactly the three ways THIS
|
|
121
|
+
* config-read can fail, and an enum whose members are not exhaustively
|
|
122
|
+
* reachable cannot be switched on exhaustively.
|
|
123
|
+
*
|
|
124
|
+
* - `script-missing` — `scripts/parse-config.mjs` is not on disk.
|
|
125
|
+
* - `spawn-failed` — the subprocess exited non-zero, timed out, or wrote
|
|
126
|
+
* nothing to stdout.
|
|
127
|
+
* - `parse-error` — stdout was not parseable JSON (or the read threw).
|
|
128
|
+
*/
|
|
129
|
+
export const CONFIG_READ_DEGRADED_REASONS = Object.freeze([
|
|
130
|
+
'script-missing',
|
|
131
|
+
'spawn-failed',
|
|
132
|
+
'parse-error',
|
|
133
|
+
]);
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Does `repoRoot` carry a Session Config file at all?
|
|
137
|
+
*
|
|
138
|
+
* Mirrors the file-name precedence `scripts/parse-config.mjs::resolveConfigFile`
|
|
139
|
+
* uses (`SO_CONFIG_FILE` → `CLAUDE.md` → `AGENTS.md`) but NOT its upward
|
|
140
|
+
* project-root walk — see the ceiling note at the call site.
|
|
141
|
+
*
|
|
142
|
+
* @param {string} repoRoot
|
|
143
|
+
* @returns {boolean}
|
|
144
|
+
*/
|
|
145
|
+
function sessionConfigFileExists(repoRoot) {
|
|
146
|
+
try {
|
|
147
|
+
const soConfigFile = process.env.SO_CONFIG_FILE;
|
|
148
|
+
if (soConfigFile && existsSync(join(repoRoot, soConfigFile))) return true;
|
|
149
|
+
return existsSync(join(repoRoot, 'CLAUDE.md')) || existsSync(join(repoRoot, 'AGENTS.md'));
|
|
150
|
+
} catch {
|
|
151
|
+
return false;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Load default commands from Session Config via `scripts/parse-config.mjs`,
|
|
157
|
+
* distinguishing "the config declares no `*-command` keys" from "the config
|
|
158
|
+
* could not be read at all".
|
|
159
|
+
*
|
|
160
|
+
* Both cases yield `commands: {}` — that half is unchanged, and every command
|
|
161
|
+
* resolution keeps falling through to DEFAULT_COMMANDS exactly as before. What
|
|
162
|
+
* is new is the second channel: on failure the result also carries a
|
|
163
|
+
* `degraded` reason from {@link CONFIG_READ_DEGRADED_REASONS}. Without it,
|
|
164
|
+
* `checkQgCommandDrift` reported "no drift" for a config it never managed to
|
|
165
|
+
* read — a silent all-clear derived from an absent measurement (#1031's
|
|
166
|
+
* failure class, one consumer over).
|
|
167
|
+
*
|
|
168
|
+
* `degraded` is OMITTED, not set to null, on the success path, so a strict
|
|
169
|
+
* `toEqual({commands: {…}})` pin holds for every readable config.
|
|
120
170
|
*
|
|
121
171
|
* Never throws.
|
|
122
172
|
*
|
|
123
173
|
* @param {string} repoRoot
|
|
124
|
-
* @returns {{lint?: string, typecheck?: string, test?: string}
|
|
174
|
+
* @returns {{commands: {lint?: string, typecheck?: string, test?: string},
|
|
175
|
+
* degraded?: 'script-missing'|'spawn-failed'|'parse-error'}}
|
|
125
176
|
*/
|
|
126
|
-
export function
|
|
177
|
+
export function loadCommandsFromSessionConfigDetailed(repoRoot) {
|
|
127
178
|
try {
|
|
128
179
|
const scriptPath = join(
|
|
129
180
|
dirname(fileURLToPath(import.meta.url)),
|
|
130
181
|
'..',
|
|
131
182
|
'parse-config.mjs',
|
|
132
183
|
);
|
|
133
|
-
if (!existsSync(scriptPath)) return {};
|
|
184
|
+
if (!existsSync(scriptPath)) return { commands: {}, degraded: 'script-missing' };
|
|
134
185
|
const result = spawnSync('node', [scriptPath], {
|
|
135
186
|
cwd: repoRoot,
|
|
136
187
|
encoding: 'utf8',
|
|
137
188
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
138
189
|
timeout: 10_000,
|
|
139
190
|
});
|
|
140
|
-
if (result.status !== 0 || !result.stdout)
|
|
191
|
+
if (result.status !== 0 || !result.stdout) {
|
|
192
|
+
// `parse-config.mjs` exits 1 for EVERY failure, including the benign
|
|
193
|
+
// "this repo has no CLAUDE.md / AGENTS.md at all" — measured 2026-09-05
|
|
194
|
+
// in an empty tmp dir: stderr `CLAUDE.md or AGENTS.md required`, exit 1.
|
|
195
|
+
// A repo with no Session Config has no `*-command` value that could
|
|
196
|
+
// drift, so reporting that as a failed READ would fire a warning on every
|
|
197
|
+
// such repo (`.claude/rules/host-resources.md` § HR-101 — a warning class
|
|
198
|
+
// that is usually present is a broken instrument).
|
|
199
|
+
//
|
|
200
|
+
// Ceiling (BV-004): `parse-config.mjs` resolves its config via
|
|
201
|
+
// `findProjectRoot(cwd)` walking UPWARD, this check looks only at
|
|
202
|
+
// `repoRoot` itself. For a `repoRoot` nested below the project root the
|
|
203
|
+
// two disagree — and the disagreement resolves toward SILENCE (no
|
|
204
|
+
// `degraded`), i.e. the pre-#1031 behaviour, never toward a false alarm.
|
|
205
|
+
// Revisit if a caller starts passing sub-directory roots.
|
|
206
|
+
return sessionConfigFileExists(repoRoot)
|
|
207
|
+
? { commands: {}, degraded: 'spawn-failed' }
|
|
208
|
+
: { commands: {} };
|
|
209
|
+
}
|
|
141
210
|
const cfg = JSON.parse(result.stdout);
|
|
142
211
|
const out = {};
|
|
143
212
|
if (typeof cfg['lint-command'] === 'string' && cfg['lint-command'].trim()) {
|
|
@@ -149,12 +218,30 @@ export function loadCommandsFromSessionConfig(repoRoot) {
|
|
|
149
218
|
if (typeof cfg['test-command'] === 'string' && cfg['test-command'].trim()) {
|
|
150
219
|
out.test = cfg['test-command'];
|
|
151
220
|
}
|
|
152
|
-
return out;
|
|
221
|
+
return { commands: out };
|
|
153
222
|
} catch {
|
|
154
|
-
return {};
|
|
223
|
+
return { commands: {}, degraded: 'parse-error' };
|
|
155
224
|
}
|
|
156
225
|
}
|
|
157
226
|
|
|
227
|
+
/**
|
|
228
|
+
* Load default commands from Session Config via `scripts/parse-config.mjs`.
|
|
229
|
+
* Returns a partial object — keys that fail to resolve are simply absent
|
|
230
|
+
* (the caller falls through to DEFAULT_COMMANDS for those).
|
|
231
|
+
*
|
|
232
|
+
* Thin wrapper over {@link loadCommandsFromSessionConfigDetailed}; byte-identical
|
|
233
|
+
* return value for every input, including every failure path. Callers that need
|
|
234
|
+
* to tell a failed read from an empty config use the detailed variant.
|
|
235
|
+
*
|
|
236
|
+
* Never throws.
|
|
237
|
+
*
|
|
238
|
+
* @param {string} repoRoot
|
|
239
|
+
* @returns {{lint?: string, typecheck?: string, test?: string}}
|
|
240
|
+
*/
|
|
241
|
+
export function loadCommandsFromSessionConfig(repoRoot) {
|
|
242
|
+
return loadCommandsFromSessionConfigDetailed(repoRoot).commands;
|
|
243
|
+
}
|
|
244
|
+
|
|
158
245
|
/**
|
|
159
246
|
* Resolve the three gate commands. Precedence: override > session config > defaults.
|
|
160
247
|
*
|
|
@@ -420,44 +507,32 @@ function listChangedFiles(repoRoot, ref) {
|
|
|
420
507
|
* The id-space THIS process belongs to, for comparison against a repo-global
|
|
421
508
|
* file that any session in the working copy may have written.
|
|
422
509
|
*
|
|
423
|
-
*
|
|
424
|
-
*
|
|
425
|
-
*
|
|
426
|
-
*
|
|
427
|
-
*
|
|
428
|
-
*
|
|
429
|
-
*
|
|
430
|
-
*
|
|
431
|
-
*
|
|
432
|
-
*
|
|
433
|
-
*
|
|
434
|
-
*
|
|
435
|
-
*
|
|
436
|
-
*
|
|
437
|
-
*
|
|
438
|
-
*
|
|
510
|
+
* Process-local witnesses ONLY (`CLAUDE_CODE_SESSION_ID`, via
|
|
511
|
+
* {@link readProcessLocalSessionIds}) — the `session.lock` fallback this
|
|
512
|
+
* function used to carry is deliberately GONE, not merely deprioritised.
|
|
513
|
+
* `session.lock` is a repo-GLOBAL artefact any session in the working copy
|
|
514
|
+
* can hold; unioning or falling back to it made a shared resource stand in
|
|
515
|
+
* for a process-local identity, which is exactly the #1194 hazard class
|
|
516
|
+
* (`.claude/rules/host-resources.md` § HR-102 — "a better signal REPLACES a
|
|
517
|
+
* worse one, it does not merely suppress it"). Concretely for this module
|
|
518
|
+
* (#1205): without the env var, a process that is NOT the lock holder used to
|
|
519
|
+
* silently adopt the lock holder's id as its own, so a `current-session.json`
|
|
520
|
+
* written by that same holder passed the ownership check by construction.
|
|
521
|
+
* `readProcessLocalSessionIds()` (`./session-identity/own-session.mjs`) is
|
|
522
|
+
* the shared, already-hardened implementation of this exact question — see
|
|
523
|
+
* its JSDoc for the full lock/STATE.md exclusion rationale.
|
|
524
|
+
*
|
|
525
|
+
* An empty result is not a mismatch: {@link classifyCurrentSessionOwnership}
|
|
526
|
+
* treats an empty `ownIds` as `'unknown'`, never `'foreign'`, so
|
|
527
|
+
* `corrective_context` is kept — the existing fail-open contract from #1058,
|
|
528
|
+
* unchanged by this fix.
|
|
439
529
|
*
|
|
440
|
-
* @param {string} repoRoot
|
|
441
530
|
* @returns {Set<string>} possibly empty — an empty set means "identity
|
|
442
531
|
* unresolvable", which the classifier below treats as `unknown`, never as a
|
|
443
532
|
* mismatch.
|
|
444
533
|
*/
|
|
445
|
-
function readOwnSessionIds(
|
|
446
|
-
|
|
447
|
-
// `.trim()` first: a whitespace-only env var is truthy and would otherwise
|
|
448
|
-
// enter the set as a phantom id (`development.md` § env-var whitespace trap).
|
|
449
|
-
const fromEnv = (process.env.CLAUDE_CODE_SESSION_ID || '').trim();
|
|
450
|
-
if (fromEnv) ids.add(fromEnv);
|
|
451
|
-
if (ids.size === 0) {
|
|
452
|
-
try {
|
|
453
|
-
const lock = readLock({ repoRoot });
|
|
454
|
-
for (const key of ['session_id', 'semantic_session_id']) {
|
|
455
|
-
const value = typeof lock?.[key] === 'string' ? lock[key].trim() : '';
|
|
456
|
-
if (value) ids.add(value);
|
|
457
|
-
}
|
|
458
|
-
} catch { /* readLock never throws, but the contract is not ours to trust */ }
|
|
459
|
-
}
|
|
460
|
-
return ids;
|
|
534
|
+
function readOwnSessionIds() {
|
|
535
|
+
return new Set(readProcessLocalSessionIds({ env: process.env, hookInput: null }));
|
|
461
536
|
}
|
|
462
537
|
|
|
463
538
|
/**
|
|
@@ -534,7 +609,8 @@ function readCorrectiveContext(repoRoot) {
|
|
|
534
609
|
if (!existsSync(p)) return [];
|
|
535
610
|
const raw = readFileSync(p, 'utf8');
|
|
536
611
|
const parsed = JSON.parse(raw);
|
|
537
|
-
const
|
|
612
|
+
const ownIds = readOwnSessionIds();
|
|
613
|
+
const { verdict, fileIds } = classifyCurrentSessionOwnership(parsed, ownIds);
|
|
538
614
|
if (verdict === 'foreign') {
|
|
539
615
|
process.stderr.write(
|
|
540
616
|
`⚠️ quality-gate: .orchestrator/current-session.json belongs to another session ` +
|
|
@@ -543,6 +619,19 @@ function readCorrectiveContext(repoRoot) {
|
|
|
543
619
|
);
|
|
544
620
|
return [];
|
|
545
621
|
}
|
|
622
|
+
// The fail-open half of `verdict === 'unknown'`: the file DOES name an id,
|
|
623
|
+
// but this process has no process-local witness of its own (ownIds is
|
|
624
|
+
// empty — no `CLAUDE_CODE_SESSION_ID`, e.g. Codex/Cursor). Ownership is
|
|
625
|
+
// unprovable, so the content is kept per the #1058 contract — but silently
|
|
626
|
+
// is the wrong word for that: make the fail-open visible on stderr rather
|
|
627
|
+
// than indistinguishable from a verified 'own' match.
|
|
628
|
+
if (verdict === 'unknown' && fileIds.length > 0 && ownIds.size === 0) {
|
|
629
|
+
process.stderr.write(
|
|
630
|
+
'⚠ quality-gate: cannot verify ownership of .orchestrator/current-session.json ' +
|
|
631
|
+
'(no process-local session id — CLAUDE_CODE_SESSION_ID unset); keeping corrective_context ' +
|
|
632
|
+
`from session ${fileIds.join(', ')} UNVERIFIED\n`,
|
|
633
|
+
);
|
|
634
|
+
}
|
|
546
635
|
const arr = Array.isArray(parsed?.corrective_context) ? parsed.corrective_context : [];
|
|
547
636
|
return arr.slice(-CORRECTIVE_CONTEXT_TAIL);
|
|
548
637
|
} catch {
|
|
@@ -172,18 +172,37 @@ const GLOB_METACHAR_RE = /[*?[\]{}()]|[!@+](?=\()/;
|
|
|
172
172
|
* {@link UNSAFE_PATH_QUOTE_RE}). Results are deduped, order-preserving on
|
|
173
173
|
* first occurrence.
|
|
174
174
|
*
|
|
175
|
+
* #1153 P13: skipping used to be entirely silent, so a learning whose ONLY
|
|
176
|
+
* `file_paths` entry was unusable produced the generic "no activation axis"
|
|
177
|
+
* rejection with nothing naming WHY the axis was empty. Every skipped entry is
|
|
178
|
+
* therefore recorded into the optional `dropped` sink (value + one-word
|
|
179
|
+
* reason), which {@link formatDroppedFilePaths} renders into that rejection
|
|
180
|
+
* reason. The sink is an out-parameter rather than a changed return type so
|
|
181
|
+
* every existing caller stays byte-identical.
|
|
182
|
+
*
|
|
175
183
|
* @param {string[]} filePaths
|
|
184
|
+
* @param {{value: string, reason: string}[]} [dropped] - out-param sink
|
|
176
185
|
* @returns {string[]}
|
|
177
186
|
*/
|
|
178
|
-
function globsFromFilePaths(filePaths) {
|
|
187
|
+
function globsFromFilePaths(filePaths, dropped) {
|
|
179
188
|
const out = [];
|
|
180
189
|
const seen = new Set();
|
|
181
190
|
for (const raw of filePaths) {
|
|
182
191
|
if (typeof raw !== 'string' || raw === '') continue;
|
|
183
|
-
if (GLOB_METACHAR_RE.test(raw))
|
|
192
|
+
if (GLOB_METACHAR_RE.test(raw)) {
|
|
193
|
+
dropped?.push({ value: raw, reason: 'glob-metachar' });
|
|
194
|
+
continue;
|
|
195
|
+
}
|
|
184
196
|
// #1015: frontmatter-structure guard. Skipping (never escaping) keeps this
|
|
185
197
|
// idempotent and non-overlapping with the renderer's sanitiser.
|
|
186
|
-
if (CONTROL_CHARS_TEST_RE.test(raw)
|
|
198
|
+
if (CONTROL_CHARS_TEST_RE.test(raw)) {
|
|
199
|
+
dropped?.push({ value: raw, reason: 'control-char' });
|
|
200
|
+
continue;
|
|
201
|
+
}
|
|
202
|
+
if (UNSAFE_PATH_QUOTE_RE.test(raw)) {
|
|
203
|
+
dropped?.push({ value: raw, reason: 'quote' });
|
|
204
|
+
continue;
|
|
205
|
+
}
|
|
187
206
|
const normalized = raw.replace(/\\/g, '/');
|
|
188
207
|
const dir = dirname(normalized);
|
|
189
208
|
const pattern = dir === '.' ? normalized : `${dir}/**`;
|
|
@@ -195,6 +214,42 @@ function globsFromFilePaths(filePaths) {
|
|
|
195
214
|
return out;
|
|
196
215
|
}
|
|
197
216
|
|
|
217
|
+
/** Max chars of a single dropped `file_paths` value in a rejection reason. */
|
|
218
|
+
const DROPPED_VALUE_MAX = 60;
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Render the `dropped` sink of {@link globsFromFilePaths} into a suffix for the
|
|
222
|
+
* never-always-on rejection reason (#1153 P13).
|
|
223
|
+
*
|
|
224
|
+
* The reason string is OPERATOR-VISIBLE (engine.mjs forwards it verbatim into
|
|
225
|
+
* `reason: emit/render error: …` and from there into the candidates ledger), so
|
|
226
|
+
* a hostile `file_paths` entry must not be able to inject newlines or ANSI into
|
|
227
|
+
* it. Each value is therefore `JSON.stringify`-escaped — which turns every
|
|
228
|
+
* control char into a `\uXXXX` literal — and then hard-truncated to
|
|
229
|
+
* {@link DROPPED_VALUE_MAX} chars, with any trailing partial backslash escape
|
|
230
|
+
* removed so the cut cannot end mid-sequence.
|
|
231
|
+
*
|
|
232
|
+
* @param {{value: string, reason: string}[]} dropped
|
|
233
|
+
* @returns {string} `''` when nothing was dropped, else a ` — dropped …` suffix
|
|
234
|
+
*/
|
|
235
|
+
function formatDroppedFilePaths(dropped) {
|
|
236
|
+
if (!Array.isArray(dropped) || dropped.length === 0) return '';
|
|
237
|
+
const rendered = dropped.map(({ value, reason }) => {
|
|
238
|
+
const escaped = JSON.stringify(String(value));
|
|
239
|
+
const clipped =
|
|
240
|
+
escaped.length <= DROPPED_VALUE_MAX
|
|
241
|
+
? escaped
|
|
242
|
+
// A `\uXXXX` escape is 6 chars, so the cut can land at FIVE distinct
|
|
243
|
+
// depths inside one — and `/\\+$/` only ever caught the shallowest
|
|
244
|
+
// (`\`). Measured at DROPPED_VALUE_MAX=60 with a NUL at offsets 53..56:
|
|
245
|
+
// the old form left `\u000`, `\u00`, `\u0` and `\u` standing. Strip the
|
|
246
|
+
// backslash together with up to three hex digits of a partial `u`-escape.
|
|
247
|
+
: `${escaped.slice(0, DROPPED_VALUE_MAX - 1).replace(/\\(u[0-9a-fA-F]{0,3})?$/, '')}…`;
|
|
248
|
+
return `${clipped} (${reason})`;
|
|
249
|
+
});
|
|
250
|
+
return ` — dropped file_paths: ${dropped.length} (${rendered.join(', ')})`;
|
|
251
|
+
}
|
|
252
|
+
|
|
198
253
|
/**
|
|
199
254
|
* Build a single-line, frontmatter-safe description from a learning.
|
|
200
255
|
*
|
|
@@ -320,7 +375,8 @@ export function toActivationMetadata(learning, { ruleExpiryDays, now, minRuleDay
|
|
|
320
375
|
}
|
|
321
376
|
|
|
322
377
|
const filePaths = Array.isArray(learning.file_paths) ? learning.file_paths : [];
|
|
323
|
-
const
|
|
378
|
+
const droppedFilePaths = [];
|
|
379
|
+
const globs = globsFromFilePaths(filePaths, droppedFilePaths);
|
|
324
380
|
|
|
325
381
|
// #1015: `host_class` is copied straight into an UNQUOTED `host-class:`
|
|
326
382
|
// frontmatter line by the renderer, so its SHAPE is load-bearing. Reject a
|
|
@@ -358,16 +414,22 @@ export function toActivationMetadata(learning, { ruleExpiryDays, now, minRuleDay
|
|
|
358
414
|
// The brandmauer: an auto-generated rule must carry ≥1 activation axis. If we
|
|
359
415
|
// could derive neither a glob nor a host-class, refuse — never emit always-on.
|
|
360
416
|
if (globs.length === 0 && hostClass === undefined) {
|
|
417
|
+
// #1153 P13: name the entries that were silently skipped on the way here —
|
|
418
|
+
// otherwise "no activation axis" is indistinguishable from "no file_paths
|
|
419
|
+
// at all", and the operator has no way to see that a malformed record (not
|
|
420
|
+
// an empty one) caused the rejection.
|
|
421
|
+
const droppedSuffix = formatDroppedFilePaths(droppedFilePaths);
|
|
361
422
|
if (hostClassDroppedByType) {
|
|
362
423
|
throw new Error(
|
|
363
424
|
`emitter: host_class ${JSON.stringify(learning.host_class)} is present and well-formed, but type ` +
|
|
364
425
|
`${JSON.stringify(learning.type)} is not host-specific (#1090 — host-class is only copied through for ` +
|
|
365
426
|
`${[...HOST_SPECIFIC_TYPES].join(', ')}) — refusing to emit always-on auto-generated rule ` +
|
|
366
|
-
|
|
427
|
+
`(never-always-on invariant); no other activation axis (globs) is available either.${droppedSuffix}`,
|
|
367
428
|
);
|
|
368
429
|
}
|
|
369
430
|
throw new Error(
|
|
370
|
-
'emitter: no activation axis (globs/host-class) — refusing to emit always-on auto-generated rule
|
|
431
|
+
'emitter: no activation axis (globs/host-class) — refusing to emit always-on auto-generated rule ' +
|
|
432
|
+
`(never-always-on invariant)${droppedSuffix}`,
|
|
371
433
|
);
|
|
372
434
|
}
|
|
373
435
|
|
|
@@ -33,10 +33,16 @@
|
|
|
33
33
|
* the engine performs is into the reconcile-candidates sidecar (via
|
|
34
34
|
* idempotency.mjs), and even that is skipped under `dryRun`.
|
|
35
35
|
*
|
|
36
|
-
* ── never-throws contract
|
|
37
|
-
* `runReconcile` NEVER throws to its caller
|
|
38
|
-
* degrades to a recorded rejection (never a
|
|
39
|
-
* error returns a zeroed result
|
|
36
|
+
* ── never-throws contract (PIPELINE errors only) ─────────────────────────────
|
|
37
|
+
* Once the pipeline is running, `runReconcile` NEVER throws to its caller: a
|
|
38
|
+
* per-learning emit/render failure degrades to a recorded rejection (never a
|
|
39
|
+
* crash), and any unexpected top-level pipeline error returns a zeroed result
|
|
40
|
+
* with an `error` field. The ONE exception is a caller-INPUT validation error
|
|
41
|
+
* at the very top of `runReconcile` — an explicitly-passed `trigger` outside
|
|
42
|
+
* `KNOWN_TRIGGERS` (issue #1201 Part A) THROWS synchronously before the
|
|
43
|
+
* pipeline ever starts, the same class of failure as passing a malformed
|
|
44
|
+
* `params` object to any other function in this codebase; see
|
|
45
|
+
* `assertKnownTrigger` below.
|
|
40
46
|
*
|
|
41
47
|
* Plain Node ESM, no external deps — Node 20+ stdlib + the four siblings only.
|
|
42
48
|
*
|
|
@@ -154,8 +160,10 @@ function zeroedResult(error) {
|
|
|
154
160
|
/**
|
|
155
161
|
* Default learnings loader — read + parse `<repoRoot>/.orchestrator/metrics/learnings.jsonl`
|
|
156
162
|
* line-by-line, migrate/normalize records through the learnings schema SSOT,
|
|
157
|
-
* and skip blank/malformed lines. A missing file yields `[]
|
|
158
|
-
*
|
|
163
|
+
* and skip blank/malformed lines. A missing file (ENOENT) yields `[]`
|
|
164
|
+
* silently; an unreadable one (EACCES/EISDIR/…) yields `[]` with a stderr
|
|
165
|
+
* WARN (#1210 — ENOENT and other read failures are different facts, same
|
|
166
|
+
* split as `sessions-canonical.mjs` `readCanonicalSessions`).
|
|
159
167
|
*
|
|
160
168
|
* @param {string|undefined} repoRoot
|
|
161
169
|
* @returns {Array<Record<string, unknown>>}
|
|
@@ -169,8 +177,15 @@ function defaultLoadLearnings(repoRoot) {
|
|
|
169
177
|
let raw;
|
|
170
178
|
try {
|
|
171
179
|
raw = readFileSync(absPath, 'utf8');
|
|
172
|
-
} catch {
|
|
173
|
-
|
|
180
|
+
} catch (err) {
|
|
181
|
+
if (!err || err.code !== 'ENOENT') {
|
|
182
|
+
process.stderr.write(
|
|
183
|
+
`⚠ defaultLoadLearnings: cannot read ${absPath} ` +
|
|
184
|
+
`(${err?.code ?? '?'}: ${err?.message ?? String(err)}) — ` +
|
|
185
|
+
'treating as EMPTY, counts below are floors\n',
|
|
186
|
+
);
|
|
187
|
+
}
|
|
188
|
+
return [];
|
|
174
189
|
}
|
|
175
190
|
|
|
176
191
|
/** @type {Array<Record<string, unknown>>} */
|
|
@@ -463,7 +478,7 @@ function defaultReadMaterializedProvenance(repoRoot) {
|
|
|
463
478
|
* @param {boolean} [opts.dryRun] - when true, compute proposals but SKIP the merge entirely.
|
|
464
479
|
* @returns {Promise<ReconcileResult>}
|
|
465
480
|
*/
|
|
466
|
-
|
|
481
|
+
async function runReconcileInner(
|
|
467
482
|
{
|
|
468
483
|
repoRoot,
|
|
469
484
|
ruleExpiryDays,
|
|
@@ -780,3 +795,228 @@ export async function runReconcile(
|
|
|
780
795
|
return zeroedResult(msg);
|
|
781
796
|
}
|
|
782
797
|
}
|
|
798
|
+
|
|
799
|
+
/**
|
|
800
|
+
* Ledger name of the reconcile run event (issue #1192). Catalogued in
|
|
801
|
+
* `docs/events-schema.md`; `events-schema.mjs` needs no registration — it
|
|
802
|
+
* validates the NAME shape only, and this name already satisfies it.
|
|
803
|
+
*/
|
|
804
|
+
export const RECONCILE_EVENT = 'orchestrator.reconcile.completed';
|
|
805
|
+
|
|
806
|
+
/**
|
|
807
|
+
* Closed enum of `trigger` values a caller may explicitly pass to
|
|
808
|
+
* {@link runReconcile} (issue #1201 Part A / Discovery D8). Two of the three
|
|
809
|
+
* real call sites pinned `trigger` by PROSE inside a `.md` skill file —
|
|
810
|
+
* `skill` (`skills/reconcile/SKILL.md`) and `session-end`
|
|
811
|
+
* (`skills/session-end/phase-3-6-tail.md`) — where nothing enforced the
|
|
812
|
+
* string ever matched this list; only `phase-skip`
|
|
813
|
+
* (`scripts/lib/session-end/phase-skip.mjs`) pinned it in code. The two `.md`
|
|
814
|
+
* callers are migrated onto {@link runReconcileFromSkill} /
|
|
815
|
+
* {@link runReconcileAtSessionEnd} in this same change, so "prose sets the
|
|
816
|
+
* trigger" stops being possible for them at all.
|
|
817
|
+
*/
|
|
818
|
+
export const KNOWN_TRIGGERS = Object.freeze(['skill', 'session-end', 'phase-skip']);
|
|
819
|
+
|
|
820
|
+
/**
|
|
821
|
+
* Reject an explicitly-passed `trigger` that is not in {@link KNOWN_TRIGGERS}
|
|
822
|
+
* — including the literal `'unknown'`, which is a DEFAULT this module mints
|
|
823
|
+
* for an ABSENT trigger, never a value a caller should pass on purpose. An
|
|
824
|
+
* absent trigger (`undefined`) is deliberately let through unchanged: it is
|
|
825
|
+
* legacy behaviour for a caller that has not yet been migrated onto a pinned
|
|
826
|
+
* wrapper, and `buildReconcilePayload` already defaults it to `'unknown'` so
|
|
827
|
+
* the per-trigger denominator in the ledger stays honest about which runs are
|
|
828
|
+
* unattributed rather than silently breaking them.
|
|
829
|
+
*
|
|
830
|
+
* BV-004 revisit trigger: `scripts/lib/session-end/phase-skip.mjs` is the one
|
|
831
|
+
* remaining caller passing `trigger: 'phase-skip'` as a hand-written literal
|
|
832
|
+
* rather than through {@link runReconcileFromPhaseSkip} (out of this task's
|
|
833
|
+
* file scope) — once every caller is migrated onto a pinned wrapper, flip the
|
|
834
|
+
* absent-trigger default below from "let it run" to a throw, closing the last
|
|
835
|
+
* legacy path.
|
|
836
|
+
*
|
|
837
|
+
* @param {unknown} trigger
|
|
838
|
+
* @throws {Error} when `trigger` is defined but not in `KNOWN_TRIGGERS`.
|
|
839
|
+
* @returns {void}
|
|
840
|
+
*/
|
|
841
|
+
export function assertKnownTrigger(trigger) {
|
|
842
|
+
if (trigger === undefined) return;
|
|
843
|
+
if (!KNOWN_TRIGGERS.includes(trigger)) {
|
|
844
|
+
throw new Error(
|
|
845
|
+
`runReconcile: invalid trigger ${JSON.stringify(trigger)} — must be one of: ` +
|
|
846
|
+
`${KNOWN_TRIGGERS.join(', ')} (or omitted entirely, for a not-yet-migrated caller).`,
|
|
847
|
+
);
|
|
848
|
+
}
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
/** Clamp for the `reason` string on the abort path — a message can be long. */
|
|
852
|
+
const REASON_MAX_CHARS = 300;
|
|
853
|
+
|
|
854
|
+
/** The closed target enum `resolveEffectiveTargets` recognises. */
|
|
855
|
+
const KNOWN_TARGETS = ['repo-local', 'baseline'];
|
|
856
|
+
|
|
857
|
+
/**
|
|
858
|
+
* Build the `orchestrator.reconcile.completed` payload from a finished run.
|
|
859
|
+
*
|
|
860
|
+
* Counter fields are written INCLUDING `0`: each was MEASURED over the whole
|
|
861
|
+
* run, so a written zero is the payload (same contract as the vault-mirror run
|
|
862
|
+
* event). The two absence-preserving exceptions are `store_records_dropped`
|
|
863
|
+
* (absent ⇒ the candidate store was never inspected — dryRun, empty
|
|
864
|
+
* short-circuit, error path) and `targets` (absent ⇒ the caller asserted no
|
|
865
|
+
* target list). `aborted`/`reason` appear only when the never-throws guard
|
|
866
|
+
* fired; their absence means "ran to the end", never "unknown".
|
|
867
|
+
*
|
|
868
|
+
* @param {ReconcileResult} result
|
|
869
|
+
* @param {{ trigger?: string, targets?: string[], dryRun?: boolean, durationMs: number }} ctx
|
|
870
|
+
* @returns {Record<string, unknown>}
|
|
871
|
+
*/
|
|
872
|
+
function buildReconcilePayload(result, ctx) {
|
|
873
|
+
const summary = (result && result.summary) || {};
|
|
874
|
+
/** @type {Record<string, unknown>} */
|
|
875
|
+
const payload = {
|
|
876
|
+
trigger: typeof ctx.trigger === 'string' && ctx.trigger.trim() !== '' ? ctx.trigger : 'unknown',
|
|
877
|
+
dry_run: ctx.dryRun === true,
|
|
878
|
+
learnings_total: summary.totalLearnings ?? 0,
|
|
879
|
+
eligible: summary.eligible ?? 0,
|
|
880
|
+
proposals: summary.proposed ?? 0,
|
|
881
|
+
rejected: summary.rejected ?? 0,
|
|
882
|
+
capped: summary.capped ?? 0,
|
|
883
|
+
already_materialized: summary.alreadyMaterialized ?? 0,
|
|
884
|
+
written: summary.written === true,
|
|
885
|
+
duration_ms: ctx.durationMs,
|
|
886
|
+
};
|
|
887
|
+
// `targets` originates in operator-authored Session Config (`reconcile.targets`)
|
|
888
|
+
// and is unbounded there. Allowlisted to the CLOSED enum `resolveEffectiveTargets`
|
|
889
|
+
// recognises before it enters the ledger and the optional Clank webhook (Q2-F4):
|
|
890
|
+
// anything else is not a target this engine can act on, so recording it would
|
|
891
|
+
// be a verbatim echo of untrusted text, never a measurement. Omitted when empty.
|
|
892
|
+
const targets = Array.isArray(ctx.targets)
|
|
893
|
+
? [...new Set(ctx.targets.filter((t) => KNOWN_TARGETS.includes(t)))]
|
|
894
|
+
: [];
|
|
895
|
+
if (targets.length > 0) payload.targets = targets;
|
|
896
|
+
if (typeof summary.skipped === 'number') payload.store_records_dropped = summary.skipped;
|
|
897
|
+
if (typeof result?.error === 'string' && result.error !== '') {
|
|
898
|
+
payload.aborted = 'engine-error';
|
|
899
|
+
payload.reason = result.error.slice(0, REASON_MAX_CHARS);
|
|
900
|
+
}
|
|
901
|
+
return payload;
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
/**
|
|
905
|
+
* Record one reconcile run in the repo's event ledger — best-effort.
|
|
906
|
+
*
|
|
907
|
+
* Refuses the ambient `SO_PROJECT_DIR` destination when no `repoRoot` was
|
|
908
|
+
* given: most engine tests call `runReconcile` without one, and a fallback
|
|
909
|
+
* would append synthetic records to the operator's REAL fleet ledger on every
|
|
910
|
+
* `npm test` (#1119, `scripts/lib/express-path.mjs`). Diagnostics on stderr.
|
|
911
|
+
*
|
|
912
|
+
* @param {ReconcileResult} result
|
|
913
|
+
* @param {{ repoRoot?: string, trigger?: string, targets?: string[], dryRun?: boolean, durationMs: number }} ctx
|
|
914
|
+
*/
|
|
915
|
+
async function emitReconcileCompleted(result, ctx) {
|
|
916
|
+
const { repoRoot } = ctx;
|
|
917
|
+
if (typeof repoRoot !== 'string' || repoRoot.trim() === '') {
|
|
918
|
+
process.stderr.write(
|
|
919
|
+
`reconcile: skipped ${RECONCILE_EVENT} — no repoRoot given; ` +
|
|
920
|
+
'refusing the ambient SO_PROJECT_DIR destination (#1119).\n',
|
|
921
|
+
);
|
|
922
|
+
return;
|
|
923
|
+
}
|
|
924
|
+
const { emitEvent } = await import('../events.mjs');
|
|
925
|
+
await emitEvent(RECONCILE_EVENT, buildReconcilePayload(result, ctx), { repoRoot });
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
/**
|
|
929
|
+
* Public boundary: run the reconciliation pipeline and record the run.
|
|
930
|
+
*
|
|
931
|
+
* A thin WRAPPER, deliberately: the pipeline has three return points (empty
|
|
932
|
+
* short-circuit, normal tail, never-throws catch), and an inline emit would
|
|
933
|
+
* miss two of them — including the empty corpus and the error path, the two
|
|
934
|
+
* runs an operator most needs recorded (`.claude/rules/host-resources.md`
|
|
935
|
+
* § HR-105). Same shape as `runNarrativeMirror` + `mirrorNarrative` in
|
|
936
|
+
* `scripts/lib/vault-status/narrative-mirror.mjs`.
|
|
937
|
+
*
|
|
938
|
+
* The emit is wrapped in try/catch because `emitEvent` THROWS
|
|
939
|
+
* `EventValidationError` on an invalid record — without the catch, telemetry
|
|
940
|
+
* would break this function's never-throws contract. The pipeline's result is
|
|
941
|
+
* returned UNTOUCHED whether or not the ledger accepted the record.
|
|
942
|
+
*
|
|
943
|
+
* @param {Object} [params] - see {@link runReconcileInner}, plus:
|
|
944
|
+
* @param {'skill'|'session-end'|'phase-skip'} [params.trigger] - which caller
|
|
945
|
+
* invoked this run; recorded ALWAYS (default `'unknown'` when omitted) so
|
|
946
|
+
* the per-trigger denominator is complete. Not read by the pipeline.
|
|
947
|
+
* VALIDATED against {@link KNOWN_TRIGGERS} via {@link assertKnownTrigger}
|
|
948
|
+
* when explicitly passed — an unknown string, including the literal
|
|
949
|
+
* `'unknown'`, THROWS synchronously; an absent trigger is still let
|
|
950
|
+
* through unchanged (issue #1201 Part A — see `assertKnownTrigger`'s
|
|
951
|
+
* BV-004 revisit-trigger note for why the absent case is not also a
|
|
952
|
+
* throw yet). Prefer {@link runReconcileFromSkill},
|
|
953
|
+
* {@link runReconcileAtSessionEnd}, or {@link runReconcileFromPhaseSkip}
|
|
954
|
+
* over passing `trigger` here directly.
|
|
955
|
+
* @param {string[]} [params.targets] - the caller's effective target list
|
|
956
|
+
* (`resolveEffectiveTargets`); recorded when non-empty, omitted otherwise.
|
|
957
|
+
* Not read by the pipeline.
|
|
958
|
+
* @param {Object} [opts] - see {@link runReconcileInner}.
|
|
959
|
+
* @returns {Promise<ReconcileResult>}
|
|
960
|
+
*/
|
|
961
|
+
export async function runReconcile(params = {}, opts = {}) {
|
|
962
|
+
assertKnownTrigger(params.trigger);
|
|
963
|
+
const t0 = Date.now();
|
|
964
|
+
const result = await runReconcileInner(params, opts);
|
|
965
|
+
try {
|
|
966
|
+
await emitReconcileCompleted(result, {
|
|
967
|
+
repoRoot: params.repoRoot,
|
|
968
|
+
trigger: params.trigger,
|
|
969
|
+
targets: params.targets,
|
|
970
|
+
dryRun: params.dryRun === true || opts.dryRun === true,
|
|
971
|
+
durationMs: Date.now() - t0,
|
|
972
|
+
});
|
|
973
|
+
} catch {
|
|
974
|
+
// Best-effort telemetry — never the reason a reconcile run fails.
|
|
975
|
+
}
|
|
976
|
+
return result;
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
/**
|
|
980
|
+
* Pin `trigger: 'skill'` in code for the `/reconcile` skill (Phase 2.3,
|
|
981
|
+
* `skills/reconcile/SKILL.md`) — the skill's own `.md` prose no longer sets
|
|
982
|
+
* the trigger string itself (issue #1201 Part A). Any `trigger` the caller
|
|
983
|
+
* passes in `params` is OVERWRITTEN; every other field forwards unchanged.
|
|
984
|
+
*
|
|
985
|
+
* @param {Object} [params] - see {@link runReconcile} `params`.
|
|
986
|
+
* @param {Object} [opts] - see {@link runReconcile} `opts` (DI seams).
|
|
987
|
+
* @returns {Promise<ReconcileResult>}
|
|
988
|
+
*/
|
|
989
|
+
export async function runReconcileFromSkill(params = {}, opts = {}) {
|
|
990
|
+
return runReconcile({ ...params, trigger: 'skill' }, opts);
|
|
991
|
+
}
|
|
992
|
+
|
|
993
|
+
/**
|
|
994
|
+
* Pin `trigger: 'session-end'` in code for session-end Phase 3.6.8
|
|
995
|
+
* (`skills/session-end/phase-3-6-tail.md`) — see
|
|
996
|
+
* {@link runReconcileFromSkill} for the rationale and contract.
|
|
997
|
+
*
|
|
998
|
+
* @param {Object} [params] - see {@link runReconcile} `params`.
|
|
999
|
+
* @param {Object} [opts] - see {@link runReconcile} `opts` (DI seams).
|
|
1000
|
+
* @returns {Promise<ReconcileResult>}
|
|
1001
|
+
*/
|
|
1002
|
+
export async function runReconcileAtSessionEnd(params = {}, opts = {}) {
|
|
1003
|
+
return runReconcile({ ...params, trigger: 'session-end' }, opts);
|
|
1004
|
+
}
|
|
1005
|
+
|
|
1006
|
+
/**
|
|
1007
|
+
* Pin `trigger: 'phase-skip'` in code for the session-end skip-plan
|
|
1008
|
+
* aggregator (`scripts/lib/session-end/phase-skip.mjs`, the highest-volume
|
|
1009
|
+
* probe-only caller) — see {@link runReconcileFromSkill} for the rationale
|
|
1010
|
+
* and contract. NOT YET wired into `phase-skip.mjs` itself, which is out of
|
|
1011
|
+
* this task's file scope and still passes `trigger: 'phase-skip'` as a
|
|
1012
|
+
* hand-written literal directly to `runReconcile`; exported here so that
|
|
1013
|
+
* migration is a one-line import swap (see `assertKnownTrigger`'s BV-004
|
|
1014
|
+
* revisit-trigger note).
|
|
1015
|
+
*
|
|
1016
|
+
* @param {Object} [params] - see {@link runReconcile} `params`.
|
|
1017
|
+
* @param {Object} [opts] - see {@link runReconcile} `opts` (DI seams).
|
|
1018
|
+
* @returns {Promise<ReconcileResult>}
|
|
1019
|
+
*/
|
|
1020
|
+
export async function runReconcileFromPhaseSkip(params = {}, opts = {}) {
|
|
1021
|
+
return runReconcile({ ...params, trigger: 'phase-skip' }, opts);
|
|
1022
|
+
}
|