session-orchestrator 5.0.0 → 5.2.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/autopilot/SKILL.md +1 -0
- package/.agents/skills/bootstrap/SKILL.md +2 -0
- package/.agents/skills/brainstorm/SKILL.md +3 -0
- package/.agents/skills/close/SKILL.md +17 -0
- package/.agents/skills/debug/SKILL.md +2 -0
- package/.agents/skills/discovery/SKILL.md +2 -1
- package/.agents/skills/dispatcher/SKILL.md +2 -0
- package/.agents/skills/eli5/SKILL.md +2 -0
- package/.agents/skills/eval/SKILL.md +1 -0
- package/.agents/skills/evolve/SKILL.md +2 -1
- package/.agents/skills/go/SKILL.md +18 -0
- package/.agents/skills/grill/SKILL.md +2 -0
- package/.agents/skills/harness-audit/SKILL.md +16 -0
- package/.agents/skills/memory-cleanup/SKILL.md +1 -0
- package/.agents/skills/persona-panel/SKILL.md +1 -0
- package/.agents/skills/plan/SKILL.md +3 -1
- package/.agents/skills/portfolio/SKILL.md +17 -0
- package/.agents/skills/reconcile/SKILL.md +1 -0
- package/.agents/skills/release/SKILL.md +18 -0
- package/.agents/skills/repo-audit/SKILL.md +1 -0
- package/.agents/skills/spinout/SKILL.md +1 -0
- package/.agents/skills/sunset-review/SKILL.md +2 -0
- package/.agents/skills/test/SKILL.md +17 -0
- package/.agents/skills/ux-grill/SKILL.md +2 -0
- package/.claude-plugin/marketplace.json +3 -3
- package/.claude-plugin/plugin.json +2 -2
- package/.codex-plugin/plugin.json +2 -2
- package/.codex-plugin/skills/autopilot/SKILL.md +5 -4
- package/.codex-plugin/skills/bootstrap/SKILL.md +8 -4
- package/.codex-plugin/skills/brainstorm/SKILL.md +11 -4
- package/.codex-plugin/skills/close/SKILL.md +3 -3
- package/.codex-plugin/skills/convergence-monitoring/SKILL.md +2 -0
- package/.codex-plugin/skills/convergence-monitoring/agents/openai.yaml +5 -0
- package/.codex-plugin/skills/debug/SKILL.md +11 -4
- package/.codex-plugin/skills/discovery/SKILL.md +8 -4
- package/.codex-plugin/skills/dispatcher/SKILL.md +4 -4
- package/.codex-plugin/skills/eli5/SKILL.md +9 -4
- package/.codex-plugin/skills/eval/SKILL.md +9 -4
- package/.codex-plugin/skills/evolve/SKILL.md +9 -4
- package/.codex-plugin/skills/go/SKILL.md +3 -3
- package/.codex-plugin/skills/grill/SKILL.md +11 -4
- package/.codex-plugin/skills/harness-audit/SKILL.md +4 -3
- package/.codex-plugin/skills/memory-cleanup/SKILL.md +9 -4
- package/.codex-plugin/skills/npm-publish/SKILL.md +2 -0
- package/.codex-plugin/skills/npm-publish/agents/openai.yaml +5 -0
- package/.codex-plugin/skills/persona-panel/SKILL.md +5 -5
- package/.codex-plugin/skills/plan/SKILL.md +8 -4
- package/.codex-plugin/skills/portfolio/SKILL.md +3 -3
- package/.codex-plugin/skills/reconcile/SKILL.md +9 -4
- package/.codex-plugin/skills/release/SKILL.md +3 -3
- package/.codex-plugin/skills/repo-audit/SKILL.md +6 -4
- package/.codex-plugin/skills/spinout/SKILL.md +4 -4
- package/.codex-plugin/skills/sunset-review/SKILL.md +5 -4
- package/.codex-plugin/skills/test/SKILL.md +3 -3
- package/.codex-plugin/skills/ux-grill/SKILL.md +11 -4
- package/.cursor/commands/autopilot.md +4 -4
- package/.cursor/commands/bootstrap.md +5 -4
- package/.cursor/commands/brainstorm.md +5 -4
- package/.cursor/commands/close.md +4 -3
- package/.cursor/commands/convergence-monitoring.md +13 -0
- package/.cursor/commands/debug.md +4 -4
- package/.cursor/commands/discovery.md +4 -4
- package/.cursor/commands/dispatcher.md +4 -4
- package/.cursor/commands/eli5.md +4 -4
- package/.cursor/commands/eval.md +4 -4
- package/.cursor/commands/evolve.md +4 -4
- package/.cursor/commands/go.md +4 -3
- package/.cursor/commands/grill.md +4 -4
- package/.cursor/commands/harness-audit.md +3 -3
- package/.cursor/commands/memory-cleanup.md +4 -4
- package/.cursor/commands/npm-publish.md +13 -0
- package/.cursor/commands/persona-panel.md +4 -4
- package/.cursor/commands/plan.md +5 -4
- package/.cursor/commands/portfolio.md +3 -3
- package/.cursor/commands/reconcile.md +4 -4
- package/.cursor/commands/release.md +4 -3
- package/.cursor/commands/repo-audit.md +4 -4
- package/.cursor/commands/spinout.md +4 -4
- package/.cursor/commands/sunset-review.md +4 -4
- package/.cursor/commands/test.md +3 -3
- package/.cursor/commands/ux-grill.md +4 -4
- package/.cursor/rules/010-session-workflow.mdc +2 -2
- package/.cursor/skills/bootstrap/SKILL.md +1 -0
- package/.cursor/skills/close/SKILL.md +13 -0
- package/.cursor/skills/debug/SKILL.md +0 -1
- package/.cursor/skills/discovery/SKILL.md +0 -1
- package/.cursor/skills/dispatcher/SKILL.md +0 -1
- package/.cursor/skills/eli5/SKILL.md +0 -1
- package/.cursor/skills/evolve/SKILL.md +0 -1
- package/.cursor/skills/go/SKILL.md +13 -0
- package/.cursor/skills/grill/SKILL.md +0 -1
- package/.cursor/skills/harness-audit/SKILL.md +12 -0
- package/.cursor/skills/portfolio/SKILL.md +12 -0
- package/.cursor/skills/release/SKILL.md +13 -0
- package/.cursor/skills/repo-audit/SKILL.md +0 -1
- package/.cursor/skills/sunset-review/SKILL.md +0 -1
- package/.cursor/skills/test/SKILL.md +12 -0
- package/.cursor/skills/ux-grill/SKILL.md +0 -1
- package/.cursor-plugin/plugin.json +2 -2
- package/.orchestrator/policy/blocked-commands.json +10 -0
- package/AGENTS.md +1 -1
- package/CHANGELOG.md +80 -0
- package/README.md +74 -235
- package/commands/session.md +10 -0
- package/docs/USER-GUIDE.md +24 -0
- package/docs/ci-setup.md +53 -0
- package/docs/codex-setup.md +1 -1
- package/docs/components.md +12 -5
- package/docs/events-schema.md +5 -1
- package/docs/install.md +128 -0
- package/docs/persona-panel.md +1 -1
- package/docs/pi-setup.md +1 -1
- package/docs/rule-authoring.md +83 -14
- package/docs/scope-collision-guard.md +2 -0
- package/docs/session-config-reference.md +6 -4
- package/docs/session-config-template.md +38 -0
- package/docs/telemetry.md +15 -0
- package/hooks/_lib/hook-import-set.json +46 -6
- package/hooks/_lib/subagent-paths.mjs +15 -0
- package/hooks/_lib/vcs-create-matcher.mjs +217 -62
- package/hooks/enforce-scope.mjs +42 -1
- package/hooks/hooks-codex.json +1 -1
- package/hooks/hooks.json +1 -1
- package/hooks/on-session-end.mjs +14 -2
- package/hooks/on-stop.mjs +43 -1
- package/hooks/post-bash-write-verify.mjs +3 -0
- package/hooks/pre-auq-clarity.mjs +3 -0
- package/hooks/pre-bash-issue-budget.mjs +103 -17
- package/hooks/pre-task-scope-disjoint.mjs +152 -3
- package/hooks/skill-invocation-telemetry.mjs +2 -1
- package/package.json +3 -2
- package/pi/prompts/autopilot.md +3 -3
- package/pi/prompts/bootstrap.md +3 -3
- package/pi/prompts/brainstorm.md +3 -3
- package/pi/prompts/close.md +2 -2
- package/pi/prompts/convergence-monitoring.md +11 -0
- package/pi/prompts/debug.md +3 -3
- package/pi/prompts/discovery.md +3 -3
- package/pi/prompts/dispatcher.md +3 -3
- package/pi/prompts/eli5.md +3 -3
- package/pi/prompts/eval.md +3 -3
- package/pi/prompts/evolve.md +3 -3
- package/pi/prompts/go.md +2 -2
- package/pi/prompts/grill.md +3 -3
- package/pi/prompts/harness-audit.md +2 -3
- package/pi/prompts/memory-cleanup.md +3 -3
- package/pi/prompts/npm-publish.md +11 -0
- package/pi/prompts/persona-panel.md +3 -3
- package/pi/prompts/plan.md +3 -3
- package/pi/prompts/portfolio.md +2 -2
- package/pi/prompts/reconcile.md +3 -3
- package/pi/prompts/release.md +3 -3
- package/pi/prompts/repo-audit.md +3 -4
- package/pi/prompts/session.md +1 -1
- package/pi/prompts/spinout.md +3 -3
- package/pi/prompts/sunset-review.md +3 -3
- package/pi/prompts/templates-ack.md +1 -1
- package/pi/prompts/test.md +3 -3
- package/pi/prompts/ux-grill.md +3 -3
- package/scripts/archive-closed-prds.mjs +2 -2
- package/scripts/auq-audit.mjs +2 -3
- package/scripts/backfill-abandoned-sessions.mjs +57 -3
- package/scripts/backfill-evidence-digest.mjs +2 -1
- package/scripts/backfill-learnings-from-vault.mjs +2 -2
- package/scripts/check-package-manager.mjs +2 -2
- package/scripts/ci/assert-vitest-green.mjs +2 -1
- package/scripts/emit-session.mjs +2 -3
- package/scripts/export-hw-learnings.mjs +2 -1
- package/scripts/express-path.mjs +1 -1
- package/scripts/gc-stale-worktrees.mjs +2 -1
- package/scripts/generate-codex-skills.mjs +48 -4
- package/scripts/generate-cursor-adapter.mjs +173 -9
- package/scripts/generate-hook-import-set.mjs +12 -27
- package/scripts/generate-pi-prompts.mjs +183 -13
- package/scripts/github-protection-audit.mjs +2 -3
- package/scripts/lib/agent-frontmatter.mjs +23 -1
- package/scripts/lib/claude-md-budget-lint.mjs +2 -5
- package/scripts/lib/command-blocker.mjs +209 -9
- package/scripts/lib/config/drift-check.mjs +19 -0
- package/scripts/lib/convergence-monitor.mjs +2 -2
- package/scripts/lib/cursor-hook-bridge.mjs +2 -2
- package/scripts/lib/description-surface.mjs +2 -5
- package/scripts/lib/dispatcher/cli.mjs +2 -1
- package/scripts/lib/ecosystem-wizard.mjs +2 -1
- package/scripts/lib/fetch-baseline.mjs +3 -8
- package/scripts/lib/gitlab-ops/stale-mr-sweep.mjs +2 -1
- package/scripts/lib/gitlab-portfolio/cli.mjs +2 -1
- package/scripts/lib/instruction-budget-guard.mjs +186 -46
- package/scripts/lib/is-main-module.mjs +82 -0
- package/scripts/lib/locks/index.mjs +32 -25
- package/scripts/lib/maintenance-due-banner.mjs +69 -3
- package/scripts/lib/peer-discovery.mjs +2 -5
- package/scripts/lib/playwright-driver/runner.mjs +63 -2
- package/scripts/lib/reconcile/rule-expiry-sweep.mjs +642 -0
- package/scripts/lib/rules-sync.mjs +2 -5
- package/scripts/lib/scope-echo.mjs +392 -7
- package/scripts/lib/session-close-backfill.mjs +58 -6
- package/scripts/lib/state-md.mjs +84 -3
- package/scripts/lib/sunset/walker.mjs +31 -4
- package/scripts/lib/tests-src-ratio.mjs +2 -6
- package/scripts/lib/tmux-layout/telemetry-stats.mjs +2 -1
- package/scripts/lib/user-invocable-skills.mjs +185 -0
- package/scripts/lib/validate/check-banner-parity.mjs +2 -2
- package/scripts/lib/validate/check-cursor-adapter.mjs +2 -2
- package/scripts/lib/validate/check-dead-bridge.mjs +2 -2
- package/scripts/lib/validate/check-doc-cli-commands.mjs +2 -2
- package/scripts/lib/validate/check-entry-guard.mjs +366 -0
- package/scripts/lib/validate/check-guard-requires-parity.mjs +2 -2
- package/scripts/lib/validate/check-hooks-emit-event-guard.mjs +2 -2
- package/scripts/lib/validate/check-learning-provenance.mjs +2 -2
- package/scripts/lib/validate/check-skill-links.mjs +27 -6
- package/scripts/lib/validate/check-skill-script-paths.mjs +2 -2
- package/scripts/lib/validate/check-test-git-config-target.mjs +2 -2
- package/scripts/lib/validate/check-unicode-safety.mjs +2 -2
- package/scripts/lib/validate/check-untracked-test-deps.mjs +2 -2
- package/scripts/lib/validate/check-unwired-features.mjs +266 -11
- package/scripts/lib/validate/check-validator-registration.mjs +2 -2
- package/scripts/lib/validate/check-vcs-repo-flag.mjs +2 -2
- package/scripts/lib/validate-vendored-rules.mjs +35 -9
- package/scripts/lib/wave-transcript-tail.mjs +2 -2
- package/scripts/lock-reaper.mjs +2 -1
- package/scripts/materialize-wave-scope.mjs +87 -4
- package/scripts/migrate-sessions-jsonl.mjs +2 -1
- package/scripts/migrate-vault-paths.mjs +2 -3
- package/scripts/release.mjs +124 -35
- package/scripts/relocate-vault-corpus.mjs +2 -3
- package/scripts/repair-invalid-sessions.mjs +2 -2
- package/scripts/session-shape.mjs +2 -2
- package/scripts/site-numbers.mjs +35 -11
- package/scripts/sweep-expired-rules.mjs +216 -0
- package/scripts/validate-plugin.mjs +9 -0
- package/scripts/vault-consolidate.mjs +2 -2
- package/scripts/vault-mirror.mjs +2 -3
- package/scripts/wave-scope-binding.mjs +2 -3
- package/skills/_shared/bootstrap-gate.md +1 -1
- package/skills/_shared/monitor-patterns.md +1 -1
- package/skills/_shared/research-evidence.md +53 -0
- package/skills/_shared/state-ownership.md +3 -0
- package/skills/autopilot/SKILL.md +58 -4
- package/skills/bootstrap/SKILL.md +51 -1
- package/skills/brainstorm/SKILL.md +16 -0
- package/skills/claude-md-drift-check/checker.mjs +49 -11
- package/{commands/close.md → skills/close/SKILL.md} +9 -3
- package/skills/debug/SKILL.md +10 -0
- package/skills/discovery/SKILL.md +24 -1
- package/skills/discovery/probes-session.md +2 -2
- package/skills/dispatcher/SKILL.md +38 -7
- package/skills/eli5/SKILL.md +11 -0
- package/skills/eval/SKILL.md +14 -0
- package/skills/evolve/SKILL.md +8 -1
- package/skills/evolve/references/evolve-dialectic-mode.md +6 -2
- package/{commands/go.md → skills/go/SKILL.md} +9 -1
- package/skills/grill/SKILL.md +19 -0
- package/{commands/harness-audit.md → skills/harness-audit/SKILL.md} +7 -2
- package/skills/hook-development/SKILL.md +46 -41
- package/skills/memory-cleanup/SKILL.md +7 -0
- package/skills/npm-publish/SKILL.md +1 -1
- package/skills/persona-panel/SKILL.md +56 -1
- package/skills/persona-panel/persona-format.md +1 -1
- package/skills/plan/SKILL.md +28 -1
- package/skills/playwright-driver/SKILL.md +7 -10
- package/{commands/portfolio.md → skills/portfolio/SKILL.md} +8 -2
- package/skills/reconcile/SKILL.md +10 -0
- package/{commands/release.md → skills/release/SKILL.md} +16 -2
- package/skills/repo-audit/SKILL.md +7 -0
- package/skills/session-end/plan-verification.md +2 -2
- package/skills/session-plan/SKILL.md +1 -1
- package/skills/session-start/SKILL.md +5 -4
- package/skills/session-start/phase-8-5-express-path.md +6 -6
- package/skills/session-start/references/phase-1-5-session-continuity.md +1 -1
- package/skills/session-start/references/phase-2-7-portfolio-snapshot.md +1 -1
- package/skills/session-start/references/phase-4-ssot-environment-check.md +4 -3
- package/skills/spinout/SKILL.md +12 -1
- package/skills/sunset-review/SKILL.md +13 -0
- package/{commands/test.md → skills/test/SKILL.md} +10 -4
- package/skills/ux-grill/SKILL.md +19 -1
- package/skills/wave-executor/SKILL.md +7 -4
- package/skills/wave-executor/references/wave-executor-state-init.md +13 -1
- package/skills/wave-executor/references/wave-loop-dispatch.md +3 -1
- package/skills/wave-executor/references/wave-loop-review.md +17 -1
- package/commands/autopilot.md +0 -80
- package/commands/bootstrap.md +0 -56
- package/commands/brainstorm.md +0 -48
- package/commands/debug.md +0 -36
- package/commands/discovery.md +0 -32
- package/commands/dispatcher.md +0 -59
- package/commands/eli5.md +0 -33
- package/commands/eval.md +0 -28
- package/commands/evolve.md +0 -10
- package/commands/grill.md +0 -45
- package/commands/memory-cleanup.md +0 -26
- package/commands/persona-panel.md +0 -121
- package/commands/plan.md +0 -15
- package/commands/reconcile.md +0 -23
- package/commands/repo-audit.md +0 -24
- package/commands/spinout.md +0 -15
- package/commands/sunset-review.md +0 -27
- package/commands/ux-grill.md +0 -51
|
@@ -166,6 +166,43 @@
|
|
|
166
166
|
* both classes stay in `findings`, and either half collapsing to zero is itself
|
|
167
167
|
* pinned by a test.
|
|
168
168
|
*
|
|
169
|
+
* ## S5 `hand-keyed-learning-subject` — the prose-only DERIVATION
|
|
170
|
+
*
|
|
171
|
+
* S1-S4 all ask about a module or a key. S5 asks about a VALUE that prose tells
|
|
172
|
+
* an LLM to derive: `skills/evolve/references/evolve-analyze-mode.md` says the
|
|
173
|
+
* `effective-sizing` subject must come from `sizingSubject()` and that nobody may
|
|
174
|
+
* hand-concatenate it. Nothing enforced that. An analyzer run that writes
|
|
175
|
+
* `${session_type}-session-sizing` by hand — which is what it did before #1247,
|
|
176
|
+
* and which the helper's own profile-less branch still returns byte-identically —
|
|
177
|
+
* silently re-merges a 7-wave `ultradeep` session onto the 5-wave `deep` row, and
|
|
178
|
+
* no test, gate or checker notices.
|
|
179
|
+
*
|
|
180
|
+
* S5 therefore imports the helper (which is also what gives it a MECHANICAL
|
|
181
|
+
* consumer at all) and re-derives the subject of every written `effective-sizing`
|
|
182
|
+
* learning from the session record it names, reporting any mismatch. The
|
|
183
|
+
* comparison is the point: the check cannot drift from the helper, because it IS
|
|
184
|
+
* the helper.
|
|
185
|
+
*
|
|
186
|
+
* ### What it judges, and the measured reason it judges no more than that
|
|
187
|
+
*
|
|
188
|
+
* Only a subject already in the canonical `<...>-session-sizing` SHAPE is judged.
|
|
189
|
+
* Measured 2026-09-13 on this repo's live ledger (6 `effective-sizing` learnings):
|
|
190
|
+
* exactly ONE is canonically keyed (`deep-session-sizing`, and it is correct); the
|
|
191
|
+
* other five are free-form sentences (`deep session of 10 small follow-up
|
|
192
|
+
* issues`, `full-gate-workers-under-host-contention`, …). Judging those would put
|
|
193
|
+
* this class at 83% on day one — the broken instrument
|
|
194
|
+
* `.claude/rules/host-resources.md` § HR-101 forbids — and it would be judging
|
|
195
|
+
* prose STYLE, not a keying defect. A free-form subject cannot COLLIDE two
|
|
196
|
+
* profiles onto one row, which is the whole bug #1247 fixed.
|
|
197
|
+
*
|
|
198
|
+
* A learning whose `source_session` resolves to no session record is skipped and
|
|
199
|
+
* counted, never reported: the ledger is host-local and append-only, so an old
|
|
200
|
+
* learning routinely outlives the record it names, and reporting that would be a
|
|
201
|
+
* finding about ledger retention rather than about keying.
|
|
202
|
+
*
|
|
203
|
+
* Both ledger files are gitignored host-local telemetry. Absent → S5 is a silent
|
|
204
|
+
* no-op (CI sees no ledger), never a tool error.
|
|
205
|
+
*
|
|
169
206
|
* ## Consumer scope, and why "prose-only" is a finding rather than an error
|
|
170
207
|
*
|
|
171
208
|
* Read sites are counted in `scripts/**` and `hooks/**` (`.mjs`/`.js`/`.cjs`),
|
|
@@ -228,7 +265,9 @@
|
|
|
228
265
|
|
|
229
266
|
import { existsSync, readFileSync, readdirSync } from 'node:fs';
|
|
230
267
|
import path from 'node:path';
|
|
231
|
-
|
|
268
|
+
|
|
269
|
+
import { sizingSubject } from '../learnings/sizing-subject.mjs';
|
|
270
|
+
import { isMainModule } from '../is-main-module.mjs';
|
|
232
271
|
|
|
233
272
|
/** Documented config surface — every `yaml` fence in this file is a declaration. */
|
|
234
273
|
const TEMPLATE_REL = 'docs/session-config-template.md';
|
|
@@ -331,6 +370,17 @@ const WIRING_DIRS = Object.freeze([
|
|
|
331
370
|
['hooks', Object.freeze(['.json', '.sh'])],
|
|
332
371
|
]);
|
|
333
372
|
|
|
373
|
+
/**
|
|
374
|
+
* Host-local telemetry the S5 subject-parity check reads. Both are gitignored
|
|
375
|
+
* (`.gitignore:55` `.orchestrator/metrics/*.jsonl`), so both are routinely
|
|
376
|
+
* ABSENT — that is a no-op, not an error. See the header, § S5.
|
|
377
|
+
*/
|
|
378
|
+
const LEARNINGS_REL = path.join('.orchestrator', 'metrics', 'learnings.jsonl');
|
|
379
|
+
const SESSIONS_REL = path.join('.orchestrator', 'metrics', 'sessions.jsonl');
|
|
380
|
+
|
|
381
|
+
/** The canonical `effective-sizing` subject shape `sizingSubject()` produces. */
|
|
382
|
+
const SIZING_SUBJECT_RE = /-session-sizing$/;
|
|
383
|
+
|
|
334
384
|
/**
|
|
335
385
|
* The config-parser layer: the files a Session Config key must pass through to
|
|
336
386
|
* become a runtime value. Signal S2 (see header) checks top-level keys against
|
|
@@ -372,7 +422,7 @@ const ALLOWLIST = Object.freeze({
|
|
|
372
422
|
* @typedef {{
|
|
373
423
|
* kind: 'unwired-config-key' | 'parser-orphan-config-key' | 'allowlist-missing-reason'
|
|
374
424
|
* | 'allowlist-stale' | 'orphaned-prose-module' | 'unreachable-library-module'
|
|
375
|
-
* | 'coordinator-invoked-module'
|
|
425
|
+
* | 'coordinator-invoked-module' | 'hand-keyed-learning-subject'
|
|
376
426
|
* | 'tool-error',
|
|
377
427
|
* key: string,
|
|
378
428
|
* message: string,
|
|
@@ -602,19 +652,57 @@ export function collectExportedSymbols(body) {
|
|
|
602
652
|
return [...names];
|
|
603
653
|
}
|
|
604
654
|
|
|
655
|
+
/**
|
|
656
|
+
* Re-export targets of a barrel: the specifiers of its `export * from '…'`
|
|
657
|
+
* (and `export * as ns from '…'`) lines.
|
|
658
|
+
*
|
|
659
|
+
* `collectExportedSymbols` cannot see these — a star re-export names no symbol —
|
|
660
|
+
* so a PURE barrel reads as `exports: []`. That does not merely hide the barrel:
|
|
661
|
+
* the S4 population predicate below requires at least one export, so the barrel
|
|
662
|
+
* never enters `unreachable` at all, and is therefore unavailable as the CLUSTER
|
|
663
|
+
* ROOT of the module it re-exports. The target is then reported as its own root.
|
|
664
|
+
*
|
|
665
|
+
* Measured 2026-09-16 on the live tree: `scripts/lib/worktree.mjs` is exactly
|
|
666
|
+
* `export * from './worktree/index.mjs';`, its only importers (`workspace.mjs`,
|
|
667
|
+
* `worktree-freshness.mjs`) are themselves unreachable — yet S4 reported
|
|
668
|
+
* `scripts/lib/worktree/index.mjs`, an interior file, instead of the cluster.
|
|
669
|
+
*
|
|
670
|
+
* Only the POPULATION is affected. Edge propagation already works: the BFS walks
|
|
671
|
+
* `mentions`, and a star re-export line is not a comment, so a barrel that IS
|
|
672
|
+
* reachable already marks its target reachable (measured the same day:
|
|
673
|
+
* `worktree.mjs` carries `index.mjs` in `mentions`).
|
|
674
|
+
*
|
|
675
|
+
* Anchored at column 0 like the export-symbol grammar above, so a docblock line
|
|
676
|
+
* (`* export * from …`) and an indented string never count.
|
|
677
|
+
*
|
|
678
|
+
* @param {string} body module source
|
|
679
|
+
* @returns {string[]} re-exported module specifiers, as written
|
|
680
|
+
*/
|
|
681
|
+
export function collectReExportTargets(body) {
|
|
682
|
+
return [
|
|
683
|
+
...body.matchAll(/^export\s+\*(?:\s+as\s+[A-Za-z0-9_$]+)?\s+from\s+['"]([^'"]+)['"]/gm),
|
|
684
|
+
].map((match) => match[1]);
|
|
685
|
+
}
|
|
686
|
+
|
|
605
687
|
/**
|
|
606
688
|
* Whether a module is a CLI entrypoint rather than a library.
|
|
607
689
|
*
|
|
608
690
|
* An entrypoint is invoked by path (npm script, hook wiring, CI job), so having
|
|
609
691
|
* no importer is its normal state and says nothing about being wired.
|
|
610
692
|
*
|
|
693
|
+
* `isMainModule(` is part of the grammar because the #1371 sweep replaced ~50
|
|
694
|
+
* hand-written `process.argv[1] === import.meta.url` guards with the shared
|
|
695
|
+
* `scripts/lib/is-main-module.mjs` predicate. Without this alternative a swept
|
|
696
|
+
* CLI matches none of the other branches and silently reclassifies as a library
|
|
697
|
+
* candidate, so S4 reports it as an `unreachable-library-module` root.
|
|
698
|
+
*
|
|
611
699
|
* @param {string} body module source
|
|
612
700
|
* @returns {boolean}
|
|
613
701
|
*/
|
|
614
702
|
export function isCliEntrypoint(body) {
|
|
615
703
|
return (
|
|
616
704
|
body.startsWith('#!') ||
|
|
617
|
-
/import\.meta\.url\s*===|require\.main\s*===\s*module|process\.argv\[1\]/.test(body)
|
|
705
|
+
/import\.meta\.url\s*===|require\.main\s*===\s*module|process\.argv\[1\]|isMainModule\(/.test(body)
|
|
618
706
|
);
|
|
619
707
|
}
|
|
620
708
|
|
|
@@ -840,6 +928,10 @@ export function collectUnreachableLibraryModules(pluginRoot) {
|
|
|
840
928
|
edgeOnly,
|
|
841
929
|
entrypoint: isCliEntrypoint(body),
|
|
842
930
|
exports: collectExportedSymbols(body),
|
|
931
|
+
// Star re-exports name no symbol, so `exports` is empty for a pure
|
|
932
|
+
// barrel — see collectReExportTargets for why that silently promoted an
|
|
933
|
+
// interior module to a root.
|
|
934
|
+
reExports: collectReExportTargets(body),
|
|
843
935
|
// This file contributes NO edges — the S4 counterpart of the SELF_REL
|
|
844
936
|
// exclusion the S1/S2 corpus already applies, and for the identical
|
|
845
937
|
// reason. Every S4 `ALLOWLIST` key is a module path written here as a
|
|
@@ -848,7 +940,18 @@ export function collectUnreachableLibraryModules(pluginRoot) {
|
|
|
848
940
|
// `allowlist-stale` — the check silently blinding itself to exactly the
|
|
849
941
|
// module an operator flagged. Measured 2026-08-28 on the first S4
|
|
850
942
|
// allowlist entry: 52 → 51 unreachable modules plus one bogus stale line.
|
|
851
|
-
mentions:
|
|
943
|
+
mentions:
|
|
944
|
+
relative === SELF_REL
|
|
945
|
+
? // This file blinds itself to its own ALLOWLIST literals (above) — but
|
|
946
|
+
// NOT to its own real imports. Dropping every edge here made a module
|
|
947
|
+
// this file genuinely `import`s (`learnings/sizing-subject.mjs`, the
|
|
948
|
+
// S5 check) still read as unreachable, which is the identical
|
|
949
|
+
// self-blinding the exclusion exists to prevent, in the other
|
|
950
|
+
// direction. Static import/export lines carry real edges; an
|
|
951
|
+
// ALLOWLIST key is never one, so scoping the scan to them keeps both
|
|
952
|
+
// properties.
|
|
953
|
+
mentionedModuleTokens(lines.filter((line) => /^\s*(?:import|export)\b/.test(line)))
|
|
954
|
+
: mentionedModuleTokens(lines),
|
|
852
955
|
// Raw text, kept for the QUALIFIED (`dirname/base`) re-check in the
|
|
853
956
|
// root filter below: `mentionedModuleTokens` strips the directory, so
|
|
854
957
|
// a colliding basename can only be disambiguated against the body.
|
|
@@ -886,8 +989,15 @@ export function collectUnreachableLibraryModules(pluginRoot) {
|
|
|
886
989
|
}
|
|
887
990
|
}
|
|
888
991
|
|
|
992
|
+
// A module with NO public surface at all is not a finding — nothing can be
|
|
993
|
+
// wired to it. A pure `export * from` barrel HAS a surface (everything its
|
|
994
|
+
// target exports), so it belongs in the population: otherwise it cannot be
|
|
995
|
+
// the cluster root of the module it re-exports (collectReExportTargets).
|
|
889
996
|
const unreachable = modules.filter(
|
|
890
|
-
(module) =>
|
|
997
|
+
(module) =>
|
|
998
|
+
!reachable.has(module.relative) &&
|
|
999
|
+
!module.entrypoint &&
|
|
1000
|
+
(module.exports.length > 0 || module.reExports.length > 0),
|
|
891
1001
|
);
|
|
892
1002
|
const unreachableSet = new Set(unreachable.map((module) => module.relative));
|
|
893
1003
|
// Basename census, shared by the root filter below and the downgrade half
|
|
@@ -923,6 +1033,35 @@ export function collectUnreachableLibraryModules(pluginRoot) {
|
|
|
923
1033
|
);
|
|
924
1034
|
});
|
|
925
1035
|
|
|
1036
|
+
// A pure `export *` barrel enters the population above so it can HEAD its own
|
|
1037
|
+
// cluster, but reporting it needs one more condition. S3 already exempts this
|
|
1038
|
+
// exact shape by name (`autopilot-telemetry.mjs`, condition 4): a star
|
|
1039
|
+
// re-export has zero NAMED symbols, so every "does prose name one of its
|
|
1040
|
+
// exports?" test is vacuously FALSE — including S4's own coordinator-invoked
|
|
1041
|
+
// downgrade below, which iterates `module.exports`. A reported pure barrel
|
|
1042
|
+
// could therefore never be downgraded, only allowlisted: a finding with no
|
|
1043
|
+
// legitimate exit. The category split is the barrel's TARGET. A barrel over a
|
|
1044
|
+
// module that is itself unreachable heads a genuinely dead cluster and is
|
|
1045
|
+
// reported; a barrel over LIVE code is a backward-compat shim, which is a
|
|
1046
|
+
// different (and much weaker) finding than "nothing can reach this feature".
|
|
1047
|
+
// Measured 2026-09-16: without this split `scripts/lib/autopilot-telemetry.mjs`
|
|
1048
|
+
// — `export * from './autopilot/telemetry.mjs'`, target reachable, sole
|
|
1049
|
+
// importer a test — became a new permanent WARN.
|
|
1050
|
+
// Ceiling: a dead shim over live code is now invisible to S4 as well as to S3.
|
|
1051
|
+
// Revisit if a stale back-compat shim is ever confirmed to have outlived its
|
|
1052
|
+
// last importer unnoticed — that wants its own check, not a looser S4.
|
|
1053
|
+
const reportableRoots = roots.filter((module) => {
|
|
1054
|
+
if (module.exports.length > 0) return true;
|
|
1055
|
+
return module.reExports.some((spec) => {
|
|
1056
|
+
if (!spec.startsWith('.')) {
|
|
1057
|
+
// Aliased/bare specifier: fall back to the basename granularity this
|
|
1058
|
+
// file already documents as its named residual.
|
|
1059
|
+
return [...unreachableSet].some((rel) => path.basename(rel) === path.basename(spec));
|
|
1060
|
+
}
|
|
1061
|
+
return unreachableSet.has(path.normalize(path.join(path.dirname(module.relative), spec)));
|
|
1062
|
+
});
|
|
1063
|
+
});
|
|
1064
|
+
|
|
926
1065
|
// Category split (see § Category split in the doc block above): an INSTRUCTION
|
|
927
1066
|
// document that names both the module AND one of its exported symbols is an
|
|
928
1067
|
// order addressed to a reader who will execute it — the same grammar
|
|
@@ -936,7 +1075,7 @@ export function collectUnreachableLibraryModules(pluginRoot) {
|
|
|
936
1075
|
.map((file) => ({ relative: path.relative(pluginRoot, file), body: readFileSync(file, 'utf8') }));
|
|
937
1076
|
|
|
938
1077
|
let coordinatorInvoked = 0;
|
|
939
|
-
const findings =
|
|
1078
|
+
const findings = reportableRoots.map((module) => {
|
|
940
1079
|
// Docs write POSIX separators regardless of host; `path.relative` does not.
|
|
941
1080
|
const relativePosix = module.relative.split(path.sep).join('/');
|
|
942
1081
|
const qualified = relativePosix.split('/').slice(-2).join('/');
|
|
@@ -971,11 +1110,17 @@ export function collectUnreachableLibraryModules(pluginRoot) {
|
|
|
971
1110
|
(token) => token !== module.base && [...unreachableSet].some((rel) => path.basename(rel) === token),
|
|
972
1111
|
);
|
|
973
1112
|
const tail = dragged.length > 0 ? `, and drags ${dragged.length} further unreachable module(s)` : '';
|
|
1113
|
+
// A pure barrel reports its re-export surface; naming "0 symbol(s)" there
|
|
1114
|
+
// would read as a checker bug rather than as the barrel it is.
|
|
1115
|
+
const surface =
|
|
1116
|
+
module.exports.length > 0
|
|
1117
|
+
? `exports ${module.exports.length} symbol(s) (${module.exports.slice(0, 3).join(', ')})`
|
|
1118
|
+
: `re-exports ${module.reExports.length} module(s) (${module.reExports.slice(0, 3).join(', ')})`;
|
|
974
1119
|
return /** @type {Finding} */ ({
|
|
975
1120
|
kind: 'unreachable-library-module',
|
|
976
1121
|
key: module.relative,
|
|
977
1122
|
message:
|
|
978
|
-
|
|
1123
|
+
`${surface} but no hook, ` +
|
|
979
1124
|
`npm script, CI job or husky stage reaches it — transitively${tail}. No instruction surface names ` +
|
|
980
1125
|
'one of its exports either: wire it, delete it, or allowlist it with a reason',
|
|
981
1126
|
});
|
|
@@ -985,13 +1130,107 @@ export function collectUnreachableLibraryModules(pluginRoot) {
|
|
|
985
1130
|
findings,
|
|
986
1131
|
scanned: {
|
|
987
1132
|
modules: modules.length,
|
|
988
|
-
roots:
|
|
1133
|
+
roots: reportableRoots.length,
|
|
989
1134
|
unreachable: unreachable.length,
|
|
990
1135
|
coordinatorInvoked,
|
|
991
1136
|
},
|
|
992
1137
|
};
|
|
993
1138
|
}
|
|
994
1139
|
|
|
1140
|
+
/**
|
|
1141
|
+
* Read a JSONL ledger, skipping blank and unparseable lines.
|
|
1142
|
+
*
|
|
1143
|
+
* A single truncated line (an interrupted append) must not blind the whole
|
|
1144
|
+
* check — the same fail-soft posture the rest of this file takes toward a
|
|
1145
|
+
* missing surface.
|
|
1146
|
+
*
|
|
1147
|
+
* @param {string} file absolute path
|
|
1148
|
+
* @returns {Record<string, unknown>[]}
|
|
1149
|
+
*/
|
|
1150
|
+
function readJsonl(file) {
|
|
1151
|
+
if (!existsSync(file)) return [];
|
|
1152
|
+
/** @type {Record<string, unknown>[]} */
|
|
1153
|
+
const records = [];
|
|
1154
|
+
for (const line of readFileSync(file, 'utf8').split('\n')) {
|
|
1155
|
+
const trimmed = line.trim();
|
|
1156
|
+
if (trimmed === '') continue;
|
|
1157
|
+
try {
|
|
1158
|
+
const parsed = JSON.parse(trimmed);
|
|
1159
|
+
if (parsed && typeof parsed === 'object') records.push(parsed);
|
|
1160
|
+
} catch {
|
|
1161
|
+
// truncated / partial append — skip the line, keep the census
|
|
1162
|
+
}
|
|
1163
|
+
}
|
|
1164
|
+
return records;
|
|
1165
|
+
}
|
|
1166
|
+
|
|
1167
|
+
/**
|
|
1168
|
+
* Signal S5 — an `effective-sizing` learning whose subject was hand-written
|
|
1169
|
+
* instead of derived from `sizingSubject()`.
|
|
1170
|
+
*
|
|
1171
|
+
* See the header § S5 for the defect, the deliberate narrowness of what is
|
|
1172
|
+
* judged, and the measured reason a free-form subject is not a finding.
|
|
1173
|
+
*
|
|
1174
|
+
* @param {string} pluginRoot absolute plugin root
|
|
1175
|
+
* @returns {{findings: Finding[], scanned: {judged: number, unattributed: number}}}
|
|
1176
|
+
*/
|
|
1177
|
+
export function collectHandKeyedLearningSubjects(pluginRoot) {
|
|
1178
|
+
/** @type {Finding[]} */
|
|
1179
|
+
const findings = [];
|
|
1180
|
+
let judged = 0;
|
|
1181
|
+
let unattributed = 0;
|
|
1182
|
+
|
|
1183
|
+
const learnings = readJsonl(path.join(pluginRoot, LEARNINGS_REL));
|
|
1184
|
+
if (learnings.length === 0) return { findings, scanned: { judged, unattributed } };
|
|
1185
|
+
|
|
1186
|
+
// A session record can be addressed by either identity form (see
|
|
1187
|
+
// `.claude/rules/identity-and-locks.md`: raw UUID + semantic id), and a
|
|
1188
|
+
// learning's `source_session` carries whichever the writer had. Index both, so
|
|
1189
|
+
// a resolvable record is never mis-counted as unattributed.
|
|
1190
|
+
/** @type {Map<string, Record<string, unknown>>} */
|
|
1191
|
+
const sessionsById = new Map();
|
|
1192
|
+
for (const record of readJsonl(path.join(pluginRoot, SESSIONS_REL))) {
|
|
1193
|
+
for (const field of ['session_id', 'semantic_session_id']) {
|
|
1194
|
+
const id = record[field];
|
|
1195
|
+
if (typeof id === 'string' && id !== '' && !sessionsById.has(id)) sessionsById.set(id, record);
|
|
1196
|
+
}
|
|
1197
|
+
}
|
|
1198
|
+
|
|
1199
|
+
for (const learning of learnings) {
|
|
1200
|
+
if (learning.type !== 'effective-sizing') continue;
|
|
1201
|
+
const subject = learning.subject;
|
|
1202
|
+
if (typeof subject !== 'string' || !SIZING_SUBJECT_RE.test(subject)) continue;
|
|
1203
|
+
|
|
1204
|
+
const sourceSession = typeof learning.source_session === 'string' ? learning.source_session : '';
|
|
1205
|
+
const record = sessionsById.get(sourceSession);
|
|
1206
|
+
if (!record) {
|
|
1207
|
+
unattributed += 1;
|
|
1208
|
+
continue;
|
|
1209
|
+
}
|
|
1210
|
+
|
|
1211
|
+
judged += 1;
|
|
1212
|
+
const expected = sizingSubject({
|
|
1213
|
+
session_type: record.session_type,
|
|
1214
|
+
session_profile: record.session_profile,
|
|
1215
|
+
});
|
|
1216
|
+
if (subject === expected) continue;
|
|
1217
|
+
|
|
1218
|
+
findings.push({
|
|
1219
|
+
kind: 'hand-keyed-learning-subject',
|
|
1220
|
+
key: typeof learning.id === 'string' ? learning.id : subject,
|
|
1221
|
+
message:
|
|
1222
|
+
`effective-sizing learning keyed '${subject}', but sizingSubject() derives '${expected}' from ` +
|
|
1223
|
+
`${sourceSession} (session_type=${String(record.session_type)}, session_profile=` +
|
|
1224
|
+
`${record.session_profile === undefined ? 'absent' : String(record.session_profile)}) — the ` +
|
|
1225
|
+
'subject was hand-concatenated instead of derived via ' +
|
|
1226
|
+
'scripts/lib/learnings/sizing-subject.mjs, so two session shapes can silently share one row ' +
|
|
1227
|
+
'(GitLab #1247); re-key the learning and derive via the helper',
|
|
1228
|
+
});
|
|
1229
|
+
}
|
|
1230
|
+
|
|
1231
|
+
return { findings, scanned: { judged, unattributed } };
|
|
1232
|
+
}
|
|
1233
|
+
|
|
995
1234
|
/**
|
|
996
1235
|
* Run the full census.
|
|
997
1236
|
*
|
|
@@ -1000,7 +1239,8 @@ export function collectUnreachableLibraryModules(pluginRoot) {
|
|
|
1000
1239
|
* ok: boolean,
|
|
1001
1240
|
* summary: {declaredKeys: number, consumerFiles: number, unwired: number, allowlisted: number,
|
|
1002
1241
|
* orphanedModules: number, unreachableModules: number,
|
|
1003
|
-
* coordinatorInvokedModules: number
|
|
1242
|
+
* coordinatorInvokedModules: number, handKeyedSubjects: number,
|
|
1243
|
+
* judgedSubjects: number},
|
|
1004
1244
|
* sourcesScanned: string[],
|
|
1005
1245
|
* findings: Finding[],
|
|
1006
1246
|
* toolError: boolean,
|
|
@@ -1019,6 +1259,8 @@ export function inspectUnwiredFeatures(pluginRoot) {
|
|
|
1019
1259
|
orphanedModules: 0,
|
|
1020
1260
|
unreachableModules: 0,
|
|
1021
1261
|
coordinatorInvokedModules: 0,
|
|
1262
|
+
handKeyedSubjects: 0,
|
|
1263
|
+
judgedSubjects: 0,
|
|
1022
1264
|
},
|
|
1023
1265
|
/** @type {string[]} */
|
|
1024
1266
|
sourcesScanned: [],
|
|
@@ -1036,6 +1278,8 @@ export function inspectUnwiredFeatures(pluginRoot) {
|
|
|
1036
1278
|
let orphans;
|
|
1037
1279
|
/** @type {ReturnType<typeof collectUnreachableLibraryModules>} */
|
|
1038
1280
|
let unreachable;
|
|
1281
|
+
/** @type {ReturnType<typeof collectHandKeyedLearningSubjects>} */
|
|
1282
|
+
let handKeyed;
|
|
1039
1283
|
try {
|
|
1040
1284
|
declared = collectDeclaredKeys(pluginRoot);
|
|
1041
1285
|
corpus = CONSUMER_DIRS.flatMap((dir) => walkCode(path.join(pluginRoot, dir)))
|
|
@@ -1054,6 +1298,7 @@ export function inspectUnwiredFeatures(pluginRoot) {
|
|
|
1054
1298
|
.join('\n');
|
|
1055
1299
|
orphans = collectOrphanedProseModules(pluginRoot);
|
|
1056
1300
|
unreachable = collectUnreachableLibraryModules(pluginRoot);
|
|
1301
|
+
handKeyed = collectHandKeyedLearningSubjects(pluginRoot);
|
|
1057
1302
|
} catch (error) {
|
|
1058
1303
|
result.toolError = true;
|
|
1059
1304
|
findings.push({
|
|
@@ -1134,6 +1379,13 @@ export function inspectUnwiredFeatures(pluginRoot) {
|
|
|
1134
1379
|
findings.push(finding);
|
|
1135
1380
|
}
|
|
1136
1381
|
|
|
1382
|
+
// S5 — a written learning subject that prose said must come from the helper,
|
|
1383
|
+
// and did not. Not allowlistable: the fix is to re-key the record, and an
|
|
1384
|
+
// exemption would preserve exactly the collision #1247 removed.
|
|
1385
|
+
result.summary.handKeyedSubjects = handKeyed.findings.length;
|
|
1386
|
+
result.summary.judgedSubjects = handKeyed.scanned.judged;
|
|
1387
|
+
findings.push(...handKeyed.findings);
|
|
1388
|
+
|
|
1137
1389
|
for (const key of Object.keys(ALLOWLIST).sort()) {
|
|
1138
1390
|
if (flagged.has(key)) continue;
|
|
1139
1391
|
findings.push({
|
|
@@ -1177,6 +1429,8 @@ export function runCheckUnwiredFeatures(pluginRoot, { list = false } = {}) {
|
|
|
1177
1429
|
orphanedModules,
|
|
1178
1430
|
unreachableModules,
|
|
1179
1431
|
coordinatorInvokedModules,
|
|
1432
|
+
handKeyedSubjects,
|
|
1433
|
+
judgedSubjects,
|
|
1180
1434
|
} = inspection.summary;
|
|
1181
1435
|
|
|
1182
1436
|
// S4 is a BACKLOG, not a per-run alarm: 50 findings on the live tree against
|
|
@@ -1215,14 +1469,15 @@ export function runCheckUnwiredFeatures(pluginRoot, { list = false } = {}) {
|
|
|
1215
1469
|
` PASS: censused ${declaredKeys} declared key(s) from ${inspection.sourcesScanned.join(' + ') || '(no source)'} ` +
|
|
1216
1470
|
`against ${consumerFiles} consumer file(s) — ${unwired} unwired, ${allowlisted} allowlisted, ` +
|
|
1217
1471
|
`${orphanedModules} prose-orphaned module(s), ${unreachableModules} unreachable module(s), ` +
|
|
1218
|
-
`${coordinatorInvokedModules} coordinator-invoked module(s)
|
|
1472
|
+
`${coordinatorInvokedModules} coordinator-invoked module(s), ${handKeyedSubjects} hand-keyed ` +
|
|
1473
|
+
`learning subject(s) of ${judgedSubjects} judged`,
|
|
1219
1474
|
);
|
|
1220
1475
|
console.log('');
|
|
1221
1476
|
console.log('Results: 1 passed, 0 failed');
|
|
1222
1477
|
return 0;
|
|
1223
1478
|
}
|
|
1224
1479
|
|
|
1225
|
-
const isMain = import.meta.url
|
|
1480
|
+
const isMain = isMainModule(import.meta.url);
|
|
1226
1481
|
if (isMain) {
|
|
1227
1482
|
const args = process.argv.slice(2);
|
|
1228
1483
|
const pluginRoot = args.find((arg) => !arg.startsWith('-'));
|
|
@@ -72,8 +72,8 @@
|
|
|
72
72
|
|
|
73
73
|
import { existsSync, readFileSync } from 'node:fs';
|
|
74
74
|
import path from 'node:path';
|
|
75
|
-
import { pathToFileURL } from 'node:url';
|
|
76
75
|
import { enumerateRepoFiles } from './enumerate-repo-files.mjs';
|
|
76
|
+
import { isMainModule } from '../is-main-module.mjs';
|
|
77
77
|
|
|
78
78
|
/** Marker line inside a checker's own header — declares deliberate CLI-only status. */
|
|
79
79
|
export const STANDALONE_MARKER = /^\s*\/\/\s*registration:\s*standalone\b(?:\s+(.*))?$/m;
|
|
@@ -243,7 +243,7 @@ export function runCheckValidatorRegistration(repoRoot) {
|
|
|
243
243
|
return fail > 0 ? 1 : 0;
|
|
244
244
|
}
|
|
245
245
|
|
|
246
|
-
const isMain =
|
|
246
|
+
const isMain =isMainModule(import.meta.url);
|
|
247
247
|
if (isMain) {
|
|
248
248
|
const root = process.argv[2];
|
|
249
249
|
if (!root) {
|
|
@@ -193,8 +193,8 @@
|
|
|
193
193
|
|
|
194
194
|
import { existsSync, readFileSync, readdirSync } from 'node:fs';
|
|
195
195
|
import path from 'node:path';
|
|
196
|
-
import { pathToFileURL } from 'node:url';
|
|
197
196
|
import { SHELL_LANGS, forEachLine } from './markdown-fences.mjs';
|
|
197
|
+
import { isMainModule } from '../is-main-module.mjs';
|
|
198
198
|
|
|
199
199
|
/** Directories whose content is scanned. Root-level `*.md` is added separately. */
|
|
200
200
|
const SCAN_DIRS = Object.freeze([
|
|
@@ -906,7 +906,7 @@ export function runCheckVcsRepoFlag(pluginRoot) {
|
|
|
906
906
|
return 0;
|
|
907
907
|
}
|
|
908
908
|
|
|
909
|
-
const isMain =
|
|
909
|
+
const isMain =isMainModule(import.meta.url);
|
|
910
910
|
if (isMain) {
|
|
911
911
|
const argv = process.argv.slice(2);
|
|
912
912
|
const flags = new Set(argv.filter((a) => a.startsWith('--')));
|
|
@@ -6,9 +6,12 @@
|
|
|
6
6
|
* Catches vendoring bugs that are invisible at the source-of-truth layer but
|
|
7
7
|
* become live footguns once synced into a target repo:
|
|
8
8
|
*
|
|
9
|
-
* - `paths:` frontmatter
|
|
10
|
-
*
|
|
11
|
-
*
|
|
9
|
+
* - `paths:` frontmatter in a `rules/` LIBRARY SOURCE — a vendoring-
|
|
10
|
+
* CONVENTION gate, not a loader-compatibility one. Since #795
|
|
11
|
+
* `rule-loader.mjs` accepts `paths:` as an alias for `globs:`, so such a
|
|
12
|
+
* rule IS glob-scoped; `globs:` stays the canonical form for a rule
|
|
13
|
+
* vendored out through the library (#742). See § Scope below — this probe
|
|
14
|
+
* never judges a repo's own consolidated `.claude/rules/` files.
|
|
12
15
|
* - Missing provenance header — `rules-sync.mjs` detects "plugin-owned vs.
|
|
13
16
|
* local override" purely by checking whether the first line starts with
|
|
14
17
|
* `PLUGIN_HEADER_PREFIX`. A source file missing that header gets
|
|
@@ -31,6 +34,25 @@
|
|
|
31
34
|
* module imports the already-exported `parseGlobsFrontmatter` from
|
|
32
35
|
* `rule-loader.mjs` directly.
|
|
33
36
|
*
|
|
37
|
+
* ## Scope: the `rules/` fleet library, never a repo's own `.claude/rules/`
|
|
38
|
+
*
|
|
39
|
+
* Every production caller feeds this module SOURCE files from the plugin's
|
|
40
|
+
* fleet library: `syncRules()` (`rules-sync.mjs`) validates
|
|
41
|
+
* `<pluginRoot>/rules/<relPath>` for each entry listed in `rules/_index.md`
|
|
42
|
+
* BEFORE writing the vendored copy into a consumer's `.claude/rules/` — the
|
|
43
|
+
* written target is never read back through the gate. The CLI's `--dir` is
|
|
44
|
+
* operator-supplied and means that same library.
|
|
45
|
+
*
|
|
46
|
+
* This is load-bearing for `paths-frontmatter`: a repo's own CONSOLIDATED rules
|
|
47
|
+
* under `.claude/rules/` are `paths:`-canonical by design, because Claude
|
|
48
|
+
* Code's native loader reads ONLY `paths:` and treats a rule without it as
|
|
49
|
+
* always-on (`validate/check-rules.mjs` #1108; `docs/rule-authoring.md`
|
|
50
|
+
* § Consolidated rules point 3). Measured 2026-09-16 in this repo: 10
|
|
51
|
+
* path-scoped rule files there, 9 of them `paths:`-only. Pointing `--dir` at
|
|
52
|
+
* such a tree would emit findings that, if obeyed, UNDO that consolidation —
|
|
53
|
+
* which is also why `check-rules.mjs` deliberately does not duplicate this
|
|
54
|
+
* probe for `.claude/rules/`.
|
|
55
|
+
*
|
|
34
56
|
* @module validate-vendored-rules
|
|
35
57
|
*/
|
|
36
58
|
|
|
@@ -40,6 +62,7 @@ import { fileURLToPath } from 'node:url';
|
|
|
40
62
|
import { execFileSync } from 'node:child_process';
|
|
41
63
|
import { createRequire } from 'node:module';
|
|
42
64
|
import { parseGlobsFrontmatter } from './rule-loader.mjs';
|
|
65
|
+
import { isMainModule } from './is-main-module.mjs';
|
|
43
66
|
|
|
44
67
|
// Mirrors rules-sync.mjs's exported PLUGIN_HEADER_PREFIX (rules-sync.mjs
|
|
45
68
|
// line ~13). NOT imported from there on purpose: rules-sync.mjs imports
|
|
@@ -478,10 +501,17 @@ export function validateRuleContent({ content, relPath, targetRoot = null, requi
|
|
|
478
501
|
// `paths:`-only rule IS glob-scoped. The probe itself stays: it enforces the canonical
|
|
479
502
|
// vendoring form, which is a convention gate, not a loader-compatibility gate. That
|
|
480
503
|
// intent survives #795 and is the subject of the #742 fleet canonicalisation sweep.
|
|
504
|
+
// SCOPE (2026-09-16): this fires only over `rules/` library sources — the module doc's
|
|
505
|
+
// § Scope section names every caller. Do NOT re-point it at a repo's own
|
|
506
|
+
// `.claude/rules/`: those consolidated files are `paths:`-canonical and obeying this
|
|
507
|
+
// remedy there would undo the consolidation (docs/rule-authoring.md point 3).
|
|
481
508
|
message:
|
|
482
509
|
`${relPath}: frontmatter declares a top-level 'paths:' key. It is a recognized alias ` +
|
|
483
510
|
`for 'globs:' (issue #795), so the rule does load glob-scoped — but 'globs:' is the ` +
|
|
484
|
-
`canonical form for
|
|
511
|
+
`canonical form for a rule VENDORED OUT through the plugin's rules/ library, which is ` +
|
|
512
|
+
`this probe's only population. Migrate to 'globs:' (see issue #742). This does NOT ` +
|
|
513
|
+
`apply to a repo's own consolidated .claude/rules/ files — those are paths:-canonical ` +
|
|
514
|
+
`(Claude Code's native loader reads only 'paths:') and this gate never scans them.`,
|
|
485
515
|
line: lineWithinFrontmatter(fm.startLine, fm.body, pathsMatch.index),
|
|
486
516
|
});
|
|
487
517
|
}
|
|
@@ -652,11 +682,7 @@ export function validateRulesDir({ dir, targetRoot = null, requireProvenance = f
|
|
|
652
682
|
|
|
653
683
|
const __filename = fileURLToPath(import.meta.url);
|
|
654
684
|
|
|
655
|
-
const isMain =
|
|
656
|
-
typeof process !== 'undefined' &&
|
|
657
|
-
process.argv[1] !== null &&
|
|
658
|
-
process.argv[1] !== undefined &&
|
|
659
|
-
resolve(process.argv[1]) === resolve(__filename);
|
|
685
|
+
const isMain =isMainModule(import.meta.url);
|
|
660
686
|
|
|
661
687
|
/**
|
|
662
688
|
* @param {{ ok: boolean, files: Array<{ file: string, violations: RuleViolation[] }>, errorCount: number, warnCount: number }} result
|
|
@@ -55,12 +55,12 @@ import {
|
|
|
55
55
|
} from 'node:fs';
|
|
56
56
|
import { homedir } from 'node:os';
|
|
57
57
|
import { join, relative, isAbsolute } from 'node:path';
|
|
58
|
-
import { pathToFileURL } from 'node:url';
|
|
59
58
|
|
|
60
59
|
import { emitEvent, sessionAttribution } from './events.mjs';
|
|
61
60
|
import { tryAcquireFileLock, releaseFileLock } from './file-lock.mjs';
|
|
62
61
|
import { readLock, isLockLive } from './session-lock.mjs';
|
|
63
62
|
import { resolveSubagentSidecar } from '../../hooks/_lib/subagent-paths.mjs';
|
|
63
|
+
import { isMainModule } from './is-main-module.mjs';
|
|
64
64
|
|
|
65
65
|
const DEFAULT_INTERVAL_S = 2;
|
|
66
66
|
const EVENTS_FILE_REL = '.orchestrator/metrics/events.jsonl';
|
|
@@ -998,6 +998,6 @@ function main() {
|
|
|
998
998
|
|
|
999
999
|
// Run only when executed as a script — importing for unit tests must not parse
|
|
1000
1000
|
// vitest's argv and exit 1.
|
|
1001
|
-
if (
|
|
1001
|
+
if (isMainModule(import.meta.url)) {
|
|
1002
1002
|
main();
|
|
1003
1003
|
}
|
package/scripts/lock-reaper.mjs
CHANGED
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
*/
|
|
23
23
|
|
|
24
24
|
import { reapStaleLocks } from './lib/lock-reaper.mjs';
|
|
25
|
+
import { isMainModule } from './lib/is-main-module.mjs';
|
|
25
26
|
|
|
26
27
|
// ---------------------------------------------------------------------------
|
|
27
28
|
// Human-readable output
|
|
@@ -175,7 +176,7 @@ EXIT CODES
|
|
|
175
176
|
// CLI guard — prevents execution during test-time imports.
|
|
176
177
|
// ---------------------------------------------------------------------------
|
|
177
178
|
|
|
178
|
-
if (import.meta.url
|
|
179
|
+
if (isMainModule(import.meta.url)) {
|
|
179
180
|
main()
|
|
180
181
|
.then((code) => process.exit(code))
|
|
181
182
|
.catch((err) => {
|