contract-driven-delivery 2.1.2 → 2.2.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/dist/cli/index.js CHANGED
@@ -101,8 +101,9 @@ var init_logger = __esm({
101
101
  // src/utils/mcp-hint.ts
102
102
  function logRecommendedMcpSetup() {
103
103
  log.info("Recommended for AI agents: enable the cdd-kit MCP server.");
104
- log.dim(" MCP server command: cdd-kit");
105
- log.dim(' MCP server args: ["mcp"]');
104
+ log.dim(" Claude Code user-scope setup: claude mcp add --scope user cdd-kit -- cdd-kit mcp");
105
+ log.dim(" Verify in Claude Code: /mcp or `claude mcp list`");
106
+ log.dim(" Note: Claude Code CLI reads MCP servers from ~/.claude.json; ~/.claude/settings.json is not enough.");
106
107
  log.dim(" Tools exposed: cdd_graph_context, cdd_graph_query, cdd_graph_impact, cdd_index_query, cdd_index_impact");
107
108
  log.dim(" Use MCP graph tools before source reads; CLI fallback: cdd-kit graph/index.");
108
109
  }
@@ -118,26 +119,26 @@ var code_map_hook_exports = {};
118
119
  __export(code_map_hook_exports, {
119
120
  installCodeMapHook: () => installCodeMapHook
120
121
  });
121
- import { existsSync as existsSync3, readFileSync as readFileSync2, writeFileSync, chmodSync, mkdirSync as mkdirSync2 } from "fs";
122
- import { join as join4 } from "path";
122
+ import { existsSync as existsSync4, readFileSync as readFileSync3, writeFileSync as writeFileSync2, chmodSync, mkdirSync as mkdirSync2 } from "fs";
123
+ import { join as join5 } from "path";
123
124
  async function installCodeMapHook(cwd) {
124
- const gitDir = join4(cwd, ".git");
125
- if (!existsSync3(gitDir)) {
125
+ const gitDir = join5(cwd, ".git");
126
+ if (!existsSync4(gitDir)) {
126
127
  log.warn("not a git repository (no .git/ found); skipping code-map hook installation");
127
128
  return;
128
129
  }
129
- const hooksDir = join4(gitDir, "hooks");
130
+ const hooksDir = join5(gitDir, "hooks");
130
131
  mkdirSync2(hooksDir, { recursive: true });
131
- const dest = join4(hooksDir, "pre-commit");
132
+ const dest = join5(hooksDir, "pre-commit");
132
133
  let final;
133
- if (!existsSync3(dest)) {
134
+ if (!existsSync4(dest)) {
134
135
  final = `#!/bin/sh
135
136
  set -e
136
137
 
137
138
  ${CODE_MAP_BLOCK}
138
139
  `;
139
140
  } else {
140
- const existing = readFileSync2(dest, "utf8");
141
+ const existing = readFileSync3(dest, "utf8");
141
142
  const startIdx = existing.indexOf(START_MARKER);
142
143
  const endIdx = existing.indexOf(END_MARKER);
143
144
  if (startIdx >= 0 && endIdx > startIdx) {
@@ -149,15 +150,15 @@ ${CODE_MAP_BLOCK}
149
150
  final = trimmed + "\n\n" + CODE_MAP_BLOCK + "\n";
150
151
  }
151
152
  }
152
- writeFileSync(dest, final, "utf8");
153
+ writeFileSync2(dest, final, "utf8");
153
154
  try {
154
155
  chmodSync(dest, 493);
155
156
  } catch {
156
157
  }
157
158
  try {
158
- const cddDir = join4(cwd, ".cdd");
159
+ const cddDir = join5(cwd, ".cdd");
159
160
  mkdirSync2(cddDir, { recursive: true });
160
- writeFileSync(join4(cddDir, ".hooks-installed"), `installed: ${(/* @__PURE__ */ new Date()).toISOString()}
161
+ writeFileSync2(join5(cddDir, ".hooks-installed"), `installed: ${(/* @__PURE__ */ new Date()).toISOString()}
161
162
  `, "utf8");
162
163
  } catch {
163
164
  }
@@ -191,27 +192,217 @@ ${END_MARKER}`;
191
192
  }
192
193
  });
193
194
 
194
- // src/utils/provider.ts
195
- import { existsSync as existsSync5, readFileSync as readFileSync4 } from "fs";
195
+ // src/commands/install-agent-hooks.ts
196
+ var install_agent_hooks_exports = {};
197
+ __export(install_agent_hooks_exports, {
198
+ installAgentHooks: () => installAgentHooks
199
+ });
200
+ import { existsSync as existsSync5, readFileSync as readFileSync4, writeFileSync as writeFileSync3, copyFileSync as copyFileSync2, chmodSync as chmodSync2, mkdirSync as mkdirSync3 } from "fs";
196
201
  import { join as join6 } from "path";
202
+ async function installAgentHooks(opts = {}) {
203
+ const mode = opts.graphFirst ?? "advisory";
204
+ if (mode !== "advisory" && mode !== "strict") {
205
+ log.error(`invalid mode: ${mode}. Use 'advisory' or 'strict'.`);
206
+ process.exit(1);
207
+ }
208
+ const cwd = process.cwd();
209
+ const srcHook = join6(ASSET.hooks, HOOK_FILENAME);
210
+ if (!existsSync5(srcHook)) {
211
+ if (opts.fromInit) {
212
+ log.warn(`graph-first hook not armed: bundled hook missing (${srcHook}). Reinstall the package, then run \`cdd-kit install-agent-hooks\`.`);
213
+ return;
214
+ }
215
+ log.error(`bundled hook not found: ${srcHook}. Reinstall the cdd-kit package.`);
216
+ process.exit(1);
217
+ }
218
+ const destHook = join6(cwd, HOOK_REL_PATH);
219
+ mkdirSync3(join6(cwd, ".claude", "hooks"), { recursive: true });
220
+ copyFileSync2(srcHook, destHook);
221
+ try {
222
+ chmodSync2(destHook, 493);
223
+ } catch {
224
+ }
225
+ const settingsPath = join6(cwd, SETTINGS_REL_PATH);
226
+ let settings = {};
227
+ if (existsSync5(settingsPath)) {
228
+ try {
229
+ settings = JSON.parse(readFileSync4(settingsPath, "utf8"));
230
+ } catch (err) {
231
+ if (opts.fromInit) {
232
+ log.warn(`graph-first hook not armed: ${SETTINGS_REL_PATH} is not valid JSON (${err.message}). Fix it, then run \`cdd-kit install-agent-hooks\`.`);
233
+ return;
234
+ }
235
+ log.error(`${SETTINGS_REL_PATH} is not valid JSON: ${err.message}. Fix or remove it, then re-run.`);
236
+ process.exit(1);
237
+ }
238
+ if (typeof settings !== "object" || settings === null || Array.isArray(settings)) {
239
+ if (opts.fromInit) {
240
+ log.warn(`graph-first hook not armed: ${SETTINGS_REL_PATH} must be a JSON object. Fix it, then run \`cdd-kit install-agent-hooks\`.`);
241
+ return;
242
+ }
243
+ log.error(`${SETTINGS_REL_PATH} must be a JSON object.`);
244
+ process.exit(1);
245
+ }
246
+ }
247
+ settings.hooks = settings.hooks ?? {};
248
+ const preTool = Array.isArray(settings.hooks.PreToolUse) ? settings.hooks.PreToolUse : [];
249
+ const isOurHandler = (h) => typeof h?.command === "string" && h.command.includes(HOOK_MARKER);
250
+ const preserved = [];
251
+ for (const e of preTool) {
252
+ if (typeof e?.command === "string" && e.command.includes(HOOK_MARKER) && !Array.isArray(e?.hooks)) {
253
+ continue;
254
+ }
255
+ if (Array.isArray(e?.hooks) && e.hooks.some(isOurHandler)) {
256
+ const others = e.hooks.filter((h) => !isOurHandler(h));
257
+ if (others.length === 0)
258
+ continue;
259
+ preserved.push({ ...e, hooks: others });
260
+ continue;
261
+ }
262
+ preserved.push(e);
263
+ }
264
+ const invoke = `./${HOOK_REL_PATH}`;
265
+ const command = mode === "strict" ? `CDD_GRAPH_FIRST_STRICT=1 ${invoke}` : invoke;
266
+ preserved.push({ matcher: "Read", hooks: [{ type: "command", command }] });
267
+ settings.hooks.PreToolUse = preserved;
268
+ mkdirSync3(join6(cwd, ".claude"), { recursive: true });
269
+ writeFileSync3(settingsPath, JSON.stringify(settings, null, 2) + "\n", "utf8");
270
+ log.ok(`graph-first PreToolUse hook installed (${mode}) at ${SETTINGS_REL_PATH}`);
271
+ log.info(`hook script: ${HOOK_REL_PATH}`);
272
+ if (mode === "advisory") {
273
+ log.info("advisory mode: reminds agents to use `cdd-kit index query --with-source` before Read; does not block.");
274
+ log.info("re-run with `--graph-first strict` to hard-block source Reads when a code-map exists.");
275
+ } else {
276
+ log.info("strict mode: blocks source-file Read when .cdd/code-map.yml exists, steering to graph/index queries.");
277
+ log.info("re-run with `--graph-first advisory` to downgrade to reminders only.");
278
+ }
279
+ }
280
+ var HOOK_FILENAME, HOOK_REL_PATH, SETTINGS_REL_PATH, HOOK_MARKER;
281
+ var init_install_agent_hooks = __esm({
282
+ "src/commands/install-agent-hooks.ts"() {
283
+ "use strict";
284
+ init_paths();
285
+ init_logger();
286
+ HOOK_FILENAME = "pre-tool-use-graph-first.sh";
287
+ HOOK_REL_PATH = `.claude/hooks/${HOOK_FILENAME}`;
288
+ SETTINGS_REL_PATH = ".claude/settings.json";
289
+ HOOK_MARKER = "pre-tool-use-graph-first";
290
+ }
291
+ });
292
+
293
+ // src/commands/install-hooks.ts
294
+ var install_hooks_exports = {};
295
+ __export(install_hooks_exports, {
296
+ installHooks: () => installHooks
297
+ });
298
+ import { existsSync as existsSync6, readFileSync as readFileSync5, writeFileSync as writeFileSync4, chmodSync as chmodSync3, mkdirSync as mkdirSync4, statSync as statSync2 } from "fs";
299
+ import { join as join7, resolve } from "path";
300
+ import { spawnSync } from "child_process";
301
+ function resolveHooksDir(cwd, gitPath, fromInit) {
302
+ const res = spawnSync("git", ["rev-parse", "--git-path", "hooks"], { cwd, encoding: "utf8" });
303
+ if (res.status === 0 && res.stdout.trim()) {
304
+ return resolve(cwd, res.stdout.trim());
305
+ }
306
+ let gitIsDir = false;
307
+ try {
308
+ gitIsDir = statSync2(gitPath).isDirectory();
309
+ } catch {
310
+ }
311
+ if (gitIsDir)
312
+ return join7(gitPath, "hooks");
313
+ const why = "`.git` is a worktree/submodule pointer and git could not resolve the hooks path";
314
+ if (fromInit) {
315
+ log.warn(`pre-commit gate not armed: ${why}. Run \`cdd-kit install-hooks\` in the main checkout.`);
316
+ return null;
317
+ }
318
+ log.error(`cannot resolve hooks dir: ${why}. Run this in the main checkout.`);
319
+ process.exit(1);
320
+ }
321
+ async function installHooks(opts = {}) {
322
+ const cwd = process.cwd();
323
+ const gitPath = join7(cwd, ".git");
324
+ if (!existsSync6(gitPath)) {
325
+ if (opts.fromInit) {
326
+ log.warn("pre-commit gate not armed: not a git repository yet. Run `cdd-kit install-hooks` after `git init`.");
327
+ return;
328
+ }
329
+ log.error("not a git repository (no .git/ found in cwd)");
330
+ process.exit(1);
331
+ }
332
+ const hooksDir = resolveHooksDir(cwd, gitPath, opts.fromInit ?? false);
333
+ if (hooksDir === null)
334
+ return;
335
+ mkdirSync4(hooksDir, { recursive: true });
336
+ const dest = join7(hooksDir, "pre-commit");
337
+ const ourHook = readFileSync5(join7(ASSET.hooks, "pre-commit"), "utf8");
338
+ let final;
339
+ if (!existsSync6(dest)) {
340
+ final = ourHook;
341
+ } else {
342
+ const existing = readFileSync5(dest, "utf8");
343
+ const startIdx = existing.indexOf(START_MARKER2);
344
+ const endIdx = existing.indexOf(END_MARKER2);
345
+ if (startIdx >= 0 && endIdx > startIdx) {
346
+ const before = existing.slice(0, startIdx);
347
+ const after = existing.slice(endIdx + END_MARKER2.length);
348
+ const ourStart = ourHook.indexOf(START_MARKER2);
349
+ const ourEnd = ourHook.indexOf(END_MARKER2) + END_MARKER2.length;
350
+ const ourBlock = ourHook.slice(ourStart, ourEnd);
351
+ final = before + ourBlock + after;
352
+ } else {
353
+ const ourStart = ourHook.indexOf(START_MARKER2);
354
+ const ourEnd = ourHook.indexOf(END_MARKER2) + END_MARKER2.length;
355
+ const ourBlock = ourHook.slice(ourStart, ourEnd);
356
+ if (existing.startsWith("#!")) {
357
+ const firstNewline = existing.indexOf("\n");
358
+ const shebang = existing.slice(0, firstNewline + 1);
359
+ const rest = existing.slice(firstNewline + 1);
360
+ final = shebang + "\n" + ourBlock + "\n" + rest;
361
+ } else {
362
+ final = "#!/bin/sh\n" + ourBlock + "\n" + existing;
363
+ }
364
+ }
365
+ }
366
+ writeFileSync4(dest, final, "utf8");
367
+ try {
368
+ chmodSync3(dest, 493);
369
+ } catch {
370
+ }
371
+ log.ok(`pre-commit hook installed at ${dest}`);
372
+ log.info("cdd-kit gate will now run automatically before each commit affecting specs/changes/");
373
+ }
374
+ var START_MARKER2, END_MARKER2;
375
+ var init_install_hooks = __esm({
376
+ "src/commands/install-hooks.ts"() {
377
+ "use strict";
378
+ init_paths();
379
+ init_logger();
380
+ START_MARKER2 = "# cdd-kit-managed-block-start";
381
+ END_MARKER2 = "# cdd-kit-managed-block-end";
382
+ }
383
+ });
384
+
385
+ // src/utils/provider.ts
386
+ import { existsSync as existsSync8, readFileSync as readFileSync7 } from "fs";
387
+ import { join as join9 } from "path";
197
388
  function validateProviderOption(provider) {
198
389
  return provider === "auto" || provider === "claude" || provider === "codex" || provider === "both";
199
390
  }
200
391
  function inferProvider(cwd, requested = "auto") {
201
392
  if (requested !== "auto")
202
393
  return requested;
203
- const modelPolicyPath = join6(cwd, ".cdd", "model-policy.json");
204
- if (existsSync5(modelPolicyPath)) {
394
+ const modelPolicyPath = join9(cwd, ".cdd", "model-policy.json");
395
+ if (existsSync8(modelPolicyPath)) {
205
396
  try {
206
- const policy = JSON.parse(readFileSync4(modelPolicyPath, "utf8"));
397
+ const policy = JSON.parse(readFileSync7(modelPolicyPath, "utf8"));
207
398
  if (policy.provider === "claude" || policy.provider === "codex" || policy.provider === "both") {
208
399
  return policy.provider;
209
400
  }
210
401
  } catch {
211
402
  }
212
403
  }
213
- const hasClaude = existsSync5(join6(cwd, "CLAUDE.md")) || existsSync5(join6(cwd, "AGENTS.md"));
214
- const hasCodex = existsSync5(join6(cwd, "CODEX.md"));
404
+ const hasClaude = existsSync8(join9(cwd, "CLAUDE.md")) || existsSync8(join9(cwd, "AGENTS.md"));
405
+ const hasCodex = existsSync8(join9(cwd, "CODEX.md"));
215
406
  if (hasClaude && hasCodex)
216
407
  return "both";
217
408
  if (hasCodex)
@@ -225,27 +416,27 @@ var init_provider = __esm({
225
416
  });
226
417
 
227
418
  // src/commands/update.ts
228
- import { join as join7 } from "path";
229
- import { existsSync as existsSync6, mkdirSync as mkdirSync3, readdirSync as readdirSync3, copyFileSync as copyFileSync2, readFileSync as readFileSync5 } from "fs";
419
+ import { join as join10 } from "path";
420
+ import { existsSync as existsSync9, mkdirSync as mkdirSync5, readdirSync as readdirSync3, copyFileSync as copyFileSync3, readFileSync as readFileSync8 } from "fs";
230
421
  import { createHash } from "crypto";
231
422
  import { homedir as homedir2 } from "os";
232
423
  function fileHash(filePath) {
233
- const buf = readFileSync5(filePath);
424
+ const buf = readFileSync8(filePath);
234
425
  return createHash("sha256").update(buf).digest("hex");
235
426
  }
236
427
  function diffDir(src, dest) {
237
428
  const entries = [];
238
- if (!existsSync6(src))
429
+ if (!existsSync9(src))
239
430
  return entries;
240
431
  function walk(currentSrc, currentDest) {
241
432
  const items = readdirSync3(currentSrc, { withFileTypes: true });
242
433
  for (const item of items) {
243
- const srcPath = join7(currentSrc, item.name);
244
- const destPath = join7(currentDest, item.name);
434
+ const srcPath = join10(currentSrc, item.name);
435
+ const destPath = join10(currentDest, item.name);
245
436
  if (item.isDirectory()) {
246
437
  walk(srcPath, destPath);
247
438
  } else {
248
- if (!existsSync6(destPath)) {
439
+ if (!existsSync9(destPath)) {
249
440
  entries.push({ src: srcPath, dest: destPath, action: "add" });
250
441
  } else if (fileHash(srcPath) !== fileHash(destPath)) {
251
442
  entries.push({ src: srcPath, dest: destPath, action: "overwrite" });
@@ -263,33 +454,33 @@ function applyDir(entries) {
263
454
  for (const e of entries) {
264
455
  if (e.action === "skip")
265
456
  continue;
266
- mkdirSync3(join7(e.dest, ".."), { recursive: true });
267
- copyFileSync2(e.src, e.dest);
457
+ mkdirSync5(join10(e.dest, ".."), { recursive: true });
458
+ copyFileSync3(e.src, e.dest);
268
459
  count += 1;
269
460
  }
270
461
  return count;
271
462
  }
272
463
  function backupDir(dir, backupDest) {
273
- if (!existsSync6(dir))
464
+ if (!existsSync9(dir))
274
465
  return;
275
- mkdirSync3(backupDest, { recursive: true });
466
+ mkdirSync5(backupDest, { recursive: true });
276
467
  function walk(src, dst) {
277
468
  const items = readdirSync3(src, { withFileTypes: true });
278
469
  for (const item of items) {
279
- const s = join7(src, item.name);
280
- const d = join7(dst, item.name);
470
+ const s = join10(src, item.name);
471
+ const d = join10(dst, item.name);
281
472
  if (item.isDirectory()) {
282
- mkdirSync3(d, { recursive: true });
473
+ mkdirSync5(d, { recursive: true });
283
474
  walk(s, d);
284
475
  } else
285
- copyFileSync2(s, d);
476
+ copyFileSync3(s, d);
286
477
  }
287
478
  }
288
479
  walk(dir, backupDest);
289
480
  }
290
481
  async function update(opts) {
291
482
  if (opts.postinstall) {
292
- if (!existsSync6(join7(SKILLS_HOME, "contract-driven-delivery"))) {
483
+ if (!existsSync9(join10(SKILLS_HOME, "contract-driven-delivery"))) {
293
484
  return;
294
485
  }
295
486
  opts.yes = true;
@@ -307,7 +498,7 @@ async function update(opts) {
307
498
  const provider = inferProvider(cwd, requestedProvider);
308
499
  const updateClaudeAssets = provider === "claude" || provider === "both";
309
500
  const agentDiff = updateClaudeAssets ? diffDir(ASSET.agents, AGENTS_HOME) : [];
310
- const skillDiff = updateClaudeAssets ? readdirSync3(ASSET.skills, { withFileTypes: true }).filter((d) => d.isDirectory()).flatMap((d) => diffDir(join7(ASSET.skills, d.name), join7(SKILLS_HOME, d.name))) : [];
501
+ const skillDiff = updateClaudeAssets ? readdirSync3(ASSET.skills, { withFileTypes: true }).filter((d) => d.isDirectory()).flatMap((d) => diffDir(join10(ASSET.skills, d.name), join10(SKILLS_HOME, d.name))) : [];
311
502
  const toWrite = [...agentDiff, ...skillDiff].filter((e) => e.action !== "skip");
312
503
  const toAdd = toWrite.filter((e) => e.action === "add");
313
504
  const toOver = toWrite.filter((e) => e.action === "overwrite");
@@ -345,13 +536,13 @@ async function update(opts) {
345
536
  return;
346
537
  }
347
538
  const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
348
- const backupRoot = join7(homedir2(), ".claude", ".cdd-kit-backup", timestamp);
539
+ const backupRoot = join10(homedir2(), ".claude", ".cdd-kit-backup", timestamp);
349
540
  if (!quiet) {
350
541
  log.blank();
351
542
  log.info(`Backing up to ${backupRoot} \u2026`);
352
543
  }
353
- backupDir(AGENTS_HOME, join7(backupRoot, "agents"));
354
- backupDir(SKILLS_HOME, join7(backupRoot, "skills"));
544
+ backupDir(AGENTS_HOME, join10(backupRoot, "agents"));
545
+ backupDir(SKILLS_HOME, join10(backupRoot, "skills"));
355
546
  if (!quiet)
356
547
  log.ok(`Backup complete: ${backupRoot}`);
357
548
  if (!quiet)
@@ -392,7 +583,7 @@ var init_update = __esm({
392
583
  });
393
584
 
394
585
  // src/utils/digest.ts
395
- import { readFileSync as readFileSync6 } from "fs";
586
+ import { readFileSync as readFileSync9 } from "fs";
396
587
  import { createHash as createHash2 } from "crypto";
397
588
  function normalizeContentForHash(buf) {
398
589
  if (!buf.includes(13))
@@ -403,7 +594,7 @@ function normalizeContentForHash(buf) {
403
594
  function sha256OfFileNormalized(path) {
404
595
  let buf;
405
596
  try {
406
- buf = readFileSync6(path);
597
+ buf = readFileSync9(path);
407
598
  } catch {
408
599
  return "";
409
600
  }
@@ -420,9 +611,9 @@ var context_scan_exports = {};
420
611
  __export(context_scan_exports, {
421
612
  contextScan: () => contextScan
422
613
  });
423
- import { existsSync as existsSync7, mkdirSync as mkdirSync4, readFileSync as readFileSync7, readdirSync as readdirSync4, writeFileSync as writeFileSync3 } from "fs";
614
+ import { existsSync as existsSync10, mkdirSync as mkdirSync6, readFileSync as readFileSync10, readdirSync as readdirSync4, writeFileSync as writeFileSync6 } from "fs";
424
615
  import { createHash as createHash3 } from "crypto";
425
- import { basename, dirname as dirname3, join as join8, relative as relative2 } from "path";
616
+ import { basename, dirname as dirname3, join as join11, relative as relative2 } from "path";
426
617
  function inputsDigest(paths, cwd) {
427
618
  const combined = paths.slice().sort().map((p) => {
428
619
  const rel = relative2(cwd, p).replace(/\\/g, "/");
@@ -435,10 +626,10 @@ function stripGlobSuffix(pattern) {
435
626
  }
436
627
  function getForbiddenPaths(cwd) {
437
628
  const forbidden = new Set(DEFAULT_FORBIDDEN);
438
- const policyPath = join8(cwd, ".cdd", "context-policy.json");
629
+ const policyPath = join11(cwd, ".cdd", "context-policy.json");
439
630
  try {
440
- if (existsSync7(policyPath)) {
441
- const policy = JSON.parse(readFileSync7(policyPath, "utf8"));
631
+ if (existsSync10(policyPath)) {
632
+ const policy = JSON.parse(readFileSync10(policyPath, "utf8"));
442
633
  for (const pattern of policy.forbiddenPaths ?? []) {
443
634
  forbidden.add(stripGlobSuffix(pattern));
444
635
  }
@@ -467,7 +658,7 @@ function buildTree(dir, cwd, forbidden, stats, prefix = "", depth = 0) {
467
658
  });
468
659
  let output = "";
469
660
  const visible = entries.filter((entry) => {
470
- const relPath = relative2(cwd, join8(dir, entry.name));
661
+ const relPath = relative2(cwd, join11(dir, entry.name));
471
662
  return !isForbidden(relPath, forbidden);
472
663
  });
473
664
  const truncated = visible.length > PER_DIR_ENTRY_CAP;
@@ -475,7 +666,7 @@ function buildTree(dir, cwd, forbidden, stats, prefix = "", depth = 0) {
475
666
  if (truncated)
476
667
  stats.truncatedDirs += 1;
477
668
  shown.forEach((entry, index2) => {
478
- const fullPath = join8(dir, entry.name);
669
+ const fullPath = join11(dir, entry.name);
479
670
  const isLast = index2 === shown.length - 1 && !truncated;
480
671
  const connector = isLast ? "\\-- " : "|-- ";
481
672
  output += `${prefix}${connector}${entry.name}${entry.isDirectory() ? "/" : ""}
@@ -537,10 +728,10 @@ function parseContractMetadata(content) {
537
728
  return { title: firstHeading(content), summary, metadata };
538
729
  }
539
730
  function findContractFiles(dir, found = []) {
540
- if (!existsSync7(dir))
731
+ if (!existsSync10(dir))
541
732
  return found;
542
733
  for (const entry of readdirSync4(dir, { withFileTypes: true })) {
543
- const fullPath = join8(dir, entry.name);
734
+ const fullPath = join11(dir, entry.name);
544
735
  if (entry.isDirectory())
545
736
  findContractFiles(fullPath, found);
546
737
  else if (entry.isFile() && entry.name.endsWith(".md") && entry.name !== "INDEX.md" && entry.name !== "CHANGELOG.md")
@@ -550,14 +741,14 @@ function findContractFiles(dir, found = []) {
550
741
  }
551
742
  async function contextScan(opts = {}) {
552
743
  const cwd = process.cwd();
553
- const specsContextDir = join8(cwd, "specs", "context");
554
- mkdirSync4(specsContextDir, { recursive: true });
744
+ const specsContextDir = join11(cwd, "specs", "context");
745
+ mkdirSync6(specsContextDir, { recursive: true });
555
746
  const forbidden = getForbiddenPaths(cwd);
556
747
  const surface = opts.surface;
557
748
  let scanRoot = cwd;
558
749
  if (surface) {
559
- const resolvedSurface = join8(cwd, surface);
560
- if (!existsSync7(resolvedSurface)) {
750
+ const resolvedSurface = join11(cwd, surface);
751
+ if (!existsSync10(resolvedSurface)) {
561
752
  log.error(`--surface path not found: ${surface}`);
562
753
  process.exit(1);
563
754
  }
@@ -569,10 +760,10 @@ async function contextScan(opts = {}) {
569
760
  }
570
761
  const treeStats = { dirs: 0, files: 0, omittedDirs: 0, truncatedDirs: 0 };
571
762
  const tree = buildTree(scanRoot, cwd, forbidden, treeStats);
572
- const policyPath = join8(cwd, ".cdd", "context-policy.json");
573
- const projectMapInputs = [policyPath].filter(existsSync7);
574
- writeFileSync3(
575
- join8(specsContextDir, "project-map.md"),
763
+ const policyPath = join11(cwd, ".cdd", "context-policy.json");
764
+ const projectMapInputs = [policyPath].filter(existsSync10);
765
+ writeFileSync6(
766
+ join11(specsContextDir, "project-map.md"),
576
767
  [
577
768
  "---",
578
769
  "artifact: project-map",
@@ -605,14 +796,14 @@ async function contextScan(opts = {}) {
605
796
  "utf8"
606
797
  );
607
798
  log.ok("Created specs/context/project-map.md");
608
- const contractFiles = findContractFiles(join8(cwd, "contracts")).sort((a, b) => relative2(cwd, a).localeCompare(relative2(cwd, b)));
799
+ const contractFiles = findContractFiles(join11(cwd, "contracts")).sort((a, b) => relative2(cwd, a).localeCompare(relative2(cwd, b)));
609
800
  const contractEntries = [];
610
801
  const inventoryRows = [];
611
802
  let missingSummary = 0;
612
803
  for (const file of contractFiles) {
613
804
  const relPath = relative2(cwd, file).replace(/\\/g, "/");
614
805
  const dir = dirname3(relPath).replace(/\\/g, "/");
615
- const { title, summary, metadata } = parseContractMetadata(readFileSync7(file, "utf8"));
806
+ const { title, summary, metadata } = parseContractMetadata(readFileSync10(file, "utf8"));
616
807
  const contractType = deriveContractType(relPath, metadata);
617
808
  const owner = metadata.owner ?? "unknown";
618
809
  const surface2 = metadata.surface ?? dir;
@@ -667,7 +858,7 @@ async function contextScan(opts = {}) {
667
858
  "",
668
859
  ...contractEntries
669
860
  ].join("\n");
670
- writeFileSync3(join8(specsContextDir, "contracts-index.md"), contractIndex, "utf8");
861
+ writeFileSync6(join11(specsContextDir, "contracts-index.md"), contractIndex, "utf8");
671
862
  if (missingSummary > 0) {
672
863
  log.warn(`Created specs/context/contracts-index.md with ${missingSummary} missing summary warning(s).`);
673
864
  } else {
@@ -3694,7 +3885,7 @@ var require_compile = __commonJS({
3694
3885
  const schOrFunc = root.refs[ref];
3695
3886
  if (schOrFunc)
3696
3887
  return schOrFunc;
3697
- let _sch = resolve2.call(this, root, ref);
3888
+ let _sch = resolve6.call(this, root, ref);
3698
3889
  if (_sch === void 0) {
3699
3890
  const schema = (_a = root.localRefs) === null || _a === void 0 ? void 0 : _a[ref];
3700
3891
  const { schemaId } = this.opts;
@@ -3721,7 +3912,7 @@ var require_compile = __commonJS({
3721
3912
  function sameSchemaEnv(s1, s2) {
3722
3913
  return s1.schema === s2.schema && s1.root === s2.root && s1.baseId === s2.baseId;
3723
3914
  }
3724
- function resolve2(root, ref) {
3915
+ function resolve6(root, ref) {
3725
3916
  let sch;
3726
3917
  while (typeof (sch = this.refs[ref]) == "string")
3727
3918
  ref = sch;
@@ -4297,7 +4488,7 @@ var require_fast_uri = __commonJS({
4297
4488
  }
4298
4489
  return uri;
4299
4490
  }
4300
- function resolve2(baseURI, relativeURI, options) {
4491
+ function resolve6(baseURI, relativeURI, options) {
4301
4492
  const schemelessOptions = options ? Object.assign({ scheme: "null" }, options) : { scheme: "null" };
4302
4493
  const resolved = resolveComponent(parse3(baseURI, schemelessOptions), parse3(relativeURI, schemelessOptions), schemelessOptions, true);
4303
4494
  schemelessOptions.skipEscape = true;
@@ -4525,7 +4716,7 @@ var require_fast_uri = __commonJS({
4525
4716
  var fastUri = {
4526
4717
  SCHEMES,
4527
4718
  normalize,
4528
- resolve: resolve2,
4719
+ resolve: resolve6,
4529
4720
  resolveComponent,
4530
4721
  equal,
4531
4722
  serialize,
@@ -7514,18 +7705,265 @@ var require_dist = __commonJS({
7514
7705
  }
7515
7706
  });
7516
7707
 
7708
+ // src/utils/tier-floor.ts
7709
+ import { existsSync as existsSync13, readFileSync as readFileSync12 } from "fs";
7710
+ import { join as join14 } from "path";
7711
+ function loadTierPolicy(cwd) {
7712
+ const path = join14(cwd, ".cdd", "tier-policy.json");
7713
+ if (!existsSync13(path))
7714
+ return DEFAULT_TIER_POLICY;
7715
+ try {
7716
+ const raw = JSON.parse(readFileSync12(path, "utf8"));
7717
+ if (typeof raw.enabled === "boolean" && raw.enabled === false) {
7718
+ return { enabled: false, rules: [] };
7719
+ }
7720
+ if (!Array.isArray(raw.rules))
7721
+ return DEFAULT_TIER_POLICY;
7722
+ const rules = [];
7723
+ for (const r of raw.rules) {
7724
+ if (r && typeof r === "object" && typeof r.maxTier === "number" && Array.isArray(r.patterns)) {
7725
+ const rule = r;
7726
+ rules.push({
7727
+ maxTier: rule.maxTier,
7728
+ label: typeof rule.label === "string" ? rule.label : `tier ${rule.maxTier} surface`,
7729
+ patterns: rule.patterns.filter((p) => typeof p === "string")
7730
+ });
7731
+ }
7732
+ }
7733
+ if (rules.length === 0)
7734
+ return DEFAULT_TIER_POLICY;
7735
+ return { enabled: raw.enabled !== false, rules };
7736
+ } catch {
7737
+ return DEFAULT_TIER_POLICY;
7738
+ }
7739
+ }
7740
+ function patternToRegExp(pattern) {
7741
+ try {
7742
+ return new RegExp(`(?<![a-z0-9])(?:${pattern})(?![a-z0-9])`, "i");
7743
+ } catch {
7744
+ return null;
7745
+ }
7746
+ }
7747
+ function computeTierFloor(text, policy = DEFAULT_TIER_POLICY, opts = {}) {
7748
+ if (!policy.enabled) {
7749
+ return { floorTier: null, label: null, matched: [] };
7750
+ }
7751
+ let floorTier = null;
7752
+ let label = null;
7753
+ const matched = /* @__PURE__ */ new Set();
7754
+ const scan = (rules, haystack) => {
7755
+ if (!haystack)
7756
+ return;
7757
+ for (const rule of rules) {
7758
+ let ruleHit = false;
7759
+ for (const pattern of rule.patterns) {
7760
+ const re = patternToRegExp(pattern);
7761
+ const hit = re ? re.exec(haystack) : null;
7762
+ if (hit) {
7763
+ ruleHit = true;
7764
+ matched.add(hit[0].toLowerCase().replace(/\s+/g, " ").trim());
7765
+ }
7766
+ }
7767
+ if (ruleHit && (floorTier === null || rule.maxTier < floorTier)) {
7768
+ floorTier = rule.maxTier;
7769
+ label = rule.label;
7770
+ }
7771
+ }
7772
+ };
7773
+ scan(policy.rules, text);
7774
+ const paths = opts.paths ?? [];
7775
+ if (paths.length > 0) {
7776
+ scan(policy.rules.filter((r) => r.maxTier === 0), paths.join("\n"));
7777
+ }
7778
+ return { floorTier, label, matched: [...matched].sort() };
7779
+ }
7780
+ var DEFAULT_TIER_POLICY;
7781
+ var init_tier_floor = __esm({
7782
+ "src/utils/tier-floor.ts"() {
7783
+ "use strict";
7784
+ DEFAULT_TIER_POLICY = {
7785
+ enabled: true,
7786
+ rules: [
7787
+ {
7788
+ maxTier: 0,
7789
+ label: "critical surface (auth / payments / data migration / concurrency / secrets)",
7790
+ patterns: [
7791
+ "auth",
7792
+ "authn",
7793
+ "authz",
7794
+ "authentication",
7795
+ "authorization",
7796
+ "authenticate",
7797
+ "authorize",
7798
+ "authenticated",
7799
+ "authorized",
7800
+ "login",
7801
+ "logout",
7802
+ "sign-?in",
7803
+ "sign-?up",
7804
+ "passwords?",
7805
+ "passwd",
7806
+ "credentials?",
7807
+ "secrets?",
7808
+ "api[- ]?keys?",
7809
+ // Qualified so security tokens trip the floor but design/CSS "tokens" do not.
7810
+ "(access|api|auth|session|bearer|refresh|csrf|id|reset)[- ]?tokens?",
7811
+ "jwt",
7812
+ "oauth",
7813
+ "oidc",
7814
+ "saml",
7815
+ "sessions?",
7816
+ "cookies?",
7817
+ "payments?",
7818
+ "billing",
7819
+ "invoices?",
7820
+ "charges?",
7821
+ "refunds?",
7822
+ "checkout",
7823
+ "stripe",
7824
+ "paypal",
7825
+ "migrations?",
7826
+ "migrate",
7827
+ "alter table",
7828
+ "drop table",
7829
+ "drop column",
7830
+ "schema change",
7831
+ "concurrency",
7832
+ "race condition",
7833
+ "mutex",
7834
+ "deadlock",
7835
+ "transaction isolation",
7836
+ "encrypt",
7837
+ "decrypt",
7838
+ "crypto",
7839
+ "hashing",
7840
+ "rbac",
7841
+ "permissions?",
7842
+ "access control",
7843
+ "privileges?",
7844
+ "pii",
7845
+ "gdpr",
7846
+ "hipaa",
7847
+ "rate limit",
7848
+ "csrf",
7849
+ "xss",
7850
+ "sql injection"
7851
+ ]
7852
+ },
7853
+ {
7854
+ maxTier: 2,
7855
+ label: "behavioral surface (api / data shape / queue / cache / external integration)",
7856
+ patterns: [
7857
+ "endpoint",
7858
+ "route",
7859
+ "api contract",
7860
+ "request schema",
7861
+ "response schema",
7862
+ "pagination",
7863
+ "queue",
7864
+ "worker",
7865
+ "cron",
7866
+ "scheduler",
7867
+ "webhook",
7868
+ "cache",
7869
+ "redis",
7870
+ "database",
7871
+ "query",
7872
+ "index",
7873
+ "external service",
7874
+ "third[- ]?party",
7875
+ "integration",
7876
+ "data shape",
7877
+ "nullable",
7878
+ "breaking change"
7879
+ ]
7880
+ }
7881
+ ]
7882
+ };
7883
+ }
7884
+ });
7885
+
7886
+ // src/utils/git-paths.ts
7887
+ import { spawnSync as spawnSync3 } from "child_process";
7888
+ function unquote(raw) {
7889
+ return raw.trim().replace(/^"(.*)"$/, "$1");
7890
+ }
7891
+ function getTouchedPaths(cwd) {
7892
+ let res;
7893
+ try {
7894
+ res = spawnSync3("git", ["status", "--porcelain", "--untracked-files=all"], { cwd, encoding: "utf8" });
7895
+ } catch {
7896
+ return [];
7897
+ }
7898
+ if (res.status !== 0 || !res.stdout)
7899
+ return [];
7900
+ const paths = [];
7901
+ for (const line of res.stdout.split("\n")) {
7902
+ if (!line.trim())
7903
+ continue;
7904
+ const body = line.slice(3);
7905
+ const arrow = body.indexOf(" -> ");
7906
+ if (arrow >= 0) {
7907
+ for (const part of [body.slice(0, arrow), body.slice(arrow + 4)]) {
7908
+ const clean = unquote(part);
7909
+ if (clean)
7910
+ paths.push(clean);
7911
+ }
7912
+ } else {
7913
+ const clean = unquote(body);
7914
+ if (clean)
7915
+ paths.push(clean);
7916
+ }
7917
+ }
7918
+ return paths;
7919
+ }
7920
+ function getStagedPaths(cwd) {
7921
+ let res;
7922
+ try {
7923
+ res = spawnSync3("git", ["diff", "--cached", "--name-status", "--no-color"], { cwd, encoding: "utf8" });
7924
+ } catch {
7925
+ return [];
7926
+ }
7927
+ if (res.status !== 0 || !res.stdout)
7928
+ return [];
7929
+ const paths = [];
7930
+ for (const line of res.stdout.split("\n")) {
7931
+ if (!line.trim())
7932
+ continue;
7933
+ const parts = line.split(" ");
7934
+ const status = parts[0] ?? "";
7935
+ if (/^[RC]/.test(status) && parts.length >= 3) {
7936
+ for (const part of [parts[1], parts[2]]) {
7937
+ const clean = unquote(part ?? "");
7938
+ if (clean)
7939
+ paths.push(clean);
7940
+ }
7941
+ } else if (parts[1]) {
7942
+ const clean = unquote(parts[1]);
7943
+ if (clean)
7944
+ paths.push(clean);
7945
+ }
7946
+ }
7947
+ return paths;
7948
+ }
7949
+ var init_git_paths = __esm({
7950
+ "src/utils/git-paths.ts"() {
7951
+ "use strict";
7952
+ }
7953
+ });
7954
+
7517
7955
  // src/utils/gitignore.ts
7518
- import { existsSync as existsSync12, readFileSync as readFileSync11, writeFileSync as writeFileSync6 } from "fs";
7519
- import { join as join13 } from "path";
7956
+ import { existsSync as existsSync16, readFileSync as readFileSync15, writeFileSync as writeFileSync9 } from "fs";
7957
+ import { join as join16 } from "path";
7520
7958
  function ensureGitignoreEntry(cwd, entry, comment = "cdd-kit generated backups (do not commit)") {
7521
- const path = join13(cwd, ".gitignore");
7959
+ const path = join16(cwd, ".gitignore");
7522
7960
  const trimmed = entry.trim();
7523
7961
  if (!trimmed)
7524
7962
  return false;
7525
7963
  const re = new RegExp(`^\\s*${trimmed.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\s*$`, "m");
7526
7964
  let existing = "";
7527
- if (existsSync12(path))
7528
- existing = readFileSync11(path, "utf8");
7965
+ if (existsSync16(path))
7966
+ existing = readFileSync15(path, "utf8");
7529
7967
  if (re.test(existing))
7530
7968
  return false;
7531
7969
  const sep = existing.length > 0 && !existing.endsWith("\n") ? "\n" : "";
@@ -7535,7 +7973,7 @@ ${trimmed}
7535
7973
  # ${comment}
7536
7974
  ${trimmed}
7537
7975
  `;
7538
- writeFileSync6(path, existing + block, "utf8");
7976
+ writeFileSync9(path, existing + block, "utf8");
7539
7977
  return true;
7540
7978
  }
7541
7979
  var init_gitignore = __esm({
@@ -7549,15 +7987,15 @@ var migrate_exports = {};
7549
7987
  __export(migrate_exports, {
7550
7988
  migrate: () => migrate
7551
7989
  });
7552
- import { join as join14 } from "path";
7553
- import { cpSync as cpSync2, existsSync as existsSync13, mkdirSync as mkdirSync6, readdirSync as readdirSync7, readFileSync as readFileSync12, renameSync, rmSync as rmSync2, writeFileSync as writeFileSync7 } from "fs";
7990
+ import { join as join17 } from "path";
7991
+ import { cpSync as cpSync2, existsSync as existsSync17, mkdirSync as mkdirSync8, readdirSync as readdirSync7, readFileSync as readFileSync16, renameSync, rmSync as rmSync2, writeFileSync as writeFileSync10 } from "fs";
7554
7992
  import yaml2 from "js-yaml";
7555
7993
  function backupChangeDir(cwd, changeId, sessionStamp) {
7556
- const backupRoot = join14(cwd, ".cdd", "migrate-backup", sessionStamp);
7557
- const backupDir2 = join14(backupRoot, changeId);
7558
- mkdirSync6(backupRoot, { recursive: true });
7559
- const sourceDir = join14(cwd, "specs", "changes", changeId);
7560
- if (existsSync13(sourceDir)) {
7994
+ const backupRoot = join17(cwd, ".cdd", "migrate-backup", sessionStamp);
7995
+ const backupDir2 = join17(backupRoot, changeId);
7996
+ mkdirSync8(backupRoot, { recursive: true });
7997
+ const sourceDir = join17(cwd, "specs", "changes", changeId);
7998
+ if (existsSync17(sourceDir)) {
7561
7999
  cpSync2(sourceDir, backupDir2, { recursive: true });
7562
8000
  }
7563
8001
  return backupDir2;
@@ -7690,16 +8128,16 @@ function parseLegacyTaskList(body) {
7690
8128
  return rows;
7691
8129
  }
7692
8130
  function migrateTasksFile(changeId, changeDir, enableContextGovernance, detectedTier, changed, warnings, pendingWrites, pendingDeletes) {
7693
- const newPath = join14(changeDir, "tasks.yml");
7694
- const legacyPath = join14(changeDir, "tasks.md");
7695
- if (existsSync13(newPath)) {
8131
+ const newPath = join17(changeDir, "tasks.yml");
8132
+ const legacyPath = join17(changeDir, "tasks.md");
8133
+ if (existsSync17(newPath)) {
7696
8134
  return;
7697
8135
  }
7698
- if (!existsSync13(legacyPath)) {
8136
+ if (!existsSync17(legacyPath)) {
7699
8137
  warnings.push("tasks.md not found and tasks.yml missing \u2014 skipping tasks migration");
7700
8138
  return;
7701
8139
  }
7702
- const raw = readFileSync12(legacyPath, "utf8");
8140
+ const raw = readFileSync16(legacyPath, "utf8");
7703
8141
  const fm = parseLegacyFrontmatter(raw);
7704
8142
  const bodyMatch = raw.match(/^---\r?\n[\s\S]*?\r?\n---\r?\n?([\s\S]*)$/);
7705
8143
  const body = bodyMatch ? bodyMatch[1] : raw;
@@ -7783,17 +8221,17 @@ function parseLegacyAgentLog(content) {
7783
8221
  return data;
7784
8222
  }
7785
8223
  function migrateAgentLogs(changeDir, changed, pendingWrites, pendingDeletes) {
7786
- const agentLogDir = join14(changeDir, "agent-log");
7787
- if (!existsSync13(agentLogDir))
8224
+ const agentLogDir = join17(changeDir, "agent-log");
8225
+ if (!existsSync17(agentLogDir))
7788
8226
  return;
7789
8227
  const mdLogs = readdirSync7(agentLogDir).filter((f) => f.endsWith(".md"));
7790
8228
  for (const f of mdLogs) {
7791
- const fullPath = join14(agentLogDir, f);
8229
+ const fullPath = join17(agentLogDir, f);
7792
8230
  const yamlName = f.replace(/\.md$/, ".yml");
7793
- const yamlFull = join14(agentLogDir, yamlName);
7794
- if (existsSync13(yamlFull))
8231
+ const yamlFull = join17(agentLogDir, yamlName);
8232
+ if (existsSync17(yamlFull))
7795
8233
  continue;
7796
- const raw = readFileSync12(fullPath, "utf8");
8234
+ const raw = readFileSync16(fullPath, "utf8");
7797
8235
  const parsed = parseLegacyAgentLog(raw);
7798
8236
  const yamlOut = yaml2.dump(parsed, { lineWidth: -1, noRefs: true });
7799
8237
  pendingWrites.push({ path: yamlFull, content: yamlOut });
@@ -7802,15 +8240,15 @@ function migrateAgentLogs(changeDir, changed, pendingWrites, pendingDeletes) {
7802
8240
  }
7803
8241
  }
7804
8242
  function ensureImplementationPlanScaffold(changeId, changeDir, changed, warnings, pendingWrites) {
7805
- const planPath = join14(changeDir, "implementation-plan.md");
7806
- if (existsSync13(planPath))
8243
+ const planPath = join17(changeDir, "implementation-plan.md");
8244
+ if (existsSync17(planPath))
7807
8245
  return;
7808
- const templatePath = join14(ASSET.specsTemplates, "implementation-plan.md");
7809
- if (!existsSync13(templatePath)) {
8246
+ const templatePath = join17(ASSET.specsTemplates, "implementation-plan.md");
8247
+ if (!existsSync17(templatePath)) {
7810
8248
  warnings.push("implementation-plan.md template not found; run cdd-kit upgrade --yes after updating cdd-kit");
7811
8249
  return;
7812
8250
  }
7813
- const template = readFileSync12(templatePath, "utf8").replace(/<change-id>/g, changeId).replace(/<id>/g, changeId);
8251
+ const template = readFileSync16(templatePath, "utf8").replace(/<change-id>/g, changeId).replace(/<id>/g, changeId);
7814
8252
  pendingWrites.push({ path: planPath, content: template });
7815
8253
  changed.push("implementation-plan.md: added scaffold");
7816
8254
  warnings.push("implementation-plan.md scaffold added; fill it before implementation agents continue");
@@ -7821,9 +8259,9 @@ function migrateOne(changeId, changeDir, enableContextGovernance) {
7821
8259
  const pending = [];
7822
8260
  const deletes = [];
7823
8261
  let detectedTier = null;
7824
- const classifPath = join14(changeDir, "change-classification.md");
7825
- if (existsSync13(classifPath)) {
7826
- const content = readFileSync12(classifPath, "utf8");
8262
+ const classifPath = join17(changeDir, "change-classification.md");
8263
+ if (existsSync17(classifPath)) {
8264
+ const content = readFileSync16(classifPath, "utf8");
7827
8265
  const hasNewTierFormat = /^## Tier\s*\n\s*-\s*\d\s*$/m.test(content);
7828
8266
  const oldMatch = content.match(/\*\*Tier[:\*]+\s*(?:Tier\s*)?(\d)/i) ?? content.match(/^-?\s*Tier:\s*(?:Tier\s*)?(\d)/mi);
7829
8267
  if (oldMatch)
@@ -7854,8 +8292,8 @@ function migrateOne(changeId, changeDir, enableContextGovernance) {
7854
8292
  migrateTasksFile(changeId, changeDir, enableContextGovernance, detectedTier, changed, warnings, pending, deletes);
7855
8293
  ensureImplementationPlanScaffold(changeId, changeDir, changed, warnings, pending);
7856
8294
  migrateAgentLogs(changeDir, changed, pending, deletes);
7857
- const manifestPath = join14(changeDir, "context-manifest.md");
7858
- if (!existsSync13(manifestPath)) {
8295
+ const manifestPath = join17(changeDir, "context-manifest.md");
8296
+ if (!existsSync17(manifestPath)) {
7859
8297
  changed.push(enableContextGovernance ? "context-manifest.md: added context-governance v1 manifest scaffold" : "context-manifest.md: added legacy context manifest scaffold");
7860
8298
  pending.push({
7861
8299
  path: manifestPath,
@@ -7871,7 +8309,7 @@ function commitWritesAtomically(pending, deletes) {
7871
8309
  try {
7872
8310
  for (const write of pending) {
7873
8311
  const tmp = `${write.path}.cdd-migrate.tmp`;
7874
- writeFileSync7(tmp, write.content, "utf8");
8312
+ writeFileSync10(tmp, write.content, "utf8");
7875
8313
  renames.push({ tmp, final: write.path });
7876
8314
  }
7877
8315
  } catch (err) {
@@ -7900,8 +8338,8 @@ async function migrate(changeId, opts = {}) {
7900
8338
  const noBackup = opts.noBackup ?? false;
7901
8339
  const idsToMigrate = [];
7902
8340
  if (opts.all) {
7903
- const changesDir = join14(cwd, "specs", "changes");
7904
- if (!existsSync13(changesDir)) {
8341
+ const changesDir = join17(cwd, "specs", "changes");
8342
+ if (!existsSync17(changesDir)) {
7905
8343
  log.info("No specs/changes/ directory found \u2014 nothing to migrate.");
7906
8344
  return;
7907
8345
  }
@@ -7909,8 +8347,8 @@ async function migrate(changeId, opts = {}) {
7909
8347
  ...readdirSync7(changesDir, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name)
7910
8348
  );
7911
8349
  } else if (changeId) {
7912
- const specificDir = join14(cwd, "specs", "changes", changeId);
7913
- if (!existsSync13(specificDir)) {
8350
+ const specificDir = join17(cwd, "specs", "changes", changeId);
8351
+ if (!existsSync17(specificDir)) {
7914
8352
  log.error(`Change not found: specs/changes/${changeId}`);
7915
8353
  process.exit(1);
7916
8354
  }
@@ -7930,10 +8368,10 @@ async function migrate(changeId, opts = {}) {
7930
8368
  const sessionStamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
7931
8369
  let migratedCount = 0;
7932
8370
  let upToDateCount = 0;
7933
- const backupRoot = join14(cwd, ".cdd", "migrate-backup", sessionStamp);
8371
+ const backupRoot = join17(cwd, ".cdd", "migrate-backup", sessionStamp);
7934
8372
  for (const id of idsToMigrate) {
7935
- const changeDir = join14(cwd, "specs", "changes", id);
7936
- if (!existsSync13(changeDir)) {
8373
+ const changeDir = join17(cwd, "specs", "changes", id);
8374
+ if (!existsSync17(changeDir)) {
7937
8375
  log.warn(` ${id}: directory not found \u2014 skipping`);
7938
8376
  continue;
7939
8377
  }
@@ -7995,40 +8433,40 @@ var upgrade_exports = {};
7995
8433
  __export(upgrade_exports, {
7996
8434
  upgrade: () => upgrade
7997
8435
  });
7998
- import { existsSync as existsSync14, mkdirSync as mkdirSync7, readdirSync as readdirSync8, copyFileSync as copyFileSync3, readFileSync as readFileSync13, writeFileSync as writeFileSync8 } from "fs";
7999
- import { dirname as dirname4, join as join15, relative as relative4 } from "path";
8436
+ import { existsSync as existsSync18, mkdirSync as mkdirSync9, readdirSync as readdirSync8, copyFileSync as copyFileSync4, readFileSync as readFileSync17, writeFileSync as writeFileSync11 } from "fs";
8437
+ import { dirname as dirname5, join as join18, relative as relative4 } from "path";
8000
8438
  function planMissingFiles(srcDir, destDir, label, planned) {
8001
- if (!existsSync14(srcDir))
8439
+ if (!existsSync18(srcDir))
8002
8440
  return;
8003
8441
  for (const entry of readdirSync8(srcDir, { withFileTypes: true })) {
8004
- const src = join15(srcDir, entry.name);
8005
- const dest = join15(destDir, entry.name);
8442
+ const src = join18(srcDir, entry.name);
8443
+ const dest = join18(destDir, entry.name);
8006
8444
  if (entry.isDirectory()) {
8007
- planMissingFiles(src, dest, join15(label, entry.name), planned);
8445
+ planMissingFiles(src, dest, join18(label, entry.name), planned);
8008
8446
  continue;
8009
8447
  }
8010
- if (!existsSync14(dest)) {
8011
- planned.push({ src, dest, rel: join15(label, relative4(srcDir, src)) });
8448
+ if (!existsSync18(dest)) {
8449
+ planned.push({ src, dest, rel: join18(label, relative4(srcDir, src)) });
8012
8450
  }
8013
8451
  }
8014
8452
  }
8015
8453
  function planProviderGuidance(cwd, provider, planned) {
8016
8454
  if (provider === "claude" || provider === "both") {
8017
- if (!existsSync14(join15(cwd, "CLAUDE.md"))) {
8018
- planned.push({ src: ASSET.claudeTemplate, dest: join15(cwd, "CLAUDE.md"), rel: "CLAUDE.md" });
8455
+ if (!existsSync18(join18(cwd, "CLAUDE.md"))) {
8456
+ planned.push({ src: ASSET.claudeTemplate, dest: join18(cwd, "CLAUDE.md"), rel: "CLAUDE.md" });
8019
8457
  }
8020
- if (!existsSync14(join15(cwd, "AGENTS.md"))) {
8021
- planned.push({ src: ASSET.agentsTemplate, dest: join15(cwd, "AGENTS.md"), rel: "AGENTS.md" });
8458
+ if (!existsSync18(join18(cwd, "AGENTS.md"))) {
8459
+ planned.push({ src: ASSET.agentsTemplate, dest: join18(cwd, "AGENTS.md"), rel: "AGENTS.md" });
8022
8460
  }
8023
8461
  }
8024
- if ((provider === "codex" || provider === "both") && !existsSync14(join15(cwd, "CODEX.md"))) {
8025
- planned.push({ src: ASSET.codexTemplate, dest: join15(cwd, "CODEX.md"), rel: "CODEX.md" });
8462
+ if ((provider === "codex" || provider === "both") && !existsSync18(join18(cwd, "CODEX.md"))) {
8463
+ planned.push({ src: ASSET.codexTemplate, dest: join18(cwd, "CODEX.md"), rel: "CODEX.md" });
8026
8464
  }
8027
8465
  }
8028
8466
  function applyCopy(plan) {
8029
8467
  for (const item of plan) {
8030
- mkdirSync7(dirname4(item.dest), { recursive: true });
8031
- copyFileSync3(item.src, item.dest);
8468
+ mkdirSync9(dirname5(item.dest), { recursive: true });
8469
+ copyFileSync4(item.src, item.dest);
8032
8470
  }
8033
8471
  }
8034
8472
  async function upgrade(opts = {}) {
@@ -8040,12 +8478,12 @@ async function upgrade(opts = {}) {
8040
8478
  }
8041
8479
  const provider = inferProvider(cwd, requestedProvider);
8042
8480
  const plan = [];
8043
- planMissingFiles(ASSET.contracts, join15(cwd, "contracts"), "contracts", plan);
8044
- planMissingFiles(ASSET.specsTemplates, join15(cwd, "specs", "templates"), "specs/templates", plan);
8045
- planMissingFiles(ASSET.testsTemplates, join15(cwd, "tests", "templates"), "tests/templates", plan);
8046
- planMissingFiles(ASSET.ci, join15(cwd, "ci"), "ci", plan);
8047
- planMissingFiles(ASSET.githubWorkflows, join15(cwd, ".github", "workflows"), ".github/workflows", plan);
8048
- planMissingFiles(ASSET.cddConfig, join15(cwd, ".cdd"), ".cdd", plan);
8481
+ planMissingFiles(ASSET.contracts, join18(cwd, "contracts"), "contracts", plan);
8482
+ planMissingFiles(ASSET.specsTemplates, join18(cwd, "specs", "templates"), "specs/templates", plan);
8483
+ planMissingFiles(ASSET.testsTemplates, join18(cwd, "tests", "templates"), "tests/templates", plan);
8484
+ planMissingFiles(ASSET.ci, join18(cwd, "ci"), "ci", plan);
8485
+ planMissingFiles(ASSET.githubWorkflows, join18(cwd, ".github", "workflows"), ".github/workflows", plan);
8486
+ planMissingFiles(ASSET.cddConfig, join18(cwd, ".cdd"), ".cdd", plan);
8049
8487
  planProviderGuidance(cwd, provider, plan);
8050
8488
  log.blank();
8051
8489
  log.info(`Upgrade provider: ${provider}`);
@@ -8082,11 +8520,11 @@ async function upgrade(opts = {}) {
8082
8520
  return;
8083
8521
  }
8084
8522
  applyCopy(plan);
8085
- const modelPolicyPath = join15(cwd, ".cdd", "model-policy.json");
8086
- if (existsSync14(modelPolicyPath)) {
8523
+ const modelPolicyPath = join18(cwd, ".cdd", "model-policy.json");
8524
+ if (existsSync18(modelPolicyPath)) {
8087
8525
  let existing = {};
8088
8526
  try {
8089
- existing = JSON.parse(readFileSync13(modelPolicyPath, "utf8"));
8527
+ existing = JSON.parse(readFileSync17(modelPolicyPath, "utf8"));
8090
8528
  } catch {
8091
8529
  }
8092
8530
  const merged = {
@@ -8095,7 +8533,7 @@ async function upgrade(opts = {}) {
8095
8533
  generated_at: (/* @__PURE__ */ new Date()).toISOString(),
8096
8534
  roles: existing.roles && typeof existing.roles === "object" ? existing.roles : {}
8097
8535
  };
8098
- writeFileSync8(modelPolicyPath, JSON.stringify(merged, null, 2) + "\n", "utf8");
8536
+ writeFileSync11(modelPolicyPath, JSON.stringify(merged, null, 2) + "\n", "utf8");
8099
8537
  }
8100
8538
  log.blank();
8101
8539
  log.ok(`Upgrade complete: ${plan.length} missing file(s) added.`);
@@ -8233,7 +8671,7 @@ function renderYaml(entries, opts) {
8233
8671
  }
8234
8672
  }
8235
8673
  }
8236
- const headerLineCount = opts.sourcesDigest ? 3 : 2;
8674
+ const headerLineCount = 2 + (opts.sourcesDigest ? 1 : 0) + (opts.surfaceRoot ? 1 : 0);
8237
8675
  const mapLines = bodyLines.length + headerLineCount;
8238
8676
  const compression = totalSrc === 0 ? 0 : totalSrc / mapLines;
8239
8677
  const fileCount = entries.length;
@@ -8244,6 +8682,9 @@ function renderYaml(entries, opts) {
8244
8682
  if (opts.sourcesDigest) {
8245
8683
  header.push(`# sources-digest: ${opts.sourcesDigest}`);
8246
8684
  }
8685
+ if (opts.surfaceRoot) {
8686
+ header.push(`# surface-root: ${opts.surfaceRoot}`);
8687
+ }
8247
8688
  return [...header, ...bodyLines].join("\n") + "\n";
8248
8689
  }
8249
8690
  var YAML_RESERVED;
@@ -8255,8 +8696,8 @@ var init_yaml_writer = __esm({
8255
8696
  });
8256
8697
 
8257
8698
  // src/code-map/config.ts
8258
- import { existsSync as existsSync15, readFileSync as readFileSync14 } from "fs";
8259
- import { join as join16 } from "path";
8699
+ import { existsSync as existsSync19, readFileSync as readFileSync18 } from "fs";
8700
+ import { join as join19 } from "path";
8260
8701
  import { load as yamlLoad } from "js-yaml";
8261
8702
  function asStringArray(value, key, where) {
8262
8703
  if (value === void 0)
@@ -8272,8 +8713,8 @@ function asStringArray(value, key, where) {
8272
8713
  return value;
8273
8714
  }
8274
8715
  function loadCodeMapConfig(cwd) {
8275
- const filePath = join16(cwd, CONFIG_REL_PATH);
8276
- if (!existsSync15(filePath)) {
8716
+ const filePath = join19(cwd, CONFIG_REL_PATH);
8717
+ if (!existsSync19(filePath)) {
8277
8718
  return {
8278
8719
  include: [...BUILTIN_INCLUDE],
8279
8720
  exclude: [...BUILTIN_EXCLUDE],
@@ -8282,7 +8723,7 @@ function loadCodeMapConfig(cwd) {
8282
8723
  }
8283
8724
  let text;
8284
8725
  try {
8285
- text = readFileSync14(filePath, "utf8");
8726
+ text = readFileSync18(filePath, "utf8");
8286
8727
  } catch (err) {
8287
8728
  throw new Error(`failed to read ${CONFIG_REL_PATH}: ${err.message}`);
8288
8729
  }
@@ -8352,8 +8793,8 @@ var init_config = __esm({
8352
8793
  });
8353
8794
 
8354
8795
  // src/code-map/include-exclude.ts
8355
- import { readdirSync as readdirSync9, statSync as statSync2 } from "fs";
8356
- import { join as join17 } from "path";
8796
+ import { readdirSync as readdirSync9, statSync as statSync3 } from "fs";
8797
+ import { join as join20 } from "path";
8357
8798
  import picomatch from "picomatch";
8358
8799
  function walkRepo(root, opts = {}) {
8359
8800
  const includes = opts.include ?? [];
@@ -8372,13 +8813,13 @@ function walkRepo(root, opts = {}) {
8372
8813
  }
8373
8814
  for (const entry of entries) {
8374
8815
  const relPath = relDir ? `${relDir}/${entry.name}` : entry.name;
8375
- const absPath = join17(dir, entry.name);
8816
+ const absPath = join20(dir, entry.name);
8376
8817
  if (entry.isDirectory() || entry.isSymbolicLink()) {
8377
8818
  const dirPattern = `${relPath}/**`;
8378
8819
  if (isExcluded(relPath) || isExcluded(dirPattern))
8379
8820
  continue;
8380
8821
  try {
8381
- const stat = statSync2(absPath);
8822
+ const stat = statSync3(absPath);
8382
8823
  if (stat.isDirectory()) {
8383
8824
  walk(absPath, relPath);
8384
8825
  }
@@ -8444,10 +8885,10 @@ var init_orchestrator = __esm({
8444
8885
  });
8445
8886
 
8446
8887
  // src/code-map/freshness.ts
8447
- import { existsSync as existsSync16, readFileSync as readFileSync15, statSync as statSync3 } from "fs";
8448
- import { join as join18 } from "path";
8888
+ import { existsSync as existsSync20, readFileSync as readFileSync19, statSync as statSync4 } from "fs";
8889
+ import { join as join21 } from "path";
8449
8890
  function checkCodeMapFreshness(cwd, mapRel = ".cdd/code-map.yml", include, exclude) {
8450
- const mapPath = join18(cwd, mapRel);
8891
+ const mapPath = join21(cwd, mapRel);
8451
8892
  let cfg;
8452
8893
  try {
8453
8894
  cfg = loadCodeMapConfig(cwd);
@@ -8463,17 +8904,17 @@ function checkCodeMapFreshness(cwd, mapRel = ".cdd/code-map.yml", include, exclu
8463
8904
  const includeFinal = [...cfg.include, ...include ?? []];
8464
8905
  const excludeFinal = [...cfg.exclude, ...exclude ?? []];
8465
8906
  const sourceFiles = walkRepo(cwd, { include: includeFinal, exclude: excludeFinal });
8466
- if (!existsSync16(mapPath)) {
8907
+ if (!existsSync20(mapPath)) {
8467
8908
  if (sourceFiles.length === 0) {
8468
8909
  return { status: "missing-greenfield", staleFiles: [], staleCount: 0, mapPath };
8469
8910
  }
8470
8911
  return { status: "missing-with-sources", staleFiles: [], staleCount: 0, mapPath };
8471
8912
  }
8472
- const mapMtime = statSync3(mapPath).mtimeMs;
8913
+ const mapMtime = statSync4(mapPath).mtimeMs;
8473
8914
  const staleAll = [];
8474
8915
  for (const absPath of sourceFiles) {
8475
8916
  try {
8476
- const mtime = statSync3(absPath).mtimeMs;
8917
+ const mtime = statSync4(absPath).mtimeMs;
8477
8918
  if (mtime > mapMtime) {
8478
8919
  const rel = absPath.replace(/\\/g, "/").replace(cwd.replace(/\\/g, "/") + "/", "");
8479
8920
  staleAll.push(rel);
@@ -8500,7 +8941,7 @@ function checkCodeMapFreshness(cwd, mapRel = ".cdd/code-map.yml", include, exclu
8500
8941
  }
8501
8942
  function readSourcesDigest(mapPath) {
8502
8943
  try {
8503
- const head = readFileSync15(mapPath, "utf8").slice(0, 2048);
8944
+ const head = readFileSync19(mapPath, "utf8").slice(0, 2048);
8504
8945
  const m = head.match(/^# sources-digest:\s*([a-f0-9]+)/m);
8505
8946
  return m ? m[1] : null;
8506
8947
  } catch {
@@ -8517,7 +8958,7 @@ var init_freshness = __esm({
8517
8958
  });
8518
8959
 
8519
8960
  // src/code-map/index-reader.ts
8520
- import { existsSync as existsSync17, readFileSync as readFileSync16 } from "fs";
8961
+ import { existsSync as existsSync21, readFileSync as readFileSync20 } from "fs";
8521
8962
  import yaml3 from "js-yaml";
8522
8963
  async function ensureCodeMapFresh(mapPath, refresh2) {
8523
8964
  if (!refresh2)
@@ -8555,13 +8996,13 @@ function sidecarPathFor(mapPath) {
8555
8996
  }
8556
8997
  function tryLoadSidecar(mapPath, mapText) {
8557
8998
  const sidecarPath = sidecarPathFor(mapPath);
8558
- if (!existsSync17(sidecarPath))
8999
+ if (!existsSync21(sidecarPath))
8559
9000
  return null;
8560
9001
  const headerDigest = mapText.match(/^# sources-digest:\s*([a-f0-9]+)/m)?.[1];
8561
9002
  if (!headerDigest)
8562
9003
  return null;
8563
9004
  try {
8564
- const raw = JSON.parse(readFileSync16(sidecarPath, "utf8"));
9005
+ const raw = JSON.parse(readFileSync20(sidecarPath, "utf8"));
8565
9006
  if (raw && raw.sourcesDigest === headerDigest && Array.isArray(raw.entries)) {
8566
9007
  return raw.entries;
8567
9008
  }
@@ -8570,10 +9011,10 @@ function tryLoadSidecar(mapPath, mapText) {
8570
9011
  return null;
8571
9012
  }
8572
9013
  function loadCodeMapEntries(mapPath) {
8573
- if (!existsSync17(mapPath)) {
9014
+ if (!existsSync21(mapPath)) {
8574
9015
  throw new Error(`${mapPath} is missing; run \`cdd-kit code-map\` first.`);
8575
9016
  }
8576
- const text = readFileSync16(mapPath, "utf8");
9017
+ const text = readFileSync20(mapPath, "utf8");
8577
9018
  const fromSidecar = tryLoadSidecar(mapPath, text);
8578
9019
  if (fromSidecar)
8579
9020
  return fromSidecar;
@@ -8945,7 +9386,7 @@ var init_builder = __esm({
8945
9386
  });
8946
9387
 
8947
9388
  // src/code-graph/reader.ts
8948
- import { existsSync as existsSync18, readFileSync as readFileSync17 } from "fs";
9389
+ import { existsSync as existsSync22, readFileSync as readFileSync21 } from "fs";
8949
9390
  function graphPathFor(mapPath) {
8950
9391
  const match = mapPath.match(/^(.*?)(?:code-map)(.*)\.ya?ml$/i);
8951
9392
  if (match)
@@ -8953,10 +9394,10 @@ function graphPathFor(mapPath) {
8953
9394
  return `${mapPath.replace(/\.ya?ml$/i, "")}.graph.json`;
8954
9395
  }
8955
9396
  function loadCodeGraph(graphPath) {
8956
- if (!existsSync18(graphPath)) {
9397
+ if (!existsSync22(graphPath)) {
8957
9398
  throw new Error(`${graphPath} is missing; run \`cdd-kit code-map\` first.`);
8958
9399
  }
8959
- const raw = JSON.parse(readFileSync17(graphPath, "utf8"));
9400
+ const raw = JSON.parse(readFileSync21(graphPath, "utf8"));
8960
9401
  if (!raw || raw.schema_version !== "1.0" || !Array.isArray(raw.nodes) || !Array.isArray(raw.edges)) {
8961
9402
  throw new Error(`${graphPath} is not a cdd-kit code graph v1 index.`);
8962
9403
  }
@@ -8974,9 +9415,9 @@ __export(worker_dispatch_exports, {
8974
9415
  scanLangWithWorkers: () => scanLangWithWorkers
8975
9416
  });
8976
9417
  import { execFile } from "child_process";
8977
- import { writeFileSync as writeFileSync9, unlinkSync } from "fs";
9418
+ import { writeFileSync as writeFileSync12, unlinkSync } from "fs";
8978
9419
  import { randomBytes } from "crypto";
8979
- import { join as join19 } from "path";
9420
+ import { join as join22 } from "path";
8980
9421
  import { tmpdir } from "os";
8981
9422
  function chunk(arr, parts) {
8982
9423
  const size = Math.ceil(arr.length / parts);
@@ -8986,15 +9427,15 @@ function chunk(arr, parts) {
8986
9427
  return out;
8987
9428
  }
8988
9429
  function scanChunkInChild(cliEntry, lang, files, repoRoot) {
8989
- return new Promise((resolve2, reject) => {
9430
+ return new Promise((resolve6, reject) => {
8990
9431
  if (!ALLOWED_LANGS.has(lang)) {
8991
9432
  return reject(new Error(`refusing to spawn worker for unknown lang: ${lang}`));
8992
9433
  }
8993
- const listFile = join19(
9434
+ const listFile = join22(
8994
9435
  tmpdir(),
8995
9436
  `cdd-cm-worker-${process.pid}-${randomBytes(12).toString("hex")}.txt`
8996
9437
  );
8997
- writeFileSync9(listFile, files.join("\n") + "\n", { encoding: "utf8", mode: 384 });
9438
+ writeFileSync12(listFile, files.join("\n") + "\n", { encoding: "utf8", mode: 384 });
8998
9439
  execFile(
8999
9440
  process.execPath,
9000
9441
  [cliEntry, "__code-map-scan", "--lang", lang, "--batch-file", listFile, "--repo-root", repoRoot],
@@ -9008,7 +9449,7 @@ function scanChunkInChild(cliEntry, lang, files, repoRoot) {
9008
9449
  return reject(err);
9009
9450
  try {
9010
9451
  const parsed = JSON.parse(stdout);
9011
- resolve2({ entries: parsed.entries ?? [], warnings: parsed.warnings ?? [] });
9452
+ resolve6({ entries: parsed.entries ?? [], warnings: parsed.warnings ?? [] });
9012
9453
  } catch (e) {
9013
9454
  reject(e);
9014
9455
  }
@@ -9066,15 +9507,15 @@ var python_exports = {};
9066
9507
  __export(python_exports, {
9067
9508
  pythonScanner: () => pythonScanner
9068
9509
  });
9069
- import { spawnSync as spawnSync2 } from "child_process";
9070
- import { writeFileSync as writeFileSync10, unlinkSync as unlinkSync2 } from "fs";
9510
+ import { spawnSync as spawnSync4 } from "child_process";
9511
+ import { writeFileSync as writeFileSync13, unlinkSync as unlinkSync2 } from "fs";
9071
9512
  import { randomBytes as randomBytes2 } from "crypto";
9072
- import { join as join20 } from "path";
9513
+ import { join as join23 } from "path";
9073
9514
  import { tmpdir as tmpdir2 } from "os";
9074
9515
  function detectPython2() {
9075
9516
  for (const candidate of ["python3", "python"]) {
9076
9517
  try {
9077
- const result = spawnSync2(candidate, ["--version"], {
9518
+ const result = spawnSync4(candidate, ["--version"], {
9078
9519
  encoding: "utf8",
9079
9520
  timeout: 5e3
9080
9521
  });
@@ -9143,13 +9584,13 @@ var init_python = __esm({
9143
9584
  const entries = [];
9144
9585
  const warnings = [];
9145
9586
  const scriptPath = ASSET.codeMapPython;
9146
- const listFile = join20(tmpdir2(), `cdd-codemap-${process.pid}-${randomBytes2(12).toString("hex")}.txt`);
9147
- writeFileSync10(listFile, absolutePaths.join("\n") + "\n", { encoding: "utf8", mode: 384 });
9587
+ const listFile = join23(tmpdir2(), `cdd-codemap-${process.pid}-${randomBytes2(12).toString("hex")}.txt`);
9588
+ writeFileSync13(listFile, absolutePaths.join("\n") + "\n", { encoding: "utf8", mode: 384 });
9148
9589
  let stdout = "";
9149
9590
  let stderr = "";
9150
9591
  let exitCode = 0;
9151
9592
  try {
9152
- const result = spawnSync2(
9593
+ const result = spawnSync4(
9153
9594
  interpreter,
9154
9595
  [scriptPath, "--batch-file", listFile, "--repo-root", repoRoot],
9155
9596
  {
@@ -9213,7 +9654,7 @@ var init_python = __esm({
9213
9654
  }
9214
9655
  const r = parsed;
9215
9656
  entries.push({
9216
- path: canonicalRelPath(join20(repoRoot, r.path), repoRoot),
9657
+ path: canonicalRelPath(join23(repoRoot, r.path), repoRoot),
9217
9658
  total_lines: r.total_lines,
9218
9659
  imports: r.imports ?? [],
9219
9660
  constants: r.constants ?? [],
@@ -9263,7 +9704,7 @@ __export(javascript_exports, {
9263
9704
  parseJsSource: () => parseJsSource,
9264
9705
  parseSourceWithPlugins: () => parseSourceWithPlugins
9265
9706
  });
9266
- import { readFileSync as readFileSync19 } from "fs";
9707
+ import { readFileSync as readFileSync23 } from "fs";
9267
9708
  import { parse } from "@babel/parser";
9268
9709
  function parseSourceWithPlugins(source, plugins) {
9269
9710
  return parse(source, {
@@ -9666,7 +10107,7 @@ var init_javascript = __esm({
9666
10107
  async scan(absolutePath, repoRoot) {
9667
10108
  let source;
9668
10109
  try {
9669
- source = readFileSync19(absolutePath, "utf8");
10110
+ source = readFileSync23(absolutePath, "utf8");
9670
10111
  } catch (err) {
9671
10112
  throw err;
9672
10113
  }
@@ -9686,7 +10127,7 @@ var typescript_exports = {};
9686
10127
  __export(typescript_exports, {
9687
10128
  tsScanner: () => tsScanner
9688
10129
  });
9689
- import { readFileSync as readFileSync20 } from "fs";
10130
+ import { readFileSync as readFileSync24 } from "fs";
9690
10131
  var TypeScriptScanner, tsScanner;
9691
10132
  var init_typescript = __esm({
9692
10133
  "src/code-map/scanners/typescript.ts"() {
@@ -9698,7 +10139,7 @@ var init_typescript = __esm({
9698
10139
  async scan(absolutePath, repoRoot) {
9699
10140
  let source;
9700
10141
  try {
9701
- source = readFileSync20(absolutePath, "utf8");
10142
+ source = readFileSync24(absolutePath, "utf8");
9702
10143
  } catch (err) {
9703
10144
  throw err;
9704
10145
  }
@@ -9736,7 +10177,7 @@ var vue_exports = {};
9736
10177
  __export(vue_exports, {
9737
10178
  vueScanner: () => vueScanner
9738
10179
  });
9739
- import { readFileSync as readFileSync21 } from "fs";
10180
+ import { readFileSync as readFileSync25 } from "fs";
9740
10181
  import { parse as parse2 } from "@vue/compiler-sfc";
9741
10182
  var VueScanner, vueScanner;
9742
10183
  var init_vue = __esm({
@@ -9749,7 +10190,7 @@ var init_vue = __esm({
9749
10190
  async scan(absolutePath, repoRoot) {
9750
10191
  let source;
9751
10192
  try {
9752
- source = readFileSync21(absolutePath, "utf8");
10193
+ source = readFileSync25(absolutePath, "utf8");
9753
10194
  } catch (err) {
9754
10195
  throw err;
9755
10196
  }
@@ -9844,14 +10285,15 @@ var init_vue = __esm({
9844
10285
  var code_map_exports = {};
9845
10286
  __export(code_map_exports, {
9846
10287
  codeMap: () => codeMap,
9847
- computeSourcesDigest: () => computeSourcesDigest
10288
+ computeSourcesDigest: () => computeSourcesDigest,
10289
+ slugifySurface: () => slugifySurface
9848
10290
  });
9849
- import { existsSync as existsSync19, mkdirSync as mkdirSync8, readFileSync as readFileSync22, writeFileSync as writeFileSync11 } from "fs";
9850
- import { resolve, dirname as dirname5, relative as relative6 } from "path";
10291
+ import { existsSync as existsSync23, mkdirSync as mkdirSync10, readFileSync as readFileSync26, writeFileSync as writeFileSync14 } from "fs";
10292
+ import { resolve as resolve3, dirname as dirname6, relative as relative6 } from "path";
9851
10293
  import { createHash as createHash6 } from "crypto";
9852
10294
  import { createRequire } from "module";
9853
10295
  import { fileURLToPath as fileURLToPath2 } from "url";
9854
- import { join as join21 } from "path";
10296
+ import { join as join24 } from "path";
9855
10297
  function computeSourcesDigest(absolutePaths, cwd) {
9856
10298
  const lines = absolutePaths.slice().sort().map((p) => {
9857
10299
  const rel = relative6(cwd, p).replace(/\\/g, "/");
@@ -9866,14 +10308,15 @@ function slugifySurface(surface) {
9866
10308
  async function codeMap(opts) {
9867
10309
  const start = Date.now();
9868
10310
  if (opts.surface) {
9869
- const resolvedSurface = resolve(process.cwd(), opts.surface);
9870
- if (!existsSync19(resolvedSurface)) {
10311
+ const resolvedSurface = resolve3(process.cwd(), opts.surface);
10312
+ if (!existsSync23(resolvedSurface)) {
9871
10313
  log.error(`code-map --surface path not found: ${opts.surface}`);
9872
10314
  return 1;
9873
10315
  }
9874
10316
  }
9875
10317
  const scanPath = opts.surface ?? opts.path;
9876
- const root = resolve(process.cwd(), scanPath);
10318
+ const root = resolve3(process.cwd(), scanPath);
10319
+ const surfaceRoot = opts.surface ? relative6(process.cwd(), root).replace(/\\/g, "/") || "." : void 0;
9877
10320
  const out = opts.out ?? (opts.surface ? `.cdd/code-map.${slugifySurface(opts.surface)}.yml` : ".cdd/code-map.yml");
9878
10321
  let cfg;
9879
10322
  try {
@@ -9941,7 +10384,8 @@ async function codeMap(opts) {
9941
10384
  const sourcesDigest = computeSourcesDigest(files, root);
9942
10385
  const yamlBody = renderYaml(result.entries, {
9943
10386
  generator: `cdd-kit ${_pkg.version}`,
9944
- sourcesDigest
10387
+ sourcesDigest,
10388
+ surfaceRoot
9945
10389
  });
9946
10390
  const totalSrc = result.entries.reduce((s, e) => s + e.total_lines, 0);
9947
10391
  const mapLines = yamlBody.split("\n").length;
@@ -9952,7 +10396,7 @@ async function codeMap(opts) {
9952
10396
  log.warn(`${w.path}: ${w.message}`);
9953
10397
  }
9954
10398
  if (opts.check) {
9955
- const existing = existsSync19(out) ? readFileSync22(out, "utf8") : "";
10399
+ const existing = existsSync23(out) ? readFileSync26(out, "utf8") : "";
9956
10400
  const normalize = (s) => s.replace(/\r\n/g, "\n").replace(/\r/g, "\n").replace(/^# generated: [^\n]+\n/m, "# generated: <normalized>\n");
9957
10401
  if (normalize(existing) !== normalize(yamlBody)) {
9958
10402
  if (!opts.silent)
@@ -9963,11 +10407,11 @@ async function codeMap(opts) {
9963
10407
  log.ok(`code-map up to date: ${out}`);
9964
10408
  return 0;
9965
10409
  }
9966
- mkdirSync8(dirname5(out), { recursive: true });
9967
- writeFileSync11(out, yamlBody, "utf8");
10410
+ mkdirSync10(dirname6(out), { recursive: true });
10411
+ writeFileSync14(out, yamlBody, "utf8");
9968
10412
  try {
9969
10413
  const sidecarPath = sidecarPathFor(out);
9970
- writeFileSync11(sidecarPath, JSON.stringify({ sourcesDigest, entries: result.entries }), "utf8");
10414
+ writeFileSync14(sidecarPath, JSON.stringify({ sourcesDigest, entries: result.entries }), "utf8");
9971
10415
  const rel = relative6(process.cwd(), sidecarPath).replace(/\\/g, "/");
9972
10416
  if (!rel.startsWith("..")) {
9973
10417
  ensureGitignoreEntry(process.cwd(), rel, "cdd-kit local cache (do not commit)");
@@ -9977,7 +10421,7 @@ async function codeMap(opts) {
9977
10421
  generator: `cdd-kit ${_pkg.version}`,
9978
10422
  sourcesDigest
9979
10423
  });
9980
- writeFileSync11(graphPath, JSON.stringify(graph2, null, 2) + "\n", "utf8");
10424
+ writeFileSync14(graphPath, JSON.stringify(graph2, null, 2) + "\n", "utf8");
9981
10425
  const graphRel = relative6(process.cwd(), graphPath).replace(/\\/g, "/");
9982
10426
  if (!graphRel.startsWith("..")) {
9983
10427
  ensureGitignoreEntry(process.cwd(), graphRel, "cdd-kit local cache (do not commit)");
@@ -10002,8 +10446,8 @@ var init_code_map = __esm({
10002
10446
  init_digest();
10003
10447
  init_gitignore();
10004
10448
  _require = createRequire(import.meta.url);
10005
- _pkgPath = join21(fileURLToPath2(import.meta.url), "..", "..", "..", "package.json");
10006
- _pkg = JSON.parse(readFileSync22(_pkgPath, "utf8"));
10449
+ _pkgPath = join24(fileURLToPath2(import.meta.url), "..", "..", "..", "package.json");
10450
+ _pkg = JSON.parse(readFileSync26(_pkgPath, "utf8"));
10007
10451
  }
10008
10452
  });
10009
10453
 
@@ -10012,15 +10456,15 @@ var refresh_exports = {};
10012
10456
  __export(refresh_exports, {
10013
10457
  refresh: () => refresh
10014
10458
  });
10015
- import { existsSync as existsSync20, mkdirSync as mkdirSync9, readdirSync as readdirSync10, copyFileSync as copyFileSync4, readFileSync as readFileSync23, writeFileSync as writeFileSync12 } from "fs";
10016
- import { dirname as dirname6, join as join22, relative as relative7 } from "path";
10459
+ import { existsSync as existsSync24, mkdirSync as mkdirSync11, readdirSync as readdirSync10, copyFileSync as copyFileSync5, readFileSync as readFileSync27, writeFileSync as writeFileSync15 } from "fs";
10460
+ import { dirname as dirname7, join as join25, relative as relative7 } from "path";
10017
10461
  import { createHash as createHash7 } from "crypto";
10018
10462
  function fileHash2(filePath) {
10019
- return createHash7("sha256").update(readFileSync23(filePath)).digest("hex");
10463
+ return createHash7("sha256").update(readFileSync27(filePath)).digest("hex");
10020
10464
  }
10021
10465
  function planForceRefresh(srcDir, destDir, sectionLabel) {
10022
10466
  const plan = [];
10023
- if (!existsSync20(srcDir))
10467
+ if (!existsSync24(srcDir))
10024
10468
  return plan;
10025
10469
  function walk(curSrc, curDest) {
10026
10470
  let items;
@@ -10030,16 +10474,16 @@ function planForceRefresh(srcDir, destDir, sectionLabel) {
10030
10474
  return;
10031
10475
  }
10032
10476
  for (const item of items) {
10033
- const sp = join22(curSrc, item.name);
10034
- const dp = join22(curDest, item.name);
10477
+ const sp = join25(curSrc, item.name);
10478
+ const dp = join25(curDest, item.name);
10035
10479
  if (item.isDirectory()) {
10036
10480
  walk(sp, dp);
10037
10481
  continue;
10038
10482
  }
10039
10483
  if (!item.isFile())
10040
10484
  continue;
10041
- const rel = join22(sectionLabel, relative7(srcDir, sp)).replace(/\\/g, "/");
10042
- if (!existsSync20(dp)) {
10485
+ const rel = join25(sectionLabel, relative7(srcDir, sp)).replace(/\\/g, "/");
10486
+ if (!existsSync24(dp)) {
10043
10487
  plan.push({ src: sp, dest: dp, rel, action: "add" });
10044
10488
  } else if (fileHash2(sp) !== fileHash2(dp)) {
10045
10489
  plan.push({ src: sp, dest: dp, rel, action: "overwrite" });
@@ -10052,9 +10496,9 @@ function planForceRefresh(srcDir, destDir, sectionLabel) {
10052
10496
  return plan;
10053
10497
  }
10054
10498
  function planSingleFile(src, dest, rel) {
10055
- if (!existsSync20(src))
10499
+ if (!existsSync24(src))
10056
10500
  return null;
10057
- if (!existsSync20(dest))
10501
+ if (!existsSync24(dest))
10058
10502
  return { src, dest, rel, action: "add" };
10059
10503
  if (fileHash2(src) !== fileHash2(dest))
10060
10504
  return { src, dest, rel, action: "overwrite" };
@@ -10067,15 +10511,15 @@ function applyPlan(plan, backupRoot) {
10067
10511
  if (item.action === "skip")
10068
10512
  continue;
10069
10513
  if (item.action === "overwrite") {
10070
- const backupPath = join22(backupRoot, item.rel);
10071
- mkdirSync9(dirname6(backupPath), { recursive: true });
10072
- copyFileSync4(item.dest, backupPath);
10514
+ const backupPath = join25(backupRoot, item.rel);
10515
+ mkdirSync11(dirname7(backupPath), { recursive: true });
10516
+ copyFileSync5(item.dest, backupPath);
10073
10517
  overwritten += 1;
10074
10518
  } else {
10075
10519
  added += 1;
10076
10520
  }
10077
- mkdirSync9(dirname6(item.dest), { recursive: true });
10078
- copyFileSync4(item.src, item.dest);
10521
+ mkdirSync11(dirname7(item.dest), { recursive: true });
10522
+ copyFileSync5(item.src, item.dest);
10079
10523
  }
10080
10524
  return { added, overwritten };
10081
10525
  }
@@ -10096,14 +10540,14 @@ function parseAgentFrontmatter(content) {
10096
10540
  return fm;
10097
10541
  }
10098
10542
  function resyncModelPolicy(cwd) {
10099
- const policyPath = join22(cwd, ".cdd", "model-policy.json");
10543
+ const policyPath = join25(cwd, ".cdd", "model-policy.json");
10100
10544
  const result = { changed: false, diff: [], policyPath };
10101
- if (!existsSync20(AGENTS_HOME))
10545
+ if (!existsSync24(AGENTS_HOME))
10102
10546
  return result;
10103
10547
  const desired = {};
10104
10548
  const agentFiles = readdirSync10(AGENTS_HOME, { withFileTypes: true }).filter((d) => d.isFile() && d.name.endsWith(".md"));
10105
10549
  for (const f of agentFiles) {
10106
- const content = readFileSync23(join22(AGENTS_HOME, f.name), "utf8");
10550
+ const content = readFileSync27(join25(AGENTS_HOME, f.name), "utf8");
10107
10551
  const fm = parseAgentFrontmatter(content);
10108
10552
  if (fm.name && fm.model)
10109
10553
  desired[fm.name] = fm.model;
@@ -10111,9 +10555,9 @@ function resyncModelPolicy(cwd) {
10111
10555
  if (Object.keys(desired).length === 0)
10112
10556
  return result;
10113
10557
  let existing = {};
10114
- if (existsSync20(policyPath)) {
10558
+ if (existsSync24(policyPath)) {
10115
10559
  try {
10116
- existing = JSON.parse(readFileSync23(policyPath, "utf8"));
10560
+ existing = JSON.parse(readFileSync27(policyPath, "utf8"));
10117
10561
  } catch {
10118
10562
  }
10119
10563
  }
@@ -10133,8 +10577,8 @@ function resyncModelPolicy(cwd) {
10133
10577
  merged["schema-version"] = "0.2.0";
10134
10578
  if (!("provider" in merged))
10135
10579
  merged["provider"] = "claude";
10136
- mkdirSync9(dirname6(policyPath), { recursive: true });
10137
- writeFileSync12(policyPath, JSON.stringify(merged, null, 2) + "\n", "utf8");
10580
+ mkdirSync11(dirname7(policyPath), { recursive: true });
10581
+ writeFileSync15(policyPath, JSON.stringify(merged, null, 2) + "\n", "utf8");
10138
10582
  result.changed = true;
10139
10583
  return result;
10140
10584
  }
@@ -10142,21 +10586,21 @@ function planTemplateRefresh(cwd) {
10142
10586
  const sections = [];
10143
10587
  sections.push({
10144
10588
  name: "specs/templates",
10145
- plan: planForceRefresh(ASSET.specsTemplates, join22(cwd, "specs", "templates"), "specs/templates")
10589
+ plan: planForceRefresh(ASSET.specsTemplates, join25(cwd, "specs", "templates"), "specs/templates")
10146
10590
  });
10147
10591
  sections.push({
10148
10592
  name: "tests/templates",
10149
- plan: planForceRefresh(ASSET.testsTemplates, join22(cwd, "tests", "templates"), "tests/templates")
10593
+ plan: planForceRefresh(ASSET.testsTemplates, join25(cwd, "tests", "templates"), "tests/templates")
10150
10594
  });
10151
- const ciTemplatesAsset = join22(ASSET.ci, "..", "ci-templates");
10152
- if (existsSync20(ciTemplatesAsset)) {
10595
+ const ciTemplatesAsset = join25(ASSET.ci, "..", "ci-templates");
10596
+ if (existsSync24(ciTemplatesAsset)) {
10153
10597
  sections.push({
10154
10598
  name: "ci-templates",
10155
- plan: planForceRefresh(ciTemplatesAsset, join22(cwd, "ci-templates"), "ci-templates")
10599
+ plan: planForceRefresh(ciTemplatesAsset, join25(cwd, "ci-templates"), "ci-templates")
10156
10600
  });
10157
10601
  }
10158
- const wfAsset = join22(ASSET.githubWorkflows, "contract-driven-gates.yml");
10159
- const wfDest = join22(cwd, ".github", "workflows", "contract-driven-gates.yml");
10602
+ const wfAsset = join25(ASSET.githubWorkflows, "contract-driven-gates.yml");
10603
+ const wfDest = join25(cwd, ".github", "workflows", "contract-driven-gates.yml");
10160
10604
  const wfPlan = planSingleFile(wfAsset, wfDest, ".github/workflows/contract-driven-gates.yml");
10161
10605
  if (wfPlan)
10162
10606
  sections.push({ name: ".github/workflows/contract-driven-gates.yml", plan: [wfPlan] });
@@ -10211,7 +10655,7 @@ async function refresh(opts) {
10211
10655
  }
10212
10656
  if (apply) {
10213
10657
  const ts = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
10214
- backupRoot = join22(cwd, ".cdd", ".refresh-backup", ts);
10658
+ backupRoot = join25(cwd, ".cdd", ".refresh-backup", ts);
10215
10659
  const result = applyPlan(total, backupRoot);
10216
10660
  templateAdded = result.added;
10217
10661
  templateOverwritten = result.overwritten;
@@ -10231,8 +10675,8 @@ async function refresh(opts) {
10231
10675
  }
10232
10676
  log.blank();
10233
10677
  if (!opts.noHooks) {
10234
- const markerPath = join22(cwd, HOOKS_MARKER_PATH);
10235
- if (existsSync20(markerPath)) {
10678
+ const markerPath = join25(cwd, HOOKS_MARKER_PATH);
10679
+ if (existsSync24(markerPath)) {
10236
10680
  log.info("[4/6] re-install code-map pre-commit hook (marker found)");
10237
10681
  if (apply) {
10238
10682
  try {
@@ -10301,7 +10745,7 @@ async function refresh(opts) {
10301
10745
  logRecommendedMcpSetup();
10302
10746
  } else {
10303
10747
  log.info("Dry-run finished. Re-run with `--yes` to apply.");
10304
- log.info('After applying, configure MCP with command `cdd-kit` and args ["mcp"] so agents use graph/code-map tools directly.');
10748
+ log.info("After applying, register MCP with `claude mcp add --scope user cdd-kit -- cdd-kit mcp` so agents use graph/code-map tools directly.");
10305
10749
  }
10306
10750
  log.blank();
10307
10751
  }
@@ -10328,8 +10772,8 @@ __export(lint_agents_exports, {
10328
10772
  lintAgentContent: () => lintAgentContent,
10329
10773
  lintAgents: () => lintAgents
10330
10774
  });
10331
- import { readdirSync as readdirSync11, readFileSync as readFileSync24 } from "fs";
10332
- import { join as join23 } from "path";
10775
+ import { readdirSync as readdirSync11, readFileSync as readFileSync28 } from "fs";
10776
+ import { join as join26 } from "path";
10333
10777
  import { load as yamlLoad2 } from "js-yaml";
10334
10778
  function extractRequiredArtifactsSection(content) {
10335
10779
  const match = content.match(
@@ -10458,7 +10902,7 @@ function lintAgentContent(filename, rawContent, opts = {}) {
10458
10902
  return violations;
10459
10903
  }
10460
10904
  function collectAgentViolations(cwd, opts = {}) {
10461
- const agentsDir = join23(cwd, ".claude", "agents");
10905
+ const agentsDir = join26(cwd, ".claude", "agents");
10462
10906
  let files;
10463
10907
  try {
10464
10908
  files = readdirSync11(agentsDir).filter((f) => f.endsWith(".md")).sort();
@@ -10469,7 +10913,7 @@ function collectAgentViolations(cwd, opts = {}) {
10469
10913
  for (const filename of files) {
10470
10914
  let content;
10471
10915
  try {
10472
- content = readFileSync24(join23(agentsDir, filename), "utf8");
10916
+ content = readFileSync28(join26(agentsDir, filename), "utf8");
10473
10917
  } catch {
10474
10918
  violations.push({
10475
10919
  file: filename,
@@ -10488,7 +10932,7 @@ async function lintAgents(opts) {
10488
10932
  const violations = collectAgentViolations(cwd, opts);
10489
10933
  if (violations === null) {
10490
10934
  log.error(
10491
- `lint-agents: cannot read ${join23(cwd, ".claude", "agents")} -- is this a cdd-kit project?`
10935
+ `lint-agents: cannot read ${join26(cwd, ".claude", "agents")} -- is this a cdd-kit project?`
10492
10936
  );
10493
10937
  return 1;
10494
10938
  }
@@ -10513,22 +10957,132 @@ var init_lint_agents = __esm({
10513
10957
  }
10514
10958
  });
10515
10959
 
10960
+ // src/commands/chokepoints.ts
10961
+ import { existsSync as existsSync25, readFileSync as readFileSync29, readdirSync as readdirSync12 } from "fs";
10962
+ import { join as join27, resolve as resolve4 } from "path";
10963
+ import { spawnSync as spawnSync5 } from "child_process";
10964
+ function safeRead2(path) {
10965
+ try {
10966
+ return readFileSync29(path, "utf8");
10967
+ } catch {
10968
+ return "";
10969
+ }
10970
+ }
10971
+ function probeGraphFirst(cwd) {
10972
+ const settingsPath = join27(cwd, ".claude", "settings.json");
10973
+ let live = false;
10974
+ if (existsSync25(settingsPath)) {
10975
+ try {
10976
+ const settings = JSON.parse(safeRead2(settingsPath));
10977
+ const entries = settings.hooks?.PreToolUse;
10978
+ if (Array.isArray(entries)) {
10979
+ live = entries.some(
10980
+ (e) => Array.isArray(e?.hooks) && e.hooks.some((h) => typeof h?.command === "string" && h.command.includes(GRAPH_FIRST_MARKER))
10981
+ );
10982
+ }
10983
+ } catch {
10984
+ }
10985
+ }
10986
+ return {
10987
+ id: "graph-first-hook",
10988
+ name: "graph-first exploration hook",
10989
+ live,
10990
+ detail: live ? "PreToolUse hook steers agents to graph/index queries before Read" : "dormant \u2014 run `cdd-kit install-agent-hooks --graph-first advisory` to stop agents defaulting to Read"
10991
+ };
10992
+ }
10993
+ function resolveHooksDir2(cwd) {
10994
+ try {
10995
+ const res = spawnSync5("git", ["rev-parse", "--git-path", "hooks"], { cwd, encoding: "utf8" });
10996
+ if (res.status === 0 && res.stdout.trim())
10997
+ return resolve4(cwd, res.stdout.trim());
10998
+ } catch {
10999
+ }
11000
+ return join27(cwd, ".git", "hooks");
11001
+ }
11002
+ function probePreCommitGate(cwd) {
11003
+ const hookPath = join27(resolveHooksDir2(cwd), "pre-commit");
11004
+ const live = existsSync25(hookPath) && safeRead2(hookPath).includes(PRECOMMIT_MARKER);
11005
+ return {
11006
+ id: "pre-commit-gate",
11007
+ name: "pre-commit gate hook",
11008
+ live,
11009
+ detail: live ? "`cdd-kit gate` runs before each commit touching specs/contracts" : "dormant \u2014 run `cdd-kit install-hooks` to block commits that fail the gate"
11010
+ };
11011
+ }
11012
+ function probeOpenApiGate(cwd) {
11013
+ let where = "";
11014
+ const pkgPath = join27(cwd, "package.json");
11015
+ if (existsSync25(pkgPath)) {
11016
+ try {
11017
+ const pkg2 = JSON.parse(safeRead2(pkgPath));
11018
+ const scripts = pkg2.scripts ?? {};
11019
+ for (const [name, cmd] of Object.entries(scripts)) {
11020
+ if (typeof cmd === "string" && cmd.includes(OPENAPI_CHECK_MARKER)) {
11021
+ where = `package.json script \`${name}\``;
11022
+ break;
11023
+ }
11024
+ }
11025
+ } catch {
11026
+ }
11027
+ }
11028
+ if (!where) {
11029
+ const wfDir = join27(cwd, ".github", "workflows");
11030
+ if (existsSync25(wfDir)) {
11031
+ try {
11032
+ for (const entry of readdirSync12(wfDir)) {
11033
+ if (!/\.ya?ml$/.test(entry))
11034
+ continue;
11035
+ if (safeRead2(join27(wfDir, entry)).includes(OPENAPI_CHECK_MARKER)) {
11036
+ where = `CI workflow ${entry}`;
11037
+ break;
11038
+ }
11039
+ }
11040
+ } catch {
11041
+ }
11042
+ }
11043
+ }
11044
+ const live = where !== "";
11045
+ return {
11046
+ id: "openapi-sync-gate",
11047
+ name: "OpenAPI sync gate",
11048
+ live,
11049
+ detail: live ? `\`cdd-kit openapi export --check\` runs in ${where}` : "dormant \u2014 wire `cdd-kit openapi export --check --out <artifact>` into CI or a package.json script (or run `cdd-kit init`)"
11050
+ };
11051
+ }
11052
+ function detectChokepoints(cwd) {
11053
+ return [
11054
+ probeGraphFirst(cwd),
11055
+ probePreCommitGate(cwd),
11056
+ probeOpenApiGate(cwd)
11057
+ ];
11058
+ }
11059
+ var GRAPH_FIRST_MARKER, PRECOMMIT_MARKER, OPENAPI_CHECK_MARKER;
11060
+ var init_chokepoints = __esm({
11061
+ "src/commands/chokepoints.ts"() {
11062
+ "use strict";
11063
+ GRAPH_FIRST_MARKER = "pre-tool-use-graph-first";
11064
+ PRECOMMIT_MARKER = "# cdd-kit-managed-block-start";
11065
+ OPENAPI_CHECK_MARKER = "openapi export --check";
11066
+ }
11067
+ });
11068
+
10516
11069
  // src/commands/doctor.ts
10517
11070
  var doctor_exports = {};
10518
11071
  __export(doctor_exports, {
10519
11072
  doctor: () => doctor
10520
11073
  });
10521
- import { existsSync as existsSync21, readdirSync as readdirSync12, readFileSync as readFileSync25 } from "fs";
11074
+ import { existsSync as existsSync26, readdirSync as readdirSync13, readFileSync as readFileSync30 } from "fs";
10522
11075
  import { createHash as createHash8 } from "crypto";
10523
- import { join as join24, relative as relative8 } from "path";
11076
+ import { spawnSync as spawnSync6 } from "child_process";
11077
+ import { join as join28, relative as relative8 } from "path";
10524
11078
  function fileExists(cwd, relPath) {
10525
- return existsSync21(join24(cwd, relPath));
11079
+ return existsSync26(join28(cwd, relPath));
10526
11080
  }
10527
11081
  function findFiles(dir, predicate, found = []) {
10528
- if (!existsSync21(dir))
11082
+ if (!existsSync26(dir))
10529
11083
  return found;
10530
- for (const entry of readdirSync12(dir, { withFileTypes: true })) {
10531
- const fullPath = join24(dir, entry.name);
11084
+ for (const entry of readdirSync13(dir, { withFileTypes: true })) {
11085
+ const fullPath = join28(dir, entry.name);
10532
11086
  if (entry.isDirectory())
10533
11087
  findFiles(fullPath, predicate, found);
10534
11088
  else if (entry.isFile() && predicate(entry.name))
@@ -10544,9 +11098,9 @@ function inputDigest(paths, cwd) {
10544
11098
  return createHash8("sha256").update(combined).digest("hex");
10545
11099
  }
10546
11100
  function readContextIndexMetadata(filePath) {
10547
- if (!existsSync21(filePath))
11101
+ if (!existsSync26(filePath))
10548
11102
  return {};
10549
- const text = readFileSync25(filePath, "utf8");
11103
+ const text = readFileSync30(filePath, "utf8");
10550
11104
  const out = {};
10551
11105
  const digestMatch = text.match(/^inputs-digest:\s*([a-f0-9]+)/m);
10552
11106
  if (digestMatch)
@@ -10558,14 +11112,14 @@ function readContextIndexMetadata(filePath) {
10558
11112
  }
10559
11113
  function checkContextFreshness(cwd) {
10560
11114
  const findings = [];
10561
- const projectMap = join24(cwd, "specs", "context", "project-map.md");
10562
- const contractsIndex = join24(cwd, "specs", "context", "contracts-index.md");
10563
- const contextPolicy = join24(cwd, ".cdd", "context-policy.json");
11115
+ const projectMap = join28(cwd, "specs", "context", "project-map.md");
11116
+ const contractsIndex = join28(cwd, "specs", "context", "contracts-index.md");
11117
+ const contextPolicy = join28(cwd, ".cdd", "context-policy.json");
10564
11118
  const contractFiles = findFiles(
10565
- join24(cwd, "contracts"),
11119
+ join28(cwd, "contracts"),
10566
11120
  (name) => name.endsWith(".md") && name !== "INDEX.md" && name !== "CHANGELOG.md"
10567
11121
  );
10568
- if (!existsSync21(projectMap) || !existsSync21(contractsIndex)) {
11122
+ if (!existsSync26(projectMap) || !existsSync26(contractsIndex)) {
10569
11123
  findings.push({
10570
11124
  level: "warning",
10571
11125
  message: "specs/context indexes are missing; run cdd-kit context-scan before classification"
@@ -10574,7 +11128,7 @@ function checkContextFreshness(cwd) {
10574
11128
  }
10575
11129
  const projectMapMeta = readContextIndexMetadata(projectMap);
10576
11130
  const contractsIndexMeta = readContextIndexMetadata(contractsIndex);
10577
- const projectInputDigest = inputDigest([contextPolicy].filter(existsSync21), cwd);
11131
+ const projectInputDigest = inputDigest([contextPolicy].filter(existsSync26), cwd);
10578
11132
  if (projectMapMeta.inputsDigest === void 0) {
10579
11133
  findings.push({
10580
11134
  level: "warning",
@@ -10611,7 +11165,7 @@ function checkContextFreshness(cwd) {
10611
11165
  }
10612
11166
  function readAgentModel(path) {
10613
11167
  try {
10614
- const text = readFileSync25(path, "utf8");
11168
+ const text = readFileSync30(path, "utf8");
10615
11169
  const m = text.match(/^model:\s*(\S+)/m);
10616
11170
  return m ? m[1] : null;
10617
11171
  } catch {
@@ -10619,12 +11173,12 @@ function readAgentModel(path) {
10619
11173
  }
10620
11174
  }
10621
11175
  function checkModelPolicyDrift(cwd) {
10622
- const policyPath = join24(cwd, ".cdd", "model-policy.json");
10623
- if (!existsSync21(policyPath))
11176
+ const policyPath = join28(cwd, ".cdd", "model-policy.json");
11177
+ if (!existsSync26(policyPath))
10624
11178
  return [];
10625
11179
  let policy;
10626
11180
  try {
10627
- policy = JSON.parse(readFileSync25(policyPath, "utf8"));
11181
+ policy = JSON.parse(readFileSync30(policyPath, "utf8"));
10628
11182
  } catch {
10629
11183
  return [{ level: "warning", message: ".cdd/model-policy.json is not valid JSON" }];
10630
11184
  }
@@ -10636,18 +11190,18 @@ function checkModelPolicyDrift(cwd) {
10636
11190
  }];
10637
11191
  }
10638
11192
  const candidateDirs = [
10639
- join24(cwd, ".claude", "agents"),
10640
- process.env.HOME ? join24(process.env.HOME, ".claude", "agents") : "",
10641
- process.env.USERPROFILE ? join24(process.env.USERPROFILE, ".claude", "agents") : ""
10642
- ].filter((p) => p && existsSync21(p));
11193
+ join28(cwd, ".claude", "agents"),
11194
+ process.env.HOME ? join28(process.env.HOME, ".claude", "agents") : "",
11195
+ process.env.USERPROFILE ? join28(process.env.USERPROFILE, ".claude", "agents") : ""
11196
+ ].filter((p) => p && existsSync26(p));
10643
11197
  if (candidateDirs.length === 0)
10644
11198
  return [];
10645
11199
  const findings = [];
10646
11200
  for (const [role, expected] of Object.entries(roles)) {
10647
11201
  let foundAny = false;
10648
11202
  for (const dir of candidateDirs) {
10649
- const path = join24(dir, `${role}.md`);
10650
- if (!existsSync21(path))
11203
+ const path = join28(dir, `${role}.md`);
11204
+ if (!existsSync26(path))
10651
11205
  continue;
10652
11206
  foundAny = true;
10653
11207
  const actual = readAgentModel(path);
@@ -10667,14 +11221,14 @@ function checkModelPolicyDrift(cwd) {
10667
11221
  return findings;
10668
11222
  }
10669
11223
  function checkAgentLint(cwd) {
10670
- const agentsDir = join24(cwd, ".claude", "agents");
10671
- if (!existsSync21(agentsDir))
11224
+ const agentsDir = join28(cwd, ".claude", "agents");
11225
+ if (!existsSync26(agentsDir))
10672
11226
  return [];
10673
11227
  const violations = collectAgentViolations(cwd);
10674
11228
  if (violations === null) {
10675
11229
  return [{
10676
11230
  level: "warning",
10677
- message: `lint-agents: could not read ${join24(".claude", "agents")} -- agent prompts were not scanned`
11231
+ message: `lint-agents: could not read ${join28(".claude", "agents")} -- agent prompts were not scanned`
10678
11232
  }];
10679
11233
  }
10680
11234
  const findings = violations.map((v) => ({
@@ -10688,8 +11242,8 @@ function checkAgentLint(cwd) {
10688
11242
  }
10689
11243
  function checkCodeMap(cwd) {
10690
11244
  const findings = [];
10691
- const mapPath = join24(cwd, ".cdd", "code-map.yml");
10692
- if (!existsSync21(mapPath)) {
11245
+ const mapPath = join28(cwd, ".cdd", "code-map.yml");
11246
+ if (!existsSync26(mapPath)) {
10693
11247
  const probe2 = checkCodeMapFreshness(cwd);
10694
11248
  if (probe2.status === "config-error") {
10695
11249
  findings.push({ level: "warning", message: `.cdd/code-map-config.yml is invalid: ${probe2.configError}` });
@@ -10708,7 +11262,7 @@ function checkCodeMap(cwd) {
10708
11262
  const more = probe.staleCount > 3 ? ` (+${probe.staleCount - 3} more)` : "";
10709
11263
  findings.push({ level: "warning", message: `code-map stale: ${top}${more}; run \`cdd-kit code-map\`` });
10710
11264
  }
10711
- const text = readFileSync25(mapPath, "utf8");
11265
+ const text = readFileSync30(mapPath, "utf8");
10712
11266
  const m = text.match(/^# files: (\d+), src-lines: (\d+), map-lines: (\d+), compression: ([\d.]+)x/m);
10713
11267
  if (m) {
10714
11268
  findings.push({ level: "ok", message: `code-map: ${m[1]} files, ${m[4]}x compression` });
@@ -10717,6 +11271,68 @@ function checkCodeMap(cwd) {
10717
11271
  }
10718
11272
  return findings;
10719
11273
  }
11274
+ function checkApiConformance(cwd) {
11275
+ const hasContract = existsSync26(join28(cwd, "contracts", "api", "api-contract.md"));
11276
+ if (!hasContract)
11277
+ return [];
11278
+ const configPath = join28(cwd, ".cdd", "conformance.json");
11279
+ if (!existsSync26(configPath)) {
11280
+ return [{
11281
+ level: "ok",
11282
+ message: 'API conformance: not configured (add .cdd/conformance.json with "enabled": true to catch frontend/backend drift against the contract)'
11283
+ }];
11284
+ }
11285
+ try {
11286
+ const cfg = JSON.parse(readFileSync30(configPath, "utf8"));
11287
+ return [{
11288
+ level: "ok",
11289
+ message: cfg.enabled ? "API conformance: enabled (cdd-kit validate --contracts checks code against the API contract)" : 'API conformance: present but disabled (set "enabled": true in .cdd/conformance.json to enforce code-vs-contract checks)'
11290
+ }];
11291
+ } catch {
11292
+ return [{ level: "warning", message: ".cdd/conformance.json is not valid JSON" }];
11293
+ }
11294
+ }
11295
+ function checkMcpRegistration(cwd, provider) {
11296
+ if (provider !== "claude" && provider !== "both")
11297
+ return [];
11298
+ if (!existsSync26(join28(cwd, ".cdd")))
11299
+ return [];
11300
+ const bin = process.env.CDD_CLAUDE_BIN || "claude";
11301
+ let result;
11302
+ try {
11303
+ result = bin.toLowerCase().endsWith(".js") ? spawnSync6(process.execPath, [bin, "mcp", "list"], { encoding: "utf8", timeout: 3e3 }) : spawnSync6(bin, ["mcp", "list"], { encoding: "utf8", timeout: 3e3 });
11304
+ } catch {
11305
+ result = { error: new Error("spawn failed") };
11306
+ }
11307
+ if (result.error || typeof result.status !== "number") {
11308
+ return [{
11309
+ level: "ok",
11310
+ message: "MCP: could not run `claude mcp list` (Claude Code CLI not found); skip if you do not use Claude Code"
11311
+ }];
11312
+ }
11313
+ if (result.status !== 0) {
11314
+ return [{
11315
+ level: "ok",
11316
+ message: `MCP: \`claude mcp list\` exited ${result.status}; cannot verify cdd-kit registration`
11317
+ }];
11318
+ }
11319
+ const output = `${result.stdout ?? ""}${result.stderr ?? ""}`;
11320
+ if (/\bcdd-kit\b/.test(output)) {
11321
+ return [{ level: "ok", message: "MCP: cdd-kit server is registered with Claude Code" }];
11322
+ }
11323
+ return [{
11324
+ level: "ok",
11325
+ message: "MCP: cdd-kit not registered \u2014 agents will fall back to Read instead of graph/index tools; run `claude mcp add --scope user cdd-kit -- cdd-kit mcp`"
11326
+ }];
11327
+ }
11328
+ function checkChokepoints(cwd) {
11329
+ if (!existsSync26(join28(cwd, ".cdd")))
11330
+ return [];
11331
+ return detectChokepoints(cwd).map((c) => ({
11332
+ level: "ok",
11333
+ message: c.live ? `chokepoint ${c.name}: live \u2014 ${c.detail}` : `chokepoint ${c.name}: ${c.detail}`
11334
+ }));
11335
+ }
10720
11336
  async function buildDoctorReport(cwd, opts) {
10721
11337
  const requestedProvider = opts.provider ?? "auto";
10722
11338
  if (!validateProviderOption(requestedProvider)) {
@@ -10742,6 +11358,9 @@ async function buildDoctorReport(cwd, opts) {
10742
11358
  findings.push(...checkModelPolicyDrift(cwd));
10743
11359
  findings.push(...checkAgentLint(cwd));
10744
11360
  findings.push(...checkCodeMap(cwd));
11361
+ findings.push(...checkApiConformance(cwd));
11362
+ findings.push(...checkMcpRegistration(cwd, provider));
11363
+ findings.push(...checkChokepoints(cwd));
10745
11364
  const errors = findings.filter((finding) => finding.level === "error").length;
10746
11365
  const warnings = findings.filter((finding) => finding.level === "warning").length;
10747
11366
  return {
@@ -10784,11 +11403,11 @@ async function attemptAutoFixes(cwd, report) {
10784
11403
  }
10785
11404
  }
10786
11405
  if (/model-policy\.json has no role bindings/i.test(finding.message)) {
10787
- const policyPath = join24(cwd, ".cdd", "model-policy.json");
11406
+ const policyPath = join28(cwd, ".cdd", "model-policy.json");
10788
11407
  try {
10789
11408
  let existing = {};
10790
11409
  try {
10791
- existing = JSON.parse(readFileSync25(policyPath, "utf8"));
11410
+ existing = JSON.parse(readFileSync30(policyPath, "utf8"));
10792
11411
  } catch {
10793
11412
  }
10794
11413
  const merged = {
@@ -10813,8 +11432,8 @@ async function attemptAutoFixes(cwd, report) {
10813
11432
  "repo-context-scanner": "haiku"
10814
11433
  }
10815
11434
  };
10816
- const { writeFileSync: writeFileSync16 } = await import("fs");
10817
- writeFileSync16(policyPath, JSON.stringify(merged, null, 2) + "\n", "utf8");
11435
+ const { writeFileSync: writeFileSync19 } = await import("fs");
11436
+ writeFileSync19(policyPath, JSON.stringify(merged, null, 2) + "\n", "utf8");
10818
11437
  fixed.push(`populated .cdd/model-policy.json with default role bindings`);
10819
11438
  continue;
10820
11439
  } catch (err) {
@@ -10883,16 +11502,188 @@ var init_doctor = __esm({
10883
11502
  init_provider();
10884
11503
  init_freshness();
10885
11504
  init_lint_agents();
11505
+ init_chokepoints();
10886
11506
  init_digest();
10887
11507
  }
10888
11508
  });
10889
11509
 
10890
- // src/commands/code-map-scan-worker.ts
10891
- var code_map_scan_worker_exports = {};
10892
- __export(code_map_scan_worker_exports, {
10893
- runScanWorker: () => runScanWorker
11510
+ // src/commands/code-map-watch.ts
11511
+ var code_map_watch_exports = {};
11512
+ __export(code_map_watch_exports, {
11513
+ codeMapWatch: () => codeMapWatch,
11514
+ createDebouncedRunner: () => createDebouncedRunner,
11515
+ generatedArtifactSet: () => generatedArtifactSet,
11516
+ makeWatchExcludeFilter: () => makeWatchExcludeFilter
10894
11517
  });
10895
- import { readFileSync as readFileSync26 } from "fs";
11518
+ import { watch } from "fs";
11519
+ import { resolve as resolve5 } from "path";
11520
+ import picomatch2 from "picomatch";
11521
+ function createDebouncedRunner(run, debounceMs) {
11522
+ let timer = null;
11523
+ let running = false;
11524
+ let queued = false;
11525
+ async function fire() {
11526
+ if (running) {
11527
+ queued = true;
11528
+ return;
11529
+ }
11530
+ running = true;
11531
+ try {
11532
+ await run();
11533
+ } finally {
11534
+ running = false;
11535
+ if (queued) {
11536
+ queued = false;
11537
+ void fire();
11538
+ }
11539
+ }
11540
+ }
11541
+ function trigger() {
11542
+ if (timer)
11543
+ clearTimeout(timer);
11544
+ timer = setTimeout(() => {
11545
+ timer = null;
11546
+ void fire();
11547
+ }, debounceMs);
11548
+ }
11549
+ function dispose() {
11550
+ if (timer) {
11551
+ clearTimeout(timer);
11552
+ timer = null;
11553
+ }
11554
+ }
11555
+ return { trigger, dispose };
11556
+ }
11557
+ function generatedArtifactSet(outRel, cwd = process.cwd()) {
11558
+ return /* @__PURE__ */ new Set([
11559
+ resolve5(cwd, outRel),
11560
+ resolve5(cwd, sidecarPathFor(outRel)),
11561
+ resolve5(cwd, graphPathFor(outRel))
11562
+ ]);
11563
+ }
11564
+ function makeWatchExcludeFilter(excludes, configRel = CONFIG_REL_PATH) {
11565
+ if (excludes.length === 0)
11566
+ return () => false;
11567
+ const match = picomatch2(excludes, { dot: true });
11568
+ return (rel) => rel !== configRel && (match(rel) || match(`${rel}/**`));
11569
+ }
11570
+ async function codeMapWatch(opts) {
11571
+ const debounceMs = opts.debounceMs ?? 500;
11572
+ const pollMs = opts.pollMs ?? 2e3;
11573
+ const scanPath = opts.surface ?? opts.path;
11574
+ const root = resolve5(process.cwd(), scanPath);
11575
+ const outRel = opts.out ?? (opts.surface ? `.cdd/code-map.${slugifySurface(opts.surface)}.yml` : ".cdd/code-map.yml");
11576
+ const generatedAbs = generatedArtifactSet(outRel);
11577
+ let isExcluded = () => false;
11578
+ try {
11579
+ const cfg = loadCodeMapConfig(root);
11580
+ isExcluded = makeWatchExcludeFilter([...cfg.exclude, ...opts.exclude ?? []]);
11581
+ } catch {
11582
+ }
11583
+ const rebuild = async () => {
11584
+ const exit = await codeMap({ ...opts, check: false, silent: true });
11585
+ if (exit === 0) {
11586
+ log.ok(`code-map refreshed (${(/* @__PURE__ */ new Date()).toLocaleTimeString()})`);
11587
+ } else {
11588
+ log.warn("code-map refresh reported a problem; map left unchanged where possible.");
11589
+ }
11590
+ return exit;
11591
+ };
11592
+ log.info(`code-map --watch: building initial map for ${scanPath}\u2026`);
11593
+ const initialExit = await rebuild();
11594
+ if (initialExit !== 0) {
11595
+ log.error("code-map --watch: initial build failed; not starting the watcher.");
11596
+ return initialExit;
11597
+ }
11598
+ const { trigger, dispose } = createDebouncedRunner(() => rebuild().then(() => void 0), debounceMs);
11599
+ let watcher = null;
11600
+ let pollTimer = null;
11601
+ let driftChecking = false;
11602
+ const triggerIfDrift = () => {
11603
+ if (driftChecking)
11604
+ return;
11605
+ driftChecking = true;
11606
+ void codeMap({ ...opts, check: true, silent: true }).then((drift) => {
11607
+ if (drift !== 0)
11608
+ trigger();
11609
+ }).finally(() => {
11610
+ driftChecking = false;
11611
+ });
11612
+ };
11613
+ const startPolling = (reason) => {
11614
+ if (pollTimer)
11615
+ return;
11616
+ if (reason)
11617
+ log.warn(reason);
11618
+ pollTimer = setInterval(triggerIfDrift, pollMs);
11619
+ log.ok(`polling ${scanPath} every ${pollMs}ms. Ctrl-C to stop.`);
11620
+ };
11621
+ const isSelfWrite = (filename) => {
11622
+ return generatedAbs.has(resolve5(root, filename));
11623
+ };
11624
+ try {
11625
+ watcher = watch(root, { recursive: true }, (_event, filename) => {
11626
+ if (filename) {
11627
+ const rel = filename.toString();
11628
+ if (isSelfWrite(rel))
11629
+ return;
11630
+ if (isExcluded(rel.replace(/\\/g, "/")))
11631
+ return;
11632
+ trigger();
11633
+ } else {
11634
+ triggerIfDrift();
11635
+ }
11636
+ });
11637
+ watcher.on("error", (err) => {
11638
+ log.warn(`watch error (${err.message}); closing watcher and falling back to polling.`);
11639
+ try {
11640
+ watcher?.close();
11641
+ } catch {
11642
+ }
11643
+ watcher = null;
11644
+ startPolling();
11645
+ });
11646
+ log.ok(`watching ${scanPath} (recursive, debounce ${debounceMs}ms). Ctrl-C to stop.`);
11647
+ } catch {
11648
+ startPolling("recursive fs.watch unavailable on this platform; falling back to freshness polling.");
11649
+ }
11650
+ return await new Promise((resolvePromise) => {
11651
+ const stop = () => {
11652
+ dispose();
11653
+ if (watcher)
11654
+ watcher.close();
11655
+ if (pollTimer)
11656
+ clearInterval(pollTimer);
11657
+ log.info("code-map --watch stopped.");
11658
+ resolvePromise(0);
11659
+ };
11660
+ const signal = opts.signal;
11661
+ if (signal) {
11662
+ if (signal.aborted) {
11663
+ stop();
11664
+ return;
11665
+ }
11666
+ signal.addEventListener("abort", stop, { once: true });
11667
+ }
11668
+ });
11669
+ }
11670
+ var init_code_map_watch = __esm({
11671
+ "src/commands/code-map-watch.ts"() {
11672
+ "use strict";
11673
+ init_logger();
11674
+ init_code_map();
11675
+ init_config();
11676
+ init_index_reader();
11677
+ init_reader();
11678
+ }
11679
+ });
11680
+
11681
+ // src/commands/code-map-scan-worker.ts
11682
+ var code_map_scan_worker_exports = {};
11683
+ __export(code_map_scan_worker_exports, {
11684
+ runScanWorker: () => runScanWorker
11685
+ });
11686
+ import { readFileSync as readFileSync31 } from "fs";
10896
11687
  async function runScanWorker(opts) {
10897
11688
  let scanner;
10898
11689
  switch (opts.lang) {
@@ -10912,7 +11703,7 @@ async function runScanWorker(opts) {
10912
11703
  }
10913
11704
  let files;
10914
11705
  try {
10915
- files = readFileSync26(opts.batchFile, "utf8").split("\n").map((s) => s.trim()).filter(Boolean);
11706
+ files = readFileSync31(opts.batchFile, "utf8").split("\n").map((s) => s.trim()).filter(Boolean);
10916
11707
  } catch (err) {
10917
11708
  process.stderr.write(`__code-map-scan: cannot read --batch-file: ${err.message}
10918
11709
  `);
@@ -10932,10 +11723,16 @@ var init_code_map_scan_worker = __esm({
10932
11723
  // src/commands/index-query.ts
10933
11724
  var index_query_exports = {};
10934
11725
  __export(index_query_exports, {
11726
+ DEFAULT_SOURCE_BUDGET: () => DEFAULT_SOURCE_BUDGET,
10935
11727
  indexQuery: () => indexQuery,
10936
- queryEntries: () => queryEntries
11728
+ queryEntries: () => queryEntries,
11729
+ resolveSourceBudget: () => resolveSourceBudget,
11730
+ surfaceRootFor: () => surfaceRootFor
10937
11731
  });
10938
- import { existsSync as existsSync22 } from "fs";
11732
+ import { existsSync as existsSync27, readFileSync as readFileSync32 } from "fs";
11733
+ function resolveSourceBudget(budget) {
11734
+ return Number.isFinite(budget) && budget > 0 ? Math.floor(budget) : DEFAULT_SOURCE_BUDGET;
11735
+ }
10939
11736
  async function indexQuery(term, opts) {
10940
11737
  const mapPath = opts.map || ".cdd/code-map.yml";
10941
11738
  const limit = Number.isFinite(opts.limit) && opts.limit > 0 ? Math.floor(opts.limit) : 10;
@@ -10945,7 +11742,7 @@ async function indexQuery(term, opts) {
10945
11742
  return printFailure(freshness.error, opts.json);
10946
11743
  }
10947
11744
  refreshed = freshness.refreshed;
10948
- if (!existsSync22(mapPath)) {
11745
+ if (!existsSync27(mapPath)) {
10949
11746
  return printFailure(`${mapPath} is missing; run \`cdd-kit code-map\` first.`, opts.json);
10950
11747
  }
10951
11748
  let entries;
@@ -10955,6 +11752,9 @@ async function indexQuery(term, opts) {
10955
11752
  return printFailure(`${mapPath} is not readable YAML: ${err.message}`, opts.json);
10956
11753
  }
10957
11754
  const results = queryEntries(entries, term).slice(0, limit);
11755
+ if (opts.withSource) {
11756
+ attachSource(results, resolveSourceBudget(opts.sourceBudget), surfaceRootFor(mapPath));
11757
+ }
10958
11758
  const payload = {
10959
11759
  index: mapPath,
10960
11760
  query: term,
@@ -11073,6 +11873,68 @@ function firstLine(lines) {
11073
11873
  const m = lines?.match(/^\d+/);
11074
11874
  return m ? Number(m[0]) : Number.MAX_SAFE_INTEGER;
11075
11875
  }
11876
+ function surfaceRootFor(mapPath) {
11877
+ try {
11878
+ if (!existsSync27(mapPath))
11879
+ return void 0;
11880
+ const head = readFileSync32(mapPath, "utf8").slice(0, 4096);
11881
+ const m = head.match(/^# surface-root:\s*(.+)$/m);
11882
+ return m ? m[1].trim() : void 0;
11883
+ } catch {
11884
+ return void 0;
11885
+ }
11886
+ }
11887
+ function attachSource(results, budget, baseDir) {
11888
+ const fileCache = /* @__PURE__ */ new Map();
11889
+ let used = 0;
11890
+ const readLines = (path) => {
11891
+ if (fileCache.has(path))
11892
+ return fileCache.get(path) ?? null;
11893
+ let lines = null;
11894
+ try {
11895
+ const resolved = baseDir && baseDir !== "." ? `${baseDir.replace(/\/+$/, "")}/${path}` : path;
11896
+ lines = existsSync27(resolved) ? readFileSync32(resolved, "utf8").split(/\r?\n/) : null;
11897
+ } catch {
11898
+ lines = null;
11899
+ }
11900
+ fileCache.set(path, lines);
11901
+ return lines;
11902
+ };
11903
+ for (const result of results) {
11904
+ const fileLines = readLines(result.path);
11905
+ if (!fileLines)
11906
+ continue;
11907
+ for (const match of result.matches) {
11908
+ const range = resolveRange(match);
11909
+ if (!range)
11910
+ continue;
11911
+ const [start, end] = range;
11912
+ if (used >= budget) {
11913
+ match.source_truncated = true;
11914
+ continue;
11915
+ }
11916
+ const allowedEnd = Math.min(end, start + (budget - used) - 1);
11917
+ const slice = fileLines.slice(start - 1, allowedEnd);
11918
+ match.source = slice.join("\n");
11919
+ used += slice.length;
11920
+ if (allowedEnd < end)
11921
+ match.source_truncated = true;
11922
+ }
11923
+ }
11924
+ }
11925
+ function resolveRange(match) {
11926
+ if (match.lines) {
11927
+ const m = match.lines.match(/^(\d+)\s*-\s*(\d+)$/);
11928
+ if (m)
11929
+ return [Number(m[1]), Number(m[2])];
11930
+ const single = match.lines.match(/^(\d+)$/);
11931
+ if (single)
11932
+ return [Number(single[1]), Number(single[1])];
11933
+ }
11934
+ if (typeof match.line === "number")
11935
+ return [match.line, match.line];
11936
+ return null;
11937
+ }
11076
11938
  function printText(payload) {
11077
11939
  if (payload.results.length === 0) {
11078
11940
  console.log(`No matches for "${payload.query}" in ${payload.index}.`);
@@ -11088,9 +11950,18 @@ function printText(payload) {
11088
11950
  const loc = match.lines ? ` lines ${match.lines}` : match.line ? ` line ${match.line}` : "";
11089
11951
  const detail = match.detail && match.detail !== match.name ? ` - ${match.detail}` : "";
11090
11952
  console.log(` - ${match.kind}: ${match.name}${loc}${detail}`);
11953
+ if (match.source !== void 0) {
11954
+ for (const srcLine of match.source.split("\n")) {
11955
+ console.log(` | ${srcLine}`);
11956
+ }
11957
+ if (match.source_truncated)
11958
+ console.log(" | \u2026 (source budget reached; Read the rest directly)");
11959
+ } else if (match.source_truncated) {
11960
+ console.log(" (source budget reached; Read this range directly)");
11961
+ }
11091
11962
  }
11092
11963
  }
11093
- console.log("Next: read only the listed file/ranges first.");
11964
+ console.log(payload.results.some((r) => r.matches.some((m) => m.source !== void 0)) ? "Source included above \u2014 no separate Read needed for these ranges." : "Next: read only the listed file/ranges first.");
11094
11965
  }
11095
11966
  function printFailure(message, json) {
11096
11967
  if (json) {
@@ -11101,10 +11972,12 @@ function printFailure(message, json) {
11101
11972
  }
11102
11973
  return 1;
11103
11974
  }
11975
+ var DEFAULT_SOURCE_BUDGET;
11104
11976
  var init_index_query = __esm({
11105
11977
  "src/commands/index-query.ts"() {
11106
11978
  "use strict";
11107
11979
  init_index_reader();
11980
+ DEFAULT_SOURCE_BUDGET = 400;
11108
11981
  }
11109
11982
  });
11110
11983
 
@@ -11113,7 +11986,7 @@ var index_impact_exports = {};
11113
11986
  __export(index_impact_exports, {
11114
11987
  indexImpact: () => indexImpact
11115
11988
  });
11116
- import { existsSync as existsSync23 } from "fs";
11989
+ import { existsSync as existsSync28 } from "fs";
11117
11990
  import { posix as posix2 } from "path";
11118
11991
  async function indexImpact(term, opts) {
11119
11992
  const mapPath = opts.map || ".cdd/code-map.yml";
@@ -11122,7 +11995,7 @@ async function indexImpact(term, opts) {
11122
11995
  if (freshness.error) {
11123
11996
  return printFailure2(freshness.error, opts.json);
11124
11997
  }
11125
- if (!existsSync23(mapPath)) {
11998
+ if (!existsSync28(mapPath)) {
11126
11999
  return printFailure2(`${mapPath} is missing; run \`cdd-kit code-map\` first.`, opts.json);
11127
12000
  }
11128
12001
  let entries;
@@ -11476,22 +12349,22 @@ __export(graph_exports, {
11476
12349
  graphStatus: () => graphStatus,
11477
12350
  graphSync: () => graphSync
11478
12351
  });
11479
- import { existsSync as existsSync24 } from "fs";
11480
- import { join as join25 } from "path";
11481
- import { spawnSync as spawnSync3 } from "child_process";
12352
+ import { existsSync as existsSync29, readFileSync as readFileSync33 } from "fs";
12353
+ import { join as join29 } from "path";
12354
+ import { spawnSync as spawnSync7 } from "child_process";
11482
12355
  function codeGraphCommand() {
11483
12356
  return process.env.CDD_CODEGRAPH_BIN || "codegraph";
11484
12357
  }
11485
12358
  function runCodeGraph(args, cwd = process.cwd()) {
11486
12359
  const command = codeGraphCommand();
11487
12360
  if (command.toLowerCase().endsWith(".js")) {
11488
- return spawnSync3(process.execPath, [command, ...args], { cwd, encoding: "buffer" });
12361
+ return spawnSync7(process.execPath, [command, ...args], { cwd, encoding: "buffer" });
11489
12362
  }
11490
- return spawnSync3(command, args, { cwd, encoding: "buffer" });
12363
+ return spawnSync7(command, args, { cwd, encoding: "buffer" });
11491
12364
  }
11492
12365
  function probeCodeGraph(cwd = process.cwd()) {
11493
12366
  const command = codeGraphCommand();
11494
- const initialized = existsSync24(join25(cwd, ".codegraph"));
12367
+ const initialized = existsSync29(join29(cwd, ".codegraph"));
11495
12368
  const version = runCodeGraph(["--version"], cwd);
11496
12369
  if (version.error) {
11497
12370
  return {
@@ -11561,7 +12434,7 @@ async function ensureNativeGraph(mapPath, refresh2) {
11561
12434
  if (freshness.error)
11562
12435
  return { graphPath: graphPathFor(mapPath), refreshed: freshness.refreshed, error: freshness.error };
11563
12436
  const graphPath = graphPathFor(mapPath);
11564
- if (!existsSync24(graphPath) && refresh2) {
12437
+ if (!existsSync29(graphPath) && refresh2) {
11565
12438
  const { codeMap: codeMap2 } = await Promise.resolve().then(() => (init_code_map(), code_map_exports));
11566
12439
  const exit = await codeMap2({
11567
12440
  path: ".",
@@ -11594,7 +12467,7 @@ async function graphStatus(opts = {}) {
11594
12467
  const graphPath = graphPathFor(mapPath);
11595
12468
  const freshness = checkCodeMapFreshness(cwd, mapPath);
11596
12469
  let graphStats;
11597
- if (existsSync24(graphPath)) {
12470
+ if (existsSync29(graphPath)) {
11598
12471
  try {
11599
12472
  const graph2 = loadCodeGraph(graphPath);
11600
12473
  graphStats = { graph: graphPath, nodes: graph2.nodes.length, edges: graph2.edges.length, unresolved: graph2.unresolved.length };
@@ -11663,8 +12536,10 @@ async function graphQuery(term, opts) {
11663
12536
  try {
11664
12537
  const graph2 = loadCodeGraph(ensured.graphPath);
11665
12538
  const results = searchGraph(graph2, term, opts.limit);
12539
+ const sources = opts.withSource ? collectNodeSources(results.map((r) => r.node), resolveSourceBudget(opts.sourceBudget), surfaceRootFor(mapPath)) : /* @__PURE__ */ new Map();
11666
12540
  if (opts.json) {
11667
- writeJson({ engine: "native", graph: ensured.graphPath, query: term, refreshed: ensured.refreshed, results });
12541
+ const withSrc = opts.withSource ? results.map((r) => ({ ...r, source: sources.get(r.node.id)?.source, source_truncated: sources.get(r.node.id)?.truncated })) : results;
12542
+ writeJson({ engine: "native", graph: ensured.graphPath, query: term, refreshed: ensured.refreshed, results: withSrc });
11668
12543
  } else {
11669
12544
  console.log(`graph: ${ensured.graphPath}${ensured.refreshed ? " (refreshed)" : ""}`);
11670
12545
  console.log(`query: ${term}`);
@@ -11673,8 +12548,15 @@ async function graphQuery(term, opts) {
11673
12548
  const n = result.node;
11674
12549
  console.log(`- ${n.kind}: ${n.qualified_name} lines ${n.start_line}-${n.end_line}`);
11675
12550
  console.log(` edges: ${result.edges.incoming} incoming, ${result.edges.outgoing} outgoing`);
12551
+ const src = sources.get(n.id);
12552
+ if (src) {
12553
+ for (const srcLine of src.source.split("\n"))
12554
+ console.log(` | ${srcLine}`);
12555
+ if (src.truncated)
12556
+ console.log(" | \u2026 (source budget reached; Read the rest directly)");
12557
+ }
11676
12558
  }
11677
- console.log("Next: run cdd-kit graph impact <node/file/symbol> before editing.");
12559
+ console.log(opts.withSource ? "Source included above \u2014 no separate Read needed for these ranges." : "Next: run cdd-kit graph impact <node/file/symbol> before editing.");
11678
12560
  }
11679
12561
  return results.length === 0 ? 1 : 0;
11680
12562
  } catch (err) {
@@ -11685,9 +12567,44 @@ async function graphQuery(term, opts) {
11685
12567
  map: opts.map || ".cdd/code-map.yml",
11686
12568
  limit: opts.limit,
11687
12569
  json: opts.json === true,
11688
- refresh: opts.refresh !== false
12570
+ refresh: opts.refresh !== false,
12571
+ withSource: opts.withSource === true,
12572
+ sourceBudget: resolveSourceBudget(opts.sourceBudget)
11689
12573
  });
11690
12574
  }
12575
+ function collectNodeSources(nodes, budget, baseDir) {
12576
+ const out = /* @__PURE__ */ new Map();
12577
+ const fileCache = /* @__PURE__ */ new Map();
12578
+ let used = 0;
12579
+ const readLines = (path) => {
12580
+ if (fileCache.has(path))
12581
+ return fileCache.get(path) ?? null;
12582
+ let lines = null;
12583
+ try {
12584
+ const resolved = baseDir && baseDir !== "." ? `${baseDir.replace(/\/+$/, "")}/${path}` : path;
12585
+ lines = existsSync29(resolved) ? readFileSync33(resolved, "utf8").split(/\r?\n/) : null;
12586
+ } catch {
12587
+ lines = null;
12588
+ }
12589
+ fileCache.set(path, lines);
12590
+ return lines;
12591
+ };
12592
+ for (const node of nodes) {
12593
+ const fileLines = readLines(node.file_path);
12594
+ if (!fileLines || !node.start_line)
12595
+ continue;
12596
+ if (used >= budget) {
12597
+ out.set(node.id, { source: "", truncated: true });
12598
+ continue;
12599
+ }
12600
+ const end = node.end_line && node.end_line >= node.start_line ? node.end_line : node.start_line;
12601
+ const allowedEnd = Math.min(end, node.start_line + (budget - used) - 1);
12602
+ const slice = fileLines.slice(node.start_line - 1, allowedEnd);
12603
+ out.set(node.id, { source: slice.join("\n"), truncated: allowedEnd < end });
12604
+ used += slice.length;
12605
+ }
12606
+ return out;
12607
+ }
11691
12608
  async function graphImpact2(term, opts) {
11692
12609
  const selected = selectEngine(opts);
11693
12610
  if ("error" in selected)
@@ -11783,7 +12700,7 @@ async function graphContext2(task, opts) {
11783
12700
  const freshness = await ensureCodeMapFresh(mapPath, opts.refresh !== false);
11784
12701
  if (freshness.error)
11785
12702
  return printEngineError(freshness.error, opts.json, selected.probe);
11786
- if (!existsSync24(mapPath))
12703
+ if (!existsSync29(mapPath))
11787
12704
  return printEngineError(`${mapPath} is missing; run \`cdd-kit code-map\` first.`, opts.json, selected.probe);
11788
12705
  let entries;
11789
12706
  try {
@@ -11834,12 +12751,70 @@ var init_graph = __esm({
11834
12751
  }
11835
12752
  });
11836
12753
 
12754
+ // src/commands/classify-check.ts
12755
+ var classify_check_exports = {};
12756
+ __export(classify_check_exports, {
12757
+ classifyCheck: () => classifyCheck
12758
+ });
12759
+ import { existsSync as existsSync30, readFileSync as readFileSync34 } from "fs";
12760
+ import { join as join30 } from "path";
12761
+ async function classifyCheck(changeId, opts = {}) {
12762
+ const cwd = process.cwd();
12763
+ const policy = loadTierPolicy(cwd);
12764
+ let intent = opts.text ?? "";
12765
+ let source = "inline --text";
12766
+ let paths = [];
12767
+ if (!intent && changeId) {
12768
+ const requestPath = join30(cwd, "specs", "changes", changeId, "change-request.md");
12769
+ if (existsSync30(requestPath))
12770
+ intent = readFileSync34(requestPath, "utf8");
12771
+ paths = getTouchedPaths(cwd);
12772
+ source = `specs/changes/${changeId}/change-request.md + touched paths`;
12773
+ }
12774
+ const floor = computeTierFloor(intent, policy, { paths });
12775
+ if (opts.json) {
12776
+ console.log(JSON.stringify({
12777
+ enabled: policy.enabled,
12778
+ source,
12779
+ floorTier: floor.floorTier,
12780
+ label: floor.label,
12781
+ matched: floor.matched
12782
+ }, null, 2));
12783
+ return 0;
12784
+ }
12785
+ if (!policy.enabled) {
12786
+ log.info("tier-floor policy is disabled (.cdd/tier-policy.json enabled:false) \u2014 no floor enforced.");
12787
+ return 0;
12788
+ }
12789
+ if (!intent && paths.length === 0) {
12790
+ log.warn('no intent text or touched paths found to scan (pass --text "<description>" or a change-id with change-request.md).');
12791
+ return 0;
12792
+ }
12793
+ if (floor.floorTier === null) {
12794
+ log.ok("no sensitive surface matched \u2014 classifier may assign any tier on merit.");
12795
+ return 0;
12796
+ }
12797
+ log.warn(`mechanical tier floor: ${floor.floorTier} (or stricter)`);
12798
+ log.info(` reason: ${floor.label}`);
12799
+ log.info(` matched: ${floor.matched.join(", ")}`);
12800
+ log.info(` \u2192 change-classification.md must declare tier ${floor.floorTier} or stricter; the gate enforces this.`);
12801
+ return 0;
12802
+ }
12803
+ var init_classify_check = __esm({
12804
+ "src/commands/classify-check.ts"() {
12805
+ "use strict";
12806
+ init_logger();
12807
+ init_tier_floor();
12808
+ init_git_paths();
12809
+ }
12810
+ });
12811
+
11837
12812
  // src/mcp/server.ts
11838
12813
  var server_exports = {};
11839
12814
  __export(server_exports, {
11840
12815
  runMcpServer: () => runMcpServer
11841
12816
  });
11842
- import { spawnSync as spawnSync4 } from "child_process";
12817
+ import { spawnSync as spawnSync8 } from "child_process";
11843
12818
  import { fileURLToPath as fileURLToPath3 } from "url";
11844
12819
  import { createInterface } from "readline";
11845
12820
  async function runMcpServer(opts) {
@@ -11930,6 +12905,7 @@ function callTool(name, args) {
11930
12905
  "--limit",
11931
12906
  String(optionalInt(args.limit, 10)),
11932
12907
  "--json",
12908
+ ...sourceArgs(args),
11933
12909
  ...refreshArgs(args)
11934
12910
  ]);
11935
12911
  case "cdd_graph_context":
@@ -11972,6 +12948,7 @@ function callTool(name, args) {
11972
12948
  "--limit",
11973
12949
  String(optionalInt(args.limit, 10)),
11974
12950
  "--json",
12951
+ ...sourceArgs(args),
11975
12952
  ...refreshArgs(args)
11976
12953
  ]);
11977
12954
  case "cdd_index_impact":
@@ -11995,7 +12972,7 @@ function callTool(name, args) {
11995
12972
  }
11996
12973
  function runCddJson(args) {
11997
12974
  const cliPath = process.argv[1] || fileURLToPath3(import.meta.url);
11998
- const result = spawnSync4(process.execPath, [cliPath, ...args], {
12975
+ const result = spawnSync8(process.execPath, [cliPath, ...args], {
11999
12976
  cwd: process.cwd(),
12000
12977
  env: process.env,
12001
12978
  encoding: "utf8"
@@ -12017,6 +12994,11 @@ function runCddJson(args) {
12017
12994
  function refreshArgs(args) {
12018
12995
  return args.refresh === false ? ["--no-refresh"] : [];
12019
12996
  }
12997
+ function sourceArgs(args) {
12998
+ if (args.withSource !== true)
12999
+ return [];
13000
+ return ["--with-source", "--source-budget", String(optionalInt(args.sourceBudget, 400))];
13001
+ }
12020
13002
  function asObject(value) {
12021
13003
  return value && typeof value === "object" && !Array.isArray(value) ? value : {};
12022
13004
  }
@@ -12062,7 +13044,7 @@ var init_server = __esm({
12062
13044
  },
12063
13045
  {
12064
13046
  name: "cdd_graph_query",
12065
- description: "Search native graph symbols/files and return candidate nodes with line ranges.",
13047
+ description: "Search native graph symbols/files and return candidate nodes with line ranges. Set withSource:true to also return the source slices inline so no separate file read is needed.",
12066
13048
  inputSchema: {
12067
13049
  type: "object",
12068
13050
  properties: {
@@ -12070,6 +13052,8 @@ var init_server = __esm({
12070
13052
  limit: { type: "integer", minimum: 1, maximum: 100, default: 10 },
12071
13053
  map: { type: "string", description: "Code-map YAML path.", default: DEFAULT_MAP },
12072
13054
  engine: { type: "string", enum: ["auto", "native", "codegraph", "codemap"], default: "auto" },
13055
+ withSource: { type: "boolean", description: "Include matched source slices inline (replaces a follow-up read).", default: false },
13056
+ sourceBudget: { type: "integer", minimum: 1, maximum: 5e3, default: 400, description: "Max total source lines to return when withSource is true." },
12073
13057
  refresh: { type: "boolean", default: true }
12074
13058
  },
12075
13059
  required: ["query"],
@@ -12111,13 +13095,15 @@ var init_server = __esm({
12111
13095
  },
12112
13096
  {
12113
13097
  name: "cdd_index_query",
12114
- description: "Fallback code-map query for files, symbols, imports, and line ranges.",
13098
+ description: "Fallback code-map query for files, symbols, imports, and line ranges. Set withSource:true to also return the source slices inline so no separate file read is needed.",
12115
13099
  inputSchema: {
12116
13100
  type: "object",
12117
13101
  properties: {
12118
13102
  query: { type: "string", description: "Symbol, file path, import module, enum member, or substring." },
12119
13103
  limit: { type: "integer", minimum: 1, maximum: 100, default: 10 },
12120
13104
  map: { type: "string", description: "Code-map YAML path.", default: DEFAULT_MAP },
13105
+ withSource: { type: "boolean", description: "Include matched source slices inline (replaces a follow-up read).", default: false },
13106
+ sourceBudget: { type: "integer", minimum: 1, maximum: 5e3, default: 400, description: "Max total source lines to return when withSource is true." },
12121
13107
  refresh: { type: "boolean", default: true }
12122
13108
  },
12123
13109
  required: ["query"],
@@ -12148,28 +13134,28 @@ var archive_exports = {};
12148
13134
  __export(archive_exports, {
12149
13135
  archive: () => archive
12150
13136
  });
12151
- import { join as join26 } from "path";
12152
- import { existsSync as existsSync25, mkdirSync as mkdirSync10, renameSync as renameSync2, readFileSync as readFileSync27, writeFileSync as writeFileSync13, appendFileSync, cpSync as cpSync3, rmSync as rmSync3 } from "fs";
13137
+ import { join as join31 } from "path";
13138
+ import { existsSync as existsSync31, mkdirSync as mkdirSync12, renameSync as renameSync2, readFileSync as readFileSync35, writeFileSync as writeFileSync16, appendFileSync, cpSync as cpSync3, rmSync as rmSync3 } from "fs";
12153
13139
  import yaml4 from "js-yaml";
12154
13140
  async function archive(changeId) {
12155
13141
  const cwd = process.cwd();
12156
- const changeDir = join26(cwd, "specs", "changes", changeId);
13142
+ const changeDir = join31(cwd, "specs", "changes", changeId);
12157
13143
  const archiveYear = (/* @__PURE__ */ new Date()).getFullYear().toString();
12158
- const archiveBase = join26(cwd, "specs", "archive", archiveYear);
12159
- const archiveDir = join26(archiveBase, changeId);
12160
- const indexPath = join26(cwd, "specs", "archive", "INDEX.md");
12161
- if (!existsSync25(changeDir)) {
13144
+ const archiveBase = join31(cwd, "specs", "archive", archiveYear);
13145
+ const archiveDir = join31(archiveBase, changeId);
13146
+ const indexPath = join31(cwd, "specs", "archive", "INDEX.md");
13147
+ if (!existsSync31(changeDir)) {
12162
13148
  log.error(`Change not found: specs/changes/${changeId}`);
12163
13149
  process.exit(1);
12164
13150
  }
12165
- if (existsSync25(archiveDir)) {
13151
+ if (existsSync31(archiveDir)) {
12166
13152
  log.error(`Already archived: specs/archive/${archiveYear}/${changeId}`);
12167
13153
  process.exit(1);
12168
13154
  }
12169
- const tasksPath = join26(changeDir, "tasks.yml");
12170
- if (existsSync25(tasksPath)) {
13155
+ const tasksPath = join31(changeDir, "tasks.yml");
13156
+ if (existsSync31(tasksPath)) {
12171
13157
  try {
12172
- const raw = readFileSync27(tasksPath, "utf8");
13158
+ const raw = readFileSync35(tasksPath, "utf8");
12173
13159
  const data = yaml4.load(raw);
12174
13160
  if (data?.status === "gate-blocked") {
12175
13161
  log.warn("tasks.yml has status: gate-blocked \u2014 archiving anyway (change was paused).");
@@ -12182,8 +13168,8 @@ async function archive(changeId) {
12182
13168
  log.warn("tasks.yml could not be parsed \u2014 archiving anyway.");
12183
13169
  }
12184
13170
  }
12185
- if (!existsSync25(archiveBase)) {
12186
- mkdirSync10(archiveBase, { recursive: true });
13171
+ if (!existsSync31(archiveBase)) {
13172
+ mkdirSync12(archiveBase, { recursive: true });
12187
13173
  }
12188
13174
  try {
12189
13175
  renameSync2(changeDir, archiveDir);
@@ -12199,8 +13185,8 @@ async function archive(changeId) {
12199
13185
  const today = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
12200
13186
  const indexLine = `| ${changeId} | ${archiveYear} | ${today} | specs/archive/${archiveYear}/${changeId}/ |
12201
13187
  `;
12202
- if (!existsSync25(indexPath)) {
12203
- writeFileSync13(indexPath, `# Archive Index
13188
+ if (!existsSync31(indexPath)) {
13189
+ writeFileSync16(indexPath, `# Archive Index
12204
13190
 
12205
13191
  | change-id | year | archived-date | path |
12206
13192
  |---|---|---|---|
@@ -12224,37 +13210,37 @@ var abandon_exports = {};
12224
13210
  __export(abandon_exports, {
12225
13211
  abandon: () => abandon
12226
13212
  });
12227
- import { join as join27 } from "path";
12228
- import { existsSync as existsSync26, readFileSync as readFileSync28, writeFileSync as writeFileSync14, appendFileSync as appendFileSync2, mkdirSync as mkdirSync11 } from "fs";
13213
+ import { join as join32 } from "path";
13214
+ import { existsSync as existsSync32, readFileSync as readFileSync36, writeFileSync as writeFileSync17, appendFileSync as appendFileSync2, mkdirSync as mkdirSync13 } from "fs";
12229
13215
  import yaml5 from "js-yaml";
12230
13216
  async function abandon(changeId, opts) {
12231
13217
  const cwd = process.cwd();
12232
- const changeDir = join27(cwd, "specs", "changes", changeId);
12233
- const tasksPath = join27(changeDir, "tasks.yml");
12234
- if (!existsSync26(changeDir)) {
13218
+ const changeDir = join32(cwd, "specs", "changes", changeId);
13219
+ const tasksPath = join32(changeDir, "tasks.yml");
13220
+ if (!existsSync32(changeDir)) {
12235
13221
  log.error(`Change not found: specs/changes/${changeId}`);
12236
13222
  process.exit(1);
12237
13223
  }
12238
- if (existsSync26(tasksPath)) {
12239
- const raw = readFileSync28(tasksPath, "utf8");
13224
+ if (existsSync32(tasksPath)) {
13225
+ const raw = readFileSync36(tasksPath, "utf8");
12240
13226
  const data = yaml5.load(raw) ?? {};
12241
13227
  data["status"] = "abandoned";
12242
13228
  if (!data["change-id"]) {
12243
13229
  data["change-id"] = changeId;
12244
13230
  }
12245
- writeFileSync14(tasksPath, yaml5.dump(data, { lineWidth: -1, noRefs: true }), "utf8");
13231
+ writeFileSync17(tasksPath, yaml5.dump(data, { lineWidth: -1, noRefs: true }), "utf8");
12246
13232
  }
12247
13233
  const today = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
12248
- const archiveDir = join27(cwd, "specs", "archive");
12249
- const indexPath = join27(archiveDir, "INDEX.md");
13234
+ const archiveDir = join32(cwd, "specs", "archive");
13235
+ const indexPath = join32(archiveDir, "INDEX.md");
12250
13236
  const reason = opts.reason ?? "no reason given";
12251
13237
  const indexLine = `| ${changeId} | abandoned | ${today} | ${reason} |
12252
13238
  `;
12253
- if (!existsSync26(archiveDir)) {
12254
- mkdirSync11(archiveDir, { recursive: true });
13239
+ if (!existsSync32(archiveDir)) {
13240
+ mkdirSync13(archiveDir, { recursive: true });
12255
13241
  }
12256
- if (!existsSync26(indexPath)) {
12257
- writeFileSync14(indexPath, `# Archive Index
13242
+ if (!existsSync32(indexPath)) {
13243
+ writeFileSync17(indexPath, `# Archive Index
12258
13244
 
12259
13245
  | change-id | status | date | notes |
12260
13246
  |---|---|---|---|
@@ -12278,28 +13264,28 @@ var list_changes_exports = {};
12278
13264
  __export(list_changes_exports, {
12279
13265
  listChanges: () => listChanges
12280
13266
  });
12281
- import { join as join28 } from "path";
12282
- import { existsSync as existsSync27, readdirSync as readdirSync13, readFileSync as readFileSync29 } from "fs";
13267
+ import { join as join33 } from "path";
13268
+ import { existsSync as existsSync33, readdirSync as readdirSync14, readFileSync as readFileSync37 } from "fs";
12283
13269
  import yaml6 from "js-yaml";
12284
13270
  async function listChanges() {
12285
13271
  const cwd = process.cwd();
12286
- const changesDir = join28(cwd, "specs", "changes");
13272
+ const changesDir = join33(cwd, "specs", "changes");
12287
13273
  log.blank();
12288
13274
  const active = [];
12289
- if (existsSync27(changesDir)) {
12290
- active.push(...readdirSync13(changesDir, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name));
13275
+ if (existsSync33(changesDir)) {
13276
+ active.push(...readdirSync14(changesDir, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name));
12291
13277
  }
12292
13278
  if (active.length === 0) {
12293
13279
  log.info("No active changes in specs/changes/");
12294
13280
  } else {
12295
13281
  log.info("Active changes:");
12296
13282
  for (const id of active) {
12297
- const tasksPath = join28(changesDir, id, "tasks.yml");
13283
+ const tasksPath = join33(changesDir, id, "tasks.yml");
12298
13284
  let status = "in-progress";
12299
13285
  let pending = 0;
12300
- if (existsSync27(tasksPath)) {
13286
+ if (existsSync33(tasksPath)) {
12301
13287
  try {
12302
- const raw = readFileSync29(tasksPath, "utf8");
13288
+ const raw = readFileSync37(tasksPath, "utf8");
12303
13289
  const data = yaml6.load(raw);
12304
13290
  if (data?.status)
12305
13291
  status = data.status;
@@ -12331,9 +13317,9 @@ __export(context_exports, {
12331
13317
  rejectContextExpansion: () => rejectContextExpansion,
12332
13318
  requestContextExpansion: () => requestContextExpansion
12333
13319
  });
12334
- import { existsSync as existsSync28, readFileSync as readFileSync30, writeFileSync as writeFileSync15 } from "fs";
12335
- import { join as join29 } from "path";
12336
- import picomatch2 from "picomatch";
13320
+ import { existsSync as existsSync34, readFileSync as readFileSync38, writeFileSync as writeFileSync18 } from "fs";
13321
+ import { join as join34 } from "path";
13322
+ import picomatch3 from "picomatch";
12337
13323
  function normalizePath(path) {
12338
13324
  return path.replace(/\\/g, "/").replace(/^\.\//, "").trim();
12339
13325
  }
@@ -12347,18 +13333,18 @@ function validateRepoRelativePath(path) {
12347
13333
  return null;
12348
13334
  }
12349
13335
  function manifestPathFor(changeId) {
12350
- return join29(process.cwd(), "specs", "changes", changeId, "context-manifest.md");
13336
+ return join34(process.cwd(), "specs", "changes", changeId, "context-manifest.md");
12351
13337
  }
12352
13338
  function readManifest(changeId) {
12353
13339
  const manifestPath = manifestPathFor(changeId);
12354
- if (!existsSync28(manifestPath)) {
13340
+ if (!existsSync34(manifestPath)) {
12355
13341
  log.error(`context manifest not found: specs/changes/${changeId}/context-manifest.md`);
12356
13342
  process.exit(1);
12357
13343
  }
12358
- return readFileSync30(manifestPath, "utf8");
13344
+ return readFileSync38(manifestPath, "utf8");
12359
13345
  }
12360
13346
  function writeManifest(changeId, content) {
12361
- writeFileSync15(manifestPathFor(changeId), content.endsWith("\n") ? content : `${content}
13347
+ writeFileSync18(manifestPathFor(changeId), content.endsWith("\n") ? content : `${content}
12362
13348
  `, "utf8");
12363
13349
  }
12364
13350
  function sectionBody(content, heading) {
@@ -12384,7 +13370,7 @@ function pathMatches(relPath, patterns, currentChangeId) {
12384
13370
  return normalized.startsWith("specs/changes/");
12385
13371
  }
12386
13372
  if (/[*?[{]/.test(pattern)) {
12387
- if (picomatch2.isMatch(normalized, pattern, { dot: true, nocase: false }))
13373
+ if (picomatch3.isMatch(normalized, pattern, { dot: true, nocase: false }))
12388
13374
  return true;
12389
13375
  if (pattern.endsWith("/**")) {
12390
13376
  const base = pattern.slice(0, -3);
@@ -12397,11 +13383,11 @@ function pathMatches(relPath, patterns, currentChangeId) {
12397
13383
  });
12398
13384
  }
12399
13385
  function loadContextPolicy() {
12400
- const policyPath = join29(process.cwd(), ".cdd", "context-policy.json");
12401
- if (!existsSync28(policyPath))
13386
+ const policyPath = join34(process.cwd(), ".cdd", "context-policy.json");
13387
+ if (!existsSync34(policyPath))
12402
13388
  return { forbiddenPaths: DEFAULT_FORBIDDEN_PATHS };
12403
13389
  try {
12404
- const custom = JSON.parse(readFileSync30(policyPath, "utf8"));
13390
+ const custom = JSON.parse(readFileSync38(policyPath, "utf8"));
12405
13391
  return {
12406
13392
  forbiddenPaths: Array.from(/* @__PURE__ */ new Set([
12407
13393
  ...DEFAULT_FORBIDDEN_PATHS,
@@ -12683,16 +13669,16 @@ var init_context = __esm({
12683
13669
  });
12684
13670
 
12685
13671
  // src/cli/index.ts
12686
- import { readFileSync as readFileSync31 } from "fs";
13672
+ import { readFileSync as readFileSync39 } from "fs";
12687
13673
  import os from "os";
12688
13674
  import { fileURLToPath as fileURLToPath4 } from "url";
12689
- import { dirname as dirname7, join as join30 } from "path";
13675
+ import { dirname as dirname8, join as join35 } from "path";
12690
13676
  import { Command } from "commander";
12691
13677
 
12692
13678
  // src/commands/init.ts
12693
13679
  init_paths();
12694
- import { join as join5 } from "path";
12695
- import { rmSync, readFileSync as readFileSync3, writeFileSync as writeFileSync2, existsSync as existsSync4, readdirSync as readdirSync2 } from "fs";
13680
+ import { join as join8 } from "path";
13681
+ import { rmSync, readFileSync as readFileSync6, writeFileSync as writeFileSync5, existsSync as existsSync7, readdirSync as readdirSync2 } from "fs";
12696
13682
 
12697
13683
  // src/utils/copy.ts
12698
13684
  init_logger();
@@ -12860,14 +13846,57 @@ function detectStack(repoRoot) {
12860
13846
  };
12861
13847
  }
12862
13848
 
13849
+ // src/commands/suggest-codegen.ts
13850
+ import { existsSync as existsSync3, readFileSync as readFileSync2, writeFileSync } from "fs";
13851
+ import { join as join4 } from "path";
13852
+ var ARTIFACT_PATH = "contracts/api/openapi.json";
13853
+ var CLIENT_OUT = "src/api/types.ts";
13854
+ var SCRIPT_GENERATE = "contract:client";
13855
+ var SCRIPT_CHECK = "contract:client:check";
13856
+ var GENERATE_CMD = `cdd-kit openapi export --out ${ARTIFACT_PATH} && npx --yes openapi-typescript ${ARTIFACT_PATH} -o ${CLIENT_OUT}`;
13857
+ var CHECK_CMD = `cdd-kit openapi export --check --out ${ARTIFACT_PATH}`;
13858
+ function suggestCodegenScript(cwd) {
13859
+ const pkgPath = join4(cwd, "package.json");
13860
+ if (!existsSync3(pkgPath)) {
13861
+ return { added: [], skipped: "no package.json (client codegen is consumer-specific; see docs/openapi-export.md)" };
13862
+ }
13863
+ let pkg2;
13864
+ let raw;
13865
+ try {
13866
+ raw = readFileSync2(pkgPath, "utf8");
13867
+ pkg2 = JSON.parse(raw);
13868
+ } catch {
13869
+ return { added: [], skipped: "package.json is not valid JSON; left untouched" };
13870
+ }
13871
+ if (typeof pkg2 !== "object" || pkg2 === null || Array.isArray(pkg2)) {
13872
+ return { added: [], skipped: "package.json is not a JSON object; left untouched" };
13873
+ }
13874
+ const scripts = typeof pkg2.scripts === "object" && pkg2.scripts !== null && !Array.isArray(pkg2.scripts) ? pkg2.scripts : {};
13875
+ const hasGenerate = SCRIPT_GENERATE in scripts;
13876
+ const hasCheck = SCRIPT_CHECK in scripts;
13877
+ if (hasGenerate || hasCheck) {
13878
+ const present = [hasGenerate ? SCRIPT_GENERATE : "", hasCheck ? SCRIPT_CHECK : ""].filter(Boolean).join(" + ");
13879
+ return { added: [], skipped: `${present} already present in package.json; leaving codegen wiring to you` };
13880
+ }
13881
+ scripts[SCRIPT_GENERATE] = GENERATE_CMD;
13882
+ scripts[SCRIPT_CHECK] = CHECK_CMD;
13883
+ pkg2.scripts = scripts;
13884
+ const trailing = raw.endsWith("\n") ? "\n" : "";
13885
+ writeFileSync(pkgPath, JSON.stringify(pkg2, null, 2) + trailing, "utf8");
13886
+ return {
13887
+ added: [SCRIPT_GENERATE, SCRIPT_CHECK],
13888
+ note: `requires openapi-typescript (\`npm i -D openapi-typescript\`); edit the output path (${CLIENT_OUT}) to fit your project, then wire \`npm run ${SCRIPT_CHECK}\` into CI`
13889
+ };
13890
+ }
13891
+
12863
13892
  // src/commands/init.ts
12864
13893
  init_mcp_hint();
12865
13894
  function readCondaEnvName(cwd) {
12866
- const envYml = join5(cwd, "environment.yml");
12867
- if (!existsSync4(envYml))
13895
+ const envYml = join8(cwd, "environment.yml");
13896
+ if (!existsSync7(envYml))
12868
13897
  return "base";
12869
13898
  try {
12870
- const content = readFileSync3(envYml, "utf8");
13899
+ const content = readFileSync6(envYml, "utf8");
12871
13900
  const match = content.match(/^name:\s*(.+)$/m);
12872
13901
  return match ? match[1].trim() : "base";
12873
13902
  } catch {
@@ -12875,11 +13904,11 @@ function readCondaEnvName(cwd) {
12875
13904
  }
12876
13905
  }
12877
13906
  function loadCiTemplate(stack) {
12878
- const templatePath = join5(ASSETS_DIR, "ci-templates", `${stack}.yml`);
12879
- if (!existsSync4(templatePath))
13907
+ const templatePath = join8(ASSETS_DIR, "ci-templates", `${stack}.yml`);
13908
+ if (!existsSync7(templatePath))
12880
13909
  return null;
12881
13910
  try {
12882
- return readFileSync3(templatePath, "utf8");
13911
+ return readFileSync6(templatePath, "utf8");
12883
13912
  } catch {
12884
13913
  return null;
12885
13914
  }
@@ -12915,6 +13944,7 @@ async function init(opts) {
12915
13944
  }
12916
13945
  const cwd = process.cwd();
12917
13946
  const createdPaths = [];
13947
+ const restoreActions = [];
12918
13948
  const installClaude = opts.provider === "claude" || opts.provider === "both";
12919
13949
  const installCodex = opts.provider === "codex" || opts.provider === "both";
12920
13950
  function track(paths) {
@@ -12930,6 +13960,13 @@ async function init(opts) {
12930
13960
  log.warn(`could not remove: ${p}`);
12931
13961
  }
12932
13962
  }
13963
+ for (const restore of [...restoreActions].reverse()) {
13964
+ try {
13965
+ restore();
13966
+ } catch {
13967
+ log.warn("could not restore an in-place file edit");
13968
+ }
13969
+ }
12933
13970
  }
12934
13971
  log.blank();
12935
13972
  log.info("Initialising contract-driven-delivery kit\u2026");
@@ -12943,9 +13980,9 @@ async function init(opts) {
12943
13980
  const skillDirs = readdirSync2(ASSET.skills, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name);
12944
13981
  let totalSkillFiles = 0;
12945
13982
  for (const skillName of skillDirs) {
12946
- const skillDest = join5(SKILLS_HOME, skillName);
13983
+ const skillDest = join8(SKILLS_HOME, skillName);
12947
13984
  log.info(`Installing skill \u2192 ${skillDest}`);
12948
- const { count, created } = copyDirTracked(join5(ASSET.skills, skillName), skillDest, { overwrite: true });
13985
+ const { count, created } = copyDirTracked(join8(ASSET.skills, skillName), skillDest, { overwrite: true });
12949
13986
  track(created);
12950
13987
  totalSkillFiles += count;
12951
13988
  }
@@ -12959,44 +13996,44 @@ async function init(opts) {
12959
13996
  log.info(`Scaffolding project files in ${cwd}`);
12960
13997
  const { count: contractsCount, created: contractsCreated } = copyDirTracked(
12961
13998
  ASSET.contracts,
12962
- join5(cwd, "contracts"),
13999
+ join8(cwd, "contracts"),
12963
14000
  { overwrite: opts.force, label: "contracts" }
12964
14001
  );
12965
14002
  track(contractsCreated);
12966
14003
  log.ok(`contracts/ \u2014 ${contractsCount} file(s) written.`);
12967
14004
  const { count: specsCount, created: specsCreated } = copyDirTracked(
12968
14005
  ASSET.specsTemplates,
12969
- join5(cwd, "specs", "templates"),
14006
+ join8(cwd, "specs", "templates"),
12970
14007
  { overwrite: opts.force, label: "specs/templates" }
12971
14008
  );
12972
14009
  track(specsCreated);
12973
14010
  log.ok(`specs/templates/ \u2014 ${specsCount} file(s) written.`);
12974
14011
  const { count: testsCount, created: testsCreated } = copyDirTracked(
12975
14012
  ASSET.testsTemplates,
12976
- join5(cwd, "tests", "templates"),
14013
+ join8(cwd, "tests", "templates"),
12977
14014
  { overwrite: opts.force, label: "tests/templates" }
12978
14015
  );
12979
14016
  track(testsCreated);
12980
14017
  log.ok(`tests/templates/ \u2014 ${testsCount} file(s) written.`);
12981
14018
  const { count: ciCount, created: ciCreated } = copyDirTracked(
12982
14019
  ASSET.ci,
12983
- join5(cwd, "ci"),
14020
+ join8(cwd, "ci"),
12984
14021
  { overwrite: opts.force, label: "ci" }
12985
14022
  );
12986
14023
  track(ciCreated);
12987
14024
  log.ok(`ci/ \u2014 ${ciCount} file(s) written.`);
12988
14025
  const { count: cddConfigCount, created: cddConfigCreated } = copyDirTracked(
12989
14026
  ASSET.cddConfig,
12990
- join5(cwd, ".cdd"),
14027
+ join8(cwd, ".cdd"),
12991
14028
  { overwrite: opts.force, label: ".cdd" }
12992
14029
  );
12993
14030
  track(cddConfigCreated);
12994
14031
  log.ok(`.cdd/ - ${cddConfigCount} file(s) written.`);
12995
- const modelPolicyPath = join5(cwd, ".cdd", "model-policy.json");
12996
- if (existsSync4(modelPolicyPath)) {
14032
+ const modelPolicyPath = join8(cwd, ".cdd", "model-policy.json");
14033
+ if (existsSync7(modelPolicyPath)) {
12997
14034
  let existing = {};
12998
14035
  try {
12999
- existing = JSON.parse(readFileSync3(modelPolicyPath, "utf8"));
14036
+ existing = JSON.parse(readFileSync6(modelPolicyPath, "utf8"));
13000
14037
  } catch {
13001
14038
  }
13002
14039
  const merged = {
@@ -13005,11 +14042,11 @@ async function init(opts) {
13005
14042
  generated_at: (/* @__PURE__ */ new Date()).toISOString(),
13006
14043
  roles: existing.roles && typeof existing.roles === "object" && Object.keys(existing.roles).length > 0 ? existing.roles : {}
13007
14044
  };
13008
- writeFileSync2(modelPolicyPath, JSON.stringify(merged, null, 2) + "\n", "utf8");
14045
+ writeFileSync5(modelPolicyPath, JSON.stringify(merged, null, 2) + "\n", "utf8");
13009
14046
  }
13010
14047
  const { count: wfCount, created: wfCreated } = copyDirTracked(
13011
14048
  ASSET.githubWorkflows,
13012
- join5(cwd, ".github", "workflows"),
14049
+ join8(cwd, ".github", "workflows"),
13013
14050
  { overwrite: opts.force, label: ".github/workflows" }
13014
14051
  );
13015
14052
  track(wfCreated);
@@ -13033,11 +14070,11 @@ async function init(opts) {
13033
14070
  } else {
13034
14071
  log.warn("Could not detect stack \u2014 CI placeholder left in place.");
13035
14072
  }
13036
- const ciYmlDest = join5(cwd, ".github", "workflows", "contract-driven-gates.yml");
13037
- if (existsSync4(ciYmlDest)) {
14073
+ const ciYmlDest = join8(cwd, ".github", "workflows", "contract-driven-gates.yml");
14074
+ if (existsSync7(ciYmlDest)) {
13038
14075
  const template = loadCiTemplate(detection.primary);
13039
14076
  if (template) {
13040
- const original = readFileSync3(ciYmlDest, "utf8");
14077
+ const original = readFileSync6(ciYmlDest, "utf8");
13041
14078
  let patched = patchFastGateYml(original, template, detection.primary);
13042
14079
  if (detection.primary === "conda" && patched.includes("{{conda-env-name}}")) {
13043
14080
  const envName = readCondaEnvName(cwd);
@@ -13045,39 +14082,52 @@ async function init(opts) {
13045
14082
  log.ok(`Conda environment name set to: ${envName}`);
13046
14083
  }
13047
14084
  if (patched !== original) {
13048
- writeFileSync2(ciYmlDest, patched, "utf8");
14085
+ writeFileSync5(ciYmlDest, patched, "utf8");
13049
14086
  log.ok(`CI fast-gate patched for stack: ${detection.primary}`);
13050
14087
  }
13051
14088
  }
13052
14089
  }
14090
+ const pkgJsonPath = join8(cwd, "package.json");
14091
+ const pkgJsonBefore = existsSync7(pkgJsonPath) ? readFileSync6(pkgJsonPath, "utf8") : null;
14092
+ const codegen = suggestCodegenScript(cwd);
14093
+ if (codegen.added.length > 0) {
14094
+ if (pkgJsonBefore !== null) {
14095
+ restoreActions.push(() => writeFileSync5(pkgJsonPath, pkgJsonBefore, "utf8"));
14096
+ }
14097
+ log.ok(`package.json: added ${codegen.added.join(" + ")} script(s) for contract\u2192client codegen`);
14098
+ if (codegen.note)
14099
+ log.info(codegen.note);
14100
+ } else if (codegen.skipped && existsSync7(pkgJsonPath)) {
14101
+ log.info(`contract\u2192client codegen: ${codegen.skipped}`);
14102
+ }
13053
14103
  if (installClaude) {
13054
14104
  const { written: claudeWritten, created: claudeCreated } = copyFileTracked(
13055
14105
  ASSET.claudeTemplate,
13056
- join5(cwd, "CLAUDE.md"),
14106
+ join8(cwd, "CLAUDE.md"),
13057
14107
  { overwrite: false, label: "CLAUDE.md" }
13058
14108
  );
13059
14109
  if (claudeCreated)
13060
- track([join5(cwd, "CLAUDE.md")]);
14110
+ track([join8(cwd, "CLAUDE.md")]);
13061
14111
  if (claudeWritten)
13062
14112
  log.ok("CLAUDE.md created.");
13063
14113
  const { written: agentsWritten, created: agentsCreated } = copyFileTracked(
13064
14114
  ASSET.agentsTemplate,
13065
- join5(cwd, "AGENTS.md"),
14115
+ join8(cwd, "AGENTS.md"),
13066
14116
  { overwrite: false, label: "AGENTS.md" }
13067
14117
  );
13068
14118
  if (agentsCreated)
13069
- track([join5(cwd, "AGENTS.md")]);
14119
+ track([join8(cwd, "AGENTS.md")]);
13070
14120
  if (agentsWritten)
13071
14121
  log.ok("AGENTS.md created.");
13072
14122
  }
13073
14123
  if (installCodex) {
13074
14124
  const { written: codexWritten, created: codexCreated } = copyFileTracked(
13075
14125
  ASSET.codexTemplate,
13076
- join5(cwd, "CODEX.md"),
14126
+ join8(cwd, "CODEX.md"),
13077
14127
  { overwrite: false, label: "CODEX.md" }
13078
14128
  );
13079
14129
  if (codexCreated)
13080
- track([join5(cwd, "CODEX.md")]);
14130
+ track([join8(cwd, "CODEX.md")]);
13081
14131
  if (codexWritten)
13082
14132
  log.ok("CODEX.md created.");
13083
14133
  }
@@ -13085,6 +14135,16 @@ async function init(opts) {
13085
14135
  const { installCodeMapHook: installCodeMapHook2 } = await Promise.resolve().then(() => (init_code_map_hook(), code_map_hook_exports));
13086
14136
  await installCodeMapHook2(cwd);
13087
14137
  }
14138
+ if (!opts.globalOnly && opts.arm !== false) {
14139
+ log.info("Arming enforcement chokepoints\u2026");
14140
+ if (installClaude) {
14141
+ const { installAgentHooks: installAgentHooks2 } = await Promise.resolve().then(() => (init_install_agent_hooks(), install_agent_hooks_exports));
14142
+ await installAgentHooks2({ graphFirst: "advisory", fromInit: true });
14143
+ }
14144
+ const { installHooks: installHooks2 } = await Promise.resolve().then(() => (init_install_hooks(), install_hooks_exports));
14145
+ await installHooks2({ fromInit: true });
14146
+ log.info("Chokepoints armed. Run `cdd-kit doctor` to see live/dormant status; use `--no-arm` next time to skip.");
14147
+ }
13088
14148
  log.blank();
13089
14149
  }
13090
14150
  } catch (err) {
@@ -13108,9 +14168,9 @@ init_update();
13108
14168
 
13109
14169
  // src/commands/new-change.ts
13110
14170
  init_paths();
13111
- import { join as join9, relative as relative3 } from "path";
14171
+ import { join as join12, relative as relative3 } from "path";
13112
14172
  import { createHash as createHash4 } from "crypto";
13113
- import { existsSync as existsSync8, readFileSync as readFileSync8, readdirSync as readdirSync5, writeFileSync as writeFileSync4 } from "fs";
14173
+ import { existsSync as existsSync11, readFileSync as readFileSync11, readdirSync as readdirSync5, writeFileSync as writeFileSync7 } from "fs";
13114
14174
  init_logger();
13115
14175
  init_context_scan();
13116
14176
  init_digest();
@@ -13122,10 +14182,10 @@ function inputsDigest2(paths, cwd) {
13122
14182
  return createHash4("sha256").update(combined).digest("hex");
13123
14183
  }
13124
14184
  function findContractFiles2(dir, found = []) {
13125
- if (!existsSync8(dir))
14185
+ if (!existsSync11(dir))
13126
14186
  return found;
13127
14187
  for (const entry of readdirSync5(dir, { withFileTypes: true })) {
13128
- const fullPath = join9(dir, entry.name);
14188
+ const fullPath = join12(dir, entry.name);
13129
14189
  if (entry.isDirectory())
13130
14190
  findContractFiles2(fullPath, found);
13131
14191
  else if (entry.isFile() && entry.name.endsWith(".md") && entry.name !== "INDEX.md" && entry.name !== "CHANGELOG.md") {
@@ -13135,22 +14195,22 @@ function findContractFiles2(dir, found = []) {
13135
14195
  return found;
13136
14196
  }
13137
14197
  function readIndexDigest(filePath) {
13138
- if (!existsSync8(filePath))
14198
+ if (!existsSync11(filePath))
13139
14199
  return null;
13140
- const m = readFileSync8(filePath, "utf8").match(/^inputs-digest:\s*([a-f0-9]+)/m);
14200
+ const m = readFileSync11(filePath, "utf8").match(/^inputs-digest:\s*([a-f0-9]+)/m);
13141
14201
  return m ? m[1] : null;
13142
14202
  }
13143
14203
  async function ensureFreshContextIndexes(cwd) {
13144
- const projectMap = join9(cwd, "specs", "context", "project-map.md");
13145
- const contractsIndex = join9(cwd, "specs", "context", "contracts-index.md");
13146
- const policyPath = join9(cwd, ".cdd", "context-policy.json");
13147
- const policyInputs = [policyPath].filter(existsSync8);
13148
- const contractFiles = findContractFiles2(join9(cwd, "contracts"));
14204
+ const projectMap = join12(cwd, "specs", "context", "project-map.md");
14205
+ const contractsIndex = join12(cwd, "specs", "context", "contracts-index.md");
14206
+ const policyPath = join12(cwd, ".cdd", "context-policy.json");
14207
+ const policyInputs = [policyPath].filter(existsSync11);
14208
+ const contractFiles = findContractFiles2(join12(cwd, "contracts"));
13149
14209
  const wantProjectDigest = inputsDigest2(policyInputs, cwd);
13150
14210
  const wantContractsDigest = inputsDigest2(contractFiles, cwd);
13151
14211
  const haveProjectDigest = readIndexDigest(projectMap);
13152
14212
  const haveContractsDigest = readIndexDigest(contractsIndex);
13153
- const needsScan = !existsSync8(projectMap) || !existsSync8(contractsIndex) || haveProjectDigest !== wantProjectDigest || haveContractsDigest !== wantContractsDigest;
14213
+ const needsScan = !existsSync11(projectMap) || !existsSync11(contractsIndex) || haveProjectDigest !== wantProjectDigest || haveContractsDigest !== wantContractsDigest;
13154
14214
  if (!needsScan)
13155
14215
  return;
13156
14216
  log.info("context indexes missing or stale \u2014 running cdd-kit context-scan\u2026");
@@ -13181,9 +14241,9 @@ function parseDependsOn(raw) {
13181
14241
  return raw.split(",").map((item) => item.trim()).filter(Boolean);
13182
14242
  }
13183
14243
  function applyScaffoldMetadata(tasksPath, changeId, dependencies) {
13184
- if (!existsSync8(tasksPath))
14244
+ if (!existsSync11(tasksPath))
13185
14245
  return;
13186
- let raw = readFileSync8(tasksPath, "utf8");
14246
+ let raw = readFileSync11(tasksPath, "utf8");
13187
14247
  raw = raw.replace(/^change-id:\s*<change-id>\s*$/m, `change-id: ${changeId}`);
13188
14248
  if (dependencies.length > 0) {
13189
14249
  const dependsOn = [
@@ -13192,7 +14252,7 @@ function applyScaffoldMetadata(tasksPath, changeId, dependencies) {
13192
14252
  ].join("\n");
13193
14253
  raw = raw.replace(/^depends-on:\s*\[\]\s*$/m, dependsOn);
13194
14254
  }
13195
- writeFileSync4(tasksPath, raw, "utf8");
14255
+ writeFileSync7(tasksPath, raw, "utf8");
13196
14256
  }
13197
14257
  async function newChange(name, opts) {
13198
14258
  if (!SAFE_NAME.test(name)) {
@@ -13207,8 +14267,8 @@ async function newChange(name, opts) {
13207
14267
  }
13208
14268
  }
13209
14269
  const cwd = process.cwd();
13210
- const changeDir = join9(cwd, "specs", "changes", name);
13211
- if (existsSync8(changeDir)) {
14270
+ const changeDir = join12(cwd, "specs", "changes", name);
14271
+ if (existsSync11(changeDir)) {
13212
14272
  if (opts.force) {
13213
14273
  log.warn(`Forcing re-scaffold of existing change directory: ${changeDir}`);
13214
14274
  log.warn("Existing files will NOT be deleted; only template files will be overwritten.");
@@ -13231,9 +14291,9 @@ async function newChange(name, opts) {
13231
14291
  const templates = opts.all ? [...REQUIRED_TEMPLATES, ...listOptional()] : [...REQUIRED_TEMPLATES];
13232
14292
  let written = 0;
13233
14293
  for (const tmpl of templates) {
13234
- const src = join9(ASSET.specsTemplates, tmpl);
13235
- const dest = join9(changeDir, tmpl);
13236
- if (!existsSync8(src)) {
14294
+ const src = join12(ASSET.specsTemplates, tmpl);
14295
+ const dest = join12(changeDir, tmpl);
14296
+ if (!existsSync11(src)) {
13237
14297
  log.warn(`Template not found, skipping: ${tmpl}`);
13238
14298
  continue;
13239
14299
  }
@@ -13241,7 +14301,7 @@ async function newChange(name, opts) {
13241
14301
  log.dim(tmpl);
13242
14302
  written += 1;
13243
14303
  }
13244
- const tasksPath = join9(changeDir, "tasks.yml");
14304
+ const tasksPath = join12(changeDir, "tasks.yml");
13245
14305
  applyScaffoldMetadata(tasksPath, name, dependencies);
13246
14306
  if (dependencies.length > 0) {
13247
14307
  log.dim(`depends-on: ${dependencies.join(", ")}`);
@@ -13254,9 +14314,9 @@ async function newChange(name, opts) {
13254
14314
  // src/commands/validate.ts
13255
14315
  init_paths();
13256
14316
  init_logger();
13257
- import { join as join10 } from "path";
13258
- import { existsSync as existsSync9 } from "fs";
13259
- import { spawnSync } from "child_process";
14317
+ import { join as join13 } from "path";
14318
+ import { existsSync as existsSync12 } from "fs";
14319
+ import { spawnSync as spawnSync2 } from "child_process";
13260
14320
  var VALIDATORS = [
13261
14321
  {
13262
14322
  flag: "contracts",
@@ -13264,6 +14324,7 @@ var VALIDATORS = [
13264
14324
  label: "contracts",
13265
14325
  chain: [
13266
14326
  { script: "validate_api_semantic.py", label: "API semantic" },
14327
+ { script: "validate_api_conformance.py", label: "API conformance" },
13267
14328
  { script: "validate_env_semantic.py", label: "Env semantic" }
13268
14329
  ]
13269
14330
  },
@@ -13274,7 +14335,7 @@ var VALIDATORS = [
13274
14335
  ];
13275
14336
  function resolvePython() {
13276
14337
  for (const cmd of ["python3", "python"]) {
13277
- const r = spawnSync(cmd, ["--version"], { stdio: "ignore" });
14338
+ const r = spawnSync2(cmd, ["--version"], { stdio: "ignore" });
13278
14339
  if (r.status === 0)
13279
14340
  return cmd;
13280
14341
  }
@@ -13288,21 +14349,22 @@ async function validate(opts) {
13288
14349
  log.error(e instanceof Error ? e.message : String(e));
13289
14350
  process.exit(1);
13290
14351
  }
13291
- const scriptsDir = join10(ASSET.skill, "scripts");
14352
+ const scriptsDir = join13(ASSET.skill, "scripts");
13292
14353
  const runAll = !opts.contracts && !opts.env && !opts.ci && !opts.spec && !opts.versions;
14354
+ const pyEnv = { ...process.env, PYTHONUTF8: "1", PYTHONIOENCODING: "utf-8" };
13293
14355
  log.blank();
13294
14356
  let failed = false;
13295
14357
  for (const v of VALIDATORS) {
13296
14358
  if (!runAll && !opts[v.flag])
13297
14359
  continue;
13298
- const scriptPath = join10(scriptsDir, v.script);
13299
- if (!existsSync9(scriptPath)) {
14360
+ const scriptPath = join13(scriptsDir, v.script);
14361
+ if (!existsSync12(scriptPath)) {
13300
14362
  log.warn(`${v.label}: script not found, skipping (${v.script})`);
13301
14363
  log.blank();
13302
14364
  continue;
13303
14365
  }
13304
14366
  log.info(`Validating ${v.label}\u2026`);
13305
- const r = spawnSync(py, [scriptPath, ...v.args ?? []], { stdio: "inherit", cwd: process.cwd() });
14367
+ const r = spawnSync2(py, [scriptPath, ...v.args ?? []], { stdio: "inherit", cwd: process.cwd(), env: pyEnv });
13306
14368
  if (r.status !== 0) {
13307
14369
  log.error(`${v.label} validation failed.`);
13308
14370
  failed = true;
@@ -13312,14 +14374,14 @@ async function validate(opts) {
13312
14374
  log.blank();
13313
14375
  if (v.chain) {
13314
14376
  for (const chained of v.chain) {
13315
- const chainedPath = join10(scriptsDir, chained.script);
13316
- if (!existsSync9(chainedPath)) {
14377
+ const chainedPath = join13(scriptsDir, chained.script);
14378
+ if (!existsSync12(chainedPath)) {
13317
14379
  log.warn(`${chained.label}: script not found, skipping (${chained.script})`);
13318
14380
  log.blank();
13319
14381
  continue;
13320
14382
  }
13321
14383
  log.info(`Validating ${chained.label}\u2026`);
13322
- const cr = spawnSync(py, [chainedPath], { stdio: "inherit", cwd: process.cwd() });
14384
+ const cr = spawnSync2(py, [chainedPath], { stdio: "inherit", cwd: process.cwd(), env: pyEnv });
13323
14385
  if (cr.status !== 0) {
13324
14386
  log.error(`${chained.label} validation failed.`);
13325
14387
  failed = true;
@@ -13343,8 +14405,8 @@ async function validate(opts) {
13343
14405
  var import_ajv = __toESM(require_ajv(), 1);
13344
14406
  var import_ajv_formats = __toESM(require_dist(), 1);
13345
14407
  init_logger();
13346
- import { existsSync as existsSync10, readFileSync as readFileSync9, readdirSync as readdirSync6 } from "fs";
13347
- import { join as join11 } from "path";
14408
+ import { existsSync as existsSync14, readFileSync as readFileSync13, readdirSync as readdirSync6 } from "fs";
14409
+ import { join as join15 } from "path";
13348
14410
  import yaml from "js-yaml";
13349
14411
 
13350
14412
  // src/schemas/tasks.schema.ts
@@ -13373,6 +14435,7 @@ var tasksSchema = {
13373
14435
  items: { type: "string", pattern: "^[a-zA-Z0-9][a-zA-Z0-9_-]{0,63}$" },
13374
14436
  default: []
13375
14437
  },
14438
+ "tier-floor-override": { type: "string", minLength: 1 },
13376
14439
  "token-budget": { type: ["string", "number"] },
13377
14440
  created: { type: "string" },
13378
14441
  completed: { type: "string" },
@@ -13395,6 +14458,8 @@ var tasksSchema = {
13395
14458
  };
13396
14459
 
13397
14460
  // src/commands/gate.ts
14461
+ init_tier_floor();
14462
+ init_git_paths();
13398
14463
  var ajv = new import_ajv.default({ allErrors: true, allowUnionTypes: true });
13399
14464
  (0, import_ajv_formats.default)(ajv);
13400
14465
  var validateTasks = ajv.compile(tasksSchema);
@@ -13432,6 +14497,15 @@ function meaningfulChars(text) {
13432
14497
  function stripHtmlComments(text) {
13433
14498
  return text.replace(/<!--[\s\S]*?-->/g, "");
13434
14499
  }
14500
+ var PLACEHOLDER_LITERALS = ["<id>", "<date>", "<change-id>"];
14501
+ var RE_META = /[.*+?^${}()|[\]\\]/g;
14502
+ function findPlaceholders(text) {
14503
+ const clean = stripHtmlComments(text);
14504
+ return PLACEHOLDER_LITERALS.filter((token) => {
14505
+ const re = new RegExp(`:[ \\t]*${token.replace(RE_META, "\\$&")}[ \\t]*\\r?$`, "m");
14506
+ return re.test(clean);
14507
+ }).sort();
14508
+ }
13435
14509
  function countPendingExpansions(sectionBody2) {
13436
14510
  if (!sectionBody2.trim())
13437
14511
  return 0;
@@ -13453,7 +14527,7 @@ function countPendingContextRequests(content) {
13453
14527
  }
13454
14528
  function loadYamlFile(path) {
13455
14529
  try {
13456
- const raw = readFileSync9(path, "utf8");
14530
+ const raw = readFileSync13(path, "utf8");
13457
14531
  return { data: yaml.load(raw, { schema: yaml.JSON_SCHEMA }), parseError: null };
13458
14532
  } catch (err) {
13459
14533
  return { data: null, parseError: err.message };
@@ -13484,8 +14558,8 @@ function ajvErrorsToMessages(errors, prefix, knownKeys) {
13484
14558
  return out;
13485
14559
  }
13486
14560
  function isContextGovernedChange(changeDir) {
13487
- const tasksPath = join11(changeDir, "tasks.yml");
13488
- if (!existsSync10(tasksPath))
14561
+ const tasksPath = join15(changeDir, "tasks.yml");
14562
+ if (!existsSync14(tasksPath))
13489
14563
  return false;
13490
14564
  const { data } = loadYamlFile(tasksPath);
13491
14565
  return data?.["context-governance"] === "v1";
@@ -13513,12 +14587,12 @@ function lintTasksFile(tasksPath, errors, warnings) {
13513
14587
  return data;
13514
14588
  }
13515
14589
  function resolveTier(changeDir) {
13516
- const classifPath = join11(changeDir, "change-classification.md");
13517
- const classificationPresent = existsSync10(classifPath);
13518
- const classificationText = classificationPresent ? readFileSync9(classifPath, "utf8") : "";
14590
+ const classifPath = join15(changeDir, "change-classification.md");
14591
+ const classificationPresent = existsSync14(classifPath);
14592
+ const classificationText = classificationPresent ? readFileSync13(classifPath, "utf8") : "";
13519
14593
  const classificationHasLooseMarker = classificationPresent && TIER_PATTERN.test(classificationText);
13520
- const tasksPath = join11(changeDir, "tasks.yml");
13521
- if (existsSync10(tasksPath)) {
14594
+ const tasksPath = join15(changeDir, "tasks.yml");
14595
+ if (existsSync14(tasksPath)) {
13522
14596
  const { data } = loadYamlFile(tasksPath);
13523
14597
  const t = data?.tier;
13524
14598
  if (typeof t === "number" && t >= 0 && t <= 5) {
@@ -13564,7 +14638,7 @@ function enforceTierConsistency(changeDir, errors, warnings) {
13564
14638
  return;
13565
14639
  }
13566
14640
  if (resolution.source === "tasks-frontmatter" && resolution.classificationPresent) {
13567
- const text = readFileSync9(join11(changeDir, "change-classification.md"), "utf8");
14641
+ const text = readFileSync13(join15(changeDir, "change-classification.md"), "utf8");
13568
14642
  const structured = text.match(/^## Tier\s*\n\s*-\s*(\d)\s*$/m);
13569
14643
  const bold = text.match(/\*\*Tier:\*\*\s*Tier\s*(\d)\b/i);
13570
14644
  const classifTier = structured ? parseInt(structured[1], 10) : bold ? parseInt(bold[1], 10) : NaN;
@@ -13575,12 +14649,50 @@ function enforceTierConsistency(changeDir, errors, warnings) {
13575
14649
  }
13576
14650
  }
13577
14651
  }
14652
+ function enforceTierFloor(changeDir, errors, warnings) {
14653
+ const cwd = process.cwd();
14654
+ const policy = loadTierPolicy(cwd);
14655
+ if (!policy.enabled)
14656
+ return;
14657
+ const requestPath = join15(changeDir, "change-request.md");
14658
+ const requestText = existsSync14(requestPath) ? readFileSync13(requestPath, "utf8") : "";
14659
+ const staged = getStagedPaths(cwd);
14660
+ const stagedChangeIds = new Set(
14661
+ staged.map((p) => /^specs\/changes\/([^/]+)\//.exec(p)?.[1]).filter((id) => id !== void 0)
14662
+ );
14663
+ const touched = stagedChangeIds.size > 1 ? [] : staged;
14664
+ const floor = computeTierFloor(requestText, policy, { paths: touched });
14665
+ if (floor.floorTier === null)
14666
+ return;
14667
+ const declared = resolveTier(changeDir).tier;
14668
+ if (declared === null) {
14669
+ warnings.push(
14670
+ `tier floor: ${floor.label} detected (matched: ${floor.matched.join(", ")}); classification must declare tier ${floor.floorTier} or stricter.`
14671
+ );
14672
+ return;
14673
+ }
14674
+ if (declared <= floor.floorTier)
14675
+ return;
14676
+ const overrideRaw = (() => {
14677
+ const { data } = loadYamlFile(join15(changeDir, "tasks.yml"));
14678
+ const o = data?.["tier-floor-override"];
14679
+ return typeof o === "string" ? o.trim() : "";
14680
+ })();
14681
+ const detail = `${floor.label} detected (matched: ${floor.matched.join(", ")}) requires tier ${floor.floorTier} or stricter, but classification declared tier ${declared}.`;
14682
+ if (overrideRaw) {
14683
+ warnings.push(`tier floor override: ${detail} Bypassed by tier-floor-override: "${overrideRaw}".`);
14684
+ } else {
14685
+ errors.push(
14686
+ `tier floor violation: ${detail} Re-classify to tier ${floor.floorTier} (or stricter), or record \`tier-floor-override: "<reason>"\` in tasks.yml frontmatter to bypass with an audit trail. Disable entirely in .cdd/tier-policy.json.`
14687
+ );
14688
+ }
14689
+ }
13578
14690
  function isArchivedChange(cwd, changeId) {
13579
- const archiveRoot = join11(cwd, "specs", "archive");
13580
- if (!existsSync10(archiveRoot))
14691
+ const archiveRoot = join15(cwd, "specs", "archive");
14692
+ if (!existsSync14(archiveRoot))
13581
14693
  return false;
13582
14694
  const years = readdirSync6(archiveRoot, { withFileTypes: true }).filter((d) => d.isDirectory());
13583
- return years.some((year) => existsSync10(join11(archiveRoot, year.name, changeId)));
14695
+ return years.some((year) => existsSync14(join15(archiveRoot, year.name, changeId)));
13584
14696
  }
13585
14697
  function detectDependencyCycle(cwd, startChangeId) {
13586
14698
  const visited = /* @__PURE__ */ new Set();
@@ -13593,8 +14705,8 @@ function detectDependencyCycle(cwd, startChangeId) {
13593
14705
  return null;
13594
14706
  visited.add(id);
13595
14707
  stack.push(id);
13596
- const tasksPath = join11(cwd, "specs", "changes", id, "tasks.yml");
13597
- if (existsSync10(tasksPath)) {
14708
+ const tasksPath = join15(cwd, "specs", "changes", id, "tasks.yml");
14709
+ if (existsSync14(tasksPath)) {
13598
14710
  const { data } = loadYamlFile(tasksPath);
13599
14711
  const deps = data?.["depends-on"] ?? [];
13600
14712
  for (const dep of deps) {
@@ -13609,8 +14721,8 @@ function detectDependencyCycle(cwd, startChangeId) {
13609
14721
  return visit(startChangeId);
13610
14722
  }
13611
14723
  function validateDependencies(cwd, changeId, changeDir) {
13612
- const tasksPath = join11(changeDir, "tasks.yml");
13613
- if (!existsSync10(tasksPath))
14724
+ const tasksPath = join15(changeDir, "tasks.yml");
14725
+ if (!existsSync14(tasksPath))
13614
14726
  return [];
13615
14727
  const { data } = loadYamlFile(tasksPath);
13616
14728
  const dependencies = data?.["depends-on"] ?? [];
@@ -13624,10 +14736,10 @@ function validateDependencies(cwd, changeId, changeDir) {
13624
14736
  errors.push(`tasks.yml: change cannot depend on itself (${dep})`);
13625
14737
  continue;
13626
14738
  }
13627
- const upstreamDir = join11(cwd, "specs", "changes", dep);
13628
- if (existsSync10(upstreamDir)) {
13629
- const upstreamTasks = join11(upstreamDir, "tasks.yml");
13630
- if (!existsSync10(upstreamTasks)) {
14739
+ const upstreamDir = join15(cwd, "specs", "changes", dep);
14740
+ if (existsSync14(upstreamDir)) {
14741
+ const upstreamTasks = join15(upstreamDir, "tasks.yml");
14742
+ if (!existsSync14(upstreamTasks)) {
13631
14743
  errors.push(`dependency ${dep}: missing tasks.yml`);
13632
14744
  continue;
13633
14745
  }
@@ -13647,19 +14759,19 @@ function validateDependencies(cwd, changeId, changeDir) {
13647
14759
  async function gate(changeId, opts = {}) {
13648
14760
  const strict = opts.strict ?? false;
13649
14761
  const cwd = process.cwd();
13650
- const changeDir = join11(cwd, "specs", "changes", changeId);
13651
- if (!existsSync10(changeDir)) {
14762
+ const changeDir = join15(cwd, "specs", "changes", changeId);
14763
+ if (!existsSync14(changeDir)) {
13652
14764
  log.error(`change not found: ${changeId} (looked in ${changeDir})`);
13653
14765
  process.exit(1);
13654
14766
  }
13655
14767
  const errors = [];
13656
14768
  const warnings = [];
13657
14769
  const isNewChange = isContextGovernedChange(changeDir);
13658
- const manifestPath = join11(changeDir, "context-manifest.md");
13659
- const hasManifest = existsSync10(manifestPath);
14770
+ const manifestPath = join15(changeDir, "context-manifest.md");
14771
+ const hasManifest = existsSync14(manifestPath);
13660
14772
  errors.push(...validateDependencies(cwd, changeId, changeDir));
13661
14773
  if (hasManifest) {
13662
- const pending = countPendingContextRequests(readFileSync9(manifestPath, "utf8"));
14774
+ const pending = countPendingContextRequests(readFileSync13(manifestPath, "utf8"));
13663
14775
  if (pending > 0) {
13664
14776
  warnings.push(`context-manifest.md: has ${pending} pending context expansion request(s)`);
13665
14777
  }
@@ -13675,7 +14787,7 @@ async function gate(changeId, opts = {}) {
13675
14787
  }
13676
14788
  continue;
13677
14789
  }
13678
- if (!existsSync10(join11(changeDir, f))) {
14790
+ if (!existsSync14(join15(changeDir, f))) {
13679
14791
  errors.push(`missing required artifact: ${f}`);
13680
14792
  }
13681
14793
  }
@@ -13685,21 +14797,30 @@ async function gate(changeId, opts = {}) {
13685
14797
  continue;
13686
14798
  if (f === "tasks.yml")
13687
14799
  continue;
13688
- const content = readFileSync9(join11(changeDir, f), "utf8");
14800
+ const content = readFileSync13(join15(changeDir, f), "utf8");
13689
14801
  const minChars = MIN_CHARS[f] ?? 100;
13690
14802
  if (meaningfulChars(content) < minChars) {
13691
14803
  errors.push(`${f}: appears to be a stub (< ${minChars} meaningful chars)`);
14804
+ continue;
14805
+ }
14806
+ if (f !== "context-manifest.md") {
14807
+ const placeholders = findPlaceholders(content);
14808
+ if (placeholders.length > 0) {
14809
+ errors.push(
14810
+ `${f}: still contains unfilled template placeholder(s) ${placeholders.join(", ")} \u2014 replace them with the change's real values before the gate can pass`
14811
+ );
14812
+ }
13692
14813
  }
13693
14814
  }
13694
- const classifPath = join11(changeDir, "change-classification.md");
14815
+ const classifPath = join15(changeDir, "change-classification.md");
13695
14816
  const tierResolution = resolveTier(changeDir);
13696
- if (tierResolution.tier === null && existsSync10(classifPath) && !tierResolution.classificationHasLooseMarker) {
14817
+ if (tierResolution.tier === null && existsSync14(classifPath) && !tierResolution.classificationHasLooseMarker) {
13697
14818
  errors.push("change-classification.md: missing tier/risk marker (set tier in tasks.yml frontmatter, or include Tier 0-5 / low|medium|high|critical in change-classification.md)");
13698
14819
  }
13699
14820
  }
13700
- const tasksPath = join11(changeDir, "tasks.yml");
14821
+ const tasksPath = join15(changeDir, "tasks.yml");
13701
14822
  let tasksData = null;
13702
- if (existsSync10(tasksPath)) {
14823
+ if (existsSync14(tasksPath)) {
13703
14824
  tasksData = lintTasksFile(tasksPath, errors, warnings);
13704
14825
  }
13705
14826
  if (tasksData) {
@@ -13714,6 +14835,7 @@ async function gate(changeId, opts = {}) {
13714
14835
  }
13715
14836
  }
13716
14837
  enforceTierConsistency(changeDir, errors, warnings);
14838
+ enforceTierFloor(changeDir, errors, warnings);
13717
14839
  for (const w of warnings) {
13718
14840
  log.warn(` ${w}`);
13719
14841
  }
@@ -13737,75 +14859,540 @@ async function gate(changeId, opts = {}) {
13737
14859
  log.ok(`gate passed for change: ${changeId}`);
13738
14860
  }
13739
14861
 
13740
- // src/commands/install-hooks.ts
13741
- init_paths();
14862
+ // src/cli/index.ts
14863
+ init_install_hooks();
14864
+ init_install_agent_hooks();
14865
+
14866
+ // src/commands/openapi-export.ts
13742
14867
  init_logger();
13743
- import { existsSync as existsSync11, readFileSync as readFileSync10, writeFileSync as writeFileSync5, chmodSync as chmodSync2, mkdirSync as mkdirSync5 } from "fs";
13744
- import { join as join12 } from "path";
13745
- var START_MARKER2 = "# cdd-kit-managed-block-start";
13746
- var END_MARKER2 = "# cdd-kit-managed-block-end";
13747
- async function installHooks() {
13748
- const cwd = process.cwd();
13749
- const gitDir = join12(cwd, ".git");
13750
- if (!existsSync11(gitDir)) {
13751
- log.error("not a git repository (no .git/ found in cwd)");
13752
- process.exit(1);
14868
+ import { existsSync as existsSync15, readFileSync as readFileSync14, writeFileSync as writeFileSync8, mkdirSync as mkdirSync7 } from "fs";
14869
+ import { dirname as dirname4, resolve as resolve2 } from "path";
14870
+ var DEFAULT_CONTRACT = "contracts/api/api-contract.md";
14871
+ var VALID_METHODS = /* @__PURE__ */ new Set(["get", "post", "put", "delete", "patch", "head", "options"]);
14872
+ function stripFrontmatter(text) {
14873
+ const fm = {};
14874
+ if (text.startsWith("---")) {
14875
+ const end = text.indexOf("\n---", 3);
14876
+ if (end !== -1) {
14877
+ const block = text.slice(3, end);
14878
+ for (const line of block.split("\n")) {
14879
+ const m = line.match(/^([A-Za-z0-9_-]+):\s*(.*)$/);
14880
+ if (m)
14881
+ fm[m[1].trim()] = m[2].trim();
14882
+ }
14883
+ return { body: text.slice(end + 4).replace(/^\n+/, ""), frontmatter: fm };
14884
+ }
13753
14885
  }
13754
- const hooksDir = join12(gitDir, "hooks");
13755
- mkdirSync5(hooksDir, { recursive: true });
13756
- const dest = join12(hooksDir, "pre-commit");
13757
- const ourHook = readFileSync10(join12(ASSET.hooks, "pre-commit"), "utf8");
13758
- let final;
13759
- if (!existsSync11(dest)) {
13760
- final = ourHook;
13761
- } else {
13762
- const existing = readFileSync10(dest, "utf8");
13763
- const startIdx = existing.indexOf(START_MARKER2);
13764
- const endIdx = existing.indexOf(END_MARKER2);
13765
- if (startIdx >= 0 && endIdx > startIdx) {
13766
- const before = existing.slice(0, startIdx);
13767
- const after = existing.slice(endIdx + END_MARKER2.length);
13768
- const ourStart = ourHook.indexOf(START_MARKER2);
13769
- const ourEnd = ourHook.indexOf(END_MARKER2) + END_MARKER2.length;
13770
- const ourBlock = ourHook.slice(ourStart, ourEnd);
13771
- final = before + ourBlock + after;
13772
- } else {
13773
- const ourStart = ourHook.indexOf(START_MARKER2);
13774
- const ourEnd = ourHook.indexOf(END_MARKER2) + END_MARKER2.length;
13775
- const ourBlock = ourHook.slice(ourStart, ourEnd);
13776
- if (existing.startsWith("#!")) {
13777
- const firstNewline = existing.indexOf("\n");
13778
- const shebang = existing.slice(0, firstNewline + 1);
13779
- const rest = existing.slice(firstNewline + 1);
13780
- final = shebang + "\n" + ourBlock + "\n" + rest;
14886
+ return { body: text, frontmatter: fm };
14887
+ }
14888
+ function parseRow(line) {
14889
+ return line.trim().replace(/^\|/, "").replace(/\|$/, "").split("|").map((c) => c.trim());
14890
+ }
14891
+ function isSeparator(cells) {
14892
+ return cells.every((c) => c === "" || /^:?-+:?$/.test(c));
14893
+ }
14894
+ function parseEndpoints(body) {
14895
+ const rows = [];
14896
+ let inTable = false;
14897
+ let sepSeen = false;
14898
+ for (const raw of body.split("\n")) {
14899
+ const line = raw.trim();
14900
+ if (!line || !line.startsWith("|"))
14901
+ continue;
14902
+ const cells = parseRow(line);
14903
+ if (cells[0]?.toLowerCase() === "method") {
14904
+ inTable = true;
14905
+ sepSeen = false;
14906
+ continue;
14907
+ }
14908
+ if (!inTable)
14909
+ continue;
14910
+ if (!sepSeen && isSeparator(cells)) {
14911
+ sepSeen = true;
14912
+ continue;
14913
+ }
14914
+ if (cells.length < 2 || !cells.some(Boolean))
14915
+ continue;
14916
+ const method = (cells[0] ?? "").toLowerCase();
14917
+ const path = cells[1] ?? "";
14918
+ if (!VALID_METHODS.has(method) || !path.startsWith("/"))
14919
+ continue;
14920
+ rows.push({
14921
+ method,
14922
+ path,
14923
+ auth: (cells[2] ?? "").toLowerCase(),
14924
+ request: cells[3] ?? "",
14925
+ response: cells[4] ?? "",
14926
+ errors: cells[5] ?? ""
14927
+ });
14928
+ }
14929
+ return rows;
14930
+ }
14931
+ var SCHEMA_NAME_RE = /^[A-Za-z][A-Za-z0-9_]*$/;
14932
+ var PRIMITIVE_TYPES = /* @__PURE__ */ new Set(["string", "integer", "number", "boolean"]);
14933
+ function extractSchemasSection(body) {
14934
+ const lines = body.split("\n");
14935
+ const start = lines.findIndex((line) => /^##\s+Schemas\s*$/i.test(line.trim()));
14936
+ if (start === -1)
14937
+ return "";
14938
+ const out = [];
14939
+ for (let i = start + 1; i < lines.length; i += 1) {
14940
+ if (/^##\s+/.test(lines[i].trim()))
14941
+ break;
14942
+ out.push(lines[i]);
14943
+ }
14944
+ return out.join("\n");
14945
+ }
14946
+ function parseSchemaSections(body) {
14947
+ const schemasBlock = extractSchemasSection(body).replace(/<!--[\s\S]*?-->/g, "");
14948
+ if (!schemasBlock.trim())
14949
+ return { sections: [], errors: [] };
14950
+ const sections = [];
14951
+ const errors = [];
14952
+ const seen = /* @__PURE__ */ new Set();
14953
+ const headingRe = /^###\s+(.+?)\s*$/gm;
14954
+ const headings = Array.from(schemasBlock.matchAll(headingRe));
14955
+ for (let i = 0; i < headings.length; i += 1) {
14956
+ const heading = headings[i];
14957
+ const name = (heading[1] ?? "").trim();
14958
+ const contentStart = (heading.index ?? 0) + heading[0].length;
14959
+ const contentEnd = i + 1 < headings.length ? headings[i + 1].index ?? schemasBlock.length : schemasBlock.length;
14960
+ if (!SCHEMA_NAME_RE.test(name))
14961
+ continue;
14962
+ if (seen.has(name)) {
14963
+ errors.push(`Duplicate schema section: ${name}`);
14964
+ continue;
14965
+ }
14966
+ seen.add(name);
14967
+ sections.push({ name, content: schemasBlock.slice(contentStart, contentEnd) });
14968
+ }
14969
+ return { sections, errors };
14970
+ }
14971
+ function parseJsonSchemaBlocks(section) {
14972
+ const blocks = [];
14973
+ const errors = [];
14974
+ const blockRe = /```json-schema\s*\n([\s\S]*?)```/g;
14975
+ for (const match of section.content.matchAll(blockRe)) {
14976
+ try {
14977
+ const parsed = JSON.parse(match[1] ?? "");
14978
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
14979
+ errors.push(`Schema ${section.name}: json-schema block must be a JSON object`);
13781
14980
  } else {
13782
- final = "#!/bin/sh\n" + ourBlock + "\n" + existing;
14981
+ blocks.push(parsed);
13783
14982
  }
14983
+ } catch (err) {
14984
+ const detail = err instanceof Error ? err.message : String(err);
14985
+ errors.push(`Schema ${section.name}: invalid json-schema block (${detail})`);
13784
14986
  }
13785
14987
  }
13786
- writeFileSync5(dest, final, "utf8");
13787
- try {
13788
- chmodSync2(dest, 493);
13789
- } catch {
14988
+ return { blocks, errors };
14989
+ }
14990
+ function findForeignFenceTag(section) {
14991
+ const fenceRe = /```([^\n]*)(?:\n|$)[\s\S]*?```/g;
14992
+ for (const match of section.content.matchAll(fenceRe)) {
14993
+ const info = (match[1] ?? "").trim();
14994
+ if (info !== "json-schema")
14995
+ return info;
13790
14996
  }
13791
- log.ok(`pre-commit hook installed at ${dest}`);
13792
- log.info("cdd-kit gate will now run automatically before each commit affecting specs/changes/");
14997
+ return null;
14998
+ }
14999
+ function parseFieldTable(section) {
15000
+ const lines = section.content.split("\n");
15001
+ for (let i = 0; i < lines.length; i += 1) {
15002
+ const line = lines[i].trim();
15003
+ if (!line.startsWith("|"))
15004
+ continue;
15005
+ const header = parseRow(line).map((c) => c.toLowerCase());
15006
+ const fieldIdx = header.indexOf("field");
15007
+ const typeIdx = header.indexOf("type");
15008
+ const requiredIdx = header.indexOf("required");
15009
+ if (fieldIdx === -1 || typeIdx === -1 || requiredIdx === -1)
15010
+ continue;
15011
+ const separator = lines[i + 1]?.trim();
15012
+ if (!separator?.startsWith("|") || !isSeparator(parseRow(separator)))
15013
+ continue;
15014
+ const notesIdx = header.indexOf("notes");
15015
+ const formatIdx = header.indexOf("format");
15016
+ const rows = [];
15017
+ for (let j = i + 2; j < lines.length; j += 1) {
15018
+ const rowLine = lines[j].trim();
15019
+ if (!rowLine || !rowLine.startsWith("|"))
15020
+ break;
15021
+ const cells = parseRow(rowLine);
15022
+ if (isSeparator(cells) || cells.length < 2 || !cells.some(Boolean))
15023
+ continue;
15024
+ rows.push({
15025
+ field: cells[fieldIdx] ?? "",
15026
+ type: cells[typeIdx] ?? "",
15027
+ required: cells[requiredIdx] ?? "",
15028
+ notes: notesIdx === -1 ? "" : cells[notesIdx] ?? "",
15029
+ format: formatIdx === -1 ? "" : cells[formatIdx] ?? ""
15030
+ });
15031
+ }
15032
+ return { rows, found: true };
15033
+ }
15034
+ return { rows: [], found: false };
15035
+ }
15036
+ function compileType(typeValue, schemaNames, resolvableNames, context2) {
15037
+ const type = typeValue.trim();
15038
+ if (!type)
15039
+ throw new Error(`${context2}: empty type`);
15040
+ if (type.endsWith("[]")) {
15041
+ const inner = type.slice(0, -2).trim();
15042
+ if (!inner || inner.endsWith("[]"))
15043
+ throw new Error(`${context2}: unsupported array type "${type}"`);
15044
+ return { type: "array", items: compileType(inner, schemaNames, resolvableNames, context2) };
15045
+ }
15046
+ if (PRIMITIVE_TYPES.has(type))
15047
+ return { type };
15048
+ const enumMatch = type.match(/^enum\((.*)\)$/);
15049
+ if (enumMatch) {
15050
+ const values = (enumMatch[1] ?? "").split(",").map((v) => v.trim()).filter(Boolean);
15051
+ if (values.length === 0)
15052
+ throw new Error(`${context2}: enum must list at least one value`);
15053
+ return { type: "string", enum: values };
15054
+ }
15055
+ if (SCHEMA_NAME_RE.test(type) && schemaNames.has(type)) {
15056
+ if (!resolvableNames.has(type)) {
15057
+ throw new Error(`${context2}: referenced schema "${type}" has no field table or json-schema block`);
15058
+ }
15059
+ return { $ref: `#/components/schemas/${type}` };
15060
+ }
15061
+ throw new Error(`${context2}: unknown type "${type}"`);
15062
+ }
15063
+ function compileFieldTable(section, rows, schemaNames, resolvableNames) {
15064
+ const properties = {};
15065
+ const required = [];
15066
+ for (const row of rows) {
15067
+ const field = row.field.trim();
15068
+ if (!field)
15069
+ throw new Error(`Schema ${section.name}: field name is required`);
15070
+ const schema = compileType(row.type, schemaNames, resolvableNames, `Schema ${section.name}, field ${field}`);
15071
+ if (row.notes.trim())
15072
+ schema.description = row.notes.trim();
15073
+ if (row.format.trim())
15074
+ schema.format = row.format.trim();
15075
+ properties[field] = schema;
15076
+ if (row.required.trim().toLowerCase() === "yes")
15077
+ required.push(field);
15078
+ }
15079
+ const compiled = { type: "object", properties };
15080
+ if (required.length > 0)
15081
+ compiled.required = required;
15082
+ return compiled;
15083
+ }
15084
+ function parseContractSchemas(body) {
15085
+ const { sections, errors } = parseSchemaSections(body);
15086
+ if (sections.length === 0)
15087
+ return { schemas: {}, errors };
15088
+ const schemaNames = new Set(sections.map((s) => s.name));
15089
+ const metadata = /* @__PURE__ */ new Map();
15090
+ const resolvableNames = /* @__PURE__ */ new Set();
15091
+ for (const section of sections) {
15092
+ const raw = parseJsonSchemaBlocks(section);
15093
+ errors.push(...raw.errors);
15094
+ if (raw.blocks.length > 1)
15095
+ errors.push(`Schema ${section.name}: expected at most one json-schema block`);
15096
+ const fields = parseFieldTable(section);
15097
+ if (raw.blocks.length > 0 && fields.found) {
15098
+ errors.push(`Schema ${section.name}: choose either a field table or a json-schema block, not both`);
15099
+ }
15100
+ const foreign = findForeignFenceTag(section);
15101
+ if (foreign !== null) {
15102
+ const desc = foreign ? `a \`\`\`${foreign} code block` : "an untagged code block";
15103
+ errors.push(
15104
+ `Schema ${section.name}: found ${desc}, but a machine-typed schema body must use a \`\`\`json-schema fence. Change the fence to \`\`\`json-schema, use a "| field | type | required | ... |" table, or remove the fence to keep it a free-form (Tier C) prose contract.`
15105
+ );
15106
+ }
15107
+ if (raw.blocks.length === 1 || fields.found)
15108
+ resolvableNames.add(section.name);
15109
+ metadata.set(section.name, {
15110
+ section,
15111
+ rawBlocks: raw.blocks,
15112
+ fieldRows: fields.rows,
15113
+ hasFieldTable: fields.found
15114
+ });
15115
+ }
15116
+ const schemas = {};
15117
+ for (const [name, item] of metadata) {
15118
+ if (item.rawBlocks.length === 1 && !item.hasFieldTable) {
15119
+ schemas[name] = item.rawBlocks[0];
15120
+ continue;
15121
+ }
15122
+ if (!item.hasFieldTable)
15123
+ continue;
15124
+ try {
15125
+ schemas[name] = compileFieldTable(item.section, item.fieldRows, schemaNames, resolvableNames);
15126
+ } catch (err) {
15127
+ errors.push(err instanceof Error ? err.message : String(err));
15128
+ }
15129
+ }
15130
+ return { schemas, errors };
15131
+ }
15132
+ function resolveSchemaCell(cell, schemas) {
15133
+ const raw = cell.trim();
15134
+ if (!raw || raw === "-")
15135
+ return void 0;
15136
+ const isArray = raw.endsWith("[]");
15137
+ const name = isArray ? raw.slice(0, -2).trim() : raw;
15138
+ if (!SCHEMA_NAME_RE.test(name) || !schemas[name])
15139
+ return void 0;
15140
+ const ref = { $ref: `#/components/schemas/${name}` };
15141
+ return isArray ? { type: "array", items: ref } : ref;
15142
+ }
15143
+ function toOpenApiPath(path) {
15144
+ const params = [];
15145
+ const seen = /* @__PURE__ */ new Set();
15146
+ const addParam = (name) => {
15147
+ if (name && !seen.has(name)) {
15148
+ seen.add(name);
15149
+ params.push({ name, in: "path", required: true, schema: { type: "string" } });
15150
+ }
15151
+ };
15152
+ let oapi = path.replace(/:([A-Za-z_][\w]*)/g, (_m, n) => {
15153
+ addParam(n);
15154
+ return `{${n}}`;
15155
+ }).replace(/\{([^}/]+)\}/g, (_m, n) => {
15156
+ addParam(n.trim());
15157
+ return `{${n.trim()}}`;
15158
+ });
15159
+ oapi = oapi.split("?", 1)[0];
15160
+ return { path: oapi, params };
15161
+ }
15162
+ function authToSecurity(auth) {
15163
+ switch (auth) {
15164
+ case "required":
15165
+ case "admin":
15166
+ return { security: [{ bearerAuth: [] }], scheme: "bearerAuth" };
15167
+ case "optional":
15168
+ return { security: [{ bearerAuth: [] }, {}], scheme: "bearerAuth" };
15169
+ case "none":
15170
+ case "public":
15171
+ case "":
15172
+ return {};
15173
+ default:
15174
+ return { security: [{ bearerAuth: [] }], scheme: "bearerAuth" };
15175
+ }
15176
+ }
15177
+ function statusFromErrors(errors) {
15178
+ const codes = (errors.match(/\b[1-5]\d\d\b/g) ?? []).filter((v, i, a) => a.indexOf(v) === i);
15179
+ return codes;
15180
+ }
15181
+ function buildDoc(endpoints, frontmatter, styleBlock, schemas) {
15182
+ const paths = {};
15183
+ let anySecurity = false;
15184
+ for (const ep of endpoints) {
15185
+ const { path, params } = toOpenApiPath(ep.path);
15186
+ const successCode = ep.method === "post" ? "201" : "200";
15187
+ const responseSchema = resolveSchemaCell(ep.response, schemas);
15188
+ const responses = {
15189
+ [successCode]: { description: ep.response ? `Contract response: ${ep.response}` : "Success" }
15190
+ };
15191
+ if (responseSchema) {
15192
+ responses[successCode].content = {
15193
+ "application/json": { schema: responseSchema }
15194
+ };
15195
+ }
15196
+ for (const code of statusFromErrors(ep.errors)) {
15197
+ if (!responses[code])
15198
+ responses[code] = { description: "Error response (see contract error format)" };
15199
+ }
15200
+ const op = {
15201
+ summary: `${ep.method.toUpperCase()} ${ep.path}`,
15202
+ responses
15203
+ };
15204
+ if (params.length > 0)
15205
+ op.parameters = params;
15206
+ const { security, scheme } = authToSecurity(ep.auth);
15207
+ if (security) {
15208
+ op.security = security;
15209
+ if (scheme)
15210
+ anySecurity = true;
15211
+ }
15212
+ if (ep.request && ep.request !== "-" && ep.method !== "get") {
15213
+ const requestSchema = resolveSchemaCell(ep.request, schemas);
15214
+ if (requestSchema) {
15215
+ op.requestBody = {
15216
+ description: `Contract request: ${ep.request}`,
15217
+ content: {
15218
+ "application/json": { schema: requestSchema }
15219
+ }
15220
+ };
15221
+ } else {
15222
+ op.requestBody = {
15223
+ description: `Contract request: ${ep.request} (schema not machine-resolved; see contract)`,
15224
+ content: {},
15225
+ "x-cdd-unresolved": true
15226
+ };
15227
+ }
15228
+ }
15229
+ if (ep.response && !responseSchema)
15230
+ op["x-cdd-response-contract"] = ep.response;
15231
+ if (ep.errors)
15232
+ op["x-cdd-errors"] = ep.errors;
15233
+ paths[path] = paths[path] ?? {};
15234
+ paths[path][ep.method] = op;
15235
+ }
15236
+ const doc = {
15237
+ openapi: "3.1.0",
15238
+ info: {
15239
+ title: frontmatter.summary || frontmatter.surface || "API",
15240
+ version: frontmatter["schema-version"] || "0.0.0"
15241
+ },
15242
+ paths,
15243
+ "x-cdd-generated-from": DEFAULT_CONTRACT,
15244
+ "x-cdd-note": "Generated by `cdd-kit openapi export` from the markdown API contract (the source of truth). Partial by design: request/response bodies are free-form prose in the contract and are marked unresolved. Do not hand-edit; regenerate from the contract."
15245
+ };
15246
+ const styleText = styleBlock.trim();
15247
+ if (styleText)
15248
+ doc.info.description = styleText;
15249
+ if (anySecurity) {
15250
+ doc.components = doc.components ?? {};
15251
+ doc.components.securitySchemes = {
15252
+ bearerAuth: { type: "http", scheme: "bearer" }
15253
+ };
15254
+ }
15255
+ if (Object.keys(schemas).length > 0) {
15256
+ doc.components = doc.components ?? {};
15257
+ doc.components.schemas = schemas;
15258
+ }
15259
+ return doc;
15260
+ }
15261
+ function extractStyleBlock(body) {
15262
+ const m = body.match(/^##\s+API Style\s*\n([\s\S]*?)(?:\n##\s|\n#\s|$)/m);
15263
+ if (!m)
15264
+ return "";
15265
+ return m[1].split("\n").map((l) => l.trim()).filter((l) => l.startsWith("-")).join("\n");
15266
+ }
15267
+ function isNonEmptyComposite(v) {
15268
+ if (typeof v !== "object" || v === null)
15269
+ return false;
15270
+ return Array.isArray(v) ? v.length > 0 : Object.keys(v).length > 0;
15271
+ }
15272
+ function yamlScalar(value) {
15273
+ if (value === null || value === void 0)
15274
+ return "null";
15275
+ if (typeof value === "number" || typeof value === "boolean")
15276
+ return String(value);
15277
+ const s = String(value);
15278
+ if (s === "" || /[:#?{}\[\],&*!|>'"%@`]/.test(s) || /^[\s-]/.test(s) || /\s$/.test(s) || /^\d/.test(s)) {
15279
+ return JSON.stringify(s);
15280
+ }
15281
+ return s;
15282
+ }
15283
+ function toYaml(value, indent = 0) {
15284
+ const pad = " ".repeat(indent);
15285
+ if (!isNonEmptyComposite(value)) {
15286
+ if (Array.isArray(value))
15287
+ return "[]";
15288
+ if (typeof value === "object" && value !== null)
15289
+ return "{}";
15290
+ return yamlScalar(value);
15291
+ }
15292
+ if (Array.isArray(value)) {
15293
+ return value.map((item) => {
15294
+ if (isNonEmptyComposite(item)) {
15295
+ return `${pad}-
15296
+ ${toYaml(item, indent + 1)}`;
15297
+ }
15298
+ return `${pad}- ${toYaml(item, indent)}`;
15299
+ }).join("\n");
15300
+ }
15301
+ return Object.entries(value).map(([k, v]) => {
15302
+ const key = /[:#\s]/.test(k) ? JSON.stringify(k) : k;
15303
+ if (isNonEmptyComposite(v)) {
15304
+ return `${pad}${key}:
15305
+ ${toYaml(v, indent + 1)}`;
15306
+ }
15307
+ return `${pad}${key}: ${toYaml(v, indent)}`;
15308
+ }).join("\n");
15309
+ }
15310
+ async function openapiExport(opts = {}) {
15311
+ const contractPath = opts.contract || DEFAULT_CONTRACT;
15312
+ const format = opts.format || "json";
15313
+ if (!existsSync15(contractPath)) {
15314
+ log.error(`API contract not found: ${contractPath}`);
15315
+ return 1;
15316
+ }
15317
+ const raw = readFileSync14(contractPath, "utf8");
15318
+ const { body, frontmatter } = stripFrontmatter(raw);
15319
+ const endpoints = parseEndpoints(body);
15320
+ const contractSchemas = parseContractSchemas(body);
15321
+ if (contractSchemas.errors.length > 0) {
15322
+ for (const err of contractSchemas.errors)
15323
+ log.error(err);
15324
+ return 1;
15325
+ }
15326
+ if (endpoints.length === 0) {
15327
+ log.error(`No endpoint table rows found in ${contractPath}. Add rows to the "| method | path | ... |" table first.`);
15328
+ return 1;
15329
+ }
15330
+ const styleBlock = extractStyleBlock(body);
15331
+ const doc = buildDoc(endpoints, frontmatter, styleBlock, contractSchemas.schemas);
15332
+ const serialized = format === "yaml" ? `${toYaml(doc)}
15333
+ ` : `${JSON.stringify(doc, null, 2)}
15334
+ `;
15335
+ if (opts.check) {
15336
+ if (!opts.out) {
15337
+ log.error("openapi export --check requires --out <path> (the committed artifact to verify against the contract)");
15338
+ return 1;
15339
+ }
15340
+ const contractFlag = contractPath === DEFAULT_CONTRACT ? "" : ` --contract ${contractPath}`;
15341
+ const yamlFlag = format === "yaml" ? " --yaml" : "";
15342
+ const regen = `cdd-kit openapi export${contractFlag} --out ${opts.out}${yamlFlag}`;
15343
+ if (!existsSync15(opts.out)) {
15344
+ log.error(`openapi export --check: ${opts.out} does not exist. Run \`${regen}\` and commit it.`);
15345
+ return 1;
15346
+ }
15347
+ const committed = readFileSync14(opts.out, "utf8");
15348
+ if (committed === serialized) {
15349
+ log.ok(`OpenAPI artifact ${opts.out} is in sync with ${contractPath} (${endpoints.length} endpoint(s))`);
15350
+ return 0;
15351
+ }
15352
+ log.error(`OpenAPI artifact ${opts.out} is OUT OF SYNC with ${contractPath}. The contract changed but the export was not regenerated.`);
15353
+ log.error(`Fix: \`${regen}\` and commit the result.`);
15354
+ return 1;
15355
+ }
15356
+ if (opts.out) {
15357
+ const outPath = resolve2(opts.out);
15358
+ mkdirSync7(dirname4(outPath), { recursive: true });
15359
+ writeFileSync8(outPath, serialized, "utf8");
15360
+ log.ok(`OpenAPI ${format.toUpperCase()} written to ${outPath} (${endpoints.length} endpoint(s))`);
15361
+ const unresolved = countUnresolved(doc);
15362
+ if (unresolved > 0) {
15363
+ log.info(`${unresolved} request body schema(s) left unresolved (free-form prose in the contract). Fill them in the consumer generator or enrich the contract.`);
15364
+ }
15365
+ } else {
15366
+ process.stdout.write(serialized);
15367
+ }
15368
+ return 0;
15369
+ }
15370
+ function countUnresolved(doc) {
15371
+ let n = 0;
15372
+ for (const methods of Object.values(doc.paths)) {
15373
+ for (const op of Object.values(methods)) {
15374
+ if (op.requestBody?.["x-cdd-unresolved"])
15375
+ n += 1;
15376
+ }
15377
+ }
15378
+ return n;
13793
15379
  }
13794
15380
 
13795
15381
  // src/cli/index.ts
13796
- var __dirname2 = dirname7(fileURLToPath4(import.meta.url));
13797
- var pkg = JSON.parse(readFileSync31(join30(__dirname2, "..", "..", "package.json"), "utf8"));
15382
+ var __dirname2 = dirname8(fileURLToPath4(import.meta.url));
15383
+ var pkg = JSON.parse(readFileSync39(join35(__dirname2, "..", "..", "package.json"), "utf8"));
13798
15384
  var program = new Command();
13799
15385
  program.name("cdd-kit").description("Contract-Driven Delivery Kit CLI").version(pkg.version);
13800
15386
  program.command("init").description(
13801
15387
  "Install agents/skill into ~/.claude and scaffold project files in cwd"
13802
- ).option("--global-only", "Only install into ~/.claude, skip project files", false).option("--local-only", "Only scaffold project files, skip ~/.claude", false).option("--force", "Overwrite existing project files", false).option("--provider <provider>", "Provider adapter to scaffold: claude, codex, or both", "claude").option("--hooks", "Install a pre-commit hook that auto-regenerates .cdd/code-map.yml", false).action(
15388
+ ).option("--global-only", "Only install into ~/.claude, skip project files", false).option("--local-only", "Only scaffold project files, skip ~/.claude", false).option("--force", "Overwrite existing project files", false).option("--provider <provider>", "Provider adapter to scaffold: claude, codex, or both", "claude").option("--hooks", "Also install the pre-commit hook that auto-regenerates .cdd/code-map.yml", false).option("--no-arm", "Skip arming enforcement chokepoints (graph-first hook + pre-commit gate)").action(
13803
15389
  (opts) => init({
13804
15390
  globalOnly: opts.globalOnly,
13805
15391
  localOnly: opts.localOnly,
13806
15392
  force: opts.force,
13807
15393
  provider: opts.provider,
13808
- hooks: opts.hooks
15394
+ hooks: opts.hooks,
15395
+ arm: opts.arm !== false
13809
15396
  })
13810
15397
  );
13811
15398
  program.command("update").description("Update provider assets for the current project (does not overwrite project guidance files)").option("--yes", "Apply changes (default is dry-run)", false).option("--provider <provider>", "Provider adapter to update: auto, claude, codex, or both", "auto").option("--postinstall", "Internal: invoked by npm postinstall; no-op if cdd has not been init-ed", false).action((opts) => update({ yes: opts.yes, provider: opts.provider, postinstall: opts.postinstall }));
@@ -13866,7 +15453,26 @@ function resolveWorkers(value) {
13866
15453
  return 1;
13867
15454
  return Math.min(n, 16);
13868
15455
  }
13869
- program.command("code-map [path]").description("Scan source files and emit a structural index at .cdd/code-map.yml").option("--out <path>", "Output YAML path (default .cdd/code-map.yml; with --surface, .cdd/code-map.<surface>.yml)").option("--surface <subpath>", "Scope the scan to a monorepo subtree and name the map after it").option("--workers [n]", "Parallelize JS/TS/Vue scanning across N child processes (default: CPU count - 1)").option("--include <glob>", "Additional include glob (repeatable)", collectRepeatable, []).option("--exclude <glob>", "Additional exclude glob (repeatable)", collectRepeatable, []).option("--check", "Exit 1 if regenerating would change the file (no write)", false).option("--max-lines <n>", "Warn for files exceeding this line count (default 100000)", "100000").action(async (path, opts) => {
15456
+ program.command("code-map [path]").description("Scan source files and emit a structural index at .cdd/code-map.yml").option("--out <path>", "Output YAML path (default .cdd/code-map.yml; with --surface, .cdd/code-map.<surface>.yml)").option("--surface <subpath>", "Scope the scan to a monorepo subtree and name the map after it").option("--workers [n]", "Parallelize JS/TS/Vue scanning across N child processes (default: CPU count - 1)").option("--include <glob>", "Additional include glob (repeatable)", collectRepeatable, []).option("--exclude <glob>", "Additional exclude glob (repeatable)", collectRepeatable, []).option("--check", "Exit 1 if regenerating would change the file (no write)", false).option("--watch", "Keep the map fresh in the background, rebuilding on file changes (debounced)", false).option("--debounce <ms>", "With --watch: coalesce change bursts within this window (default 500)", "500").option("--max-lines <n>", "Warn for files exceeding this line count (default 100000)", "100000").action(async (path, opts) => {
15457
+ if (opts.watch) {
15458
+ const { codeMapWatch: codeMapWatch2 } = await Promise.resolve().then(() => (init_code_map_watch(), code_map_watch_exports));
15459
+ const controller = new AbortController();
15460
+ const onSignal = () => controller.abort();
15461
+ process.once("SIGINT", onSignal);
15462
+ process.once("SIGTERM", onSignal);
15463
+ const exit2 = await codeMapWatch2({
15464
+ path: path ?? ".",
15465
+ out: opts.out,
15466
+ surface: opts.surface,
15467
+ workers: resolveWorkers(opts.workers),
15468
+ include: opts.include,
15469
+ exclude: opts.exclude,
15470
+ maxLines: parseInt(opts.maxLines, 10),
15471
+ debounceMs: parseInt(opts.debounce ?? "500", 10),
15472
+ signal: controller.signal
15473
+ });
15474
+ process.exit(exit2);
15475
+ }
13870
15476
  const { codeMap: codeMap2 } = await Promise.resolve().then(() => (init_code_map(), code_map_exports));
13871
15477
  const exit = await codeMap2({
13872
15478
  path: path ?? ".",
@@ -13886,13 +15492,15 @@ program.command("__code-map-scan", { hidden: true }).requiredOption("--lang <lan
13886
15492
  process.exit(exit);
13887
15493
  });
13888
15494
  var index = program.command("index").description("Query machine-readable project indexes before opening source files");
13889
- index.command("query <term>").description("Search .cdd/code-map.yml for files, symbols, imports, and line ranges").option("--map <path>", "Code-map YAML path", ".cdd/code-map.yml").option("--limit <n>", "Maximum result files to print", "10").option("--json", "Print machine-readable JSON", false).option("--no-refresh", "Do not auto-regenerate stale or missing code-map before querying").action(async (term, opts) => {
15495
+ index.command("query <term>").description("Search .cdd/code-map.yml for files, symbols, imports, and line ranges").option("--map <path>", "Code-map YAML path", ".cdd/code-map.yml").option("--limit <n>", "Maximum result files to print", "10").option("--json", "Print machine-readable JSON", false).option("--with-source", "Include the matched source slices inline so no separate Read is needed", false).option("--source-budget <n>", "Max total source lines to emit with --with-source", "400").option("--no-refresh", "Do not auto-regenerate stale or missing code-map before querying").action(async (term, opts) => {
13890
15496
  const { indexQuery: indexQuery2 } = await Promise.resolve().then(() => (init_index_query(), index_query_exports));
13891
15497
  const exit = await indexQuery2(term, {
13892
15498
  map: opts.map,
13893
15499
  limit: parseInt(opts.limit, 10),
13894
15500
  json: opts.json === true,
13895
- refresh: opts.refresh !== false
15501
+ refresh: opts.refresh !== false,
15502
+ withSource: opts.withSource === true,
15503
+ sourceBudget: parseInt(opts.sourceBudget ?? "400", 10)
13896
15504
  });
13897
15505
  process.exit(exit);
13898
15506
  });
@@ -13917,14 +15525,16 @@ graph.command("sync [path]").description("Run CodeGraph incremental sync (requir
13917
15525
  const exit = await graphSync2({ path, engine: opts.engine, json: opts.json === true });
13918
15526
  process.exit(exit);
13919
15527
  });
13920
- graph.command("query <term>").description("Search native graph symbols, optionally delegating to CodeGraph or code-map").option("--engine <engine>", "Graph engine: auto, native, codegraph, or codemap", "auto").option("--map <path>", "Code-map YAML path for fallback", ".cdd/code-map.yml").option("--limit <n>", "Maximum results to print", "10").option("--json", "Print machine-readable JSON", false).option("--no-refresh", "Do not auto-regenerate stale or missing fallback code-map").action(async (term, opts) => {
15528
+ graph.command("query <term>").description("Search native graph symbols, optionally delegating to CodeGraph or code-map").option("--engine <engine>", "Graph engine: auto, native, codegraph, or codemap", "auto").option("--map <path>", "Code-map YAML path for fallback", ".cdd/code-map.yml").option("--limit <n>", "Maximum results to print", "10").option("--json", "Print machine-readable JSON", false).option("--with-source", "Include matched source slices inline so no separate Read is needed (native/codemap engines)", false).option("--source-budget <n>", "Max total source lines to emit with --with-source", "400").option("--no-refresh", "Do not auto-regenerate stale or missing fallback code-map").action(async (term, opts) => {
13921
15529
  const { graphQuery: graphQuery2 } = await Promise.resolve().then(() => (init_graph(), graph_exports));
13922
15530
  const exit = await graphQuery2(term, {
13923
15531
  engine: opts.engine,
13924
15532
  map: opts.map,
13925
15533
  limit: parseInt(opts.limit, 10),
13926
15534
  json: opts.json === true,
13927
- refresh: opts.refresh !== false
15535
+ refresh: opts.refresh !== false,
15536
+ withSource: opts.withSource === true,
15537
+ sourceBudget: parseInt(opts.sourceBudget ?? "400", 10)
13928
15538
  });
13929
15539
  process.exit(exit);
13930
15540
  });
@@ -13951,6 +15561,11 @@ graph.command("context <task>").description("Build task context with native grap
13951
15561
  });
13952
15562
  process.exit(exit);
13953
15563
  });
15564
+ program.command("classify-check [change-id]").description("Show the mechanical risk-tier floor for a change before classification (advisory; gate enforces it)").option("--text <text>", "Scan this inline intent text instead of a change directory").option("--json", "Print machine-readable JSON", false).action(async (changeId, opts) => {
15565
+ const { classifyCheck: classifyCheck2 } = await Promise.resolve().then(() => (init_classify_check(), classify_check_exports));
15566
+ const exit = await classifyCheck2(changeId, { text: opts.text, json: opts.json });
15567
+ process.exit(exit);
15568
+ });
13954
15569
  program.command("mcp").description("Run the cdd-kit MCP stdio server exposing graph and code-map tools").action(async () => {
13955
15570
  const { runMcpServer: runMcpServer2 } = await Promise.resolve().then(() => (init_server(), server_exports));
13956
15571
  await runMcpServer2({ version: pkg.version });
@@ -13982,6 +15597,19 @@ program.command("list").description("List active changes in specs/changes/").act
13982
15597
  program.command("install-hooks").description("Install pre-commit hook that runs cdd-kit gate on staged changes").action(async () => {
13983
15598
  await installHooks();
13984
15599
  });
15600
+ program.command("install-agent-hooks").description("Install Claude Code agent hooks into .claude/settings.json (graph-first exploration)").option("--graph-first <mode>", "Install the graph-first PreToolUse hook: 'advisory' (default) or 'strict'", "advisory").action(async (opts) => {
15601
+ await installAgentHooks({ graphFirst: opts.graphFirst });
15602
+ });
15603
+ var openapi = program.command("openapi").description("Project the API contract into tooling artifacts (see docs/adr/0001-contract-to-openapi-export.md)");
15604
+ openapi.command("export").description("Export contracts/api/api-contract.md as a minimal OpenAPI 3.1 skeleton").option("--contract <path>", "API contract markdown path", "contracts/api/api-contract.md").option("--out <path>", "Write to a file instead of stdout").option("--yaml", "Emit YAML instead of JSON", false).option("--check", "Verify the artifact at --out is in sync with the contract (exits 1 on drift); does not write", false).action(async (opts) => {
15605
+ const exit = await openapiExport({
15606
+ contract: opts.contract,
15607
+ out: opts.out,
15608
+ format: opts.yaml ? "yaml" : "json",
15609
+ check: opts.check
15610
+ });
15611
+ process.exit(exit);
15612
+ });
13985
15613
  program.command("detect-stack").description("Detect the project tech stack and print the result").action(() => {
13986
15614
  const cwd = process.cwd();
13987
15615
  const result = detectStack(cwd);