session-orchestrator 3.20.0 → 3.22.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/rules/000-session-orchestrator.mdc +3 -2
- package/.cursor/rules/030-wave-execution.mdc +10 -8
- package/.cursor/rules/040-discovery.mdc +6 -6
- package/.cursor/rules/050-plan.mdc +8 -8
- package/CHANGELOG.md +515 -0
- package/README.md +16 -11
- package/agents/analyst.md +1 -1
- package/agents/architect-reviewer.md +1 -1
- package/agents/code-implementer.md +4 -2
- package/agents/db-specialist.md +1 -1
- package/agents/dialectic-deriver.md +1 -1
- package/agents/docs-writer.md +1 -1
- package/agents/memory-proposal-collector.md +7 -5
- package/agents/qa-strategist.md +1 -1
- package/agents/security-reviewer.md +1 -1
- package/agents/session-reviewer.md +42 -1
- package/agents/skill-applied-judge.md +1 -1
- package/agents/test-writer.md +1 -1
- package/agents/ui-developer.md +1 -1
- package/agents/ux-evaluator.md +1 -1
- package/commands/eli5.md +33 -0
- package/commands/release.md +62 -0
- package/commands/test.md +2 -2
- package/docs/components.md +6 -5
- package/docs/migration-v3.md +9 -6
- package/docs/persona-panel.md +3 -1
- package/docs/scope-collision-guard.md +167 -0
- package/docs/session-config-reference.md +31 -8
- package/hooks/_lib/lock-bootstrap.mjs +19 -13
- package/hooks/enforce-scope.mjs +103 -3
- package/hooks/hooks-codex.json +1 -1
- package/hooks/hooks.json +21 -1
- package/hooks/on-session-end.mjs +76 -97
- package/hooks/on-session-start.mjs +195 -104
- package/hooks/on-stop.mjs +127 -12
- package/hooks/post-bash-write-verify.mjs +8 -32
- package/hooks/pre-auq-clarity.mjs +787 -0
- package/hooks/pre-bash-issue-budget.mjs +17 -18
- package/hooks/pre-task-scope-disjoint.mjs +1042 -0
- package/package.json +3 -1
- package/pi/prompts/eli5.md +12 -0
- package/pi/prompts/release.md +12 -0
- package/scripts/auq-audit.mjs +825 -0
- package/scripts/autopilot.mjs +10 -9
- package/scripts/emit-session.mjs +42 -0
- package/scripts/export-hw-learnings.mjs +61 -2
- package/scripts/lib/auq/clarity.mjs +1314 -0
- package/scripts/lib/auq/parse.mjs +1006 -0
- package/scripts/lib/auq/schema.mjs +1457 -0
- package/scripts/lib/autopilot/worktree-pipeline.mjs +5 -5
- package/scripts/lib/backlog-scan.mjs +106 -15
- package/scripts/lib/build-live-signals.mjs +7 -3
- package/scripts/lib/ci-status-banner.mjs +267 -77
- package/scripts/lib/config/dispatcher-autonomy-capture.mjs +32 -9
- package/scripts/lib/config/vault-integration.mjs +12 -1
- package/scripts/lib/dispatcher/rank.mjs +4 -7
- package/scripts/lib/gates/gate-full.mjs +3 -3
- package/scripts/lib/gates/gate-helpers.mjs +17 -6
- package/scripts/lib/git-config-drift.mjs +471 -0
- package/scripts/lib/harness-audit/categories/category6.mjs +65 -12
- package/scripts/lib/io.mjs +432 -7
- package/scripts/lib/issue-budget.mjs +63 -9
- package/scripts/lib/learnings/select.mjs +157 -3
- package/scripts/lib/memory-cleanup-stamp.mjs +132 -8
- package/scripts/lib/mirror-issues-banner.mjs +266 -0
- package/scripts/lib/named-vault-resolver.mjs +105 -16
- package/scripts/lib/owner-interview.mjs +78 -32
- package/scripts/lib/peer-cards/schema.mjs +6 -2
- package/scripts/lib/peer-discovery.mjs +73 -22
- package/scripts/lib/project-hygiene.mjs +64 -4
- package/scripts/lib/reconcile/renderer.mjs +17 -4
- package/scripts/lib/reconcile/writer.mjs +69 -30
- package/scripts/lib/redact-spans.mjs +89 -0
- package/scripts/lib/resource-probe/evaluate.mjs +330 -149
- package/scripts/lib/resource-probe/probe-platform.mjs +35 -0
- package/scripts/lib/resource-probe.mjs +18 -2
- package/scripts/lib/scope-baseline.mjs +77 -17
- package/scripts/lib/scope-gate.mjs +658 -0
- package/scripts/lib/secret-masker.mjs +262 -0
- package/scripts/lib/session-lock.mjs +34 -10
- package/scripts/lib/session-registry.mjs +9 -1
- package/scripts/lib/spiral-carryover.mjs +23 -2
- package/scripts/lib/state-md/mission-status.mjs +164 -58
- package/scripts/lib/tmux-layout/vcs-detector.mjs +108 -4
- package/scripts/lib/validate/check-agents.mjs +77 -5
- package/scripts/lib/validate/check-auq-clarity.mjs +274 -0
- package/scripts/lib/validate/check-commands.mjs +2 -20
- package/scripts/lib/validate/check-doc-cli-commands.mjs +514 -0
- package/scripts/lib/validate/check-hooks-symmetry.mjs +48 -0
- package/scripts/lib/validate/check-owner-leakage.mjs +185 -17
- package/scripts/lib/validate/check-rules.mjs +153 -9
- package/scripts/lib/validate/check-skills.mjs +191 -0
- package/scripts/lib/validate/check-test-git-config-target.mjs +665 -0
- package/scripts/lib/validate/check-unicode-safety.mjs +22 -2
- package/scripts/lib/validate/check-untracked-test-deps.mjs +925 -0
- package/scripts/lib/validate/check-unwired-features.mjs +219 -11
- package/scripts/lib/validate/check-vcs-repo-flag.mjs +965 -0
- package/scripts/lib/validate/frontmatter-block.mjs +61 -0
- package/scripts/lib/validate/tier-inference.mjs +46 -8
- package/scripts/lib/vault-backfill/glab.mjs +91 -58
- package/scripts/lib/vault-backfill/manifest.mjs +28 -8
- package/scripts/lib/vault-mirror/namespace.mjs +146 -1
- package/scripts/lib/vault-mirror/process.mjs +264 -31
- package/scripts/lib/vault-mirror/render-sessions.mjs +115 -4
- package/scripts/lib/vault-status/board-writer.mjs +300 -56
- package/scripts/lib/vault-status/narrative-mirror.mjs +119 -5
- package/scripts/lib/vcs-repo-spec.mjs +680 -30
- package/scripts/lib/wave-resource-gate.mjs +67 -73
- package/scripts/materialize-wave-scope.mjs +281 -0
- package/scripts/print-learnings-index.mjs +30 -3
- package/scripts/release.mjs +983 -107
- package/scripts/run-quality-gate.mjs +14 -0
- package/scripts/site-numbers.mjs +1049 -0
- package/scripts/validate-plugin.mjs +64 -0
- package/scripts/validate-wave-scope.mjs +286 -12
- package/scripts/vault-backfill.mjs +32 -5
- package/scripts/vault-mirror.mjs +26 -1
- package/skills/_shared/monitor-patterns.md +24 -4
- package/skills/_shared/parallel-aware-auq.md +30 -24
- package/skills/_shared/parallel-aware-preamble.md +31 -2
- package/skills/_shared/state-ownership.md +49 -6
- package/skills/bootstrap/SKILL.md +2 -1
- package/skills/brainstorm/SKILL.md +18 -18
- package/skills/brainstorm/soul.md +12 -0
- package/skills/claude-md-drift-check/SKILL.md +9 -1
- package/skills/debug/SKILL.md +4 -1
- package/skills/discovery/SKILL.md +28 -24
- package/skills/discovery/issue-templates.md +4 -4
- package/skills/discovery/probes-code.md +2 -2
- package/skills/discovery/probes-feature.md +6 -6
- package/skills/discovery/probes-infra.md +2 -2
- package/skills/discovery/probes-session.md +5 -5
- package/skills/dispatcher/SKILL.md +10 -1
- package/skills/eli5/SKILL.md +43 -0
- package/skills/evolve/SKILL.md +8 -9
- package/skills/frontmatter-guard/SKILL.md +9 -1
- package/skills/gitlab-ops/SKILL.md +73 -59
- package/skills/gitlab-portfolio/SKILL.md +10 -1
- package/skills/grill/SKILL.md +6 -6
- package/skills/grill/soul.md +16 -0
- package/skills/memory-cleanup/SKILL.md +20 -7
- package/skills/npm-publish/SKILL.md +23 -51
- package/skills/peekaboo-driver/SKILL.md +3 -3
- package/skills/persona-panel/SKILL.md +3 -1
- package/skills/plan/SKILL.md +18 -16
- package/skills/plan/mode-feature.md +1 -1
- package/skills/plan/mode-new.md +42 -12
- package/skills/plan/soul.md +12 -0
- package/skills/reconcile/SKILL.md +3 -3
- package/skills/repo-audit/SKILL.md +10 -1
- package/skills/session-end/SKILL.md +97 -22
- package/skills/session-end/metrics-collection.md +1 -1
- package/skills/session-end/phase-3-6-tail.md +37 -2
- package/skills/session-end/session-metrics-write.md +4 -10
- package/skills/session-plan/SKILL.md +2 -2
- package/skills/session-plan/wave-template.md +1 -1
- package/skills/session-start/SKILL.md +82 -36
- package/skills/session-start/phase-2-5-docs-planning.md +8 -8
- package/skills/session-start/phase-4-5-resource-health.md +82 -19
- package/skills/session-start/soul.md +110 -0
- package/skills/spinout/SKILL.md +5 -1
- package/skills/sunset-review/SKILL.md +11 -1
- package/skills/test-runner/SKILL.md +2 -2
- package/skills/tmux-layout/SKILL.md +7 -2
- package/skills/using-orchestrator/SKILL.md +1 -1
- package/skills/vault-mirror/SKILL.md +10 -1
- package/skills/vault-sync/SKILL.md +10 -1
- package/skills/vault-sync/validator.mjs +55 -6
- package/skills/wave-executor/wave-loop.md +64 -12
- package/skills/write-executable-plan/SKILL.md +6 -6
- package/scripts/lib/mission-status-schema.mjs +0 -114
- package/scripts/tests/fixtures/fetch-baseline/sample-rule.md +0 -8
- package/skills/vault-sync/tests/fixtures/archive-test-vault/90-archive/bad-archived.md +0 -8
- package/skills/vault-sync/tests/fixtures/archive-test-vault/_meta/.gitkeep +0 -0
- package/skills/vault-sync/tests/fixtures/archive-test-vault/live-note.md +0 -8
- package/skills/vault-sync/tests/fixtures/broken-frontmatter-vault/_meta/.gitkeep +0 -0
- package/skills/vault-sync/tests/fixtures/broken-frontmatter-vault/bad-type.md +0 -8
- package/skills/vault-sync/tests/fixtures/broken-frontmatter-vault/good-note.md +0 -8
- package/skills/vault-sync/tests/fixtures/clean-vault/.obsidian/config.md +0 -8
- package/skills/vault-sync/tests/fixtures/clean-vault/01-projects/foo/projects-baseline.md +0 -10
- package/skills/vault-sync/tests/fixtures/clean-vault/03-daily/daily-2026-04-13.md +0 -8
- package/skills/vault-sync/tests/fixtures/clean-vault/README.md +0 -3
- package/skills/vault-sync/tests/fixtures/clean-vault/hello-world.md +0 -11
- package/skills/vault-sync/tests/fixtures/dangling-link-vault/_meta/.gitkeep +0 -0
- package/skills/vault-sync/tests/fixtures/dangling-link-vault/has-dangling.md +0 -9
- package/skills/vault-sync/tests/fixtures/dangling-link-vault/real-target.md +0 -8
- package/skills/vault-sync/tests/fixtures/empty-vault/_meta/.gitkeep +0 -0
- package/skills/vault-sync/tests/fixtures/missing-field-vault/_meta/.gitkeep +0 -0
- package/skills/vault-sync/tests/fixtures/missing-field-vault/missing-id.md +0 -7
- package/skills/vault-sync/tests/fixtures/nested-tag-vault/03-daily/daily-2026-04-13.md +0 -9
- package/skills/vault-sync/tests/fixtures/nested-tag-vault/_meta/.gitkeep +0 -0
- package/skills/vault-sync/tests/fixtures/nested-tag-vault/nested-tags-note.md +0 -11
- package/skills/vault-sync/tests/fixtures/no-frontmatter-vault/README.md +0 -3
- package/skills/vault-sync/tests/fixtures/no-frontmatter-vault/_MOC.md +0 -3
- package/skills/vault-sync/tests/fixtures/no-frontmatter-vault/_meta/.gitkeep +0 -0
- package/skills/vault-sync/tests/fixtures/with-moc-vault/_MOC.md +0 -11
- package/skills/vault-sync/tests/fixtures/with-moc-vault/_meta/.gitkeep +0 -0
- package/skills/vault-sync/tests/fixtures/with-moc-vault/hello-world.md +0 -11
- package/skills/vault-sync/tests/schema-drift.test.mjs +0 -133
|
@@ -262,10 +262,23 @@ export function renderRule(learning, metadata) {
|
|
|
262
262
|
// purely by `host-class:` (loads only on a matching host) — the intended
|
|
263
263
|
// behavior, and still never always-on (host-class IS an activation axis).
|
|
264
264
|
if (globs.length > 0) {
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
265
|
+
// TWO keys, same list, on purpose (#1108). `globs:` is what THIS repo's
|
|
266
|
+
// `rule-loader.mjs` and Cursor read; `paths:` is the ONLY key Claude Code's
|
|
267
|
+
// own loader honours, and a rule without it is loaded UNCONDITIONALLY into
|
|
268
|
+
// every agent (https://code.claude.com/docs/en/memory § Path-specific
|
|
269
|
+
// rules). Emitting `globs:` alone therefore produced a rule that LOOKS
|
|
270
|
+
// scoped, IS scoped for us, and is always-on for the harness — measured
|
|
271
|
+
// 2026-08-22: 16 such rules put 72_195 bytes (~18_300 tokens) into every
|
|
272
|
+
// single dispatch. `check-rules.mjs` now hard-FAILs that shape, so a
|
|
273
|
+
// globs-only rule emitted here would be rejected by the gate the moment it
|
|
274
|
+
// lands. Keep the two lists IDENTICAL: divergence is its own FAIL class,
|
|
275
|
+
// because it would scope Claude Code and Cursor to different rule sets.
|
|
276
|
+
for (const key of ['globs', 'paths']) {
|
|
277
|
+
fm.push(`${key}:`);
|
|
278
|
+
for (const glob of globs) {
|
|
279
|
+
// Always double-quote — keeps `*`/`[`/`{` safe; loader strips quotes.
|
|
280
|
+
fm.push(` - "${glob}"`);
|
|
281
|
+
}
|
|
269
282
|
}
|
|
270
283
|
}
|
|
271
284
|
if (hostClass !== undefined) {
|
|
@@ -123,12 +123,37 @@ function writeTextAtomic(destPath, content) {
|
|
|
123
123
|
* `globs=null, meta={}, parseError=true` → always-on again, and with empty
|
|
124
124
|
* meta it passes every gate by design.
|
|
125
125
|
*
|
|
126
|
-
* Scope
|
|
127
|
-
*
|
|
128
|
-
*
|
|
129
|
-
*
|
|
130
|
-
*
|
|
131
|
-
*
|
|
126
|
+
* Scope (#1018 L2) — three tiers, deliberately not one:
|
|
127
|
+
*
|
|
128
|
+
* 1. PARSE — every document. Unparseable means unsafe, not unknown.
|
|
129
|
+
* 2. `alwaysApply: true` — every document, regardless of any marker. A rule
|
|
130
|
+
* file that DECLARES itself always-on is precisely the outcome this gate
|
|
131
|
+
* exists to prevent, and declaring it must not be the way around the gate.
|
|
132
|
+
* Before #1018 this check sat behind the `auto-generated: true` branch, so
|
|
133
|
+
* a document with `alwaysApply: true` and no `auto-generated` key was
|
|
134
|
+
* written to disk unexamined — measured, not inferred: a `writeApprovedRules`
|
|
135
|
+
* probe returned `written: 1` for exactly that input.
|
|
136
|
+
* 3. The never-always-on invariant set (activation axis, non-empty globs,
|
|
137
|
+
* `learning-key`, `expires-at`) — every document carrying ANY machine-
|
|
138
|
+
* provenance marker (`auto-generated: true`, `learning-key`, or
|
|
139
|
+
* `expires-at`). Keying on the marker SET rather than on `auto-generated`
|
|
140
|
+
* alone means a document that loses its `auto-generated` line to truncation
|
|
141
|
+
* but keeps its provenance keys is still held to the invariant.
|
|
142
|
+
*
|
|
143
|
+
* Why tier 3 is marker-scoped and not universal — measured against the live
|
|
144
|
+
* corpus, not assumed: applying the invariant set to EVERY document would
|
|
145
|
+
* refuse 16 of the 29 files currently in `.claude/rules/`. Those 16 are the
|
|
146
|
+
* hand-authored always-on rules (`development.md`, `security.md`,
|
|
147
|
+
* `parallel-sessions.md`, …), for which always-on is the intended, correct
|
|
148
|
+
* shape — they carry no frontmatter at all. A universal gate would therefore
|
|
149
|
+
* refuse the legitimate majority of the corpus to catch a machine-path defect.
|
|
150
|
+
* Marker-scoped refuses 0 of 29. The module's general contract survives: a
|
|
151
|
+
* caller may still write a document with no frontmatter.
|
|
152
|
+
*
|
|
153
|
+
* CEILING (BV-004): the marker set is a fixed three-key list, not a schema
|
|
154
|
+
* lookup. Revisit if the renderer gains a fourth provenance key, or if a second
|
|
155
|
+
* non-reconcile caller of `writeApprovedRules` appears — today there is exactly
|
|
156
|
+
* one production caller and it passes renderer output.
|
|
132
157
|
*
|
|
133
158
|
* Mirrors `scripts/lib/validate/check-rules.mjs`, which enforces the same
|
|
134
159
|
* invariants as a CI gate. Two enforcement points, one invariant: CI catches
|
|
@@ -147,37 +172,51 @@ function frontmatterRefusalReason(content) {
|
|
|
147
172
|
}
|
|
148
173
|
|
|
149
174
|
const { globs, meta } = parsed;
|
|
150
|
-
|
|
151
|
-
// Not a machine-authored auto-generated rule → the never-always-on invariant
|
|
152
|
-
// does not bind (see the scope note above).
|
|
153
|
-
if (meta['auto-generated'] !== true) return null;
|
|
175
|
+
const hasKey = (key) => Object.prototype.hasOwnProperty.call(meta, key);
|
|
154
176
|
|
|
155
177
|
const problems = [];
|
|
156
178
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
if (hasEmptyGlobs) {
|
|
162
|
-
// NOT the "no axis" case and NOT always-on — the opposite: rule-loader.mjs
|
|
163
|
-
// excludes on `globs.length === 0` unconditionally, AFTER gating, so the
|
|
164
|
-
// rule never loads in ANY context even alongside a host-class: key.
|
|
165
|
-
problems.push('empty globs array (globs: []) — the rule would match nothing and never load in ANY context');
|
|
166
|
-
} else if (!hasGlobs && !hasHostClass) {
|
|
167
|
-
problems.push('no activation axis (globs absent AND host-class absent) — the rule would load always-on');
|
|
168
|
-
}
|
|
169
|
-
if (!Object.prototype.hasOwnProperty.call(meta, 'learning-key')) {
|
|
170
|
-
problems.push('missing required frontmatter key: learning-key');
|
|
171
|
-
}
|
|
172
|
-
if (!Object.prototype.hasOwnProperty.call(meta, 'expires-at')) {
|
|
173
|
-
problems.push('missing required frontmatter key: expires-at');
|
|
174
|
-
}
|
|
179
|
+
// ── Tier 2: binds on EVERY document (see the scope note above) ────────────
|
|
180
|
+
// Not gated behind any marker: a document that declares itself always-on is
|
|
181
|
+
// the exact outcome this gate prevents, so the declaration cannot be the
|
|
182
|
+
// escape hatch. The renderer only ever emits `alwaysApply: false`.
|
|
175
183
|
if (meta.alwaysApply === true) {
|
|
176
|
-
problems.push(
|
|
184
|
+
problems.push(
|
|
185
|
+
'alwaysApply: true — a rule written through this writer must never declare itself always-on (the renderer only ever emits false)',
|
|
186
|
+
);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// ── Tier 3: binds on any machine-provenance-bearing document ──────────────
|
|
190
|
+
// Marker-scoped rather than universal so the hand-authored always-on corpus
|
|
191
|
+
// (16 of 29 live rule files) keeps writing; marker-scoped rather than
|
|
192
|
+
// `auto-generated`-only so a document that loses that line to truncation but
|
|
193
|
+
// keeps its provenance keys is still held to the invariant.
|
|
194
|
+
const isProvenanceBearing =
|
|
195
|
+
meta['auto-generated'] === true || hasKey('learning-key') || hasKey('expires-at');
|
|
196
|
+
|
|
197
|
+
if (isProvenanceBearing) {
|
|
198
|
+
const hasEmptyGlobs = Array.isArray(globs) && globs.length === 0;
|
|
199
|
+
const hasGlobs = Array.isArray(globs) && globs.length > 0;
|
|
200
|
+
const hasHostClass = hasKey('host-class');
|
|
201
|
+
|
|
202
|
+
if (hasEmptyGlobs) {
|
|
203
|
+
// NOT the "no axis" case and NOT always-on — the opposite: rule-loader.mjs
|
|
204
|
+
// excludes on `globs.length === 0` unconditionally, AFTER gating, so the
|
|
205
|
+
// rule never loads in ANY context even alongside a host-class: key.
|
|
206
|
+
problems.push('empty globs array (globs: []) — the rule would match nothing and never load in ANY context');
|
|
207
|
+
} else if (!hasGlobs && !hasHostClass) {
|
|
208
|
+
problems.push('no activation axis (globs absent AND host-class absent) — the rule would load always-on');
|
|
209
|
+
}
|
|
210
|
+
if (!hasKey('learning-key')) {
|
|
211
|
+
problems.push('missing required frontmatter key: learning-key');
|
|
212
|
+
}
|
|
213
|
+
if (!hasKey('expires-at')) {
|
|
214
|
+
problems.push('missing required frontmatter key: expires-at');
|
|
215
|
+
}
|
|
177
216
|
}
|
|
178
217
|
|
|
179
218
|
if (problems.length === 0) return null;
|
|
180
|
-
return `
|
|
219
|
+
return `rule fails the never-always-on invariant: ${problems.join('; ')}`;
|
|
181
220
|
}
|
|
182
221
|
|
|
183
222
|
// ---------------------------------------------------------------------------
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* redact-spans.mjs — order-independent, overlap-safe span redaction primitive.
|
|
3
|
+
*
|
|
4
|
+
* Extracted verbatim from `scripts/lib/validate/check-owner-leakage.mjs` (issue
|
|
5
|
+
* #974) so that every redaction sink in the repo shares ONE implementation of the
|
|
6
|
+
* overlap-safe span merge rather than each re-deriving it (and each re-deriving
|
|
7
|
+
* the prefix/suffix bug described below). Pure and synchronous — no I/O, no
|
|
8
|
+
* module state, no dependencies — because hot-path consumers import it.
|
|
9
|
+
*
|
|
10
|
+
* Behaviour is unchanged from the original: this module is a move, not a rewrite.
|
|
11
|
+
*
|
|
12
|
+
* DELIBERATE SECOND COPY — `check-owner-leakage.mjs` still carries its OWN inline
|
|
13
|
+
* `redactSpans()` and must keep it. That scanner is a documented standalone
|
|
14
|
+
* single-file vendoring target (`.claude/rules/security.md` § "Owner-Privacy
|
|
15
|
+
* Pre-Commit Hook"): consumer repos copy that ONE file, so a static import of this
|
|
16
|
+
* module throws ERR_MODULE_NOT_FOUND in every vendored copy and blocks all commits
|
|
17
|
+
* (proven by the three cases in `tests/husky/pre-commit-owner-leakage.test.mjs`).
|
|
18
|
+
* Unlike CP11's confidential-names helpers, a redaction sink cannot degrade to
|
|
19
|
+
* inert — an absent redaction prints confidential names into a PUBLIC CI log.
|
|
20
|
+
* So: THIS module serves in-tree consumers, the inline copy serves the vendored
|
|
21
|
+
* path, and `tests/lib/redact-spans.test.mjs` pins the two byte-for-byte against
|
|
22
|
+
* each other. Edit one → that drift guard goes red until both agree again.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Redact every confidential-name span from `line`, ORDER-INDEPENDENTLY (Fix 1 + Fix 2).
|
|
27
|
+
*
|
|
28
|
+
* This is the single redaction sink for the confidential-names privacy invariant.
|
|
29
|
+
* In `check-owner-leakage.mjs` it is applied at the print choke-point over EVERY
|
|
30
|
+
* violation's lineContent — not only CP11 hits — because that scanner runs in a
|
|
31
|
+
* PUBLIC GitHub-Actions mirror: a confidential customer/repo name that co-occurs
|
|
32
|
+
* with a CP1–CP10 hit on the same line (e.g. a name beside an RFC1918 IP that
|
|
33
|
+
* fails CP8) would otherwise be echoed verbatim to the public CI log (Fix 1).
|
|
34
|
+
*
|
|
35
|
+
* ORDER-INDEPENDENCE (Fix 2): a naïve chain of `.replace()` calls is order-dependent
|
|
36
|
+
* — when one configured name is a PREFIX of another (`['acme','acme-corp-secret']`),
|
|
37
|
+
* redacting the shorter first destroys the longer's match and leaks a suffix residue
|
|
38
|
+
* (`[REDACTED]-corp-secret`). Instead we compute ALL match spans against the ORIGINAL
|
|
39
|
+
* (unmutated) string across every pattern, merge overlapping/adjacent intervals, and
|
|
40
|
+
* splice `[REDACTED]` per merged interval. No pattern ever sees a string another
|
|
41
|
+
* pattern already rewrote, so prefix/suffix overlap cannot leak regardless of list
|
|
42
|
+
* order.
|
|
43
|
+
*
|
|
44
|
+
* @param {string} line — the raw (already-trimmed) violation lineContent.
|
|
45
|
+
* @param {RegExp[]} patterns — confidential-name regexes (word-boundary, case-insensitive).
|
|
46
|
+
* @returns {string} the line with every configured name span replaced by [REDACTED].
|
|
47
|
+
*/
|
|
48
|
+
export function redactSpans(line, patterns) {
|
|
49
|
+
if (!Array.isArray(patterns) || patterns.length === 0) return line;
|
|
50
|
+
|
|
51
|
+
// 1. Collect [start, end) spans of every match of every pattern against the
|
|
52
|
+
// ORIGINAL line (never a partially-mutated one). Global clone so exec() walks
|
|
53
|
+
// all matches; zero-width guard prevents an infinite loop on a degenerate regex.
|
|
54
|
+
const spans = [];
|
|
55
|
+
for (const re of patterns) {
|
|
56
|
+
const g = new RegExp(re.source, re.flags.includes('g') ? re.flags : re.flags + 'g');
|
|
57
|
+
let m;
|
|
58
|
+
while ((m = g.exec(line)) !== null) {
|
|
59
|
+
if (m[0].length === 0) {
|
|
60
|
+
g.lastIndex += 1;
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
spans.push([m.index, m.index + m[0].length]);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
if (spans.length === 0) return line;
|
|
67
|
+
|
|
68
|
+
// 2. Merge overlapping / adjacent intervals (sorted by start).
|
|
69
|
+
spans.sort((a, b) => a[0] - b[0]);
|
|
70
|
+
const merged = [];
|
|
71
|
+
for (const [s, e] of spans) {
|
|
72
|
+
const last = merged[merged.length - 1];
|
|
73
|
+
if (last && s <= last[1]) {
|
|
74
|
+
last[1] = Math.max(last[1], e);
|
|
75
|
+
} else {
|
|
76
|
+
merged.push([s, e]);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// 3. Splice [REDACTED] per merged interval, left-to-right over the ORIGINAL line.
|
|
81
|
+
let out = '';
|
|
82
|
+
let cursor = 0;
|
|
83
|
+
for (const [s, e] of merged) {
|
|
84
|
+
out += line.slice(cursor, s) + '[REDACTED]';
|
|
85
|
+
cursor = e;
|
|
86
|
+
}
|
|
87
|
+
out += line.slice(cursor);
|
|
88
|
+
return out;
|
|
89
|
+
}
|