session-orchestrator 3.22.0 → 3.24.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/remote-offload/SKILL.md +13 -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 +446 -0
- package/README.md +22 -17
- package/agents/AGENTS.md +23 -4
- package/agents/code-implementer.md +2 -1
- package/agents/db-specialist.md +2 -2
- 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 +194 -25
- package/docs/codex-setup.md +64 -0
- package/docs/components.md +7 -7
- package/docs/cursor-setup.md +26 -47
- package/docs/events-schema.md +120 -10
- 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 +89 -9
- package/docs/session-config-template.md +38 -7
- package/docs/telemetry/telemetry-claims.md +11 -10
- package/docs/telemetry.md +52 -1
- package/hooks/_lib/atomic-json.mjs +111 -0
- package/hooks/_lib/lock-bootstrap.mjs +8 -4
- package/hooks/_lib/subagent-paths.mjs +143 -0
- package/hooks/_lib/vcs-create-matcher.mjs +397 -38
- package/hooks/cwd-change-restore.mjs +9 -29
- package/hooks/enforce-scope.mjs +93 -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 +486 -19
- package/hooks/on-session-start.mjs +263 -12
- package/hooks/on-stop.mjs +392 -24
- package/hooks/post-bash-write-verify.mjs +104 -4
- package/hooks/post-subagent-discovery-validator.mjs +182 -21
- package/hooks/post-tool-batch-wave-signal.mjs +165 -42
- package/hooks/post-tool-failure-corrective-context.mjs +9 -32
- package/hooks/pre-bash-issue-budget.mjs +117 -4
- package/hooks/pre-bash-memory-propose-audit.mjs +13 -7
- 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/hooks/subagent-telemetry.mjs +11 -26
- 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 +31 -12
- package/scripts/backfill-abandoned-sessions.mjs +80 -11
- package/scripts/backfill-evidence-digest.mjs +376 -0
- package/scripts/cursor-install.mjs +89 -48
- package/scripts/emit-event.mjs +10 -2
- 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/auq/parse.mjs +5 -29
- package/scripts/lib/auto-dialectic.mjs +68 -0
- package/scripts/lib/autopilot/worktree-pipeline.mjs +318 -18
- package/scripts/lib/build-live-signals.mjs +49 -27
- package/scripts/lib/ci-status-banner.mjs +158 -11
- package/scripts/lib/cold-start-detector.mjs +23 -14
- package/scripts/lib/command-blocker.mjs +70 -0
- package/scripts/lib/config/block-header.mjs +55 -0
- package/scripts/lib/config/discovery-validator.mjs +7 -2
- package/scripts/lib/config/health-endpoints.mjs +383 -0
- package/scripts/lib/config/reconcile.mjs +79 -4
- package/scripts/lib/config/remote-hosts.mjs +233 -0
- package/scripts/lib/config/section-extractor.mjs +235 -36
- package/scripts/lib/config-schema.mjs +9 -1
- package/scripts/lib/config.mjs +87 -8
- 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/dispatcher/enumerate.mjs +2 -17
- package/scripts/lib/events-schema.mjs +48 -0
- package/scripts/lib/events.mjs +238 -5
- package/scripts/lib/evolve/autonomy-verdict.mjs +9 -4
- package/scripts/lib/evolve/autopilot-effectiveness.mjs +18 -1
- 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/gitlab-portfolio/cli.mjs +3 -15
- package/scripts/lib/harness-audit/categories/category1.mjs +17 -6
- 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 +25 -10
- package/scripts/lib/memory-paths.mjs +15 -6
- package/scripts/lib/mode-selector/scoring.mjs +53 -6
- package/scripts/lib/peer-discovery.mjs +20 -2
- 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 +517 -18
- 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 +126 -0
- package/scripts/lib/session-close-backfill.mjs +427 -37
- package/scripts/lib/session-discovery.mjs +69 -5
- package/scripts/lib/session-end/phase-skip.mjs +38 -5
- 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 +220 -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-canonical.mjs +446 -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/schema.mjs +74 -8
- package/scripts/lib/telemetry/sync.mjs +91 -16
- package/scripts/lib/tmux-layout/telemetry.mjs +14 -2
- 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 +25 -65
- package/scripts/lib/validate/check-hooks-emit-event-guard.mjs +370 -0
- 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-skill-script-paths.mjs +436 -0
- package/scripts/lib/validate/check-test-value-bans.mjs +165 -17
- package/scripts/lib/validate/check-untracked-test-deps.mjs +10 -0
- package/scripts/lib/validate/check-unwired-features.mjs +333 -32
- package/scripts/lib/validate/check-validator-registration.mjs +248 -0
- package/scripts/lib/validate/check-vcs-repo-flag.mjs +6 -28
- package/scripts/lib/validate/markdown-fences.mjs +196 -0
- 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-lock.mjs +185 -0
- package/scripts/lib/vault-status/board-writer.mjs +381 -141
- package/scripts/lib/vault-status/narrative-mirror.mjs +190 -27
- package/scripts/lib/wave-executor/foreign-dispatch.mjs +832 -0
- package/scripts/lib/wave-executor/remote-dispatch.mjs +504 -0
- package/scripts/lib/wave-resource-gate.mjs +127 -7
- package/scripts/lib/wave-transcript-tail.mjs +889 -0
- package/scripts/materialize-wave-scope.mjs +228 -15
- package/scripts/mcp-server.sh +11 -2
- package/scripts/memory-propose.mjs +132 -8
- package/scripts/parse-config.mjs +65 -0
- package/scripts/promote-vault-strict.mjs +4 -15
- package/scripts/site-numbers.mjs +36 -4
- package/scripts/token-audit.sh +9 -2
- package/scripts/validate-plugin.mjs +29 -0
- package/scripts/validate-wave-scope.mjs +67 -0
- package/scripts/vault-consolidate.mjs +3 -11
- package/scripts/vault-integration-watcher.mjs +2 -4
- package/scripts/vault-mirror.mjs +305 -51
- package/skills/_shared/monitor-patterns.md +31 -5
- package/skills/_shared/parallel-aware-auq.md +31 -2
- package/skills/_shared/parallel-aware-preamble.md +19 -4
- 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/contract-version-bump/SKILL.md +1 -1
- package/skills/dispatcher/SKILL.md +1 -1
- package/skills/ecosystem-health/SKILL.md +4 -1
- package/skills/ecosystem-health/wizard.md +5 -0
- package/skills/evolve/SKILL.md +38 -1
- package/skills/journey-audit/SKILL.md +270 -0
- package/skills/peekaboo-driver/SKILL.md +15 -3
- package/skills/persona-panel/SKILL.md +1 -1
- package/skills/reconcile/SKILL.md +46 -3
- package/skills/remote-offload/SKILL.md +89 -0
- 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 +20 -9
- 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 +54 -17
- 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 +5 -3
- package/skills/wave-executor/circuit-breaker.md +34 -9
- package/skills/wave-executor/wave-loop.md +143 -22
- 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
|
}
|
|
@@ -918,6 +918,28 @@ export function suggestForScopeViolation(relPath, allowedCsv) {
|
|
|
918
918
|
);
|
|
919
919
|
}
|
|
920
920
|
|
|
921
|
+
/**
|
|
922
|
+
* Prefix that marks an aggregate-sidecar record as a PEER SESSION's declared
|
|
923
|
+
* scope rather than one of this wave's own agents (#1195).
|
|
924
|
+
*
|
|
925
|
+
* SSOT: this constant lives here — in the module both the `--union` helper and
|
|
926
|
+
* `hooks/post-bash-write-verify.mjs` already import — so the union exclusion
|
|
927
|
+
* below and the hook's peer-write notice can never disagree about what a peer
|
|
928
|
+
* record IS.
|
|
929
|
+
*/
|
|
930
|
+
export const PEER_RECORD_PREFIX = 'peer-session-';
|
|
931
|
+
|
|
932
|
+
/**
|
|
933
|
+
* Is this record id a PEER SESSION's record (see {@link PEER_RECORD_PREFIX})?
|
|
934
|
+
* Fail-closed: a non-string id is not a peer record.
|
|
935
|
+
*
|
|
936
|
+
* @param {unknown} id
|
|
937
|
+
* @returns {boolean}
|
|
938
|
+
*/
|
|
939
|
+
export function isPeerRecordId(id) {
|
|
940
|
+
return typeof id === 'string' && id.startsWith(PEER_RECORD_PREFIX);
|
|
941
|
+
}
|
|
942
|
+
|
|
921
943
|
/**
|
|
922
944
|
* Merge many agents' declared file scopes into ONE deduplicated, order-stable
|
|
923
945
|
* list — the mechanical form of "allowedPaths is the UNION of all agent file
|
|
@@ -943,6 +965,19 @@ export function suggestForScopeViolation(relPath, allowedCsv) {
|
|
|
943
965
|
* non-array / non-object members and non-string, empty entries are skipped.
|
|
944
966
|
* Pure, sync, no I/O — hook-safe per the module header.
|
|
945
967
|
*
|
|
968
|
+
* PEER RECORDS ARE EXCLUDED (#1195 follow-through). A record whose id starts
|
|
969
|
+
* with `peer-session-` declares a territory NO agent of this wave may write —
|
|
970
|
+
* it exists so a peer's paths take part in the DISJOINTNESS check and so
|
|
971
|
+
* `hooks/post-bash-write-verify.mjs` can name a peer write instead of alarming
|
|
972
|
+
* about it. Unioning it into `allowedPaths` would do the exact inverse: Gate 7
|
|
973
|
+
* (`hooks/enforce-scope.mjs`) would GRANT every agent of the wave write access
|
|
974
|
+
* to the peer's files, and the hook's peer branch would become dead code (a
|
|
975
|
+
* peer path can only reach it while it is OUTSIDE `allowedPaths`). The
|
|
976
|
+
* exclusion lives HERE — the one helper `--union` runs — rather than in the
|
|
977
|
+
* CLI, so every consumer of the union inherits it.
|
|
978
|
+
* `findScopeCollisions` deliberately does NOT filter: a peer/agent path
|
|
979
|
+
* collision is a real collision and must surface.
|
|
980
|
+
*
|
|
946
981
|
* @param {Array<string[]|{id?: string, files?: string[]}>} scopes
|
|
947
982
|
* @returns {string[]} deduplicated union in first-seen order
|
|
948
983
|
*/
|
|
@@ -954,6 +989,7 @@ export function unionFileScopes(scopes) {
|
|
|
954
989
|
let files = null;
|
|
955
990
|
if (Array.isArray(scope)) files = scope;
|
|
956
991
|
else if (scope !== null && typeof scope === 'object' && Array.isArray(scope.files)) {
|
|
992
|
+
if (isPeerRecordId(scope.id)) continue;
|
|
957
993
|
files = scope.files;
|
|
958
994
|
}
|
|
959
995
|
if (files === null) continue;
|
|
@@ -992,6 +1028,90 @@ function literalScopeSuffix(entry) {
|
|
|
992
1028
|
return star === -1 ? null : entry.slice(star + 1);
|
|
993
1029
|
}
|
|
994
1030
|
|
|
1031
|
+
/**
|
|
1032
|
+
* Segments of a scope entry, with the implicit recursion of a trailing `/` made
|
|
1033
|
+
* EXPLICIT as a `**` segment (`tests/` → `['tests', '**']`).
|
|
1034
|
+
*
|
|
1035
|
+
* Load-bearing: {@link pathMatchesPattern} matches a `dir/` prefix with
|
|
1036
|
+
* `startsWith`, i.e. at ANY depth. Splitting `tests/` naively yields a trailing
|
|
1037
|
+
* EMPTY segment, which {@link globsDisagreeOnLiteralSegment} would then compare
|
|
1038
|
+
* against `lib` as if the entry ended there — turning `tests/` vs
|
|
1039
|
+
* `tests/lib/*.mjs` into a false "disjoint".
|
|
1040
|
+
*
|
|
1041
|
+
* @param {string} entry
|
|
1042
|
+
* @returns {string[]}
|
|
1043
|
+
*/
|
|
1044
|
+
function scopeEntrySegments(entry) {
|
|
1045
|
+
const segs = entry.split('/');
|
|
1046
|
+
if (segs.length > 1 && segs[segs.length - 1] === '') segs[segs.length - 1] = '**';
|
|
1047
|
+
return segs;
|
|
1048
|
+
}
|
|
1049
|
+
|
|
1050
|
+
/**
|
|
1051
|
+
* Is this segment a PLAIN literal — one that can only match the byte-identical
|
|
1052
|
+
* path segment? Deliberately conservative: `*`, `**`, `?`, `{a,b}` and `[...]`
|
|
1053
|
+
* all count as "could match anything", and an empty segment does too.
|
|
1054
|
+
* @param {string} seg
|
|
1055
|
+
* @returns {boolean}
|
|
1056
|
+
*/
|
|
1057
|
+
function isPlainLiteralSegment(seg) {
|
|
1058
|
+
return seg.length > 0 && !/[*?{}[\]]/.test(seg);
|
|
1059
|
+
}
|
|
1060
|
+
|
|
1061
|
+
/**
|
|
1062
|
+
* Do two glob entries carry PLAIN LITERAL segments that DISAGREE at an aligned
|
|
1063
|
+
* position? If so no path can satisfy both, and the pair is disjoint however
|
|
1064
|
+
* much literal prefix and suffix they share (#1130).
|
|
1065
|
+
*
|
|
1066
|
+
* Alignment is exact — and only within the two scans below. Every segment that
|
|
1067
|
+
* is not `**` consumes EXACTLY ONE path segment (`*` compiles to `[^/]*`, which
|
|
1068
|
+
* never crosses `/`; `?`/`{}`/`[]` are regex-ESCAPED by
|
|
1069
|
+
* {@link pathMatchesPattern}, so they are literal text). `**` is the only
|
|
1070
|
+
* variable-width segment, so each scan walks inward from one end and STOPS at
|
|
1071
|
+
* the first `**` on either side — past that wall, positions no longer
|
|
1072
|
+
* correspond. That is why the #1130 pair is decided from the TAIL:
|
|
1073
|
+
* `scripts/**\/reconcile/*.mjs` vs `scripts/**\/learnings/*.mjs` share the
|
|
1074
|
+
* `scripts` head and the `*.mjs` tail, and `reconcile` vs `learnings` is the
|
|
1075
|
+
* first aligned position inward from the tail.
|
|
1076
|
+
*
|
|
1077
|
+
* CEILING (BV-004) — literal-vs-literal ONLY. A `{a,b}`, `?` or `[...]` segment
|
|
1078
|
+
* is treated as "could match anything" and proves nothing, so
|
|
1079
|
+
* `scripts/**\/{reconcile,x}/*.mjs` vs `scripts/**\/learnings/*.mjs` still
|
|
1080
|
+
* reports a collision even though this dialect escapes braces into literal
|
|
1081
|
+
* text. That over-approximation is the SAFE direction for a collision gate (a
|
|
1082
|
+
* reported non-collision costs a coordinator one re-plan; a missed one is the
|
|
1083
|
+
* #1020 incident). Revisit only if {@link pathMatchesPattern} gains real brace
|
|
1084
|
+
* or char-class expansion — then those segments become enumerable and can be
|
|
1085
|
+
* compared set-wise instead of being waved through.
|
|
1086
|
+
*
|
|
1087
|
+
* @param {string} x
|
|
1088
|
+
* @param {string} y
|
|
1089
|
+
* @returns {boolean} true when the two entries provably cannot match one path
|
|
1090
|
+
*/
|
|
1091
|
+
function globsDisagreeOnLiteralSegment(x, y) {
|
|
1092
|
+
const xs = scopeEntrySegments(x);
|
|
1093
|
+
const ys = scopeEntrySegments(y);
|
|
1094
|
+
const isWall = (seg) => seg.includes('**');
|
|
1095
|
+
const disagree = (a, b) =>
|
|
1096
|
+
isPlainLiteralSegment(a) && isPlainLiteralSegment(b) && a !== b;
|
|
1097
|
+
|
|
1098
|
+
// Scan inward from the HEAD, stopping at the first `**` wall.
|
|
1099
|
+
for (let k = 0; k < xs.length && k < ys.length; k++) {
|
|
1100
|
+
if (isWall(xs[k]) || isWall(ys[k])) break;
|
|
1101
|
+
if (disagree(xs[k], ys[k])) return true;
|
|
1102
|
+
}
|
|
1103
|
+
// Scan inward from the TAIL, same wall rule. Independent of the head scan:
|
|
1104
|
+
// either end can carry the deciding literal, and #1130's does not sit at the
|
|
1105
|
+
// head (both entries begin `scripts/**`).
|
|
1106
|
+
for (let k = 0; k < xs.length && k < ys.length; k++) {
|
|
1107
|
+
const xv = xs[xs.length - 1 - k];
|
|
1108
|
+
const yv = ys[ys.length - 1 - k];
|
|
1109
|
+
if (isWall(xv) || isWall(yv)) break;
|
|
1110
|
+
if (disagree(xv, yv)) return true;
|
|
1111
|
+
}
|
|
1112
|
+
return false;
|
|
1113
|
+
}
|
|
1114
|
+
|
|
995
1115
|
/**
|
|
996
1116
|
* Normalize the `agentScopes` input of {@link findScopeCollisions} into
|
|
997
1117
|
* `{id, declaredId, files}` records. Never throws; malformed members are
|
|
@@ -1072,6 +1192,12 @@ function classifyEntryCollision(x, y, expand) {
|
|
|
1072
1192
|
) {
|
|
1073
1193
|
return null;
|
|
1074
1194
|
}
|
|
1195
|
+
// Prefix + suffix say nothing about the MIDDLE, so `scripts/**\/reconcile/
|
|
1196
|
+
// *.mjs` vs `scripts/**\/learnings/*.mjs` passed every check above and was
|
|
1197
|
+
// reported as a collision (#1130, reproduced 2026-08-24 @ f0766e1). One
|
|
1198
|
+
// aligned pair of DISAGREEING plain literals settles it — see the ceiling
|
|
1199
|
+
// note on the helper for what it deliberately does not decide.
|
|
1200
|
+
if (globsDisagreeOnLiteralSegment(x, y)) return null;
|
|
1075
1201
|
return 'glob-prefix';
|
|
1076
1202
|
}
|
|
1077
1203
|
|