session-orchestrator 3.23.0 → 4.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.agents/skills/architecture/SKILL.md +18 -0
- package/.agents/skills/autopilot/SKILL.md +17 -0
- package/.agents/skills/bootstrap/SKILL.md +20 -0
- package/.agents/skills/brainstorm/SKILL.md +22 -0
- package/.agents/skills/claude-md-drift-check/SKILL.md +15 -0
- package/.agents/skills/convergence-monitoring/SKILL.md +22 -0
- package/.agents/skills/debug/SKILL.md +22 -0
- package/.agents/skills/discovery/SKILL.md +20 -0
- package/.agents/skills/dispatcher/SKILL.md +15 -0
- package/.agents/skills/docs-orchestrator/SKILL.md +18 -0
- package/.agents/skills/ecosystem-health/SKILL.md +20 -0
- package/.agents/skills/eli5/SKILL.md +20 -0
- package/.agents/skills/eval/SKILL.md +21 -0
- package/.agents/skills/evolve/SKILL.md +21 -0
- package/.agents/skills/frontmatter-guard/SKILL.md +15 -0
- package/.agents/skills/gitlab-ops/SKILL.md +20 -0
- package/.agents/skills/gitlab-portfolio/SKILL.md +15 -0
- package/.agents/skills/grill/SKILL.md +22 -0
- package/.agents/skills/hook-development/SKILL.md +15 -0
- package/.agents/skills/mcp-builder/SKILL.md +15 -0
- package/.agents/skills/memory-cleanup/SKILL.md +21 -0
- package/.agents/skills/mode-selector/SKILL.md +17 -0
- package/.agents/skills/npm-publish/SKILL.md +16 -0
- package/.agents/skills/peekaboo-driver/SKILL.md +18 -0
- package/.agents/skills/persona-panel/SKILL.md +17 -0
- package/.agents/skills/plan/SKILL.md +20 -0
- package/.agents/skills/playwright-driver/SKILL.md +20 -0
- package/.agents/skills/quality-gates/SKILL.md +20 -0
- package/.agents/skills/reconcile/SKILL.md +21 -0
- package/.agents/skills/remote-offload/SKILL.md +20 -0
- package/.agents/skills/repo-audit/SKILL.md +16 -0
- package/.agents/skills/session-end/SKILL.md +20 -0
- package/.agents/skills/session-plan/SKILL.md +20 -0
- package/.agents/skills/session-start/SKILL.md +20 -0
- package/.agents/skills/spinout/SKILL.md +16 -0
- package/.agents/skills/sunset-review/SKILL.md +16 -0
- package/.agents/skills/test-runner/SKILL.md +20 -0
- package/.agents/skills/tmux-layout/SKILL.md +21 -0
- package/.agents/skills/using-orchestrator/SKILL.md +17 -0
- package/.agents/skills/vault-mirror/SKILL.md +15 -0
- package/.agents/skills/vault-sync/SKILL.md +15 -0
- package/.agents/skills/wave-executor/SKILL.md +20 -0
- package/.agents/skills/write-executable-plan/SKILL.md +22 -0
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/.cursor/commands/autopilot.md +2 -2
- package/.cursor/commands/bootstrap.md +1 -1
- package/.cursor/commands/brainstorm.md +1 -1
- package/.cursor/commands/debug.md +1 -1
- package/.cursor/commands/discovery.md +1 -1
- package/.cursor/commands/dispatcher.md +2 -2
- package/.cursor/commands/eli5.md +2 -2
- package/.cursor/commands/eval.md +2 -2
- package/.cursor/commands/evolve.md +1 -1
- package/.cursor/commands/go.md +1 -1
- package/.cursor/commands/grill.md +2 -2
- package/.cursor/commands/memory-cleanup.md +2 -2
- package/.cursor/commands/persona-panel.md +1 -1
- package/.cursor/commands/plan.md +1 -1
- package/.cursor/commands/portfolio.md +1 -1
- package/.cursor/commands/reconcile.md +2 -2
- package/.cursor/commands/release.md +2 -2
- package/.cursor/commands/session.md +2 -2
- package/.cursor/commands/spinout.md +2 -2
- package/.cursor/commands/sunset-review.md +2 -2
- package/.cursor/commands/templates-ack.md +2 -2
- package/.cursor/commands/test.md +2 -2
- package/.cursor/skills/brainstorm/SKILL.md +1 -1
- package/.cursor/skills/eval/SKILL.md +1 -1
- package/.cursor/skills/quality-gates/SKILL.md +1 -1
- package/.cursor/skills/remote-offload/SKILL.md +13 -0
- package/.orchestrator/policy/blocked-commands.json +121 -0
- package/.orchestrator/policy/ecosystem.schema.json +66 -0
- package/.orchestrator/policy/quality-gates.example.json +16 -0
- package/.orchestrator/policy/quality-gates.schema.json +38 -0
- package/.orchestrator/policy/templates-policy.json +27 -0
- package/.orchestrator/policy/test-profiles.json +47 -0
- package/AGENTS.md +225 -0
- package/CHANGELOG.md +1401 -0
- package/NOTICE +11 -6
- package/README.md +127 -92
- package/agents/db-specialist.md +0 -1
- package/agents/eval-judge.md +1 -1
- package/agents/skill-applied-judge.md +1 -1
- package/assets/wave-lifecycle.svg +98 -0
- package/commands/release.md +6 -3
- package/commands/session.md +18 -3
- package/docs/README.md +4 -0
- package/{agents/AGENTS.md → docs/agent-authoring.md} +19 -26
- package/docs/baseline.md +67 -0
- package/docs/ci-setup.md +249 -48
- package/docs/codex-setup.md +66 -22
- package/docs/components.md +37 -16
- package/docs/cursor-setup.md +6 -2
- package/docs/events-schema.md +51 -10
- package/docs/instruction-delivery.md +62 -0
- package/{agents/memory-proposal-collector.md → docs/memory-proposal-flow.md} +1 -8
- package/docs/migration-v4.md +341 -0
- package/docs/pi-setup.md +6 -1
- package/docs/plugin-architecture-v3.md +1 -1
- package/docs/rule-authoring.md +85 -19
- package/docs/scope-collision-guard.md +8 -8
- package/docs/session-config-reference.md +120 -61
- package/docs/session-config-template.md +40 -33
- package/docs/telemetry/telemetry-claims.md +11 -10
- package/docs/telemetry.md +187 -4
- package/docs/vault-docs-architecture.md +50 -11
- package/hooks/_lib/atomic-json.mjs +111 -0
- package/hooks/_lib/hook-import-set.json +1487 -0
- package/hooks/_lib/subagent-paths.mjs +143 -0
- package/hooks/_lib/subagent-transcript.mjs +562 -0
- package/hooks/config-protection.mjs +2 -2
- package/hooks/cwd-change-restore.mjs +11 -31
- package/hooks/enforce-commands.mjs +69 -0
- package/hooks/enforce-scope.mjs +35 -6
- package/hooks/hooks-codex.json +1 -1
- package/hooks/hooks-cursor.json +10 -0
- package/hooks/hooks-pi.json +5 -0
- package/hooks/hooks.json +6 -1
- package/hooks/loop-guard.mjs +3 -3
- package/hooks/on-session-end.mjs +280 -14
- package/hooks/on-session-start.mjs +153 -4
- package/hooks/on-stop.mjs +371 -17
- package/hooks/operator-steer.mjs +2 -2
- package/hooks/post-bash-write-verify.mjs +189 -4
- package/hooks/post-edit-import-probe.mjs +344 -0
- package/hooks/post-subagent-discovery-validator.mjs +278 -392
- package/hooks/post-tool-batch-wave-signal.mjs +272 -44
- package/hooks/post-tool-failure-corrective-context.mjs +11 -34
- package/hooks/post-tooluse-frontend-slop.mjs +3 -3
- package/hooks/pre-bash-destructive-guard.mjs +39 -13
- package/hooks/pre-bash-memory-propose-audit.mjs +13 -7
- package/hooks/skill-invocation-telemetry.mjs +17 -5
- package/hooks/subagent-telemetry.mjs +24 -30
- package/monitors/monitors.json +3 -3
- package/package.json +9 -1
- package/pi/prompts/session.md +2 -2
- package/plugin.json +27 -0
- package/scripts/autopilot.mjs +26 -12
- package/scripts/backfill-abandoned-sessions.mjs +130 -15
- package/scripts/backfill-learnings-from-vault.mjs +9 -3
- package/scripts/dialectic-deriver.mjs +73 -8
- package/scripts/emit-event.mjs +10 -2
- package/scripts/export-hw-learnings.mjs +113 -1
- package/scripts/generate-agents-skills.mjs +378 -0
- package/scripts/generate-cursor-adapter.mjs +45 -8
- package/scripts/generate-hook-import-set.mjs +249 -0
- package/scripts/lib/agent-status.mjs +13 -2
- package/scripts/lib/auq/parse.mjs +5 -29
- package/scripts/lib/auto-dialectic.mjs +68 -0
- package/scripts/lib/auto-dream.mjs +38 -36
- package/scripts/lib/autonomy/suitability.mjs +6 -0
- package/scripts/lib/autopilot/loop.mjs +2 -2
- package/scripts/lib/autopilot/worktree-pipeline.mjs +82 -6
- package/scripts/lib/build-live-signals.mjs +25 -22
- package/scripts/lib/ci-status-banner.mjs +220 -75
- package/scripts/lib/codex/plugin-contract.mjs +82 -6
- package/scripts/lib/cold-start-detector.mjs +23 -14
- package/scripts/lib/config/auto-dream.mjs +2 -1
- package/scripts/lib/config/block-header.mjs +63 -0
- package/scripts/lib/config/block-preprocess.mjs +177 -0
- package/scripts/lib/config/broken-window.mjs +2 -1
- package/scripts/lib/config/cold-start.mjs +2 -1
- package/scripts/lib/config/config-protection.mjs +22 -2
- package/scripts/lib/config/context-coverage.mjs +2 -1
- package/scripts/lib/config/cross-repo.mjs +2 -1
- package/scripts/lib/config/custom-phases.mjs +2 -1
- package/scripts/lib/config/dialectic.mjs +2 -1
- package/scripts/lib/config/discovery-validator.mjs +9 -3
- package/scripts/lib/config/dispatcher-autonomy-capture.mjs +24 -1
- package/scripts/lib/config/dispatcher-autonomy.mjs +2 -1
- package/scripts/lib/config/docs-orchestrator.mjs +2 -1
- package/scripts/lib/config/docs-staleness.mjs +2 -1
- package/scripts/lib/config/drift-check.mjs +2 -1
- package/scripts/lib/config/eval.mjs +2 -1
- package/scripts/lib/config/events-rotation.mjs +2 -1
- package/scripts/lib/config/evolve.mjs +8 -2
- package/scripts/lib/config/frontend-slop-hook.mjs +7 -3
- package/scripts/lib/config/gitlab-portfolio.mjs +2 -1
- package/scripts/lib/config/handover-gate.mjs +2 -1
- package/scripts/lib/config/health-endpoints.mjs +388 -0
- package/scripts/lib/config/issue-budget.mjs +2 -1
- package/scripts/lib/config/loop-guard.mjs +2 -1
- package/scripts/lib/config/memory.mjs +2 -1
- package/scripts/lib/config/moc-staleness.mjs +2 -1
- package/scripts/lib/config/persona-gate-wave.mjs +2 -1
- package/scripts/lib/config/private-config-dir.mjs +67 -0
- package/scripts/lib/config/reconcile.mjs +2 -1
- package/scripts/lib/config/remote-hosts.mjs +234 -0
- package/scripts/lib/config/section-extractor.mjs +7 -1
- package/scripts/lib/config/skill-evolution.mjs +2 -1
- package/scripts/lib/config/slopcheck.mjs +2 -1
- package/scripts/lib/config/state-md-lock.mjs +2 -1
- package/scripts/lib/config/templates-first.mjs +2 -1
- package/scripts/lib/config/test.mjs +2 -1
- package/scripts/lib/config/vault-integration.mjs +7 -1
- package/scripts/lib/config/vault-mirror-quality.mjs +2 -1
- package/scripts/lib/config/vault-staleness.mjs +2 -1
- package/scripts/lib/config/vault-sync.mjs +2 -1
- package/scripts/lib/config/verification-auto-fix.mjs +2 -1
- package/scripts/lib/config/wave-reviewers.mjs +2 -1
- package/scripts/lib/config/worktree-orphans.mjs +2 -1
- package/scripts/lib/config.mjs +31 -3
- package/scripts/lib/convergence-monitor.mjs +82 -16
- package/scripts/lib/dispatcher/enumerate.mjs +2 -17
- package/scripts/lib/dispatcher/rank.mjs +124 -48
- package/scripts/lib/ecosystem-health.mjs +16 -2
- package/scripts/lib/eval/engine.mjs +9 -1
- package/scripts/lib/eval/session-resolve.mjs +23 -4
- package/scripts/lib/events-schema.mjs +48 -0
- package/scripts/lib/events.mjs +256 -7
- package/scripts/lib/evolve/autonomy-verdict.mjs +9 -4
- package/scripts/lib/evolve/autopilot-effectiveness.mjs +18 -1
- package/scripts/lib/frontmatter-guard.mjs +131 -13
- package/scripts/lib/gates/gate-full.mjs +26 -0
- package/scripts/lib/gates/gate-helpers.mjs +76 -0
- package/scripts/lib/gitlab-portfolio/cli.mjs +3 -15
- package/scripts/lib/hardware-pattern-detector.mjs +18 -1
- package/scripts/lib/harness-audit/categories/category1.mjs +17 -6
- package/scripts/lib/harness-audit/categories/category4.mjs +31 -11
- package/scripts/lib/host-identity.mjs +50 -11
- package/scripts/lib/instruction-budget-guard.mjs +171 -5
- package/scripts/lib/learnings/evolve-telemetry.mjs +178 -0
- package/scripts/lib/learnings/io.mjs +60 -6
- package/scripts/lib/memory-banner.mjs +20 -8
- package/scripts/lib/memory-proposals/store.mjs +30 -22
- package/scripts/lib/owner-config-banner.mjs +43 -6
- package/scripts/lib/owner-config-loader.mjs +21 -10
- package/scripts/lib/owner-interview.mjs +3 -3
- package/scripts/lib/owner-yaml.mjs +207 -14
- package/scripts/lib/peer-discovery.mjs +20 -2
- package/scripts/lib/platform.mjs +108 -15
- package/scripts/lib/plugin-update-banner.mjs +406 -0
- package/scripts/lib/project-hygiene.mjs +38 -2
- package/scripts/lib/qg-command-drift-banner.mjs +50 -12
- package/scripts/lib/quality-gate.mjs +133 -44
- package/scripts/lib/reconcile/emitter.mjs +68 -6
- package/scripts/lib/reconcile/engine.mjs +249 -9
- package/scripts/lib/reconcile/idempotency.mjs +37 -4
- package/scripts/lib/reconcile/writer.mjs +40 -18
- package/scripts/lib/scope-gate.mjs +36 -0
- package/scripts/lib/session-close-backfill.mjs +125 -18
- package/scripts/lib/session-discovery.mjs +57 -3
- package/scripts/lib/session-end/phase-skip.mjs +2 -2
- package/scripts/lib/session-id.mjs +12 -23
- package/scripts/lib/session-identity/own-session.mjs +187 -11
- package/scripts/lib/session-lock-shape.mjs +43 -0
- package/scripts/lib/session-lock.mjs +5 -10
- package/scripts/lib/session-registry.mjs +25 -9
- package/scripts/lib/session-schema/constants.mjs +36 -2
- package/scripts/lib/session-schema/validator.mjs +38 -4
- package/scripts/lib/session-start-probes.mjs +18 -1
- package/scripts/lib/session-transition.mjs +1 -1
- package/scripts/lib/sessions-canonical.mjs +446 -0
- package/scripts/lib/sessions-staleness-banner.mjs +18 -11
- package/scripts/lib/skill-health/join.mjs +17 -4
- package/scripts/lib/state-md.mjs +78 -0
- package/scripts/lib/sunset/walker.mjs +6 -0
- package/scripts/lib/telemetry/schema.mjs +255 -17
- package/scripts/lib/telemetry/sync.mjs +417 -24
- package/scripts/lib/tmux-layout/telemetry.mjs +14 -2
- package/scripts/lib/validate/check-agents-skills.mjs +327 -0
- package/scripts/lib/validate/check-agents.mjs +3 -3
- package/scripts/lib/validate/check-cursor-adapter.mjs +234 -72
- package/scripts/lib/validate/check-doc-cli-commands.mjs +9 -33
- package/scripts/lib/validate/check-hooks-emit-event-guard.mjs +370 -0
- package/scripts/lib/validate/check-hooks-symmetry.mjs +45 -16
- package/scripts/lib/validate/check-owner-leakage.mjs +281 -20
- package/scripts/lib/validate/check-skill-links.mjs +163 -0
- package/scripts/lib/validate/check-skill-script-paths.mjs +455 -0
- package/scripts/lib/validate/check-untracked-test-deps.mjs +10 -0
- package/scripts/lib/validate/check-unwired-features.mjs +0 -9
- package/scripts/lib/validate/check-validator-registration.mjs +254 -0
- package/scripts/lib/validate/check-vcs-repo-flag.mjs +6 -28
- package/scripts/lib/validate/enumerate-repo-files.mjs +317 -0
- package/scripts/lib/validate/markdown-fences.mjs +196 -0
- package/scripts/lib/vault-backfill/template.mjs +63 -6
- package/scripts/lib/vault-mirror/process.mjs +165 -42
- package/scripts/lib/vault-mirror/telemetry.mjs +2 -2
- package/scripts/lib/vault-status/board-lock.mjs +185 -0
- package/scripts/lib/vault-status/board-writer.mjs +174 -135
- package/scripts/lib/vault-status/narrative-mirror.mjs +129 -37
- package/scripts/lib/wave-executor/dispatch-common.mjs +164 -0
- package/scripts/lib/wave-executor/foreign-dispatch.mjs +7 -142
- package/scripts/lib/wave-executor/remote-dispatch.mjs +502 -0
- package/scripts/lib/wave-resource-gate.mjs +133 -7
- package/scripts/lib/wave-sizing.mjs +4 -1
- package/scripts/lib/wave-transcript-tail.mjs +142 -8
- package/scripts/materialize-wave-scope.mjs +32 -9
- package/scripts/memory-propose.mjs +146 -8
- package/scripts/migrate-cold-start-seed.mjs +4 -1
- package/scripts/parse-config.mjs +60 -3
- package/scripts/promote-vault-strict.mjs +4 -15
- package/scripts/release.mjs +337 -29
- package/scripts/repair-invalid-sessions.mjs +3 -3
- package/scripts/run-quality-gate.mjs +128 -11
- package/scripts/site-numbers.mjs +36 -4
- package/scripts/sweep-expired-learnings.mjs +90 -0
- package/scripts/sync-vault-schema.mjs +3 -1
- package/scripts/telemetry.mjs +2 -2
- package/scripts/validate-plugin.mjs +187 -0
- package/scripts/validate-wave-scope.mjs +28 -8
- package/scripts/vault-consolidate.mjs +3 -11
- package/scripts/vault-integration-watcher.mjs +2 -4
- package/scripts/vault-mirror.mjs +111 -26
- package/scripts/wave-scope-binding.mjs +215 -0
- package/skills/_shared/instruction-file-resolution.md +10 -0
- package/skills/_shared/parallel-aware-auq.md +31 -2
- package/skills/_shared/parallel-aware-preamble.md +18 -4
- package/skills/_shared/platform-tools.md +1 -1
- package/skills/_shared/state-ownership.md +1 -1
- package/skills/architecture/SKILL.md +7 -5
- package/skills/{domain-model/SKILL.md → architecture/references/domain-model.md} +9 -9
- package/skills/autopilot/SKILL.md +4 -18
- package/skills/claude-md-drift-check/SKILL.md +5 -1
- package/skills/claude-md-drift-check/checker.mjs +62 -2
- package/skills/convergence-monitoring/SIGNALS.md +55 -0
- package/skills/discovery/probes/vault-staleness.mjs +37 -13
- package/skills/discovery/probes-arch.md +20 -18
- package/skills/dispatcher/SKILL.md +3 -2
- package/skills/ecosystem-health/SKILL.md +4 -1
- package/skills/ecosystem-health/wizard.md +5 -0
- package/skills/evolve/SKILL.md +87 -11
- package/skills/frontmatter-guard/SKILL.md +11 -5
- package/skills/npm-publish/SKILL.md +1 -1
- package/skills/reconcile/SKILL.md +38 -2
- package/skills/remote-offload/SKILL.md +89 -0
- package/skills/session-end/SKILL.md +18 -905
- package/skills/session-end/phase-3-6-tail.md +19 -9
- package/skills/session-end/plan-verification.md +221 -155
- package/skills/session-end/references/phase-2-quality-gate.md +93 -0
- package/skills/session-end/references/phase-3-documentation-updates.md +229 -0
- package/skills/session-end/references/phase-4a-worktree-cleanup.md +120 -0
- package/skills/session-end/references/phase-4b-worktree-orphan-sweep.md +58 -0
- package/skills/session-end/references/phase-5-issue-cleanup.md +104 -0
- package/skills/session-end/references/session-summary-template.md +62 -0
- package/skills/session-plan/SKILL.md +49 -0
- package/skills/session-start/SKILL.md +41 -900
- package/skills/session-start/phase-8-5-express-path.md +1 -1
- package/skills/session-start/references/phase-1-1-dispatcher-autonomy-capture.md +55 -0
- package/skills/session-start/references/phase-1-2-session-lock.md +140 -0
- package/skills/session-start/references/phase-1-5-session-continuity.md +254 -0
- package/skills/session-start/references/phase-1-7-vault-status-board.md +53 -0
- package/skills/session-start/references/phase-2-7-portfolio-snapshot.md +75 -0
- package/skills/session-start/references/phase-4-ssot-environment-check.md +155 -0
- package/skills/session-start/references/phase-6-5-forced-reads.md +75 -0
- package/skills/session-start/references/phase-6-6-project-intelligence.md +81 -0
- package/skills/session-start/references/phase-6-7-memory-banner-telemetry-consent.md +103 -0
- package/skills/vault-sync/validator.mjs +21 -27
- package/skills/wave-executor/SKILL.md +16 -2
- package/skills/wave-executor/references/wave-loop-dispatch.md +612 -0
- package/skills/wave-executor/references/wave-loop-review.md +570 -0
- package/skills/wave-executor/references/wave-loop-scope-manifest.md +162 -0
- package/skills/wave-executor/wave-loop.md +14 -1271
- package/templates/_shared/journey-manifest.md +10 -6
- package/.cursor/commands/autopilot-multi.md +0 -14
- package/.cursor/commands/contract-version-bump.md +0 -14
- package/.cursor/commands/journey-audit.md +0 -14
- package/.cursor/skills/contract-version-bump/SKILL.md +0 -12
- package/.cursor/skills/daily/SKILL.md +0 -12
- package/.cursor/skills/domain-model/SKILL.md +0 -13
- package/.cursor/skills/journey-audit/SKILL.md +0 -13
- package/.cursor/skills/skill-creator/SKILL.md +0 -13
- package/.cursor/skills/ubiquitous-language/SKILL.md +0 -13
- package/commands/autopilot-multi.md +0 -74
- package/commands/contract-version-bump.md +0 -28
- package/commands/journey-audit.md +0 -43
- package/pi/prompts/autopilot-multi.md +0 -12
- package/pi/prompts/contract-version-bump.md +0 -12
- package/pi/prompts/journey-audit.md +0 -12
- package/scripts/autopilot-multi.mjs +0 -885
- package/scripts/backfill-learnings-expires.mjs +0 -196
- package/scripts/backfill-learnings.mjs +0 -203
- package/scripts/fleet-instruction-scan.mjs +0 -141
- package/scripts/lib/autopilot/dep-graph.mjs +0 -417
- package/scripts/lib/autopilot/multi-killswitch.mjs +0 -184
- package/scripts/lib/webhook-url.mjs +0 -105
- package/scripts/lifecycle-sim-v6.mjs +0 -347
- package/scripts/migrate-learnings-jsonl.mjs +0 -189
- package/scripts/migrate-subagents-jsonl.mjs +0 -196
- package/scripts/upload-social-preview.mjs +0 -316
- package/skills/_shared/model-selection.md +0 -64
- package/skills/contract-version-bump/SKILL.md +0 -219
- package/skills/daily/SKILL.md +0 -222
- package/skills/daily/generate.sh +0 -92
- package/skills/daily/templates/daily.md.tpl +0 -36
- package/skills/journey-audit/SKILL.md +0 -269
- package/skills/skill-creator/SKILL.md +0 -168
- package/skills/ubiquitous-language/SKILL.md +0 -97
- package/skills/vault-sync/package-lock.json +0 -40
- /package/skills/{domain-model → architecture/references}/ADR-FORMAT.md +0 -0
- /package/skills/{domain-model → architecture/references}/CONTEXT-FORMAT.md +0 -0
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* check-validator-registration.mjs — every `scripts/lib/validate/check-*.mjs`
|
|
4
|
+
* must be referenced by basename from at least one of the three surfaces
|
|
5
|
+
* that actually RUN a validator (`scripts/validate-plugin.mjs`,
|
|
6
|
+
* `.husky/pre-commit`, `.gitlab-ci.yml`), or declare itself deliberately
|
|
7
|
+
* standalone. #1184.
|
|
8
|
+
*
|
|
9
|
+
* THE CLASS. A checker with real detection logic and zero callers is built
|
|
10
|
+
* work nobody ever runs — this repo's own `check-unwired-features.mjs`
|
|
11
|
+
* census exists for a sibling shape of the same problem (config keys nobody
|
|
12
|
+
* reads); this checker is that same discipline applied to the checkers
|
|
13
|
+
* directory itself. `scripts/validate-plugin.mjs` is the canonical local
|
|
14
|
+
* runner (`runCheck('check-foo.mjs')`); `.husky/pre-commit` and
|
|
15
|
+
* `.gitlab-ci.yml` are the two OTHER surfaces some checkers wire into
|
|
16
|
+
* directly instead of (or in addition to) the orchestrator —
|
|
17
|
+
* `check-owner-leakage.mjs`, `check-test-fixture-shapes.mjs` and
|
|
18
|
+
* `check-test-value-bans.mjs` all do this (verified live at HEAD 2ccea0f2).
|
|
19
|
+
*
|
|
20
|
+
* OPT-OUT. A checker that is deliberately CLI-only (invoked by a human or a
|
|
21
|
+
* different tool, never by these three surfaces) declares itself with a
|
|
22
|
+
* header-comment marker: `// registration: standalone <reason>`. The marker
|
|
23
|
+
* makes the checker report PASS as "standalone", not merely silence a
|
|
24
|
+
* warning — the same shape of inline self-declared exemption
|
|
25
|
+
* `check-untracked-test-deps.mjs`'s `IGNORE_MARKER` and
|
|
26
|
+
* `check-dead-bridge.mjs`'s `:ignore` marker already use in this directory.
|
|
27
|
+
*
|
|
28
|
+
* ORACLE. A plain substring match of the checker's own basename (e.g.
|
|
29
|
+
* `"check-foo.mjs"`) against the COMMENT-STRIPPED text of the three surface
|
|
30
|
+
* files — the same granularity `runCheck('check-foo.mjs')` calls and
|
|
31
|
+
* `.husky`/CI script lines already use to name a checker. MEASURED (HEAD
|
|
32
|
+
* 2ccea0f2, all 33 live `check-*.mjs` basenames): zero basenames are a
|
|
33
|
+
* substring of another, so this match cannot cross-attribute one checker's
|
|
34
|
+
* registration to a different one.
|
|
35
|
+
*
|
|
36
|
+
* COMMENT-STRIPPING (HIGH, qa review, #1184 FX-C). A basename referenced
|
|
37
|
+
* ONLY inside a `//`/`#` line comment or a `/* *\/` block comment is NOT a
|
|
38
|
+
* real registration — the surface text is stripped of comments (quote-aware,
|
|
39
|
+
* so a `#`/`//` INSIDE a string — a URL fragment, a shell parameter
|
|
40
|
+
* expansion `${VAR#pattern}` — is never mistaken for a comment start) before
|
|
41
|
+
* matching. MEASURED before this fix: a fixture whose only reference to
|
|
42
|
+
* `check-ghost.mjs` sat in `// runCheck('check-ghost.mjs'); // DISABLED`
|
|
43
|
+
* reported `registered: true` (exit 0) — commenting a checker OUT silently
|
|
44
|
+
* kept it PASSing. `scripts/validate-plugin.mjs` uses `//`+`/* *\/` (js);
|
|
45
|
+
* `.husky/pre-commit` and `.gitlab-ci.yml` use `#` (sh/yaml) — see
|
|
46
|
+
* {@link commentStyleForSurface}. The checker's OWN
|
|
47
|
+
* `// registration: standalone` header marker is read from the CHECKER file
|
|
48
|
+
* directly (never from a surface text) and is unaffected by this stripping.
|
|
49
|
+
*
|
|
50
|
+
* NAMED CEILING (BV-004): a checker referenced only in prose (a `.md` doc, a
|
|
51
|
+
* skill body) and nowhere in the three RUN surfaces above still reports
|
|
52
|
+
* UNREGISTERED — being documented is not being run. REVISIT if a fourth run
|
|
53
|
+
* surface (a new CI job file, a different git hook) is ever added: extend
|
|
54
|
+
* `RUN_SURFACES`, do not special-case it here. The comment stripper's own
|
|
55
|
+
* quote-tracking is a single flat state — an escaped quote (`\"`) inside a
|
|
56
|
+
* double-quoted string is not honoured, and a template-literal's `${...}`
|
|
57
|
+
* interpolation is not walked separately. Both failure directions lean
|
|
58
|
+
* toward treating MORE text as "inside a string" than a real parser would,
|
|
59
|
+
* which can only make the stripper MISS a comment (false "still
|
|
60
|
+
* registered"), never manufacture a false UNREGISTERED — the direction this
|
|
61
|
+
* checker's own false-positive history (the paragraph above) already
|
|
62
|
+
* measured as the live hazard.
|
|
63
|
+
*
|
|
64
|
+
* Usage: check-validator-registration.mjs <repo-root>
|
|
65
|
+
* Output: ` PASS: …` / ` FAIL: …` lines (two leading spaces), then
|
|
66
|
+
* `Results: N passed, M failed`. Exit 0 = every checker registered or
|
|
67
|
+
* standalone, 1 = at least one unregistered checker, 2 = tool error.
|
|
68
|
+
*
|
|
69
|
+
* Import-safety: importing this module MUST NOT execute anything — the
|
|
70
|
+
* isMain guard at the bottom is the only side-effecting path.
|
|
71
|
+
*/
|
|
72
|
+
|
|
73
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
74
|
+
import path from 'node:path';
|
|
75
|
+
import { pathToFileURL } from 'node:url';
|
|
76
|
+
import { enumerateRepoFiles } from './enumerate-repo-files.mjs';
|
|
77
|
+
|
|
78
|
+
/** Marker line inside a checker's own header — declares deliberate CLI-only status. */
|
|
79
|
+
export const STANDALONE_MARKER = /^\s*\/\/\s*registration:\s*standalone\b(?:\s+(.*))?$/m;
|
|
80
|
+
|
|
81
|
+
/** The three surfaces that actually RUN a validator (not merely mention it). */
|
|
82
|
+
const RUN_SURFACES = Object.freeze([
|
|
83
|
+
path.join('scripts', 'validate-plugin.mjs'),
|
|
84
|
+
path.join('.husky', 'pre-commit'),
|
|
85
|
+
path.join('.gitlab-ci.yml'),
|
|
86
|
+
]);
|
|
87
|
+
|
|
88
|
+
const VALIDATE_DIR_REL = path.join('scripts', 'lib', 'validate');
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Comment style for a RUN_SURFACES path, keyed on extension rather than
|
|
92
|
+
* position — avoids a silent index-drift if `RUN_SURFACES` is ever
|
|
93
|
+
* reordered. `.mjs` gets JS-shaped comments (`//`, `/* *\/`, quotes
|
|
94
|
+
* `'`/`"`/`` ` ``); everything else (`.husky/pre-commit` has no extension,
|
|
95
|
+
* `.gitlab-ci.yml`) gets shell/YAML-shaped comments (`#` only, quotes
|
|
96
|
+
* `'`/`"`).
|
|
97
|
+
*
|
|
98
|
+
* @param {string} rel repo-relative surface path
|
|
99
|
+
* @returns {{lineComment: string, blockComment: boolean, quoteChars: string[]}}
|
|
100
|
+
*/
|
|
101
|
+
function commentStyleForSurface(rel) {
|
|
102
|
+
return rel.endsWith('.mjs')
|
|
103
|
+
? { lineComment: '//', blockComment: true, quoteChars: ['"', "'", '`'] }
|
|
104
|
+
: { lineComment: '#', blockComment: false, quoteChars: ['"', "'"] };
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Strip comments from `text` so a checker basename mentioned only inside a
|
|
109
|
+
* comment is never read as a real registration. Quote-aware: walks `'`/`"`
|
|
110
|
+
* (and, for the js style, `` ` ``) spans without inspecting their contents,
|
|
111
|
+
* so a comment marker INSIDE a string (a URL fragment `#frag`, a shell
|
|
112
|
+
* parameter expansion `${VAR#pattern}`) is left untouched rather than
|
|
113
|
+
* truncating the line early. See the header NAMED CEILING for what this
|
|
114
|
+
* quote-tracking deliberately does not attempt.
|
|
115
|
+
*
|
|
116
|
+
* @param {string} text
|
|
117
|
+
* @param {{lineComment: string, blockComment: boolean, quoteChars: string[]}} style
|
|
118
|
+
* @returns {string}
|
|
119
|
+
*/
|
|
120
|
+
export function stripComments(text, { lineComment, blockComment, quoteChars }) {
|
|
121
|
+
let out = '';
|
|
122
|
+
let i = 0;
|
|
123
|
+
let inQuote = null;
|
|
124
|
+
while (i < text.length) {
|
|
125
|
+
const ch = text[i];
|
|
126
|
+
if (inQuote) {
|
|
127
|
+
out += ch;
|
|
128
|
+
if (ch === '\\' && i + 1 < text.length) {
|
|
129
|
+
out += text[i + 1];
|
|
130
|
+
i += 2;
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
if (ch === inQuote) inQuote = null;
|
|
134
|
+
i += 1;
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
if (quoteChars.includes(ch)) {
|
|
138
|
+
inQuote = ch;
|
|
139
|
+
out += ch;
|
|
140
|
+
i += 1;
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
143
|
+
if (blockComment && ch === '/' && text[i + 1] === '*') {
|
|
144
|
+
const end = text.indexOf('*/', i + 2);
|
|
145
|
+
i = end === -1 ? text.length : end + 2;
|
|
146
|
+
continue;
|
|
147
|
+
}
|
|
148
|
+
if (text.startsWith(lineComment, i)) {
|
|
149
|
+
const nl = text.indexOf('\n', i);
|
|
150
|
+
i = nl === -1 ? text.length : nl; // keep the newline itself, drop the comment text
|
|
151
|
+
continue;
|
|
152
|
+
}
|
|
153
|
+
out += ch;
|
|
154
|
+
i += 1;
|
|
155
|
+
}
|
|
156
|
+
return out;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* @typedef {{basename: string, registered: boolean, standalone: boolean, surfaces: string[]}} RegistrationResult
|
|
161
|
+
*/
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* @param {string} repoRoot
|
|
165
|
+
* @returns {RegistrationResult[]} sorted by basename
|
|
166
|
+
*/
|
|
167
|
+
export function scanValidatorRegistration(repoRoot) {
|
|
168
|
+
// "Exists in this directory, tracked or not" (#1248) — a checker written but
|
|
169
|
+
// not yet staged is precisely the one most likely to be unregistered, and the
|
|
170
|
+
// bare git index cannot see it. `enumerateRepoFiles` still honours
|
|
171
|
+
// `.gitignore`, so no ignored tree enters the census (#1143).
|
|
172
|
+
const checkerFiles = enumerateRepoFiles({
|
|
173
|
+
repoRoot,
|
|
174
|
+
dirs: [VALIDATE_DIR_REL],
|
|
175
|
+
exts: ['mjs'],
|
|
176
|
+
}).filter((f) => /^check-.*\.mjs$/.test(path.basename(f)));
|
|
177
|
+
|
|
178
|
+
// Comment-stripped before matching (HIGH, #1184 FX-C): a basename
|
|
179
|
+
// referenced only inside a `//`/`#`/`/* *\/` comment is NOT a real
|
|
180
|
+
// registration — see the header's COMMENT-STRIPPING paragraph.
|
|
181
|
+
const surfaceTexts = RUN_SURFACES.map((rel) => {
|
|
182
|
+
const abs = path.join(repoRoot, rel);
|
|
183
|
+
try {
|
|
184
|
+
const raw = existsSync(abs) ? readFileSync(abs, 'utf8') : '';
|
|
185
|
+
return stripComments(raw, commentStyleForSurface(rel));
|
|
186
|
+
} catch {
|
|
187
|
+
return '';
|
|
188
|
+
}
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
/** @type {RegistrationResult[]} */
|
|
192
|
+
const results = [];
|
|
193
|
+
for (const abs of checkerFiles) {
|
|
194
|
+
const basename = path.basename(abs);
|
|
195
|
+
let source = '';
|
|
196
|
+
try {
|
|
197
|
+
source = readFileSync(abs, 'utf8');
|
|
198
|
+
} catch {
|
|
199
|
+
/* unreadable — no marker can be found, no surface reference can save it either */
|
|
200
|
+
}
|
|
201
|
+
const standalone = STANDALONE_MARKER.test(source);
|
|
202
|
+
const surfaces = RUN_SURFACES.filter((_, i) => surfaceTexts[i].includes(basename));
|
|
203
|
+
results.push({ basename, registered: surfaces.length > 0, standalone, surfaces });
|
|
204
|
+
}
|
|
205
|
+
return results.sort((a, b) => a.basename.localeCompare(b.basename));
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// ---------------------------------------------------------------------------
|
|
209
|
+
// CLI
|
|
210
|
+
// ---------------------------------------------------------------------------
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Run the check against a repo root, printing the validate-plugin line
|
|
214
|
+
* vocabulary.
|
|
215
|
+
*
|
|
216
|
+
* @param {string} repoRoot
|
|
217
|
+
* @returns {number} 0 = every checker registered or standalone, 1 = finding(s), 2 = tool error
|
|
218
|
+
*/
|
|
219
|
+
export function runCheckValidatorRegistration(repoRoot) {
|
|
220
|
+
console.log('--- Check: validator registration (check-*.mjs must be wired or standalone) ---');
|
|
221
|
+
|
|
222
|
+
const results = scanValidatorRegistration(repoRoot);
|
|
223
|
+
let pass = 0;
|
|
224
|
+
let fail = 0;
|
|
225
|
+
|
|
226
|
+
for (const r of results) {
|
|
227
|
+
if (r.standalone) {
|
|
228
|
+
console.log(` PASS: ${r.basename} — declared standalone (registration: standalone)`);
|
|
229
|
+
pass += 1;
|
|
230
|
+
} else if (r.registered) {
|
|
231
|
+
console.log(` PASS: ${r.basename} — referenced in ${r.surfaces.join(', ')}`);
|
|
232
|
+
pass += 1;
|
|
233
|
+
} else {
|
|
234
|
+
console.log(
|
|
235
|
+
` FAIL: ${r.basename} — referenced by NEITHER scripts/validate-plugin.mjs, .husky/pre-commit, NOR .gitlab-ci.yml, and carries no "registration: standalone" marker`,
|
|
236
|
+
);
|
|
237
|
+
fail += 1;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
console.log('');
|
|
242
|
+
console.log(`Results: ${pass} passed, ${fail} failed`);
|
|
243
|
+
return fail > 0 ? 1 : 0;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
const isMain = import.meta.url === pathToFileURL(process.argv[1] || '').href;
|
|
247
|
+
if (isMain) {
|
|
248
|
+
const root = process.argv[2];
|
|
249
|
+
if (!root) {
|
|
250
|
+
console.error('Usage: check-validator-registration.mjs <repo-root>');
|
|
251
|
+
process.exit(2);
|
|
252
|
+
}
|
|
253
|
+
process.exit(runCheckValidatorRegistration(path.resolve(root)));
|
|
254
|
+
}
|
|
@@ -194,6 +194,7 @@
|
|
|
194
194
|
import { existsSync, readFileSync, readdirSync } from 'node:fs';
|
|
195
195
|
import path from 'node:path';
|
|
196
196
|
import { pathToFileURL } from 'node:url';
|
|
197
|
+
import { SHELL_LANGS, forEachLine } from './markdown-fences.mjs';
|
|
197
198
|
|
|
198
199
|
/** Directories whose content is scanned. Root-level `*.md` is added separately. */
|
|
199
200
|
const SCAN_DIRS = Object.freeze([
|
|
@@ -224,9 +225,6 @@ const DOC_EXTENSIONS = Object.freeze(['.md']);
|
|
|
224
225
|
/** Code extensions (comment + string-literal rules apply). */
|
|
225
226
|
const CODE_EXTENSIONS = Object.freeze(['.mjs', '.js', '.cjs']);
|
|
226
227
|
|
|
227
|
-
/** Fence languages whose body is shell. Anything else is prose. */
|
|
228
|
-
const SHELL_LANGS = Object.freeze(new Set(['bash', 'sh', 'shell', 'console', 'zsh']));
|
|
229
|
-
|
|
230
228
|
/**
|
|
231
229
|
* Real top-level subcommands, harvested from `gh --help` / `glab --help`
|
|
232
230
|
* (gh 2.86.0 / glab 1.91.0, 2026-08-14). Filter 4: a token that is not in this
|
|
@@ -576,33 +574,13 @@ export function scanMarkdown(relative, body, tally) {
|
|
|
576
574
|
const findings = [];
|
|
577
575
|
/** @type {{line: number, text: string}[]} */
|
|
578
576
|
const shellLines = [];
|
|
579
|
-
/** @type {{marker: string, length: number, shell: boolean} | null} */
|
|
580
|
-
let fence = null;
|
|
581
577
|
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
const raw = lines[index];
|
|
585
|
-
const fenceMatch = raw.match(/^\s*(`{3,}|~{3,})\s*([A-Za-z0-9_+-]*)/);
|
|
586
|
-
if (fenceMatch) {
|
|
587
|
-
const marker = fenceMatch[1][0];
|
|
588
|
-
const length = fenceMatch[1].length;
|
|
589
|
-
const lang = fenceMatch[2].toLowerCase();
|
|
590
|
-
if (fence === null) {
|
|
591
|
-
fence = { marker, length, shell: SHELL_LANGS.has(lang) };
|
|
592
|
-
continue;
|
|
593
|
-
}
|
|
594
|
-
// A closing fence uses the same char, is at least as long, and has no info string.
|
|
595
|
-
if (marker === fence.marker && length >= fence.length && lang === '') {
|
|
596
|
-
fence = null;
|
|
597
|
-
continue;
|
|
598
|
-
}
|
|
599
|
-
// Otherwise it is fence content (a nested fence inside a wider one).
|
|
600
|
-
}
|
|
601
|
-
if (fence === null || !fence.shell) continue;
|
|
578
|
+
forEachLine(body, (raw, { lineNumber, inFence, lang }) => {
|
|
579
|
+
if (!inFence || !SHELL_LANGS.has(lang)) return;
|
|
602
580
|
const stripped = raw.replace(/^\s*[$❯>]\s+/, '');
|
|
603
|
-
if (/^\s*#/.test(stripped))
|
|
604
|
-
shellLines.push({ line:
|
|
605
|
-
}
|
|
581
|
+
if (/^\s*#/.test(stripped)) return;
|
|
582
|
+
shellLines.push({ line: lineNumber, text: stripped });
|
|
583
|
+
});
|
|
606
584
|
|
|
607
585
|
for (const entry of joinContinuations(shellLines)) {
|
|
608
586
|
for (const hit of extractBareInvocations(entry.text, tally)) {
|
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* scripts/lib/validate/enumerate-repo-files.mjs
|
|
3
|
+
*
|
|
4
|
+
* ONE enumerator for the scanners whose question is **"does this file exist in
|
|
5
|
+
* the repository right now?"** — GitLab #1248.
|
|
6
|
+
*
|
|
7
|
+
* ## The three populations, and why this is a third one
|
|
8
|
+
*
|
|
9
|
+
* `./repo-files.mjs` already answers two questions, and this module answers a
|
|
10
|
+
* third rather than duplicating either:
|
|
11
|
+
*
|
|
12
|
+
* | function | population |
|
|
13
|
+
* |-----------------------------|-----------------------------------------------------|
|
|
14
|
+
* | `listRepoFiles()` | what git TRACKS — "will ship / is versioned" |
|
|
15
|
+
* | `listOnDiskFiles()` | what the filesystem HOLDS minus a name list |
|
|
16
|
+
* | `enumerateRepoFiles()` here | **exists under these roots, tracked or not, minus what `.gitignore` excludes** |
|
|
17
|
+
*
|
|
18
|
+
* Pick deliberately. A scanner asking "is this file versioned / will it ship?"
|
|
19
|
+
* (a packaging check, a tarball manifest) MUST keep `listRepoFiles()`. A
|
|
20
|
+
* scanner asking "is this file PRESENT, so is the citation pointing at it
|
|
21
|
+
* dead?" belongs here — for it, the index is the wrong oracle.
|
|
22
|
+
*
|
|
23
|
+
* ## Why the index is the wrong oracle for an existence check (#1248)
|
|
24
|
+
*
|
|
25
|
+
* MEASURED in a clone (Wave-1 D6, 2026-09-06): an UNTRACKED
|
|
26
|
+
* `skills/zz-probe/SKILL.md` citing `scripts/does-not-exist.mjs` made
|
|
27
|
+
* `check-skill-script-paths.mjs` report `1 passed, 0 failed` BEFORE
|
|
28
|
+
* `git add -A` and `0 passed, 1 failed` AFTER — same working tree, same
|
|
29
|
+
* defect, no edit in between. The moment a defect is most likely to exist (a
|
|
30
|
+
* brand-new, not-yet-staged skill or checker) is exactly the moment the index
|
|
31
|
+
* cannot see it, so the check reports clean on the tree that carries the bug.
|
|
32
|
+
* `.claude/rules/measurement-discipline.md` § "A `git grep` drift sweep cannot
|
|
33
|
+
* see untracked files" is the same incident class on the release sweep.
|
|
34
|
+
*
|
|
35
|
+
* ## Why NOT a bare `readdirSync` walk
|
|
36
|
+
*
|
|
37
|
+
* The naive repair — swap `git ls-files` for a filesystem walk with a fixed
|
|
38
|
+
* prune list — reintroduces the MEASURED regression #1143 that put
|
|
39
|
+
* `repo-files.mjs` there in the first place: a walk cannot see `.gitignore`,
|
|
40
|
+
* so a gitignored worktree under `.claude/worktrees/<name>` drops a COMPLETE
|
|
41
|
+
* second checkout into the census (measured with one peer worktree present:
|
|
42
|
+
* +755 `.md`, +1209 `.mjs`, 133 MB, and a peer's copy of a rule file counted
|
|
43
|
+
* as an independent document). MEASURED here 2026-09-06 @ `befdda47` on a
|
|
44
|
+
* clean tree with `.claude/worktrees/` empty:
|
|
45
|
+
*
|
|
46
|
+
* git ls-files -- skills commands agents docs → 287 .md
|
|
47
|
+
* git ls-files --cached --others
|
|
48
|
+
* --exclude-standard -- (same dirs) → 287 .md
|
|
49
|
+
* listOnDiskFiles() walk, EXCLUDED_DIRS -- (same dirs) → 290 .md
|
|
50
|
+
*
|
|
51
|
+
* The 3-file surplus of the walk is entirely gitignored content
|
|
52
|
+
* (`docs/specs/2026-04-04-plan-skill-design.md`,
|
|
53
|
+
* `docs/specs/2026-04-16-bootstrap-gate-design.md`,
|
|
54
|
+
* `docs/specs/2026-05-26-parallel-aware-sessions-design.md`) — private design
|
|
55
|
+
* notes that would enter the census as if they were repository documentation.
|
|
56
|
+
* So the oracle is `--cached --others --exclude-standard`: it sees the
|
|
57
|
+
* untracked file #1248 is about AND honours `.gitignore`, which no prune list
|
|
58
|
+
* can approximate.
|
|
59
|
+
*
|
|
60
|
+
* ## The failure contract: absent is not unreadable (#1248 follow-up)
|
|
61
|
+
*
|
|
62
|
+
* The population above is "exists under these roots". Deciding whether a path
|
|
63
|
+
* EXISTS costs one `statSync`, and that `statSync` can fail for two
|
|
64
|
+
* fundamentally different reasons which an earlier version of this module
|
|
65
|
+
* collapsed into one bare `catch { continue }`:
|
|
66
|
+
*
|
|
67
|
+
* | stat error | what it means | this module |
|
|
68
|
+
* |-----------------------------------|-------------------------------------------------|-------------|
|
|
69
|
+
* | `ENOENT` | git lists it, the checkout lacks it — sparse checkout, a deletion staged elsewhere | SKIP (contributes zero files) |
|
|
70
|
+
* | `ENOTDIR` | a parent component is a file, so the path cannot exist either | SKIP (same class) |
|
|
71
|
+
* | `EACCES` / `EPERM` | the file EXISTS; this process may not look at it | THROW {@link RepoEnumerationError} |
|
|
72
|
+
* | `ELOOP` / `EIO` / `ENAMETOOLONG` / anything else | the answer is unknown | THROW {@link RepoEnumerationError} |
|
|
73
|
+
*
|
|
74
|
+
* The skip set is deliberately the same two codes `listRepoFiles()` documents
|
|
75
|
+
* in its own filter comment ("a tracked path can be absent from the working
|
|
76
|
+
* tree"), narrowed from a catch-all to exactly the codes that mean ABSENT.
|
|
77
|
+
* Every other code means the enumerator does not know whether the file is
|
|
78
|
+
* there, and a census that silently omits a file it could not look at reports
|
|
79
|
+
* a SMALLER population than the truth — with no signal that it did.
|
|
80
|
+
*
|
|
81
|
+
* That silence had a measured consequence. `collectDriftHits()` in
|
|
82
|
+
* `scripts/release.mjs` sweeps every enumerated file for the previous release
|
|
83
|
+
* literal; with `README.md` unreadable, the swallow made the sweep read ZERO
|
|
84
|
+
* files, return `status 1` ("no match"), and `evaluateDriftSweep()` reported
|
|
85
|
+
* `ok: true` — a release gate passing on a file it could not open. Fail-closed
|
|
86
|
+
* is one line there, and it was already written:
|
|
87
|
+
*
|
|
88
|
+
* } catch (err) {
|
|
89
|
+
* return { status: 128, stdout: '', stderr: `enumerateRepoFiles failed: ${err && err.message}` };
|
|
90
|
+
*
|
|
91
|
+
* (`scripts/release.mjs:576-578`; `evaluateDriftSweep` maps any status outside
|
|
92
|
+
* {0,1} to `ok: false` — "sweep is inconclusive".) So THROWING is what turns
|
|
93
|
+
* an unreadable file into an inconclusive sweep instead of a clean one.
|
|
94
|
+
*
|
|
95
|
+
* NAMED CEILING (BV-004): only the git path fails closed. The non-git fallback
|
|
96
|
+
* walks through `listOnDiskFiles()`, whose `readdirSync` walk skips an
|
|
97
|
+
* unreadable sub-tree by design and cannot report it — see
|
|
98
|
+
* {@link enumerateRepoFiles}. Revisit trigger: the first consumer that needs
|
|
99
|
+
* fail-closed enumeration on a NON-git root (a tarball export, a vendored
|
|
100
|
+
* copy); the answer then is an error-collecting walk in `repo-files.mjs`, not
|
|
101
|
+
* a second stat pass here.
|
|
102
|
+
*
|
|
103
|
+
* ## NAMED CEILING (BV-004)
|
|
104
|
+
*
|
|
105
|
+
* The `prune` list is a fixed set of path segments / repo-relative prefixes,
|
|
106
|
+
* NOT a `.gitignore` parser — it is only load-bearing on the FALLBACK path
|
|
107
|
+
* (a root that is not a git top level: a tarball export, a vendored copy, a
|
|
108
|
+
* tmpdir fixture). On the primary path git already applies the real ignore
|
|
109
|
+
* rules and the prune list is a cheap second filter. REVISIT if a scanner
|
|
110
|
+
* ever needs this on a non-git root whose ignored trees are not covered by
|
|
111
|
+
* the default prune set — then reach for a real ignore parser, do not grow
|
|
112
|
+
* this list a sixth time.
|
|
113
|
+
*
|
|
114
|
+
* @module scripts/lib/validate/enumerate-repo-files
|
|
115
|
+
*/
|
|
116
|
+
|
|
117
|
+
import { execFileSync } from 'node:child_process';
|
|
118
|
+
import { statSync } from 'node:fs';
|
|
119
|
+
import path from 'node:path';
|
|
120
|
+
|
|
121
|
+
import { EXCLUDED_DIRS, isGitToplevel, listOnDiskFiles } from './repo-files.mjs';
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Environment handed to `git`. An allowlist rather than `process.env`: an
|
|
125
|
+
* inherited `GIT_DIR` / `GIT_WORK_TREE` (set by any hook that spawned us)
|
|
126
|
+
* would silently re-point `ls-files` at a DIFFERENT repository, and the result
|
|
127
|
+
* would look like a plausible file list. Same allowlist as
|
|
128
|
+
* `repo-files.mjs`, which is not exported there.
|
|
129
|
+
*/
|
|
130
|
+
const GIT_ENV_ALLOWLIST = Object.freeze(['PATH', 'HOME', 'LANG', 'LC_ALL', 'TMPDIR', 'TZ']);
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Path segments (or repo-relative path prefixes) never enumerated — reused
|
|
134
|
+
* from `repo-files.mjs` rather than retyped, so an addition there reaches this
|
|
135
|
+
* module too.
|
|
136
|
+
*
|
|
137
|
+
* Deliberately NOT extended with this repo's own ignored trees
|
|
138
|
+
* (`.orchestrator/tmp/`, `.claude/worktrees/`): on the primary path `git` has
|
|
139
|
+
* already applied the real ignore rules — measured, `.orchestrator/tmp/` is
|
|
140
|
+
* `.gitignore:122` — so such an entry would be dead weight there, and naming
|
|
141
|
+
* an untracked path in a module a test imports is itself a finding
|
|
142
|
+
* (`check-untracked-test-deps.mjs` R2). A caller scanning a NON-git root whose
|
|
143
|
+
* ignored subtree a basename cannot express passes it via `prune` instead —
|
|
144
|
+
* that is what the prefix form is for.
|
|
145
|
+
*/
|
|
146
|
+
export const DEFAULT_PRUNE = EXCLUDED_DIRS;
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* `stat` error codes that mean the path is ABSENT from the working tree, and
|
|
150
|
+
* are therefore skipped rather than raised. See the module header's failure
|
|
151
|
+
* table for why the set is exactly these two and not a catch-all.
|
|
152
|
+
*/
|
|
153
|
+
const ABSENT_STAT_CODES = Object.freeze(new Set(['ENOENT', 'ENOTDIR']));
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Raised when a path git listed could not be RESOLVED — the file may well be
|
|
157
|
+
* there and this process could not look at it (`EACCES`, `EPERM`, `ELOOP`,
|
|
158
|
+
* `EIO`, …). Callers are expected to fail closed on it: an enumeration that
|
|
159
|
+
* threw describes no population at all.
|
|
160
|
+
*
|
|
161
|
+
* @property {string} code the underlying `stat` error code (`EACCES`, …)
|
|
162
|
+
* @property {string} path the absolute path that could not be resolved
|
|
163
|
+
*/
|
|
164
|
+
export class RepoEnumerationError extends Error {
|
|
165
|
+
/**
|
|
166
|
+
* @param {string} message
|
|
167
|
+
* @param {{code?: string, path?: string, cause?: unknown}} details
|
|
168
|
+
*/
|
|
169
|
+
constructor(message, { code, path: target, cause } = {}) {
|
|
170
|
+
super(message, cause === undefined ? undefined : { cause });
|
|
171
|
+
this.name = 'RepoEnumerationError';
|
|
172
|
+
this.code = code;
|
|
173
|
+
this.path = target;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* True when `relative` (POSIX, repo-relative) is inside a pruned tree.
|
|
179
|
+
* A prune entry matches either a whole path SEGMENT (`node_modules`) or a
|
|
180
|
+
* repo-relative path PREFIX (`.orchestrator/tmp`).
|
|
181
|
+
*
|
|
182
|
+
* @param {string} relative repo-relative POSIX path
|
|
183
|
+
* @param {string[]} prune
|
|
184
|
+
* @returns {boolean}
|
|
185
|
+
*/
|
|
186
|
+
function isPruned(relative, prune) {
|
|
187
|
+
const segments = relative.split('/');
|
|
188
|
+
for (const entry of prune) {
|
|
189
|
+
if (entry.includes('/')) {
|
|
190
|
+
if (relative === entry || relative.startsWith(`${entry}/`)) return true;
|
|
191
|
+
} else if (segments.includes(entry)) {
|
|
192
|
+
return true;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
return false;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/** @returns {NodeJS.ProcessEnv} the allowlisted git environment */
|
|
199
|
+
function gitEnv() {
|
|
200
|
+
/** @type {NodeJS.ProcessEnv} */
|
|
201
|
+
const env = {};
|
|
202
|
+
for (const key of GIT_ENV_ALLOWLIST) {
|
|
203
|
+
if (process.env[key] !== undefined) env[key] = process.env[key];
|
|
204
|
+
}
|
|
205
|
+
return env;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Normalise `exts` into a predicate over an absolute path.
|
|
210
|
+
* @param {string[] | null | undefined} exts extensions, with or without the dot
|
|
211
|
+
* @returns {(absolute: string) => boolean}
|
|
212
|
+
*/
|
|
213
|
+
function extFilter(exts) {
|
|
214
|
+
if (!exts || exts.length === 0) return () => true;
|
|
215
|
+
const set = new Set(exts.map((e) => (e.startsWith('.') ? e : `.${e}`)));
|
|
216
|
+
return (absolute) => set.has(path.extname(absolute));
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Files that EXIST under `dirs` — tracked or not — minus gitignored content
|
|
221
|
+
* and minus `prune`.
|
|
222
|
+
*
|
|
223
|
+
* Drop-in shaped like `listRepoFiles()`: absolute paths, sorted, deduplicated,
|
|
224
|
+
* never throwing for a missing directory (it contributes zero files).
|
|
225
|
+
*
|
|
226
|
+
* @param {object} options
|
|
227
|
+
* @param {string} options.repoRoot absolute repository root
|
|
228
|
+
* @param {string[]} [options.dirs] repo-relative directories to scan (default: the whole root)
|
|
229
|
+
* @param {string[] | null} [options.exts] extensions to keep (default: every file)
|
|
230
|
+
* @param {string[]} [options.prune] path segments / repo-relative prefixes to skip
|
|
231
|
+
* (default: {@link DEFAULT_PRUNE}; REPLACES the default when given, so pass
|
|
232
|
+
* `[...DEFAULT_PRUNE, 'extra']` to extend it rather than to swap it)
|
|
233
|
+
* @param {(target: string) => import('node:fs').Stats} [options.stat] injection
|
|
234
|
+
* seam for tests (default: `statSync`) — the same shape `collectDriftHits()`
|
|
235
|
+
* uses for its `enumerate`/`read` seams
|
|
236
|
+
* @returns {string[]} absolute paths, sorted
|
|
237
|
+
* @throws {RepoEnumerationError} when a listed path can neither be resolved
|
|
238
|
+
* nor proven absent (`EACCES`, `EPERM`, `ELOOP`, …) — see the module header's
|
|
239
|
+
* failure table. Only the git path raises; the non-git fallback walk cannot.
|
|
240
|
+
*/
|
|
241
|
+
export function enumerateRepoFiles({
|
|
242
|
+
repoRoot,
|
|
243
|
+
dirs,
|
|
244
|
+
exts = null,
|
|
245
|
+
prune = DEFAULT_PRUNE,
|
|
246
|
+
stat = statSync,
|
|
247
|
+
} = {}) {
|
|
248
|
+
const matches = extFilter(exts);
|
|
249
|
+
const pruneList = [...prune];
|
|
250
|
+
const env = gitEnv();
|
|
251
|
+
|
|
252
|
+
if (isGitToplevel(repoRoot, env)) {
|
|
253
|
+
const pathspecs = dirs && dirs.length > 0 ? dirs.filter((d) => d !== '.') : [];
|
|
254
|
+
try {
|
|
255
|
+
// `--cached --others --exclude-standard` = tracked PLUS untracked, minus
|
|
256
|
+
// everything `.gitignore`/`.git/info/exclude` excludes. `--deduplicate`
|
|
257
|
+
// is deliberately not used: it needs git >= 2.31 and the Set below is
|
|
258
|
+
// free. See the module header for why this beats both a bare
|
|
259
|
+
// `ls-files` (#1248) and a bare walk (#1143).
|
|
260
|
+
const out = execFileSync(
|
|
261
|
+
'git',
|
|
262
|
+
['ls-files', '-z', '--cached', '--others', '--exclude-standard', '--', ...pathspecs],
|
|
263
|
+
{
|
|
264
|
+
cwd: repoRoot,
|
|
265
|
+
encoding: 'utf8',
|
|
266
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
267
|
+
maxBuffer: 64 * 1024 * 1024,
|
|
268
|
+
env,
|
|
269
|
+
},
|
|
270
|
+
);
|
|
271
|
+
const found = new Set();
|
|
272
|
+
for (const rel of out.split('\0')) {
|
|
273
|
+
if (!rel) continue;
|
|
274
|
+
if (isPruned(rel, pruneList)) continue;
|
|
275
|
+
const absolute = path.join(repoRoot, rel);
|
|
276
|
+
if (!matches(absolute)) continue;
|
|
277
|
+
// A tracked path can be absent from the working tree (sparse checkout,
|
|
278
|
+
// a deletion staged elsewhere). A scanner that then read it would
|
|
279
|
+
// report a tool-error for a file nobody removed — so ABSENT is skipped.
|
|
280
|
+
// Anything else means the file may be there and we could not look:
|
|
281
|
+
// fail closed rather than shrink the census in silence (header table).
|
|
282
|
+
let entry;
|
|
283
|
+
try {
|
|
284
|
+
entry = stat(absolute);
|
|
285
|
+
} catch (err) {
|
|
286
|
+
const code = err && err.code;
|
|
287
|
+
if (ABSENT_STAT_CODES.has(code)) continue;
|
|
288
|
+
throw new RepoEnumerationError(
|
|
289
|
+
`cannot stat ${absolute} (${code || 'unknown error'}): the file may exist and could not be read — enumeration is inconclusive`,
|
|
290
|
+
{ code, path: absolute, cause: err },
|
|
291
|
+
);
|
|
292
|
+
}
|
|
293
|
+
if (!entry.isFile()) continue;
|
|
294
|
+
found.add(absolute);
|
|
295
|
+
}
|
|
296
|
+
return [...found].sort();
|
|
297
|
+
} catch (err) {
|
|
298
|
+
// A resolution failure is NOT a reason to retry with a weaker oracle:
|
|
299
|
+
// the walk would skip the same unreadable entry silently and hand back a
|
|
300
|
+
// census that looks complete. Only a git/`ls-files` failure falls
|
|
301
|
+
// through — there we have no index to trust in the first place.
|
|
302
|
+
if (err instanceof RepoEnumerationError) throw err;
|
|
303
|
+
// fall through to the walk — a git that answered rev-parse but failed
|
|
304
|
+
// ls-files leaves us with no index to trust.
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// Non-git root (tarball export, vendored copy, tmpdir fixture): the walk in
|
|
309
|
+
// `repo-files.mjs` is the reuse — it already skips symlinks and unreadable
|
|
310
|
+
// sub-trees. It excludes by directory BASENAME only, so prefix-shaped prune
|
|
311
|
+
// entries are re-applied here.
|
|
312
|
+
const basenamePrune = pruneList.filter((entry) => !entry.includes('/'));
|
|
313
|
+
return listOnDiskFiles(repoRoot, { dirs, exts, exclude: basenamePrune }).filter((absolute) => {
|
|
314
|
+
const rel = path.relative(repoRoot, absolute).split(path.sep).join('/');
|
|
315
|
+
return !isPruned(rel, pruneList);
|
|
316
|
+
});
|
|
317
|
+
}
|