session-orchestrator 3.16.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 +29 -0
- package/.claude-plugin/plugin.json +18 -0
- package/.codex-plugin/agents/explorer.toml +14 -0
- package/.codex-plugin/agents/session-reviewer.toml +23 -0
- package/.codex-plugin/agents/wave-worker.toml +15 -0
- package/.codex-plugin/config.toml +20 -0
- package/.codex-plugin/plugin.json +37 -0
- package/.cursor/rules/000-session-orchestrator.mdc +73 -0
- package/.cursor/rules/010-session-workflow.mdc +170 -0
- package/.cursor/rules/020-quality-gates.mdc +128 -0
- package/.cursor/rules/030-wave-execution.mdc +216 -0
- package/.cursor/rules/040-discovery.mdc +242 -0
- package/.cursor/rules/050-plan.mdc +235 -0
- package/.cursor/rules/060-evolve.mdc +232 -0
- package/.cursor/rules/070-gitlab-ops.mdc +246 -0
- package/.cursor/rules/080-ecosystem-health.mdc +145 -0
- package/.mcp.json +8 -0
- package/CHANGELOG.md +1544 -0
- package/LICENSE +21 -0
- package/NOTICE +64 -0
- package/README.md +242 -0
- package/SECURITY.md +90 -0
- package/agents/AGENTS.md +136 -0
- package/agents/analyst.md +99 -0
- package/agents/architect-reviewer.md +93 -0
- package/agents/code-implementer.md +106 -0
- package/agents/db-specialist.md +104 -0
- package/agents/dialectic-deriver.md +139 -0
- package/agents/docs-writer.md +113 -0
- package/agents/eval-judge.md +146 -0
- package/agents/memory-proposal-collector.md +297 -0
- package/agents/qa-strategist.md +102 -0
- package/agents/schemas/analyst.schema.json +46 -0
- package/agents/schemas/architect-reviewer.schema.json +50 -0
- package/agents/schemas/code-implementer.schema.json +61 -0
- package/agents/schemas/db-specialist.schema.json +80 -0
- package/agents/schemas/docs-writer.schema.json +56 -0
- package/agents/schemas/persona-panel-sidecar.schema.json +245 -0
- package/agents/schemas/qa-strategist.schema.json +46 -0
- package/agents/schemas/security-reviewer.schema.json +86 -0
- package/agents/schemas/session-reviewer.schema.json +69 -0
- package/agents/schemas/test-writer.schema.json +69 -0
- package/agents/schemas/ui-developer.schema.json +90 -0
- package/agents/schemas/ux-evaluator.schema.json +51 -0
- package/agents/security-reviewer.md +236 -0
- package/agents/session-reviewer.md +201 -0
- package/agents/skill-applied-judge.md +122 -0
- package/agents/test-writer.md +123 -0
- package/agents/ui-developer.md +109 -0
- package/agents/ux-evaluator.md +161 -0
- package/assets/icon.svg +11 -0
- package/assets/og-card.png +0 -0
- package/assets/og-card.svg +47 -0
- package/commands/autopilot-multi.md +74 -0
- package/commands/autopilot.md +80 -0
- package/commands/bootstrap.md +56 -0
- package/commands/brainstorm.md +48 -0
- package/commands/close.md +24 -0
- package/commands/debug.md +36 -0
- package/commands/discovery.md +32 -0
- package/commands/dispatcher.md +59 -0
- package/commands/eval.md +28 -0
- package/commands/evolve.md +10 -0
- package/commands/go.md +41 -0
- package/commands/grill.md +45 -0
- package/commands/harness-audit.md +26 -0
- package/commands/memory-cleanup.md +25 -0
- package/commands/persona-panel.md +121 -0
- package/commands/plan.md +15 -0
- package/commands/portfolio.md +97 -0
- package/commands/reconcile.md +23 -0
- package/commands/repo-audit.md +24 -0
- package/commands/session.md +30 -0
- package/commands/spinout.md +15 -0
- package/commands/sunset-review.md +27 -0
- package/commands/templates-ack.md +96 -0
- package/commands/test.md +97 -0
- package/docs/README.md +105 -0
- package/docs/USER-GUIDE.md +1403 -0
- package/docs/ci-setup.md +81 -0
- package/docs/codex-setup.md +142 -0
- package/docs/components.md +74 -0
- package/docs/cursor-setup.md +104 -0
- package/docs/events-schema.md +81 -0
- package/docs/migration-v3.md +148 -0
- package/docs/owner-config-schema.md +154 -0
- package/docs/persona-panel.md +433 -0
- package/docs/pi-setup.md +115 -0
- package/docs/plugin-architecture-v3.md +296 -0
- package/docs/pm-skills-marketplace.md +114 -0
- package/docs/policy-cache-validation-2026-04-28.md +118 -0
- package/docs/rule-authoring.md +316 -0
- package/docs/session-config-reference.md +1439 -0
- package/docs/session-config-template.md +961 -0
- package/docs/vault-docs-architecture.md +297 -0
- package/hooks/_lib/lock-bootstrap.mjs +272 -0
- package/hooks/_lib/lock-reconcile.mjs +93 -0
- package/hooks/_lib/profile-gate.mjs +95 -0
- package/hooks/_lib/transcript-history.mjs +211 -0
- package/hooks/agent-teams-h3-test.sh +362 -0
- package/hooks/config-protection.mjs +0 -0
- package/hooks/cwd-change-restore.mjs +131 -0
- package/hooks/enforce-commands.mjs +179 -0
- package/hooks/enforce-scope.mjs +273 -0
- package/hooks/hooks-codex.json +60 -0
- package/hooks/hooks-cursor.json +15 -0
- package/hooks/hooks-pi.json +115 -0
- package/hooks/hooks.json +215 -0
- package/hooks/loop-guard.mjs +260 -0
- package/hooks/on-session-end.mjs +217 -0
- package/hooks/on-session-start.mjs +660 -0
- package/hooks/on-stop.mjs +294 -0
- package/hooks/operator-steer.mjs +64 -0
- package/hooks/post-edit-validate.mjs +225 -0
- package/hooks/post-subagent-discovery-validator.mjs +398 -0
- package/hooks/post-tool-batch-wave-signal.mjs +328 -0
- package/hooks/post-tool-failure-corrective-context.mjs +248 -0
- package/hooks/post-tooluse-frontend-slop.mjs +184 -0
- package/hooks/pre-bash-destructive-guard.mjs +515 -0
- package/hooks/pre-bash-memory-propose-audit.mjs +206 -0
- package/hooks/pre-bash-staging-fence.mjs +223 -0
- package/hooks/pre-bash-templates-first.mjs +404 -0
- package/hooks/run-node.sh +72 -0
- package/hooks/skill-invocation-telemetry.mjs +99 -0
- package/hooks/subagent-telemetry.mjs +249 -0
- package/hooks/wave-scope-commit-guard.mjs +191 -0
- package/monitors/monitors.json +14 -0
- package/output-styles/finding-report.md +48 -0
- package/output-styles/session-report.md +53 -0
- package/output-styles/wave-summary.md +38 -0
- package/package.json +94 -0
- package/pi/extensions/session-orchestrator.ts +25 -0
- package/pi/prompts/autopilot-multi.md +12 -0
- package/pi/prompts/autopilot.md +12 -0
- package/pi/prompts/bootstrap.md +12 -0
- package/pi/prompts/brainstorm.md +12 -0
- package/pi/prompts/close.md +11 -0
- package/pi/prompts/debug.md +12 -0
- package/pi/prompts/discovery.md +12 -0
- package/pi/prompts/dispatcher.md +12 -0
- package/pi/prompts/eval.md +12 -0
- package/pi/prompts/evolve.md +12 -0
- package/pi/prompts/go.md +12 -0
- package/pi/prompts/grill.md +12 -0
- package/pi/prompts/harness-audit.md +12 -0
- package/pi/prompts/memory-cleanup.md +12 -0
- package/pi/prompts/persona-panel.md +12 -0
- package/pi/prompts/plan.md +12 -0
- package/pi/prompts/portfolio.md +12 -0
- package/pi/prompts/reconcile.md +12 -0
- package/pi/prompts/repo-audit.md +12 -0
- package/pi/prompts/session.md +12 -0
- package/pi/prompts/spinout.md +12 -0
- package/pi/prompts/sunset-review.md +12 -0
- package/pi/prompts/templates-ack.md +12 -0
- package/pi/prompts/test.md +12 -0
- package/rules/_index.md +51 -0
- package/rules/always-on/commit-discipline.md +26 -0
- package/rules/always-on/npm-quality-gates.md +26 -0
- package/rules/always-on/parallel-sessions.md +43 -0
- package/rules/opt-in-domain/prompt-caching.md +270 -0
- package/rules/opt-in-stack/backend-data.md +188 -0
- package/rules/opt-in-stack/backend.md +390 -0
- package/rules/opt-in-stack/frontend.md +98 -0
- package/rules/opt-in-stack/security-web.md +194 -0
- package/rules/opt-in-stack/swift.md +65 -0
- package/scripts/archive-closed-prds.mjs +416 -0
- package/scripts/autopilot-multi.mjs +802 -0
- package/scripts/autopilot.mjs +383 -0
- package/scripts/backfill-abandoned-sessions.mjs +265 -0
- package/scripts/backfill-learnings-expires.mjs +196 -0
- package/scripts/backfill-learnings.mjs +203 -0
- package/scripts/backfill-sessions.mjs +282 -0
- package/scripts/check-doc-consistency.sh +279 -0
- package/scripts/check-package-manager.mjs +445 -0
- package/scripts/ci/assert-vitest-green.mjs +267 -0
- package/scripts/codex-install.mjs +435 -0
- package/scripts/compute-grounding-injection.sh +186 -0
- package/scripts/cursor-install.mjs +113 -0
- package/scripts/dialectic-deriver.mjs +573 -0
- package/scripts/emit-event.mjs +160 -0
- package/scripts/emit-session.mjs +212 -0
- package/scripts/eval-session.mjs +262 -0
- package/scripts/export-hw-learnings.mjs +437 -0
- package/scripts/gc-stale-worktrees.mjs +666 -0
- package/scripts/generate-pi-prompts.mjs +127 -0
- package/scripts/harness-audit.mjs +287 -0
- package/scripts/lib/agent-frontmatter.mjs +266 -0
- package/scripts/lib/agent-output-schema.mjs +166 -0
- package/scripts/lib/agent-status.mjs +303 -0
- package/scripts/lib/ajv-loader.mjs +34 -0
- package/scripts/lib/auto-dialectic.mjs +382 -0
- package/scripts/lib/auto-dream.mjs +471 -0
- package/scripts/lib/autonomy/suitability.mjs +212 -0
- package/scripts/lib/autopilot/dep-graph.mjs +417 -0
- package/scripts/lib/autopilot/durable-telemetry.mjs +121 -0
- package/scripts/lib/autopilot/flags.mjs +104 -0
- package/scripts/lib/autopilot/kill-switches.mjs +174 -0
- package/scripts/lib/autopilot/loop.mjs +320 -0
- package/scripts/lib/autopilot/mr-draft.mjs +520 -0
- package/scripts/lib/autopilot/multi-killswitch.mjs +184 -0
- package/scripts/lib/autopilot/recent-runs.mjs +106 -0
- package/scripts/lib/autopilot/stall-sampler.mjs +97 -0
- package/scripts/lib/autopilot/telemetry.mjs +224 -0
- package/scripts/lib/autopilot/worktree-pipeline.mjs +605 -0
- package/scripts/lib/autopilot-telemetry.mjs +11 -0
- package/scripts/lib/autopilot.mjs +39 -0
- package/scripts/lib/backlog-scan.mjs +179 -0
- package/scripts/lib/bootstrap-lock-freshness.mjs +260 -0
- package/scripts/lib/bootstrap-lock-refresh.mjs +186 -0
- package/scripts/lib/build-live-signals.mjs +150 -0
- package/scripts/lib/ci-status-banner.mjs +425 -0
- package/scripts/lib/claude-md-budget-lint.mjs +246 -0
- package/scripts/lib/cli-flags.mjs +158 -0
- package/scripts/lib/codex/plugin-contract.mjs +610 -0
- package/scripts/lib/cold-start-detector.mjs +240 -0
- package/scripts/lib/command-blocker.mjs +458 -0
- package/scripts/lib/common.mjs +333 -0
- package/scripts/lib/config/auto-dream.mjs +77 -0
- package/scripts/lib/config/block-header.mjs +94 -0
- package/scripts/lib/config/broken-window.mjs +114 -0
- package/scripts/lib/config/coercers.mjs +248 -0
- package/scripts/lib/config/cold-start.mjs +92 -0
- package/scripts/lib/config/config-protection.mjs +120 -0
- package/scripts/lib/config/cross-repo.mjs +104 -0
- package/scripts/lib/config/custom-phases.mjs +213 -0
- package/scripts/lib/config/dialectic.mjs +92 -0
- package/scripts/lib/config/discovery-validator.mjs +75 -0
- package/scripts/lib/config/dispatcher-autonomy-capture.mjs +240 -0
- package/scripts/lib/config/dispatcher-autonomy.mjs +152 -0
- package/scripts/lib/config/docs-orchestrator.mjs +90 -0
- package/scripts/lib/config/docs-staleness.mjs +96 -0
- package/scripts/lib/config/drift-check.mjs +155 -0
- package/scripts/lib/config/eval.mjs +130 -0
- package/scripts/lib/config/events-rotation.mjs +74 -0
- package/scripts/lib/config/evolve.mjs +308 -0
- package/scripts/lib/config/frontend-slop-hook.mjs +104 -0
- package/scripts/lib/config/gitlab-portfolio.mjs +150 -0
- package/scripts/lib/config/handover-gate.mjs +106 -0
- package/scripts/lib/config/host-paths.mjs +76 -0
- package/scripts/lib/config/io.mjs +54 -0
- package/scripts/lib/config/loop-guard.mjs +117 -0
- package/scripts/lib/config/memory.mjs +150 -0
- package/scripts/lib/config/persona-gate-wave.mjs +258 -0
- package/scripts/lib/config/reconcile.mjs +205 -0
- package/scripts/lib/config/section-extractor.mjs +100 -0
- package/scripts/lib/config/skill-evolution.mjs +112 -0
- package/scripts/lib/config/slopcheck.mjs +99 -0
- package/scripts/lib/config/state-md-lock.mjs +83 -0
- package/scripts/lib/config/templates-first.mjs +94 -0
- package/scripts/lib/config/test.mjs +113 -0
- package/scripts/lib/config/vault-integration.mjs +201 -0
- package/scripts/lib/config/vault-mirror-quality.mjs +99 -0
- package/scripts/lib/config/vault-staleness.mjs +84 -0
- package/scripts/lib/config/vault-sync.mjs +96 -0
- package/scripts/lib/config/verification-auto-fix.mjs +84 -0
- package/scripts/lib/config/wave-reviewers.mjs +133 -0
- package/scripts/lib/config-schema.mjs +345 -0
- package/scripts/lib/config.mjs +474 -0
- package/scripts/lib/convergence-monitor.mjs +389 -0
- package/scripts/lib/coordinator-snapshot.mjs +371 -0
- package/scripts/lib/crypto-digest-utils.mjs +91 -0
- package/scripts/lib/discovery/helpers.mjs +127 -0
- package/scripts/lib/discovery/triage-state.mjs +279 -0
- package/scripts/lib/dispatcher/cli.mjs +257 -0
- package/scripts/lib/dispatcher/enumerate.mjs +243 -0
- package/scripts/lib/dispatcher/rank.mjs +363 -0
- package/scripts/lib/ecosystem-health.mjs +224 -0
- package/scripts/lib/ecosystem-wizard/ci-detector.mjs +18 -0
- package/scripts/lib/ecosystem-wizard/config-parser.mjs +54 -0
- package/scripts/lib/ecosystem-wizard/config-writer.mjs +287 -0
- package/scripts/lib/ecosystem-wizard/package-manager-detector.mjs +42 -0
- package/scripts/lib/ecosystem-wizard/wizard-prompt.mjs +246 -0
- package/scripts/lib/ecosystem-wizard.mjs +48 -0
- package/scripts/lib/env-check.mjs +89 -0
- package/scripts/lib/eval/engine.mjs +605 -0
- package/scripts/lib/eval/judge.mjs +433 -0
- package/scripts/lib/eval/report.mjs +367 -0
- package/scripts/lib/eval/schema.mjs +618 -0
- package/scripts/lib/eval/session-resolve.mjs +137 -0
- package/scripts/lib/eval/sink.mjs +77 -0
- package/scripts/lib/events-rotation.mjs +86 -0
- package/scripts/lib/events-schema.mjs +81 -0
- package/scripts/lib/events.mjs +80 -0
- package/scripts/lib/evolve/autonomy-verdict.mjs +461 -0
- package/scripts/lib/evolve/autopilot-effectiveness.mjs +293 -0
- package/scripts/lib/exclusivity-matrix.mjs +68 -0
- package/scripts/lib/fetch-baseline.mjs +311 -0
- package/scripts/lib/file-lock.mjs +512 -0
- package/scripts/lib/frontend-detect/detect.mjs +138 -0
- package/scripts/lib/frontend-detect/rules.mjs +295 -0
- package/scripts/lib/frontmatter-guard.mjs +241 -0
- package/scripts/lib/gates/echo-stub-detect.mjs +39 -0
- package/scripts/lib/gates/gate-baseline.mjs +42 -0
- package/scripts/lib/gates/gate-full.mjs +85 -0
- package/scripts/lib/gates/gate-helpers.mjs +231 -0
- package/scripts/lib/gates/gate-incremental.mjs +76 -0
- package/scripts/lib/gates/gate-per-file.mjs +55 -0
- package/scripts/lib/gitlab-ops/stale-mr-sweep.mjs +447 -0
- package/scripts/lib/gitlab-portfolio/aggregator.mjs +383 -0
- package/scripts/lib/gitlab-portfolio/cli.mjs +428 -0
- package/scripts/lib/gitlab-portfolio/markdown-writer.mjs +289 -0
- package/scripts/lib/gitlab-portfolio/vcs-detect.mjs +182 -0
- package/scripts/lib/handover-gate.mjs +222 -0
- package/scripts/lib/hardening.mjs +43 -0
- package/scripts/lib/hardware-pattern-detector.mjs +238 -0
- package/scripts/lib/harness-audit/categories/category1.mjs +123 -0
- package/scripts/lib/harness-audit/categories/category2.mjs +145 -0
- package/scripts/lib/harness-audit/categories/category3.mjs +143 -0
- package/scripts/lib/harness-audit/categories/category4.mjs +202 -0
- package/scripts/lib/harness-audit/categories/category5.mjs +152 -0
- package/scripts/lib/harness-audit/categories/category6.mjs +211 -0
- package/scripts/lib/harness-audit/categories/category7.mjs +125 -0
- package/scripts/lib/harness-audit/categories/category8.mjs +328 -0
- package/scripts/lib/harness-audit/categories/category9.mjs +294 -0
- package/scripts/lib/harness-audit/categories/helpers.mjs +165 -0
- package/scripts/lib/harness-audit/categories.mjs +19 -0
- package/scripts/lib/historical-guard.mjs +15 -0
- package/scripts/lib/host-identity.mjs +262 -0
- package/scripts/lib/instruction-budget-guard.mjs +332 -0
- package/scripts/lib/io.mjs +304 -0
- package/scripts/lib/issue-close-strip-labels.mjs +161 -0
- package/scripts/lib/language-mappers/README.md +57 -0
- package/scripts/lib/language-mappers/index.mjs +165 -0
- package/scripts/lib/language-mappers/markdown.mjs +149 -0
- package/scripts/lib/language-mappers/python.mjs +249 -0
- package/scripts/lib/language-mappers/swift.mjs +201 -0
- package/scripts/lib/language-mappers/typescript.mjs +433 -0
- package/scripts/lib/learnings/expiry-sweep.mjs +164 -0
- package/scripts/lib/learnings/filters.mjs +43 -0
- package/scripts/lib/learnings/io.mjs +255 -0
- package/scripts/lib/learnings/schema.mjs +518 -0
- package/scripts/lib/learnings/surface.mjs +207 -0
- package/scripts/lib/learnings.mjs +42 -0
- package/scripts/lib/lock-reaper.mjs +648 -0
- package/scripts/lib/locks/index.mjs +31 -0
- package/scripts/lib/locks/lock-body.mjs +62 -0
- package/scripts/lib/locks/staging-fence-lock.mjs +267 -0
- package/scripts/lib/locks/state-md-lock.mjs +351 -0
- package/scripts/lib/loop-readiness-banner.mjs +144 -0
- package/scripts/lib/memory-banner.mjs +478 -0
- package/scripts/lib/memory-cleanup/worktree-sweep.mjs +108 -0
- package/scripts/lib/memory-cleanup-stamp.mjs +56 -0
- package/scripts/lib/memory-paths.mjs +31 -0
- package/scripts/lib/memory-proposals/collector.mjs +334 -0
- package/scripts/lib/memory-proposals/schema.mjs +289 -0
- package/scripts/lib/memory-proposals/sink.mjs +507 -0
- package/scripts/lib/memory-proposals/store.mjs +441 -0
- package/scripts/lib/mission-status-schema.mjs +114 -0
- package/scripts/lib/mode-selector/alternatives.mjs +64 -0
- package/scripts/lib/mode-selector/constants.mjs +29 -0
- package/scripts/lib/mode-selector/context-pressure.mjs +157 -0
- package/scripts/lib/mode-selector/rationale.mjs +55 -0
- package/scripts/lib/mode-selector/scoring.mjs +221 -0
- package/scripts/lib/mode-selector-accuracy.mjs +121 -0
- package/scripts/lib/mode-selector.mjs +160 -0
- package/scripts/lib/multi-provider-build/providers.mjs +64 -0
- package/scripts/lib/multi-provider-build/templating.mjs +130 -0
- package/scripts/lib/named-baseline-resolver.mjs +233 -0
- package/scripts/lib/named-vault-resolver.mjs +433 -0
- package/scripts/lib/owner-config/coerce.mjs +29 -0
- package/scripts/lib/owner-config/constants.mjs +21 -0
- package/scripts/lib/owner-config/defaults.mjs +50 -0
- package/scripts/lib/owner-config/error.mjs +19 -0
- package/scripts/lib/owner-config/index.mjs +13 -0
- package/scripts/lib/owner-config/merge.mjs +52 -0
- package/scripts/lib/owner-config/validate.mjs +259 -0
- package/scripts/lib/owner-config-banner.mjs +126 -0
- package/scripts/lib/owner-config-loader.mjs +159 -0
- package/scripts/lib/owner-config.example.yaml +72 -0
- package/scripts/lib/owner-config.mjs +28 -0
- package/scripts/lib/owner-interview.mjs +243 -0
- package/scripts/lib/owner-yaml.mjs +571 -0
- package/scripts/lib/package-manager.mjs +160 -0
- package/scripts/lib/path-utils.mjs +217 -0
- package/scripts/lib/peer-cards/merger.mjs +310 -0
- package/scripts/lib/peer-cards/reader.mjs +125 -0
- package/scripts/lib/peer-cards/schema.mjs +230 -0
- package/scripts/lib/peer-cards/staleness-banner.mjs +86 -0
- package/scripts/lib/peer-cards/writer.mjs +138 -0
- package/scripts/lib/peer-discovery.mjs +200 -0
- package/scripts/lib/persona-panel/catalog-loader.mjs +577 -0
- package/scripts/lib/persona-panel/consolidator.mjs +370 -0
- package/scripts/lib/persona-panel/persona-runner.mjs +375 -0
- package/scripts/lib/persona-panel/threshold.mjs +130 -0
- package/scripts/lib/pi-hook-bridge.mjs +328 -0
- package/scripts/lib/platform.mjs +266 -0
- package/scripts/lib/playwright-driver/runner.mjs +297 -0
- package/scripts/lib/plugin-root.mjs +210 -0
- package/scripts/lib/pre-dispatch-check.mjs +126 -0
- package/scripts/lib/product-repo-detect.mjs +121 -0
- package/scripts/lib/profiles/registry.mjs +176 -0
- package/scripts/lib/profiles/schema.mjs +209 -0
- package/scripts/lib/qg-command-drift-banner.mjs +88 -0
- package/scripts/lib/quality-gate/diagnostics.mjs +92 -0
- package/scripts/lib/quality-gate.mjs +536 -0
- package/scripts/lib/quality-gates-cache.mjs +228 -0
- package/scripts/lib/quality-gates-policy.mjs +95 -0
- package/scripts/lib/recommendations-v0.mjs +156 -0
- package/scripts/lib/reconcile/eligibility.mjs +203 -0
- package/scripts/lib/reconcile/emitter.mjs +244 -0
- package/scripts/lib/reconcile/engine.mjs +412 -0
- package/scripts/lib/reconcile/idempotency.mjs +239 -0
- package/scripts/lib/reconcile/renderer.mjs +211 -0
- package/scripts/lib/reconcile/writer.mjs +293 -0
- package/scripts/lib/reconcile-nudge-banner.mjs +284 -0
- package/scripts/lib/resource-probe/evaluate.mjs +190 -0
- package/scripts/lib/resource-probe/parsers.mjs +181 -0
- package/scripts/lib/resource-probe/probe-platform.mjs +300 -0
- package/scripts/lib/resource-probe.mjs +95 -0
- package/scripts/lib/rule-loader.mjs +552 -0
- package/scripts/lib/rules-sync.mjs +439 -0
- package/scripts/lib/scope-gate.mjs +496 -0
- package/scripts/lib/session-close-backfill.mjs +539 -0
- package/scripts/lib/session-discovery.mjs +256 -0
- package/scripts/lib/session-end/phase-skip.mjs +357 -0
- package/scripts/lib/session-end/worktree-cleanup.mjs +112 -0
- package/scripts/lib/session-id.mjs +362 -0
- package/scripts/lib/session-lock.mjs +703 -0
- package/scripts/lib/session-registry.mjs +355 -0
- package/scripts/lib/session-schema/aliases.mjs +71 -0
- package/scripts/lib/session-schema/constants.mjs +115 -0
- package/scripts/lib/session-schema/normalizer.mjs +66 -0
- package/scripts/lib/session-schema/timestamps.mjs +64 -0
- package/scripts/lib/session-schema/validator.mjs +453 -0
- package/scripts/lib/session-schema.mjs +71 -0
- package/scripts/lib/session-token-rollup.mjs +137 -0
- package/scripts/lib/sessions-staleness-banner.mjs +247 -0
- package/scripts/lib/skill-evolution/blast-radius-classifier.mjs +114 -0
- package/scripts/lib/skill-evolution/candidate-intake.mjs +270 -0
- package/scripts/lib/skill-evolution/config-validation-gate.mjs +279 -0
- package/scripts/lib/skill-evolution/engine.mjs +719 -0
- package/scripts/lib/skill-evolution/idempotency.mjs +279 -0
- package/scripts/lib/skill-evolution/mr-opener.mjs +507 -0
- package/scripts/lib/skill-health/join.mjs +181 -0
- package/scripts/lib/skill-health/score.mjs +123 -0
- package/scripts/lib/skill-invocations-schema.mjs +214 -0
- package/scripts/lib/skill-judge.mjs +348 -0
- package/scripts/lib/skill-judgments-schema.mjs +264 -0
- package/scripts/lib/slopcheck.mjs +501 -0
- package/scripts/lib/soul-resolve.mjs +118 -0
- package/scripts/lib/spiral-carryover.mjs +495 -0
- package/scripts/lib/state-md/body-sections.mjs +851 -0
- package/scripts/lib/state-md/frontmatter-mutators.mjs +453 -0
- package/scripts/lib/state-md/mission-status.mjs +247 -0
- package/scripts/lib/state-md/recommendations.mjs +57 -0
- package/scripts/lib/state-md/yaml-parser.mjs +234 -0
- package/scripts/lib/state-md-peer-guard.mjs +232 -0
- package/scripts/lib/state-md.mjs +53 -0
- package/scripts/lib/subagents-schema.mjs +309 -0
- package/scripts/lib/sunset/walker.mjs +1192 -0
- package/scripts/lib/test-runner/artifact-paths.mjs +94 -0
- package/scripts/lib/test-runner/fingerprint.mjs +33 -0
- package/scripts/lib/test-runner/issue-reconcile.mjs +770 -0
- package/scripts/lib/tmux-layout/layouts.mjs +224 -0
- package/scripts/lib/tmux-layout/telemetry-stats.mjs +100 -0
- package/scripts/lib/tmux-layout/telemetry.mjs +88 -0
- package/scripts/lib/tmux-layout/tmux-shell.mjs +82 -0
- package/scripts/lib/tmux-layout/vcs-detector.mjs +88 -0
- package/scripts/lib/validate/check-agents.mjs +457 -0
- package/scripts/lib/validate/check-codex-plugin.mjs +37 -0
- package/scripts/lib/validate/check-commands.mjs +148 -0
- package/scripts/lib/validate/check-component-paths.mjs +112 -0
- package/scripts/lib/validate/check-dead-bridge.mjs +180 -0
- package/scripts/lib/validate/check-hooks-symmetry.mjs +258 -0
- package/scripts/lib/validate/check-json-files.mjs +116 -0
- package/scripts/lib/validate/check-owner-leakage.mjs +1011 -0
- package/scripts/lib/validate/check-path-utils-canary.mjs +175 -0
- package/scripts/lib/validate/check-peekaboo-driver-canary.mjs +201 -0
- package/scripts/lib/validate/check-pi-package.mjs +110 -0
- package/scripts/lib/validate/check-pi-prompts.mjs +43 -0
- package/scripts/lib/validate/check-playwright-mcp-canary.mjs +154 -0
- package/scripts/lib/validate/check-plugin-json.mjs +96 -0
- package/scripts/lib/validate/check-plugin-monitors.mjs +206 -0
- package/scripts/lib/validate/check-plugin-schema.mjs +137 -0
- package/scripts/lib/validate/check-rules.mjs +143 -0
- package/scripts/lib/validate/check-session-plan-routing.mjs +154 -0
- package/scripts/lib/validate/check-test-fixture-shapes.mjs +280 -0
- package/scripts/lib/validate/check-unicode-safety.mjs +533 -0
- package/scripts/lib/validate/confidential-names.mjs +169 -0
- package/scripts/lib/validate/dead-bridge-corpus.mjs +141 -0
- package/scripts/lib/validate/dead-bridge-detectors.mjs +568 -0
- package/scripts/lib/validate/tier-inference.mjs +100 -0
- package/scripts/lib/validate-vendored-rules.mjs +519 -0
- package/scripts/lib/vault-archive.mjs +404 -0
- package/scripts/lib/vault-backfill/glab.mjs +164 -0
- package/scripts/lib/vault-backfill/manifest.mjs +75 -0
- package/scripts/lib/vault-backfill/template.mjs +130 -0
- package/scripts/lib/vault-consolidate-fs.mjs +331 -0
- package/scripts/lib/vault-migration-rules.mjs +155 -0
- package/scripts/lib/vault-mirror/auto-commit.mjs +203 -0
- package/scripts/lib/vault-mirror/namespace.mjs +152 -0
- package/scripts/lib/vault-mirror/process.mjs +567 -0
- package/scripts/lib/vault-mirror/pseudonym-map.mjs +164 -0
- package/scripts/lib/vault-mirror/render-learnings.mjs +201 -0
- package/scripts/lib/vault-mirror/render-sessions.mjs +367 -0
- package/scripts/lib/vault-mirror/render.mjs +8 -0
- package/scripts/lib/vault-mirror/utils.mjs +217 -0
- package/scripts/lib/vault-relocation-rules.mjs +555 -0
- package/scripts/lib/vault-repo-backfill.mjs +235 -0
- package/scripts/lib/vault-staleness-banner.mjs +142 -0
- package/scripts/lib/vault-status/board-writer.mjs +769 -0
- package/scripts/lib/vault-status/narrative-mirror.mjs +544 -0
- package/scripts/lib/vault-sync-baseline.mjs +152 -0
- package/scripts/lib/wave-context.mjs +29 -0
- package/scripts/lib/wave-executor/pool.mjs +248 -0
- package/scripts/lib/wave-resource-gate.mjs +204 -0
- package/scripts/lib/wave-sizing.mjs +75 -0
- package/scripts/lib/webhook-url.mjs +105 -0
- package/scripts/lib/workspace.mjs +198 -0
- package/scripts/lib/worktree/constants.mjs +35 -0
- package/scripts/lib/worktree/index.mjs +17 -0
- package/scripts/lib/worktree/lifecycle.mjs +287 -0
- package/scripts/lib/worktree/listing.mjs +118 -0
- package/scripts/lib/worktree/meta.mjs +64 -0
- package/scripts/lib/worktree-freshness.mjs +313 -0
- package/scripts/lib/worktree.mjs +15 -0
- package/scripts/lifecycle-sim-v6.mjs +347 -0
- package/scripts/lock-reaper.mjs +185 -0
- package/scripts/mcp-server.sh +241 -0
- package/scripts/measure-policy-cache-effectiveness.mjs +427 -0
- package/scripts/memory-propose.mjs +464 -0
- package/scripts/migrate-cold-start-seed.mjs +404 -0
- package/scripts/migrate-learnings-jsonl.mjs +189 -0
- package/scripts/migrate-legacy-learnings.sh +61 -0
- package/scripts/migrate-sessions-jsonl.mjs +448 -0
- package/scripts/migrate-subagents-jsonl.mjs +196 -0
- package/scripts/migrate-vault-paths.mjs +796 -0
- package/scripts/parse-config.mjs +149 -0
- package/scripts/pi-install.mjs +117 -0
- package/scripts/print-applicable-rules.mjs +247 -0
- package/scripts/promote-vault-strict.mjs +496 -0
- package/scripts/relocate-vault-corpus.mjs +1178 -0
- package/scripts/run-migrate-v2-cross-repo.mjs +385 -0
- package/scripts/run-quality-gate.mjs +216 -0
- package/scripts/spikes/h3-agent-teams/preflight.sh +53 -0
- package/scripts/spikes/h3-agent-teams/run-h3.sh +112 -0
- package/scripts/spikes/h3-agent-teams/setup.sh +137 -0
- package/scripts/spikes/h3-agent-teams/toggle.sh +38 -0
- package/scripts/sweep-expired-learnings.mjs +135 -0
- package/scripts/sync-vault-schema.mjs +376 -0
- package/scripts/tests/fixtures/fetch-baseline/sample-rule.md +8 -0
- package/scripts/tmux-layout.mjs +245 -0
- package/scripts/token-audit.sh +191 -0
- package/scripts/typecheck.mjs +42 -0
- package/scripts/upload-social-preview.mjs +316 -0
- package/scripts/validate-config.mjs +46 -0
- package/scripts/validate-plugin-manifests.mjs +163 -0
- package/scripts/validate-plugin.mjs +264 -0
- package/scripts/validate-wave-scope.mjs +289 -0
- package/scripts/vault-backfill.mjs +404 -0
- package/scripts/vault-consolidate.mjs +596 -0
- package/scripts/vault-integration-watcher.mjs +394 -0
- package/scripts/vault-mirror.mjs +430 -0
- package/skills/_shared/bootstrap-gate.md +111 -0
- package/skills/_shared/config-reading.md +226 -0
- package/skills/_shared/instruction-file-resolution.md +79 -0
- package/skills/_shared/model-selection.md +64 -0
- package/skills/_shared/monitor-patterns.md +300 -0
- package/skills/_shared/parallel-aware-auq.md +121 -0
- package/skills/_shared/parallel-aware-preamble.md +185 -0
- package/skills/_shared/platform-tools.md +96 -0
- package/skills/_shared/state-ownership.md +221 -0
- package/skills/architecture/DEEPENING.md +37 -0
- package/skills/architecture/INTERFACE-DESIGN.md +44 -0
- package/skills/architecture/LANGUAGE.md +53 -0
- package/skills/architecture/SKILL.md +92 -0
- package/skills/autopilot/SKILL.md +419 -0
- package/skills/bootstrap/SKILL.md +592 -0
- package/skills/bootstrap/STATE.md.template +24 -0
- package/skills/bootstrap/_shared-template.md +243 -0
- package/skills/bootstrap/deep-template.md +659 -0
- package/skills/bootstrap/fast-template.md +251 -0
- package/skills/bootstrap/intensity-heuristic.md +80 -0
- package/skills/bootstrap/public-fallback.md +342 -0
- package/skills/bootstrap/standard-template.md +736 -0
- package/skills/bootstrap/templates/agents/project-code-review.md +18 -0
- package/skills/bootstrap/templates/agents/project-discovery.md +18 -0
- package/skills/bootstrap/templates/agents/project-quality-gate.md +18 -0
- package/skills/brainstorm/SKILL.md +268 -0
- package/skills/brainstorm/soul.md +49 -0
- package/skills/claude-md-drift-check/SKILL.md +186 -0
- package/skills/claude-md-drift-check/checker.mjs +1380 -0
- package/skills/claude-md-drift-check/checker.sh +37 -0
- package/skills/claude-md-drift-check/package.json +16 -0
- package/skills/convergence-monitoring/README.md +39 -0
- package/skills/convergence-monitoring/SIGNALS.md +246 -0
- package/skills/convergence-monitoring/SKILL.md +285 -0
- package/skills/daily/SKILL.md +222 -0
- package/skills/daily/generate.sh +92 -0
- package/skills/daily/templates/daily.md.tpl +36 -0
- package/skills/debug/SKILL.md +188 -0
- package/skills/debug/soul.md +35 -0
- package/skills/discovery/SKILL.md +567 -0
- package/skills/discovery/issue-templates.md +237 -0
- package/skills/discovery/probes/docs-staleness.mjs +195 -0
- package/skills/discovery/probes/frontend-slop.mjs +186 -0
- package/skills/discovery/probes/ssot-code-diff.mjs +310 -0
- package/skills/discovery/probes/supply-chain-slopcheck.mjs +440 -0
- package/skills/discovery/probes/vault-narrative-staleness.mjs +355 -0
- package/skills/discovery/probes/vault-staleness.mjs +272 -0
- package/skills/discovery/probes-arch.md +252 -0
- package/skills/discovery/probes-audit.md +95 -0
- package/skills/discovery/probes-code.md +329 -0
- package/skills/discovery/probes-docs.md +76 -0
- package/skills/discovery/probes-feature.md +150 -0
- package/skills/discovery/probes-infra.md +138 -0
- package/skills/discovery/probes-intro.md +25 -0
- package/skills/discovery/probes-session.md +495 -0
- package/skills/discovery/probes-supply-chain.md +94 -0
- package/skills/discovery/probes-ui.md +147 -0
- package/skills/discovery/probes-vault.md +64 -0
- package/skills/discovery/slop-patterns.md +115 -0
- package/skills/dispatcher/SKILL.md +173 -0
- package/skills/docs-orchestrator/SKILL.md +362 -0
- package/skills/docs-orchestrator/audience-mapping.md +140 -0
- package/skills/domain-model/ADR-FORMAT.md +47 -0
- package/skills/domain-model/CONTEXT-FORMAT.md +77 -0
- package/skills/domain-model/SKILL.md +85 -0
- package/skills/ecosystem-health/SKILL.md +119 -0
- package/skills/ecosystem-health/wizard.md +193 -0
- package/skills/eval/SKILL.md +293 -0
- package/skills/eval/rubric-v1.md +218 -0
- package/skills/evolve/SKILL.md +546 -0
- package/skills/frontmatter-guard/SKILL.md +126 -0
- package/skills/gitlab-ops/SKILL.md +368 -0
- package/skills/gitlab-portfolio/SKILL.md +196 -0
- package/skills/grill/SKILL.md +185 -0
- package/skills/grill/soul.md +55 -0
- package/skills/hook-development/SKILL.md +413 -0
- package/skills/mcp-builder/SKILL.md +260 -0
- package/skills/memory-cleanup/SKILL.md +310 -0
- package/skills/mode-selector/SKILL.md +226 -0
- package/skills/peekaboo-driver/SKILL.md +237 -0
- package/skills/peekaboo-driver/soul.md +32 -0
- package/skills/persona-panel/SKILL.md +365 -0
- package/skills/persona-panel/persona-format.md +205 -0
- package/skills/persona-panel/presets/designer-lens.md +87 -0
- package/skills/persona-panel/presets/engineer-lens.md +88 -0
- package/skills/persona-panel/presets/pm-lens.md +86 -0
- package/skills/plan/SKILL.md +496 -0
- package/skills/plan/mode-feature.md +141 -0
- package/skills/plan/mode-new.md +297 -0
- package/skills/plan/mode-retro.md +271 -0
- package/skills/plan/prd-feature-template.md +132 -0
- package/skills/plan/prd-full-template.md +151 -0
- package/skills/plan/prd-reviewer-prompt.md +103 -0
- package/skills/plan/retro-template.md +75 -0
- package/skills/plan/soul.md +62 -0
- package/skills/playwright-driver/SKILL.md +226 -0
- package/skills/playwright-driver/soul.md +30 -0
- package/skills/quality-gates/SKILL.md +212 -0
- package/skills/reconcile/SKILL.md +324 -0
- package/skills/repo-audit/SKILL.md +272 -0
- package/skills/session-end/SKILL.md +1044 -0
- package/skills/session-end/discovery-scan.md +37 -0
- package/skills/session-end/drift-operations.md +97 -0
- package/skills/session-end/learning-patterns.md +78 -0
- package/skills/session-end/metrics-collection.md +175 -0
- package/skills/session-end/phase-3-2-docs-verification.md +148 -0
- package/skills/session-end/phase-3-6-tail.md +344 -0
- package/skills/session-end/phase-3-7a-recommendations.md +86 -0
- package/skills/session-end/plan-verification.md +288 -0
- package/skills/session-end/session-metrics-write.md +223 -0
- package/skills/session-end/vault-operations.md +50 -0
- package/skills/session-end/verification-checklist.md +20 -0
- package/skills/session-plan/SKILL.md +554 -0
- package/skills/session-plan/wave-template.md +37 -0
- package/skills/session-start/SKILL.md +1043 -0
- package/skills/session-start/phase-2-5-docs-planning.md +119 -0
- package/skills/session-start/phase-4-5-resource-health.md +49 -0
- package/skills/session-start/phase-7-1-premise-check.md +47 -0
- package/skills/session-start/phase-7-5-mode-selector.md +237 -0
- package/skills/session-start/phase-8-5-express-path.md +61 -0
- package/skills/session-start/presentation-format.md +81 -0
- package/skills/session-start/soul.md +57 -0
- package/skills/skill-creator/SKILL.md +168 -0
- package/skills/spinout/SKILL.md +76 -0
- package/skills/sunset-review/SKILL.md +96 -0
- package/skills/test-runner/SKILL.md +362 -0
- package/skills/test-runner/rubric-v1.md +388 -0
- package/skills/test-runner/soul.md +46 -0
- package/skills/tmux-layout/SKILL.md +104 -0
- package/skills/ubiquitous-language/SKILL.md +97 -0
- package/skills/using-orchestrator/SKILL.md +144 -0
- package/skills/vault-mirror/SKILL.md +234 -0
- package/skills/vault-sync/SKILL.md +319 -0
- package/skills/vault-sync/package-lock.json +40 -0
- package/skills/vault-sync/package.json +11 -0
- package/skills/vault-sync/tests/fixtures/archive-test-vault/90-archive/bad-archived.md +8 -0
- package/skills/vault-sync/tests/fixtures/archive-test-vault/_meta/.gitkeep +0 -0
- package/skills/vault-sync/tests/fixtures/archive-test-vault/live-note.md +8 -0
- package/skills/vault-sync/tests/fixtures/broken-frontmatter-vault/_meta/.gitkeep +0 -0
- package/skills/vault-sync/tests/fixtures/broken-frontmatter-vault/bad-type.md +8 -0
- package/skills/vault-sync/tests/fixtures/broken-frontmatter-vault/good-note.md +8 -0
- package/skills/vault-sync/tests/fixtures/clean-vault/.obsidian/config.md +8 -0
- package/skills/vault-sync/tests/fixtures/clean-vault/01-projects/foo/projects-baseline.md +10 -0
- package/skills/vault-sync/tests/fixtures/clean-vault/03-daily/daily-2026-04-13.md +8 -0
- package/skills/vault-sync/tests/fixtures/clean-vault/README.md +3 -0
- package/skills/vault-sync/tests/fixtures/clean-vault/hello-world.md +11 -0
- package/skills/vault-sync/tests/fixtures/dangling-link-vault/_meta/.gitkeep +0 -0
- package/skills/vault-sync/tests/fixtures/dangling-link-vault/has-dangling.md +9 -0
- package/skills/vault-sync/tests/fixtures/dangling-link-vault/real-target.md +8 -0
- package/skills/vault-sync/tests/fixtures/empty-vault/_meta/.gitkeep +0 -0
- package/skills/vault-sync/tests/fixtures/missing-field-vault/_meta/.gitkeep +0 -0
- package/skills/vault-sync/tests/fixtures/missing-field-vault/missing-id.md +7 -0
- package/skills/vault-sync/tests/fixtures/nested-tag-vault/03-daily/daily-2026-04-13.md +9 -0
- package/skills/vault-sync/tests/fixtures/nested-tag-vault/_meta/.gitkeep +0 -0
- package/skills/vault-sync/tests/fixtures/nested-tag-vault/nested-tags-note.md +11 -0
- package/skills/vault-sync/tests/fixtures/no-frontmatter-vault/README.md +3 -0
- package/skills/vault-sync/tests/fixtures/no-frontmatter-vault/_MOC.md +3 -0
- package/skills/vault-sync/tests/fixtures/no-frontmatter-vault/_meta/.gitkeep +0 -0
- package/skills/vault-sync/tests/fixtures/with-moc-vault/_MOC.md +11 -0
- package/skills/vault-sync/tests/fixtures/with-moc-vault/_meta/.gitkeep +0 -0
- package/skills/vault-sync/tests/fixtures/with-moc-vault/hello-world.md +11 -0
- package/skills/vault-sync/tests/schema-drift.test.mjs +133 -0
- package/skills/vault-sync/validator.mjs +658 -0
- package/skills/vault-sync/validator.sh +55 -0
- package/skills/wave-executor/SKILL.md +496 -0
- package/skills/wave-executor/circuit-breaker.md +169 -0
- package/skills/wave-executor/wave-loop.md +1043 -0
- package/skills/write-executable-plan/SKILL.md +237 -0
- package/skills/write-executable-plan/plan-template.md +154 -0
- package/templates/_minimal/CLAUDE.md.tmpl +41 -0
- package/templates/_minimal/README.md.tmpl +15 -0
- package/templates/_minimal/gitignore.tmpl +47 -0
- package/templates/_shared/harte-regeln.md +16 -0
- package/templates/_shared/loop.md +90 -0
- package/templates/_shared/rules/parallel-sessions.md +77 -0
- package/templates/nextjs-minimal/README.md +30 -0
- package/templates/nextjs-minimal/app/layout.tsx +18 -0
- package/templates/nextjs-minimal/app/page.tsx +7 -0
- package/templates/nextjs-minimal/eslint.config.mjs +16 -0
- package/templates/nextjs-minimal/next.config.mjs +4 -0
- package/templates/nextjs-minimal/package.json +27 -0
- package/templates/nextjs-minimal/tsconfig.json +23 -0
- package/templates/node-minimal/README.md +33 -0
- package/templates/node-minimal/eslint.config.mjs +10 -0
- package/templates/node-minimal/package.json +21 -0
- package/templates/node-minimal/src/index.ts +1 -0
- package/templates/node-minimal/tests/sanity.test.ts +5 -0
- package/templates/node-minimal/tsconfig.json +17 -0
- package/templates/personas/README.md +150 -0
- package/templates/personas/accounting-compliance.v1.md +120 -0
- package/templates/personas/accounting-tax-advisor.v1.md +116 -0
- package/templates/personas/buyer-p1-cto.v1.md +125 -0
- package/templates/personas/buyer-p2-kanzlei.v1.md +134 -0
- package/templates/personas/buyer-p3-build.v1.md +130 -0
- package/templates/personas/buyer-p4-tech-veto.v1.md +130 -0
- package/templates/personas/buyer-p5-solo.v1.md +132 -0
- package/templates/personas/buyer-p6-ld.v1.md +130 -0
- package/templates/personas/klima-ai-expert.v1.md +114 -0
- package/templates/personas/klima-physicist.v1.md +117 -0
- package/templates/python-uv/README.md +28 -0
- package/templates/python-uv/pyproject.toml +32 -0
- package/templates/python-uv/src/__PROJECT_NAME__/__init__.py +0 -0
- package/templates/python-uv/src/__PROJECT_NAME__/main.py +6 -0
- package/templates/python-uv/tests/test_sanity.py +2 -0
- package/templates/static-html/README.md +19 -0
- package/templates/static-html/index.html +15 -0
- package/templates/static-html/script.js +1 -0
- package/templates/static-html/styles.css +26 -0
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* writer.mjs — FA3 writer seam for the Reconciliation Engine (Epic #693, issue #696).
|
|
3
|
+
*
|
|
4
|
+
* Persists APPROVED reconciliation rule proposals to `.claude/rules/` AFTER
|
|
5
|
+
* operator approval. This is the one and only module that writes `.claude/rules/`
|
|
6
|
+
* on behalf of the engine — the FA2 engine/renderer NEVER touch the filesystem
|
|
7
|
+
* for rule files.
|
|
8
|
+
*
|
|
9
|
+
* Responsibilities:
|
|
10
|
+
* - Acquire a per-write file lock (`.orchestrator/rules.lock`) to serialise
|
|
11
|
+
* concurrent writers — mirrors PSA-005 (withStateMdLock) pattern.
|
|
12
|
+
* - For each approved proposal: path-safety guard → mkdirSync → atomic tmp+rename write.
|
|
13
|
+
* - For each rejected proposal: JSONL-append to `.orchestrator/reconcile.rejected.log`.
|
|
14
|
+
* - Never throws — all failures are collected into errors[] and returned.
|
|
15
|
+
*
|
|
16
|
+
* Path-safety:
|
|
17
|
+
* - `validatePathInsideProject(item.path, repoRoot, {canonicalizeRoot:true})` is
|
|
18
|
+
* the primary guard (two-phase lexical + realpath, CWE-22 defence).
|
|
19
|
+
* - Additional assertion: resolved path must be inside `<repoRoot>/.claude/rules/`.
|
|
20
|
+
* - Both guards must pass; failure skips the record and pushes an error string.
|
|
21
|
+
*
|
|
22
|
+
* Atomic write strategy (rule files):
|
|
23
|
+
* - Write content to `<target>.XXXXXXXX.tmp` via `writeFileSync`, then
|
|
24
|
+
* `renameSync` over the final path. Same-filesystem rename is atomic on POSIX,
|
|
25
|
+
* so the rule file is never partially visible. Mirrors idempotency.mjs pattern.
|
|
26
|
+
*
|
|
27
|
+
* JSONL append strategy (rejected log):
|
|
28
|
+
* - `appendFileSync` for the rejected log — each record is a self-contained line,
|
|
29
|
+
* and POSIX append (O_APPEND) is atomic for writes < PIPE_BUF (4096 bytes),
|
|
30
|
+
* which every JSONL record satisfies. Mirrors memory-proposals/sink.mjs pattern.
|
|
31
|
+
*
|
|
32
|
+
* DI-friendly: accepts repoRoot as a parameter; no global cwd assumptions.
|
|
33
|
+
*
|
|
34
|
+
* Plain Node ESM, no external deps — Node 20+ stdlib + sibling scripts/lib only.
|
|
35
|
+
*
|
|
36
|
+
* Part of Epic #693 → issue #696 (FA3 Advisory Delivery).
|
|
37
|
+
*
|
|
38
|
+
* @module reconcile/writer
|
|
39
|
+
*/
|
|
40
|
+
|
|
41
|
+
import { mkdirSync, writeFileSync, renameSync, appendFileSync, realpathSync } from 'node:fs';
|
|
42
|
+
import { randomBytes } from 'node:crypto';
|
|
43
|
+
import path from 'node:path';
|
|
44
|
+
|
|
45
|
+
import { withFileLock } from '../file-lock.mjs';
|
|
46
|
+
import { validatePathInsideProject } from '../path-utils.mjs';
|
|
47
|
+
|
|
48
|
+
// ---------------------------------------------------------------------------
|
|
49
|
+
// Path constants (relative to repoRoot)
|
|
50
|
+
// ---------------------------------------------------------------------------
|
|
51
|
+
|
|
52
|
+
/** Lock file that serialises concurrent rule-write operations. */
|
|
53
|
+
const RULES_LOCK_REL = path.join('.orchestrator', 'rules.lock');
|
|
54
|
+
|
|
55
|
+
/** Directory where auto-generated rule files are written (repo-relative). */
|
|
56
|
+
const RULES_DIR_REL = path.join('.claude', 'rules');
|
|
57
|
+
|
|
58
|
+
/** Rejected-proposals log for rules declined by the operator (repo-relative). */
|
|
59
|
+
const REJECTED_LOG_REL = path.join('.orchestrator', 'reconcile.rejected.log');
|
|
60
|
+
|
|
61
|
+
// ---------------------------------------------------------------------------
|
|
62
|
+
// Internal helpers
|
|
63
|
+
// ---------------------------------------------------------------------------
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Resolve the absolute rules directory for the given repoRoot. Used both for
|
|
67
|
+
* the per-proposal assertion and for mkdirSync.
|
|
68
|
+
*
|
|
69
|
+
* @param {string} repoRoot
|
|
70
|
+
* @returns {string}
|
|
71
|
+
*/
|
|
72
|
+
function rulesAbsDir(repoRoot) {
|
|
73
|
+
return path.resolve(repoRoot, RULES_DIR_REL);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Write `content` to `destPath` atomically via tmp+rename.
|
|
78
|
+
*
|
|
79
|
+
* Uses a random 8-hex-char suffix for the tmp file to avoid collisions when
|
|
80
|
+
* multiple proposals write to the same directory concurrently (defensive;
|
|
81
|
+
* under the lock this should not happen, but the pattern is cheap).
|
|
82
|
+
*
|
|
83
|
+
* Throws on filesystem errors — callers must catch.
|
|
84
|
+
*
|
|
85
|
+
* @param {string} destPath - absolute path of the target rule file.
|
|
86
|
+
* @param {string} content - UTF-8 text content to write.
|
|
87
|
+
*/
|
|
88
|
+
function writeTextAtomic(destPath, content) {
|
|
89
|
+
const dir = path.dirname(destPath);
|
|
90
|
+
mkdirSync(dir, { recursive: true });
|
|
91
|
+
const suffix = randomBytes(4).toString('hex');
|
|
92
|
+
const tmpPath = `${destPath}.${suffix}.tmp`;
|
|
93
|
+
writeFileSync(tmpPath, content, 'utf8');
|
|
94
|
+
renameSync(tmpPath, destPath);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// ---------------------------------------------------------------------------
|
|
98
|
+
// Public API
|
|
99
|
+
// ---------------------------------------------------------------------------
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* @typedef {Object} WriterApprovedItem
|
|
103
|
+
* @property {string} slug - kebab-case slug (from renderer.mjs / engine.mjs).
|
|
104
|
+
* @property {string} path - repo-relative rule path (`.claude/rules/<slug>.md`).
|
|
105
|
+
* @property {string} content - full rendered markdown content.
|
|
106
|
+
* @property {string} [learningKey]
|
|
107
|
+
* @property {number} [confidence]
|
|
108
|
+
* @property {string} [candidateId]
|
|
109
|
+
* @property {string} [status]
|
|
110
|
+
*/
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* @typedef {Object} WriterRejectedItem
|
|
114
|
+
* @property {string} [learningKey]
|
|
115
|
+
* @property {string} [type]
|
|
116
|
+
* @property {string} [reason]
|
|
117
|
+
* @property {string} [status]
|
|
118
|
+
*/
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* @typedef {Object} WriteApprovedRulesResult
|
|
122
|
+
* @property {number} written - number of rule files successfully written.
|
|
123
|
+
* @property {number} archived - number of rejected records appended to the log.
|
|
124
|
+
* @property {string[]} errors - per-item error strings (never fatal).
|
|
125
|
+
*/
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Persist approved reconciliation rule proposals to `.claude/rules/` and
|
|
129
|
+
* archive rejected proposals to the rejected log.
|
|
130
|
+
*
|
|
131
|
+
* NEVER throws — all per-item failures are collected into `errors[]`.
|
|
132
|
+
*
|
|
133
|
+
* @param {Object} opts
|
|
134
|
+
* @param {WriterApprovedItem[]} opts.approved - proposals approved by the operator.
|
|
135
|
+
* @param {WriterRejectedItem[]} [opts.rejected] - proposals declined by the operator.
|
|
136
|
+
* @param {string} opts.repoRoot - absolute repo root path.
|
|
137
|
+
* @param {string} [opts.sessionId] - current session id (informational; unused in v1).
|
|
138
|
+
* @returns {Promise<WriteApprovedRulesResult>}
|
|
139
|
+
*/
|
|
140
|
+
export async function writeApprovedRules({ approved, rejected = [], repoRoot, sessionId: _sessionId }) {
|
|
141
|
+
// Defensive: coerce inputs
|
|
142
|
+
const approvedItems = Array.isArray(approved) ? approved : [];
|
|
143
|
+
const rejectedItems = Array.isArray(rejected) ? rejected : [];
|
|
144
|
+
|
|
145
|
+
if (approvedItems.length === 0 && rejectedItems.length === 0) {
|
|
146
|
+
return { written: 0, archived: 0, errors: [] };
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const lockPath = path.join(repoRoot, RULES_LOCK_REL);
|
|
150
|
+
|
|
151
|
+
// Ensure .orchestrator dir exists so lock acquisition can create the lock file.
|
|
152
|
+
try {
|
|
153
|
+
mkdirSync(path.dirname(lockPath), { recursive: true });
|
|
154
|
+
} catch {
|
|
155
|
+
// Non-fatal: withFileLock will surface the fs-error if the lock cannot be created.
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Acquire the rules lock — serialises concurrent rule writes.
|
|
159
|
+
const lockResult = await withFileLock(
|
|
160
|
+
lockPath,
|
|
161
|
+
async () => {
|
|
162
|
+
/** @type {string[]} */
|
|
163
|
+
const errors = [];
|
|
164
|
+
let written = 0;
|
|
165
|
+
let archived = 0;
|
|
166
|
+
|
|
167
|
+
const rulesDir = rulesAbsDir(repoRoot);
|
|
168
|
+
|
|
169
|
+
// Parent-directory symlink hardening (#697 security follow-up): if
|
|
170
|
+
// `.claude/rules/` is itself a pre-planted symlink to a directory outside
|
|
171
|
+
// the repo, a lexically-safe leaf path would still be written through it.
|
|
172
|
+
// Resolve the directory's realpath once and refuse all writes if it
|
|
173
|
+
// escapes the canonical repo root. Requires local FS write access to
|
|
174
|
+
// exploit (below the VCS trust boundary) but the guard is one cheap call.
|
|
175
|
+
let rulesDirSafe = true;
|
|
176
|
+
try {
|
|
177
|
+
mkdirSync(rulesDir, { recursive: true });
|
|
178
|
+
let canonRoot = repoRoot;
|
|
179
|
+
try { canonRoot = realpathSync(repoRoot); } catch { /* fall back to lexical */ }
|
|
180
|
+
const expectedRulesDir = path.resolve(canonRoot, RULES_DIR_REL);
|
|
181
|
+
const realRulesDir = realpathSync(rulesDir);
|
|
182
|
+
if (realRulesDir !== expectedRulesDir && !realRulesDir.startsWith(expectedRulesDir + path.sep)) {
|
|
183
|
+
errors.push('path-confinement: .claude/rules/ resolves outside the repo (symlinked dir) — all approved writes skipped');
|
|
184
|
+
rulesDirSafe = false;
|
|
185
|
+
}
|
|
186
|
+
} catch { /* mkdir/realpath failure — per-item writes will surface errors normally */ }
|
|
187
|
+
|
|
188
|
+
// ── Step 1: write approved rule files ──────────────────────────────────
|
|
189
|
+
for (const item of approvedItems) {
|
|
190
|
+
if (!rulesDirSafe) break;
|
|
191
|
+
// Guard: item must have a path string
|
|
192
|
+
if (!item || typeof item.path !== 'string' || item.path.length === 0) {
|
|
193
|
+
errors.push(`approved item missing path: ${JSON.stringify(item)}`);
|
|
194
|
+
continue;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// Primary path-safety guard (two-phase lexical + realpath, CWE-22 defence)
|
|
198
|
+
const pathResult = validatePathInsideProject(item.path, repoRoot, { canonicalizeRoot: true });
|
|
199
|
+
if (!pathResult.ok) {
|
|
200
|
+
errors.push(`path-safety (${pathResult.reason}): "${item.path}" — skipped`);
|
|
201
|
+
continue;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// Resolve the absolute write target from the validated lexical path
|
|
205
|
+
const absPath = pathResult.realPath ?? pathResult.lexicalPath;
|
|
206
|
+
|
|
207
|
+
// Defense-in-depth: assert the resolved path is inside .claude/rules/.
|
|
208
|
+
// Canonicalize repoRoot the same way validatePathInsideProject does
|
|
209
|
+
// (opts.canonicalizeRoot:true) so the prefix check is consistent on
|
|
210
|
+
// platforms where os.tmpdir() has a symlink (e.g. macOS /var → /private/var).
|
|
211
|
+
let canonRoot = repoRoot;
|
|
212
|
+
try { canonRoot = realpathSync(repoRoot); } catch { /* ENOENT/EACCES: fall back to lexical */ }
|
|
213
|
+
const resolvedRulesDir = path.resolve(canonRoot, RULES_DIR_REL);
|
|
214
|
+
if (!absPath.startsWith(resolvedRulesDir + path.sep) && absPath !== resolvedRulesDir) {
|
|
215
|
+
errors.push(`path-confinement: "${item.path}" resolves outside .claude/rules/ — skipped`);
|
|
216
|
+
continue;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// Guard: content must be a string
|
|
220
|
+
if (typeof item.content !== 'string') {
|
|
221
|
+
errors.push(`approved item "${item.path}" has non-string content — skipped`);
|
|
222
|
+
continue;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// Ensure .claude/rules/ exists and write atomically
|
|
226
|
+
try {
|
|
227
|
+
mkdirSync(rulesDir, { recursive: true });
|
|
228
|
+
writeTextAtomic(absPath, item.content);
|
|
229
|
+
written++;
|
|
230
|
+
} catch (err) {
|
|
231
|
+
const msg = err && err.message ? err.message : String(err);
|
|
232
|
+
errors.push(`write failed "${item.path}": ${msg}`);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// ── Step 2: archive rejected proposals to the rejected log ─────────────
|
|
237
|
+
if (rejectedItems.length > 0) {
|
|
238
|
+
const rejectedLogRelPath = REJECTED_LOG_REL;
|
|
239
|
+
const logResult = validatePathInsideProject(rejectedLogRelPath, repoRoot, { canonicalizeRoot: true });
|
|
240
|
+
|
|
241
|
+
if (!logResult.ok) {
|
|
242
|
+
errors.push(`path-safety (rejected log): ${logResult.reason} (${rejectedLogRelPath})`);
|
|
243
|
+
} else {
|
|
244
|
+
const rejectedLogPath = logResult.realPath ?? logResult.lexicalPath;
|
|
245
|
+
|
|
246
|
+
// Ensure parent directory exists
|
|
247
|
+
try {
|
|
248
|
+
mkdirSync(path.dirname(rejectedLogPath), { recursive: true });
|
|
249
|
+
} catch (err) {
|
|
250
|
+
const msg = err && err.message ? err.message : String(err);
|
|
251
|
+
errors.push(`mkdir failed (rejected log): ${msg}`);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
const rejectedAt = new Date().toISOString();
|
|
255
|
+
|
|
256
|
+
for (const item of rejectedItems) {
|
|
257
|
+
try {
|
|
258
|
+
const archiveRecord = {
|
|
259
|
+
...item,
|
|
260
|
+
_rejected_reason: (item && typeof item.reason === 'string' && item.reason !== '')
|
|
261
|
+
? item.reason
|
|
262
|
+
: 'user-declined',
|
|
263
|
+
_rejected_at: rejectedAt,
|
|
264
|
+
};
|
|
265
|
+
const line = JSON.stringify(archiveRecord) + '\n';
|
|
266
|
+
appendFileSync(rejectedLogPath, line, 'utf8');
|
|
267
|
+
archived++;
|
|
268
|
+
} catch (err) {
|
|
269
|
+
const msg = err && err.message ? err.message : String(err);
|
|
270
|
+
const key = (item && item.learningKey) ? item.learningKey : '<unknown>';
|
|
271
|
+
errors.push(`archive failed "${key}": ${msg}`);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
return { written, archived, errors };
|
|
278
|
+
},
|
|
279
|
+
{ timeoutMs: 10000 },
|
|
280
|
+
);
|
|
281
|
+
|
|
282
|
+
// If lock acquisition failed, return a zeroed result with the lock error.
|
|
283
|
+
if (lockResult.ok === false) {
|
|
284
|
+
return {
|
|
285
|
+
written: 0,
|
|
286
|
+
archived: 0,
|
|
287
|
+
errors: [`lock-${lockResult.reason ?? 'unknown'}`],
|
|
288
|
+
};
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// Unwrap the result returned from inside the lock body.
|
|
292
|
+
return lockResult.value;
|
|
293
|
+
}
|
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* reconcile-nudge-banner.mjs — #723 (Epic #723 B1, Flaggschiff-Konvergenz 11).
|
|
3
|
+
*
|
|
4
|
+
* Deterministic session-start nudge for the Learning→Rule reconciliation
|
|
5
|
+
* pipeline. Fleet finding: `reconcile.enabled` defaults to `false` (silent
|
|
6
|
+
* no-op) and `/reconcile` is on-demand-only — repos with 131/123/101
|
|
7
|
+
* learnings on disk had ZERO reconcile runs, because nothing ever told the
|
|
8
|
+
* operator the corpus had accumulated enough evidence to be worth converting.
|
|
9
|
+
* This probe closes that gap the same way `loop-readiness-banner.mjs` (#633)
|
|
10
|
+
* closes the "no .claude/loop.md" gap: a single `checkXxx({repoRoot})` entry
|
|
11
|
+
* point that returns `null` (silent no-op) or `{severity:'warn', message}`.
|
|
12
|
+
*
|
|
13
|
+
* Deliberately introduces NO new Session Config key — per the #723 scope note,
|
|
14
|
+
* an advisory-only banner needs no on/off switch of its own (mirrors
|
|
15
|
+
* `ci-status-banner.mjs` / `qg-command-drift-banner.mjs` / `loop-readiness-banner.mjs`,
|
|
16
|
+
* none of which carry a dedicated enable flag). The EXISTING `reconcile.enabled`
|
|
17
|
+
* key is read only to decide whether to append an informational parenthetical —
|
|
18
|
+
* never to gate the probe itself.
|
|
19
|
+
*
|
|
20
|
+
* "Last reconcile run" provenance (researched, not guessed): the reconciliation
|
|
21
|
+
* engine (`scripts/lib/reconcile/engine.mjs` `runReconcile`) writes exactly ONE
|
|
22
|
+
* disk artifact per run — the idempotency sidecar at
|
|
23
|
+
* `.orchestrator/runtime/reconcile-candidates.jsonl` (owned by
|
|
24
|
+
* `scripts/lib/reconcile/idempotency.mjs`). Every learning present at a given
|
|
25
|
+
* run (eligible OR rejected) gets a candidate row, so:
|
|
26
|
+
* - the MAX `created_at` across `loadCandidates({repoRoot})` is the most
|
|
27
|
+
* recent reconcile run's timestamp (or `null` when the store is empty —
|
|
28
|
+
* i.e. no run on record, matching the fleet finding above).
|
|
29
|
+
* - the candidate COUNT is a reasonable proxy for "how many learnings had
|
|
30
|
+
* been seen as of the last run" (dedup is by `learning_key`, so it is a
|
|
31
|
+
* high-water mark across all runs to date), used for the "new learnings
|
|
32
|
+
* since last run" delta heuristic.
|
|
33
|
+
* There is no separate `reconcile`-scoped event in `.orchestrator/metrics/events.jsonl`
|
|
34
|
+
* — the idempotency sidecar IS the durable run record, so this module reads
|
|
35
|
+
* that directly rather than re-deriving a signal from the generic event log.
|
|
36
|
+
*
|
|
37
|
+
* Reuses the existing learnings-corpus helpers rather than re-implementing
|
|
38
|
+
* filter logic:
|
|
39
|
+
* - `readLearnings` (`./learnings/io.mjs`) — normalized entries (files→file_paths
|
|
40
|
+
* dialect rename applied), used for the eligibility count.
|
|
41
|
+
* - `surfaceTopN` (`./learnings/surface.mjs`) — the canonical active-learning
|
|
42
|
+
* filter (confidence > floor, not expired); called with a large cap to get
|
|
43
|
+
* the full active set rather than a top-N slice.
|
|
44
|
+
* - `filterEligible` (`./reconcile/eligibility.mjs`) — the SAME type/file_paths
|
|
45
|
+
* allow-list gate the reconcile engine itself uses (deliberately NOT
|
|
46
|
+
* confidence-gated — mirrors `runReconcile`'s own posture, see
|
|
47
|
+
* `skills/reconcile/SKILL.md` Phase 2.2).
|
|
48
|
+
*
|
|
49
|
+
* Plain-JS — no Zod dependency. Never throws. `computeReconcileNudge` always
|
|
50
|
+
* returns the full shape (never null); `checkReconcileNudge` returns the
|
|
51
|
+
* session-start banner object or null.
|
|
52
|
+
*
|
|
53
|
+
* Cross-references:
|
|
54
|
+
* - `.claude/rules/verification-before-completion.md` — evidence-before-claims.
|
|
55
|
+
* - `scripts/lib/loop-readiness-banner.mjs` — the banner-shape template.
|
|
56
|
+
* - `scripts/lib/reconcile/engine.mjs` / `idempotency.mjs` / `eligibility.mjs`.
|
|
57
|
+
* - `scripts/lib/learnings/io.mjs` / `surface.mjs`.
|
|
58
|
+
* - `scripts/lib/config/reconcile.mjs` (`_parseReconcile`) — existing config key.
|
|
59
|
+
* - `skills/session-start/SKILL.md` Phase 4 — banner render site.
|
|
60
|
+
* - Issue #723.
|
|
61
|
+
*/
|
|
62
|
+
|
|
63
|
+
import { join } from 'node:path';
|
|
64
|
+
|
|
65
|
+
import { readLearnings } from './learnings/io.mjs';
|
|
66
|
+
import { surfaceTopN } from './learnings/surface.mjs';
|
|
67
|
+
import { filterEligible } from './reconcile/eligibility.mjs';
|
|
68
|
+
import { loadCandidates } from './reconcile/idempotency.mjs';
|
|
69
|
+
import { readConfigFile } from './config/io.mjs';
|
|
70
|
+
import { _parseReconcile } from './config/reconcile.mjs';
|
|
71
|
+
|
|
72
|
+
/** Repo-relative location of the learnings corpus (mirrors engine.mjs's own constant — not exported there). */
|
|
73
|
+
const LEARNINGS_PATH = '.orchestrator/metrics/learnings.jsonl';
|
|
74
|
+
|
|
75
|
+
/** Nudge threshold (a): active-learning count with no reconcile run on record. */
|
|
76
|
+
export const NUDGE_MIN_LEARNINGS = 20;
|
|
77
|
+
|
|
78
|
+
/** Nudge threshold (b): new learnings accrued since the last determinable reconcile run. */
|
|
79
|
+
export const NUDGE_MIN_DELTA = 15;
|
|
80
|
+
|
|
81
|
+
/** Nudge threshold (c): rule-eligible learnings (type + file_paths allow-list), regardless of confidence. */
|
|
82
|
+
export const NUDGE_MIN_ELIGIBLE = 3;
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Resolve the max `created_at` across reconcile-candidate sidecar records —
|
|
86
|
+
* the most recent reconcile run's timestamp, or `null` when no run is on
|
|
87
|
+
* record (empty store, missing file, or no parseable timestamp).
|
|
88
|
+
*
|
|
89
|
+
* @param {Array<{created_at?: unknown}>} candidates
|
|
90
|
+
* @returns {string|null} ISO 8601 timestamp, or null
|
|
91
|
+
*/
|
|
92
|
+
function _lastRunAt(candidates) {
|
|
93
|
+
if (!Array.isArray(candidates) || candidates.length === 0) return null;
|
|
94
|
+
let maxMs = -Infinity;
|
|
95
|
+
for (const c of candidates) {
|
|
96
|
+
const t = c && typeof c.created_at === 'string' ? Date.parse(c.created_at) : NaN;
|
|
97
|
+
if (Number.isFinite(t) && t > maxMs) maxMs = t;
|
|
98
|
+
}
|
|
99
|
+
return Number.isFinite(maxMs) && maxMs > -Infinity ? new Date(maxMs).toISOString() : null;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Pure computation — always returns the full shape (never null, never throws).
|
|
104
|
+
* Touches disk only via the injected/default learnings + candidates readers,
|
|
105
|
+
* all of which are themselves never-throwing.
|
|
106
|
+
*
|
|
107
|
+
* @param {object} [opts]
|
|
108
|
+
* @param {string} [opts.repoRoot] — project root (defaults to process.cwd()).
|
|
109
|
+
* @param {Date|number} [opts.now] — injectable clock, forwarded to the active-learning filter.
|
|
110
|
+
* @returns {Promise<{
|
|
111
|
+
* totalLearnings: number,
|
|
112
|
+
* activeLearnings: number,
|
|
113
|
+
* eligibleCount: number,
|
|
114
|
+
* lastRunAt: string|null,
|
|
115
|
+
* lastRunCandidateCount: number,
|
|
116
|
+
* delta: number,
|
|
117
|
+
* nudge: boolean,
|
|
118
|
+
* reasons: string[],
|
|
119
|
+
* }>}
|
|
120
|
+
*/
|
|
121
|
+
export async function computeReconcileNudge(opts = {}) {
|
|
122
|
+
const empty = {
|
|
123
|
+
totalLearnings: 0,
|
|
124
|
+
activeLearnings: 0,
|
|
125
|
+
eligibleCount: 0,
|
|
126
|
+
lastRunAt: null,
|
|
127
|
+
lastRunCandidateCount: 0,
|
|
128
|
+
delta: 0,
|
|
129
|
+
nudge: false,
|
|
130
|
+
reasons: [],
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
const repoRoot = typeof opts.repoRoot === 'string' && opts.repoRoot.length > 0 ? opts.repoRoot : process.cwd();
|
|
134
|
+
const learningsPath = join(repoRoot, LEARNINGS_PATH);
|
|
135
|
+
|
|
136
|
+
/** @type {object[]} */
|
|
137
|
+
let entries;
|
|
138
|
+
try {
|
|
139
|
+
const result = await readLearnings(learningsPath);
|
|
140
|
+
entries = Array.isArray(result?.entries) ? result.entries : [];
|
|
141
|
+
} catch {
|
|
142
|
+
return empty; // fail-open — never throw out of the probe
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Silent no-op: missing file, empty file, or all-malformed lines all
|
|
146
|
+
// collapse to entries.length === 0 here.
|
|
147
|
+
if (entries.length === 0) return empty;
|
|
148
|
+
|
|
149
|
+
/** @type {object[]} */
|
|
150
|
+
let active;
|
|
151
|
+
try {
|
|
152
|
+
// Reuse surfaceTopN's active-filter (confidence > floor, not expired)
|
|
153
|
+
// uncapped — pass a large `n` to get the FULL active set, not a top slice.
|
|
154
|
+
active = await surfaceTopN(learningsPath, Number.MAX_SAFE_INTEGER, { now: opts.now });
|
|
155
|
+
} catch {
|
|
156
|
+
active = [];
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if (active.length === 0) return empty;
|
|
160
|
+
|
|
161
|
+
let eligibleCount;
|
|
162
|
+
try {
|
|
163
|
+
eligibleCount = filterEligible(entries).eligible.length;
|
|
164
|
+
} catch {
|
|
165
|
+
eligibleCount = 0;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/** @type {Array<Record<string, unknown>>} */
|
|
169
|
+
let candidates;
|
|
170
|
+
try {
|
|
171
|
+
candidates = loadCandidates({ repoRoot });
|
|
172
|
+
} catch {
|
|
173
|
+
candidates = [];
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const lastRunAt = _lastRunAt(candidates);
|
|
177
|
+
const lastRunCandidateCount = Array.isArray(candidates) ? candidates.length : 0;
|
|
178
|
+
const delta = entries.length - lastRunCandidateCount;
|
|
179
|
+
|
|
180
|
+
const reasons = [];
|
|
181
|
+
// (a) — plenty of active learnings, but no reconcile run has ever recorded them.
|
|
182
|
+
if (active.length >= NUDGE_MIN_LEARNINGS && lastRunAt === null) {
|
|
183
|
+
reasons.push(`${active.length} active learnings with no reconcile run on record`);
|
|
184
|
+
}
|
|
185
|
+
// (b) — a determinable prior run exists, and the corpus has grown meaningfully since.
|
|
186
|
+
if (lastRunAt !== null && delta > NUDGE_MIN_DELTA) {
|
|
187
|
+
reasons.push(`${delta} new learnings since the last reconcile run`);
|
|
188
|
+
}
|
|
189
|
+
// (c) — enough rule-eligible learnings to be worth a batch, independent of confidence.
|
|
190
|
+
if (eligibleCount >= NUDGE_MIN_ELIGIBLE) {
|
|
191
|
+
reasons.push(`${eligibleCount} rule-eligible learnings`);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
return {
|
|
195
|
+
totalLearnings: entries.length,
|
|
196
|
+
activeLearnings: active.length,
|
|
197
|
+
eligibleCount,
|
|
198
|
+
lastRunAt,
|
|
199
|
+
lastRunCandidateCount,
|
|
200
|
+
delta,
|
|
201
|
+
nudge: reasons.length > 0,
|
|
202
|
+
reasons,
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Best-effort extraction of `reconcile.enabled` from an already-parsed Session
|
|
208
|
+
* Config object (DI/test seam — avoids re-reading CLAUDE.md when the caller
|
|
209
|
+
* already has it). Returns `null` when unresolvable from `config` alone, in
|
|
210
|
+
* which case `checkReconcileNudge` falls back to reading CLAUDE.md/AGENTS.md.
|
|
211
|
+
*
|
|
212
|
+
* @param {unknown} config
|
|
213
|
+
* @returns {boolean|null}
|
|
214
|
+
*/
|
|
215
|
+
function _reconcileEnabledFromConfig(config) {
|
|
216
|
+
if (config && typeof config === 'object') {
|
|
217
|
+
const rc = /** @type {Record<string, unknown>} */ (config).reconcile;
|
|
218
|
+
if (rc && typeof rc === 'object' && typeof (/** @type {any} */ (rc).enabled) === 'boolean') {
|
|
219
|
+
return /** @type {any} */ (rc).enabled;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
return null;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Check reconcile-nudge readiness and produce a session-start banner.
|
|
227
|
+
*
|
|
228
|
+
* Silent (`null`) when: no learnings corpus (missing/empty/all-malformed),
|
|
229
|
+
* zero active learnings, or none of the three nudge thresholds are met.
|
|
230
|
+
* Never throws.
|
|
231
|
+
*
|
|
232
|
+
* @param {{repoRoot: string, config?: object, now?: Date|number}} opts
|
|
233
|
+
* - `repoRoot`: REQUIRED absolute path to the repo root.
|
|
234
|
+
* - `config`: optional already-parsed Session Config (DI seam; avoids a
|
|
235
|
+
* second CLAUDE.md read when the caller already parsed it).
|
|
236
|
+
* - `now`: optional injectable clock, forwarded to the active-learning filter.
|
|
237
|
+
* @returns {Promise<null | {severity:'warn', message:string}>}
|
|
238
|
+
*/
|
|
239
|
+
export async function checkReconcileNudge(opts = {}) {
|
|
240
|
+
try {
|
|
241
|
+
const { repoRoot, config, now } = opts;
|
|
242
|
+
if (!repoRoot || typeof repoRoot !== 'string') return null;
|
|
243
|
+
|
|
244
|
+
let computed;
|
|
245
|
+
try {
|
|
246
|
+
computed = await computeReconcileNudge({ repoRoot, now });
|
|
247
|
+
} catch {
|
|
248
|
+
return null;
|
|
249
|
+
}
|
|
250
|
+
if (!computed || computed.nudge !== true) return null;
|
|
251
|
+
|
|
252
|
+
let reconcileEnabled = _reconcileEnabledFromConfig(config);
|
|
253
|
+
if (reconcileEnabled === null) {
|
|
254
|
+
try {
|
|
255
|
+
const content = await readConfigFile(repoRoot);
|
|
256
|
+
reconcileEnabled = _parseReconcile(content).enabled;
|
|
257
|
+
} catch {
|
|
258
|
+
// Config unreadable — fall back to the documented config default (false)
|
|
259
|
+
// so the informational parenthetical still renders conservatively.
|
|
260
|
+
reconcileEnabled = false;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
const lastRunLabel =
|
|
265
|
+
typeof computed.lastRunAt === 'string' && computed.lastRunAt.length >= 10
|
|
266
|
+
? computed.lastRunAt.slice(0, 10)
|
|
267
|
+
: 'never';
|
|
268
|
+
|
|
269
|
+
const lines = [
|
|
270
|
+
`⚠ reconcile-nudge: ${computed.activeLearnings} active learnings, ${computed.eligibleCount} rule-eligible, ` +
|
|
271
|
+
`last reconcile run: ${lastRunLabel} — run /reconcile to convert learnings into rules.`,
|
|
272
|
+
];
|
|
273
|
+
if (reconcileEnabled === false) {
|
|
274
|
+
lines.push(
|
|
275
|
+
' (reconcile.enabled: false — banner is advisory only; /reconcile still runs on-demand.)'
|
|
276
|
+
);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
return { severity: 'warn', message: lines.join('\n') };
|
|
280
|
+
} catch {
|
|
281
|
+
// Defensive catch-all — banner must never throw.
|
|
282
|
+
return null;
|
|
283
|
+
}
|
|
284
|
+
}
|