@event4u/agent-config 14.7.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 +175 -390
- package/README.md +1 -1
- package/agents/templates/.ai-council.yml.example +34 -4
- package/dist/agent-src/commands/evals/git-pr-merge.json +13 -0
- package/dist/agent-src/commands/evals/roadmap.json +17 -1
- package/dist/agent-src/commands/pr/create.md +14 -2
- package/dist/agent-src/commands/pr/merge.md +383 -0
- package/dist/agent-src/commands/review/changes.md +95 -14
- package/dist/agent-src/commands/roadmap/process-full.md +157 -3
- package/dist/agent-src/commands/roadmap.md +1 -1
- package/dist/agent-src/contexts/execution/auto-dispatch-classification.md +46 -36
- package/dist/agent-src/contexts/execution/autonomy-mechanics.md +37 -2
- package/dist/agent-src/contexts/execution/host-capability-manifest.md +77 -0
- package/dist/agent-src/contexts/execution/roadmap-execution-contract.md +100 -2
- package/dist/agent-src/contexts/execution/roadmap-process-loop.md +154 -2
- package/dist/agent-src/contexts/execution/subagent-response-contract.md +80 -0
- package/dist/agent-src/rules/artifact-drafting-protocol.md +5 -1
- package/dist/agent-src/rules/decision-revisit-gate.md +65 -33
- package/dist/agent-src/rules/recurring-criticism.md +1 -1
- package/dist/agent-src/rules/roadmap-progress-sync.md +19 -0
- package/dist/agent-src/rules/source-confidentiality.md +1 -1
- package/dist/agent-src/rules/untrusted-input-defense.md +9 -3
- package/dist/agent-src/scripts/archive_completed_roadmaps.ts +7 -4
- package/dist/agent-src/scripts/dashboard_mode.ts +212 -0
- package/dist/agent-src/scripts/update_roadmap_progress.ts +47 -48
- package/dist/agent-src/skills/adr-create/SKILL.md +124 -5
- package/dist/agent-src/skills/code-review/evals/evals.json +87 -46
- package/dist/agent-src/skills/decision-record/SKILL.md +61 -3
- package/dist/agent-src/skills/decision-review/SKILL.md +65 -0
- 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/merge-conflicts/SKILL.md +26 -0
- package/dist/agent-src/skills/roadmap-writing/SKILL.md +18 -18
- package/dist/agent-src/skills/security/SKILL.md +8 -0
- package/dist/agent-src/skills/security-audit/SKILL.md +12 -1
- package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
- package/dist/agent-src/templates/roadmaps.md +28 -2
- package/dist/cli/registry.js +3 -0
- package/dist/cli/registry.js.map +1 -1
- package/dist/cli-delegate/{chunk-FBKL6JWT.js → chunk-77GR3VD4.js} +68 -43
- package/dist/cli-delegate/{chunk-JZ4NR2TW.js → chunk-7PRE6ZV2.js} +1 -1
- package/dist/cli-delegate/{chunk-UEZ7D35A.js → chunk-CYH5SP6M.js} +4 -23
- package/dist/cli-delegate/{chunk-Q26HT4QT.js → chunk-E46KPN6K.js} +251 -150
- package/dist/cli-delegate/{chunk-74E3TKWV.js → chunk-H4VIVD4C.js} +5 -5
- package/dist/cli-delegate/chunk-I3EIN2YG.js +36 -0
- package/dist/cli-delegate/{chunk-4VR4ORQ6.js → chunk-LPPWO5CE.js} +4 -3
- package/dist/cli-delegate/{chunk-7F3VYSM2.js → chunk-PLSG6UL5.js} +14 -14
- package/dist/cli-delegate/cmd_adr_effective.js +1162 -0
- package/dist/cli-delegate/cmd_conformance.js +101 -25
- package/dist/cli-delegate/cmd_converge.js +3 -3
- package/dist/cli-delegate/cmd_doctor.js +7 -7
- package/dist/cli-delegate/cmd_export.js +6 -6
- package/dist/cli-delegate/cmd_handoff.js +7 -6
- package/dist/cli-delegate/cmd_memory_get.js +2 -1
- package/dist/cli-delegate/cmd_migrate.js +2 -2
- package/dist/cli-delegate/cmd_preflight.js +5 -5
- package/dist/cli-delegate/cmd_prune.js +6 -6
- package/dist/cli-delegate/cmd_route_audit.js +6 -5
- package/dist/cli-delegate/cmd_route_explain.js +3 -3
- package/dist/cli-delegate/cmd_session_recycle.js +3 -3
- package/dist/cli-delegate/cmd_settings_get.js +4 -4
- package/dist/cli-delegate/cmd_settings_migrate.js +6 -6
- package/dist/cli-delegate/cmd_settings_set.js +1 -1
- package/dist/cli-delegate/cmd_sync.js +6 -6
- package/dist/cli-delegate/cmd_uninstall.js +6 -6
- package/dist/cli-delegate/cmd_update.js +3 -3
- package/dist/cli-delegate/cmd_upgrade.js +3 -3
- package/dist/cli-delegate/cmd_validate.js +6 -6
- package/dist/cli-delegate/cmd_versions.js +3 -3
- package/dist/discovery/deprecation-report.md +1 -1
- package/dist/discovery/discovery-manifest.json +83 -30
- package/dist/discovery/discovery-manifest.json.sha256 +1 -1
- package/dist/discovery/discovery-manifest.summary.md +4 -4
- package/dist/discovery/dormancy-report.md +1 -1
- package/dist/discovery/orphan-report.md +1 -1
- package/dist/discovery/packs.json +11 -9
- package/dist/discovery/trust-report.md +2 -2
- package/dist/discovery/workspaces.json +9 -7
- package/dist/hooks/dispatch.js +1295 -812
- package/dist/install/globalRuleLayers.js +169 -0
- package/dist/install/globalRuleLayers.js.map +1 -0
- package/dist/install/partitionEligibility.js +156 -0
- package/dist/install/partitionEligibility.js.map +1 -1
- package/dist/install/ruleLayerPartition.js +157 -0
- package/dist/install/ruleLayerPartition.js.map +1 -0
- package/dist/mcp/registry-manifest.json +3 -3
- package/dist/mcp/server.json +2 -2
- package/dist/mcp/server.mjs +1358 -805
- 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/prune_empty_dirs.js +85 -0
- package/dist/scripts/_lib/prune_empty_dirs.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/scripts/condense.js +52 -89
- package/dist/scripts/condense.js.map +1 -1
- 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/artifact-drafting-protocol-mechanics.md +45 -1
- package/docs/guidelines/agent-infra/false-green.md +47 -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/agent-src/scripts/archive_completed_roadmaps.ts +7 -4
- package/src/agent-src/scripts/dashboard_mode.ts +212 -0
- package/src/agent-src/scripts/update_roadmap_progress.ts +47 -48
- package/src/config/ci-local-parity.yml +21 -0
- package/src/config/estate-count-budget.json +13 -471
- package/src/config/evaluator-budgets.json +4 -4
- package/src/config/gate-coverage.yml +458 -12
- package/src/config/gate-violation-baselines.json +18 -8
- package/src/config/gitignore-block.txt +15 -2
- package/src/config/hook-latency-budget.json +5 -0
- package/src/config/publish-surface.json +60 -0
- package/src/config/requirements-trace-budget.json +32 -0
- package/src/config/rule-stub-ceilings.json +25 -4
- package/src/install/globalRuleLayers.ts +197 -0
- package/src/install/partitionEligibility.ts +177 -0
- package/src/install/ruleLayerPartition.ts +212 -0
- package/src/scripts/_cli/cmd_adr_effective.ts +484 -0
- package/src/scripts/_dispatch.bash +18 -0
- package/src/scripts/_lib/adr_frontmatter.test.ts +365 -0
- package/src/scripts/_lib/adr_frontmatter.ts +459 -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/envelope_grounding.ts +11 -1
- package/src/scripts/_lib/host_capability.ts +59 -5
- package/src/scripts/_lib/layer_overlap_notice.ts +11 -2
- package/src/scripts/_lib/prune_empty_dirs.ts +80 -0
- 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/evidence_census.ts +899 -0
- package/src/scripts/adr/regenerate_index.ts +25 -38
- package/src/scripts/adr_cite_check.ts +540 -39
- 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 +134 -21
- package/src/scripts/bench_hook_latency.ts +204 -0
- package/src/scripts/build_archive_index.ts +34 -0
- package/src/scripts/check_adr_frontmatter.ts +574 -39
- 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_hook_bundle_content.ts +219 -0
- package/src/scripts/check_new_adr_evidence.ts +529 -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_references.ts +18 -0
- package/src/scripts/check_requirements_trace.ts +378 -0
- package/src/scripts/check_rule_layer_partition.ts +531 -0
- package/src/scripts/check_secret_leak.ts +166 -12
- package/src/scripts/check_single_delivery.ts +137 -4
- package/src/scripts/check_standing_rule_delivery.ts +8 -2
- package/src/scripts/code_graph/cli.ts +44 -5
- package/src/scripts/code_graph/extract.ts +87 -0
- package/src/scripts/condense.ts +61 -91
- 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/external_sources_denylist.json +4 -1
- 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 +41 -3
- package/src/scripts/hooks/dispatch_hook.ts +154 -187
- package/src/scripts/hooks/dispatch_issues.ts +38 -19
- package/src/scripts/hooks/fallback_yaml.ts +66 -0
- package/src/scripts/hooks/host_semantics.ts +21 -0
- package/src/scripts/hooks/injection_budget.ts +13 -3
- package/src/scripts/hooks/py_json_dumps.ts +103 -0
- package/src/scripts/hooks/run_continuation_hook.ts +3 -1
- package/src/scripts/hooks/state_io.ts +88 -1
- package/src/scripts/hooks/stdin_failure_policy.ts +96 -0
- package/src/scripts/hooks/subagent_ledger_hook.ts +76 -11
- package/src/scripts/injection_scan_hook.ts +257 -15
- package/src/scripts/lint_adr_sweep_routing.ts +365 -0
- package/src/scripts/lint_hook_manifest.ts +49 -0
- package/src/scripts/lint_provenance_vocabulary.ts +472 -4
- 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/routing_doctor.ts +5 -1
- package/src/scripts/run_supervise.ts +46 -1
- package/src/scripts/schemas/evals.schema.json +5 -0
- package/src/scripts/sync_pr_branch.ts +170 -20
- 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-PPQO4NGR.js → chunk-6RJA2F5A.js} +3 -3
- package/dist/cli-delegate/{chunk-6EOCNQAM.js → chunk-E4XIZDIB.js} +0 -0
- package/dist/cli-delegate/{chunk-GJ5BVHXM.js → chunk-PMEKGDED.js} +3 -3
|
@@ -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) {
|
|
@@ -76,9 +76,9 @@ import { reportScanned } from './_lib/scan_scope.js';
|
|
|
76
76
|
|
|
77
77
|
// ledger-exempt: this gate has no skip semantics to account for. `_lib/gate_ledger`
|
|
78
78
|
// exists so a gate that PASSES OVER targets says which and why; here the target set
|
|
79
|
-
// is
|
|
79
|
+
// is six fixed artefact types, every one of them appears in the output, and a type
|
|
80
80
|
// whose layer is missing is printed as `absent` rather than quietly dropped — see
|
|
81
|
-
// the `absent` field and the `types_compared=N of
|
|
81
|
+
// the `absent` field and the `types_compared=N of 6` ledger line, which together
|
|
82
82
|
// already carry what a per-target ledger would. Adding one would restate the render
|
|
83
83
|
// in a second format, and the gate's own docstring warns that the degenerate pass
|
|
84
84
|
// here is a marker with no argument behind it. If a future version gains a real skip
|
|
@@ -88,10 +88,51 @@ import { reportScanned } from './_lib/scan_scope.js';
|
|
|
88
88
|
const _HERE = fileURLToPath(import.meta.url);
|
|
89
89
|
const REPO_ROOT = path.resolve(path.dirname(_HERE), '..', '..');
|
|
90
90
|
|
|
91
|
-
/**
|
|
92
|
-
|
|
91
|
+
/**
|
|
92
|
+
* Artefact types the host loads from both layers.
|
|
93
|
+
*
|
|
94
|
+
* ## The identity key, named because it is not the same for every row
|
|
95
|
+
*
|
|
96
|
+
* Every row below compares BASENAMES within one family directory. For `rules`,
|
|
97
|
+
* `personas`, `user-types` and `agents` that IS the host's unit, so basename
|
|
98
|
+
* equality is the right key. For `commands` it is not: the host-visible unit is
|
|
99
|
+
* the invocation name `/cluster:sub`, which the colon form encodes in the PATH
|
|
100
|
+
* (`commands/<cluster>/<sub>.md`), so this gate's `commands` row compares cluster
|
|
101
|
+
* DIRECTORY names — a coarser key than the thing the host registers.
|
|
102
|
+
*
|
|
103
|
+
* That gap is recorded rather than closed, and the reason is a measurement: since
|
|
104
|
+
* `commandsWithheld` gates the colon-form generator, a machine with a verified
|
|
105
|
+
* host layer projects ZERO commands, so there is no project-side name for the
|
|
106
|
+
* coarse key to be wrong about. On a machine WITHOUT one, the project layer is
|
|
107
|
+
* the only layer and there is nothing to compare. The key is therefore imprecise
|
|
108
|
+
* exactly where it has no work to do. Closing it would mean reading each command
|
|
109
|
+
* file's own slug — real work for a row whose overlap is 0 in both topologies.
|
|
110
|
+
*
|
|
111
|
+
* `personas` and `user-types` were added 2026-08-21 (closure correction). See
|
|
112
|
+
* `unknownProjectFamilies` for what now prevents the next omission.
|
|
113
|
+
*/
|
|
114
|
+
const TYPES = ['rules', 'skills', 'commands', 'personas', 'user-types', 'agents'] as const;
|
|
93
115
|
type ArtefactType = (typeof TYPES)[number];
|
|
94
116
|
|
|
117
|
+
/**
|
|
118
|
+
* Directory names under `<repo>/.claude/` that the HOST writes, not a generator.
|
|
119
|
+
*
|
|
120
|
+
* `unknownProjectFamilies` used to rest on the premise that `<repo>/.claude/` is
|
|
121
|
+
* "written by `condense.ts` generators and nothing else". That premise is false
|
|
122
|
+
* and the gate proved it on 2026-08-22, refusing a release push over
|
|
123
|
+
* `.claude/worktrees/` — the maintainer's git-worktree root, gitignored at
|
|
124
|
+
* `.gitignore:246`, named as a conventional worktree location by
|
|
125
|
+
* `worktree_cleanup_check.STANDARD_WORKTREE_DIRS`, and excluded from
|
|
126
|
+
* `inventory_abstraction_budget`. Nothing about it is an artefact family, so
|
|
127
|
+
* "add it to TYPES" was advice that could not be taken.
|
|
128
|
+
*
|
|
129
|
+
* The set is hand-written and stays independent of any producer registry, for
|
|
130
|
+
* the same reason `TYPES` is: a verifier that derives its scope from a producer
|
|
131
|
+
* inherits that producer's omissions. It grows only on tree evidence that a name
|
|
132
|
+
* is host state — never to silence a family a generator actually emits.
|
|
133
|
+
*/
|
|
134
|
+
const HOST_OWNED = new Set(['worktrees']);
|
|
135
|
+
|
|
95
136
|
/**
|
|
96
137
|
* What a layer's entries physically ARE — symlinks, directories, or regular files.
|
|
97
138
|
*
|
|
@@ -273,6 +314,54 @@ export interface Verdict {
|
|
|
273
314
|
namesRead: number;
|
|
274
315
|
/** True when no type had BOTH layers present — nothing was actually compared. */
|
|
275
316
|
readNothing: boolean;
|
|
317
|
+
/**
|
|
318
|
+
* Family directories present in the PROJECT layer that `TYPES` does not name.
|
|
319
|
+
*
|
|
320
|
+
* The forcing function that makes this gate's own 2026-08-21 omission
|
|
321
|
+
* non-recurring: `personas` was written by a generator, shipped by the
|
|
322
|
+
* installer, and named by neither measurer, so it was invisible rather than
|
|
323
|
+
* reported — a gate cannot report a family it was never told exists.
|
|
324
|
+
*
|
|
325
|
+
* Only the PROJECT layer is scanned, and that is the whole reason this can be
|
|
326
|
+
* a hard refusal rather than a warning. Every entry in `<repo>/.claude/` is a
|
|
327
|
+
* family this repository chose to emit, EXCEPT the host-written names in
|
|
328
|
+
* `HOST_OWNED` — see that constant for why the unqualified version of this
|
|
329
|
+
* sentence was wrong. `~/.claude/` is the host's own directory —
|
|
330
|
+
* `plugins`, `projects`, `sessions`, `shell-snapshots`, `telemetry` and a
|
|
331
|
+
* dozen more live there — and treating an unrecognised name in it as a defect
|
|
332
|
+
* would be a false-positive generator, not a check.
|
|
333
|
+
*/
|
|
334
|
+
unknownFamilies: string[];
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
/**
|
|
338
|
+
* Family directories the project layer holds that `TYPES` does not name.
|
|
339
|
+
*
|
|
340
|
+
* Deliberately NOT derived from the installer's `_CLAUDE_SKILL_BUNDLE` or from
|
|
341
|
+
* `check_generator_output_coverage`'s registry: a verifier whose scope comes from
|
|
342
|
+
* a producer inherits that producer's omissions, which is the exact defect this
|
|
343
|
+
* function exists to catch. `TYPES` stays hand-written and independent; this
|
|
344
|
+
* compares it against the filesystem the generators actually produced and refuses
|
|
345
|
+
* on disagreement.
|
|
346
|
+
*/
|
|
347
|
+
function unknownProjectFamilies(projectRoot: string): string[] {
|
|
348
|
+
const known = new Set<string>(TYPES);
|
|
349
|
+
let entries: fs.Dirent[];
|
|
350
|
+
try {
|
|
351
|
+
entries = fs.readdirSync(projectRoot, { withFileTypes: true });
|
|
352
|
+
} catch {
|
|
353
|
+
return []; // no project layer at all — nothing emitted, nothing to miss
|
|
354
|
+
}
|
|
355
|
+
return entries
|
|
356
|
+
.filter(
|
|
357
|
+
(e) =>
|
|
358
|
+
e.isDirectory() &&
|
|
359
|
+
!e.name.startsWith('.') &&
|
|
360
|
+
!known.has(e.name) &&
|
|
361
|
+
!HOST_OWNED.has(e.name),
|
|
362
|
+
)
|
|
363
|
+
.map((e) => e.name)
|
|
364
|
+
.sort();
|
|
276
365
|
}
|
|
277
366
|
|
|
278
367
|
export function evaluate(globalRoot: string, projectRoot: string): Verdict {
|
|
@@ -299,6 +388,7 @@ export function evaluate(globalRoot: string, projectRoot: string): Verdict {
|
|
|
299
388
|
0,
|
|
300
389
|
),
|
|
301
390
|
readNothing: compared === 0,
|
|
391
|
+
unknownFamilies: unknownProjectFamilies(projectRoot),
|
|
302
392
|
};
|
|
303
393
|
}
|
|
304
394
|
|
|
@@ -354,12 +444,24 @@ export function render(v: Verdict, globalRoot: string, projectRoot: string): str
|
|
|
354
444
|
out.push('');
|
|
355
445
|
out.push(` layers not present (nothing to compare there): ${v.absent.join(', ')}`);
|
|
356
446
|
}
|
|
447
|
+
if (v.unknownFamilies.length > 0) {
|
|
448
|
+
out.push('');
|
|
449
|
+
out.push(
|
|
450
|
+
` ❌ unknown family in the project layer: ${v.unknownFamilies.join(', ')}` +
|
|
451
|
+
' — add it to TYPES in this gate AND in _lib/layer_overlap_notice.ts,',
|
|
452
|
+
);
|
|
453
|
+
out.push(
|
|
454
|
+
' or the next family shipped from two layers is invisible the way `personas`' +
|
|
455
|
+
' was until 2026-08-21.',
|
|
456
|
+
);
|
|
457
|
+
}
|
|
357
458
|
out.push('');
|
|
358
459
|
out.push(
|
|
359
460
|
`check_single_delivery ledger: duplicated=${v.duplicated}` +
|
|
360
461
|
` name_overlap_different_shape=${v.nameOverlapDifferentShape}` +
|
|
361
462
|
` scope_defeat=${v.defeated}` +
|
|
362
463
|
` types_compared=${v.typesCompared} of ${v.readings.length}` +
|
|
464
|
+
` unknown_families=${v.unknownFamilies.length}` +
|
|
363
465
|
` names_read=${v.namesRead}`,
|
|
364
466
|
);
|
|
365
467
|
return out.join('\n');
|
|
@@ -459,6 +561,37 @@ export function main(argv?: readonly string[]): number {
|
|
|
459
561
|
' case (layers present but never paired) is refused separately.',
|
|
460
562
|
});
|
|
461
563
|
|
|
564
|
+
// Refuses in both modes, unlike every other branch here — but ONLY over this
|
|
565
|
+
// repository's own generated tree, and that qualification was added by a
|
|
566
|
+
// neutral review (2026-08-21) rather than being there from the start.
|
|
567
|
+
//
|
|
568
|
+
// The asymmetry is the argument for the refusal: every other verdict here is
|
|
569
|
+
// a property of ONE MACHINE's two layers, which is why `--enforce` is opt-in.
|
|
570
|
+
// "this repository emits a family no measurer names" is true on every checkout
|
|
571
|
+
// and fixable in one line, so gating it behind `--enforce` would put the one
|
|
572
|
+
// topology-independent check behind the flag meant for topology-dependent
|
|
573
|
+
// ones.
|
|
574
|
+
//
|
|
575
|
+
// The qualification is the review's, and it is right: an unrecognised
|
|
576
|
+
// DIRECTORY does not by itself establish generator ownership. It does under
|
|
577
|
+
// `<repo>/.claude/`, which `condense.ts` writes and nothing else does. It does
|
|
578
|
+
// NOT under an arbitrary `--project` path, which may be a consumer tree with
|
|
579
|
+
// a host-native or project-only family that legitimately cannot overlap — and
|
|
580
|
+
// refusing there would make a nominal report exit 1 on a correct topology.
|
|
581
|
+
// So a repointed root REPORTS and does not refuse.
|
|
582
|
+
if (v.unknownFamilies.length > 0) {
|
|
583
|
+
const own = projectRoot === path.join(REPO_ROOT, '.claude');
|
|
584
|
+
process.stdout.write(
|
|
585
|
+
`${own ? '❌' : '⚠️ '} check_single_delivery: unknown family in the project` +
|
|
586
|
+
` layer: ${v.unknownFamilies.join(', ')}. Add measurement before it can ship` +
|
|
587
|
+
' — TYPES in this gate and in _lib/layer_overlap_notice.ts.' +
|
|
588
|
+
(own ? '' : ' Reported, not enforced: --project is repointed, so generator' +
|
|
589
|
+
' ownership of this tree is not established.') +
|
|
590
|
+
'\n',
|
|
591
|
+
);
|
|
592
|
+
if (own) return 1;
|
|
593
|
+
}
|
|
594
|
+
|
|
462
595
|
if (v.readNothing) {
|
|
463
596
|
// Never a pass. A gate that compared nothing has not verified anything,
|
|
464
597
|
// and saying "invariant holds" here is the false-green shape this repo
|
|
@@ -326,8 +326,14 @@ export function main(argv: readonly string[] = process.argv.slice(2)): number {
|
|
|
326
326
|
if (pct > 1) {
|
|
327
327
|
process.stderr.write(
|
|
328
328
|
`❌ ${PROG}: ${total} tok of standing rule prose exceeds the ${budget.total_cap_tokens} cap. `
|
|
329
|
-
+ 'If two layers overlap,
|
|
330
|
-
+ '
|
|
329
|
+
+ 'If two layers overlap, that is the ADR-236 partition not yet armed on this '
|
|
330
|
+
+ 'machine: run `agent-config install` so the host layer is fingerprinted in '
|
|
331
|
+
+ 'installed.lock, and the next `task generate-tools` withholds the globally-owned '
|
|
332
|
+
+ 'artefacts instead of writing a second copy. (Superseded advice: this line used to '
|
|
333
|
+
+ 'name `install --layer=<global|project>`, the layer-suppression remedy ADR-226 '
|
|
334
|
+
+ 'declined and ADR-236 replaced — a reader following it could not fix the '
|
|
335
|
+
+ 'condition.) Otherwise the total is body length, not duplication: scope rules with '
|
|
336
|
+
+ '`paths:` or move bodies into the digest.\n',
|
|
331
337
|
);
|
|
332
338
|
return 1;
|
|
333
339
|
}
|
|
@@ -77,8 +77,47 @@ function flag(argv: string[], name: string): string | null {
|
|
|
77
77
|
return i >= 0 && i + 1 < argv.length ? (argv[i + 1] as string) : null;
|
|
78
78
|
}
|
|
79
79
|
|
|
80
|
+
/**
|
|
81
|
+
* Resolve the repository to operate on. `--root` > `AGENT_CONFIG_PROJECT_ROOT` >
|
|
82
|
+
* this module's own tree.
|
|
83
|
+
*
|
|
84
|
+
* The middle rung is the whole point, and its absence was a confused deputy:
|
|
85
|
+
* `_dispatch.bash` parses `--root` GLOBALLY, strips it from argv, and exports
|
|
86
|
+
* `AGENT_CONFIG_PROJECT_ROOT` (`:1655`) — then this engine read neither and fell
|
|
87
|
+
* back to its own module path. So `agent-config code-graph build --root <other
|
|
88
|
+
* repo>` silently indexed THIS package. The dispatcher made the decision and the
|
|
89
|
+
* engine substituted its own, which is why every consumer-facing invocation was
|
|
90
|
+
* wrong about which repository it had just read.
|
|
91
|
+
*
|
|
92
|
+
* `realpathSync` rather than `resolve` alone: a symlinked root must normalise to
|
|
93
|
+
* what will actually be walked, or the path a caller sees in the output is not
|
|
94
|
+
* the path that was indexed. Fail-loud on a root that does not exist — a silent
|
|
95
|
+
* fallback to the package tree is the defect this function exists to remove, and
|
|
96
|
+
* reintroducing it for a typo'd path would be the same bug wearing a smaller
|
|
97
|
+
* hat.
|
|
98
|
+
*
|
|
99
|
+
* Deliberately NOT touching `DEFAULT_CACHE`, which is anchored to `REPO_ROOT`
|
|
100
|
+
* for the same reason and is a separate decision: it is a WRITE path, with its
|
|
101
|
+
* own ownership, cleanup, concurrency and multi-repo namespacing questions.
|
|
102
|
+
* Council 2026-08-22, both seats, explicitly out of scope here.
|
|
103
|
+
*/
|
|
104
|
+
function resolveRoot(argv: string[]): string {
|
|
105
|
+
const explicit = flag(argv, '--root');
|
|
106
|
+
const fromDispatcher = process.env['AGENT_CONFIG_PROJECT_ROOT'];
|
|
107
|
+
const candidate =
|
|
108
|
+
explicit ?? (fromDispatcher !== undefined && fromDispatcher !== '' ? fromDispatcher : REPO_ROOT);
|
|
109
|
+
try {
|
|
110
|
+
return fs.realpathSync(path.resolve(candidate));
|
|
111
|
+
} catch {
|
|
112
|
+
// Returned unresolved on purpose: every caller checks existence and
|
|
113
|
+
// prints the path it was given, which is more useful in the error than
|
|
114
|
+
// a normalised form of a path that does not exist.
|
|
115
|
+
return path.resolve(candidate);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
80
119
|
async function cmdBuild(argv: string[]): Promise<number> {
|
|
81
|
-
const root =
|
|
120
|
+
const root = resolveRoot(argv);
|
|
82
121
|
const out = argv.includes('--out') ? flag(argv, '--out') : DEFAULT_CACHE;
|
|
83
122
|
if (!fs.existsSync(root)) {
|
|
84
123
|
process.stderr.write(`code-graph: root not found: ${root}\n`);
|
|
@@ -122,7 +161,7 @@ function cmdValidate(argv: string[]): number {
|
|
|
122
161
|
|
|
123
162
|
function resolveGraph(argv: string[]): { g: LoadedGraph; note: string } | { err: string } {
|
|
124
163
|
const explicit = flag(argv, '--graph');
|
|
125
|
-
const root =
|
|
164
|
+
const root = resolveRoot(argv);
|
|
126
165
|
if (explicit) {
|
|
127
166
|
if (!fs.existsSync(explicit)) return { err: `graph not found: ${explicit}` };
|
|
128
167
|
return { g: loadGraph(explicit, path.relative(REPO_ROOT, explicit)), note: '' };
|
|
@@ -179,7 +218,7 @@ function cmdQuery(kind: 'query' | 'explain' | 'affected' | 'path', argv: string[
|
|
|
179
218
|
const positional = argv.filter((a) => !a.startsWith('--') && !isFlagValue(argv, a));
|
|
180
219
|
const budget = budgetOf(argv);
|
|
181
220
|
if (kind === 'affected' && flag(argv, '--since')) {
|
|
182
|
-
const root =
|
|
221
|
+
const root = resolveRoot(argv);
|
|
183
222
|
const seeds = changedNodeSeeds(r.g, root, flag(argv, '--since') as string);
|
|
184
223
|
const merged: QueryResult = { source: r.g.source, seeds: [], lines: [], truncated: false, recommended_reads: [] };
|
|
185
224
|
const perSeed: QueryResult[] = [];
|
|
@@ -216,7 +255,7 @@ function isFlagValue(argv: string[], token: string): boolean {
|
|
|
216
255
|
}
|
|
217
256
|
|
|
218
257
|
function cmdDetect(argv: string[]): number {
|
|
219
|
-
const root =
|
|
258
|
+
const root = resolveRoot(argv);
|
|
220
259
|
if (flag(argv, '--format') === 'json') {
|
|
221
260
|
// Machine-readable three-state verdict (Phase 2). Existing human-text
|
|
222
261
|
// output below is unchanged for existing consumers.
|
|
@@ -316,7 +355,7 @@ function runBudgetedBuild(root: string, out: string, update: boolean, budgetSeco
|
|
|
316
355
|
}
|
|
317
356
|
|
|
318
357
|
function cmdRefresh(argv: string[]): number {
|
|
319
|
-
const root =
|
|
358
|
+
const root = resolveRoot(argv);
|
|
320
359
|
const out = flag(argv, '--out') ?? DEFAULT_CACHE;
|
|
321
360
|
const budgetSeconds = budgetSecondsOf(argv);
|
|
322
361
|
if (!fs.existsSync(root)) {
|
|
@@ -306,6 +306,93 @@ function extractTsJs(root: TsNode, file: string, out: FileExtract): void {
|
|
|
306
306
|
if (body) for (const c of namedChildren(body)) walk(c, classId, id);
|
|
307
307
|
return;
|
|
308
308
|
}
|
|
309
|
+
// Modern TS declares most of its functions as bindings, not as
|
|
310
|
+
// `function` statements. Before this case the extractor produced
|
|
311
|
+
// NO node for `export const f = () => {}` or
|
|
312
|
+
// `const g = function () {}`, which is the measured cause of the
|
|
313
|
+
// 170-vs-13,428 TS/PHP symbol gap: the six handled kinds simply do
|
|
314
|
+
// not cover the dominant declaration form.
|
|
315
|
+
//
|
|
316
|
+
// Scoped deliberately to bindings whose value IS a function. A
|
|
317
|
+
// `const x = 3` is data, not a symbol this graph answers questions
|
|
318
|
+
// about, and emitting a node per constant would inflate the count
|
|
319
|
+
// without improving recall — which is the cosmetic-improvement
|
|
320
|
+
// failure the phase falsifier exists to catch.
|
|
321
|
+
case 'lexical_declaration':
|
|
322
|
+
case 'variable_declaration': {
|
|
323
|
+
for (const d of namedChildren(n)) {
|
|
324
|
+
if (d.type !== 'variable_declarator') continue;
|
|
325
|
+
const value = d.childForFieldName('value');
|
|
326
|
+
if (!value || (value.type !== 'arrow_function' && value.type !== 'function_expression')) {
|
|
327
|
+
continue;
|
|
328
|
+
}
|
|
329
|
+
const nm = nameField(d) ?? '<anon>';
|
|
330
|
+
const id = classId ? `${classId}::${nm}` : `${file}#${nm}`;
|
|
331
|
+
out.nodes.push({
|
|
332
|
+
id,
|
|
333
|
+
label: nm,
|
|
334
|
+
kind: classId ? 'method' : 'function',
|
|
335
|
+
source_file: file,
|
|
336
|
+
source_location: loc(d),
|
|
337
|
+
});
|
|
338
|
+
out.rawEdges.push({
|
|
339
|
+
sourceId: classId ?? fileId,
|
|
340
|
+
relation: 'member',
|
|
341
|
+
targetName: nm,
|
|
342
|
+
confidenceHint: 'EXTRACTED',
|
|
343
|
+
});
|
|
344
|
+
// Walk the body under the BINDING's scope, so a call inside
|
|
345
|
+
// an arrow function attributes to that function rather than
|
|
346
|
+
// to the enclosing file — the same treatment
|
|
347
|
+
// `function_declaration` gets.
|
|
348
|
+
//
|
|
349
|
+
// `walk(body)` and not `namedChildren(body)`: an
|
|
350
|
+
// expression-bodied arrow (`() => helper()`) has the CALL
|
|
351
|
+
// itself as its body, so descending one level first steps
|
|
352
|
+
// straight past it and the edge is lost. The sibling cases
|
|
353
|
+
// can iterate children because a `statement_block` is never
|
|
354
|
+
// the interesting node; here it often is.
|
|
355
|
+
const body = value.childForFieldName('body');
|
|
356
|
+
if (body) walk(body, classId, id);
|
|
357
|
+
}
|
|
358
|
+
// Non-function declarators still need walking for their calls.
|
|
359
|
+
for (const d of namedChildren(n)) {
|
|
360
|
+
if (d.type !== 'variable_declarator') continue;
|
|
361
|
+
const value = d.childForFieldName('value');
|
|
362
|
+
if (value && (value.type === 'arrow_function' || value.type === 'function_expression')) {
|
|
363
|
+
continue;
|
|
364
|
+
}
|
|
365
|
+
walk(d, classId, scopeId);
|
|
366
|
+
}
|
|
367
|
+
return;
|
|
368
|
+
}
|
|
369
|
+
// `class C { m = () => 1 }` — a class property holding a function.
|
|
370
|
+
// Emitted as a `method`, the same kind `method_definition` emits,
|
|
371
|
+
// because it is one to every caller.
|
|
372
|
+
case 'public_field_definition': {
|
|
373
|
+
const value = n.childForFieldName('value');
|
|
374
|
+
const nm = nameField(n) ?? '<anon>';
|
|
375
|
+
if (value && (value.type === 'arrow_function' || value.type === 'function_expression')) {
|
|
376
|
+
const id = classId ? `${classId}::${nm}` : `${file}#${nm}`;
|
|
377
|
+
out.nodes.push({
|
|
378
|
+
id,
|
|
379
|
+
label: nm,
|
|
380
|
+
kind: 'method',
|
|
381
|
+
source_file: file,
|
|
382
|
+
source_location: loc(n),
|
|
383
|
+
});
|
|
384
|
+
out.rawEdges.push({
|
|
385
|
+
sourceId: classId ?? fileId,
|
|
386
|
+
relation: 'member',
|
|
387
|
+
targetName: nm,
|
|
388
|
+
confidenceHint: 'EXTRACTED',
|
|
389
|
+
});
|
|
390
|
+
const body = value.childForFieldName('body');
|
|
391
|
+
if (body) walk(body, classId, id);
|
|
392
|
+
return;
|
|
393
|
+
}
|
|
394
|
+
break;
|
|
395
|
+
}
|
|
309
396
|
case 'new_expression': {
|
|
310
397
|
const ctor = n.childForFieldName('constructor');
|
|
311
398
|
if (ctor && (ctor.type === 'identifier' || ctor.type === 'type_identifier' || ctor.type === 'member_expression'))
|