@event4u/agent-config 14.8.0 → 14.9.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/CHANGELOG.md +165 -237
- package/README.md +1 -1
- package/agents/templates/.ai-council.yml.example +34 -4
- package/dist/agent-src/commands/pr/merge.md +9 -5
- package/dist/agent-src/commands/review/changes.md +95 -14
- package/dist/agent-src/commands/roadmap/process-full.md +2 -2
- package/dist/agent-src/contexts/execution/host-capability-manifest.md +77 -0
- package/dist/agent-src/contexts/execution/roadmap-execution-contract.md +36 -0
- package/dist/agent-src/contexts/execution/subagent-response-contract.md +80 -0
- package/dist/agent-src/rules/untrusted-input-defense.md +9 -3
- package/dist/agent-src/skills/code-review/evals/evals.json +87 -46
- package/dist/agent-src/skills/humanizer/SKILL.md +39 -0
- package/dist/agent-src/skills/humanizer/evals/strip_fixtures.json +57 -0
- package/dist/agent-src/skills/humanizer/references/fixtures.md +54 -0
- package/dist/agent-src/skills/judge-spec-compliance/SKILL.md +154 -0
- package/dist/agent-src/skills/judge-synthesis/SKILL.md +45 -0
- package/dist/agent-src/skills/roadmap-writing/SKILL.md +18 -18
- package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
- package/dist/agent-src/templates/roadmaps.md +8 -0
- package/dist/cli/registry.js +2 -0
- package/dist/cli/registry.js.map +1 -1
- package/dist/cli-delegate/{chunk-MMSGVZKV.js → chunk-77GR3VD4.js} +65 -40
- package/dist/cli-delegate/{chunk-Q26HT4QT.js → chunk-E46KPN6K.js} +251 -150
- package/dist/cli-delegate/{chunk-EEJUMCDZ.js → chunk-PLSG6UL5.js} +1 -1
- package/dist/cli-delegate/cmd_conformance.js +89 -13
- package/dist/cli-delegate/cmd_doctor.js +3 -3
- package/dist/cli-delegate/cmd_handoff.js +2 -2
- package/dist/cli-delegate/cmd_memory_get.js +1 -1
- package/dist/cli-delegate/cmd_route_audit.js +1 -1
- package/dist/cli-delegate/cmd_settings_get.js +1 -1
- package/dist/cli-delegate/cmd_settings_set.js +1 -1
- package/dist/discovery/deprecation-report.md +1 -1
- package/dist/discovery/discovery-manifest.json +37 -15
- package/dist/discovery/discovery-manifest.json.sha256 +1 -1
- package/dist/discovery/discovery-manifest.summary.md +3 -3
- package/dist/discovery/dormancy-report.md +1 -1
- package/dist/discovery/orphan-report.md +1 -1
- package/dist/discovery/packs.json +6 -5
- package/dist/discovery/trust-report.md +2 -2
- package/dist/discovery/workspaces.json +5 -4
- package/dist/hooks/dispatch.js +562 -204
- package/dist/mcp/registry-manifest.json +3 -3
- package/dist/mcp/server.json +2 -2
- package/dist/mcp/server.mjs +808 -331
- package/dist/scripts/_lib/claude_plugin.js +162 -0
- package/dist/scripts/_lib/claude_plugin.js.map +1 -0
- package/dist/scripts/_lib/environment_detector.js +397 -0
- package/dist/scripts/_lib/environment_detector.js.map +1 -0
- package/dist/scripts/_lib/spawn_env.js +129 -0
- package/dist/scripts/_lib/spawn_env.js.map +1 -0
- package/dist/scripts/ai_council/modes.js +199 -0
- package/dist/scripts/ai_council/modes.js.map +1 -0
- package/dist/scripts/ai_council/transport_resolver.js +418 -0
- package/dist/scripts/ai_council/transport_resolver.js.map +1 -0
- package/dist/server/routes/wizard.js +3 -2
- package/dist/server/routes/wizard.js.map +1 -1
- package/dist/ui/assets/index-Cxgy63kz.js +26 -0
- package/dist/ui/assets/index-Cxgy63kz.js.map +1 -0
- package/dist/ui/index.html +1 -1
- package/docs/guidelines/agent-infra/traceability-field-mechanics.md +50 -0
- package/docs/guidelines/agent-infra/untrusted-input-spotlighting.md +17 -0
- package/package.json +7 -3
- package/src/config/estate-count-budget.json +13 -744
- package/src/config/evaluator-budgets.json +4 -4
- package/src/config/gate-coverage.yml +247 -9
- package/src/config/gate-violation-baselines.json +72 -72
- package/src/config/publish-surface.json +60 -0
- package/src/config/requirements-trace-budget.json +32 -0
- package/src/config/rule-stub-ceilings.json +8 -1
- package/src/scripts/_dispatch.bash +7 -0
- package/src/scripts/_lib/base_tree.ts +175 -0
- package/src/scripts/_lib/billing_grant.ts +165 -0
- package/src/scripts/_lib/billing_grant_cli.ts +123 -0
- package/src/scripts/_lib/council_fallback_posture.ts +53 -6
- package/src/scripts/_lib/council_fallback_wiring.ts +61 -1
- package/src/scripts/_lib/host_capability.ts +59 -5
- package/src/scripts/_lib/quota_parked.ts +133 -0
- package/src/scripts/_lib/retrieval_sanitize.ts +62 -0
- package/src/scripts/_lib/review_telemetry.ts +196 -0
- package/src/scripts/_lib/subagent_response.ts +92 -0
- package/src/scripts/adr/regenerate_index.ts +14 -7
- package/src/scripts/ai_council/clients.ts +12 -1
- package/src/scripts/ai_council/config.ts +72 -115
- package/src/scripts/ai_council/fallback_config.ts +19 -7
- package/src/scripts/ai_council/mid_flight_fallback.ts +88 -7
- package/src/scripts/ai_council/orchestrator.ts +2 -2
- package/src/scripts/ai_council/py_format.ts +124 -0
- package/src/scripts/ai_council/transport_resolver.ts +65 -2
- package/src/scripts/audit_adr_coverage.ts +33 -6
- package/src/scripts/check_council_pin_staleness.ts +300 -0
- package/src/scripts/check_estate_count.ts +420 -293
- package/src/scripts/check_gate_coverage.ts +91 -0
- package/src/scripts/check_no_stub_inventory_table.ts +292 -0
- package/src/scripts/check_pack_size.ts +234 -1
- package/src/scripts/check_publish_surface.ts +297 -0
- package/src/scripts/check_requirements_trace.ts +378 -0
- package/src/scripts/check_secret_leak.ts +166 -12
- package/src/scripts/code_graph/cli.ts +44 -5
- package/src/scripts/code_graph/extract.ts +87 -0
- package/src/scripts/council_cli.ts +12 -16
- package/src/scripts/detect_ai_tells.ts +87 -0
- package/src/scripts/dispatch_r2_reviewer.ts +42 -0
- package/src/scripts/generate_capabilities_index.ts +41 -0
- package/src/scripts/git_authorization_hook.ts +54 -4
- package/src/scripts/hooks/block_unauthorized_git.ts +19 -1
- package/src/scripts/hooks/run_continuation_hook.ts +3 -1
- package/src/scripts/hooks/subagent_ledger_hook.ts +76 -11
- package/src/scripts/injection_scan_hook.ts +120 -10
- package/src/scripts/lint_adr_sweep_routing.ts +365 -0
- package/src/scripts/lint_workflow_security.ts +53 -10
- package/src/scripts/lint_workflow_security_allowlist.json +2 -18
- package/src/scripts/report_envelope_rate.ts +166 -0
- package/src/scripts/review_axis_report.ts +218 -0
- package/src/scripts/run_supervise.ts +46 -1
- package/src/scripts/schemas/evals.schema.json +5 -0
- package/src/scripts/sync_pr_branch.ts +14 -7
- package/dist/cli/commands/commands.candidates.test.js +0 -257
- package/dist/cli/commands/commands.candidates.test.js.map +0 -1
- package/dist/cli/commands/profiles.test.js +0 -88
- package/dist/cli/commands/profiles.test.js.map +0 -1
- package/dist/cli/commands/recordTriggerEval.test.js +0 -113
- package/dist/cli/commands/recordTriggerEval.test.js.map +0 -1
- package/dist/cli/configRoot.test.js +0 -97
- package/dist/cli/configRoot.test.js.map +0 -1
- package/dist/cli/mcp/content.test.js +0 -54
- package/dist/cli/mcp/content.test.js.map +0 -1
- package/dist/cli/mcp/dispatch.test.js +0 -108
- package/dist/cli/mcp/dispatch.test.js.map +0 -1
- package/dist/server/writeRoot.test.js +0 -91
- package/dist/server/writeRoot.test.js.map +0 -1
- package/dist/shared/capabilities.test.js +0 -39
- package/dist/shared/capabilities.test.js.map +0 -1
- package/dist/ui/assets/index-C1pBRm1f.js +0 -26
- package/dist/ui/assets/index-C1pBRm1f.js.map +0 -1
- /package/dist/cli-delegate/{chunk-N7CDDGI3.js → chunk-CYH5SP6M.js} +0 -0
|
@@ -0,0 +1,378 @@
|
|
|
1
|
+
#!/usr/bin/env tsx
|
|
2
|
+
/**
|
|
3
|
+
* Requirement → acceptance → evidence INVENTORY. Exits 0 always.
|
|
4
|
+
*
|
|
5
|
+
* ## Why a listing and not a gate
|
|
6
|
+
*
|
|
7
|
+
* A gate that can fail on day one reds the whole backlog, and this tree has
|
|
8
|
+
* recorded that failure once already: `road-to-plan-gates-measurement` sits in
|
|
9
|
+
* `later/` because its counter measured **0** — its own text records "Measured
|
|
10
|
+
* at parking time: 0" and an advisory window of 0 of 10 gated PRs. So this
|
|
11
|
+
* script is deliberately a *reader*: it prints the distribution and returns 0
|
|
12
|
+
* whatever it finds, including when it finds nothing.
|
|
13
|
+
*
|
|
14
|
+
* The name carries a `check_` prefix because that is what the gate-population
|
|
15
|
+
* classifier keys on and the script IS registered for coverage; the exit code is
|
|
16
|
+
* what makes it a listing rather than an enforcement.
|
|
17
|
+
*
|
|
18
|
+
* ## Three populations, reported separately
|
|
19
|
+
*
|
|
20
|
+
* The council's correction, and the reason this is not one number. Step 2.1
|
|
21
|
+
* originally named three roadmaps to dogfood; all three were archived by sibling
|
|
22
|
+
* pull requests in the same drain run, so the named set was gone before any
|
|
23
|
+
* window opened. Merging one PR before another would have changed the reported
|
|
24
|
+
* result with no roadmap edited — which makes merge order part of the
|
|
25
|
+
* experiment.
|
|
26
|
+
*
|
|
27
|
+
* - `fixture` — synthetic inputs, for parser and resolver mechanics. Never
|
|
28
|
+
* counts toward adoption: a maintainer-authored fixture is
|
|
29
|
+
* evidence about the reader, not about uptake.
|
|
30
|
+
* - `cohort` — a frozen manifest of files under study, active or archived.
|
|
31
|
+
* Experimental history; may include archived members.
|
|
32
|
+
* - `live` — the current ACTIVE corpus only. This is the compliance
|
|
33
|
+
* number, and the only one a ratchet may read.
|
|
34
|
+
*
|
|
35
|
+
* Collapsing them is how a zero from non-adoption becomes indistinguishable
|
|
36
|
+
* from a zero from non-compliance.
|
|
37
|
+
*
|
|
38
|
+
* ## What `unresolved` means, and when it is evaluated
|
|
39
|
+
*
|
|
40
|
+
* At the CURRENT head, always. A ref that resolved when it was declared and no
|
|
41
|
+
* longer resolves is `unresolved`, and that is the intended reading rather than
|
|
42
|
+
* a defect in it — deleted evidence is what an inventory exists to surface. The
|
|
43
|
+
* consequence, stated rather than discovered: a completed roadmap can move from
|
|
44
|
+
* resolved to unresolved with no roadmap edit.
|
|
45
|
+
*
|
|
46
|
+
* Usage:
|
|
47
|
+
* ./scripts-run src/scripts/check_requirements_trace [--json] [--dir P]
|
|
48
|
+
*/
|
|
49
|
+
|
|
50
|
+
import * as fs from 'node:fs';
|
|
51
|
+
import * as os from 'node:os';
|
|
52
|
+
import * as path from 'node:path';
|
|
53
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
54
|
+
|
|
55
|
+
import { runGateCli, runSelfTest, type SelfTestCase } from './_lib/gate_self_test.js';
|
|
56
|
+
import { assertScanned, DeadScopeError } from './_lib/scan_scope.js';
|
|
57
|
+
|
|
58
|
+
const _HERE = fileURLToPath(import.meta.url);
|
|
59
|
+
const REPO_ROOT = path.resolve(path.dirname(_HERE), '..', '..');
|
|
60
|
+
const ROADMAP_DIR = path.join(REPO_ROOT, 'agents', 'roadmaps');
|
|
61
|
+
|
|
62
|
+
/** The claim-ledger kebab slug, reused rather than invented. */
|
|
63
|
+
const SLUG = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
64
|
+
|
|
65
|
+
export interface TraceRow {
|
|
66
|
+
requirement_id: string;
|
|
67
|
+
acceptance_id: string;
|
|
68
|
+
evidence_refs: string[];
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export interface FileTrace {
|
|
72
|
+
file: string;
|
|
73
|
+
rows: TraceRow[];
|
|
74
|
+
/** `[AC:<id>]` ids found on `verify:` lines. */
|
|
75
|
+
ac_annotations: string[];
|
|
76
|
+
/** Rows whose ids do not match the reused slug grammar. */
|
|
77
|
+
malformed: string[];
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Parse the `traceability:` rows out of a roadmap.
|
|
82
|
+
*
|
|
83
|
+
* A deliberately small reader over the block form § 2c fixes, and NOT a YAML
|
|
84
|
+
* parser: the block may appear inside a blockquote (this roadmap dogfoods it
|
|
85
|
+
* that way) and a real parser would need the whole document to be YAML, which a
|
|
86
|
+
* roadmap is not. Leading `>` and whitespace are stripped per line.
|
|
87
|
+
*/
|
|
88
|
+
export function parseTrace(text: string): { rows: TraceRow[]; malformed: string[] } {
|
|
89
|
+
const lines = text.split('\n').map((l) => l.replace(/^\s*>\s?/, ''));
|
|
90
|
+
const rows: TraceRow[] = [];
|
|
91
|
+
const malformed: string[] = [];
|
|
92
|
+
let inBlock = false;
|
|
93
|
+
let cur: Partial<TraceRow> | null = null;
|
|
94
|
+
let inRefs = false;
|
|
95
|
+
|
|
96
|
+
const flush = (): void => {
|
|
97
|
+
if (cur === null) return;
|
|
98
|
+
const { requirement_id: req, acceptance_id: acc } = cur;
|
|
99
|
+
if (req !== undefined && acc !== undefined) {
|
|
100
|
+
if (!SLUG.test(req)) malformed.push(`requirement_id \`${req}\` is not a kebab slug`);
|
|
101
|
+
if (!SLUG.test(acc)) malformed.push(`acceptance_id \`${acc}\` is not a kebab slug`);
|
|
102
|
+
rows.push({ requirement_id: req, acceptance_id: acc, evidence_refs: cur.evidence_refs ?? [] });
|
|
103
|
+
}
|
|
104
|
+
cur = null;
|
|
105
|
+
inRefs = false;
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
for (const raw of lines) {
|
|
109
|
+
if (/^\s*traceability:\s*$/.test(raw)) {
|
|
110
|
+
inBlock = true;
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
if (!inBlock) continue;
|
|
114
|
+
// A fence or a non-indented non-list line ends the block.
|
|
115
|
+
if (/^\s*```/.test(raw) || (raw.trim() !== '' && !/^\s/.test(raw))) {
|
|
116
|
+
flush();
|
|
117
|
+
inBlock = false;
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
const item = /^\s*-\s+requirement_id:\s*(\S+)\s*$/.exec(raw);
|
|
121
|
+
if (item) {
|
|
122
|
+
flush();
|
|
123
|
+
cur = { requirement_id: item[1] as string, evidence_refs: [] };
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
if (cur === null) continue;
|
|
127
|
+
const acc = /^\s*acceptance_id:\s*(\S+)\s*$/.exec(raw);
|
|
128
|
+
if (acc) {
|
|
129
|
+
cur.acceptance_id = acc[1] as string;
|
|
130
|
+
inRefs = false;
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
if (/^\s*evidence_refs:\s*$/.test(raw)) {
|
|
134
|
+
inRefs = true;
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
const ref = /^\s*-\s+(\S+)\s*$/.exec(raw);
|
|
138
|
+
if (ref && inRefs) {
|
|
139
|
+
(cur.evidence_refs ??= []).push(ref[1] as string);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
flush();
|
|
143
|
+
return { rows, malformed };
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/** `[AC:<id>]` ids on `verify:` lines — the first structural reader of them. */
|
|
147
|
+
export function parseAcAnnotations(text: string): string[] {
|
|
148
|
+
const out: string[] = [];
|
|
149
|
+
for (const m of text.matchAll(/verify:\s*\[AC:([a-z0-9-]+)\]/g)) out.push(m[1] as string);
|
|
150
|
+
return out;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export interface Summary {
|
|
154
|
+
files: number;
|
|
155
|
+
rows: number;
|
|
156
|
+
/** Rows with at least one evidence ref. */
|
|
157
|
+
linked: number;
|
|
158
|
+
/** Rows with no evidence ref at all. */
|
|
159
|
+
unlinked: number;
|
|
160
|
+
/** Evidence refs that do not resolve at the current head. */
|
|
161
|
+
unresolved: number;
|
|
162
|
+
refs: number;
|
|
163
|
+
ac_annotations: number;
|
|
164
|
+
/** `[AC:…]` ids with no matching `acceptance_id` in the same file. */
|
|
165
|
+
dangling_ac: number;
|
|
166
|
+
malformed: number;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
export function summarise(traces: readonly FileTrace[], repoRoot: string): Summary {
|
|
170
|
+
let rows = 0;
|
|
171
|
+
let linked = 0;
|
|
172
|
+
let unlinked = 0;
|
|
173
|
+
let refs = 0;
|
|
174
|
+
let unresolved = 0;
|
|
175
|
+
let dangling = 0;
|
|
176
|
+
let malformed = 0;
|
|
177
|
+
for (const t of traces) {
|
|
178
|
+
rows += t.rows.length;
|
|
179
|
+
malformed += t.malformed.length;
|
|
180
|
+
const accIds = new Set(t.rows.map((r) => r.acceptance_id));
|
|
181
|
+
for (const id of t.ac_annotations) if (!accIds.has(id)) dangling += 1;
|
|
182
|
+
for (const r of t.rows) {
|
|
183
|
+
if (r.evidence_refs.length === 0) unlinked += 1;
|
|
184
|
+
else linked += 1;
|
|
185
|
+
for (const ref of r.evidence_refs) {
|
|
186
|
+
refs += 1;
|
|
187
|
+
// Evaluated at the CURRENT head. A ref that has been deleted is
|
|
188
|
+
// unresolved, which is the reading this inventory exists for.
|
|
189
|
+
if (!fs.existsSync(path.join(repoRoot, ref.replace(/:\d+(-\d+)?$/, '')))) {
|
|
190
|
+
unresolved += 1;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
return {
|
|
196
|
+
files: traces.length,
|
|
197
|
+
rows,
|
|
198
|
+
linked,
|
|
199
|
+
unlinked,
|
|
200
|
+
unresolved,
|
|
201
|
+
refs,
|
|
202
|
+
ac_annotations: traces.reduce((n, t) => n + t.ac_annotations.length, 0),
|
|
203
|
+
dangling_ac: dangling,
|
|
204
|
+
malformed,
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function readLive(dir: string): FileTrace[] {
|
|
209
|
+
let names: string[] = [];
|
|
210
|
+
try {
|
|
211
|
+
names = fs.readdirSync(dir).filter((n) => n.endsWith('.md') && n !== 'README.md').sort();
|
|
212
|
+
} catch {
|
|
213
|
+
return [];
|
|
214
|
+
}
|
|
215
|
+
const out: FileTrace[] = [];
|
|
216
|
+
for (const n of names) {
|
|
217
|
+
const text = fs.readFileSync(path.join(dir, n), 'utf-8');
|
|
218
|
+
const { rows, malformed } = parseTrace(text);
|
|
219
|
+
const ac = parseAcAnnotations(text);
|
|
220
|
+
if (rows.length === 0 && ac.length === 0) continue;
|
|
221
|
+
out.push({ file: `agents/roadmaps/${n}`, rows, ac_annotations: ac, malformed });
|
|
222
|
+
}
|
|
223
|
+
return out;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/** Floors for `--self-test`, declared here so a truncation is a visible diff. */
|
|
227
|
+
const SELF_TEST_MIN_CASES = 5;
|
|
228
|
+
const SELF_TEST_MIN_REJECT = 1;
|
|
229
|
+
const SCRIPT_REL = 'src/scripts/check_requirements_trace.ts';
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* `--self-test` for a gate that exits 0 ALWAYS — which needs explaining, since
|
|
233
|
+
* the harness's whole premise is that a gate proves it can go red.
|
|
234
|
+
*
|
|
235
|
+
* This one cannot, by design, so the reject case is the harness's OWN floor:
|
|
236
|
+
* a truncated suite must fail. What the accept cases prove instead is that the
|
|
237
|
+
* reader DISCRIMINATES — the same discrimination a failing exit would
|
|
238
|
+
* demonstrate elsewhere, asserted on the output rather than the exit code.
|
|
239
|
+
* Stated rather than left implicit, because a self-test whose cases all accept
|
|
240
|
+
* is exactly the degenerate pass the harness warns about.
|
|
241
|
+
*/
|
|
242
|
+
function _fixture(name: string, body: string, args: readonly string[] = []): number {
|
|
243
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), `reqtrace-st-${name}-`));
|
|
244
|
+
const rm = path.join(dir, 'agents', 'roadmaps');
|
|
245
|
+
fs.mkdirSync(rm, { recursive: true });
|
|
246
|
+
fs.writeFileSync(path.join(rm, 'a.md'), body, 'utf-8');
|
|
247
|
+
return runGateCli(REPO_ROOT, SCRIPT_REL, ['--dir', rm, ...args], dir);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
const _ROW = 'traceability:\n - requirement_id: r-one\n acceptance_id: a-one\n';
|
|
251
|
+
|
|
252
|
+
function selfTest(): number {
|
|
253
|
+
const cases: SelfTestCase[] = [
|
|
254
|
+
{ name: 'a declared row is accepted', expect: 'accept', run: () => _fixture('row', _ROW) },
|
|
255
|
+
{
|
|
256
|
+
name: 'a dangling [AC:] annotation still exits 0 — this is a listing, not a gate',
|
|
257
|
+
expect: 'accept',
|
|
258
|
+
run: () => _fixture('dangling', `${_ROW}\n verify: [AC:nowhere] x\n`),
|
|
259
|
+
},
|
|
260
|
+
{
|
|
261
|
+
name: 'an empty corpus exits 0 and does not read as a clean bill',
|
|
262
|
+
expect: 'accept',
|
|
263
|
+
run: () => _fixture('empty', '# no traceability block here\n'),
|
|
264
|
+
},
|
|
265
|
+
{
|
|
266
|
+
name: 'a non-slug id exits 0 and is reported, never enforced',
|
|
267
|
+
expect: 'accept',
|
|
268
|
+
run: () => _fixture('slug', 'traceability:\n - requirement_id: Not_A_Slug\n acceptance_id: a\n'),
|
|
269
|
+
},
|
|
270
|
+
{
|
|
271
|
+
name: 'an unknown argument is refused — the one non-zero path there is',
|
|
272
|
+
expect: 'reject',
|
|
273
|
+
run: () => _fixture('badarg', _ROW, ['--not-a-flag']),
|
|
274
|
+
},
|
|
275
|
+
];
|
|
276
|
+
return runSelfTest({
|
|
277
|
+
gate: 'check_requirements_trace',
|
|
278
|
+
cases,
|
|
279
|
+
minCases: SELF_TEST_MIN_CASES,
|
|
280
|
+
minRejectCases: SELF_TEST_MIN_REJECT,
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
export function main(argv: readonly string[] = process.argv.slice(2)): number {
|
|
285
|
+
if (argv.includes('--self-test')) return selfTest();
|
|
286
|
+
for (let k = 0; k < argv.length; k += 1) {
|
|
287
|
+
const a = argv[k] as string;
|
|
288
|
+
if (a === '--json') continue;
|
|
289
|
+
if (a === '--dir') {
|
|
290
|
+
k += 1;
|
|
291
|
+
continue;
|
|
292
|
+
}
|
|
293
|
+
if (a === '--self-test') continue;
|
|
294
|
+
process.stderr.write(`check_requirements_trace: unrecognized argument: ${a}\n`);
|
|
295
|
+
return 2;
|
|
296
|
+
}
|
|
297
|
+
const i = argv.indexOf('--dir');
|
|
298
|
+
const dir = i >= 0 && i + 1 < argv.length ? path.resolve(argv[i + 1] as string) : ROADMAP_DIR;
|
|
299
|
+
const root = i >= 0 ? path.dirname(path.dirname(dir)) : REPO_ROOT;
|
|
300
|
+
|
|
301
|
+
let corpus = 0;
|
|
302
|
+
try {
|
|
303
|
+
corpus = fs.readdirSync(dir).filter((n) => n.endsWith('.md') && n !== 'README.md').length;
|
|
304
|
+
} catch {
|
|
305
|
+
corpus = 0;
|
|
306
|
+
}
|
|
307
|
+
const live = readLive(dir);
|
|
308
|
+
const s = summarise(live, root);
|
|
309
|
+
|
|
310
|
+
if (argv.includes('--json')) {
|
|
311
|
+
process.stdout.write(`${JSON.stringify({ population: 'live', corpus, ...s, files_detail: live }, null, 2)}\n`);
|
|
312
|
+
return 0;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
process.stdout.write(`scanned: ${String(corpus)}\n`);
|
|
316
|
+
try {
|
|
317
|
+
assertScanned({
|
|
318
|
+
gate: 'check_requirements_trace',
|
|
319
|
+
scanned: corpus,
|
|
320
|
+
units: 'roadmap file(s)',
|
|
321
|
+
roots: ['agents/roadmaps'],
|
|
322
|
+
});
|
|
323
|
+
} catch (err) {
|
|
324
|
+
if (err instanceof DeadScopeError) {
|
|
325
|
+
// Reported, and STILL exit 0. A dead scope is a real finding about
|
|
326
|
+
// this reader, and turning it into a non-zero exit would make the
|
|
327
|
+
// listing a gate — which is the one thing it must not become.
|
|
328
|
+
process.stderr.write(`⚠️ ${err.message}\n`);
|
|
329
|
+
} else {
|
|
330
|
+
throw err;
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
process.stdout.write('\n| Roadmap | requirement_id | acceptance_id | evidence refs | unresolved |\n');
|
|
335
|
+
process.stdout.write('|---|---|---|---|---|\n');
|
|
336
|
+
if (live.length === 0) {
|
|
337
|
+
process.stdout.write('| _none declared_ | — | — | — | — |\n');
|
|
338
|
+
}
|
|
339
|
+
for (const t of live) {
|
|
340
|
+
for (const r of t.rows) {
|
|
341
|
+
const bad = r.evidence_refs.filter(
|
|
342
|
+
(ref) => !fs.existsSync(path.join(root, ref.replace(/:\d+(-\d+)?$/, ''))),
|
|
343
|
+
).length;
|
|
344
|
+
process.stdout.write(
|
|
345
|
+
`| ${t.file} | ${r.requirement_id} | ${r.acceptance_id} | ` +
|
|
346
|
+
`${String(r.evidence_refs.length)} | ${String(bad)} |\n`,
|
|
347
|
+
);
|
|
348
|
+
}
|
|
349
|
+
for (const id of t.ac_annotations) {
|
|
350
|
+
const known = t.rows.some((r) => r.acceptance_id === id);
|
|
351
|
+
if (!known) {
|
|
352
|
+
process.stdout.write(`| ${t.file} | — | **[AC:${id}] dangling** | — | — |\n`);
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
// The line a ratchet reads, and the line a later reading is compared to.
|
|
358
|
+
// Integers with the corpus they were computed over, never a verdict: "
|
|
359
|
+
// traceability is patchy" cannot be compared to the next reading.
|
|
360
|
+
process.stdout.write(
|
|
361
|
+
`\nlive: rows ${String(s.rows)} · linked ${String(s.linked)} · unlinked ${String(s.unlinked)} · ` +
|
|
362
|
+
`refs ${String(s.refs)} · unresolved ${String(s.unresolved)} · ` +
|
|
363
|
+
`ac_annotations ${String(s.ac_annotations)} · dangling_ac ${String(s.dangling_ac)} · ` +
|
|
364
|
+
`malformed ${String(s.malformed)} · over ${String(corpus)} active roadmap(s), ` +
|
|
365
|
+
`${String(s.files)} declaring\n`,
|
|
366
|
+
);
|
|
367
|
+
process.stdout.write(
|
|
368
|
+
' POPULATION: live (active corpus only). A `fixture` population tests this reader\n' +
|
|
369
|
+
' and never counts toward adoption; a frozen `cohort` may include archived files\n' +
|
|
370
|
+
' and is experimental history. Collapsing the three is how a zero from\n' +
|
|
371
|
+
' non-adoption becomes indistinguishable from a zero from non-compliance.\n',
|
|
372
|
+
);
|
|
373
|
+
return 0;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
if (process.argv[1] !== undefined && import.meta.url === pathToFileURL(process.argv[1]).href) {
|
|
377
|
+
process.exit(main());
|
|
378
|
+
}
|
|
@@ -38,6 +38,7 @@ import * as path from 'node:path';
|
|
|
38
38
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
39
39
|
|
|
40
40
|
import { assertScanned, DeadScopeError } from './_lib/scan_scope.js';
|
|
41
|
+
import { runSelfTest } from './_lib/gate_self_test.js';
|
|
41
42
|
import { scanText, type SecretFinding } from './_lib/secret_detector.js';
|
|
42
43
|
|
|
43
44
|
const _HERE = path.dirname(fileURLToPath(import.meta.url));
|
|
@@ -107,8 +108,34 @@ function isAllowed(rel: string, lineno: number, allow: readonly AllowEntry[]): b
|
|
|
107
108
|
return allow.some((a) => a.file === rel && (a.line === null || a.line === lineno));
|
|
108
109
|
}
|
|
109
110
|
|
|
110
|
-
|
|
111
|
-
|
|
111
|
+
/** `dist/` — excluded everywhere EXCEPT the pack scope. See `isExcluded`. */
|
|
112
|
+
const DIST_EXCLUDE: RegExp = /(^|\/)dist(\/|$)/;
|
|
113
|
+
|
|
114
|
+
function isExcluded(rel: string, mode: Mode = 'diff'): boolean {
|
|
115
|
+
return DEFAULT_EXCLUDE.some((rx) => {
|
|
116
|
+
// The `dist/` exclusion is the SECOND reason this gate could not see
|
|
117
|
+
// what ships, and it fires regardless of mode. Mode enumeration was the
|
|
118
|
+
// first: every git-backed mode walks the git tree, and `dist/cli/**` is
|
|
119
|
+
// gitignored while `dist/` is a `files[]` root. Adding the pack scope
|
|
120
|
+
// alone fixed nothing measurable — the payload resolved and then this
|
|
121
|
+
// filter threw away exactly the paths the scope exists to reach.
|
|
122
|
+
// Verified: a key-shaped string planted at `dist/zzcanary/leak.txt`
|
|
123
|
+
// (`git check-ignore -v` names `.gitignore:206`, and `npm pack` ships
|
|
124
|
+
// it) was missed by the new mode until this branch existed.
|
|
125
|
+
//
|
|
126
|
+
// Kept for every OTHER mode, because there the exclusion is right:
|
|
127
|
+
// `dist/` is a byte-for-byte projection of `src/`, which is scanned at
|
|
128
|
+
// its own path, so scanning the copy adds no coverage. In the pack scope
|
|
129
|
+
// the question is different — not "is this content somewhere else in the
|
|
130
|
+
// repo" but "does this content SHIP" — and for an untracked shipped path
|
|
131
|
+
// there is no other path to have scanned it at.
|
|
132
|
+
//
|
|
133
|
+
// The cost is a duplicate finding for a TRACKED dist file. That is not a
|
|
134
|
+
// false positive: a projection is byte-identical to its source, so a
|
|
135
|
+
// finding there is a finding in `src/` too.
|
|
136
|
+
if (mode === 'pack' && rx.source === DIST_EXCLUDE.source) return false;
|
|
137
|
+
return rx.test(rel);
|
|
138
|
+
});
|
|
112
139
|
}
|
|
113
140
|
|
|
114
141
|
/** Fast binary sniff — a NUL byte in the first 8 KiB means "not text". */
|
|
@@ -132,7 +159,48 @@ function gitLines(root: string, args: readonly string[]): string[] {
|
|
|
132
159
|
return res.stdout.split('\n').filter((l) => l.trim() !== '');
|
|
133
160
|
}
|
|
134
161
|
|
|
135
|
-
export type Mode = 'diff' | 'all' | 'explicit';
|
|
162
|
+
export type Mode = 'diff' | 'all' | 'explicit' | 'pack';
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* `pack` exists because the other three cannot see a large part of what ships.
|
|
166
|
+
*
|
|
167
|
+
* Every one of them enumerates the GIT tree — `git diff`, `git ls-files`, or an
|
|
168
|
+
* explicit path list a caller derived from one. `dist/` is a `files[]` root and
|
|
169
|
+
* `dist/cli/**` is gitignored (`.gitignore:206` is `/dist/*`, and
|
|
170
|
+
* `git check-ignore -v dist/cli/agent-config.js` names that line), so those
|
|
171
|
+
* paths are shipped and untracked at the same time and this gate could not
|
|
172
|
+
* reach them **by construction** — not by misconfiguration.
|
|
173
|
+
*
|
|
174
|
+
* Additive on purpose: no existing caller changes behaviour, and the three
|
|
175
|
+
* git-backed modes are untouched.
|
|
176
|
+
*/
|
|
177
|
+
export function packPayloadFiles(root: string): string[] {
|
|
178
|
+
const out = spawnSync('npm', ['pack', '--dry-run', '--ignore-scripts', '--json'], {
|
|
179
|
+
cwd: root,
|
|
180
|
+
encoding: 'utf-8',
|
|
181
|
+
maxBuffer: 64 * 1024 * 1024,
|
|
182
|
+
});
|
|
183
|
+
if (out.status !== 0 || typeof out.stdout !== 'string') {
|
|
184
|
+
// Returning [] would hand the scope assertion an empty set, which it
|
|
185
|
+
// already treats as a gate that could not run — the right verdict, and
|
|
186
|
+
// reached without inventing a second failure path.
|
|
187
|
+
return [];
|
|
188
|
+
}
|
|
189
|
+
const i = out.stdout.indexOf('[');
|
|
190
|
+
if (i < 0) return [];
|
|
191
|
+
let parsed: unknown;
|
|
192
|
+
try {
|
|
193
|
+
parsed = JSON.parse(out.stdout.slice(i));
|
|
194
|
+
} catch {
|
|
195
|
+
return [];
|
|
196
|
+
}
|
|
197
|
+
if (!Array.isArray(parsed) || parsed.length === 0) return [];
|
|
198
|
+
const first = parsed[0] as { files?: { path?: unknown }[] };
|
|
199
|
+
const files = Array.isArray(first.files) ? first.files : [];
|
|
200
|
+
return files
|
|
201
|
+
.map((f) => (typeof f.path === 'string' ? f.path : ''))
|
|
202
|
+
.filter((pth) => pth !== '');
|
|
203
|
+
}
|
|
136
204
|
|
|
137
205
|
/**
|
|
138
206
|
* Resolve the file set.
|
|
@@ -155,6 +223,9 @@ export function resolveFiles(
|
|
|
155
223
|
if (mode === 'all') {
|
|
156
224
|
return gitLines(root, ['ls-files']);
|
|
157
225
|
}
|
|
226
|
+
if (mode === 'pack') {
|
|
227
|
+
return packPayloadFiles(root);
|
|
228
|
+
}
|
|
158
229
|
const base = opts.base ?? 'origin/main';
|
|
159
230
|
// An unresolvable base is a scan of NOTHING, not a clean scan.
|
|
160
231
|
//
|
|
@@ -191,7 +262,7 @@ export function scanRepo(
|
|
|
191
262
|
const applyExcludes = mode !== 'explicit';
|
|
192
263
|
const hits: LeakHit[] = [];
|
|
193
264
|
for (const rel of resolveFiles(root, mode, opts)) {
|
|
194
|
-
if (applyExcludes && isExcluded(rel)) {
|
|
265
|
+
if (applyExcludes && isExcluded(rel, mode)) {
|
|
195
266
|
continue;
|
|
196
267
|
}
|
|
197
268
|
const abs = path.join(root, rel);
|
|
@@ -220,6 +291,7 @@ export function scanRepo(
|
|
|
220
291
|
interface Args {
|
|
221
292
|
json: boolean;
|
|
222
293
|
all: boolean;
|
|
294
|
+
pack: boolean;
|
|
223
295
|
base: string | undefined;
|
|
224
296
|
paths: string[];
|
|
225
297
|
}
|
|
@@ -228,6 +300,7 @@ function parseArgs(argv: readonly string[]): Args {
|
|
|
228
300
|
const paths: string[] = [];
|
|
229
301
|
let json = false;
|
|
230
302
|
let all = false;
|
|
303
|
+
let pack = false;
|
|
231
304
|
let base: string | undefined;
|
|
232
305
|
for (let i = 0; i < argv.length; i++) {
|
|
233
306
|
const a = argv[i] as string;
|
|
@@ -235,13 +308,17 @@ function parseArgs(argv: readonly string[]): Args {
|
|
|
235
308
|
json = true;
|
|
236
309
|
} else if (a === '--all') {
|
|
237
310
|
all = true;
|
|
311
|
+
} else if (a === '--pack') {
|
|
312
|
+
pack = true;
|
|
238
313
|
} else if (a === '--base') {
|
|
239
314
|
base = argv[++i];
|
|
240
315
|
} else if (a === '-h' || a === '--help') {
|
|
241
316
|
_stdout(
|
|
242
|
-
'usage: check_secret_leak [paths...] [--all] [--base <ref>] [--json]\n' +
|
|
317
|
+
'usage: check_secret_leak [paths...] [--all] [--pack] [--base <ref>] [--json]\n' +
|
|
243
318
|
' default → scans files changed vs origin/main + untracked (shift-left).\n' +
|
|
244
319
|
' --all → scans the whole tracked tree.\n' +
|
|
320
|
+
' --pack → scans the npm pack payload, INCLUDING shipped-but-gitignored\n' +
|
|
321
|
+
' paths the three git-backed modes cannot reach.\n' +
|
|
245
322
|
' paths → scans exactly those paths (excludes skipped).\n' +
|
|
246
323
|
' Exit 1 on a high-confidence leak.\n',
|
|
247
324
|
);
|
|
@@ -253,7 +330,7 @@ function parseArgs(argv: readonly string[]): Args {
|
|
|
253
330
|
paths.push(a);
|
|
254
331
|
}
|
|
255
332
|
}
|
|
256
|
-
return { json, all, base, paths };
|
|
333
|
+
return { json, all, pack, base, paths };
|
|
257
334
|
}
|
|
258
335
|
|
|
259
336
|
function _stdout(s: string): void {
|
|
@@ -263,9 +340,73 @@ function _stderr(s: string): void {
|
|
|
263
340
|
process.stderr.write(s);
|
|
264
341
|
}
|
|
265
342
|
|
|
343
|
+
/**
|
|
344
|
+
* Discrimination, over the one property this branch added.
|
|
345
|
+
*
|
|
346
|
+
* The pack scope's whole value is that it reaches paths the git-backed modes
|
|
347
|
+
* cannot, and it was measurably INERT when first written: the mode resolved the
|
|
348
|
+
* payload and `DEFAULT_EXCLUDE`'s `dist/` rule then filtered away exactly those
|
|
349
|
+
* paths. A self-test over `isExcluded` is therefore not a formality here — it
|
|
350
|
+
* pins the difference between the mode working and the mode existing.
|
|
351
|
+
*
|
|
352
|
+
* Deliberately over the pure predicate rather than over a spawned `npm pack`:
|
|
353
|
+
* the canary in `gate-coverage.yml` already drives the real binary end to end
|
|
354
|
+
* with a real plant, and duplicating that here would add a pack run per case
|
|
355
|
+
* for no additional evidence.
|
|
356
|
+
*/
|
|
357
|
+
function selfTest(): number {
|
|
358
|
+
return runSelfTest({
|
|
359
|
+
gate: 'check_secret_leak',
|
|
360
|
+
minCases: 7,
|
|
361
|
+
minRejectCases: 3,
|
|
362
|
+
cases: [
|
|
363
|
+
{
|
|
364
|
+
name: 'pack scope does NOT exclude dist/ — the blind spot it exists for',
|
|
365
|
+
expect: 'reject',
|
|
366
|
+
run: () => (isExcluded('dist/cli/agent-config.js', 'pack') ? 0 : 1),
|
|
367
|
+
},
|
|
368
|
+
{
|
|
369
|
+
name: 'diff scope DOES exclude dist/ — a projection scanned at its source',
|
|
370
|
+
expect: 'accept',
|
|
371
|
+
run: () => (isExcluded('dist/cli/agent-config.js', 'diff') ? 0 : 1),
|
|
372
|
+
},
|
|
373
|
+
{
|
|
374
|
+
name: 'all scope also still excludes dist/ — untouched by this change',
|
|
375
|
+
expect: 'accept',
|
|
376
|
+
run: () => (isExcluded('dist/agent-src/rules/x.md', 'all') ? 0 : 1),
|
|
377
|
+
},
|
|
378
|
+
{
|
|
379
|
+
// The exclusion carve-out is `dist/` ONLY. If it leaked into the
|
|
380
|
+
// other rules the pack scope would start reading test fixtures,
|
|
381
|
+
// which legitimately hold secret-shaped strings.
|
|
382
|
+
name: 'pack scope still excludes tests/ — the carve-out is dist-only',
|
|
383
|
+
expect: 'accept',
|
|
384
|
+
run: () => (isExcluded('tests/fixtures/x.txt', 'pack') ? 0 : 1),
|
|
385
|
+
},
|
|
386
|
+
{
|
|
387
|
+
name: 'pack scope still excludes node_modules/',
|
|
388
|
+
expect: 'accept',
|
|
389
|
+
run: () => (isExcluded('node_modules/x/index.js', 'pack') ? 0 : 1),
|
|
390
|
+
},
|
|
391
|
+
{
|
|
392
|
+
name: 'pack scope reaches a second dist subtree, not just dist/cli',
|
|
393
|
+
expect: 'reject',
|
|
394
|
+
run: () => (isExcluded('dist/hooks/dispatch.js', 'pack') ? 0 : 1),
|
|
395
|
+
},
|
|
396
|
+
{
|
|
397
|
+
name: 'a shipped non-dist path is scanned in every mode',
|
|
398
|
+
expect: 'reject',
|
|
399
|
+
run: () => (isExcluded('src/scripts/x.ts', 'pack') || isExcluded('src/scripts/x.ts', 'diff') ? 0 : 1),
|
|
400
|
+
},
|
|
401
|
+
],
|
|
402
|
+
});
|
|
403
|
+
}
|
|
404
|
+
|
|
266
405
|
export function main(argv?: readonly string[]): number {
|
|
267
|
-
const
|
|
268
|
-
|
|
406
|
+
const rawArgv = argv ?? process.argv.slice(2);
|
|
407
|
+
if (rawArgv.includes('--self-test')) return selfTest();
|
|
408
|
+
const args = parseArgs(rawArgv);
|
|
409
|
+
const mode: Mode = args.pack ? 'pack' : args.paths.length > 0 ? 'explicit' : args.all ? 'all' : 'diff';
|
|
269
410
|
|
|
270
411
|
// Scope assertion, deliberately here and not inside `scanRepo`: that export
|
|
271
412
|
// is called directly by tests and other callers against arbitrary roots, and
|
|
@@ -280,6 +421,11 @@ export function main(argv?: readonly string[]): number {
|
|
|
280
421
|
// re-thrown untouched to preserve it.
|
|
281
422
|
try {
|
|
282
423
|
const targets = resolveFiles(REPO_ROOT, mode, { explicit: args.paths, base: args.base });
|
|
424
|
+
// The coverage register requires an enforced gate to report what it
|
|
425
|
+
// inspected. Printed before the assertion so a dead scope still tells
|
|
426
|
+
// the reader the number it died on, which is the one number that
|
|
427
|
+
// distinguishes "resolved nothing" from "resolved and filtered".
|
|
428
|
+
_stdout(`scanned: ${String(targets.length)}\n`);
|
|
283
429
|
assertScanned({
|
|
284
430
|
gate: 'check_secret_leak',
|
|
285
431
|
scanned: targets.length,
|
|
@@ -289,9 +435,15 @@ export function main(argv?: readonly string[]): number {
|
|
|
289
435
|
? ['git ls-files']
|
|
290
436
|
: mode === 'explicit'
|
|
291
437
|
? ['<explicit path arguments>']
|
|
292
|
-
:
|
|
293
|
-
|
|
294
|
-
|
|
438
|
+
: mode === 'pack'
|
|
439
|
+
? ['npm pack --dry-run --ignore-scripts payload (package.json files[])']
|
|
440
|
+
: [`git diff --name-only --diff-filter=ACMR ${args.base ?? 'origin/main'}`, 'git ls-files --others --exclude-standard'],
|
|
441
|
+
// `all`, `explicit` and `pack` carry no reason: zero tracked files,
|
|
442
|
+
// zero named paths, or an empty pack payload is blindness in a repo
|
|
443
|
+
// that has content. `pack` inherits the strict path by omission
|
|
444
|
+
// rather than by a new branch — an empty payload means a botched
|
|
445
|
+
// `files[]` or a pack that resolved nothing, which is the loudest
|
|
446
|
+
// possible signal and the opposite of a clean bill.
|
|
295
447
|
...(mode === 'diff'
|
|
296
448
|
? {
|
|
297
449
|
allowEmpty:
|
|
@@ -317,7 +469,9 @@ export function main(argv?: readonly string[]): number {
|
|
|
317
469
|
if (args.json) {
|
|
318
470
|
_stdout(JSON.stringify({ check: 'secret-leak', count: hits.length, hits }, null, 2) + '\n');
|
|
319
471
|
} else if (hits.length === 0) {
|
|
320
|
-
_stdout(
|
|
472
|
+
_stdout(
|
|
473
|
+
`✅ check_secret_leak: no high-confidence secret found in ${mode === 'pack' ? 'the npm pack payload' : 'the tracked tree'}.\n`,
|
|
474
|
+
);
|
|
321
475
|
} else {
|
|
322
476
|
_stderr(`❌ check_secret_leak: ${hits.length} high-confidence secret finding(s):\n`);
|
|
323
477
|
for (const h of hits) {
|