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,1380 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* checker.mjs — CLAUDE.md narrative drift-check (claude-md-drift-check).
|
|
4
|
+
*
|
|
5
|
+
* Ten checks: path-resolver, project-count-sync, issue-reference-freshness,
|
|
6
|
+
* session-file-existence, surface-count (command/skill/agent/hook-event/
|
|
7
|
+
* hook-matcher/test families), session-config-parity, vault-dir-parity,
|
|
8
|
+
* generated-rule-staleness (WARN-mode only), rule-scoping (paths-presence,
|
|
9
|
+
* cited-but-missing rule citations, zero-match-globs, and foreign-glob
|
|
10
|
+
* PascalCase tokens in .claude/rules/*.md frontmatter), and docs-parity
|
|
11
|
+
* (count-claims in docs/components.md vs actual on-disk counts, Session-Config
|
|
12
|
+
* key parity between docs/session-config-template.md and
|
|
13
|
+
* docs/session-config-reference.md, and stale `.claude/metrics/` path
|
|
14
|
+
* references in the live docs tree).
|
|
15
|
+
* Scans CLAUDE.md + _meta/**\/*.md by default.
|
|
16
|
+
* Emits JSON on stdout. Exit 0 (ok/warn/skip), 1 (hard + errors), 2 (infra).
|
|
17
|
+
*
|
|
18
|
+
* The surface-count family (issue #663) generalizes the original command-count
|
|
19
|
+
* drift check into a parity guard over six artifact surfaces. Each surface
|
|
20
|
+
* derives an ACTUAL on-disk count (glob / hooks.json wiring) and compares it
|
|
21
|
+
* against a CLAIMED count extracted by regex from any scanned doc (README.md,
|
|
22
|
+
* .orchestrator/steering/structure.md, CLAUDE.md). A surface that the doc never
|
|
23
|
+
* claims numerically is skipped gracefully — no claim is ever invented.
|
|
24
|
+
*
|
|
25
|
+
* Reuses `_parseVaultIntegration` from scripts/lib/config/ for Check 7;
|
|
26
|
+
* reuses `parseGlobsFrontmatter` from scripts/lib/rule-loader.mjs for Check 9's
|
|
27
|
+
* glob extraction (mirrors the same picomatch-with-inline-fallback resolution
|
|
28
|
+
* rule-loader.mjs uses); otherwise pure Node stdlib.
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
import { readFileSync, readdirSync, existsSync, statSync } from 'node:fs';
|
|
32
|
+
import { join, relative, resolve } from 'node:path';
|
|
33
|
+
import { execFileSync } from 'node:child_process';
|
|
34
|
+
import { createRequire } from 'node:module';
|
|
35
|
+
import { resolveInstructionFile } from '../../scripts/lib/common.mjs';
|
|
36
|
+
import { _parseVaultIntegration } from '../../scripts/lib/config/vault-integration.mjs';
|
|
37
|
+
import { parseGlobsFrontmatter } from '../../scripts/lib/rule-loader.mjs';
|
|
38
|
+
|
|
39
|
+
const FORWARD_HEADING_RE =
|
|
40
|
+
/(?:^|\b)(what'?s?\s+next|backlog|open\s+issues?|offene\s+(?:issues?|themen)|todo|next\s+steps?|roadmap)(?:$|\b)/i;
|
|
41
|
+
const BACKWARD_HEADING_RE =
|
|
42
|
+
/(?:^|\b)(recently\s+closed|done|closed|archive|history|changelog|decisions?|status|references?)(?:$|\b)/i;
|
|
43
|
+
|
|
44
|
+
function parseArgs(argv) {
|
|
45
|
+
const out = {
|
|
46
|
+
mode: 'warn',
|
|
47
|
+
includePaths: [],
|
|
48
|
+
skipPathResolver: false,
|
|
49
|
+
skipProjectCount: false,
|
|
50
|
+
skipIssueRefs: false,
|
|
51
|
+
skipSessionFiles: false,
|
|
52
|
+
skipCommandCount: false,
|
|
53
|
+
skipSurfaceCount: false,
|
|
54
|
+
skipSessionConfigParity: false,
|
|
55
|
+
skipVaultDirParity: false,
|
|
56
|
+
skipGeneratedRuleStaleness: false,
|
|
57
|
+
skipRuleScoping: false,
|
|
58
|
+
skipDocsParity: false,
|
|
59
|
+
repo: null,
|
|
60
|
+
commandsDir: null,
|
|
61
|
+
configTemplate: null,
|
|
62
|
+
};
|
|
63
|
+
for (let i = 0; i < argv.length; i++) {
|
|
64
|
+
const a = argv[i];
|
|
65
|
+
if (a === '--mode') out.mode = argv[++i];
|
|
66
|
+
else if (a === '--include-path') out.includePaths.push(argv[++i]);
|
|
67
|
+
else if (a === '--repo') out.repo = argv[++i];
|
|
68
|
+
else if (a === '--commands-dir') out.commandsDir = argv[++i];
|
|
69
|
+
else if (a === '--config-template') out.configTemplate = argv[++i];
|
|
70
|
+
else if (a === '--skip-path-resolver') out.skipPathResolver = true;
|
|
71
|
+
else if (a === '--skip-project-count') out.skipProjectCount = true;
|
|
72
|
+
else if (a === '--skip-issue-refs') out.skipIssueRefs = true;
|
|
73
|
+
else if (a === '--skip-session-files') out.skipSessionFiles = true;
|
|
74
|
+
else if (a === '--skip-command-count') out.skipCommandCount = true;
|
|
75
|
+
else if (a === '--skip-surface-count') out.skipSurfaceCount = true;
|
|
76
|
+
else if (a === '--skip-session-config-parity') out.skipSessionConfigParity = true;
|
|
77
|
+
else if (a === '--skip-vault-dir-parity') out.skipVaultDirParity = true;
|
|
78
|
+
else if (a === '--skip-generated-rule-staleness') out.skipGeneratedRuleStaleness = true;
|
|
79
|
+
else if (a === '--skip-rule-scoping') out.skipRuleScoping = true;
|
|
80
|
+
else if (a === '--skip-docs-parity') out.skipDocsParity = true;
|
|
81
|
+
else if (a === '--help' || a === '-h') {
|
|
82
|
+
process.stdout.write('Usage: checker.mjs [--mode hard|warn|off] [--include-path GLOB]... [--repo OWNER/NAME] [--commands-dir PATH] [--config-template PATH] [--skip-surface-count] [--skip-command-count] [--skip-generated-rule-staleness] [--skip-rule-scoping] [--skip-docs-parity] [--skip-*]\n');
|
|
83
|
+
process.exit(0);
|
|
84
|
+
} else {
|
|
85
|
+
process.stderr.write(`{"status":"infra-error","reason":"unknown arg: ${a}"}\n`);
|
|
86
|
+
process.exit(2);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
// Defaults for includePaths are seeded post-vaultDir resolution so the
|
|
90
|
+
// instruction file (CLAUDE.md or AGENTS.md alias) can be alias-resolved.
|
|
91
|
+
return out;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function resolveScopeFiles(vaultDir, patterns) {
|
|
95
|
+
const files = new Set();
|
|
96
|
+
for (const pattern of patterns) {
|
|
97
|
+
if (pattern.includes('**/')) {
|
|
98
|
+
const [prefix, suffix] = pattern.split('/**/');
|
|
99
|
+
const extMatch = /^\*\.(\w+)$/.exec(suffix || '');
|
|
100
|
+
const ext = extMatch ? `.${extMatch[1]}` : null;
|
|
101
|
+
const dir = join(vaultDir, prefix);
|
|
102
|
+
if (existsSync(dir) && statSync(dir).isDirectory()) {
|
|
103
|
+
walkDir(dir, (f) => {
|
|
104
|
+
if (!ext || f.endsWith(ext)) files.add(f);
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
} else {
|
|
108
|
+
const abs = join(vaultDir, pattern);
|
|
109
|
+
if (existsSync(abs) && statSync(abs).isFile()) files.add(abs);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
return Array.from(files).sort();
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function walkDir(dir, visit) {
|
|
116
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
117
|
+
if (entry.name.startsWith('.') || entry.name === 'node_modules') continue;
|
|
118
|
+
const full = join(dir, entry.name);
|
|
119
|
+
if (entry.isDirectory()) walkDir(full, visit);
|
|
120
|
+
else if (entry.isFile()) visit(full);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Extract the YAML block under a `## Session Config` heading from a Markdown
|
|
126
|
+
* document. Tries fenced YAML first (```yaml ... ```) *within the matched
|
|
127
|
+
* heading's own body slice*, then falls back to the raw body up to the next
|
|
128
|
+
* `^## ` heading or EOF.
|
|
129
|
+
*
|
|
130
|
+
* `docs/session-config-template.md` carries TWO literal `## Session Config`
|
|
131
|
+
* lines — both live INSIDE fenced ```yaml example blocks: the "Full minimal
|
|
132
|
+
* baseline" (7 mandatory keys) and the "Full opt-in baseline" (the complete
|
|
133
|
+
* key catalog, a strict superset of the minimal block). By default this
|
|
134
|
+
* function matches the FIRST occurrence (correct for a local CLAUDE.md /
|
|
135
|
+
* AGENTS.md instruction file, which has exactly one real heading). Pass
|
|
136
|
+
* `{ occurrence: 'last' }` to match the LAST occurrence instead — the template
|
|
137
|
+
* call site uses this to reach the opt-in baseline's full key set rather than
|
|
138
|
+
* silently stopping at the 7-key minimal block (issue #785).
|
|
139
|
+
*
|
|
140
|
+
* Returns { body: string, headingLine: number } or null when no heading found.
|
|
141
|
+
*
|
|
142
|
+
* @param {string} content
|
|
143
|
+
* @param {{ occurrence?: 'first'|'last' }} [options]
|
|
144
|
+
*/
|
|
145
|
+
function extractSessionConfigBlock(content, { occurrence = 'first' } = {}) {
|
|
146
|
+
const lines = content.split('\n');
|
|
147
|
+
const headingIdxs = [];
|
|
148
|
+
for (let i = 0; i < lines.length; i++) {
|
|
149
|
+
if (/^##\s+Session Config\b/.test(lines[i])) headingIdxs.push(i);
|
|
150
|
+
}
|
|
151
|
+
if (headingIdxs.length === 0) return null;
|
|
152
|
+
|
|
153
|
+
const targetIdx = occurrence === 'last' ? headingIdxs[headingIdxs.length - 1] : headingIdxs[0];
|
|
154
|
+
const headingLine = targetIdx + 1; // 1-based line number for error reporting
|
|
155
|
+
|
|
156
|
+
// Raw body: from the line after the heading up to the next `## ` heading or EOF.
|
|
157
|
+
let endIdx = lines.length;
|
|
158
|
+
for (let i = targetIdx + 1; i < lines.length; i++) {
|
|
159
|
+
if (/^##\s+/.test(lines[i])) { endIdx = i; break; }
|
|
160
|
+
}
|
|
161
|
+
const rawBody = lines.slice(targetIdx + 1, endIdx).join('\n');
|
|
162
|
+
|
|
163
|
+
// Prefer a fenced YAML block found anywhere within the raw body slice
|
|
164
|
+
// (tolerates a blank line between the heading and the fence); fall back to
|
|
165
|
+
// the raw body itself when no fence is present.
|
|
166
|
+
const fenced = rawBody.match(/```ya?ml\r?\n([\s\S]*?)\r?\n```/);
|
|
167
|
+
const body = fenced ? fenced[1] : rawBody;
|
|
168
|
+
|
|
169
|
+
return { body, headingLine };
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Extract top-level YAML keys from a YAML body. Only column-0 keys are
|
|
174
|
+
* collected (indented keys are children and ignored).
|
|
175
|
+
*/
|
|
176
|
+
function extractTopLevelKeys(body) {
|
|
177
|
+
const keys = [];
|
|
178
|
+
const re = /^([A-Za-z][\w-]*):/gm;
|
|
179
|
+
let m;
|
|
180
|
+
while ((m = re.exec(body)) !== null) {
|
|
181
|
+
keys.push(m[1]);
|
|
182
|
+
}
|
|
183
|
+
return keys;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Extract Session-Config-key-shaped mentions from a "reference" style doc
|
|
188
|
+
* (docs/session-config-reference.md for Check 10b). Unlike the template,
|
|
189
|
+
* which carries one big fenced Session Config block, the reference documents
|
|
190
|
+
* most fields as ONE MARKDOWN TABLE ROW PER KEY (`| \`key-name\` | type |
|
|
191
|
+
* default | description |`) rather than in a single YAML block — a plain
|
|
192
|
+
* "union all yaml fences" extraction (as in `extractSessionConfigBlock`)
|
|
193
|
+
* massively under-counts what the reference actually documents (verified:
|
|
194
|
+
* 53 false-positive "missing" keys against the live doc when restricted to
|
|
195
|
+
* fences/headings alone, vs. the true gap of 6 keys once table rows are
|
|
196
|
+
* counted). Three surfaces contribute to the returned key set:
|
|
197
|
+
* - top-level keys inside any ```yaml fenced block (any indent depth);
|
|
198
|
+
* - `##`/`###`/`####` heading key-tokens (optionally backtick-wrapped),
|
|
199
|
+
* e.g. "## Templates-First Hook" → no match (not kebab-lowercase-shaped),
|
|
200
|
+
* "### resource-thresholds" → `resource-thresholds`;
|
|
201
|
+
* - the FIRST CELL of a markdown table row when it is a backtick-wrapped
|
|
202
|
+
* kebab-case token, e.g. `| \`plan-prd-location\` | string | ... |`.
|
|
203
|
+
* Fence state is tracked for ALL code fences (not just yaml-tagged ones) so
|
|
204
|
+
* headings/table-rows accidentally written inside an unrelated code example
|
|
205
|
+
* are not mistaken for real documentation structure.
|
|
206
|
+
*
|
|
207
|
+
* @param {string} content
|
|
208
|
+
* @returns {Set<string>}
|
|
209
|
+
*/
|
|
210
|
+
function extractDocKeyMentions(content) {
|
|
211
|
+
const keys = new Set();
|
|
212
|
+
const lines = content.split('\n');
|
|
213
|
+
let inFence = false;
|
|
214
|
+
let fenceLang = null;
|
|
215
|
+
let fenceBuf = [];
|
|
216
|
+
for (const line of lines) {
|
|
217
|
+
if (!inFence) {
|
|
218
|
+
const fenceOpen = /^```\s*(\w*)\s*$/.exec(line);
|
|
219
|
+
if (fenceOpen) { inFence = true; fenceLang = fenceOpen[1]; fenceBuf = []; continue; }
|
|
220
|
+
} else {
|
|
221
|
+
if (/^```\s*$/.test(line)) {
|
|
222
|
+
inFence = false;
|
|
223
|
+
if (/^ya?ml$/i.test(fenceLang || '')) {
|
|
224
|
+
const re = /^\s*([a-z][\w-]*):/gm;
|
|
225
|
+
let m;
|
|
226
|
+
const body = fenceBuf.join('\n');
|
|
227
|
+
while ((m = re.exec(body)) !== null) keys.add(m[1]);
|
|
228
|
+
}
|
|
229
|
+
continue;
|
|
230
|
+
}
|
|
231
|
+
fenceBuf.push(line);
|
|
232
|
+
continue;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
const heading = /^#{2,4}\s+`?([a-z][\w-]*)`?/.exec(line);
|
|
236
|
+
if (heading) keys.add(heading[1]);
|
|
237
|
+
|
|
238
|
+
const tableRow = /^\|\s*`([a-z][\w-]*)`\s*\|/.exec(line);
|
|
239
|
+
if (tableRow) keys.add(tableRow[1]);
|
|
240
|
+
}
|
|
241
|
+
return keys;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Read a file's `vault-integration` settings for the vault-dir-parity check.
|
|
246
|
+
*
|
|
247
|
+
* Returns `{ present, vaultDir }`:
|
|
248
|
+
* - `present` is true when the file contains a `vault-integration:` block or
|
|
249
|
+
* inline-object literal (block-form header `vault-integration:` on its own
|
|
250
|
+
* line, or `vault-integration: { ... }`), matching the source shapes
|
|
251
|
+
* `_parseVaultIntegration` recognises.
|
|
252
|
+
* - `vaultDir` is the parsed `vault-integration.vault-dir` value (null when
|
|
253
|
+
* unset/absent), produced by reusing `_parseVaultIntegration` rather than
|
|
254
|
+
* hand-rolling YAML.
|
|
255
|
+
*
|
|
256
|
+
* @param {string} filePath — absolute path to CLAUDE.md / AGENTS.md
|
|
257
|
+
* @returns {{ present: boolean, vaultDir: string|null }}
|
|
258
|
+
*/
|
|
259
|
+
function readVaultIntegration(filePath) {
|
|
260
|
+
const content = readFileSync(filePath, 'utf8');
|
|
261
|
+
// Detect block-form header (`vault-integration:` alone on a line) or inline
|
|
262
|
+
// object literal (`vault-integration: { ... }`), with or without a list dash.
|
|
263
|
+
const present =
|
|
264
|
+
/^(?:-\s+)?vault-integration:\s*$/m.test(content) ||
|
|
265
|
+
/^(?:-\s+)?vault-integration:\s*\{[^}]*\}\s*(?:#.*)?$/m.test(content);
|
|
266
|
+
const parsed = _parseVaultIntegration(content);
|
|
267
|
+
return { present, vaultDir: parsed['vault-dir'] };
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
function classifySection(heading) {
|
|
271
|
+
if (!heading) return null;
|
|
272
|
+
if (BACKWARD_HEADING_RE.test(heading)) return 'backward';
|
|
273
|
+
if (FORWARD_HEADING_RE.test(heading)) return 'forward';
|
|
274
|
+
return null;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// ───────────────────────────────────────────────────────────────────────────
|
|
278
|
+
// Surface-count family (issue #663) — generalizes the original command-count
|
|
279
|
+
// drift check. Each surface has:
|
|
280
|
+
// - id: the check id surfaced in errors[].check and checks_run.
|
|
281
|
+
// - actual(vaultDir): pure function returning the ACTUAL on-disk count, or
|
|
282
|
+
// null when the source artifact is absent (→ surface skipped).
|
|
283
|
+
// - claimRe: a /g/i regex whose capture group 1 is the CLAIMED integer. Only
|
|
284
|
+
// an explicit numeric claim triggers a comparison; surfaces with no
|
|
285
|
+
// matching phrase in any doc are skipped gracefully (requirement #3).
|
|
286
|
+
// - noun: human label used in the drift message.
|
|
287
|
+
// These are EXACT-count drift checks by design — the whole point is catching
|
|
288
|
+
// drift, so NO floor/ceiling (see .claude/rules/testing.md "Dynamic Artifact
|
|
289
|
+
// Counts" carve-out — this is the explicit exception).
|
|
290
|
+
// ───────────────────────────────────────────────────────────────────────────
|
|
291
|
+
|
|
292
|
+
/** Count `skills/<name>/SKILL.md` directories (excludes `_shared/` which has no SKILL.md). */
|
|
293
|
+
function countSkills(vaultDir) {
|
|
294
|
+
const dir = join(vaultDir, 'skills');
|
|
295
|
+
if (!existsSync(dir) || !statSync(dir).isDirectory()) return null;
|
|
296
|
+
let n = 0;
|
|
297
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
298
|
+
if (!entry.isDirectory() || entry.name.startsWith('.')) continue;
|
|
299
|
+
const skillFile = join(dir, entry.name, 'SKILL.md');
|
|
300
|
+
if (existsSync(skillFile) && statSync(skillFile).isFile()) n++;
|
|
301
|
+
}
|
|
302
|
+
return n;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/** Count `agents/*.md` definitions, excluding the AGENTS.md authoring spec. */
|
|
306
|
+
function countAgents(vaultDir) {
|
|
307
|
+
const dir = join(vaultDir, 'agents');
|
|
308
|
+
if (!existsSync(dir) || !statSync(dir).isDirectory()) return null;
|
|
309
|
+
return readdirSync(dir)
|
|
310
|
+
.filter((f) => f.endsWith('.md') && f !== 'AGENTS.md' && !f.startsWith('.'))
|
|
311
|
+
.length;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/**
|
|
315
|
+
* Parse `hooks/hooks.json` and return `{ events, matchers }`:
|
|
316
|
+
* - events: distinct top-level event names (keys of `.hooks`).
|
|
317
|
+
* - matchers: total matcher entries across all events (the authoritative
|
|
318
|
+
* wiring count, not a raw file count).
|
|
319
|
+
* Returns null when hooks.json is absent or unparseable.
|
|
320
|
+
*/
|
|
321
|
+
function readHookCounts(vaultDir) {
|
|
322
|
+
const file = join(vaultDir, 'hooks', 'hooks.json');
|
|
323
|
+
if (!existsSync(file) || !statSync(file).isFile()) return null;
|
|
324
|
+
let json;
|
|
325
|
+
try {
|
|
326
|
+
json = JSON.parse(readFileSync(file, 'utf8'));
|
|
327
|
+
} catch {
|
|
328
|
+
return null;
|
|
329
|
+
}
|
|
330
|
+
const hooks = json && json.hooks;
|
|
331
|
+
if (!hooks || typeof hooks !== 'object') return null;
|
|
332
|
+
const events = Object.keys(hooks);
|
|
333
|
+
let matchers = 0;
|
|
334
|
+
for (const ev of events) {
|
|
335
|
+
if (Array.isArray(hooks[ev])) matchers += hooks[ev].length;
|
|
336
|
+
}
|
|
337
|
+
return { events: events.length, matchers };
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
/** Count test files (`*.test.mjs`) under `tests/`. */
|
|
341
|
+
function countTestFiles(vaultDir) {
|
|
342
|
+
const dir = join(vaultDir, 'tests');
|
|
343
|
+
if (!existsSync(dir) || !statSync(dir).isDirectory()) return null;
|
|
344
|
+
let n = 0;
|
|
345
|
+
walkDir(dir, (f) => { if (f.endsWith('.test.mjs')) n++; });
|
|
346
|
+
return n;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
/**
|
|
350
|
+
* Build the surface-count descriptor table for the given vault. `hookCounts`
|
|
351
|
+
* is computed once and shared by the two hook surfaces. Each descriptor's
|
|
352
|
+
* `actual` is resolved eagerly so a single drift pass can skip null surfaces.
|
|
353
|
+
*
|
|
354
|
+
* `command-count` keeps its original id + message phrasing for back-compat with
|
|
355
|
+
* the result.command_count field and existing tests; the four new surfaces
|
|
356
|
+
* follow the same shape.
|
|
357
|
+
*/
|
|
358
|
+
function buildSurfaceDescriptors(vaultDir, commandsDir) {
|
|
359
|
+
const hookCounts = readHookCounts(vaultDir);
|
|
360
|
+
return [
|
|
361
|
+
{
|
|
362
|
+
id: 'command-count',
|
|
363
|
+
noun: 'commands',
|
|
364
|
+
actual: (() => {
|
|
365
|
+
const dir = commandsDir || join(vaultDir, 'commands');
|
|
366
|
+
if (!existsSync(dir) || !statSync(dir).isDirectory()) return null;
|
|
367
|
+
return readdirSync(dir).filter((f) => f.endsWith('.md') && !f.startsWith('.')).length;
|
|
368
|
+
})(),
|
|
369
|
+
// "8 commands", "8 /commands", "8 slash commands"
|
|
370
|
+
claimRe: /\b(\d+)\s+(?:\/)?commands?\b/gi,
|
|
371
|
+
skipMsg: 'command-count: no commands/ directory found (use --commands-dir to override)',
|
|
372
|
+
},
|
|
373
|
+
{
|
|
374
|
+
id: 'skill-count',
|
|
375
|
+
noun: 'skills',
|
|
376
|
+
actual: countSkills(vaultDir),
|
|
377
|
+
// "40 skills", "40 user-facing skills"
|
|
378
|
+
claimRe: /\b(\d+)\s+(?:user-facing\s+)?skills?\b/gi,
|
|
379
|
+
skipMsg: 'skill-count: no skills/ directory found',
|
|
380
|
+
},
|
|
381
|
+
{
|
|
382
|
+
id: 'agent-count',
|
|
383
|
+
noun: 'agents',
|
|
384
|
+
actual: countAgents(vaultDir),
|
|
385
|
+
// "14 sub-agent definitions" / "14 sub-agents" / "14 agent definitions"
|
|
386
|
+
// Deliberately NOT matched: "3 agent role definitions" (Codex-specific).
|
|
387
|
+
claimRe: /\b(\d+)\s+(?:sub-agents?|agent\s+definitions?)\b/gi,
|
|
388
|
+
skipMsg: 'agent-count: no agents/ directory found',
|
|
389
|
+
},
|
|
390
|
+
{
|
|
391
|
+
id: 'hook-event-count',
|
|
392
|
+
noun: 'distinct hook events',
|
|
393
|
+
actual: hookCounts ? hookCounts.events : null,
|
|
394
|
+
// "10 distinct events"
|
|
395
|
+
claimRe: /\b(\d+)\s+distinct\s+events?\b/gi,
|
|
396
|
+
skipMsg: 'hook-event-count: no hooks/hooks.json found or unparseable',
|
|
397
|
+
},
|
|
398
|
+
{
|
|
399
|
+
id: 'hook-matcher-count',
|
|
400
|
+
noun: 'hook matcher entries',
|
|
401
|
+
actual: hookCounts ? hookCounts.matchers : null,
|
|
402
|
+
// "14 matcher entries"
|
|
403
|
+
claimRe: /\b(\d+)\s+matcher\s+entries\b/gi,
|
|
404
|
+
skipMsg: 'hook-matcher-count: no hooks/hooks.json found or unparseable',
|
|
405
|
+
},
|
|
406
|
+
{
|
|
407
|
+
id: 'test-count',
|
|
408
|
+
noun: 'test files',
|
|
409
|
+
actual: countTestFiles(vaultDir),
|
|
410
|
+
// "N test files" — the README badge claims a runtime PASS count
|
|
411
|
+
// ("9303 tests"), which a static checker cannot derive, so we match only
|
|
412
|
+
// the explicit file-count phrasing. No doc claims this today → skipped.
|
|
413
|
+
claimRe: /\b(\d+)\s+test\s+files?\b/gi,
|
|
414
|
+
skipMsg: 'test-count: no tests/ directory found',
|
|
415
|
+
},
|
|
416
|
+
];
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
const REPO_SHAPE_RE = /^[A-Za-z0-9_.-]+(?:\/[A-Za-z0-9_.-]+)+$/;
|
|
420
|
+
|
|
421
|
+
function detectRepo(vaultDir) {
|
|
422
|
+
const url = execFileSync('git', ['remote', 'get-url', 'origin'], { cwd: vaultDir, encoding: 'utf8' }).trim();
|
|
423
|
+
const m = /[:/]([^:/\s]+\/[^/\s]+?)(?:\.git)?$/.exec(url);
|
|
424
|
+
const candidate = m ? m[1] : null;
|
|
425
|
+
return candidate && REPO_SHAPE_RE.test(candidate) ? candidate : null;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
function hasGlab() {
|
|
429
|
+
try {
|
|
430
|
+
execFileSync('sh', ['-c', 'command -v glab'], { stdio: 'pipe' });
|
|
431
|
+
return true;
|
|
432
|
+
} catch { return false; }
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
function lookupIssueState(iid, repo, cache) {
|
|
436
|
+
if (cache.has(iid)) return cache.get(iid);
|
|
437
|
+
let state = 'unknown';
|
|
438
|
+
try {
|
|
439
|
+
const out = execFileSync('glab', ['issue', 'view', iid, '--repo', repo], {
|
|
440
|
+
encoding: 'utf8',
|
|
441
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
442
|
+
});
|
|
443
|
+
const m = /^state:\s*(open|closed)/im.exec(out);
|
|
444
|
+
if (m) state = m[1];
|
|
445
|
+
} catch { /* glab returned non-zero — leave as unknown */ }
|
|
446
|
+
cache.set(iid, state);
|
|
447
|
+
return state;
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
// ───────────────────────────────────────────────────────────────────────────
|
|
451
|
+
// Rule-scoping family (Check 9) — validates .claude/rules/*.md frontmatter
|
|
452
|
+
// against the scripts/lib/rule-loader.mjs contract:
|
|
453
|
+
// 1. paths-presence → errors[]: a top-level `paths:` key (rule-loader.mjs
|
|
454
|
+
// only recognises `globs:`; `paths:` silently loads the rule ALWAYS-ON).
|
|
455
|
+
// 2. cited-but-missing → errors[]: (a) `.claude/rules/<name>.md` citations in
|
|
456
|
+
// CLAUDE.md/AGENTS.md that don't exist on disk; (b) bare `<name>.md`
|
|
457
|
+
// tokens in a rule's own `## See Also` footer that don't exist on disk
|
|
458
|
+
// (tokens carrying a path separator are cross-directory references and
|
|
459
|
+
// out of scope).
|
|
460
|
+
// 3. zero-match-globs → warnings[]: a `globs:` pattern matching 0 tracked
|
|
461
|
+
// files (git ls-files). WARN, not error — library/exemplar repos may
|
|
462
|
+
// legitimately carry dead stack rules.
|
|
463
|
+
// 4. foreign-glob → warnings[]: a glob pattern containing a PascalCase
|
|
464
|
+
// product-like token (e.g. `WalkAITalkieTests`) — a likely copy-paste
|
|
465
|
+
// leftover from another project's rule.
|
|
466
|
+
// 5. unreadable-file → warnings[]: a rule file that could not be read
|
|
467
|
+
// (permissions, race with a concurrent delete, etc.) — surfaced instead
|
|
468
|
+
// of being silently skipped, since a completeness audit that silently
|
|
469
|
+
// drops files defeats its purpose. WARN, not error — an unreadable file
|
|
470
|
+
// must not brick the gate under `mode: hard`.
|
|
471
|
+
// ───────────────────────────────────────────────────────────────────────────
|
|
472
|
+
|
|
473
|
+
let _picomatchRuleScoping = null;
|
|
474
|
+
function getPicomatchForRuleScoping() {
|
|
475
|
+
if (_picomatchRuleScoping !== null) return _picomatchRuleScoping;
|
|
476
|
+
try {
|
|
477
|
+
const require = createRequire(import.meta.url);
|
|
478
|
+
_picomatchRuleScoping = require('picomatch');
|
|
479
|
+
} catch {
|
|
480
|
+
_picomatchRuleScoping = false;
|
|
481
|
+
}
|
|
482
|
+
return _picomatchRuleScoping;
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
/** Minimal glob-to-RegExp fallback (mirrors scripts/lib/rule-loader.mjs),
|
|
486
|
+
* used only when picomatch is unavailable in node_modules. */
|
|
487
|
+
function globToRegExpFallback(pattern) {
|
|
488
|
+
const p = pattern.replace(/\\/g, '/');
|
|
489
|
+
let re = '';
|
|
490
|
+
let i = 0;
|
|
491
|
+
while (i < p.length) {
|
|
492
|
+
const c = p[i];
|
|
493
|
+
if (c === '*' && p[i + 1] === '*') {
|
|
494
|
+
re += '.*';
|
|
495
|
+
i += 2;
|
|
496
|
+
if (p[i] === '/') i++;
|
|
497
|
+
} else if (c === '*') {
|
|
498
|
+
re += '[^/]*';
|
|
499
|
+
i++;
|
|
500
|
+
} else if (c === '?') {
|
|
501
|
+
re += '[^/]';
|
|
502
|
+
i++;
|
|
503
|
+
} else if (c === '.') {
|
|
504
|
+
re += '\\.';
|
|
505
|
+
i++;
|
|
506
|
+
} else {
|
|
507
|
+
re += c.replace(/[$()+[\]^{|}]/g, '\\$&');
|
|
508
|
+
i++;
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
return new RegExp(`^${re}$`);
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
/** Returns true when `pattern` matches at least one entry in `trackedFiles`. */
|
|
515
|
+
function globMatchesAny(pattern, trackedFiles) {
|
|
516
|
+
const pm = getPicomatchForRuleScoping();
|
|
517
|
+
if (pm) {
|
|
518
|
+
return trackedFiles.some((f) => pm.isMatch(f, pattern, { dot: true }));
|
|
519
|
+
}
|
|
520
|
+
const re = globToRegExpFallback(pattern);
|
|
521
|
+
return trackedFiles.some((f) => re.test(f));
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
/**
|
|
525
|
+
* Returns the repo's tracked files (relative, forward-slash separated) via
|
|
526
|
+
* `git ls-files`; falls back to a manual walk (excluding dotdirs and
|
|
527
|
+
* node_modules — same exclusions as `walkDir`) when git is unavailable.
|
|
528
|
+
*/
|
|
529
|
+
function listTrackedFiles(vaultDir) {
|
|
530
|
+
try {
|
|
531
|
+
const out = execFileSync('git', ['ls-files'], { cwd: vaultDir, encoding: 'utf8' });
|
|
532
|
+
return out.split('\n').filter(Boolean);
|
|
533
|
+
} catch {
|
|
534
|
+
const files = [];
|
|
535
|
+
walkDir(vaultDir, (f) => files.push(relative(vaultDir, f).replace(/\\/g, '/')));
|
|
536
|
+
return files;
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
const RULE_HEADER_LINE_RE = /^[ \t]*(?:<!--.*-->)?[ \t]*$/;
|
|
541
|
+
|
|
542
|
+
function stripLeadingRuleHeaderLines(content) {
|
|
543
|
+
const lines = content.split(/\r?\n/);
|
|
544
|
+
let idx = 0;
|
|
545
|
+
while (idx < lines.length && RULE_HEADER_LINE_RE.test(lines[idx])) idx++;
|
|
546
|
+
return lines.slice(idx).join('\n');
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
/** Extracts the raw frontmatter block body (text between the `---` delimiters),
|
|
550
|
+
* or null when the file has no frontmatter block. Leading blank/comment
|
|
551
|
+
* provenance lines are tolerated so Check 9 sees the same frontmatter shape
|
|
552
|
+
* that rule-loader.mjs accepts. */
|
|
553
|
+
function extractFrontmatterBlockBody(content) {
|
|
554
|
+
const m = /^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/.exec(stripLeadingRuleHeaderLines(content));
|
|
555
|
+
return m ? m[1] : null;
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
/** PascalCase product-like token check (e.g. `WalkAITalkieTests`) — the
|
|
559
|
+
* foreign-glob probe's discriminator, per Check 9 spec. */
|
|
560
|
+
const FOREIGN_GLOB_TOKEN_RE = /[A-Z][a-z]+[A-Z]/;
|
|
561
|
+
|
|
562
|
+
/**
|
|
563
|
+
* Extracts bare `<name>.md` tokens from a "## See Also" footer's body lines,
|
|
564
|
+
* skipping any token that carries a path separator (cross-directory
|
|
565
|
+
* references are out of scope) or is not shaped like a kebab-case rule
|
|
566
|
+
* filename. Returns `[{ token, lineOffset }]`, `lineOffset` 0-based relative
|
|
567
|
+
* to `bodyLines[0]`.
|
|
568
|
+
*/
|
|
569
|
+
function extractSeeAlsoTokens(bodyLines) {
|
|
570
|
+
const results = [];
|
|
571
|
+
for (let i = 0; i < bodyLines.length; i++) {
|
|
572
|
+
const rawTokens = bodyLines[i].split(/[\s·]+/).filter(Boolean);
|
|
573
|
+
for (const raw of rawTokens) {
|
|
574
|
+
const stripped = raw.replace(/^[`[(]+/, '').replace(/[`\]),.;:]+$/, '');
|
|
575
|
+
if (!/\.md$/.test(stripped)) continue;
|
|
576
|
+
if (stripped.includes('/')) continue; // cross-directory reference — out of scope
|
|
577
|
+
if (!/^[a-z][a-z0-9-]*\.md$/.test(stripped)) continue; // not kebab-case rule-filename shaped
|
|
578
|
+
results.push({ token: stripped, lineOffset: i });
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
return results;
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
function main() {
|
|
585
|
+
const args = parseArgs(process.argv.slice(2));
|
|
586
|
+
const vaultDir = resolve(process.env.VAULT_DIR || process.cwd());
|
|
587
|
+
|
|
588
|
+
if (!['hard', 'warn', 'off'].includes(args.mode)) {
|
|
589
|
+
process.stderr.write(`{"status":"infra-error","reason":"invalid --mode: ${args.mode}"}\n`);
|
|
590
|
+
process.exit(2);
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
// Alias-aware instruction file resolution (issue #33).
|
|
594
|
+
// CLAUDE.md (Claude Code / Cursor IDE) and AGENTS.md (Codex CLI) are
|
|
595
|
+
// transparent aliases — see skills/_shared/instruction-file-resolution.md.
|
|
596
|
+
const instr = resolveInstructionFile(vaultDir); // {path, kind} | null
|
|
597
|
+
const resolvedPath = instr ? instr.path : null;
|
|
598
|
+
const resolvedKind = instr ? instr.kind : null;
|
|
599
|
+
|
|
600
|
+
if (args.mode === 'off') {
|
|
601
|
+
process.stdout.write(JSON.stringify({
|
|
602
|
+
status: 'skipped-mode-off', mode: 'off', vault_dir: vaultDir,
|
|
603
|
+
resolved_path: resolvedPath, resolved_kind: resolvedKind,
|
|
604
|
+
}) + '\n');
|
|
605
|
+
process.exit(0);
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
if (!existsSync(vaultDir) || !statSync(vaultDir).isDirectory()) {
|
|
609
|
+
process.stderr.write(`{"status":"infra-error","reason":"VAULT_DIR does not exist: ${vaultDir}"}\n`);
|
|
610
|
+
process.exit(2);
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
// Seed default includePaths post-vaultDir resolution (alias-aware).
|
|
614
|
+
if (args.includePaths.length === 0) {
|
|
615
|
+
args.includePaths = [
|
|
616
|
+
...(instr ? [relative(vaultDir, instr.path)] : []),
|
|
617
|
+
'_meta/**/*.md',
|
|
618
|
+
];
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
const scopeFiles = resolveScopeFiles(vaultDir, args.includePaths);
|
|
622
|
+
const checksSkipped = [];
|
|
623
|
+
|
|
624
|
+
let actualProjectCount = null;
|
|
625
|
+
if (!args.skipProjectCount) {
|
|
626
|
+
const projectsDir = join(vaultDir, '01-projects');
|
|
627
|
+
if (existsSync(projectsDir) && statSync(projectsDir).isDirectory()) {
|
|
628
|
+
actualProjectCount = readdirSync(projectsDir, { withFileTypes: true })
|
|
629
|
+
.filter((e) => e.isDirectory() && !e.name.startsWith('_') && !e.name.startsWith('.'))
|
|
630
|
+
.length;
|
|
631
|
+
} else {
|
|
632
|
+
checksSkipped.push('project-count-sync: no 01-projects/ directory at vault root');
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
// Check 5: surface-count family (command/skill/agent/hook-event/hook-matcher/
|
|
637
|
+
// test). Each surface derives an ACTUAL on-disk count and compares it to a
|
|
638
|
+
// CLAIMED count regex-extracted from any scanned doc. Surfaces whose source
|
|
639
|
+
// artifact is absent (actual === null) are skipped; surfaces the doc never
|
|
640
|
+
// claims are skipped per-line in the scan loop (no claim invented).
|
|
641
|
+
// `--skip-surface-count` disables the whole family; `--skip-command-count`
|
|
642
|
+
// remains a back-compat alias that disables only the command-count surface.
|
|
643
|
+
const commandsDir = args.commandsDir ? resolve(args.commandsDir) : null;
|
|
644
|
+
let surfaces = [];
|
|
645
|
+
if (!args.skipSurfaceCount) {
|
|
646
|
+
surfaces = buildSurfaceDescriptors(vaultDir, commandsDir);
|
|
647
|
+
if (args.skipCommandCount) {
|
|
648
|
+
surfaces = surfaces.filter((s) => s.id !== 'command-count');
|
|
649
|
+
}
|
|
650
|
+
for (const s of surfaces) {
|
|
651
|
+
if (s.actual === null) checksSkipped.push(s.skipMsg);
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
// Active surfaces = family enabled AND source artifact present on disk.
|
|
655
|
+
const activeSurfaces = surfaces.filter((s) => s.actual !== null);
|
|
656
|
+
// Back-compat: the command-count surface's actual feeds result.command_count.
|
|
657
|
+
const commandSurface = activeSurfaces.find((s) => s.id === 'command-count');
|
|
658
|
+
const actualCommandCount = commandSurface ? commandSurface.actual : null;
|
|
659
|
+
|
|
660
|
+
let repo = args.repo;
|
|
661
|
+
const glabPresent = !args.skipIssueRefs && hasGlab();
|
|
662
|
+
if (!args.skipIssueRefs && !glabPresent) {
|
|
663
|
+
checksSkipped.push('issue-reference-freshness: glab not found in PATH');
|
|
664
|
+
}
|
|
665
|
+
if (!args.skipIssueRefs && glabPresent && !repo) {
|
|
666
|
+
try { repo = detectRepo(vaultDir); } catch { /* ignore */ }
|
|
667
|
+
if (!repo) checksSkipped.push('issue-reference-freshness: could not detect origin repo (use --repo)');
|
|
668
|
+
}
|
|
669
|
+
const runIssueCheck = !args.skipIssueRefs && glabPresent && !!repo;
|
|
670
|
+
|
|
671
|
+
const checksRun = [];
|
|
672
|
+
if (!args.skipPathResolver) checksRun.push('path-resolver');
|
|
673
|
+
if (!args.skipProjectCount && actualProjectCount !== null) checksRun.push('project-count-sync');
|
|
674
|
+
if (runIssueCheck) checksRun.push('issue-reference-freshness');
|
|
675
|
+
if (!args.skipSessionFiles) checksRun.push('session-file-existence');
|
|
676
|
+
for (const s of activeSurfaces) checksRun.push(s.id);
|
|
677
|
+
|
|
678
|
+
const errors = [];
|
|
679
|
+
const warnings = [];
|
|
680
|
+
const issueCache = new Map();
|
|
681
|
+
|
|
682
|
+
// Check 6: session-config-parity (issue #30) — diff top-level keys under
|
|
683
|
+
// `## Session Config` between the canonical template and the local
|
|
684
|
+
// instruction file.
|
|
685
|
+
//
|
|
686
|
+
// Severity split (issue #785 follow-up — coordinator triage of the initial
|
|
687
|
+
// #785 fix): a missing MANDATORY key (present in the template's "Full
|
|
688
|
+
// minimal baseline" — the 7 schema-enforced keys) is an ERROR. A missing
|
|
689
|
+
// OPT-IN key (present only in the "Full opt-in baseline" — i.e. NOT in the
|
|
690
|
+
// minimal block) is a WARNING, not an error: a consumer repo that
|
|
691
|
+
// legitimately does not adopt an opt-in feature (e.g. no `handover-gate`)
|
|
692
|
+
// must not go red — this repo's own CLAUDE.md deliberately omits 37
|
|
693
|
+
// opt-in-baseline keys. `mode: hard` only exits non-zero on `errors[]`, so
|
|
694
|
+
// pure opt-in gaps never block `autonomous-gated` skill-evolution's
|
|
695
|
+
// `runConfigValidationGate()`.
|
|
696
|
+
//
|
|
697
|
+
// This is a deliberate, coordinator-directed DEVIATION from #785's literal
|
|
698
|
+
// fix-direction ("plants an opt-in key... asserts a session-config-parity
|
|
699
|
+
// ERROR") — the mechanism (union template keys via `{ occurrence: 'last' }`)
|
|
700
|
+
// is unchanged, but the missing-opt-in-key case now lands in `warnings[]`
|
|
701
|
+
// instead of `errors[]`. Local keys unknown to the template union were
|
|
702
|
+
// (and remain) never flagged in either direction — no existing code path
|
|
703
|
+
// inspected that direction before this change either.
|
|
704
|
+
let configParityRan = false;
|
|
705
|
+
if (!args.skipSessionConfigParity) {
|
|
706
|
+
const templatePath = args.configTemplate
|
|
707
|
+
? resolve(args.configTemplate)
|
|
708
|
+
: join(vaultDir, 'docs', 'session-config-template.md');
|
|
709
|
+
if (!instr) {
|
|
710
|
+
checksSkipped.push('session-config-parity: no instruction file');
|
|
711
|
+
} else if (!existsSync(templatePath) || !statSync(templatePath).isFile()) {
|
|
712
|
+
checksSkipped.push('session-config-parity: no docs/session-config-template.md found');
|
|
713
|
+
} else {
|
|
714
|
+
const templateContent = readFileSync(templatePath, 'utf8');
|
|
715
|
+
const localContent = readFileSync(instr.path, 'utf8');
|
|
716
|
+
// 'last' occurrence reaches the "Full opt-in baseline" block (the full
|
|
717
|
+
// key catalog, a strict superset of the minimal block) — issue #785.
|
|
718
|
+
// 'first' occurrence reaches the "Full minimal baseline" (7 mandatory
|
|
719
|
+
// keys) — used here to classify which missing keys are mandatory vs
|
|
720
|
+
// opt-in. When the template has only ONE `## Session Config` heading
|
|
721
|
+
// (e.g. a test fixture, or a repo that never split the two baselines),
|
|
722
|
+
// 'first' and 'last' resolve to the SAME block, so every key is
|
|
723
|
+
// treated as mandatory — preserving pre-existing behaviour for
|
|
724
|
+
// single-block templates.
|
|
725
|
+
const tplBlock = extractSessionConfigBlock(templateContent, { occurrence: 'last' });
|
|
726
|
+
const tplMinimalBlock = extractSessionConfigBlock(templateContent, { occurrence: 'first' });
|
|
727
|
+
const localBlock = extractSessionConfigBlock(localContent);
|
|
728
|
+
if (!tplBlock) {
|
|
729
|
+
checksSkipped.push('session-config-parity: template has no ## Session Config block');
|
|
730
|
+
} else {
|
|
731
|
+
configParityRan = true;
|
|
732
|
+
const tplKeys = extractTopLevelKeys(tplBlock.body);
|
|
733
|
+
const tplMinimalKeys = new Set(tplMinimalBlock ? extractTopLevelKeys(tplMinimalBlock.body) : []);
|
|
734
|
+
const localKeys = new Set(localBlock ? extractTopLevelKeys(localBlock.body) : []);
|
|
735
|
+
// Absent local block: line 1 (nothing to anchor to); present local
|
|
736
|
+
// block: attribute to its own heading line — same as before #785.
|
|
737
|
+
const rel = relative(vaultDir, instr.path);
|
|
738
|
+
const line = localBlock ? localBlock.headingLine : 1;
|
|
739
|
+
const missing = tplKeys.filter((k) => !localKeys.has(k));
|
|
740
|
+
for (const key of missing) {
|
|
741
|
+
if (tplMinimalKeys.has(key)) {
|
|
742
|
+
errors.push({
|
|
743
|
+
check: 'session-config-parity', file: rel, line,
|
|
744
|
+
message: `Session Config missing top-level key '${key}' (present in docs/session-config-template.md)`,
|
|
745
|
+
extracted: key,
|
|
746
|
+
});
|
|
747
|
+
} else {
|
|
748
|
+
warnings.push({
|
|
749
|
+
check: 'session-config-parity', file: rel, line,
|
|
750
|
+
message: `Session Config omits opt-in top-level key '${key}' (documented in docs/session-config-template.md's opt-in baseline; not required)`,
|
|
751
|
+
extracted: key,
|
|
752
|
+
});
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
} else {
|
|
758
|
+
checksSkipped.push('session-config-parity: explicitly skipped');
|
|
759
|
+
}
|
|
760
|
+
if (configParityRan) checksRun.push('session-config-parity');
|
|
761
|
+
|
|
762
|
+
// Check 7: vault-dir-parity (issue #600) — when BOTH instruction files
|
|
763
|
+
// (CLAUDE.md AND AGENTS.md) exist as transparent aliases, they must agree on
|
|
764
|
+
// `vault-integration.vault-dir`. A sibling project carried a dead vault-dir in
|
|
765
|
+
// AGENTS.md for weeks while CLAUDE.md was correct; resolveInstructionFile()
|
|
766
|
+
// picks only one file, so the disagreement went undetected. This check reads
|
|
767
|
+
// both files independently and flags any divergence.
|
|
768
|
+
let vaultDirParityRan = false;
|
|
769
|
+
if (!args.skipVaultDirParity) {
|
|
770
|
+
const claudePath = join(vaultDir, 'CLAUDE.md');
|
|
771
|
+
const agentsPath = join(vaultDir, 'AGENTS.md');
|
|
772
|
+
const claudeExists = existsSync(claudePath) && statSync(claudePath).isFile();
|
|
773
|
+
const agentsExists = existsSync(agentsPath) && statSync(agentsPath).isFile();
|
|
774
|
+
|
|
775
|
+
if (!claudeExists || !agentsExists) {
|
|
776
|
+
checksSkipped.push('vault-dir-parity: requires both CLAUDE.md and AGENTS.md (only one present)');
|
|
777
|
+
} else {
|
|
778
|
+
const claudeVi = readVaultIntegration(claudePath);
|
|
779
|
+
const agentsVi = readVaultIntegration(agentsPath);
|
|
780
|
+
if (!claudeVi.present && !agentsVi.present) {
|
|
781
|
+
checksSkipped.push('vault-dir-parity: neither file has a vault-integration: block');
|
|
782
|
+
} else {
|
|
783
|
+
vaultDirParityRan = true;
|
|
784
|
+
const claudeDir = claudeVi.vaultDir;
|
|
785
|
+
const agentsDir = agentsVi.vaultDir;
|
|
786
|
+
if (claudeDir !== agentsDir) {
|
|
787
|
+
errors.push({
|
|
788
|
+
check: 'vault-dir-parity', file: 'AGENTS.md', line: 1,
|
|
789
|
+
message: `vault-integration.vault-dir disagrees between instruction files: CLAUDE.md='${claudeDir ?? '(unset)'}' vs AGENTS.md='${agentsDir ?? '(unset)'}'`,
|
|
790
|
+
extracted: agentsDir ?? '(unset)',
|
|
791
|
+
});
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
} else {
|
|
796
|
+
checksSkipped.push('vault-dir-parity: explicitly skipped');
|
|
797
|
+
}
|
|
798
|
+
if (vaultDirParityRan) checksRun.push('vault-dir-parity');
|
|
799
|
+
|
|
800
|
+
// Check 8: generated-rule-staleness (WARN-mode, advisory — Epic #693 FA4 #697).
|
|
801
|
+
// For each .claude/rules/*.md with frontmatter `auto-generated: true`, extract
|
|
802
|
+
// `learning-key` and verify it matches a non-expired entry in
|
|
803
|
+
// `.orchestrator/metrics/learnings.jsonl`.
|
|
804
|
+
//
|
|
805
|
+
// Key derivation mirrors emitter.mjs `toActivationMetadata`:
|
|
806
|
+
// learningKey = `${type}/${kebab(title || subject || '')}`
|
|
807
|
+
// kebab(s) = s.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '')
|
|
808
|
+
//
|
|
809
|
+
// WARN (never error) when:
|
|
810
|
+
// - learnings.jsonl is present AND no entry's derived key matches the rule's
|
|
811
|
+
// `learning-key` (absent learning), OR
|
|
812
|
+
// - the matching entry's `expires_at` < now (expired learning).
|
|
813
|
+
// When learnings.jsonl is absent, every key counts as absent — warn on each
|
|
814
|
+
// generated rule's key.
|
|
815
|
+
// The check is silently skipped (no id pushed) when .claude/rules/ is absent
|
|
816
|
+
// or contains no .md files with auto-generated: true.
|
|
817
|
+
if (!args.skipGeneratedRuleStaleness) {
|
|
818
|
+
(function runGeneratedRuleStaleness() {
|
|
819
|
+
const rulesDir = join(vaultDir, '.claude', 'rules');
|
|
820
|
+
if (!existsSync(rulesDir) || !statSync(rulesDir).isDirectory()) return;
|
|
821
|
+
|
|
822
|
+
// Minimal inline frontmatter extractor for auto-generated + learning-key.
|
|
823
|
+
// Reads the opening --- ... --- block from a markdown file.
|
|
824
|
+
function extractFrontmatterFields(mdContent) {
|
|
825
|
+
const m = /^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/.exec(stripLeadingRuleHeaderLines(mdContent));
|
|
826
|
+
if (!m) return { autoGenerated: false, learningKey: null, expiresAt: null };
|
|
827
|
+
const block = m[1];
|
|
828
|
+
const autoGenM = /^auto-generated:\s*(.+)$/m.exec(block);
|
|
829
|
+
const learningKeyM = /^learning-key:\s*(.+)$/m.exec(block);
|
|
830
|
+
const expiresAtM = /^expires-at:\s*(.+)$/m.exec(block);
|
|
831
|
+
return {
|
|
832
|
+
autoGenerated: autoGenM ? autoGenM[1].trim() === 'true' : false,
|
|
833
|
+
learningKey: learningKeyM ? learningKeyM[1].trim() : null,
|
|
834
|
+
expiresAt: expiresAtM ? expiresAtM[1].trim() : null,
|
|
835
|
+
};
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
// Collect all .md files in .claude/rules/ with auto-generated: true.
|
|
839
|
+
const ruleFiles = readdirSync(rulesDir)
|
|
840
|
+
.filter((f) => f.endsWith('.md') && !f.startsWith('.'))
|
|
841
|
+
.map((f) => join(rulesDir, f))
|
|
842
|
+
.filter((absPath) => statSync(absPath).isFile());
|
|
843
|
+
|
|
844
|
+
const generatedRules = [];
|
|
845
|
+
for (const absPath of ruleFiles) {
|
|
846
|
+
let content;
|
|
847
|
+
try { content = readFileSync(absPath, 'utf8'); } catch { continue; }
|
|
848
|
+
const fields = extractFrontmatterFields(content);
|
|
849
|
+
if (!fields.autoGenerated) continue;
|
|
850
|
+
generatedRules.push({
|
|
851
|
+
absPath,
|
|
852
|
+
relPath: relative(vaultDir, absPath),
|
|
853
|
+
learningKey: fields.learningKey,
|
|
854
|
+
expiresAt: fields.expiresAt,
|
|
855
|
+
});
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
// No generated rules found → silently skip (don't push the check id).
|
|
859
|
+
if (generatedRules.length === 0) return;
|
|
860
|
+
|
|
861
|
+
// Slugify function mirroring emitter.mjs `kebab()`.
|
|
862
|
+
const kebab = (s) =>
|
|
863
|
+
String(s)
|
|
864
|
+
.toLowerCase()
|
|
865
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
866
|
+
.replace(/^-+|-+$/g, '');
|
|
867
|
+
|
|
868
|
+
// Build the set of known learning keys from learnings.jsonl.
|
|
869
|
+
// key → expires_at (ISO string or null)
|
|
870
|
+
const knownKeys = new Map();
|
|
871
|
+
const learningsPath = join(vaultDir, '.orchestrator', 'metrics', 'learnings.jsonl');
|
|
872
|
+
const learningsPresent = existsSync(learningsPath) && statSync(learningsPath).isFile();
|
|
873
|
+
|
|
874
|
+
if (learningsPresent) {
|
|
875
|
+
let raw;
|
|
876
|
+
try { raw = readFileSync(learningsPath, 'utf8'); } catch { /* leave knownKeys empty */ }
|
|
877
|
+
if (raw) {
|
|
878
|
+
for (const line of raw.split('\n')) {
|
|
879
|
+
const trimmed = line.trim();
|
|
880
|
+
if (!trimmed) continue;
|
|
881
|
+
let entry;
|
|
882
|
+
try { entry = JSON.parse(trimmed); } catch { continue; }
|
|
883
|
+
if (!entry || typeof entry !== 'object') continue;
|
|
884
|
+
const type = typeof entry.type === 'string' ? entry.type : '';
|
|
885
|
+
const subjectOrTitle =
|
|
886
|
+
(typeof entry.title === 'string' && entry.title !== '' ? entry.title : '') ||
|
|
887
|
+
(typeof entry.subject === 'string' && entry.subject !== '' ? entry.subject : '');
|
|
888
|
+
if (!type || !subjectOrTitle) continue;
|
|
889
|
+
const derivedKey = `${type}/${kebab(subjectOrTitle)}`;
|
|
890
|
+
// Store the most-recent expires_at (later entries overwrite earlier ones
|
|
891
|
+
// with the same key — safe; duplicates are rare and same-key learnings
|
|
892
|
+
// share the same expiry semantics).
|
|
893
|
+
knownKeys.set(derivedKey, entry.expires_at ?? null);
|
|
894
|
+
}
|
|
895
|
+
}
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
const nowMs = Date.now();
|
|
899
|
+
|
|
900
|
+
for (const rule of generatedRules) {
|
|
901
|
+
const { relPath, learningKey, expiresAt } = rule;
|
|
902
|
+
|
|
903
|
+
// If we cannot evaluate the key (no learning-key frontmatter), skip
|
|
904
|
+
// silently — avoid false positives on malformed rules.
|
|
905
|
+
if (!learningKey) continue;
|
|
906
|
+
|
|
907
|
+
if (!learningsPresent || !knownKeys.has(learningKey)) {
|
|
908
|
+
// Learning is absent from learnings.jsonl (or the file is missing entirely).
|
|
909
|
+
warnings.push({
|
|
910
|
+
check: 'generated-rule-staleness',
|
|
911
|
+
file: relPath,
|
|
912
|
+
line: 1,
|
|
913
|
+
message: `Auto-generated rule references learning-key '${learningKey}' which is absent from .orchestrator/metrics/learnings.jsonl`,
|
|
914
|
+
extracted: learningKey,
|
|
915
|
+
});
|
|
916
|
+
continue;
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
// Learning exists — check expiry.
|
|
920
|
+
const storedExpiresAt = knownKeys.get(learningKey);
|
|
921
|
+
// Prefer the frontmatter expires-at on the rule file; fall back to the
|
|
922
|
+
// stored expires_at from the learning entry (both should agree, but the
|
|
923
|
+
// rule file is authoritative for its own expiry).
|
|
924
|
+
const expiryStr = expiresAt ?? storedExpiresAt;
|
|
925
|
+
if (expiryStr) {
|
|
926
|
+
const expiryMs = Date.parse(expiryStr);
|
|
927
|
+
if (Number.isFinite(expiryMs) && expiryMs < nowMs) {
|
|
928
|
+
warnings.push({
|
|
929
|
+
check: 'generated-rule-staleness',
|
|
930
|
+
file: relPath,
|
|
931
|
+
line: 1,
|
|
932
|
+
message: `Auto-generated rule's learning '${learningKey}' expired on ${expiryStr} (rule should be reviewed or removed)`,
|
|
933
|
+
extracted: learningKey,
|
|
934
|
+
});
|
|
935
|
+
}
|
|
936
|
+
}
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
// Push the check id only when ≥1 generated rule was scanned.
|
|
940
|
+
checksRun.push('generated-rule-staleness');
|
|
941
|
+
})();
|
|
942
|
+
} else {
|
|
943
|
+
checksSkipped.push('generated-rule-staleness: explicitly skipped');
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
// Check 9: rule-scoping — validates .claude/rules/*.md frontmatter against
|
|
947
|
+
// the rule-loader.mjs contract (see the doc-comment above the helper
|
|
948
|
+
// functions for the five probes, incl. unreadable-file). Silently skipped (no id pushed, no
|
|
949
|
+
// checksSkipped entry) when .claude/rules/ is absent — mirrors Check 8's
|
|
950
|
+
// silent-skip semantics. `--skip-rule-scoping` disables the whole check.
|
|
951
|
+
if (!args.skipRuleScoping) {
|
|
952
|
+
(function runRuleScoping() {
|
|
953
|
+
const rulesDir = join(vaultDir, '.claude', 'rules');
|
|
954
|
+
if (!existsSync(rulesDir) || !statSync(rulesDir).isDirectory()) return;
|
|
955
|
+
|
|
956
|
+
checksRun.push('rule-scoping');
|
|
957
|
+
|
|
958
|
+
const ruleFileNames = readdirSync(rulesDir)
|
|
959
|
+
.filter((f) => f.endsWith('.md') && !f.startsWith('.'))
|
|
960
|
+
.filter((f) => statSync(join(rulesDir, f)).isFile());
|
|
961
|
+
const ruleFileSet = new Set(ruleFileNames);
|
|
962
|
+
|
|
963
|
+
// Lazily computed — only needed once any rule declares ≥1 glob pattern.
|
|
964
|
+
let trackedFiles = null;
|
|
965
|
+
|
|
966
|
+
for (const fname of ruleFileNames) {
|
|
967
|
+
const absPath = join(rulesDir, fname);
|
|
968
|
+
const relPath = relative(vaultDir, absPath);
|
|
969
|
+
let content;
|
|
970
|
+
try {
|
|
971
|
+
content = readFileSync(absPath, 'utf8');
|
|
972
|
+
} catch (err) {
|
|
973
|
+
warnings.push({
|
|
974
|
+
check: 'rule-scoping', file: relPath, line: 1,
|
|
975
|
+
message: `rule file unreadable — skipped from rule-scoping analysis (${err.code || err.message})`,
|
|
976
|
+
extracted: fname,
|
|
977
|
+
});
|
|
978
|
+
continue;
|
|
979
|
+
}
|
|
980
|
+
|
|
981
|
+
// --- Probe 1: paths-presence → errors[] ---
|
|
982
|
+
const fmBody = extractFrontmatterBlockBody(content);
|
|
983
|
+
if (fmBody && /^paths:/m.test(fmBody)) {
|
|
984
|
+
errors.push({
|
|
985
|
+
check: 'rule-scoping', file: relPath, line: 1,
|
|
986
|
+
message: `Rule frontmatter declares 'paths:' which rule-loader.mjs does not recognise — the rule silently loads ALWAYS-ON regardless of file scope. Migrate to 'globs:'.`,
|
|
987
|
+
extracted: 'paths:',
|
|
988
|
+
});
|
|
989
|
+
}
|
|
990
|
+
|
|
991
|
+
// --- Probes 3 & 4: zero-match-globs / foreign-glob → warnings[] ---
|
|
992
|
+
let parsed;
|
|
993
|
+
try { parsed = parseGlobsFrontmatter(content); } catch { parsed = { globs: null, meta: {} }; }
|
|
994
|
+
const globs = parsed.globs;
|
|
995
|
+
if (Array.isArray(globs) && globs.length > 0) {
|
|
996
|
+
if (trackedFiles === null) trackedFiles = listTrackedFiles(vaultDir);
|
|
997
|
+
for (const pattern of globs) {
|
|
998
|
+
if (!globMatchesAny(pattern, trackedFiles)) {
|
|
999
|
+
warnings.push({
|
|
1000
|
+
check: 'rule-scoping', file: relPath, line: 1,
|
|
1001
|
+
message: `glob '${pattern}' matches 0 tracked files (library/exemplar repos may legitimately carry dead stack rules)`,
|
|
1002
|
+
extracted: pattern,
|
|
1003
|
+
});
|
|
1004
|
+
}
|
|
1005
|
+
if (FOREIGN_GLOB_TOKEN_RE.test(pattern)) {
|
|
1006
|
+
warnings.push({
|
|
1007
|
+
check: 'rule-scoping', file: relPath, line: 1,
|
|
1008
|
+
message: `glob '${pattern}' contains a PascalCase product-like token — verify this rule was not copy-pasted from another project`,
|
|
1009
|
+
extracted: pattern,
|
|
1010
|
+
});
|
|
1011
|
+
}
|
|
1012
|
+
}
|
|
1013
|
+
}
|
|
1014
|
+
|
|
1015
|
+
// --- Probe 2b: cited-but-missing (this rule's own "## See Also" footer) → errors[] ---
|
|
1016
|
+
const lines = content.split('\n');
|
|
1017
|
+
let seeAlsoStart = -1;
|
|
1018
|
+
for (let i = 0; i < lines.length; i++) {
|
|
1019
|
+
if (/^##\s+See Also\s*$/i.test(lines[i])) { seeAlsoStart = i + 1; break; }
|
|
1020
|
+
}
|
|
1021
|
+
if (seeAlsoStart !== -1) {
|
|
1022
|
+
let seeAlsoEnd = lines.length;
|
|
1023
|
+
for (let i = seeAlsoStart; i < lines.length; i++) {
|
|
1024
|
+
if (/^#{1,6}\s+/.test(lines[i])) { seeAlsoEnd = i; break; }
|
|
1025
|
+
}
|
|
1026
|
+
const bodyLines = lines.slice(seeAlsoStart, seeAlsoEnd);
|
|
1027
|
+
for (const { token, lineOffset } of extractSeeAlsoTokens(bodyLines)) {
|
|
1028
|
+
if (!ruleFileSet.has(token)) {
|
|
1029
|
+
errors.push({
|
|
1030
|
+
check: 'rule-scoping', file: relPath, line: seeAlsoStart + lineOffset + 1,
|
|
1031
|
+
message: `'## See Also' cites '${token}' which does not exist in .claude/rules/`,
|
|
1032
|
+
extracted: token,
|
|
1033
|
+
});
|
|
1034
|
+
}
|
|
1035
|
+
}
|
|
1036
|
+
}
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1039
|
+
// --- Probe 2a: cited-but-missing (CLAUDE.md / AGENTS.md citations) → errors[] ---
|
|
1040
|
+
for (const instrName of ['CLAUDE.md', 'AGENTS.md']) {
|
|
1041
|
+
const filePath = join(vaultDir, instrName);
|
|
1042
|
+
if (!existsSync(filePath) || !statSync(filePath).isFile()) continue;
|
|
1043
|
+
const fcontent = readFileSync(filePath, 'utf8');
|
|
1044
|
+
const flines = fcontent.split('\n');
|
|
1045
|
+
const citeRe = /\.claude\/rules\/([A-Za-z0-9_-]+\.md)\b/g;
|
|
1046
|
+
for (let i = 0; i < flines.length; i++) {
|
|
1047
|
+
citeRe.lastIndex = 0;
|
|
1048
|
+
let m;
|
|
1049
|
+
while ((m = citeRe.exec(flines[i])) !== null) {
|
|
1050
|
+
const cited = m[1];
|
|
1051
|
+
if (!ruleFileSet.has(cited)) {
|
|
1052
|
+
errors.push({
|
|
1053
|
+
check: 'rule-scoping', file: instrName, line: i + 1,
|
|
1054
|
+
message: `References .claude/rules/${cited} which does not exist on disk`,
|
|
1055
|
+
extracted: cited,
|
|
1056
|
+
});
|
|
1057
|
+
}
|
|
1058
|
+
}
|
|
1059
|
+
}
|
|
1060
|
+
}
|
|
1061
|
+
})();
|
|
1062
|
+
} else {
|
|
1063
|
+
checksSkipped.push('rule-scoping: explicitly skipped');
|
|
1064
|
+
}
|
|
1065
|
+
|
|
1066
|
+
// Check 10: docs-parity (issue #780) — three sub-checks over the public docs
|
|
1067
|
+
// surface, silently skipped (no checksRun push, no checksSkipped entry) when
|
|
1068
|
+
// docs/components.md is absent (mirrors Check 8/9's silent-skip semantics):
|
|
1069
|
+
// (a) count-claims — docs/components.md's own heading counts ("## Skills
|
|
1070
|
+
// (N user-facing)", "## Commands (N)", "## Agents (N typed
|
|
1071
|
+
// sub-agents)", "## Hook event types (N)") vs the SAME actual on-disk
|
|
1072
|
+
// derivation the surface-count family (Check 5) uses (countSkills /
|
|
1073
|
+
// the commands-dir listing / countAgents / readHookCounts). This does
|
|
1074
|
+
// NOT reuse the surface-count family's `claimRe` regexes — those are
|
|
1075
|
+
// tuned for CLAUDE.md/README prose phrasing ("40 skills", "8 distinct
|
|
1076
|
+
// events") and verifiably do not match components.md's own heading
|
|
1077
|
+
// convention (count precedes/follows the noun differently); reusing
|
|
1078
|
+
// them verbatim would silently never fire. New regexes tailored to
|
|
1079
|
+
// the doc's actual authored structure are used instead.
|
|
1080
|
+
// (b) config-block-parity — top-level Session Config keys documented in
|
|
1081
|
+
// docs/session-config-template.md (opt-in baseline, via the Part 1
|
|
1082
|
+
// 'last'-occurrence extractor) vs mentioned anywhere in
|
|
1083
|
+
// docs/session-config-reference.md (`extractDocKeyMentions` — yaml
|
|
1084
|
+
// fences, headings, and markdown-table first-cell keys; see its
|
|
1085
|
+
// doc-comment for why a naive fence-only extraction under-counts).
|
|
1086
|
+
// (c) metrics-path-liveness — stale `.claude/metrics/` path references
|
|
1087
|
+
// (canonical path is `.orchestrator/metrics/`) in root `docs/*.md` or
|
|
1088
|
+
// `docs/examples/*.md`.
|
|
1089
|
+
// `--skip-docs-parity` disables the whole check.
|
|
1090
|
+
if (!args.skipDocsParity) {
|
|
1091
|
+
(function runDocsParity() {
|
|
1092
|
+
const componentsPath = join(vaultDir, 'docs', 'components.md');
|
|
1093
|
+
if (!existsSync(componentsPath) || !statSync(componentsPath).isFile()) return; // silent skip
|
|
1094
|
+
|
|
1095
|
+
checksRun.push('docs-parity');
|
|
1096
|
+
const componentsRel = relative(vaultDir, componentsPath);
|
|
1097
|
+
const componentsContent = readFileSync(componentsPath, 'utf8');
|
|
1098
|
+
const componentsLines = componentsContent.split('\n');
|
|
1099
|
+
|
|
1100
|
+
// --- Sub-check (a): count-claims ---
|
|
1101
|
+
const docsParitySurfaces = [
|
|
1102
|
+
{
|
|
1103
|
+
noun: 'skills',
|
|
1104
|
+
actual: countSkills(vaultDir),
|
|
1105
|
+
re: /^##\s+Skills\s+\((\d+)\s+user-facing\)/i,
|
|
1106
|
+
},
|
|
1107
|
+
{
|
|
1108
|
+
noun: 'commands',
|
|
1109
|
+
actual: (() => {
|
|
1110
|
+
const dir = commandsDir || join(vaultDir, 'commands');
|
|
1111
|
+
if (!existsSync(dir) || !statSync(dir).isDirectory()) return null;
|
|
1112
|
+
return readdirSync(dir).filter((f) => f.endsWith('.md') && !f.startsWith('.')).length;
|
|
1113
|
+
})(),
|
|
1114
|
+
re: /^##\s+Commands\s+\((\d+)\)/i,
|
|
1115
|
+
},
|
|
1116
|
+
{
|
|
1117
|
+
noun: 'agents',
|
|
1118
|
+
actual: countAgents(vaultDir),
|
|
1119
|
+
re: /^##\s+Agents\s+\((\d+)\s+typed\s+sub-agents\)/i,
|
|
1120
|
+
},
|
|
1121
|
+
{
|
|
1122
|
+
noun: 'distinct hook events',
|
|
1123
|
+
actual: (() => {
|
|
1124
|
+
const hc = readHookCounts(vaultDir);
|
|
1125
|
+
return hc ? hc.events : null;
|
|
1126
|
+
})(),
|
|
1127
|
+
re: /^##\s+Hook\s+event\s+types\s+\((\d+)\)/i,
|
|
1128
|
+
},
|
|
1129
|
+
];
|
|
1130
|
+
for (let i = 0; i < componentsLines.length; i++) {
|
|
1131
|
+
const line = componentsLines[i];
|
|
1132
|
+
for (const surface of docsParitySurfaces) {
|
|
1133
|
+
if (surface.actual === null) continue; // artifact absent — nothing to compare
|
|
1134
|
+
const m = surface.re.exec(line);
|
|
1135
|
+
if (m) {
|
|
1136
|
+
const claimed = parseInt(m[1], 10);
|
|
1137
|
+
if (claimed !== surface.actual) {
|
|
1138
|
+
errors.push({
|
|
1139
|
+
check: 'docs-parity', file: componentsRel, line: i + 1,
|
|
1140
|
+
message: `docs/components.md claims ${claimed} ${surface.noun} but actual on-disk count is ${surface.actual}`,
|
|
1141
|
+
extracted: m[0],
|
|
1142
|
+
});
|
|
1143
|
+
}
|
|
1144
|
+
}
|
|
1145
|
+
}
|
|
1146
|
+
}
|
|
1147
|
+
|
|
1148
|
+
// --- Sub-check (b): config-block-parity ---
|
|
1149
|
+
const templatePathForDocsParity = join(vaultDir, 'docs', 'session-config-template.md');
|
|
1150
|
+
const referencePath = join(vaultDir, 'docs', 'session-config-reference.md');
|
|
1151
|
+
if (
|
|
1152
|
+
existsSync(templatePathForDocsParity) && statSync(templatePathForDocsParity).isFile() &&
|
|
1153
|
+
existsSync(referencePath) && statSync(referencePath).isFile()
|
|
1154
|
+
) {
|
|
1155
|
+
const templateContentForDocsParity = readFileSync(templatePathForDocsParity, 'utf8');
|
|
1156
|
+
const referenceContent = readFileSync(referencePath, 'utf8');
|
|
1157
|
+
const tplBlockForDocsParity = extractSessionConfigBlock(templateContentForDocsParity, { occurrence: 'last' });
|
|
1158
|
+
if (tplBlockForDocsParity) {
|
|
1159
|
+
const tplKeysForDocsParity = extractTopLevelKeys(tplBlockForDocsParity.body);
|
|
1160
|
+
const refKeys = extractDocKeyMentions(referenceContent);
|
|
1161
|
+
const referenceRel = relative(vaultDir, referencePath);
|
|
1162
|
+
for (const key of tplKeysForDocsParity) {
|
|
1163
|
+
if (!refKeys.has(key)) {
|
|
1164
|
+
errors.push({
|
|
1165
|
+
check: 'docs-parity', file: referenceRel, line: 1,
|
|
1166
|
+
message: `Session Config key '${key}' (documented in docs/session-config-template.md) is not documented in docs/session-config-reference.md`,
|
|
1167
|
+
extracted: key,
|
|
1168
|
+
});
|
|
1169
|
+
}
|
|
1170
|
+
}
|
|
1171
|
+
}
|
|
1172
|
+
}
|
|
1173
|
+
|
|
1174
|
+
// --- Sub-check (c): metrics-path-liveness ---
|
|
1175
|
+
const docsDir = join(vaultDir, 'docs');
|
|
1176
|
+
const metricsScanTargets = [];
|
|
1177
|
+
if (existsSync(docsDir) && statSync(docsDir).isDirectory()) {
|
|
1178
|
+
for (const f of readdirSync(docsDir, { withFileTypes: true })) {
|
|
1179
|
+
if (f.isFile() && f.name.endsWith('.md')) metricsScanTargets.push(join(docsDir, f.name));
|
|
1180
|
+
}
|
|
1181
|
+
const examplesDir = join(docsDir, 'examples');
|
|
1182
|
+
if (existsSync(examplesDir) && statSync(examplesDir).isDirectory()) {
|
|
1183
|
+
for (const f of readdirSync(examplesDir, { withFileTypes: true })) {
|
|
1184
|
+
if (f.isFile() && f.name.endsWith('.md')) metricsScanTargets.push(join(examplesDir, f.name));
|
|
1185
|
+
}
|
|
1186
|
+
}
|
|
1187
|
+
}
|
|
1188
|
+
for (const abs of metricsScanTargets) {
|
|
1189
|
+
const rel = relative(vaultDir, abs);
|
|
1190
|
+
let content;
|
|
1191
|
+
try { content = readFileSync(abs, 'utf8'); } catch { continue; }
|
|
1192
|
+
const metricsLines = content.split('\n');
|
|
1193
|
+
for (let i = 0; i < metricsLines.length; i++) {
|
|
1194
|
+
if (metricsLines[i].includes('.claude/metrics/')) {
|
|
1195
|
+
errors.push({
|
|
1196
|
+
check: 'docs-parity', file: rel, line: i + 1,
|
|
1197
|
+
message: `Stale metrics path '.claude/metrics/' referenced — canonical path is '.orchestrator/metrics/'`,
|
|
1198
|
+
extracted: '.claude/metrics/',
|
|
1199
|
+
});
|
|
1200
|
+
}
|
|
1201
|
+
}
|
|
1202
|
+
}
|
|
1203
|
+
})();
|
|
1204
|
+
} else {
|
|
1205
|
+
checksSkipped.push('docs-parity: explicitly skipped');
|
|
1206
|
+
}
|
|
1207
|
+
|
|
1208
|
+
if (scopeFiles.length === 0) {
|
|
1209
|
+
process.stdout.write(JSON.stringify({
|
|
1210
|
+
status: 'skipped', mode: args.mode, vault_dir: vaultDir,
|
|
1211
|
+
resolved_path: resolvedPath, resolved_kind: resolvedKind,
|
|
1212
|
+
files_scanned: 0, checks_run: checksRun, checks_skipped: checksSkipped,
|
|
1213
|
+
errors, warnings, reason: 'no scope files matched',
|
|
1214
|
+
}) + '\n');
|
|
1215
|
+
process.exit(errors.length > 0 && args.mode === 'hard' ? 1 : 0);
|
|
1216
|
+
}
|
|
1217
|
+
|
|
1218
|
+
for (const abs of scopeFiles) {
|
|
1219
|
+
const rel = relative(vaultDir, abs);
|
|
1220
|
+
const content = readFileSync(abs, 'utf8');
|
|
1221
|
+
const lines = content.split('\n');
|
|
1222
|
+
|
|
1223
|
+
let inFence = false;
|
|
1224
|
+
let currentSection = null;
|
|
1225
|
+
let currentSectionType = null;
|
|
1226
|
+
|
|
1227
|
+
for (let i = 0; i < lines.length; i++) {
|
|
1228
|
+
const line = lines[i];
|
|
1229
|
+
const lineNum = i + 1;
|
|
1230
|
+
|
|
1231
|
+
if (/^\s*```/.test(line)) { inFence = !inFence; continue; }
|
|
1232
|
+
|
|
1233
|
+
const hmatch = /^(#{1,6})\s+(.+?)\s*$/.exec(line);
|
|
1234
|
+
if (hmatch && !inFence) {
|
|
1235
|
+
currentSection = hmatch[2];
|
|
1236
|
+
currentSectionType = classifySection(currentSection);
|
|
1237
|
+
continue;
|
|
1238
|
+
}
|
|
1239
|
+
|
|
1240
|
+
if (!args.skipPathResolver && !inFence) {
|
|
1241
|
+
const pathRegex = /\/Users\/[A-Za-z0-9._/-]+/g;
|
|
1242
|
+
let m;
|
|
1243
|
+
while ((m = pathRegex.exec(line)) !== null) {
|
|
1244
|
+
const p = m[0].replace(/[.,;:)\]`"']+$/, '');
|
|
1245
|
+
if (!existsSync(p)) {
|
|
1246
|
+
errors.push({
|
|
1247
|
+
check: 'path-resolver', file: rel, line: lineNum,
|
|
1248
|
+
message: `Absolute path does not exist: ${p}`,
|
|
1249
|
+
extracted: p,
|
|
1250
|
+
});
|
|
1251
|
+
}
|
|
1252
|
+
}
|
|
1253
|
+
}
|
|
1254
|
+
|
|
1255
|
+
if (!args.skipProjectCount && actualProjectCount !== null) {
|
|
1256
|
+
const countRegex = /\((\d+)\s+(registered|projects?|subfolders?)\)/gi;
|
|
1257
|
+
let m;
|
|
1258
|
+
while ((m = countRegex.exec(line)) !== null) {
|
|
1259
|
+
const claimed = parseInt(m[1], 10);
|
|
1260
|
+
if (claimed !== actualProjectCount) {
|
|
1261
|
+
errors.push({
|
|
1262
|
+
check: 'project-count-sync', file: rel, line: lineNum,
|
|
1263
|
+
message: `Hardcoded count claims ${claimed} ${m[2]} but actual 01-projects/ count is ${actualProjectCount}`,
|
|
1264
|
+
extracted: m[0],
|
|
1265
|
+
});
|
|
1266
|
+
}
|
|
1267
|
+
}
|
|
1268
|
+
}
|
|
1269
|
+
|
|
1270
|
+
if (runIssueCheck && currentSectionType === 'forward' && !inFence) {
|
|
1271
|
+
const issueRegex = /#(\d+)\b/g;
|
|
1272
|
+
let m;
|
|
1273
|
+
while ((m = issueRegex.exec(line)) !== null) {
|
|
1274
|
+
const iid = m[1];
|
|
1275
|
+
const state = lookupIssueState(iid, repo, issueCache);
|
|
1276
|
+
if (state === 'closed') {
|
|
1277
|
+
errors.push({
|
|
1278
|
+
check: 'issue-reference-freshness', file: rel, line: lineNum,
|
|
1279
|
+
message: `Issue #${iid} is closed but referenced in forward-looking section "${currentSection}"`,
|
|
1280
|
+
extracted: `#${iid}`,
|
|
1281
|
+
});
|
|
1282
|
+
} else if (state === 'unknown') {
|
|
1283
|
+
warnings.push({
|
|
1284
|
+
check: 'issue-reference-freshness', file: rel, line: lineNum,
|
|
1285
|
+
message: `Could not resolve state of issue #${iid} via glab (may not exist or auth issue)`,
|
|
1286
|
+
extracted: `#${iid}`,
|
|
1287
|
+
});
|
|
1288
|
+
}
|
|
1289
|
+
}
|
|
1290
|
+
}
|
|
1291
|
+
|
|
1292
|
+
if (!args.skipSessionFiles) {
|
|
1293
|
+
// Dual-read: tolerate both flat legacy `50-sessions/<id>.md` AND
|
|
1294
|
+
// per-repo namespaced `50-sessions/<repo>/<id>.md` (Issue #660).
|
|
1295
|
+
const sessionRegex = /50-sessions\/(?:([a-z0-9][a-z0-9-]*)\/)?(\d{4}-\d{2}-\d{2}-[a-z0-9-]+)\.md/g;
|
|
1296
|
+
let m;
|
|
1297
|
+
while ((m = sessionRegex.exec(line)) !== null) {
|
|
1298
|
+
const repo = m[1]; // optional repo segment (undefined if flat reference)
|
|
1299
|
+
const id = m[2]; // session date-slug
|
|
1300
|
+
let found = false;
|
|
1301
|
+
if (repo !== undefined) {
|
|
1302
|
+
// Namespaced reference: only check the specific namespaced path.
|
|
1303
|
+
found = existsSync(join(vaultDir, '50-sessions', repo, `${id}.md`));
|
|
1304
|
+
} else {
|
|
1305
|
+
// Flat reference: accept if the flat path exists OR if ANY per-repo
|
|
1306
|
+
// subfolder contains a file with the same id (migration tolerance).
|
|
1307
|
+
const flatPath = join(vaultDir, '50-sessions', `${id}.md`);
|
|
1308
|
+
if (existsSync(flatPath)) {
|
|
1309
|
+
found = true;
|
|
1310
|
+
} else {
|
|
1311
|
+
// Scan one level of subdirectories under 50-sessions/ for <id>.md.
|
|
1312
|
+
const sessionsDir = join(vaultDir, '50-sessions');
|
|
1313
|
+
try {
|
|
1314
|
+
const subdirs = readdirSync(sessionsDir, { withFileTypes: true })
|
|
1315
|
+
.filter((e) => e.isDirectory())
|
|
1316
|
+
.map((e) => e.name);
|
|
1317
|
+
found = subdirs.some((sub) =>
|
|
1318
|
+
existsSync(join(sessionsDir, sub, `${id}.md`))
|
|
1319
|
+
);
|
|
1320
|
+
} catch {
|
|
1321
|
+
// sessionsDir does not exist or is unreadable — found stays false.
|
|
1322
|
+
}
|
|
1323
|
+
}
|
|
1324
|
+
}
|
|
1325
|
+
if (!found) {
|
|
1326
|
+
errors.push({
|
|
1327
|
+
check: 'session-file-existence', file: rel, line: lineNum,
|
|
1328
|
+
message: `Referenced session file does not exist: ${m[0]}`,
|
|
1329
|
+
extracted: m[0],
|
|
1330
|
+
});
|
|
1331
|
+
}
|
|
1332
|
+
}
|
|
1333
|
+
}
|
|
1334
|
+
|
|
1335
|
+
// Surface-count family: one EXACT-count drift check per active surface.
|
|
1336
|
+
// A surface only fires when the doc makes an explicit numeric claim that
|
|
1337
|
+
// its regex matches; an unclaimed surface produces no errors (skip).
|
|
1338
|
+
for (const surface of activeSurfaces) {
|
|
1339
|
+
const re = surface.claimRe;
|
|
1340
|
+
re.lastIndex = 0; // reset shared /g/ regex before each line
|
|
1341
|
+
let m;
|
|
1342
|
+
while ((m = re.exec(line)) !== null) {
|
|
1343
|
+
const claimed = parseInt(m[1], 10);
|
|
1344
|
+
if (claimed !== surface.actual) {
|
|
1345
|
+
const err = {
|
|
1346
|
+
check: surface.id, file: rel, line: lineNum,
|
|
1347
|
+
message: `Narrative claims ${claimed} ${surface.noun} but actual on-disk count is ${surface.actual}`,
|
|
1348
|
+
extracted: m[0],
|
|
1349
|
+
count: { surface: surface.id, actual: surface.actual, claimed },
|
|
1350
|
+
};
|
|
1351
|
+
// Back-compat: the original command-count error also carried a
|
|
1352
|
+
// top-level `command_count` field; preserve it.
|
|
1353
|
+
if (surface.id === 'command-count') {
|
|
1354
|
+
err.command_count = { actual: surface.actual, claimed };
|
|
1355
|
+
}
|
|
1356
|
+
errors.push(err);
|
|
1357
|
+
}
|
|
1358
|
+
}
|
|
1359
|
+
}
|
|
1360
|
+
}
|
|
1361
|
+
}
|
|
1362
|
+
|
|
1363
|
+
const status = errors.length === 0 ? 'ok' : 'invalid';
|
|
1364
|
+
const result = {
|
|
1365
|
+
status, mode: args.mode, vault_dir: vaultDir,
|
|
1366
|
+
resolved_path: resolvedPath, resolved_kind: resolvedKind,
|
|
1367
|
+
files_scanned: scopeFiles.length,
|
|
1368
|
+
checks_run: checksRun,
|
|
1369
|
+
checks_skipped: checksSkipped,
|
|
1370
|
+
errors, warnings,
|
|
1371
|
+
};
|
|
1372
|
+
if (actualCommandCount !== null) {
|
|
1373
|
+
result.command_count = { actual: actualCommandCount };
|
|
1374
|
+
}
|
|
1375
|
+
process.stdout.write(JSON.stringify(result) + '\n');
|
|
1376
|
+
|
|
1377
|
+
process.exit(errors.length > 0 && args.mode === 'hard' ? 1 : 0);
|
|
1378
|
+
}
|
|
1379
|
+
|
|
1380
|
+
main();
|