session-orchestrator 3.22.0 → 3.23.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/.cursor/commands/autopilot-multi.md +14 -0
- package/.cursor/commands/autopilot.md +14 -0
- package/.cursor/commands/bootstrap.md +14 -0
- package/.cursor/commands/brainstorm.md +14 -0
- package/.cursor/commands/close.md +13 -0
- package/.cursor/commands/contract-version-bump.md +14 -0
- package/.cursor/commands/debug.md +14 -0
- package/.cursor/commands/discovery.md +14 -0
- package/.cursor/commands/dispatcher.md +14 -0
- package/.cursor/commands/eli5.md +14 -0
- package/.cursor/commands/eval.md +14 -0
- package/.cursor/commands/evolve.md +14 -0
- package/.cursor/commands/go.md +14 -0
- package/.cursor/commands/grill.md +14 -0
- package/.cursor/commands/harness-audit.md +13 -0
- package/.cursor/commands/journey-audit.md +14 -0
- package/.cursor/commands/memory-cleanup.md +14 -0
- package/.cursor/commands/persona-panel.md +14 -0
- package/.cursor/commands/plan.md +14 -0
- package/.cursor/commands/portfolio.md +14 -0
- package/.cursor/commands/reconcile.md +14 -0
- package/.cursor/commands/release.md +14 -0
- package/.cursor/commands/repo-audit.md +13 -0
- package/.cursor/commands/session.md +14 -0
- package/.cursor/commands/spinout.md +14 -0
- package/.cursor/commands/sunset-review.md +14 -0
- package/.cursor/commands/templates-ack.md +14 -0
- package/.cursor/commands/test.md +14 -0
- package/.cursor/hooks.json +60 -0
- package/.cursor/rules/000-session-orchestrator.mdc +8 -0
- package/.cursor/rules/010-session-workflow.mdc +9 -1
- package/.cursor/rules/020-quality-gates.mdc +1 -1
- package/.cursor/rules/030-wave-execution.mdc +1 -1
- package/.cursor/rules/050-plan.mdc +2 -2
- package/.cursor/rules/070-gitlab-ops.mdc +73 -57
- package/.cursor/rules/080-ecosystem-health.mdc +7 -7
- package/.cursor/skills/architecture/SKILL.md +13 -0
- package/.cursor/skills/autopilot/SKILL.md +12 -0
- package/.cursor/skills/bootstrap/SKILL.md +12 -0
- package/.cursor/skills/brainstorm/SKILL.md +13 -0
- package/.cursor/skills/claude-md-drift-check/SKILL.md +13 -0
- package/.cursor/skills/contract-version-bump/SKILL.md +12 -0
- package/.cursor/skills/convergence-monitoring/SKILL.md +12 -0
- package/.cursor/skills/daily/SKILL.md +12 -0
- package/.cursor/skills/debug/SKILL.md +13 -0
- package/.cursor/skills/discovery/SKILL.md +13 -0
- package/.cursor/skills/dispatcher/SKILL.md +13 -0
- package/.cursor/skills/docs-orchestrator/SKILL.md +13 -0
- package/.cursor/skills/domain-model/SKILL.md +13 -0
- package/.cursor/skills/ecosystem-health/SKILL.md +13 -0
- package/.cursor/skills/eli5/SKILL.md +13 -0
- package/.cursor/skills/eval/SKILL.md +12 -0
- package/.cursor/skills/evolve/SKILL.md +13 -0
- package/.cursor/skills/frontmatter-guard/SKILL.md +13 -0
- package/.cursor/skills/gitlab-ops/SKILL.md +13 -0
- package/.cursor/skills/gitlab-portfolio/SKILL.md +13 -0
- package/.cursor/skills/grill/SKILL.md +13 -0
- package/.cursor/skills/hook-development/SKILL.md +13 -0
- package/.cursor/skills/journey-audit/SKILL.md +13 -0
- package/.cursor/skills/mcp-builder/SKILL.md +13 -0
- package/.cursor/skills/memory-cleanup/SKILL.md +12 -0
- package/.cursor/skills/mode-selector/SKILL.md +13 -0
- package/.cursor/skills/npm-publish/SKILL.md +12 -0
- package/.cursor/skills/peekaboo-driver/SKILL.md +13 -0
- package/.cursor/skills/persona-panel/SKILL.md +12 -0
- package/.cursor/skills/plan/SKILL.md +13 -0
- package/.cursor/skills/playwright-driver/SKILL.md +13 -0
- package/.cursor/skills/quality-gates/SKILL.md +13 -0
- package/.cursor/skills/reconcile/SKILL.md +12 -0
- package/.cursor/skills/repo-audit/SKILL.md +13 -0
- package/.cursor/skills/session-end/SKILL.md +13 -0
- package/.cursor/skills/session-plan/SKILL.md +13 -0
- package/.cursor/skills/session-start/SKILL.md +13 -0
- package/.cursor/skills/skill-creator/SKILL.md +13 -0
- package/.cursor/skills/spinout/SKILL.md +12 -0
- package/.cursor/skills/sunset-review/SKILL.md +13 -0
- package/.cursor/skills/test-runner/SKILL.md +13 -0
- package/.cursor/skills/tmux-layout/SKILL.md +13 -0
- package/.cursor/skills/ubiquitous-language/SKILL.md +13 -0
- package/.cursor/skills/using-orchestrator/SKILL.md +13 -0
- package/.cursor/skills/vault-mirror/SKILL.md +13 -0
- package/.cursor/skills/vault-sync/SKILL.md +13 -0
- package/.cursor/skills/wave-executor/SKILL.md +13 -0
- package/.cursor/skills/write-executable-plan/SKILL.md +13 -0
- package/.mcp.json +4 -1
- package/CHANGELOG.md +168 -0
- package/README.md +18 -15
- package/agents/AGENTS.md +23 -4
- package/agents/code-implementer.md +2 -1
- package/agents/db-specialist.md +2 -1
- package/agents/docs-writer.md +3 -1
- package/agents/eval-judge.md +1 -1
- package/agents/session-reviewer.md +7 -1
- package/agents/test-writer.md +2 -1
- package/agents/ui-developer.md +2 -1
- package/commands/bootstrap.md +2 -2
- package/commands/close.md +3 -1
- package/commands/go.md +1 -1
- package/commands/journey-audit.md +43 -0
- package/docs/USER-GUIDE.md +2 -2
- package/docs/ci-setup.md +14 -0
- package/docs/codex-setup.md +64 -0
- package/docs/components.md +6 -6
- package/docs/cursor-setup.md +26 -47
- package/docs/events-schema.md +76 -4
- package/docs/github-mirror-protection.md +197 -0
- package/docs/pi-setup.md +2 -0
- package/docs/rule-authoring.md +3 -1
- package/docs/scope-collision-guard.md +49 -2
- package/docs/session-config-reference.md +26 -4
- package/docs/session-config-template.md +4 -3
- package/docs/telemetry.md +22 -0
- package/hooks/_lib/lock-bootstrap.mjs +8 -4
- package/hooks/_lib/vcs-create-matcher.mjs +397 -38
- package/hooks/enforce-scope.mjs +64 -0
- package/hooks/hooks-codex.json +1 -1
- package/hooks/hooks-cursor.json +201 -20
- package/hooks/hooks-pi.json +1 -1
- package/hooks/hooks.json +2 -2
- package/hooks/on-session-end.mjs +211 -10
- package/hooks/on-session-start.mjs +214 -11
- package/hooks/on-stop.mjs +48 -9
- package/hooks/post-subagent-discovery-validator.mjs +34 -3
- package/hooks/post-tool-batch-wave-signal.mjs +11 -2
- package/hooks/pre-bash-issue-budget.mjs +117 -4
- package/hooks/pre-bash-sessions-ledger-guard.mjs +159 -0
- package/hooks/pre-bash-staging-fence.mjs +4 -0
- package/hooks/pre-task-scope-disjoint.mjs +368 -35
- package/hooks/skill-invocation-telemetry.mjs +21 -10
- package/monitors/monitors.json +6 -0
- package/package.json +1 -1
- package/pi/prompts/journey-audit.md +12 -0
- package/rules/_index.md +9 -1
- package/rules/always-on/ask-via-tool.md +62 -0
- package/rules/always-on/bash-harness-pitfalls.md +168 -0
- package/rules/always-on/build-value.md +47 -0
- package/rules/always-on/cross-session-messaging.md +59 -0
- package/rules/always-on/loop-and-monitor.md +221 -0
- package/rules/always-on/parallel-sessions.md +142 -12
- package/rules/always-on/receiving-review.md +108 -0
- package/rules/always-on/test-value.md +40 -0
- package/rules/always-on/verification-before-completion.md +77 -0
- package/scripts/archive-closed-prds.mjs +258 -18
- package/scripts/autopilot.mjs +5 -0
- package/scripts/backfill-evidence-digest.mjs +376 -0
- package/scripts/cursor-install.mjs +89 -48
- package/scripts/export-hw-learnings.mjs +143 -2
- package/scripts/express-path.mjs +299 -0
- package/scripts/generate-cursor-adapter.mjs +253 -0
- package/scripts/github-protection-audit.mjs +358 -0
- package/scripts/lib/autopilot/worktree-pipeline.mjs +240 -16
- package/scripts/lib/build-live-signals.mjs +24 -5
- package/scripts/lib/ci-status-banner.mjs +158 -11
- package/scripts/lib/command-blocker.mjs +70 -0
- package/scripts/lib/config/reconcile.mjs +79 -4
- package/scripts/lib/config/section-extractor.mjs +235 -36
- package/scripts/lib/config-schema.mjs +9 -1
- package/scripts/lib/config.mjs +57 -6
- package/scripts/lib/convergence-monitor.mjs +13 -2
- package/scripts/lib/cursor-hook-bridge.mjs +443 -0
- package/scripts/lib/dispatcher/cli.mjs +2 -2
- package/scripts/lib/express-path.mjs +327 -0
- package/scripts/lib/file-lock.mjs +22 -4
- package/scripts/lib/gates/gate-full.mjs +81 -8
- package/scripts/lib/gates/gate-helpers.mjs +76 -15
- package/scripts/lib/git-config-drift.mjs +134 -5
- package/scripts/lib/host-identity.mjs +247 -2
- package/scripts/lib/instruction-budget-guard.mjs +31 -1
- package/scripts/lib/issue-budget.mjs +229 -30
- package/scripts/lib/learnings/io.mjs +55 -10
- package/scripts/lib/learnings/schema.mjs +95 -28
- package/scripts/lib/lock-reaper.mjs +7 -1
- package/scripts/lib/locks/staging-fence-lock.mjs +5 -1
- package/scripts/lib/locks/state-md-lock.mjs +8 -1
- package/scripts/lib/memory-banner.mjs +5 -2
- package/scripts/lib/memory-paths.mjs +15 -6
- package/scripts/lib/mode-selector/scoring.mjs +53 -6
- package/scripts/lib/platform.mjs +72 -9
- package/scripts/lib/plugin-root.mjs +143 -19
- package/scripts/lib/project-hygiene.mjs +43 -3
- package/scripts/lib/quality-gate.mjs +271 -13
- package/scripts/lib/reconcile/emitter.mjs +87 -19
- package/scripts/lib/reconcile/engine.mjs +281 -13
- package/scripts/lib/reconcile/idempotency.mjs +102 -1
- package/scripts/lib/reconcile/renderer.mjs +148 -3
- package/scripts/lib/reconcile/sanitize.mjs +40 -17
- package/scripts/lib/reconcile/writer.mjs +415 -84
- package/scripts/lib/rule-loader.mjs +37 -2
- package/scripts/lib/rules-sync.mjs +51 -8
- package/scripts/lib/scope-gate.mjs +90 -0
- package/scripts/lib/session-close-backfill.mjs +369 -28
- package/scripts/lib/session-discovery.mjs +13 -3
- package/scripts/lib/session-end/phase-skip.mjs +37 -4
- package/scripts/lib/session-end/worktree-cleanup.mjs +154 -7
- package/scripts/lib/session-id.mjs +30 -14
- package/scripts/lib/session-identity/own-session.mjs +159 -0
- package/scripts/lib/session-lock.mjs +85 -30
- package/scripts/lib/session-schema/normalizer.mjs +70 -3
- package/scripts/lib/session-schema/validator.mjs +40 -0
- package/scripts/lib/session-start-probes.mjs +608 -0
- package/scripts/lib/session-transition.mjs +277 -0
- package/scripts/lib/sessions-staleness-banner.mjs +124 -57
- package/scripts/lib/spiral-carryover.mjs +90 -9
- package/scripts/lib/state-md/frontmatter-mutators.mjs +41 -8
- package/scripts/lib/state-md/mission-status.mjs +350 -52
- package/scripts/lib/state-md/yaml-parser.mjs +145 -16
- package/scripts/lib/state-md.mjs +12 -2
- package/scripts/lib/telemetry/sync.mjs +46 -8
- package/scripts/lib/validate/check-agents.mjs +66 -0
- package/scripts/lib/validate/check-cursor-adapter.mjs +102 -0
- package/scripts/lib/validate/check-dead-bridge.mjs +24 -2
- package/scripts/lib/validate/check-doc-cli-commands.mjs +16 -32
- package/scripts/lib/validate/check-hooks-symmetry.mjs +29 -63
- package/scripts/lib/validate/check-playwright-mcp-canary.mjs +13 -22
- package/scripts/lib/validate/check-plugin-monitors.mjs +10 -4
- package/scripts/lib/validate/check-test-value-bans.mjs +165 -17
- package/scripts/lib/validate/check-unwired-features.mjs +340 -32
- package/scripts/lib/validate/repo-files.mjs +275 -0
- package/scripts/lib/validate-vendored-rules.mjs +229 -7
- package/scripts/lib/vault-mirror/process.mjs +99 -43
- package/scripts/lib/vault-mirror/telemetry.mjs +210 -0
- package/scripts/lib/vault-staleness-banner.mjs +76 -6
- package/scripts/lib/vault-status/board-writer.mjs +211 -10
- package/scripts/lib/vault-status/narrative-mirror.mjs +188 -8
- package/scripts/lib/wave-executor/foreign-dispatch.mjs +832 -0
- package/scripts/lib/wave-transcript-tail.mjs +869 -0
- package/scripts/materialize-wave-scope.mjs +209 -12
- package/scripts/mcp-server.sh +11 -2
- package/scripts/parse-config.mjs +65 -0
- package/scripts/token-audit.sh +9 -2
- package/scripts/validate-plugin.mjs +3 -0
- package/scripts/validate-wave-scope.mjs +67 -0
- package/scripts/vault-mirror.mjs +203 -34
- package/skills/_shared/monitor-patterns.md +31 -5
- package/skills/_shared/parallel-aware-auq.md +1 -1
- package/skills/_shared/parallel-aware-preamble.md +4 -2
- package/skills/_shared/platform-tools.md +11 -5
- package/skills/_shared/state-ownership.md +29 -2
- package/skills/autopilot/SKILL.md +5 -1
- package/skills/bootstrap/SKILL.md +3 -3
- package/skills/bootstrap/_shared-template.md +18 -10
- package/skills/bootstrap/deep-template.md +10 -6
- package/skills/bootstrap/fast-template.md +15 -8
- package/skills/bootstrap/standard-template.md +10 -6
- package/skills/claude-md-drift-check/checker.mjs +39 -11
- package/skills/dispatcher/SKILL.md +1 -1
- package/skills/journey-audit/SKILL.md +269 -0
- package/skills/peekaboo-driver/SKILL.md +15 -3
- package/skills/persona-panel/SKILL.md +1 -1
- package/skills/reconcile/SKILL.md +41 -1
- package/skills/session-end/SKILL.md +17 -4
- package/skills/session-end/metrics-collection.md +7 -4
- package/skills/session-end/phase-3-6-tail.md +11 -3
- package/skills/session-end/phase-3-7a-recommendations.md +16 -2
- package/skills/session-plan/SKILL.md +6 -1
- package/skills/session-plan/wave-template.md +1 -0
- package/skills/session-start/SKILL.md +30 -16
- package/skills/session-start/phase-7-5-mode-selector.md +15 -3
- package/skills/session-start/phase-8-5-express-path.md +77 -12
- package/skills/vault-sync/validator.mjs +31 -0
- package/skills/wave-executor/SKILL.md +4 -2
- package/skills/wave-executor/circuit-breaker.md +34 -9
- package/skills/wave-executor/wave-loop.md +102 -19
- package/templates/_shared/journey-manifest.md +110 -0
- package/templates/_shared/rules/parallel-sessions.md +0 -77
|
@@ -220,6 +220,28 @@ const SCALAR_META_KEYS = new Set([
|
|
|
220
220
|
* @param {string} contents - raw file contents
|
|
221
221
|
* @returns {{ globs: string[] | null, meta: Record<string, unknown> }}
|
|
222
222
|
*/
|
|
223
|
+
/**
|
|
224
|
+
* Undo the ONE quoting style the renderer emits for a scalar (#1041 follow-up).
|
|
225
|
+
*
|
|
226
|
+
* `scripts/lib/reconcile/renderer.mjs` serialises `description:` through
|
|
227
|
+
* js-yaml, which picks SINGLE-quote style whenever the plain form would be
|
|
228
|
+
* ambiguous (a `: ` inside) and escapes an interior `'` as `''`. A bare
|
|
229
|
+
* leading/trailing-quote strip then returns `it''s` for `it's` — measured
|
|
230
|
+
* 2026-08-23 on 40 of 146 live learning insights carrying both characters.
|
|
231
|
+
* Single-quoted: strip + un-double. Double-quoted: strip + unescape `\"` and
|
|
232
|
+
* `\\`. Unquoted: returned verbatim (the pre-#1041 behaviour, unchanged).
|
|
233
|
+
*/
|
|
234
|
+
function unquoteYamlScalar(valuePart) {
|
|
235
|
+
const v = valuePart.trim();
|
|
236
|
+
if (v.length >= 2 && v.startsWith("'") && v.endsWith("'")) {
|
|
237
|
+
return v.slice(1, -1).replace(/''/g, "'");
|
|
238
|
+
}
|
|
239
|
+
if (v.length >= 2 && v.startsWith('"') && v.endsWith('"')) {
|
|
240
|
+
return v.slice(1, -1).replace(/\\(["\\])/g, '$1');
|
|
241
|
+
}
|
|
242
|
+
return v.replace(/^["']|["']$/g, '');
|
|
243
|
+
}
|
|
244
|
+
|
|
223
245
|
export function parseGlobsFrontmatter(contents) {
|
|
224
246
|
const match = FRONTMATTER_RE.exec(stripLeadingProvenanceHeader(contents));
|
|
225
247
|
if (!match) return { globs: null, meta: {} };
|
|
@@ -299,7 +321,7 @@ export function parseGlobsFrontmatter(contents) {
|
|
|
299
321
|
} else if (SCALAR_META_KEYS.has(key)) {
|
|
300
322
|
// Scalar activation key (issue #694). Strip surrounding quotes, then
|
|
301
323
|
// apply per-key coercion. Empty values are skipped (key not present).
|
|
302
|
-
const stripped = valuePart
|
|
324
|
+
const stripped = unquoteYamlScalar(valuePart);
|
|
303
325
|
if (stripped === '') continue;
|
|
304
326
|
if (key === 'alwaysApply' || key === 'auto-generated') {
|
|
305
327
|
if (stripped === 'true') meta[key] = true;
|
|
@@ -468,7 +490,20 @@ export function loadApplicableRules({
|
|
|
468
490
|
try {
|
|
469
491
|
entries = readdirSync(rulesDir);
|
|
470
492
|
} catch (err) {
|
|
471
|
-
|
|
493
|
+
// Category separation, not channel muting (`.claude/rules/development.md`
|
|
494
|
+
// § Guard & Threshold Design). An absent `.claude/rules/` (ENOENT), or a
|
|
495
|
+
// plain file sitting where that directory would be (ENOTDIR), is the
|
|
496
|
+
// NORMAL state of every repo that has not adopted the rules layer — a
|
|
497
|
+
// legitimate empty corpus, not a diagnosable fault. Diagnosing it made
|
|
498
|
+
// every stderr guard around the callers unusable: `instruction-budget-
|
|
499
|
+
// guard.mjs` calls this function three times unconditionally, so ONE probe
|
|
500
|
+
// run emitted the same non-finding three times (#1132).
|
|
501
|
+
// EACCES/EPERM — and anything else — stay genuinely diagnosable and keep
|
|
502
|
+
// the line. Both branches still `return []`, so no caller's control flow
|
|
503
|
+
// changes; only the side effect is gated.
|
|
504
|
+
if (err?.code !== 'ENOENT' && err?.code !== 'ENOTDIR') {
|
|
505
|
+
process.stderr.write(`[rule-loader] Cannot read rulesDir ${rulesDir}: ${err.message}\n`);
|
|
506
|
+
}
|
|
472
507
|
return [];
|
|
473
508
|
}
|
|
474
509
|
|
|
@@ -9,7 +9,13 @@
|
|
|
9
9
|
import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs';
|
|
10
10
|
import { join, basename, dirname, resolve } from 'node:path';
|
|
11
11
|
import { fileURLToPath } from 'node:url';
|
|
12
|
-
import { validateRuleContent } from './validate-vendored-rules.mjs';
|
|
12
|
+
import { validateRuleContent, scanVendoringLeaks } from './validate-vendored-rules.mjs';
|
|
13
|
+
|
|
14
|
+
// The vendoring sanitizer (issue #1098) lives in validate-vendored-rules.mjs —
|
|
15
|
+
// it has that module's shape ("judge one rule file → findings") and its
|
|
16
|
+
// standalone CLI needs it too. Re-exported here so importers that predate the
|
|
17
|
+
// move (and this file's own tests) keep resolving it from rules-sync.mjs.
|
|
18
|
+
export { scanVendoringLeaks };
|
|
13
19
|
|
|
14
20
|
// Exported (issue #722 Epic A Wave 2) for external consumers (e.g. tests).
|
|
15
21
|
// NOT imported by validate-vendored-rules.mjs — that module imports
|
|
@@ -188,6 +194,19 @@ function escapeRegex(s) {
|
|
|
188
194
|
* at least one concrete bullet entry. This keeps `/bootstrap --sync-rules`
|
|
189
195
|
* ready for future opt-in categories without requiring CLI changes.
|
|
190
196
|
*
|
|
197
|
+
* Vendoring sanitizer (issue #1098): every source file that reaches the write
|
|
198
|
+
* decision is additionally scanned by `scanVendoringLeaks()`, and its findings
|
|
199
|
+
* are collected into the additive `sanitizer[]` array. This runs in
|
|
200
|
+
* **report-only mode, always** — it never rewrites content, never skips a
|
|
201
|
+
* write, and never contributes to `errors[]`. That default is deliberate and
|
|
202
|
+
* matches the existing CLI contract: `rules-sync.mjs`'s exit code is driven by
|
|
203
|
+
* `errors[]` alone, so adding `sanitizer[]` extends the JSON envelope without
|
|
204
|
+
* changing any exit code or write decision an existing caller depends on.
|
|
205
|
+
* Silent stripping was rejected outright — it would change a rule's meaning at
|
|
206
|
+
* vendoring time, invisibly to author and consumer alike. There is no opt-out
|
|
207
|
+
* flag because the scan is read-only and free relative to the file read it
|
|
208
|
+
* piggybacks on.
|
|
209
|
+
*
|
|
191
210
|
* @param {{
|
|
192
211
|
* pluginRoot: string,
|
|
193
212
|
* repoRoot: string,
|
|
@@ -202,7 +221,8 @@ function escapeRegex(s) {
|
|
|
202
221
|
* skipped: Array<string|{file: string, reason: string}>,
|
|
203
222
|
* preserved: string[],
|
|
204
223
|
* errors: Array<{file: string, reason: string}>,
|
|
205
|
-
* warnings: Array<{file: string, reason: string}
|
|
224
|
+
* warnings: Array<{file: string, reason: string}>,
|
|
225
|
+
* sanitizer: Array<{file: string, line: number, kind: string, text: string}>
|
|
206
226
|
* }}
|
|
207
227
|
*/
|
|
208
228
|
export function syncRules({
|
|
@@ -219,20 +239,21 @@ export function syncRules({
|
|
|
219
239
|
const preserved = [];
|
|
220
240
|
const errors = [];
|
|
221
241
|
const warnings = [];
|
|
242
|
+
const sanitizer = [];
|
|
222
243
|
|
|
223
244
|
if (!pluginRoot || typeof pluginRoot !== 'string') {
|
|
224
245
|
errors.push({ file: '_index.md', reason: 'pluginRoot not provided' });
|
|
225
|
-
return { written, skipped, preserved, errors, warnings };
|
|
246
|
+
return { written, skipped, preserved, errors, warnings, sanitizer };
|
|
226
247
|
}
|
|
227
248
|
if (!repoRoot || typeof repoRoot !== 'string') {
|
|
228
249
|
errors.push({ file: '_index.md', reason: 'repoRoot not provided' });
|
|
229
|
-
return { written, skipped, preserved, errors, warnings };
|
|
250
|
+
return { written, skipped, preserved, errors, warnings, sanitizer };
|
|
230
251
|
}
|
|
231
252
|
|
|
232
253
|
const indexPath = join(pluginRoot, 'rules', '_index.md');
|
|
233
254
|
if (!existsSync(indexPath)) {
|
|
234
255
|
errors.push({ file: '_index.md', reason: `_index.md not found at ${indexPath}` });
|
|
235
|
-
return { written, skipped, preserved, errors, warnings };
|
|
256
|
+
return { written, skipped, preserved, errors, warnings, sanitizer };
|
|
236
257
|
}
|
|
237
258
|
|
|
238
259
|
let indexContent;
|
|
@@ -240,7 +261,7 @@ export function syncRules({
|
|
|
240
261
|
indexContent = readFileSync(indexPath, 'utf8');
|
|
241
262
|
} catch (err) {
|
|
242
263
|
errors.push({ file: '_index.md', reason: `failed to read _index.md: ${err.message}` });
|
|
243
|
-
return { written, skipped, preserved, errors, warnings };
|
|
264
|
+
return { written, skipped, preserved, errors, warnings, sanitizer };
|
|
244
265
|
}
|
|
245
266
|
|
|
246
267
|
const selectedCategories = Array.isArray(categories)
|
|
@@ -248,6 +269,14 @@ export function syncRules({
|
|
|
248
269
|
: listManifestCategories(indexContent);
|
|
249
270
|
const entries = parseIndex(indexContent, selectedCategories);
|
|
250
271
|
|
|
272
|
+
// Resolvability for the See-Also sanitizer is judged against the FULL
|
|
273
|
+
// manifest, not `selectedCategories`: an archetype-scoped rule is a
|
|
274
|
+
// legitimate citation target in every repo whose archetype matches, so
|
|
275
|
+
// narrowing this to the current selection would report false leaks.
|
|
276
|
+
const manifestBasenames = new Set(
|
|
277
|
+
parseIndex(indexContent, listManifestCategories(indexContent)).map((e) => basename(e.relPath)),
|
|
278
|
+
);
|
|
279
|
+
|
|
251
280
|
if (entries.length === 0) {
|
|
252
281
|
// No sources resolved — could be empty categories or malformed index
|
|
253
282
|
for (const cat of selectedCategories.length > 0 ? selectedCategories : ['<none>']) {
|
|
@@ -256,7 +285,7 @@ export function syncRules({
|
|
|
256
285
|
reason: `no sources resolved for category '${cat}' in _index.md`,
|
|
257
286
|
});
|
|
258
287
|
}
|
|
259
|
-
return { written, skipped, preserved, errors, warnings };
|
|
288
|
+
return { written, skipped, preserved, errors, warnings, sanitizer };
|
|
260
289
|
}
|
|
261
290
|
|
|
262
291
|
const targetDir = join(repoRoot, '.claude', 'rules');
|
|
@@ -304,6 +333,13 @@ export function syncRules({
|
|
|
304
333
|
continue;
|
|
305
334
|
}
|
|
306
335
|
|
|
336
|
+
// Vendoring sanitizer (issue #1098) — report-only, never mutates
|
|
337
|
+
// srcContent. Deliberately placed BEFORE the validation gate so a file
|
|
338
|
+
// that fails validation still reports its leaks in one pass.
|
|
339
|
+
sanitizer.push(
|
|
340
|
+
...scanVendoringLeaks({ content: srcContent, relPath, pluginRoot, manifestBasenames }),
|
|
341
|
+
);
|
|
342
|
+
|
|
307
343
|
// Pre-write validation gate (issue #722 Epic A Wave 2). Runs BEFORE the
|
|
308
344
|
// existsSync(targetPath) write-decision branch below, so a validation
|
|
309
345
|
// failure skips the write regardless of whether the target already
|
|
@@ -376,7 +412,7 @@ export function syncRules({
|
|
|
376
412
|
}
|
|
377
413
|
}
|
|
378
414
|
|
|
379
|
-
return { written, skipped, preserved, errors, warnings };
|
|
415
|
+
return { written, skipped, preserved, errors, warnings, sanitizer };
|
|
380
416
|
}
|
|
381
417
|
|
|
382
418
|
// ── CLI ───────────────────────────────────────────────────────────────────────
|
|
@@ -432,6 +468,13 @@ if (isMain) {
|
|
|
432
468
|
|
|
433
469
|
process.stdout.write(JSON.stringify(result, null, 2) + '\n');
|
|
434
470
|
|
|
471
|
+
// Vendoring sanitizer (issue #1098) — surfaced on stderr so the operator sees
|
|
472
|
+
// it without parsing the JSON envelope. Report-only by construction: it never
|
|
473
|
+
// enters errors[] and never changes the exit code below.
|
|
474
|
+
for (const f of result.sanitizer) {
|
|
475
|
+
process.stderr.write(`rules-sync: sanitizer ${f.kind} ${f.file}:${f.line} — ${f.text}\n`);
|
|
476
|
+
}
|
|
477
|
+
|
|
435
478
|
if (result.errors.length > 0) {
|
|
436
479
|
process.exit(1);
|
|
437
480
|
}
|
|
@@ -992,6 +992,90 @@ function literalScopeSuffix(entry) {
|
|
|
992
992
|
return star === -1 ? null : entry.slice(star + 1);
|
|
993
993
|
}
|
|
994
994
|
|
|
995
|
+
/**
|
|
996
|
+
* Segments of a scope entry, with the implicit recursion of a trailing `/` made
|
|
997
|
+
* EXPLICIT as a `**` segment (`tests/` → `['tests', '**']`).
|
|
998
|
+
*
|
|
999
|
+
* Load-bearing: {@link pathMatchesPattern} matches a `dir/` prefix with
|
|
1000
|
+
* `startsWith`, i.e. at ANY depth. Splitting `tests/` naively yields a trailing
|
|
1001
|
+
* EMPTY segment, which {@link globsDisagreeOnLiteralSegment} would then compare
|
|
1002
|
+
* against `lib` as if the entry ended there — turning `tests/` vs
|
|
1003
|
+
* `tests/lib/*.mjs` into a false "disjoint".
|
|
1004
|
+
*
|
|
1005
|
+
* @param {string} entry
|
|
1006
|
+
* @returns {string[]}
|
|
1007
|
+
*/
|
|
1008
|
+
function scopeEntrySegments(entry) {
|
|
1009
|
+
const segs = entry.split('/');
|
|
1010
|
+
if (segs.length > 1 && segs[segs.length - 1] === '') segs[segs.length - 1] = '**';
|
|
1011
|
+
return segs;
|
|
1012
|
+
}
|
|
1013
|
+
|
|
1014
|
+
/**
|
|
1015
|
+
* Is this segment a PLAIN literal — one that can only match the byte-identical
|
|
1016
|
+
* path segment? Deliberately conservative: `*`, `**`, `?`, `{a,b}` and `[...]`
|
|
1017
|
+
* all count as "could match anything", and an empty segment does too.
|
|
1018
|
+
* @param {string} seg
|
|
1019
|
+
* @returns {boolean}
|
|
1020
|
+
*/
|
|
1021
|
+
function isPlainLiteralSegment(seg) {
|
|
1022
|
+
return seg.length > 0 && !/[*?{}[\]]/.test(seg);
|
|
1023
|
+
}
|
|
1024
|
+
|
|
1025
|
+
/**
|
|
1026
|
+
* Do two glob entries carry PLAIN LITERAL segments that DISAGREE at an aligned
|
|
1027
|
+
* position? If so no path can satisfy both, and the pair is disjoint however
|
|
1028
|
+
* much literal prefix and suffix they share (#1130).
|
|
1029
|
+
*
|
|
1030
|
+
* Alignment is exact — and only within the two scans below. Every segment that
|
|
1031
|
+
* is not `**` consumes EXACTLY ONE path segment (`*` compiles to `[^/]*`, which
|
|
1032
|
+
* never crosses `/`; `?`/`{}`/`[]` are regex-ESCAPED by
|
|
1033
|
+
* {@link pathMatchesPattern}, so they are literal text). `**` is the only
|
|
1034
|
+
* variable-width segment, so each scan walks inward from one end and STOPS at
|
|
1035
|
+
* the first `**` on either side — past that wall, positions no longer
|
|
1036
|
+
* correspond. That is why the #1130 pair is decided from the TAIL:
|
|
1037
|
+
* `scripts/**\/reconcile/*.mjs` vs `scripts/**\/learnings/*.mjs` share the
|
|
1038
|
+
* `scripts` head and the `*.mjs` tail, and `reconcile` vs `learnings` is the
|
|
1039
|
+
* first aligned position inward from the tail.
|
|
1040
|
+
*
|
|
1041
|
+
* CEILING (BV-004) — literal-vs-literal ONLY. A `{a,b}`, `?` or `[...]` segment
|
|
1042
|
+
* is treated as "could match anything" and proves nothing, so
|
|
1043
|
+
* `scripts/**\/{reconcile,x}/*.mjs` vs `scripts/**\/learnings/*.mjs` still
|
|
1044
|
+
* reports a collision even though this dialect escapes braces into literal
|
|
1045
|
+
* text. That over-approximation is the SAFE direction for a collision gate (a
|
|
1046
|
+
* reported non-collision costs a coordinator one re-plan; a missed one is the
|
|
1047
|
+
* #1020 incident). Revisit only if {@link pathMatchesPattern} gains real brace
|
|
1048
|
+
* or char-class expansion — then those segments become enumerable and can be
|
|
1049
|
+
* compared set-wise instead of being waved through.
|
|
1050
|
+
*
|
|
1051
|
+
* @param {string} x
|
|
1052
|
+
* @param {string} y
|
|
1053
|
+
* @returns {boolean} true when the two entries provably cannot match one path
|
|
1054
|
+
*/
|
|
1055
|
+
function globsDisagreeOnLiteralSegment(x, y) {
|
|
1056
|
+
const xs = scopeEntrySegments(x);
|
|
1057
|
+
const ys = scopeEntrySegments(y);
|
|
1058
|
+
const isWall = (seg) => seg.includes('**');
|
|
1059
|
+
const disagree = (a, b) =>
|
|
1060
|
+
isPlainLiteralSegment(a) && isPlainLiteralSegment(b) && a !== b;
|
|
1061
|
+
|
|
1062
|
+
// Scan inward from the HEAD, stopping at the first `**` wall.
|
|
1063
|
+
for (let k = 0; k < xs.length && k < ys.length; k++) {
|
|
1064
|
+
if (isWall(xs[k]) || isWall(ys[k])) break;
|
|
1065
|
+
if (disagree(xs[k], ys[k])) return true;
|
|
1066
|
+
}
|
|
1067
|
+
// Scan inward from the TAIL, same wall rule. Independent of the head scan:
|
|
1068
|
+
// either end can carry the deciding literal, and #1130's does not sit at the
|
|
1069
|
+
// head (both entries begin `scripts/**`).
|
|
1070
|
+
for (let k = 0; k < xs.length && k < ys.length; k++) {
|
|
1071
|
+
const xv = xs[xs.length - 1 - k];
|
|
1072
|
+
const yv = ys[ys.length - 1 - k];
|
|
1073
|
+
if (isWall(xv) || isWall(yv)) break;
|
|
1074
|
+
if (disagree(xv, yv)) return true;
|
|
1075
|
+
}
|
|
1076
|
+
return false;
|
|
1077
|
+
}
|
|
1078
|
+
|
|
995
1079
|
/**
|
|
996
1080
|
* Normalize the `agentScopes` input of {@link findScopeCollisions} into
|
|
997
1081
|
* `{id, declaredId, files}` records. Never throws; malformed members are
|
|
@@ -1072,6 +1156,12 @@ function classifyEntryCollision(x, y, expand) {
|
|
|
1072
1156
|
) {
|
|
1073
1157
|
return null;
|
|
1074
1158
|
}
|
|
1159
|
+
// Prefix + suffix say nothing about the MIDDLE, so `scripts/**\/reconcile/
|
|
1160
|
+
// *.mjs` vs `scripts/**\/learnings/*.mjs` passed every check above and was
|
|
1161
|
+
// reported as a collision (#1130, reproduced 2026-08-24 @ f0766e1). One
|
|
1162
|
+
// aligned pair of DISAGREEING plain literals settles it — see the ceiling
|
|
1163
|
+
// note on the helper for what it deliberately does not decide.
|
|
1164
|
+
if (globsDisagreeOnLiteralSegment(x, y)) return null;
|
|
1075
1165
|
return 'glob-prefix';
|
|
1076
1166
|
}
|
|
1077
1167
|
|