contract-driven-delivery 2.1.3 → 2.2.1

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
@@ -119,26 +119,26 @@ var code_map_hook_exports = {};
119
119
  __export(code_map_hook_exports, {
120
120
  installCodeMapHook: () => installCodeMapHook
121
121
  });
122
- import { existsSync as existsSync3, readFileSync as readFileSync2, writeFileSync, chmodSync, mkdirSync as mkdirSync2 } from "fs";
123
- 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";
124
124
  async function installCodeMapHook(cwd) {
125
- const gitDir = join4(cwd, ".git");
126
- if (!existsSync3(gitDir)) {
125
+ const gitDir = join5(cwd, ".git");
126
+ if (!existsSync4(gitDir)) {
127
127
  log.warn("not a git repository (no .git/ found); skipping code-map hook installation");
128
128
  return;
129
129
  }
130
- const hooksDir = join4(gitDir, "hooks");
130
+ const hooksDir = join5(gitDir, "hooks");
131
131
  mkdirSync2(hooksDir, { recursive: true });
132
- const dest = join4(hooksDir, "pre-commit");
132
+ const dest = join5(hooksDir, "pre-commit");
133
133
  let final;
134
- if (!existsSync3(dest)) {
134
+ if (!existsSync4(dest)) {
135
135
  final = `#!/bin/sh
136
136
  set -e
137
137
 
138
138
  ${CODE_MAP_BLOCK}
139
139
  `;
140
140
  } else {
141
- const existing = readFileSync2(dest, "utf8");
141
+ const existing = readFileSync3(dest, "utf8");
142
142
  const startIdx = existing.indexOf(START_MARKER);
143
143
  const endIdx = existing.indexOf(END_MARKER);
144
144
  if (startIdx >= 0 && endIdx > startIdx) {
@@ -150,15 +150,15 @@ ${CODE_MAP_BLOCK}
150
150
  final = trimmed + "\n\n" + CODE_MAP_BLOCK + "\n";
151
151
  }
152
152
  }
153
- writeFileSync(dest, final, "utf8");
153
+ writeFileSync2(dest, final, "utf8");
154
154
  try {
155
155
  chmodSync(dest, 493);
156
156
  } catch {
157
157
  }
158
158
  try {
159
- const cddDir = join4(cwd, ".cdd");
159
+ const cddDir = join5(cwd, ".cdd");
160
160
  mkdirSync2(cddDir, { recursive: true });
161
- writeFileSync(join4(cddDir, ".hooks-installed"), `installed: ${(/* @__PURE__ */ new Date()).toISOString()}
161
+ writeFileSync2(join5(cddDir, ".hooks-installed"), `installed: ${(/* @__PURE__ */ new Date()).toISOString()}
162
162
  `, "utf8");
163
163
  } catch {
164
164
  }
@@ -192,27 +192,217 @@ ${END_MARKER}`;
192
192
  }
193
193
  });
194
194
 
195
- // src/utils/provider.ts
196
- 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";
197
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";
198
388
  function validateProviderOption(provider) {
199
389
  return provider === "auto" || provider === "claude" || provider === "codex" || provider === "both";
200
390
  }
201
391
  function inferProvider(cwd, requested = "auto") {
202
392
  if (requested !== "auto")
203
393
  return requested;
204
- const modelPolicyPath = join6(cwd, ".cdd", "model-policy.json");
205
- if (existsSync5(modelPolicyPath)) {
394
+ const modelPolicyPath = join9(cwd, ".cdd", "model-policy.json");
395
+ if (existsSync8(modelPolicyPath)) {
206
396
  try {
207
- const policy = JSON.parse(readFileSync4(modelPolicyPath, "utf8"));
397
+ const policy = JSON.parse(readFileSync7(modelPolicyPath, "utf8"));
208
398
  if (policy.provider === "claude" || policy.provider === "codex" || policy.provider === "both") {
209
399
  return policy.provider;
210
400
  }
211
401
  } catch {
212
402
  }
213
403
  }
214
- const hasClaude = existsSync5(join6(cwd, "CLAUDE.md")) || existsSync5(join6(cwd, "AGENTS.md"));
215
- 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"));
216
406
  if (hasClaude && hasCodex)
217
407
  return "both";
218
408
  if (hasCodex)
@@ -226,27 +416,27 @@ var init_provider = __esm({
226
416
  });
227
417
 
228
418
  // src/commands/update.ts
229
- import { join as join7 } from "path";
230
- 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";
231
421
  import { createHash } from "crypto";
232
422
  import { homedir as homedir2 } from "os";
233
423
  function fileHash(filePath) {
234
- const buf = readFileSync5(filePath);
424
+ const buf = readFileSync8(filePath);
235
425
  return createHash("sha256").update(buf).digest("hex");
236
426
  }
237
427
  function diffDir(src, dest) {
238
428
  const entries = [];
239
- if (!existsSync6(src))
429
+ if (!existsSync9(src))
240
430
  return entries;
241
431
  function walk(currentSrc, currentDest) {
242
432
  const items = readdirSync3(currentSrc, { withFileTypes: true });
243
433
  for (const item of items) {
244
- const srcPath = join7(currentSrc, item.name);
245
- const destPath = join7(currentDest, item.name);
434
+ const srcPath = join10(currentSrc, item.name);
435
+ const destPath = join10(currentDest, item.name);
246
436
  if (item.isDirectory()) {
247
437
  walk(srcPath, destPath);
248
438
  } else {
249
- if (!existsSync6(destPath)) {
439
+ if (!existsSync9(destPath)) {
250
440
  entries.push({ src: srcPath, dest: destPath, action: "add" });
251
441
  } else if (fileHash(srcPath) !== fileHash(destPath)) {
252
442
  entries.push({ src: srcPath, dest: destPath, action: "overwrite" });
@@ -264,33 +454,33 @@ function applyDir(entries) {
264
454
  for (const e of entries) {
265
455
  if (e.action === "skip")
266
456
  continue;
267
- mkdirSync3(join7(e.dest, ".."), { recursive: true });
268
- copyFileSync2(e.src, e.dest);
457
+ mkdirSync5(join10(e.dest, ".."), { recursive: true });
458
+ copyFileSync3(e.src, e.dest);
269
459
  count += 1;
270
460
  }
271
461
  return count;
272
462
  }
273
463
  function backupDir(dir, backupDest) {
274
- if (!existsSync6(dir))
464
+ if (!existsSync9(dir))
275
465
  return;
276
- mkdirSync3(backupDest, { recursive: true });
466
+ mkdirSync5(backupDest, { recursive: true });
277
467
  function walk(src, dst) {
278
468
  const items = readdirSync3(src, { withFileTypes: true });
279
469
  for (const item of items) {
280
- const s = join7(src, item.name);
281
- const d = join7(dst, item.name);
470
+ const s = join10(src, item.name);
471
+ const d = join10(dst, item.name);
282
472
  if (item.isDirectory()) {
283
- mkdirSync3(d, { recursive: true });
473
+ mkdirSync5(d, { recursive: true });
284
474
  walk(s, d);
285
475
  } else
286
- copyFileSync2(s, d);
476
+ copyFileSync3(s, d);
287
477
  }
288
478
  }
289
479
  walk(dir, backupDest);
290
480
  }
291
481
  async function update(opts) {
292
482
  if (opts.postinstall) {
293
- if (!existsSync6(join7(SKILLS_HOME, "contract-driven-delivery"))) {
483
+ if (!existsSync9(join10(SKILLS_HOME, "contract-driven-delivery"))) {
294
484
  return;
295
485
  }
296
486
  opts.yes = true;
@@ -308,7 +498,7 @@ async function update(opts) {
308
498
  const provider = inferProvider(cwd, requestedProvider);
309
499
  const updateClaudeAssets = provider === "claude" || provider === "both";
310
500
  const agentDiff = updateClaudeAssets ? diffDir(ASSET.agents, AGENTS_HOME) : [];
311
- 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))) : [];
312
502
  const toWrite = [...agentDiff, ...skillDiff].filter((e) => e.action !== "skip");
313
503
  const toAdd = toWrite.filter((e) => e.action === "add");
314
504
  const toOver = toWrite.filter((e) => e.action === "overwrite");
@@ -346,13 +536,13 @@ async function update(opts) {
346
536
  return;
347
537
  }
348
538
  const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
349
- const backupRoot = join7(homedir2(), ".claude", ".cdd-kit-backup", timestamp);
539
+ const backupRoot = join10(homedir2(), ".claude", ".cdd-kit-backup", timestamp);
350
540
  if (!quiet) {
351
541
  log.blank();
352
542
  log.info(`Backing up to ${backupRoot} \u2026`);
353
543
  }
354
- backupDir(AGENTS_HOME, join7(backupRoot, "agents"));
355
- backupDir(SKILLS_HOME, join7(backupRoot, "skills"));
544
+ backupDir(AGENTS_HOME, join10(backupRoot, "agents"));
545
+ backupDir(SKILLS_HOME, join10(backupRoot, "skills"));
356
546
  if (!quiet)
357
547
  log.ok(`Backup complete: ${backupRoot}`);
358
548
  if (!quiet)
@@ -393,7 +583,7 @@ var init_update = __esm({
393
583
  });
394
584
 
395
585
  // src/utils/digest.ts
396
- import { readFileSync as readFileSync6 } from "fs";
586
+ import { readFileSync as readFileSync9 } from "fs";
397
587
  import { createHash as createHash2 } from "crypto";
398
588
  function normalizeContentForHash(buf) {
399
589
  if (!buf.includes(13))
@@ -404,7 +594,7 @@ function normalizeContentForHash(buf) {
404
594
  function sha256OfFileNormalized(path) {
405
595
  let buf;
406
596
  try {
407
- buf = readFileSync6(path);
597
+ buf = readFileSync9(path);
408
598
  } catch {
409
599
  return "";
410
600
  }
@@ -421,9 +611,9 @@ var context_scan_exports = {};
421
611
  __export(context_scan_exports, {
422
612
  contextScan: () => contextScan
423
613
  });
424
- 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";
425
615
  import { createHash as createHash3 } from "crypto";
426
- 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";
427
617
  function inputsDigest(paths, cwd) {
428
618
  const combined = paths.slice().sort().map((p) => {
429
619
  const rel = relative2(cwd, p).replace(/\\/g, "/");
@@ -436,10 +626,10 @@ function stripGlobSuffix(pattern) {
436
626
  }
437
627
  function getForbiddenPaths(cwd) {
438
628
  const forbidden = new Set(DEFAULT_FORBIDDEN);
439
- const policyPath = join8(cwd, ".cdd", "context-policy.json");
629
+ const policyPath = join11(cwd, ".cdd", "context-policy.json");
440
630
  try {
441
- if (existsSync7(policyPath)) {
442
- const policy = JSON.parse(readFileSync7(policyPath, "utf8"));
631
+ if (existsSync10(policyPath)) {
632
+ const policy = JSON.parse(readFileSync10(policyPath, "utf8"));
443
633
  for (const pattern of policy.forbiddenPaths ?? []) {
444
634
  forbidden.add(stripGlobSuffix(pattern));
445
635
  }
@@ -468,7 +658,7 @@ function buildTree(dir, cwd, forbidden, stats, prefix = "", depth = 0) {
468
658
  });
469
659
  let output = "";
470
660
  const visible = entries.filter((entry) => {
471
- const relPath = relative2(cwd, join8(dir, entry.name));
661
+ const relPath = relative2(cwd, join11(dir, entry.name));
472
662
  return !isForbidden(relPath, forbidden);
473
663
  });
474
664
  const truncated = visible.length > PER_DIR_ENTRY_CAP;
@@ -476,7 +666,7 @@ function buildTree(dir, cwd, forbidden, stats, prefix = "", depth = 0) {
476
666
  if (truncated)
477
667
  stats.truncatedDirs += 1;
478
668
  shown.forEach((entry, index2) => {
479
- const fullPath = join8(dir, entry.name);
669
+ const fullPath = join11(dir, entry.name);
480
670
  const isLast = index2 === shown.length - 1 && !truncated;
481
671
  const connector = isLast ? "\\-- " : "|-- ";
482
672
  output += `${prefix}${connector}${entry.name}${entry.isDirectory() ? "/" : ""}
@@ -538,10 +728,10 @@ function parseContractMetadata(content) {
538
728
  return { title: firstHeading(content), summary, metadata };
539
729
  }
540
730
  function findContractFiles(dir, found = []) {
541
- if (!existsSync7(dir))
731
+ if (!existsSync10(dir))
542
732
  return found;
543
733
  for (const entry of readdirSync4(dir, { withFileTypes: true })) {
544
- const fullPath = join8(dir, entry.name);
734
+ const fullPath = join11(dir, entry.name);
545
735
  if (entry.isDirectory())
546
736
  findContractFiles(fullPath, found);
547
737
  else if (entry.isFile() && entry.name.endsWith(".md") && entry.name !== "INDEX.md" && entry.name !== "CHANGELOG.md")
@@ -551,14 +741,14 @@ function findContractFiles(dir, found = []) {
551
741
  }
552
742
  async function contextScan(opts = {}) {
553
743
  const cwd = process.cwd();
554
- const specsContextDir = join8(cwd, "specs", "context");
555
- mkdirSync4(specsContextDir, { recursive: true });
744
+ const specsContextDir = join11(cwd, "specs", "context");
745
+ mkdirSync6(specsContextDir, { recursive: true });
556
746
  const forbidden = getForbiddenPaths(cwd);
557
747
  const surface = opts.surface;
558
748
  let scanRoot = cwd;
559
749
  if (surface) {
560
- const resolvedSurface = join8(cwd, surface);
561
- if (!existsSync7(resolvedSurface)) {
750
+ const resolvedSurface = join11(cwd, surface);
751
+ if (!existsSync10(resolvedSurface)) {
562
752
  log.error(`--surface path not found: ${surface}`);
563
753
  process.exit(1);
564
754
  }
@@ -570,10 +760,10 @@ async function contextScan(opts = {}) {
570
760
  }
571
761
  const treeStats = { dirs: 0, files: 0, omittedDirs: 0, truncatedDirs: 0 };
572
762
  const tree = buildTree(scanRoot, cwd, forbidden, treeStats);
573
- const policyPath = join8(cwd, ".cdd", "context-policy.json");
574
- const projectMapInputs = [policyPath].filter(existsSync7);
575
- writeFileSync3(
576
- 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"),
577
767
  [
578
768
  "---",
579
769
  "artifact: project-map",
@@ -606,14 +796,14 @@ async function contextScan(opts = {}) {
606
796
  "utf8"
607
797
  );
608
798
  log.ok("Created specs/context/project-map.md");
609
- 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)));
610
800
  const contractEntries = [];
611
801
  const inventoryRows = [];
612
802
  let missingSummary = 0;
613
803
  for (const file of contractFiles) {
614
804
  const relPath = relative2(cwd, file).replace(/\\/g, "/");
615
805
  const dir = dirname3(relPath).replace(/\\/g, "/");
616
- const { title, summary, metadata } = parseContractMetadata(readFileSync7(file, "utf8"));
806
+ const { title, summary, metadata } = parseContractMetadata(readFileSync10(file, "utf8"));
617
807
  const contractType = deriveContractType(relPath, metadata);
618
808
  const owner = metadata.owner ?? "unknown";
619
809
  const surface2 = metadata.surface ?? dir;
@@ -668,7 +858,7 @@ async function contextScan(opts = {}) {
668
858
  "",
669
859
  ...contractEntries
670
860
  ].join("\n");
671
- writeFileSync3(join8(specsContextDir, "contracts-index.md"), contractIndex, "utf8");
861
+ writeFileSync6(join11(specsContextDir, "contracts-index.md"), contractIndex, "utf8");
672
862
  if (missingSummary > 0) {
673
863
  log.warn(`Created specs/context/contracts-index.md with ${missingSummary} missing summary warning(s).`);
674
864
  } else {
@@ -3695,7 +3885,7 @@ var require_compile = __commonJS({
3695
3885
  const schOrFunc = root.refs[ref];
3696
3886
  if (schOrFunc)
3697
3887
  return schOrFunc;
3698
- let _sch = resolve2.call(this, root, ref);
3888
+ let _sch = resolve6.call(this, root, ref);
3699
3889
  if (_sch === void 0) {
3700
3890
  const schema = (_a = root.localRefs) === null || _a === void 0 ? void 0 : _a[ref];
3701
3891
  const { schemaId } = this.opts;
@@ -3722,7 +3912,7 @@ var require_compile = __commonJS({
3722
3912
  function sameSchemaEnv(s1, s2) {
3723
3913
  return s1.schema === s2.schema && s1.root === s2.root && s1.baseId === s2.baseId;
3724
3914
  }
3725
- function resolve2(root, ref) {
3915
+ function resolve6(root, ref) {
3726
3916
  let sch;
3727
3917
  while (typeof (sch = this.refs[ref]) == "string")
3728
3918
  ref = sch;
@@ -4298,7 +4488,7 @@ var require_fast_uri = __commonJS({
4298
4488
  }
4299
4489
  return uri;
4300
4490
  }
4301
- function resolve2(baseURI, relativeURI, options) {
4491
+ function resolve6(baseURI, relativeURI, options) {
4302
4492
  const schemelessOptions = options ? Object.assign({ scheme: "null" }, options) : { scheme: "null" };
4303
4493
  const resolved = resolveComponent(parse3(baseURI, schemelessOptions), parse3(relativeURI, schemelessOptions), schemelessOptions, true);
4304
4494
  schemelessOptions.skipEscape = true;
@@ -4526,7 +4716,7 @@ var require_fast_uri = __commonJS({
4526
4716
  var fastUri = {
4527
4717
  SCHEMES,
4528
4718
  normalize,
4529
- resolve: resolve2,
4719
+ resolve: resolve6,
4530
4720
  resolveComponent,
4531
4721
  equal,
4532
4722
  serialize,
@@ -7515,18 +7705,265 @@ var require_dist = __commonJS({
7515
7705
  }
7516
7706
  });
7517
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
+
7518
7955
  // src/utils/gitignore.ts
7519
- import { existsSync as existsSync12, readFileSync as readFileSync11, writeFileSync as writeFileSync6 } from "fs";
7520
- 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";
7521
7958
  function ensureGitignoreEntry(cwd, entry, comment = "cdd-kit generated backups (do not commit)") {
7522
- const path = join13(cwd, ".gitignore");
7959
+ const path = join16(cwd, ".gitignore");
7523
7960
  const trimmed = entry.trim();
7524
7961
  if (!trimmed)
7525
7962
  return false;
7526
7963
  const re = new RegExp(`^\\s*${trimmed.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\s*$`, "m");
7527
7964
  let existing = "";
7528
- if (existsSync12(path))
7529
- existing = readFileSync11(path, "utf8");
7965
+ if (existsSync16(path))
7966
+ existing = readFileSync15(path, "utf8");
7530
7967
  if (re.test(existing))
7531
7968
  return false;
7532
7969
  const sep = existing.length > 0 && !existing.endsWith("\n") ? "\n" : "";
@@ -7536,7 +7973,7 @@ ${trimmed}
7536
7973
  # ${comment}
7537
7974
  ${trimmed}
7538
7975
  `;
7539
- writeFileSync6(path, existing + block, "utf8");
7976
+ writeFileSync9(path, existing + block, "utf8");
7540
7977
  return true;
7541
7978
  }
7542
7979
  var init_gitignore = __esm({
@@ -7550,15 +7987,15 @@ var migrate_exports = {};
7550
7987
  __export(migrate_exports, {
7551
7988
  migrate: () => migrate
7552
7989
  });
7553
- import { join as join14 } from "path";
7554
- 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";
7555
7992
  import yaml2 from "js-yaml";
7556
7993
  function backupChangeDir(cwd, changeId, sessionStamp) {
7557
- const backupRoot = join14(cwd, ".cdd", "migrate-backup", sessionStamp);
7558
- const backupDir2 = join14(backupRoot, changeId);
7559
- mkdirSync6(backupRoot, { recursive: true });
7560
- const sourceDir = join14(cwd, "specs", "changes", changeId);
7561
- 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)) {
7562
7999
  cpSync2(sourceDir, backupDir2, { recursive: true });
7563
8000
  }
7564
8001
  return backupDir2;
@@ -7691,16 +8128,16 @@ function parseLegacyTaskList(body) {
7691
8128
  return rows;
7692
8129
  }
7693
8130
  function migrateTasksFile(changeId, changeDir, enableContextGovernance, detectedTier, changed, warnings, pendingWrites, pendingDeletes) {
7694
- const newPath = join14(changeDir, "tasks.yml");
7695
- const legacyPath = join14(changeDir, "tasks.md");
7696
- if (existsSync13(newPath)) {
8131
+ const newPath = join17(changeDir, "tasks.yml");
8132
+ const legacyPath = join17(changeDir, "tasks.md");
8133
+ if (existsSync17(newPath)) {
7697
8134
  return;
7698
8135
  }
7699
- if (!existsSync13(legacyPath)) {
8136
+ if (!existsSync17(legacyPath)) {
7700
8137
  warnings.push("tasks.md not found and tasks.yml missing \u2014 skipping tasks migration");
7701
8138
  return;
7702
8139
  }
7703
- const raw = readFileSync12(legacyPath, "utf8");
8140
+ const raw = readFileSync16(legacyPath, "utf8");
7704
8141
  const fm = parseLegacyFrontmatter(raw);
7705
8142
  const bodyMatch = raw.match(/^---\r?\n[\s\S]*?\r?\n---\r?\n?([\s\S]*)$/);
7706
8143
  const body = bodyMatch ? bodyMatch[1] : raw;
@@ -7784,17 +8221,17 @@ function parseLegacyAgentLog(content) {
7784
8221
  return data;
7785
8222
  }
7786
8223
  function migrateAgentLogs(changeDir, changed, pendingWrites, pendingDeletes) {
7787
- const agentLogDir = join14(changeDir, "agent-log");
7788
- if (!existsSync13(agentLogDir))
8224
+ const agentLogDir = join17(changeDir, "agent-log");
8225
+ if (!existsSync17(agentLogDir))
7789
8226
  return;
7790
8227
  const mdLogs = readdirSync7(agentLogDir).filter((f) => f.endsWith(".md"));
7791
8228
  for (const f of mdLogs) {
7792
- const fullPath = join14(agentLogDir, f);
8229
+ const fullPath = join17(agentLogDir, f);
7793
8230
  const yamlName = f.replace(/\.md$/, ".yml");
7794
- const yamlFull = join14(agentLogDir, yamlName);
7795
- if (existsSync13(yamlFull))
8231
+ const yamlFull = join17(agentLogDir, yamlName);
8232
+ if (existsSync17(yamlFull))
7796
8233
  continue;
7797
- const raw = readFileSync12(fullPath, "utf8");
8234
+ const raw = readFileSync16(fullPath, "utf8");
7798
8235
  const parsed = parseLegacyAgentLog(raw);
7799
8236
  const yamlOut = yaml2.dump(parsed, { lineWidth: -1, noRefs: true });
7800
8237
  pendingWrites.push({ path: yamlFull, content: yamlOut });
@@ -7803,15 +8240,15 @@ function migrateAgentLogs(changeDir, changed, pendingWrites, pendingDeletes) {
7803
8240
  }
7804
8241
  }
7805
8242
  function ensureImplementationPlanScaffold(changeId, changeDir, changed, warnings, pendingWrites) {
7806
- const planPath = join14(changeDir, "implementation-plan.md");
7807
- if (existsSync13(planPath))
8243
+ const planPath = join17(changeDir, "implementation-plan.md");
8244
+ if (existsSync17(planPath))
7808
8245
  return;
7809
- const templatePath = join14(ASSET.specsTemplates, "implementation-plan.md");
7810
- if (!existsSync13(templatePath)) {
8246
+ const templatePath = join17(ASSET.specsTemplates, "implementation-plan.md");
8247
+ if (!existsSync17(templatePath)) {
7811
8248
  warnings.push("implementation-plan.md template not found; run cdd-kit upgrade --yes after updating cdd-kit");
7812
8249
  return;
7813
8250
  }
7814
- 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);
7815
8252
  pendingWrites.push({ path: planPath, content: template });
7816
8253
  changed.push("implementation-plan.md: added scaffold");
7817
8254
  warnings.push("implementation-plan.md scaffold added; fill it before implementation agents continue");
@@ -7822,9 +8259,9 @@ function migrateOne(changeId, changeDir, enableContextGovernance) {
7822
8259
  const pending = [];
7823
8260
  const deletes = [];
7824
8261
  let detectedTier = null;
7825
- const classifPath = join14(changeDir, "change-classification.md");
7826
- if (existsSync13(classifPath)) {
7827
- const content = readFileSync12(classifPath, "utf8");
8262
+ const classifPath = join17(changeDir, "change-classification.md");
8263
+ if (existsSync17(classifPath)) {
8264
+ const content = readFileSync16(classifPath, "utf8");
7828
8265
  const hasNewTierFormat = /^## Tier\s*\n\s*-\s*\d\s*$/m.test(content);
7829
8266
  const oldMatch = content.match(/\*\*Tier[:\*]+\s*(?:Tier\s*)?(\d)/i) ?? content.match(/^-?\s*Tier:\s*(?:Tier\s*)?(\d)/mi);
7830
8267
  if (oldMatch)
@@ -7855,8 +8292,8 @@ function migrateOne(changeId, changeDir, enableContextGovernance) {
7855
8292
  migrateTasksFile(changeId, changeDir, enableContextGovernance, detectedTier, changed, warnings, pending, deletes);
7856
8293
  ensureImplementationPlanScaffold(changeId, changeDir, changed, warnings, pending);
7857
8294
  migrateAgentLogs(changeDir, changed, pending, deletes);
7858
- const manifestPath = join14(changeDir, "context-manifest.md");
7859
- if (!existsSync13(manifestPath)) {
8295
+ const manifestPath = join17(changeDir, "context-manifest.md");
8296
+ if (!existsSync17(manifestPath)) {
7860
8297
  changed.push(enableContextGovernance ? "context-manifest.md: added context-governance v1 manifest scaffold" : "context-manifest.md: added legacy context manifest scaffold");
7861
8298
  pending.push({
7862
8299
  path: manifestPath,
@@ -7872,7 +8309,7 @@ function commitWritesAtomically(pending, deletes) {
7872
8309
  try {
7873
8310
  for (const write of pending) {
7874
8311
  const tmp = `${write.path}.cdd-migrate.tmp`;
7875
- writeFileSync7(tmp, write.content, "utf8");
8312
+ writeFileSync10(tmp, write.content, "utf8");
7876
8313
  renames.push({ tmp, final: write.path });
7877
8314
  }
7878
8315
  } catch (err) {
@@ -7901,8 +8338,8 @@ async function migrate(changeId, opts = {}) {
7901
8338
  const noBackup = opts.noBackup ?? false;
7902
8339
  const idsToMigrate = [];
7903
8340
  if (opts.all) {
7904
- const changesDir = join14(cwd, "specs", "changes");
7905
- if (!existsSync13(changesDir)) {
8341
+ const changesDir = join17(cwd, "specs", "changes");
8342
+ if (!existsSync17(changesDir)) {
7906
8343
  log.info("No specs/changes/ directory found \u2014 nothing to migrate.");
7907
8344
  return;
7908
8345
  }
@@ -7910,8 +8347,8 @@ async function migrate(changeId, opts = {}) {
7910
8347
  ...readdirSync7(changesDir, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name)
7911
8348
  );
7912
8349
  } else if (changeId) {
7913
- const specificDir = join14(cwd, "specs", "changes", changeId);
7914
- if (!existsSync13(specificDir)) {
8350
+ const specificDir = join17(cwd, "specs", "changes", changeId);
8351
+ if (!existsSync17(specificDir)) {
7915
8352
  log.error(`Change not found: specs/changes/${changeId}`);
7916
8353
  process.exit(1);
7917
8354
  }
@@ -7931,10 +8368,10 @@ async function migrate(changeId, opts = {}) {
7931
8368
  const sessionStamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
7932
8369
  let migratedCount = 0;
7933
8370
  let upToDateCount = 0;
7934
- const backupRoot = join14(cwd, ".cdd", "migrate-backup", sessionStamp);
8371
+ const backupRoot = join17(cwd, ".cdd", "migrate-backup", sessionStamp);
7935
8372
  for (const id of idsToMigrate) {
7936
- const changeDir = join14(cwd, "specs", "changes", id);
7937
- if (!existsSync13(changeDir)) {
8373
+ const changeDir = join17(cwd, "specs", "changes", id);
8374
+ if (!existsSync17(changeDir)) {
7938
8375
  log.warn(` ${id}: directory not found \u2014 skipping`);
7939
8376
  continue;
7940
8377
  }
@@ -7996,40 +8433,40 @@ var upgrade_exports = {};
7996
8433
  __export(upgrade_exports, {
7997
8434
  upgrade: () => upgrade
7998
8435
  });
7999
- import { existsSync as existsSync14, mkdirSync as mkdirSync7, readdirSync as readdirSync8, copyFileSync as copyFileSync3, readFileSync as readFileSync13, writeFileSync as writeFileSync8 } from "fs";
8000
- 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";
8001
8438
  function planMissingFiles(srcDir, destDir, label, planned) {
8002
- if (!existsSync14(srcDir))
8439
+ if (!existsSync18(srcDir))
8003
8440
  return;
8004
8441
  for (const entry of readdirSync8(srcDir, { withFileTypes: true })) {
8005
- const src = join15(srcDir, entry.name);
8006
- const dest = join15(destDir, entry.name);
8442
+ const src = join18(srcDir, entry.name);
8443
+ const dest = join18(destDir, entry.name);
8007
8444
  if (entry.isDirectory()) {
8008
- planMissingFiles(src, dest, join15(label, entry.name), planned);
8445
+ planMissingFiles(src, dest, join18(label, entry.name), planned);
8009
8446
  continue;
8010
8447
  }
8011
- if (!existsSync14(dest)) {
8012
- 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)) });
8013
8450
  }
8014
8451
  }
8015
8452
  }
8016
8453
  function planProviderGuidance(cwd, provider, planned) {
8017
8454
  if (provider === "claude" || provider === "both") {
8018
- if (!existsSync14(join15(cwd, "CLAUDE.md"))) {
8019
- 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" });
8020
8457
  }
8021
- if (!existsSync14(join15(cwd, "AGENTS.md"))) {
8022
- 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" });
8023
8460
  }
8024
8461
  }
8025
- if ((provider === "codex" || provider === "both") && !existsSync14(join15(cwd, "CODEX.md"))) {
8026
- 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" });
8027
8464
  }
8028
8465
  }
8029
8466
  function applyCopy(plan) {
8030
8467
  for (const item of plan) {
8031
- mkdirSync7(dirname4(item.dest), { recursive: true });
8032
- copyFileSync3(item.src, item.dest);
8468
+ mkdirSync9(dirname5(item.dest), { recursive: true });
8469
+ copyFileSync4(item.src, item.dest);
8033
8470
  }
8034
8471
  }
8035
8472
  async function upgrade(opts = {}) {
@@ -8041,12 +8478,12 @@ async function upgrade(opts = {}) {
8041
8478
  }
8042
8479
  const provider = inferProvider(cwd, requestedProvider);
8043
8480
  const plan = [];
8044
- planMissingFiles(ASSET.contracts, join15(cwd, "contracts"), "contracts", plan);
8045
- planMissingFiles(ASSET.specsTemplates, join15(cwd, "specs", "templates"), "specs/templates", plan);
8046
- planMissingFiles(ASSET.testsTemplates, join15(cwd, "tests", "templates"), "tests/templates", plan);
8047
- planMissingFiles(ASSET.ci, join15(cwd, "ci"), "ci", plan);
8048
- planMissingFiles(ASSET.githubWorkflows, join15(cwd, ".github", "workflows"), ".github/workflows", plan);
8049
- 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);
8050
8487
  planProviderGuidance(cwd, provider, plan);
8051
8488
  log.blank();
8052
8489
  log.info(`Upgrade provider: ${provider}`);
@@ -8083,11 +8520,11 @@ async function upgrade(opts = {}) {
8083
8520
  return;
8084
8521
  }
8085
8522
  applyCopy(plan);
8086
- const modelPolicyPath = join15(cwd, ".cdd", "model-policy.json");
8087
- if (existsSync14(modelPolicyPath)) {
8523
+ const modelPolicyPath = join18(cwd, ".cdd", "model-policy.json");
8524
+ if (existsSync18(modelPolicyPath)) {
8088
8525
  let existing = {};
8089
8526
  try {
8090
- existing = JSON.parse(readFileSync13(modelPolicyPath, "utf8"));
8527
+ existing = JSON.parse(readFileSync17(modelPolicyPath, "utf8"));
8091
8528
  } catch {
8092
8529
  }
8093
8530
  const merged = {
@@ -8096,7 +8533,7 @@ async function upgrade(opts = {}) {
8096
8533
  generated_at: (/* @__PURE__ */ new Date()).toISOString(),
8097
8534
  roles: existing.roles && typeof existing.roles === "object" ? existing.roles : {}
8098
8535
  };
8099
- writeFileSync8(modelPolicyPath, JSON.stringify(merged, null, 2) + "\n", "utf8");
8536
+ writeFileSync11(modelPolicyPath, JSON.stringify(merged, null, 2) + "\n", "utf8");
8100
8537
  }
8101
8538
  log.blank();
8102
8539
  log.ok(`Upgrade complete: ${plan.length} missing file(s) added.`);
@@ -8234,7 +8671,7 @@ function renderYaml(entries, opts) {
8234
8671
  }
8235
8672
  }
8236
8673
  }
8237
- const headerLineCount = opts.sourcesDigest ? 3 : 2;
8674
+ const headerLineCount = 2 + (opts.sourcesDigest ? 1 : 0) + (opts.surfaceRoot ? 1 : 0);
8238
8675
  const mapLines = bodyLines.length + headerLineCount;
8239
8676
  const compression = totalSrc === 0 ? 0 : totalSrc / mapLines;
8240
8677
  const fileCount = entries.length;
@@ -8245,6 +8682,9 @@ function renderYaml(entries, opts) {
8245
8682
  if (opts.sourcesDigest) {
8246
8683
  header.push(`# sources-digest: ${opts.sourcesDigest}`);
8247
8684
  }
8685
+ if (opts.surfaceRoot) {
8686
+ header.push(`# surface-root: ${opts.surfaceRoot}`);
8687
+ }
8248
8688
  return [...header, ...bodyLines].join("\n") + "\n";
8249
8689
  }
8250
8690
  var YAML_RESERVED;
@@ -8256,8 +8696,8 @@ var init_yaml_writer = __esm({
8256
8696
  });
8257
8697
 
8258
8698
  // src/code-map/config.ts
8259
- import { existsSync as existsSync15, readFileSync as readFileSync14 } from "fs";
8260
- import { join as join16 } from "path";
8699
+ import { existsSync as existsSync19, readFileSync as readFileSync18 } from "fs";
8700
+ import { join as join19 } from "path";
8261
8701
  import { load as yamlLoad } from "js-yaml";
8262
8702
  function asStringArray(value, key, where) {
8263
8703
  if (value === void 0)
@@ -8273,8 +8713,8 @@ function asStringArray(value, key, where) {
8273
8713
  return value;
8274
8714
  }
8275
8715
  function loadCodeMapConfig(cwd) {
8276
- const filePath = join16(cwd, CONFIG_REL_PATH);
8277
- if (!existsSync15(filePath)) {
8716
+ const filePath = join19(cwd, CONFIG_REL_PATH);
8717
+ if (!existsSync19(filePath)) {
8278
8718
  return {
8279
8719
  include: [...BUILTIN_INCLUDE],
8280
8720
  exclude: [...BUILTIN_EXCLUDE],
@@ -8283,7 +8723,7 @@ function loadCodeMapConfig(cwd) {
8283
8723
  }
8284
8724
  let text;
8285
8725
  try {
8286
- text = readFileSync14(filePath, "utf8");
8726
+ text = readFileSync18(filePath, "utf8");
8287
8727
  } catch (err) {
8288
8728
  throw new Error(`failed to read ${CONFIG_REL_PATH}: ${err.message}`);
8289
8729
  }
@@ -8353,8 +8793,8 @@ var init_config = __esm({
8353
8793
  });
8354
8794
 
8355
8795
  // src/code-map/include-exclude.ts
8356
- import { readdirSync as readdirSync9, statSync as statSync2 } from "fs";
8357
- import { join as join17 } from "path";
8796
+ import { readdirSync as readdirSync9, statSync as statSync3 } from "fs";
8797
+ import { join as join20 } from "path";
8358
8798
  import picomatch from "picomatch";
8359
8799
  function walkRepo(root, opts = {}) {
8360
8800
  const includes = opts.include ?? [];
@@ -8373,13 +8813,13 @@ function walkRepo(root, opts = {}) {
8373
8813
  }
8374
8814
  for (const entry of entries) {
8375
8815
  const relPath = relDir ? `${relDir}/${entry.name}` : entry.name;
8376
- const absPath = join17(dir, entry.name);
8816
+ const absPath = join20(dir, entry.name);
8377
8817
  if (entry.isDirectory() || entry.isSymbolicLink()) {
8378
8818
  const dirPattern = `${relPath}/**`;
8379
8819
  if (isExcluded(relPath) || isExcluded(dirPattern))
8380
8820
  continue;
8381
8821
  try {
8382
- const stat = statSync2(absPath);
8822
+ const stat = statSync3(absPath);
8383
8823
  if (stat.isDirectory()) {
8384
8824
  walk(absPath, relPath);
8385
8825
  }
@@ -8445,10 +8885,10 @@ var init_orchestrator = __esm({
8445
8885
  });
8446
8886
 
8447
8887
  // src/code-map/freshness.ts
8448
- import { existsSync as existsSync16, readFileSync as readFileSync15, statSync as statSync3 } from "fs";
8449
- 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";
8450
8890
  function checkCodeMapFreshness(cwd, mapRel = ".cdd/code-map.yml", include, exclude) {
8451
- const mapPath = join18(cwd, mapRel);
8891
+ const mapPath = join21(cwd, mapRel);
8452
8892
  let cfg;
8453
8893
  try {
8454
8894
  cfg = loadCodeMapConfig(cwd);
@@ -8464,17 +8904,17 @@ function checkCodeMapFreshness(cwd, mapRel = ".cdd/code-map.yml", include, exclu
8464
8904
  const includeFinal = [...cfg.include, ...include ?? []];
8465
8905
  const excludeFinal = [...cfg.exclude, ...exclude ?? []];
8466
8906
  const sourceFiles = walkRepo(cwd, { include: includeFinal, exclude: excludeFinal });
8467
- if (!existsSync16(mapPath)) {
8907
+ if (!existsSync20(mapPath)) {
8468
8908
  if (sourceFiles.length === 0) {
8469
8909
  return { status: "missing-greenfield", staleFiles: [], staleCount: 0, mapPath };
8470
8910
  }
8471
8911
  return { status: "missing-with-sources", staleFiles: [], staleCount: 0, mapPath };
8472
8912
  }
8473
- const mapMtime = statSync3(mapPath).mtimeMs;
8913
+ const mapMtime = statSync4(mapPath).mtimeMs;
8474
8914
  const staleAll = [];
8475
8915
  for (const absPath of sourceFiles) {
8476
8916
  try {
8477
- const mtime = statSync3(absPath).mtimeMs;
8917
+ const mtime = statSync4(absPath).mtimeMs;
8478
8918
  if (mtime > mapMtime) {
8479
8919
  const rel = absPath.replace(/\\/g, "/").replace(cwd.replace(/\\/g, "/") + "/", "");
8480
8920
  staleAll.push(rel);
@@ -8501,7 +8941,7 @@ function checkCodeMapFreshness(cwd, mapRel = ".cdd/code-map.yml", include, exclu
8501
8941
  }
8502
8942
  function readSourcesDigest(mapPath) {
8503
8943
  try {
8504
- const head = readFileSync15(mapPath, "utf8").slice(0, 2048);
8944
+ const head = readFileSync19(mapPath, "utf8").slice(0, 2048);
8505
8945
  const m = head.match(/^# sources-digest:\s*([a-f0-9]+)/m);
8506
8946
  return m ? m[1] : null;
8507
8947
  } catch {
@@ -8518,7 +8958,7 @@ var init_freshness = __esm({
8518
8958
  });
8519
8959
 
8520
8960
  // src/code-map/index-reader.ts
8521
- import { existsSync as existsSync17, readFileSync as readFileSync16 } from "fs";
8961
+ import { existsSync as existsSync21, readFileSync as readFileSync20 } from "fs";
8522
8962
  import yaml3 from "js-yaml";
8523
8963
  async function ensureCodeMapFresh(mapPath, refresh2) {
8524
8964
  if (!refresh2)
@@ -8556,13 +8996,13 @@ function sidecarPathFor(mapPath) {
8556
8996
  }
8557
8997
  function tryLoadSidecar(mapPath, mapText) {
8558
8998
  const sidecarPath = sidecarPathFor(mapPath);
8559
- if (!existsSync17(sidecarPath))
8999
+ if (!existsSync21(sidecarPath))
8560
9000
  return null;
8561
9001
  const headerDigest = mapText.match(/^# sources-digest:\s*([a-f0-9]+)/m)?.[1];
8562
9002
  if (!headerDigest)
8563
9003
  return null;
8564
9004
  try {
8565
- const raw = JSON.parse(readFileSync16(sidecarPath, "utf8"));
9005
+ const raw = JSON.parse(readFileSync20(sidecarPath, "utf8"));
8566
9006
  if (raw && raw.sourcesDigest === headerDigest && Array.isArray(raw.entries)) {
8567
9007
  return raw.entries;
8568
9008
  }
@@ -8571,10 +9011,10 @@ function tryLoadSidecar(mapPath, mapText) {
8571
9011
  return null;
8572
9012
  }
8573
9013
  function loadCodeMapEntries(mapPath) {
8574
- if (!existsSync17(mapPath)) {
9014
+ if (!existsSync21(mapPath)) {
8575
9015
  throw new Error(`${mapPath} is missing; run \`cdd-kit code-map\` first.`);
8576
9016
  }
8577
- const text = readFileSync16(mapPath, "utf8");
9017
+ const text = readFileSync20(mapPath, "utf8");
8578
9018
  const fromSidecar = tryLoadSidecar(mapPath, text);
8579
9019
  if (fromSidecar)
8580
9020
  return fromSidecar;
@@ -8946,7 +9386,7 @@ var init_builder = __esm({
8946
9386
  });
8947
9387
 
8948
9388
  // src/code-graph/reader.ts
8949
- import { existsSync as existsSync18, readFileSync as readFileSync17 } from "fs";
9389
+ import { existsSync as existsSync22, readFileSync as readFileSync21 } from "fs";
8950
9390
  function graphPathFor(mapPath) {
8951
9391
  const match = mapPath.match(/^(.*?)(?:code-map)(.*)\.ya?ml$/i);
8952
9392
  if (match)
@@ -8954,10 +9394,10 @@ function graphPathFor(mapPath) {
8954
9394
  return `${mapPath.replace(/\.ya?ml$/i, "")}.graph.json`;
8955
9395
  }
8956
9396
  function loadCodeGraph(graphPath) {
8957
- if (!existsSync18(graphPath)) {
9397
+ if (!existsSync22(graphPath)) {
8958
9398
  throw new Error(`${graphPath} is missing; run \`cdd-kit code-map\` first.`);
8959
9399
  }
8960
- const raw = JSON.parse(readFileSync17(graphPath, "utf8"));
9400
+ const raw = JSON.parse(readFileSync21(graphPath, "utf8"));
8961
9401
  if (!raw || raw.schema_version !== "1.0" || !Array.isArray(raw.nodes) || !Array.isArray(raw.edges)) {
8962
9402
  throw new Error(`${graphPath} is not a cdd-kit code graph v1 index.`);
8963
9403
  }
@@ -8975,9 +9415,9 @@ __export(worker_dispatch_exports, {
8975
9415
  scanLangWithWorkers: () => scanLangWithWorkers
8976
9416
  });
8977
9417
  import { execFile } from "child_process";
8978
- import { writeFileSync as writeFileSync9, unlinkSync } from "fs";
9418
+ import { writeFileSync as writeFileSync12, unlinkSync } from "fs";
8979
9419
  import { randomBytes } from "crypto";
8980
- import { join as join19 } from "path";
9420
+ import { join as join22 } from "path";
8981
9421
  import { tmpdir } from "os";
8982
9422
  function chunk(arr, parts) {
8983
9423
  const size = Math.ceil(arr.length / parts);
@@ -8987,15 +9427,15 @@ function chunk(arr, parts) {
8987
9427
  return out;
8988
9428
  }
8989
9429
  function scanChunkInChild(cliEntry, lang, files, repoRoot) {
8990
- return new Promise((resolve2, reject) => {
9430
+ return new Promise((resolve6, reject) => {
8991
9431
  if (!ALLOWED_LANGS.has(lang)) {
8992
9432
  return reject(new Error(`refusing to spawn worker for unknown lang: ${lang}`));
8993
9433
  }
8994
- const listFile = join19(
9434
+ const listFile = join22(
8995
9435
  tmpdir(),
8996
9436
  `cdd-cm-worker-${process.pid}-${randomBytes(12).toString("hex")}.txt`
8997
9437
  );
8998
- writeFileSync9(listFile, files.join("\n") + "\n", { encoding: "utf8", mode: 384 });
9438
+ writeFileSync12(listFile, files.join("\n") + "\n", { encoding: "utf8", mode: 384 });
8999
9439
  execFile(
9000
9440
  process.execPath,
9001
9441
  [cliEntry, "__code-map-scan", "--lang", lang, "--batch-file", listFile, "--repo-root", repoRoot],
@@ -9009,7 +9449,7 @@ function scanChunkInChild(cliEntry, lang, files, repoRoot) {
9009
9449
  return reject(err);
9010
9450
  try {
9011
9451
  const parsed = JSON.parse(stdout);
9012
- resolve2({ entries: parsed.entries ?? [], warnings: parsed.warnings ?? [] });
9452
+ resolve6({ entries: parsed.entries ?? [], warnings: parsed.warnings ?? [] });
9013
9453
  } catch (e) {
9014
9454
  reject(e);
9015
9455
  }
@@ -9067,15 +9507,15 @@ var python_exports = {};
9067
9507
  __export(python_exports, {
9068
9508
  pythonScanner: () => pythonScanner
9069
9509
  });
9070
- import { spawnSync as spawnSync2 } from "child_process";
9071
- 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";
9072
9512
  import { randomBytes as randomBytes2 } from "crypto";
9073
- import { join as join20 } from "path";
9513
+ import { join as join23 } from "path";
9074
9514
  import { tmpdir as tmpdir2 } from "os";
9075
9515
  function detectPython2() {
9076
9516
  for (const candidate of ["python3", "python"]) {
9077
9517
  try {
9078
- const result = spawnSync2(candidate, ["--version"], {
9518
+ const result = spawnSync4(candidate, ["--version"], {
9079
9519
  encoding: "utf8",
9080
9520
  timeout: 5e3
9081
9521
  });
@@ -9144,13 +9584,13 @@ var init_python = __esm({
9144
9584
  const entries = [];
9145
9585
  const warnings = [];
9146
9586
  const scriptPath = ASSET.codeMapPython;
9147
- const listFile = join20(tmpdir2(), `cdd-codemap-${process.pid}-${randomBytes2(12).toString("hex")}.txt`);
9148
- 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 });
9149
9589
  let stdout = "";
9150
9590
  let stderr = "";
9151
9591
  let exitCode = 0;
9152
9592
  try {
9153
- const result = spawnSync2(
9593
+ const result = spawnSync4(
9154
9594
  interpreter,
9155
9595
  [scriptPath, "--batch-file", listFile, "--repo-root", repoRoot],
9156
9596
  {
@@ -9214,7 +9654,7 @@ var init_python = __esm({
9214
9654
  }
9215
9655
  const r = parsed;
9216
9656
  entries.push({
9217
- path: canonicalRelPath(join20(repoRoot, r.path), repoRoot),
9657
+ path: canonicalRelPath(join23(repoRoot, r.path), repoRoot),
9218
9658
  total_lines: r.total_lines,
9219
9659
  imports: r.imports ?? [],
9220
9660
  constants: r.constants ?? [],
@@ -9264,7 +9704,7 @@ __export(javascript_exports, {
9264
9704
  parseJsSource: () => parseJsSource,
9265
9705
  parseSourceWithPlugins: () => parseSourceWithPlugins
9266
9706
  });
9267
- import { readFileSync as readFileSync19 } from "fs";
9707
+ import { readFileSync as readFileSync23 } from "fs";
9268
9708
  import { parse } from "@babel/parser";
9269
9709
  function parseSourceWithPlugins(source, plugins) {
9270
9710
  return parse(source, {
@@ -9667,7 +10107,7 @@ var init_javascript = __esm({
9667
10107
  async scan(absolutePath, repoRoot) {
9668
10108
  let source;
9669
10109
  try {
9670
- source = readFileSync19(absolutePath, "utf8");
10110
+ source = readFileSync23(absolutePath, "utf8");
9671
10111
  } catch (err) {
9672
10112
  throw err;
9673
10113
  }
@@ -9687,7 +10127,7 @@ var typescript_exports = {};
9687
10127
  __export(typescript_exports, {
9688
10128
  tsScanner: () => tsScanner
9689
10129
  });
9690
- import { readFileSync as readFileSync20 } from "fs";
10130
+ import { readFileSync as readFileSync24 } from "fs";
9691
10131
  var TypeScriptScanner, tsScanner;
9692
10132
  var init_typescript = __esm({
9693
10133
  "src/code-map/scanners/typescript.ts"() {
@@ -9699,7 +10139,7 @@ var init_typescript = __esm({
9699
10139
  async scan(absolutePath, repoRoot) {
9700
10140
  let source;
9701
10141
  try {
9702
- source = readFileSync20(absolutePath, "utf8");
10142
+ source = readFileSync24(absolutePath, "utf8");
9703
10143
  } catch (err) {
9704
10144
  throw err;
9705
10145
  }
@@ -9737,7 +10177,7 @@ var vue_exports = {};
9737
10177
  __export(vue_exports, {
9738
10178
  vueScanner: () => vueScanner
9739
10179
  });
9740
- import { readFileSync as readFileSync21 } from "fs";
10180
+ import { readFileSync as readFileSync25 } from "fs";
9741
10181
  import { parse as parse2 } from "@vue/compiler-sfc";
9742
10182
  var VueScanner, vueScanner;
9743
10183
  var init_vue = __esm({
@@ -9750,7 +10190,7 @@ var init_vue = __esm({
9750
10190
  async scan(absolutePath, repoRoot) {
9751
10191
  let source;
9752
10192
  try {
9753
- source = readFileSync21(absolutePath, "utf8");
10193
+ source = readFileSync25(absolutePath, "utf8");
9754
10194
  } catch (err) {
9755
10195
  throw err;
9756
10196
  }
@@ -9845,14 +10285,15 @@ var init_vue = __esm({
9845
10285
  var code_map_exports = {};
9846
10286
  __export(code_map_exports, {
9847
10287
  codeMap: () => codeMap,
9848
- computeSourcesDigest: () => computeSourcesDigest
10288
+ computeSourcesDigest: () => computeSourcesDigest,
10289
+ slugifySurface: () => slugifySurface
9849
10290
  });
9850
- import { existsSync as existsSync19, mkdirSync as mkdirSync8, readFileSync as readFileSync22, writeFileSync as writeFileSync11 } from "fs";
9851
- 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";
9852
10293
  import { createHash as createHash6 } from "crypto";
9853
10294
  import { createRequire } from "module";
9854
10295
  import { fileURLToPath as fileURLToPath2 } from "url";
9855
- import { join as join21 } from "path";
10296
+ import { join as join24 } from "path";
9856
10297
  function computeSourcesDigest(absolutePaths, cwd) {
9857
10298
  const lines = absolutePaths.slice().sort().map((p) => {
9858
10299
  const rel = relative6(cwd, p).replace(/\\/g, "/");
@@ -9867,14 +10308,15 @@ function slugifySurface(surface) {
9867
10308
  async function codeMap(opts) {
9868
10309
  const start = Date.now();
9869
10310
  if (opts.surface) {
9870
- const resolvedSurface = resolve(process.cwd(), opts.surface);
9871
- if (!existsSync19(resolvedSurface)) {
10311
+ const resolvedSurface = resolve3(process.cwd(), opts.surface);
10312
+ if (!existsSync23(resolvedSurface)) {
9872
10313
  log.error(`code-map --surface path not found: ${opts.surface}`);
9873
10314
  return 1;
9874
10315
  }
9875
10316
  }
9876
10317
  const scanPath = opts.surface ?? opts.path;
9877
- 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;
9878
10320
  const out = opts.out ?? (opts.surface ? `.cdd/code-map.${slugifySurface(opts.surface)}.yml` : ".cdd/code-map.yml");
9879
10321
  let cfg;
9880
10322
  try {
@@ -9942,7 +10384,8 @@ async function codeMap(opts) {
9942
10384
  const sourcesDigest = computeSourcesDigest(files, root);
9943
10385
  const yamlBody = renderYaml(result.entries, {
9944
10386
  generator: `cdd-kit ${_pkg.version}`,
9945
- sourcesDigest
10387
+ sourcesDigest,
10388
+ surfaceRoot
9946
10389
  });
9947
10390
  const totalSrc = result.entries.reduce((s, e) => s + e.total_lines, 0);
9948
10391
  const mapLines = yamlBody.split("\n").length;
@@ -9953,7 +10396,7 @@ async function codeMap(opts) {
9953
10396
  log.warn(`${w.path}: ${w.message}`);
9954
10397
  }
9955
10398
  if (opts.check) {
9956
- const existing = existsSync19(out) ? readFileSync22(out, "utf8") : "";
10399
+ const existing = existsSync23(out) ? readFileSync26(out, "utf8") : "";
9957
10400
  const normalize = (s) => s.replace(/\r\n/g, "\n").replace(/\r/g, "\n").replace(/^# generated: [^\n]+\n/m, "# generated: <normalized>\n");
9958
10401
  if (normalize(existing) !== normalize(yamlBody)) {
9959
10402
  if (!opts.silent)
@@ -9964,11 +10407,11 @@ async function codeMap(opts) {
9964
10407
  log.ok(`code-map up to date: ${out}`);
9965
10408
  return 0;
9966
10409
  }
9967
- mkdirSync8(dirname5(out), { recursive: true });
9968
- writeFileSync11(out, yamlBody, "utf8");
10410
+ mkdirSync10(dirname6(out), { recursive: true });
10411
+ writeFileSync14(out, yamlBody, "utf8");
9969
10412
  try {
9970
10413
  const sidecarPath = sidecarPathFor(out);
9971
- writeFileSync11(sidecarPath, JSON.stringify({ sourcesDigest, entries: result.entries }), "utf8");
10414
+ writeFileSync14(sidecarPath, JSON.stringify({ sourcesDigest, entries: result.entries }), "utf8");
9972
10415
  const rel = relative6(process.cwd(), sidecarPath).replace(/\\/g, "/");
9973
10416
  if (!rel.startsWith("..")) {
9974
10417
  ensureGitignoreEntry(process.cwd(), rel, "cdd-kit local cache (do not commit)");
@@ -9978,7 +10421,7 @@ async function codeMap(opts) {
9978
10421
  generator: `cdd-kit ${_pkg.version}`,
9979
10422
  sourcesDigest
9980
10423
  });
9981
- writeFileSync11(graphPath, JSON.stringify(graph2, null, 2) + "\n", "utf8");
10424
+ writeFileSync14(graphPath, JSON.stringify(graph2, null, 2) + "\n", "utf8");
9982
10425
  const graphRel = relative6(process.cwd(), graphPath).replace(/\\/g, "/");
9983
10426
  if (!graphRel.startsWith("..")) {
9984
10427
  ensureGitignoreEntry(process.cwd(), graphRel, "cdd-kit local cache (do not commit)");
@@ -10003,8 +10446,8 @@ var init_code_map = __esm({
10003
10446
  init_digest();
10004
10447
  init_gitignore();
10005
10448
  _require = createRequire(import.meta.url);
10006
- _pkgPath = join21(fileURLToPath2(import.meta.url), "..", "..", "..", "package.json");
10007
- _pkg = JSON.parse(readFileSync22(_pkgPath, "utf8"));
10449
+ _pkgPath = join24(fileURLToPath2(import.meta.url), "..", "..", "..", "package.json");
10450
+ _pkg = JSON.parse(readFileSync26(_pkgPath, "utf8"));
10008
10451
  }
10009
10452
  });
10010
10453
 
@@ -10013,15 +10456,15 @@ var refresh_exports = {};
10013
10456
  __export(refresh_exports, {
10014
10457
  refresh: () => refresh
10015
10458
  });
10016
- import { existsSync as existsSync20, mkdirSync as mkdirSync9, readdirSync as readdirSync10, copyFileSync as copyFileSync4, readFileSync as readFileSync23, writeFileSync as writeFileSync12 } from "fs";
10017
- 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";
10018
10461
  import { createHash as createHash7 } from "crypto";
10019
10462
  function fileHash2(filePath) {
10020
- return createHash7("sha256").update(readFileSync23(filePath)).digest("hex");
10463
+ return createHash7("sha256").update(readFileSync27(filePath)).digest("hex");
10021
10464
  }
10022
10465
  function planForceRefresh(srcDir, destDir, sectionLabel) {
10023
10466
  const plan = [];
10024
- if (!existsSync20(srcDir))
10467
+ if (!existsSync24(srcDir))
10025
10468
  return plan;
10026
10469
  function walk(curSrc, curDest) {
10027
10470
  let items;
@@ -10031,16 +10474,16 @@ function planForceRefresh(srcDir, destDir, sectionLabel) {
10031
10474
  return;
10032
10475
  }
10033
10476
  for (const item of items) {
10034
- const sp = join22(curSrc, item.name);
10035
- const dp = join22(curDest, item.name);
10477
+ const sp = join25(curSrc, item.name);
10478
+ const dp = join25(curDest, item.name);
10036
10479
  if (item.isDirectory()) {
10037
10480
  walk(sp, dp);
10038
10481
  continue;
10039
10482
  }
10040
10483
  if (!item.isFile())
10041
10484
  continue;
10042
- const rel = join22(sectionLabel, relative7(srcDir, sp)).replace(/\\/g, "/");
10043
- if (!existsSync20(dp)) {
10485
+ const rel = join25(sectionLabel, relative7(srcDir, sp)).replace(/\\/g, "/");
10486
+ if (!existsSync24(dp)) {
10044
10487
  plan.push({ src: sp, dest: dp, rel, action: "add" });
10045
10488
  } else if (fileHash2(sp) !== fileHash2(dp)) {
10046
10489
  plan.push({ src: sp, dest: dp, rel, action: "overwrite" });
@@ -10053,9 +10496,9 @@ function planForceRefresh(srcDir, destDir, sectionLabel) {
10053
10496
  return plan;
10054
10497
  }
10055
10498
  function planSingleFile(src, dest, rel) {
10056
- if (!existsSync20(src))
10499
+ if (!existsSync24(src))
10057
10500
  return null;
10058
- if (!existsSync20(dest))
10501
+ if (!existsSync24(dest))
10059
10502
  return { src, dest, rel, action: "add" };
10060
10503
  if (fileHash2(src) !== fileHash2(dest))
10061
10504
  return { src, dest, rel, action: "overwrite" };
@@ -10068,15 +10511,15 @@ function applyPlan(plan, backupRoot) {
10068
10511
  if (item.action === "skip")
10069
10512
  continue;
10070
10513
  if (item.action === "overwrite") {
10071
- const backupPath = join22(backupRoot, item.rel);
10072
- mkdirSync9(dirname6(backupPath), { recursive: true });
10073
- copyFileSync4(item.dest, backupPath);
10514
+ const backupPath = join25(backupRoot, item.rel);
10515
+ mkdirSync11(dirname7(backupPath), { recursive: true });
10516
+ copyFileSync5(item.dest, backupPath);
10074
10517
  overwritten += 1;
10075
10518
  } else {
10076
10519
  added += 1;
10077
10520
  }
10078
- mkdirSync9(dirname6(item.dest), { recursive: true });
10079
- copyFileSync4(item.src, item.dest);
10521
+ mkdirSync11(dirname7(item.dest), { recursive: true });
10522
+ copyFileSync5(item.src, item.dest);
10080
10523
  }
10081
10524
  return { added, overwritten };
10082
10525
  }
@@ -10097,14 +10540,14 @@ function parseAgentFrontmatter(content) {
10097
10540
  return fm;
10098
10541
  }
10099
10542
  function resyncModelPolicy(cwd) {
10100
- const policyPath = join22(cwd, ".cdd", "model-policy.json");
10543
+ const policyPath = join25(cwd, ".cdd", "model-policy.json");
10101
10544
  const result = { changed: false, diff: [], policyPath };
10102
- if (!existsSync20(AGENTS_HOME))
10545
+ if (!existsSync24(AGENTS_HOME))
10103
10546
  return result;
10104
10547
  const desired = {};
10105
10548
  const agentFiles = readdirSync10(AGENTS_HOME, { withFileTypes: true }).filter((d) => d.isFile() && d.name.endsWith(".md"));
10106
10549
  for (const f of agentFiles) {
10107
- const content = readFileSync23(join22(AGENTS_HOME, f.name), "utf8");
10550
+ const content = readFileSync27(join25(AGENTS_HOME, f.name), "utf8");
10108
10551
  const fm = parseAgentFrontmatter(content);
10109
10552
  if (fm.name && fm.model)
10110
10553
  desired[fm.name] = fm.model;
@@ -10112,9 +10555,9 @@ function resyncModelPolicy(cwd) {
10112
10555
  if (Object.keys(desired).length === 0)
10113
10556
  return result;
10114
10557
  let existing = {};
10115
- if (existsSync20(policyPath)) {
10558
+ if (existsSync24(policyPath)) {
10116
10559
  try {
10117
- existing = JSON.parse(readFileSync23(policyPath, "utf8"));
10560
+ existing = JSON.parse(readFileSync27(policyPath, "utf8"));
10118
10561
  } catch {
10119
10562
  }
10120
10563
  }
@@ -10134,8 +10577,8 @@ function resyncModelPolicy(cwd) {
10134
10577
  merged["schema-version"] = "0.2.0";
10135
10578
  if (!("provider" in merged))
10136
10579
  merged["provider"] = "claude";
10137
- mkdirSync9(dirname6(policyPath), { recursive: true });
10138
- 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");
10139
10582
  result.changed = true;
10140
10583
  return result;
10141
10584
  }
@@ -10143,21 +10586,21 @@ function planTemplateRefresh(cwd) {
10143
10586
  const sections = [];
10144
10587
  sections.push({
10145
10588
  name: "specs/templates",
10146
- plan: planForceRefresh(ASSET.specsTemplates, join22(cwd, "specs", "templates"), "specs/templates")
10589
+ plan: planForceRefresh(ASSET.specsTemplates, join25(cwd, "specs", "templates"), "specs/templates")
10147
10590
  });
10148
10591
  sections.push({
10149
10592
  name: "tests/templates",
10150
- plan: planForceRefresh(ASSET.testsTemplates, join22(cwd, "tests", "templates"), "tests/templates")
10593
+ plan: planForceRefresh(ASSET.testsTemplates, join25(cwd, "tests", "templates"), "tests/templates")
10151
10594
  });
10152
- const ciTemplatesAsset = join22(ASSET.ci, "..", "ci-templates");
10153
- if (existsSync20(ciTemplatesAsset)) {
10595
+ const ciTemplatesAsset = join25(ASSET.ci, "..", "ci-templates");
10596
+ if (existsSync24(ciTemplatesAsset)) {
10154
10597
  sections.push({
10155
10598
  name: "ci-templates",
10156
- plan: planForceRefresh(ciTemplatesAsset, join22(cwd, "ci-templates"), "ci-templates")
10599
+ plan: planForceRefresh(ciTemplatesAsset, join25(cwd, "ci-templates"), "ci-templates")
10157
10600
  });
10158
10601
  }
10159
- const wfAsset = join22(ASSET.githubWorkflows, "contract-driven-gates.yml");
10160
- 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");
10161
10604
  const wfPlan = planSingleFile(wfAsset, wfDest, ".github/workflows/contract-driven-gates.yml");
10162
10605
  if (wfPlan)
10163
10606
  sections.push({ name: ".github/workflows/contract-driven-gates.yml", plan: [wfPlan] });
@@ -10212,7 +10655,7 @@ async function refresh(opts) {
10212
10655
  }
10213
10656
  if (apply) {
10214
10657
  const ts = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
10215
- backupRoot = join22(cwd, ".cdd", ".refresh-backup", ts);
10658
+ backupRoot = join25(cwd, ".cdd", ".refresh-backup", ts);
10216
10659
  const result = applyPlan(total, backupRoot);
10217
10660
  templateAdded = result.added;
10218
10661
  templateOverwritten = result.overwritten;
@@ -10232,8 +10675,8 @@ async function refresh(opts) {
10232
10675
  }
10233
10676
  log.blank();
10234
10677
  if (!opts.noHooks) {
10235
- const markerPath = join22(cwd, HOOKS_MARKER_PATH);
10236
- if (existsSync20(markerPath)) {
10678
+ const markerPath = join25(cwd, HOOKS_MARKER_PATH);
10679
+ if (existsSync24(markerPath)) {
10237
10680
  log.info("[4/6] re-install code-map pre-commit hook (marker found)");
10238
10681
  if (apply) {
10239
10682
  try {
@@ -10329,8 +10772,8 @@ __export(lint_agents_exports, {
10329
10772
  lintAgentContent: () => lintAgentContent,
10330
10773
  lintAgents: () => lintAgents
10331
10774
  });
10332
- import { readdirSync as readdirSync11, readFileSync as readFileSync24 } from "fs";
10333
- import { join as join23 } from "path";
10775
+ import { readdirSync as readdirSync11, readFileSync as readFileSync28 } from "fs";
10776
+ import { join as join26 } from "path";
10334
10777
  import { load as yamlLoad2 } from "js-yaml";
10335
10778
  function extractRequiredArtifactsSection(content) {
10336
10779
  const match = content.match(
@@ -10459,7 +10902,7 @@ function lintAgentContent(filename, rawContent, opts = {}) {
10459
10902
  return violations;
10460
10903
  }
10461
10904
  function collectAgentViolations(cwd, opts = {}) {
10462
- const agentsDir = join23(cwd, ".claude", "agents");
10905
+ const agentsDir = join26(cwd, ".claude", "agents");
10463
10906
  let files;
10464
10907
  try {
10465
10908
  files = readdirSync11(agentsDir).filter((f) => f.endsWith(".md")).sort();
@@ -10470,7 +10913,7 @@ function collectAgentViolations(cwd, opts = {}) {
10470
10913
  for (const filename of files) {
10471
10914
  let content;
10472
10915
  try {
10473
- content = readFileSync24(join23(agentsDir, filename), "utf8");
10916
+ content = readFileSync28(join26(agentsDir, filename), "utf8");
10474
10917
  } catch {
10475
10918
  violations.push({
10476
10919
  file: filename,
@@ -10489,7 +10932,7 @@ async function lintAgents(opts) {
10489
10932
  const violations = collectAgentViolations(cwd, opts);
10490
10933
  if (violations === null) {
10491
10934
  log.error(
10492
- `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?`
10493
10936
  );
10494
10937
  return 1;
10495
10938
  }
@@ -10514,22 +10957,132 @@ var init_lint_agents = __esm({
10514
10957
  }
10515
10958
  });
10516
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
+
10517
11069
  // src/commands/doctor.ts
10518
11070
  var doctor_exports = {};
10519
11071
  __export(doctor_exports, {
10520
11072
  doctor: () => doctor
10521
11073
  });
10522
- 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";
10523
11075
  import { createHash as createHash8 } from "crypto";
10524
- 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";
10525
11078
  function fileExists(cwd, relPath) {
10526
- return existsSync21(join24(cwd, relPath));
11079
+ return existsSync26(join28(cwd, relPath));
10527
11080
  }
10528
11081
  function findFiles(dir, predicate, found = []) {
10529
- if (!existsSync21(dir))
11082
+ if (!existsSync26(dir))
10530
11083
  return found;
10531
- for (const entry of readdirSync12(dir, { withFileTypes: true })) {
10532
- const fullPath = join24(dir, entry.name);
11084
+ for (const entry of readdirSync13(dir, { withFileTypes: true })) {
11085
+ const fullPath = join28(dir, entry.name);
10533
11086
  if (entry.isDirectory())
10534
11087
  findFiles(fullPath, predicate, found);
10535
11088
  else if (entry.isFile() && predicate(entry.name))
@@ -10545,9 +11098,9 @@ function inputDigest(paths, cwd) {
10545
11098
  return createHash8("sha256").update(combined).digest("hex");
10546
11099
  }
10547
11100
  function readContextIndexMetadata(filePath) {
10548
- if (!existsSync21(filePath))
11101
+ if (!existsSync26(filePath))
10549
11102
  return {};
10550
- const text = readFileSync25(filePath, "utf8");
11103
+ const text = readFileSync30(filePath, "utf8");
10551
11104
  const out = {};
10552
11105
  const digestMatch = text.match(/^inputs-digest:\s*([a-f0-9]+)/m);
10553
11106
  if (digestMatch)
@@ -10559,14 +11112,14 @@ function readContextIndexMetadata(filePath) {
10559
11112
  }
10560
11113
  function checkContextFreshness(cwd) {
10561
11114
  const findings = [];
10562
- const projectMap = join24(cwd, "specs", "context", "project-map.md");
10563
- const contractsIndex = join24(cwd, "specs", "context", "contracts-index.md");
10564
- 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");
10565
11118
  const contractFiles = findFiles(
10566
- join24(cwd, "contracts"),
11119
+ join28(cwd, "contracts"),
10567
11120
  (name) => name.endsWith(".md") && name !== "INDEX.md" && name !== "CHANGELOG.md"
10568
11121
  );
10569
- if (!existsSync21(projectMap) || !existsSync21(contractsIndex)) {
11122
+ if (!existsSync26(projectMap) || !existsSync26(contractsIndex)) {
10570
11123
  findings.push({
10571
11124
  level: "warning",
10572
11125
  message: "specs/context indexes are missing; run cdd-kit context-scan before classification"
@@ -10575,7 +11128,7 @@ function checkContextFreshness(cwd) {
10575
11128
  }
10576
11129
  const projectMapMeta = readContextIndexMetadata(projectMap);
10577
11130
  const contractsIndexMeta = readContextIndexMetadata(contractsIndex);
10578
- const projectInputDigest = inputDigest([contextPolicy].filter(existsSync21), cwd);
11131
+ const projectInputDigest = inputDigest([contextPolicy].filter(existsSync26), cwd);
10579
11132
  if (projectMapMeta.inputsDigest === void 0) {
10580
11133
  findings.push({
10581
11134
  level: "warning",
@@ -10612,7 +11165,7 @@ function checkContextFreshness(cwd) {
10612
11165
  }
10613
11166
  function readAgentModel(path) {
10614
11167
  try {
10615
- const text = readFileSync25(path, "utf8");
11168
+ const text = readFileSync30(path, "utf8");
10616
11169
  const m = text.match(/^model:\s*(\S+)/m);
10617
11170
  return m ? m[1] : null;
10618
11171
  } catch {
@@ -10620,12 +11173,12 @@ function readAgentModel(path) {
10620
11173
  }
10621
11174
  }
10622
11175
  function checkModelPolicyDrift(cwd) {
10623
- const policyPath = join24(cwd, ".cdd", "model-policy.json");
10624
- if (!existsSync21(policyPath))
11176
+ const policyPath = join28(cwd, ".cdd", "model-policy.json");
11177
+ if (!existsSync26(policyPath))
10625
11178
  return [];
10626
11179
  let policy;
10627
11180
  try {
10628
- policy = JSON.parse(readFileSync25(policyPath, "utf8"));
11181
+ policy = JSON.parse(readFileSync30(policyPath, "utf8"));
10629
11182
  } catch {
10630
11183
  return [{ level: "warning", message: ".cdd/model-policy.json is not valid JSON" }];
10631
11184
  }
@@ -10637,18 +11190,18 @@ function checkModelPolicyDrift(cwd) {
10637
11190
  }];
10638
11191
  }
10639
11192
  const candidateDirs = [
10640
- join24(cwd, ".claude", "agents"),
10641
- process.env.HOME ? join24(process.env.HOME, ".claude", "agents") : "",
10642
- process.env.USERPROFILE ? join24(process.env.USERPROFILE, ".claude", "agents") : ""
10643
- ].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));
10644
11197
  if (candidateDirs.length === 0)
10645
11198
  return [];
10646
11199
  const findings = [];
10647
11200
  for (const [role, expected] of Object.entries(roles)) {
10648
11201
  let foundAny = false;
10649
11202
  for (const dir of candidateDirs) {
10650
- const path = join24(dir, `${role}.md`);
10651
- if (!existsSync21(path))
11203
+ const path = join28(dir, `${role}.md`);
11204
+ if (!existsSync26(path))
10652
11205
  continue;
10653
11206
  foundAny = true;
10654
11207
  const actual = readAgentModel(path);
@@ -10668,14 +11221,14 @@ function checkModelPolicyDrift(cwd) {
10668
11221
  return findings;
10669
11222
  }
10670
11223
  function checkAgentLint(cwd) {
10671
- const agentsDir = join24(cwd, ".claude", "agents");
10672
- if (!existsSync21(agentsDir))
11224
+ const agentsDir = join28(cwd, ".claude", "agents");
11225
+ if (!existsSync26(agentsDir))
10673
11226
  return [];
10674
11227
  const violations = collectAgentViolations(cwd);
10675
11228
  if (violations === null) {
10676
11229
  return [{
10677
11230
  level: "warning",
10678
- 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`
10679
11232
  }];
10680
11233
  }
10681
11234
  const findings = violations.map((v) => ({
@@ -10689,8 +11242,8 @@ function checkAgentLint(cwd) {
10689
11242
  }
10690
11243
  function checkCodeMap(cwd) {
10691
11244
  const findings = [];
10692
- const mapPath = join24(cwd, ".cdd", "code-map.yml");
10693
- if (!existsSync21(mapPath)) {
11245
+ const mapPath = join28(cwd, ".cdd", "code-map.yml");
11246
+ if (!existsSync26(mapPath)) {
10694
11247
  const probe2 = checkCodeMapFreshness(cwd);
10695
11248
  if (probe2.status === "config-error") {
10696
11249
  findings.push({ level: "warning", message: `.cdd/code-map-config.yml is invalid: ${probe2.configError}` });
@@ -10709,7 +11262,7 @@ function checkCodeMap(cwd) {
10709
11262
  const more = probe.staleCount > 3 ? ` (+${probe.staleCount - 3} more)` : "";
10710
11263
  findings.push({ level: "warning", message: `code-map stale: ${top}${more}; run \`cdd-kit code-map\`` });
10711
11264
  }
10712
- const text = readFileSync25(mapPath, "utf8");
11265
+ const text = readFileSync30(mapPath, "utf8");
10713
11266
  const m = text.match(/^# files: (\d+), src-lines: (\d+), map-lines: (\d+), compression: ([\d.]+)x/m);
10714
11267
  if (m) {
10715
11268
  findings.push({ level: "ok", message: `code-map: ${m[1]} files, ${m[4]}x compression` });
@@ -10718,6 +11271,68 @@ function checkCodeMap(cwd) {
10718
11271
  }
10719
11272
  return findings;
10720
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
+ }
10721
11336
  async function buildDoctorReport(cwd, opts) {
10722
11337
  const requestedProvider = opts.provider ?? "auto";
10723
11338
  if (!validateProviderOption(requestedProvider)) {
@@ -10743,6 +11358,9 @@ async function buildDoctorReport(cwd, opts) {
10743
11358
  findings.push(...checkModelPolicyDrift(cwd));
10744
11359
  findings.push(...checkAgentLint(cwd));
10745
11360
  findings.push(...checkCodeMap(cwd));
11361
+ findings.push(...checkApiConformance(cwd));
11362
+ findings.push(...checkMcpRegistration(cwd, provider));
11363
+ findings.push(...checkChokepoints(cwd));
10746
11364
  const errors = findings.filter((finding) => finding.level === "error").length;
10747
11365
  const warnings = findings.filter((finding) => finding.level === "warning").length;
10748
11366
  return {
@@ -10785,11 +11403,11 @@ async function attemptAutoFixes(cwd, report) {
10785
11403
  }
10786
11404
  }
10787
11405
  if (/model-policy\.json has no role bindings/i.test(finding.message)) {
10788
- const policyPath = join24(cwd, ".cdd", "model-policy.json");
11406
+ const policyPath = join28(cwd, ".cdd", "model-policy.json");
10789
11407
  try {
10790
11408
  let existing = {};
10791
11409
  try {
10792
- existing = JSON.parse(readFileSync25(policyPath, "utf8"));
11410
+ existing = JSON.parse(readFileSync30(policyPath, "utf8"));
10793
11411
  } catch {
10794
11412
  }
10795
11413
  const merged = {
@@ -10814,8 +11432,8 @@ async function attemptAutoFixes(cwd, report) {
10814
11432
  "repo-context-scanner": "haiku"
10815
11433
  }
10816
11434
  };
10817
- const { writeFileSync: writeFileSync16 } = await import("fs");
10818
- 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");
10819
11437
  fixed.push(`populated .cdd/model-policy.json with default role bindings`);
10820
11438
  continue;
10821
11439
  } catch (err) {
@@ -10884,16 +11502,188 @@ var init_doctor = __esm({
10884
11502
  init_provider();
10885
11503
  init_freshness();
10886
11504
  init_lint_agents();
11505
+ init_chokepoints();
10887
11506
  init_digest();
10888
11507
  }
10889
11508
  });
10890
11509
 
10891
- // src/commands/code-map-scan-worker.ts
10892
- var code_map_scan_worker_exports = {};
10893
- __export(code_map_scan_worker_exports, {
10894
- 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
10895
11517
  });
10896
- 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";
10897
11687
  async function runScanWorker(opts) {
10898
11688
  let scanner;
10899
11689
  switch (opts.lang) {
@@ -10913,7 +11703,7 @@ async function runScanWorker(opts) {
10913
11703
  }
10914
11704
  let files;
10915
11705
  try {
10916
- 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);
10917
11707
  } catch (err) {
10918
11708
  process.stderr.write(`__code-map-scan: cannot read --batch-file: ${err.message}
10919
11709
  `);
@@ -10933,10 +11723,16 @@ var init_code_map_scan_worker = __esm({
10933
11723
  // src/commands/index-query.ts
10934
11724
  var index_query_exports = {};
10935
11725
  __export(index_query_exports, {
11726
+ DEFAULT_SOURCE_BUDGET: () => DEFAULT_SOURCE_BUDGET,
10936
11727
  indexQuery: () => indexQuery,
10937
- queryEntries: () => queryEntries
11728
+ queryEntries: () => queryEntries,
11729
+ resolveSourceBudget: () => resolveSourceBudget,
11730
+ surfaceRootFor: () => surfaceRootFor
10938
11731
  });
10939
- 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
+ }
10940
11736
  async function indexQuery(term, opts) {
10941
11737
  const mapPath = opts.map || ".cdd/code-map.yml";
10942
11738
  const limit = Number.isFinite(opts.limit) && opts.limit > 0 ? Math.floor(opts.limit) : 10;
@@ -10946,7 +11742,7 @@ async function indexQuery(term, opts) {
10946
11742
  return printFailure(freshness.error, opts.json);
10947
11743
  }
10948
11744
  refreshed = freshness.refreshed;
10949
- if (!existsSync22(mapPath)) {
11745
+ if (!existsSync27(mapPath)) {
10950
11746
  return printFailure(`${mapPath} is missing; run \`cdd-kit code-map\` first.`, opts.json);
10951
11747
  }
10952
11748
  let entries;
@@ -10956,6 +11752,9 @@ async function indexQuery(term, opts) {
10956
11752
  return printFailure(`${mapPath} is not readable YAML: ${err.message}`, opts.json);
10957
11753
  }
10958
11754
  const results = queryEntries(entries, term).slice(0, limit);
11755
+ if (opts.withSource) {
11756
+ attachSource(results, resolveSourceBudget(opts.sourceBudget), surfaceRootFor(mapPath));
11757
+ }
10959
11758
  const payload = {
10960
11759
  index: mapPath,
10961
11760
  query: term,
@@ -11074,6 +11873,68 @@ function firstLine(lines) {
11074
11873
  const m = lines?.match(/^\d+/);
11075
11874
  return m ? Number(m[0]) : Number.MAX_SAFE_INTEGER;
11076
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
+ }
11077
11938
  function printText(payload) {
11078
11939
  if (payload.results.length === 0) {
11079
11940
  console.log(`No matches for "${payload.query}" in ${payload.index}.`);
@@ -11089,9 +11950,18 @@ function printText(payload) {
11089
11950
  const loc = match.lines ? ` lines ${match.lines}` : match.line ? ` line ${match.line}` : "";
11090
11951
  const detail = match.detail && match.detail !== match.name ? ` - ${match.detail}` : "";
11091
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
+ }
11092
11962
  }
11093
11963
  }
11094
- 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.");
11095
11965
  }
11096
11966
  function printFailure(message, json) {
11097
11967
  if (json) {
@@ -11102,10 +11972,12 @@ function printFailure(message, json) {
11102
11972
  }
11103
11973
  return 1;
11104
11974
  }
11975
+ var DEFAULT_SOURCE_BUDGET;
11105
11976
  var init_index_query = __esm({
11106
11977
  "src/commands/index-query.ts"() {
11107
11978
  "use strict";
11108
11979
  init_index_reader();
11980
+ DEFAULT_SOURCE_BUDGET = 400;
11109
11981
  }
11110
11982
  });
11111
11983
 
@@ -11114,7 +11986,7 @@ var index_impact_exports = {};
11114
11986
  __export(index_impact_exports, {
11115
11987
  indexImpact: () => indexImpact
11116
11988
  });
11117
- import { existsSync as existsSync23 } from "fs";
11989
+ import { existsSync as existsSync28 } from "fs";
11118
11990
  import { posix as posix2 } from "path";
11119
11991
  async function indexImpact(term, opts) {
11120
11992
  const mapPath = opts.map || ".cdd/code-map.yml";
@@ -11123,7 +11995,7 @@ async function indexImpact(term, opts) {
11123
11995
  if (freshness.error) {
11124
11996
  return printFailure2(freshness.error, opts.json);
11125
11997
  }
11126
- if (!existsSync23(mapPath)) {
11998
+ if (!existsSync28(mapPath)) {
11127
11999
  return printFailure2(`${mapPath} is missing; run \`cdd-kit code-map\` first.`, opts.json);
11128
12000
  }
11129
12001
  let entries;
@@ -11477,22 +12349,22 @@ __export(graph_exports, {
11477
12349
  graphStatus: () => graphStatus,
11478
12350
  graphSync: () => graphSync
11479
12351
  });
11480
- import { existsSync as existsSync24 } from "fs";
11481
- import { join as join25 } from "path";
11482
- 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";
11483
12355
  function codeGraphCommand() {
11484
12356
  return process.env.CDD_CODEGRAPH_BIN || "codegraph";
11485
12357
  }
11486
12358
  function runCodeGraph(args, cwd = process.cwd()) {
11487
12359
  const command = codeGraphCommand();
11488
12360
  if (command.toLowerCase().endsWith(".js")) {
11489
- return spawnSync3(process.execPath, [command, ...args], { cwd, encoding: "buffer" });
12361
+ return spawnSync7(process.execPath, [command, ...args], { cwd, encoding: "buffer" });
11490
12362
  }
11491
- return spawnSync3(command, args, { cwd, encoding: "buffer" });
12363
+ return spawnSync7(command, args, { cwd, encoding: "buffer" });
11492
12364
  }
11493
12365
  function probeCodeGraph(cwd = process.cwd()) {
11494
12366
  const command = codeGraphCommand();
11495
- const initialized = existsSync24(join25(cwd, ".codegraph"));
12367
+ const initialized = existsSync29(join29(cwd, ".codegraph"));
11496
12368
  const version = runCodeGraph(["--version"], cwd);
11497
12369
  if (version.error) {
11498
12370
  return {
@@ -11562,7 +12434,7 @@ async function ensureNativeGraph(mapPath, refresh2) {
11562
12434
  if (freshness.error)
11563
12435
  return { graphPath: graphPathFor(mapPath), refreshed: freshness.refreshed, error: freshness.error };
11564
12436
  const graphPath = graphPathFor(mapPath);
11565
- if (!existsSync24(graphPath) && refresh2) {
12437
+ if (!existsSync29(graphPath) && refresh2) {
11566
12438
  const { codeMap: codeMap2 } = await Promise.resolve().then(() => (init_code_map(), code_map_exports));
11567
12439
  const exit = await codeMap2({
11568
12440
  path: ".",
@@ -11595,7 +12467,7 @@ async function graphStatus(opts = {}) {
11595
12467
  const graphPath = graphPathFor(mapPath);
11596
12468
  const freshness = checkCodeMapFreshness(cwd, mapPath);
11597
12469
  let graphStats;
11598
- if (existsSync24(graphPath)) {
12470
+ if (existsSync29(graphPath)) {
11599
12471
  try {
11600
12472
  const graph2 = loadCodeGraph(graphPath);
11601
12473
  graphStats = { graph: graphPath, nodes: graph2.nodes.length, edges: graph2.edges.length, unresolved: graph2.unresolved.length };
@@ -11664,8 +12536,10 @@ async function graphQuery(term, opts) {
11664
12536
  try {
11665
12537
  const graph2 = loadCodeGraph(ensured.graphPath);
11666
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();
11667
12540
  if (opts.json) {
11668
- 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 });
11669
12543
  } else {
11670
12544
  console.log(`graph: ${ensured.graphPath}${ensured.refreshed ? " (refreshed)" : ""}`);
11671
12545
  console.log(`query: ${term}`);
@@ -11674,8 +12548,15 @@ async function graphQuery(term, opts) {
11674
12548
  const n = result.node;
11675
12549
  console.log(`- ${n.kind}: ${n.qualified_name} lines ${n.start_line}-${n.end_line}`);
11676
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
+ }
11677
12558
  }
11678
- 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.");
11679
12560
  }
11680
12561
  return results.length === 0 ? 1 : 0;
11681
12562
  } catch (err) {
@@ -11686,9 +12567,44 @@ async function graphQuery(term, opts) {
11686
12567
  map: opts.map || ".cdd/code-map.yml",
11687
12568
  limit: opts.limit,
11688
12569
  json: opts.json === true,
11689
- refresh: opts.refresh !== false
12570
+ refresh: opts.refresh !== false,
12571
+ withSource: opts.withSource === true,
12572
+ sourceBudget: resolveSourceBudget(opts.sourceBudget)
11690
12573
  });
11691
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
+ }
11692
12608
  async function graphImpact2(term, opts) {
11693
12609
  const selected = selectEngine(opts);
11694
12610
  if ("error" in selected)
@@ -11784,7 +12700,7 @@ async function graphContext2(task, opts) {
11784
12700
  const freshness = await ensureCodeMapFresh(mapPath, opts.refresh !== false);
11785
12701
  if (freshness.error)
11786
12702
  return printEngineError(freshness.error, opts.json, selected.probe);
11787
- if (!existsSync24(mapPath))
12703
+ if (!existsSync29(mapPath))
11788
12704
  return printEngineError(`${mapPath} is missing; run \`cdd-kit code-map\` first.`, opts.json, selected.probe);
11789
12705
  let entries;
11790
12706
  try {
@@ -11835,12 +12751,70 @@ var init_graph = __esm({
11835
12751
  }
11836
12752
  });
11837
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
+
11838
12812
  // src/mcp/server.ts
11839
12813
  var server_exports = {};
11840
12814
  __export(server_exports, {
11841
12815
  runMcpServer: () => runMcpServer
11842
12816
  });
11843
- import { spawnSync as spawnSync4 } from "child_process";
12817
+ import { spawnSync as spawnSync8 } from "child_process";
11844
12818
  import { fileURLToPath as fileURLToPath3 } from "url";
11845
12819
  import { createInterface } from "readline";
11846
12820
  async function runMcpServer(opts) {
@@ -11931,6 +12905,7 @@ function callTool(name, args) {
11931
12905
  "--limit",
11932
12906
  String(optionalInt(args.limit, 10)),
11933
12907
  "--json",
12908
+ ...sourceArgs(args),
11934
12909
  ...refreshArgs(args)
11935
12910
  ]);
11936
12911
  case "cdd_graph_context":
@@ -11973,6 +12948,7 @@ function callTool(name, args) {
11973
12948
  "--limit",
11974
12949
  String(optionalInt(args.limit, 10)),
11975
12950
  "--json",
12951
+ ...sourceArgs(args),
11976
12952
  ...refreshArgs(args)
11977
12953
  ]);
11978
12954
  case "cdd_index_impact":
@@ -11996,7 +12972,7 @@ function callTool(name, args) {
11996
12972
  }
11997
12973
  function runCddJson(args) {
11998
12974
  const cliPath = process.argv[1] || fileURLToPath3(import.meta.url);
11999
- const result = spawnSync4(process.execPath, [cliPath, ...args], {
12975
+ const result = spawnSync8(process.execPath, [cliPath, ...args], {
12000
12976
  cwd: process.cwd(),
12001
12977
  env: process.env,
12002
12978
  encoding: "utf8"
@@ -12018,6 +12994,11 @@ function runCddJson(args) {
12018
12994
  function refreshArgs(args) {
12019
12995
  return args.refresh === false ? ["--no-refresh"] : [];
12020
12996
  }
12997
+ function sourceArgs(args) {
12998
+ if (args.withSource !== true)
12999
+ return [];
13000
+ return ["--with-source", "--source-budget", String(optionalInt(args.sourceBudget, 400))];
13001
+ }
12021
13002
  function asObject(value) {
12022
13003
  return value && typeof value === "object" && !Array.isArray(value) ? value : {};
12023
13004
  }
@@ -12063,7 +13044,7 @@ var init_server = __esm({
12063
13044
  },
12064
13045
  {
12065
13046
  name: "cdd_graph_query",
12066
- 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.",
12067
13048
  inputSchema: {
12068
13049
  type: "object",
12069
13050
  properties: {
@@ -12071,6 +13052,8 @@ var init_server = __esm({
12071
13052
  limit: { type: "integer", minimum: 1, maximum: 100, default: 10 },
12072
13053
  map: { type: "string", description: "Code-map YAML path.", default: DEFAULT_MAP },
12073
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." },
12074
13057
  refresh: { type: "boolean", default: true }
12075
13058
  },
12076
13059
  required: ["query"],
@@ -12112,13 +13095,15 @@ var init_server = __esm({
12112
13095
  },
12113
13096
  {
12114
13097
  name: "cdd_index_query",
12115
- 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.",
12116
13099
  inputSchema: {
12117
13100
  type: "object",
12118
13101
  properties: {
12119
13102
  query: { type: "string", description: "Symbol, file path, import module, enum member, or substring." },
12120
13103
  limit: { type: "integer", minimum: 1, maximum: 100, default: 10 },
12121
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." },
12122
13107
  refresh: { type: "boolean", default: true }
12123
13108
  },
12124
13109
  required: ["query"],
@@ -12149,28 +13134,28 @@ var archive_exports = {};
12149
13134
  __export(archive_exports, {
12150
13135
  archive: () => archive
12151
13136
  });
12152
- import { join as join26 } from "path";
12153
- 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";
12154
13139
  import yaml4 from "js-yaml";
12155
13140
  async function archive(changeId) {
12156
13141
  const cwd = process.cwd();
12157
- const changeDir = join26(cwd, "specs", "changes", changeId);
13142
+ const changeDir = join31(cwd, "specs", "changes", changeId);
12158
13143
  const archiveYear = (/* @__PURE__ */ new Date()).getFullYear().toString();
12159
- const archiveBase = join26(cwd, "specs", "archive", archiveYear);
12160
- const archiveDir = join26(archiveBase, changeId);
12161
- const indexPath = join26(cwd, "specs", "archive", "INDEX.md");
12162
- 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)) {
12163
13148
  log.error(`Change not found: specs/changes/${changeId}`);
12164
13149
  process.exit(1);
12165
13150
  }
12166
- if (existsSync25(archiveDir)) {
13151
+ if (existsSync31(archiveDir)) {
12167
13152
  log.error(`Already archived: specs/archive/${archiveYear}/${changeId}`);
12168
13153
  process.exit(1);
12169
13154
  }
12170
- const tasksPath = join26(changeDir, "tasks.yml");
12171
- if (existsSync25(tasksPath)) {
13155
+ const tasksPath = join31(changeDir, "tasks.yml");
13156
+ if (existsSync31(tasksPath)) {
12172
13157
  try {
12173
- const raw = readFileSync27(tasksPath, "utf8");
13158
+ const raw = readFileSync35(tasksPath, "utf8");
12174
13159
  const data = yaml4.load(raw);
12175
13160
  if (data?.status === "gate-blocked") {
12176
13161
  log.warn("tasks.yml has status: gate-blocked \u2014 archiving anyway (change was paused).");
@@ -12183,8 +13168,8 @@ async function archive(changeId) {
12183
13168
  log.warn("tasks.yml could not be parsed \u2014 archiving anyway.");
12184
13169
  }
12185
13170
  }
12186
- if (!existsSync25(archiveBase)) {
12187
- mkdirSync10(archiveBase, { recursive: true });
13171
+ if (!existsSync31(archiveBase)) {
13172
+ mkdirSync12(archiveBase, { recursive: true });
12188
13173
  }
12189
13174
  try {
12190
13175
  renameSync2(changeDir, archiveDir);
@@ -12200,8 +13185,8 @@ async function archive(changeId) {
12200
13185
  const today = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
12201
13186
  const indexLine = `| ${changeId} | ${archiveYear} | ${today} | specs/archive/${archiveYear}/${changeId}/ |
12202
13187
  `;
12203
- if (!existsSync25(indexPath)) {
12204
- writeFileSync13(indexPath, `# Archive Index
13188
+ if (!existsSync31(indexPath)) {
13189
+ writeFileSync16(indexPath, `# Archive Index
12205
13190
 
12206
13191
  | change-id | year | archived-date | path |
12207
13192
  |---|---|---|---|
@@ -12225,37 +13210,37 @@ var abandon_exports = {};
12225
13210
  __export(abandon_exports, {
12226
13211
  abandon: () => abandon
12227
13212
  });
12228
- import { join as join27 } from "path";
12229
- 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";
12230
13215
  import yaml5 from "js-yaml";
12231
13216
  async function abandon(changeId, opts) {
12232
13217
  const cwd = process.cwd();
12233
- const changeDir = join27(cwd, "specs", "changes", changeId);
12234
- const tasksPath = join27(changeDir, "tasks.yml");
12235
- if (!existsSync26(changeDir)) {
13218
+ const changeDir = join32(cwd, "specs", "changes", changeId);
13219
+ const tasksPath = join32(changeDir, "tasks.yml");
13220
+ if (!existsSync32(changeDir)) {
12236
13221
  log.error(`Change not found: specs/changes/${changeId}`);
12237
13222
  process.exit(1);
12238
13223
  }
12239
- if (existsSync26(tasksPath)) {
12240
- const raw = readFileSync28(tasksPath, "utf8");
13224
+ if (existsSync32(tasksPath)) {
13225
+ const raw = readFileSync36(tasksPath, "utf8");
12241
13226
  const data = yaml5.load(raw) ?? {};
12242
13227
  data["status"] = "abandoned";
12243
13228
  if (!data["change-id"]) {
12244
13229
  data["change-id"] = changeId;
12245
13230
  }
12246
- writeFileSync14(tasksPath, yaml5.dump(data, { lineWidth: -1, noRefs: true }), "utf8");
13231
+ writeFileSync17(tasksPath, yaml5.dump(data, { lineWidth: -1, noRefs: true }), "utf8");
12247
13232
  }
12248
13233
  const today = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
12249
- const archiveDir = join27(cwd, "specs", "archive");
12250
- const indexPath = join27(archiveDir, "INDEX.md");
13234
+ const archiveDir = join32(cwd, "specs", "archive");
13235
+ const indexPath = join32(archiveDir, "INDEX.md");
12251
13236
  const reason = opts.reason ?? "no reason given";
12252
13237
  const indexLine = `| ${changeId} | abandoned | ${today} | ${reason} |
12253
13238
  `;
12254
- if (!existsSync26(archiveDir)) {
12255
- mkdirSync11(archiveDir, { recursive: true });
13239
+ if (!existsSync32(archiveDir)) {
13240
+ mkdirSync13(archiveDir, { recursive: true });
12256
13241
  }
12257
- if (!existsSync26(indexPath)) {
12258
- writeFileSync14(indexPath, `# Archive Index
13242
+ if (!existsSync32(indexPath)) {
13243
+ writeFileSync17(indexPath, `# Archive Index
12259
13244
 
12260
13245
  | change-id | status | date | notes |
12261
13246
  |---|---|---|---|
@@ -12279,28 +13264,28 @@ var list_changes_exports = {};
12279
13264
  __export(list_changes_exports, {
12280
13265
  listChanges: () => listChanges
12281
13266
  });
12282
- import { join as join28 } from "path";
12283
- 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";
12284
13269
  import yaml6 from "js-yaml";
12285
13270
  async function listChanges() {
12286
13271
  const cwd = process.cwd();
12287
- const changesDir = join28(cwd, "specs", "changes");
13272
+ const changesDir = join33(cwd, "specs", "changes");
12288
13273
  log.blank();
12289
13274
  const active = [];
12290
- if (existsSync27(changesDir)) {
12291
- 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));
12292
13277
  }
12293
13278
  if (active.length === 0) {
12294
13279
  log.info("No active changes in specs/changes/");
12295
13280
  } else {
12296
13281
  log.info("Active changes:");
12297
13282
  for (const id of active) {
12298
- const tasksPath = join28(changesDir, id, "tasks.yml");
13283
+ const tasksPath = join33(changesDir, id, "tasks.yml");
12299
13284
  let status = "in-progress";
12300
13285
  let pending = 0;
12301
- if (existsSync27(tasksPath)) {
13286
+ if (existsSync33(tasksPath)) {
12302
13287
  try {
12303
- const raw = readFileSync29(tasksPath, "utf8");
13288
+ const raw = readFileSync37(tasksPath, "utf8");
12304
13289
  const data = yaml6.load(raw);
12305
13290
  if (data?.status)
12306
13291
  status = data.status;
@@ -12332,9 +13317,9 @@ __export(context_exports, {
12332
13317
  rejectContextExpansion: () => rejectContextExpansion,
12333
13318
  requestContextExpansion: () => requestContextExpansion
12334
13319
  });
12335
- import { existsSync as existsSync28, readFileSync as readFileSync30, writeFileSync as writeFileSync15 } from "fs";
12336
- import { join as join29 } from "path";
12337
- 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";
12338
13323
  function normalizePath(path) {
12339
13324
  return path.replace(/\\/g, "/").replace(/^\.\//, "").trim();
12340
13325
  }
@@ -12348,18 +13333,18 @@ function validateRepoRelativePath(path) {
12348
13333
  return null;
12349
13334
  }
12350
13335
  function manifestPathFor(changeId) {
12351
- return join29(process.cwd(), "specs", "changes", changeId, "context-manifest.md");
13336
+ return join34(process.cwd(), "specs", "changes", changeId, "context-manifest.md");
12352
13337
  }
12353
13338
  function readManifest(changeId) {
12354
13339
  const manifestPath = manifestPathFor(changeId);
12355
- if (!existsSync28(manifestPath)) {
13340
+ if (!existsSync34(manifestPath)) {
12356
13341
  log.error(`context manifest not found: specs/changes/${changeId}/context-manifest.md`);
12357
13342
  process.exit(1);
12358
13343
  }
12359
- return readFileSync30(manifestPath, "utf8");
13344
+ return readFileSync38(manifestPath, "utf8");
12360
13345
  }
12361
13346
  function writeManifest(changeId, content) {
12362
- writeFileSync15(manifestPathFor(changeId), content.endsWith("\n") ? content : `${content}
13347
+ writeFileSync18(manifestPathFor(changeId), content.endsWith("\n") ? content : `${content}
12363
13348
  `, "utf8");
12364
13349
  }
12365
13350
  function sectionBody(content, heading) {
@@ -12385,7 +13370,7 @@ function pathMatches(relPath, patterns, currentChangeId) {
12385
13370
  return normalized.startsWith("specs/changes/");
12386
13371
  }
12387
13372
  if (/[*?[{]/.test(pattern)) {
12388
- if (picomatch2.isMatch(normalized, pattern, { dot: true, nocase: false }))
13373
+ if (picomatch3.isMatch(normalized, pattern, { dot: true, nocase: false }))
12389
13374
  return true;
12390
13375
  if (pattern.endsWith("/**")) {
12391
13376
  const base = pattern.slice(0, -3);
@@ -12398,11 +13383,11 @@ function pathMatches(relPath, patterns, currentChangeId) {
12398
13383
  });
12399
13384
  }
12400
13385
  function loadContextPolicy() {
12401
- const policyPath = join29(process.cwd(), ".cdd", "context-policy.json");
12402
- if (!existsSync28(policyPath))
13386
+ const policyPath = join34(process.cwd(), ".cdd", "context-policy.json");
13387
+ if (!existsSync34(policyPath))
12403
13388
  return { forbiddenPaths: DEFAULT_FORBIDDEN_PATHS };
12404
13389
  try {
12405
- const custom = JSON.parse(readFileSync30(policyPath, "utf8"));
13390
+ const custom = JSON.parse(readFileSync38(policyPath, "utf8"));
12406
13391
  return {
12407
13392
  forbiddenPaths: Array.from(/* @__PURE__ */ new Set([
12408
13393
  ...DEFAULT_FORBIDDEN_PATHS,
@@ -12684,16 +13669,16 @@ var init_context = __esm({
12684
13669
  });
12685
13670
 
12686
13671
  // src/cli/index.ts
12687
- import { readFileSync as readFileSync31 } from "fs";
13672
+ import { readFileSync as readFileSync39 } from "fs";
12688
13673
  import os from "os";
12689
13674
  import { fileURLToPath as fileURLToPath4 } from "url";
12690
- import { dirname as dirname7, join as join30 } from "path";
13675
+ import { dirname as dirname8, join as join35 } from "path";
12691
13676
  import { Command } from "commander";
12692
13677
 
12693
13678
  // src/commands/init.ts
12694
13679
  init_paths();
12695
- import { join as join5 } from "path";
12696
- 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";
12697
13682
 
12698
13683
  // src/utils/copy.ts
12699
13684
  init_logger();
@@ -12861,14 +13846,57 @@ function detectStack(repoRoot) {
12861
13846
  };
12862
13847
  }
12863
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
+
12864
13892
  // src/commands/init.ts
12865
13893
  init_mcp_hint();
12866
13894
  function readCondaEnvName(cwd) {
12867
- const envYml = join5(cwd, "environment.yml");
12868
- if (!existsSync4(envYml))
13895
+ const envYml = join8(cwd, "environment.yml");
13896
+ if (!existsSync7(envYml))
12869
13897
  return "base";
12870
13898
  try {
12871
- const content = readFileSync3(envYml, "utf8");
13899
+ const content = readFileSync6(envYml, "utf8");
12872
13900
  const match = content.match(/^name:\s*(.+)$/m);
12873
13901
  return match ? match[1].trim() : "base";
12874
13902
  } catch {
@@ -12876,11 +13904,11 @@ function readCondaEnvName(cwd) {
12876
13904
  }
12877
13905
  }
12878
13906
  function loadCiTemplate(stack) {
12879
- const templatePath = join5(ASSETS_DIR, "ci-templates", `${stack}.yml`);
12880
- if (!existsSync4(templatePath))
13907
+ const templatePath = join8(ASSETS_DIR, "ci-templates", `${stack}.yml`);
13908
+ if (!existsSync7(templatePath))
12881
13909
  return null;
12882
13910
  try {
12883
- return readFileSync3(templatePath, "utf8");
13911
+ return readFileSync6(templatePath, "utf8");
12884
13912
  } catch {
12885
13913
  return null;
12886
13914
  }
@@ -12916,6 +13944,7 @@ async function init(opts) {
12916
13944
  }
12917
13945
  const cwd = process.cwd();
12918
13946
  const createdPaths = [];
13947
+ const restoreActions = [];
12919
13948
  const installClaude = opts.provider === "claude" || opts.provider === "both";
12920
13949
  const installCodex = opts.provider === "codex" || opts.provider === "both";
12921
13950
  function track(paths) {
@@ -12931,6 +13960,13 @@ async function init(opts) {
12931
13960
  log.warn(`could not remove: ${p}`);
12932
13961
  }
12933
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
+ }
12934
13970
  }
12935
13971
  log.blank();
12936
13972
  log.info("Initialising contract-driven-delivery kit\u2026");
@@ -12944,9 +13980,9 @@ async function init(opts) {
12944
13980
  const skillDirs = readdirSync2(ASSET.skills, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name);
12945
13981
  let totalSkillFiles = 0;
12946
13982
  for (const skillName of skillDirs) {
12947
- const skillDest = join5(SKILLS_HOME, skillName);
13983
+ const skillDest = join8(SKILLS_HOME, skillName);
12948
13984
  log.info(`Installing skill \u2192 ${skillDest}`);
12949
- const { count, created } = copyDirTracked(join5(ASSET.skills, skillName), skillDest, { overwrite: true });
13985
+ const { count, created } = copyDirTracked(join8(ASSET.skills, skillName), skillDest, { overwrite: true });
12950
13986
  track(created);
12951
13987
  totalSkillFiles += count;
12952
13988
  }
@@ -12960,44 +13996,44 @@ async function init(opts) {
12960
13996
  log.info(`Scaffolding project files in ${cwd}`);
12961
13997
  const { count: contractsCount, created: contractsCreated } = copyDirTracked(
12962
13998
  ASSET.contracts,
12963
- join5(cwd, "contracts"),
13999
+ join8(cwd, "contracts"),
12964
14000
  { overwrite: opts.force, label: "contracts" }
12965
14001
  );
12966
14002
  track(contractsCreated);
12967
14003
  log.ok(`contracts/ \u2014 ${contractsCount} file(s) written.`);
12968
14004
  const { count: specsCount, created: specsCreated } = copyDirTracked(
12969
14005
  ASSET.specsTemplates,
12970
- join5(cwd, "specs", "templates"),
14006
+ join8(cwd, "specs", "templates"),
12971
14007
  { overwrite: opts.force, label: "specs/templates" }
12972
14008
  );
12973
14009
  track(specsCreated);
12974
14010
  log.ok(`specs/templates/ \u2014 ${specsCount} file(s) written.`);
12975
14011
  const { count: testsCount, created: testsCreated } = copyDirTracked(
12976
14012
  ASSET.testsTemplates,
12977
- join5(cwd, "tests", "templates"),
14013
+ join8(cwd, "tests", "templates"),
12978
14014
  { overwrite: opts.force, label: "tests/templates" }
12979
14015
  );
12980
14016
  track(testsCreated);
12981
14017
  log.ok(`tests/templates/ \u2014 ${testsCount} file(s) written.`);
12982
14018
  const { count: ciCount, created: ciCreated } = copyDirTracked(
12983
14019
  ASSET.ci,
12984
- join5(cwd, "ci"),
14020
+ join8(cwd, "ci"),
12985
14021
  { overwrite: opts.force, label: "ci" }
12986
14022
  );
12987
14023
  track(ciCreated);
12988
14024
  log.ok(`ci/ \u2014 ${ciCount} file(s) written.`);
12989
14025
  const { count: cddConfigCount, created: cddConfigCreated } = copyDirTracked(
12990
14026
  ASSET.cddConfig,
12991
- join5(cwd, ".cdd"),
14027
+ join8(cwd, ".cdd"),
12992
14028
  { overwrite: opts.force, label: ".cdd" }
12993
14029
  );
12994
14030
  track(cddConfigCreated);
12995
14031
  log.ok(`.cdd/ - ${cddConfigCount} file(s) written.`);
12996
- const modelPolicyPath = join5(cwd, ".cdd", "model-policy.json");
12997
- if (existsSync4(modelPolicyPath)) {
14032
+ const modelPolicyPath = join8(cwd, ".cdd", "model-policy.json");
14033
+ if (existsSync7(modelPolicyPath)) {
12998
14034
  let existing = {};
12999
14035
  try {
13000
- existing = JSON.parse(readFileSync3(modelPolicyPath, "utf8"));
14036
+ existing = JSON.parse(readFileSync6(modelPolicyPath, "utf8"));
13001
14037
  } catch {
13002
14038
  }
13003
14039
  const merged = {
@@ -13006,11 +14042,11 @@ async function init(opts) {
13006
14042
  generated_at: (/* @__PURE__ */ new Date()).toISOString(),
13007
14043
  roles: existing.roles && typeof existing.roles === "object" && Object.keys(existing.roles).length > 0 ? existing.roles : {}
13008
14044
  };
13009
- writeFileSync2(modelPolicyPath, JSON.stringify(merged, null, 2) + "\n", "utf8");
14045
+ writeFileSync5(modelPolicyPath, JSON.stringify(merged, null, 2) + "\n", "utf8");
13010
14046
  }
13011
14047
  const { count: wfCount, created: wfCreated } = copyDirTracked(
13012
14048
  ASSET.githubWorkflows,
13013
- join5(cwd, ".github", "workflows"),
14049
+ join8(cwd, ".github", "workflows"),
13014
14050
  { overwrite: opts.force, label: ".github/workflows" }
13015
14051
  );
13016
14052
  track(wfCreated);
@@ -13034,11 +14070,11 @@ async function init(opts) {
13034
14070
  } else {
13035
14071
  log.warn("Could not detect stack \u2014 CI placeholder left in place.");
13036
14072
  }
13037
- const ciYmlDest = join5(cwd, ".github", "workflows", "contract-driven-gates.yml");
13038
- if (existsSync4(ciYmlDest)) {
14073
+ const ciYmlDest = join8(cwd, ".github", "workflows", "contract-driven-gates.yml");
14074
+ if (existsSync7(ciYmlDest)) {
13039
14075
  const template = loadCiTemplate(detection.primary);
13040
14076
  if (template) {
13041
- const original = readFileSync3(ciYmlDest, "utf8");
14077
+ const original = readFileSync6(ciYmlDest, "utf8");
13042
14078
  let patched = patchFastGateYml(original, template, detection.primary);
13043
14079
  if (detection.primary === "conda" && patched.includes("{{conda-env-name}}")) {
13044
14080
  const envName = readCondaEnvName(cwd);
@@ -13046,39 +14082,52 @@ async function init(opts) {
13046
14082
  log.ok(`Conda environment name set to: ${envName}`);
13047
14083
  }
13048
14084
  if (patched !== original) {
13049
- writeFileSync2(ciYmlDest, patched, "utf8");
14085
+ writeFileSync5(ciYmlDest, patched, "utf8");
13050
14086
  log.ok(`CI fast-gate patched for stack: ${detection.primary}`);
13051
14087
  }
13052
14088
  }
13053
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
+ }
13054
14103
  if (installClaude) {
13055
14104
  const { written: claudeWritten, created: claudeCreated } = copyFileTracked(
13056
14105
  ASSET.claudeTemplate,
13057
- join5(cwd, "CLAUDE.md"),
14106
+ join8(cwd, "CLAUDE.md"),
13058
14107
  { overwrite: false, label: "CLAUDE.md" }
13059
14108
  );
13060
14109
  if (claudeCreated)
13061
- track([join5(cwd, "CLAUDE.md")]);
14110
+ track([join8(cwd, "CLAUDE.md")]);
13062
14111
  if (claudeWritten)
13063
14112
  log.ok("CLAUDE.md created.");
13064
14113
  const { written: agentsWritten, created: agentsCreated } = copyFileTracked(
13065
14114
  ASSET.agentsTemplate,
13066
- join5(cwd, "AGENTS.md"),
14115
+ join8(cwd, "AGENTS.md"),
13067
14116
  { overwrite: false, label: "AGENTS.md" }
13068
14117
  );
13069
14118
  if (agentsCreated)
13070
- track([join5(cwd, "AGENTS.md")]);
14119
+ track([join8(cwd, "AGENTS.md")]);
13071
14120
  if (agentsWritten)
13072
14121
  log.ok("AGENTS.md created.");
13073
14122
  }
13074
14123
  if (installCodex) {
13075
14124
  const { written: codexWritten, created: codexCreated } = copyFileTracked(
13076
14125
  ASSET.codexTemplate,
13077
- join5(cwd, "CODEX.md"),
14126
+ join8(cwd, "CODEX.md"),
13078
14127
  { overwrite: false, label: "CODEX.md" }
13079
14128
  );
13080
14129
  if (codexCreated)
13081
- track([join5(cwd, "CODEX.md")]);
14130
+ track([join8(cwd, "CODEX.md")]);
13082
14131
  if (codexWritten)
13083
14132
  log.ok("CODEX.md created.");
13084
14133
  }
@@ -13086,6 +14135,16 @@ async function init(opts) {
13086
14135
  const { installCodeMapHook: installCodeMapHook2 } = await Promise.resolve().then(() => (init_code_map_hook(), code_map_hook_exports));
13087
14136
  await installCodeMapHook2(cwd);
13088
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
+ }
13089
14148
  log.blank();
13090
14149
  }
13091
14150
  } catch (err) {
@@ -13109,9 +14168,9 @@ init_update();
13109
14168
 
13110
14169
  // src/commands/new-change.ts
13111
14170
  init_paths();
13112
- import { join as join9, relative as relative3 } from "path";
14171
+ import { join as join12, relative as relative3 } from "path";
13113
14172
  import { createHash as createHash4 } from "crypto";
13114
- 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";
13115
14174
  init_logger();
13116
14175
  init_context_scan();
13117
14176
  init_digest();
@@ -13123,10 +14182,10 @@ function inputsDigest2(paths, cwd) {
13123
14182
  return createHash4("sha256").update(combined).digest("hex");
13124
14183
  }
13125
14184
  function findContractFiles2(dir, found = []) {
13126
- if (!existsSync8(dir))
14185
+ if (!existsSync11(dir))
13127
14186
  return found;
13128
14187
  for (const entry of readdirSync5(dir, { withFileTypes: true })) {
13129
- const fullPath = join9(dir, entry.name);
14188
+ const fullPath = join12(dir, entry.name);
13130
14189
  if (entry.isDirectory())
13131
14190
  findContractFiles2(fullPath, found);
13132
14191
  else if (entry.isFile() && entry.name.endsWith(".md") && entry.name !== "INDEX.md" && entry.name !== "CHANGELOG.md") {
@@ -13136,22 +14195,22 @@ function findContractFiles2(dir, found = []) {
13136
14195
  return found;
13137
14196
  }
13138
14197
  function readIndexDigest(filePath) {
13139
- if (!existsSync8(filePath))
14198
+ if (!existsSync11(filePath))
13140
14199
  return null;
13141
- 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);
13142
14201
  return m ? m[1] : null;
13143
14202
  }
13144
14203
  async function ensureFreshContextIndexes(cwd) {
13145
- const projectMap = join9(cwd, "specs", "context", "project-map.md");
13146
- const contractsIndex = join9(cwd, "specs", "context", "contracts-index.md");
13147
- const policyPath = join9(cwd, ".cdd", "context-policy.json");
13148
- const policyInputs = [policyPath].filter(existsSync8);
13149
- 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"));
13150
14209
  const wantProjectDigest = inputsDigest2(policyInputs, cwd);
13151
14210
  const wantContractsDigest = inputsDigest2(contractFiles, cwd);
13152
14211
  const haveProjectDigest = readIndexDigest(projectMap);
13153
14212
  const haveContractsDigest = readIndexDigest(contractsIndex);
13154
- const needsScan = !existsSync8(projectMap) || !existsSync8(contractsIndex) || haveProjectDigest !== wantProjectDigest || haveContractsDigest !== wantContractsDigest;
14213
+ const needsScan = !existsSync11(projectMap) || !existsSync11(contractsIndex) || haveProjectDigest !== wantProjectDigest || haveContractsDigest !== wantContractsDigest;
13155
14214
  if (!needsScan)
13156
14215
  return;
13157
14216
  log.info("context indexes missing or stale \u2014 running cdd-kit context-scan\u2026");
@@ -13182,9 +14241,9 @@ function parseDependsOn(raw) {
13182
14241
  return raw.split(",").map((item) => item.trim()).filter(Boolean);
13183
14242
  }
13184
14243
  function applyScaffoldMetadata(tasksPath, changeId, dependencies) {
13185
- if (!existsSync8(tasksPath))
14244
+ if (!existsSync11(tasksPath))
13186
14245
  return;
13187
- let raw = readFileSync8(tasksPath, "utf8");
14246
+ let raw = readFileSync11(tasksPath, "utf8");
13188
14247
  raw = raw.replace(/^change-id:\s*<change-id>\s*$/m, `change-id: ${changeId}`);
13189
14248
  if (dependencies.length > 0) {
13190
14249
  const dependsOn = [
@@ -13193,7 +14252,7 @@ function applyScaffoldMetadata(tasksPath, changeId, dependencies) {
13193
14252
  ].join("\n");
13194
14253
  raw = raw.replace(/^depends-on:\s*\[\]\s*$/m, dependsOn);
13195
14254
  }
13196
- writeFileSync4(tasksPath, raw, "utf8");
14255
+ writeFileSync7(tasksPath, raw, "utf8");
13197
14256
  }
13198
14257
  async function newChange(name, opts) {
13199
14258
  if (!SAFE_NAME.test(name)) {
@@ -13208,8 +14267,8 @@ async function newChange(name, opts) {
13208
14267
  }
13209
14268
  }
13210
14269
  const cwd = process.cwd();
13211
- const changeDir = join9(cwd, "specs", "changes", name);
13212
- if (existsSync8(changeDir)) {
14270
+ const changeDir = join12(cwd, "specs", "changes", name);
14271
+ if (existsSync11(changeDir)) {
13213
14272
  if (opts.force) {
13214
14273
  log.warn(`Forcing re-scaffold of existing change directory: ${changeDir}`);
13215
14274
  log.warn("Existing files will NOT be deleted; only template files will be overwritten.");
@@ -13232,9 +14291,9 @@ async function newChange(name, opts) {
13232
14291
  const templates = opts.all ? [...REQUIRED_TEMPLATES, ...listOptional()] : [...REQUIRED_TEMPLATES];
13233
14292
  let written = 0;
13234
14293
  for (const tmpl of templates) {
13235
- const src = join9(ASSET.specsTemplates, tmpl);
13236
- const dest = join9(changeDir, tmpl);
13237
- if (!existsSync8(src)) {
14294
+ const src = join12(ASSET.specsTemplates, tmpl);
14295
+ const dest = join12(changeDir, tmpl);
14296
+ if (!existsSync11(src)) {
13238
14297
  log.warn(`Template not found, skipping: ${tmpl}`);
13239
14298
  continue;
13240
14299
  }
@@ -13242,7 +14301,7 @@ async function newChange(name, opts) {
13242
14301
  log.dim(tmpl);
13243
14302
  written += 1;
13244
14303
  }
13245
- const tasksPath = join9(changeDir, "tasks.yml");
14304
+ const tasksPath = join12(changeDir, "tasks.yml");
13246
14305
  applyScaffoldMetadata(tasksPath, name, dependencies);
13247
14306
  if (dependencies.length > 0) {
13248
14307
  log.dim(`depends-on: ${dependencies.join(", ")}`);
@@ -13255,9 +14314,9 @@ async function newChange(name, opts) {
13255
14314
  // src/commands/validate.ts
13256
14315
  init_paths();
13257
14316
  init_logger();
13258
- import { join as join10 } from "path";
13259
- import { existsSync as existsSync9 } from "fs";
13260
- 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";
13261
14320
  var VALIDATORS = [
13262
14321
  {
13263
14322
  flag: "contracts",
@@ -13265,6 +14324,7 @@ var VALIDATORS = [
13265
14324
  label: "contracts",
13266
14325
  chain: [
13267
14326
  { script: "validate_api_semantic.py", label: "API semantic" },
14327
+ { script: "validate_api_conformance.py", label: "API conformance" },
13268
14328
  { script: "validate_env_semantic.py", label: "Env semantic" }
13269
14329
  ]
13270
14330
  },
@@ -13275,7 +14335,7 @@ var VALIDATORS = [
13275
14335
  ];
13276
14336
  function resolvePython() {
13277
14337
  for (const cmd of ["python3", "python"]) {
13278
- const r = spawnSync(cmd, ["--version"], { stdio: "ignore" });
14338
+ const r = spawnSync2(cmd, ["--version"], { stdio: "ignore" });
13279
14339
  if (r.status === 0)
13280
14340
  return cmd;
13281
14341
  }
@@ -13289,21 +14349,22 @@ async function validate(opts) {
13289
14349
  log.error(e instanceof Error ? e.message : String(e));
13290
14350
  process.exit(1);
13291
14351
  }
13292
- const scriptsDir = join10(ASSET.skill, "scripts");
14352
+ const scriptsDir = join13(ASSET.skill, "scripts");
13293
14353
  const runAll = !opts.contracts && !opts.env && !opts.ci && !opts.spec && !opts.versions;
14354
+ const pyEnv = { ...process.env, PYTHONUTF8: "1", PYTHONIOENCODING: "utf-8" };
13294
14355
  log.blank();
13295
14356
  let failed = false;
13296
14357
  for (const v of VALIDATORS) {
13297
14358
  if (!runAll && !opts[v.flag])
13298
14359
  continue;
13299
- const scriptPath = join10(scriptsDir, v.script);
13300
- if (!existsSync9(scriptPath)) {
14360
+ const scriptPath = join13(scriptsDir, v.script);
14361
+ if (!existsSync12(scriptPath)) {
13301
14362
  log.warn(`${v.label}: script not found, skipping (${v.script})`);
13302
14363
  log.blank();
13303
14364
  continue;
13304
14365
  }
13305
14366
  log.info(`Validating ${v.label}\u2026`);
13306
- 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 });
13307
14368
  if (r.status !== 0) {
13308
14369
  log.error(`${v.label} validation failed.`);
13309
14370
  failed = true;
@@ -13313,14 +14374,14 @@ async function validate(opts) {
13313
14374
  log.blank();
13314
14375
  if (v.chain) {
13315
14376
  for (const chained of v.chain) {
13316
- const chainedPath = join10(scriptsDir, chained.script);
13317
- if (!existsSync9(chainedPath)) {
14377
+ const chainedPath = join13(scriptsDir, chained.script);
14378
+ if (!existsSync12(chainedPath)) {
13318
14379
  log.warn(`${chained.label}: script not found, skipping (${chained.script})`);
13319
14380
  log.blank();
13320
14381
  continue;
13321
14382
  }
13322
14383
  log.info(`Validating ${chained.label}\u2026`);
13323
- const cr = spawnSync(py, [chainedPath], { stdio: "inherit", cwd: process.cwd() });
14384
+ const cr = spawnSync2(py, [chainedPath], { stdio: "inherit", cwd: process.cwd(), env: pyEnv });
13324
14385
  if (cr.status !== 0) {
13325
14386
  log.error(`${chained.label} validation failed.`);
13326
14387
  failed = true;
@@ -13344,8 +14405,8 @@ async function validate(opts) {
13344
14405
  var import_ajv = __toESM(require_ajv(), 1);
13345
14406
  var import_ajv_formats = __toESM(require_dist(), 1);
13346
14407
  init_logger();
13347
- import { existsSync as existsSync10, readFileSync as readFileSync9, readdirSync as readdirSync6 } from "fs";
13348
- 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";
13349
14410
  import yaml from "js-yaml";
13350
14411
 
13351
14412
  // src/schemas/tasks.schema.ts
@@ -13374,6 +14435,7 @@ var tasksSchema = {
13374
14435
  items: { type: "string", pattern: "^[a-zA-Z0-9][a-zA-Z0-9_-]{0,63}$" },
13375
14436
  default: []
13376
14437
  },
14438
+ "tier-floor-override": { type: "string", minLength: 1 },
13377
14439
  "token-budget": { type: ["string", "number"] },
13378
14440
  created: { type: "string" },
13379
14441
  completed: { type: "string" },
@@ -13396,6 +14458,8 @@ var tasksSchema = {
13396
14458
  };
13397
14459
 
13398
14460
  // src/commands/gate.ts
14461
+ init_tier_floor();
14462
+ init_git_paths();
13399
14463
  var ajv = new import_ajv.default({ allErrors: true, allowUnionTypes: true });
13400
14464
  (0, import_ajv_formats.default)(ajv);
13401
14465
  var validateTasks = ajv.compile(tasksSchema);
@@ -13433,6 +14497,15 @@ function meaningfulChars(text) {
13433
14497
  function stripHtmlComments(text) {
13434
14498
  return text.replace(/<!--[\s\S]*?-->/g, "");
13435
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
+ }
13436
14509
  function countPendingExpansions(sectionBody2) {
13437
14510
  if (!sectionBody2.trim())
13438
14511
  return 0;
@@ -13454,7 +14527,7 @@ function countPendingContextRequests(content) {
13454
14527
  }
13455
14528
  function loadYamlFile(path) {
13456
14529
  try {
13457
- const raw = readFileSync9(path, "utf8");
14530
+ const raw = readFileSync13(path, "utf8");
13458
14531
  return { data: yaml.load(raw, { schema: yaml.JSON_SCHEMA }), parseError: null };
13459
14532
  } catch (err) {
13460
14533
  return { data: null, parseError: err.message };
@@ -13485,8 +14558,8 @@ function ajvErrorsToMessages(errors, prefix, knownKeys) {
13485
14558
  return out;
13486
14559
  }
13487
14560
  function isContextGovernedChange(changeDir) {
13488
- const tasksPath = join11(changeDir, "tasks.yml");
13489
- if (!existsSync10(tasksPath))
14561
+ const tasksPath = join15(changeDir, "tasks.yml");
14562
+ if (!existsSync14(tasksPath))
13490
14563
  return false;
13491
14564
  const { data } = loadYamlFile(tasksPath);
13492
14565
  return data?.["context-governance"] === "v1";
@@ -13514,12 +14587,12 @@ function lintTasksFile(tasksPath, errors, warnings) {
13514
14587
  return data;
13515
14588
  }
13516
14589
  function resolveTier(changeDir) {
13517
- const classifPath = join11(changeDir, "change-classification.md");
13518
- const classificationPresent = existsSync10(classifPath);
13519
- 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") : "";
13520
14593
  const classificationHasLooseMarker = classificationPresent && TIER_PATTERN.test(classificationText);
13521
- const tasksPath = join11(changeDir, "tasks.yml");
13522
- if (existsSync10(tasksPath)) {
14594
+ const tasksPath = join15(changeDir, "tasks.yml");
14595
+ if (existsSync14(tasksPath)) {
13523
14596
  const { data } = loadYamlFile(tasksPath);
13524
14597
  const t = data?.tier;
13525
14598
  if (typeof t === "number" && t >= 0 && t <= 5) {
@@ -13565,7 +14638,7 @@ function enforceTierConsistency(changeDir, errors, warnings) {
13565
14638
  return;
13566
14639
  }
13567
14640
  if (resolution.source === "tasks-frontmatter" && resolution.classificationPresent) {
13568
- const text = readFileSync9(join11(changeDir, "change-classification.md"), "utf8");
14641
+ const text = readFileSync13(join15(changeDir, "change-classification.md"), "utf8");
13569
14642
  const structured = text.match(/^## Tier\s*\n\s*-\s*(\d)\s*$/m);
13570
14643
  const bold = text.match(/\*\*Tier:\*\*\s*Tier\s*(\d)\b/i);
13571
14644
  const classifTier = structured ? parseInt(structured[1], 10) : bold ? parseInt(bold[1], 10) : NaN;
@@ -13576,12 +14649,50 @@ function enforceTierConsistency(changeDir, errors, warnings) {
13576
14649
  }
13577
14650
  }
13578
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
+ }
13579
14690
  function isArchivedChange(cwd, changeId) {
13580
- const archiveRoot = join11(cwd, "specs", "archive");
13581
- if (!existsSync10(archiveRoot))
14691
+ const archiveRoot = join15(cwd, "specs", "archive");
14692
+ if (!existsSync14(archiveRoot))
13582
14693
  return false;
13583
14694
  const years = readdirSync6(archiveRoot, { withFileTypes: true }).filter((d) => d.isDirectory());
13584
- return years.some((year) => existsSync10(join11(archiveRoot, year.name, changeId)));
14695
+ return years.some((year) => existsSync14(join15(archiveRoot, year.name, changeId)));
13585
14696
  }
13586
14697
  function detectDependencyCycle(cwd, startChangeId) {
13587
14698
  const visited = /* @__PURE__ */ new Set();
@@ -13594,8 +14705,8 @@ function detectDependencyCycle(cwd, startChangeId) {
13594
14705
  return null;
13595
14706
  visited.add(id);
13596
14707
  stack.push(id);
13597
- const tasksPath = join11(cwd, "specs", "changes", id, "tasks.yml");
13598
- if (existsSync10(tasksPath)) {
14708
+ const tasksPath = join15(cwd, "specs", "changes", id, "tasks.yml");
14709
+ if (existsSync14(tasksPath)) {
13599
14710
  const { data } = loadYamlFile(tasksPath);
13600
14711
  const deps = data?.["depends-on"] ?? [];
13601
14712
  for (const dep of deps) {
@@ -13610,8 +14721,8 @@ function detectDependencyCycle(cwd, startChangeId) {
13610
14721
  return visit(startChangeId);
13611
14722
  }
13612
14723
  function validateDependencies(cwd, changeId, changeDir) {
13613
- const tasksPath = join11(changeDir, "tasks.yml");
13614
- if (!existsSync10(tasksPath))
14724
+ const tasksPath = join15(changeDir, "tasks.yml");
14725
+ if (!existsSync14(tasksPath))
13615
14726
  return [];
13616
14727
  const { data } = loadYamlFile(tasksPath);
13617
14728
  const dependencies = data?.["depends-on"] ?? [];
@@ -13625,10 +14736,10 @@ function validateDependencies(cwd, changeId, changeDir) {
13625
14736
  errors.push(`tasks.yml: change cannot depend on itself (${dep})`);
13626
14737
  continue;
13627
14738
  }
13628
- const upstreamDir = join11(cwd, "specs", "changes", dep);
13629
- if (existsSync10(upstreamDir)) {
13630
- const upstreamTasks = join11(upstreamDir, "tasks.yml");
13631
- 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)) {
13632
14743
  errors.push(`dependency ${dep}: missing tasks.yml`);
13633
14744
  continue;
13634
14745
  }
@@ -13648,19 +14759,19 @@ function validateDependencies(cwd, changeId, changeDir) {
13648
14759
  async function gate(changeId, opts = {}) {
13649
14760
  const strict = opts.strict ?? false;
13650
14761
  const cwd = process.cwd();
13651
- const changeDir = join11(cwd, "specs", "changes", changeId);
13652
- if (!existsSync10(changeDir)) {
14762
+ const changeDir = join15(cwd, "specs", "changes", changeId);
14763
+ if (!existsSync14(changeDir)) {
13653
14764
  log.error(`change not found: ${changeId} (looked in ${changeDir})`);
13654
14765
  process.exit(1);
13655
14766
  }
13656
14767
  const errors = [];
13657
14768
  const warnings = [];
13658
14769
  const isNewChange = isContextGovernedChange(changeDir);
13659
- const manifestPath = join11(changeDir, "context-manifest.md");
13660
- const hasManifest = existsSync10(manifestPath);
14770
+ const manifestPath = join15(changeDir, "context-manifest.md");
14771
+ const hasManifest = existsSync14(manifestPath);
13661
14772
  errors.push(...validateDependencies(cwd, changeId, changeDir));
13662
14773
  if (hasManifest) {
13663
- const pending = countPendingContextRequests(readFileSync9(manifestPath, "utf8"));
14774
+ const pending = countPendingContextRequests(readFileSync13(manifestPath, "utf8"));
13664
14775
  if (pending > 0) {
13665
14776
  warnings.push(`context-manifest.md: has ${pending} pending context expansion request(s)`);
13666
14777
  }
@@ -13676,7 +14787,7 @@ async function gate(changeId, opts = {}) {
13676
14787
  }
13677
14788
  continue;
13678
14789
  }
13679
- if (!existsSync10(join11(changeDir, f))) {
14790
+ if (!existsSync14(join15(changeDir, f))) {
13680
14791
  errors.push(`missing required artifact: ${f}`);
13681
14792
  }
13682
14793
  }
@@ -13686,21 +14797,30 @@ async function gate(changeId, opts = {}) {
13686
14797
  continue;
13687
14798
  if (f === "tasks.yml")
13688
14799
  continue;
13689
- const content = readFileSync9(join11(changeDir, f), "utf8");
14800
+ const content = readFileSync13(join15(changeDir, f), "utf8");
13690
14801
  const minChars = MIN_CHARS[f] ?? 100;
13691
14802
  if (meaningfulChars(content) < minChars) {
13692
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
+ }
13693
14813
  }
13694
14814
  }
13695
- const classifPath = join11(changeDir, "change-classification.md");
14815
+ const classifPath = join15(changeDir, "change-classification.md");
13696
14816
  const tierResolution = resolveTier(changeDir);
13697
- if (tierResolution.tier === null && existsSync10(classifPath) && !tierResolution.classificationHasLooseMarker) {
14817
+ if (tierResolution.tier === null && existsSync14(classifPath) && !tierResolution.classificationHasLooseMarker) {
13698
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)");
13699
14819
  }
13700
14820
  }
13701
- const tasksPath = join11(changeDir, "tasks.yml");
14821
+ const tasksPath = join15(changeDir, "tasks.yml");
13702
14822
  let tasksData = null;
13703
- if (existsSync10(tasksPath)) {
14823
+ if (existsSync14(tasksPath)) {
13704
14824
  tasksData = lintTasksFile(tasksPath, errors, warnings);
13705
14825
  }
13706
14826
  if (tasksData) {
@@ -13715,6 +14835,7 @@ async function gate(changeId, opts = {}) {
13715
14835
  }
13716
14836
  }
13717
14837
  enforceTierConsistency(changeDir, errors, warnings);
14838
+ enforceTierFloor(changeDir, errors, warnings);
13718
14839
  for (const w of warnings) {
13719
14840
  log.warn(` ${w}`);
13720
14841
  }
@@ -13738,75 +14859,540 @@ async function gate(changeId, opts = {}) {
13738
14859
  log.ok(`gate passed for change: ${changeId}`);
13739
14860
  }
13740
14861
 
13741
- // src/commands/install-hooks.ts
13742
- init_paths();
14862
+ // src/cli/index.ts
14863
+ init_install_hooks();
14864
+ init_install_agent_hooks();
14865
+
14866
+ // src/commands/openapi-export.ts
13743
14867
  init_logger();
13744
- import { existsSync as existsSync11, readFileSync as readFileSync10, writeFileSync as writeFileSync5, chmodSync as chmodSync2, mkdirSync as mkdirSync5 } from "fs";
13745
- import { join as join12 } from "path";
13746
- var START_MARKER2 = "# cdd-kit-managed-block-start";
13747
- var END_MARKER2 = "# cdd-kit-managed-block-end";
13748
- async function installHooks() {
13749
- const cwd = process.cwd();
13750
- const gitDir = join12(cwd, ".git");
13751
- if (!existsSync11(gitDir)) {
13752
- log.error("not a git repository (no .git/ found in cwd)");
13753
- 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
+ }
13754
14885
  }
13755
- const hooksDir = join12(gitDir, "hooks");
13756
- mkdirSync5(hooksDir, { recursive: true });
13757
- const dest = join12(hooksDir, "pre-commit");
13758
- const ourHook = readFileSync10(join12(ASSET.hooks, "pre-commit"), "utf8");
13759
- let final;
13760
- if (!existsSync11(dest)) {
13761
- final = ourHook;
13762
- } else {
13763
- const existing = readFileSync10(dest, "utf8");
13764
- const startIdx = existing.indexOf(START_MARKER2);
13765
- const endIdx = existing.indexOf(END_MARKER2);
13766
- if (startIdx >= 0 && endIdx > startIdx) {
13767
- const before = existing.slice(0, startIdx);
13768
- const after = existing.slice(endIdx + END_MARKER2.length);
13769
- const ourStart = ourHook.indexOf(START_MARKER2);
13770
- const ourEnd = ourHook.indexOf(END_MARKER2) + END_MARKER2.length;
13771
- const ourBlock = ourHook.slice(ourStart, ourEnd);
13772
- final = before + ourBlock + after;
13773
- } else {
13774
- const ourStart = ourHook.indexOf(START_MARKER2);
13775
- const ourEnd = ourHook.indexOf(END_MARKER2) + END_MARKER2.length;
13776
- const ourBlock = ourHook.slice(ourStart, ourEnd);
13777
- if (existing.startsWith("#!")) {
13778
- const firstNewline = existing.indexOf("\n");
13779
- const shebang = existing.slice(0, firstNewline + 1);
13780
- const rest = existing.slice(firstNewline + 1);
13781
- 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`);
13782
14980
  } else {
13783
- final = "#!/bin/sh\n" + ourBlock + "\n" + existing;
14981
+ blocks.push(parsed);
13784
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})`);
13785
14986
  }
13786
14987
  }
13787
- writeFileSync5(dest, final, "utf8");
13788
- try {
13789
- chmodSync2(dest, 493);
13790
- } 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;
13791
14996
  }
13792
- log.ok(`pre-commit hook installed at ${dest}`);
13793
- 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;
13794
15379
  }
13795
15380
 
13796
15381
  // src/cli/index.ts
13797
- var __dirname2 = dirname7(fileURLToPath4(import.meta.url));
13798
- 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"));
13799
15384
  var program = new Command();
13800
15385
  program.name("cdd-kit").description("Contract-Driven Delivery Kit CLI").version(pkg.version);
13801
15386
  program.command("init").description(
13802
15387
  "Install agents/skill into ~/.claude and scaffold project files in cwd"
13803
- ).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(
13804
15389
  (opts) => init({
13805
15390
  globalOnly: opts.globalOnly,
13806
15391
  localOnly: opts.localOnly,
13807
15392
  force: opts.force,
13808
15393
  provider: opts.provider,
13809
- hooks: opts.hooks
15394
+ hooks: opts.hooks,
15395
+ arm: opts.arm !== false
13810
15396
  })
13811
15397
  );
13812
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 }));
@@ -13867,7 +15453,26 @@ function resolveWorkers(value) {
13867
15453
  return 1;
13868
15454
  return Math.min(n, 16);
13869
15455
  }
13870
- 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
+ }
13871
15476
  const { codeMap: codeMap2 } = await Promise.resolve().then(() => (init_code_map(), code_map_exports));
13872
15477
  const exit = await codeMap2({
13873
15478
  path: path ?? ".",
@@ -13887,13 +15492,15 @@ program.command("__code-map-scan", { hidden: true }).requiredOption("--lang <lan
13887
15492
  process.exit(exit);
13888
15493
  });
13889
15494
  var index = program.command("index").description("Query machine-readable project indexes before opening source files");
13890
- 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) => {
13891
15496
  const { indexQuery: indexQuery2 } = await Promise.resolve().then(() => (init_index_query(), index_query_exports));
13892
15497
  const exit = await indexQuery2(term, {
13893
15498
  map: opts.map,
13894
15499
  limit: parseInt(opts.limit, 10),
13895
15500
  json: opts.json === true,
13896
- refresh: opts.refresh !== false
15501
+ refresh: opts.refresh !== false,
15502
+ withSource: opts.withSource === true,
15503
+ sourceBudget: parseInt(opts.sourceBudget ?? "400", 10)
13897
15504
  });
13898
15505
  process.exit(exit);
13899
15506
  });
@@ -13918,14 +15525,16 @@ graph.command("sync [path]").description("Run CodeGraph incremental sync (requir
13918
15525
  const exit = await graphSync2({ path, engine: opts.engine, json: opts.json === true });
13919
15526
  process.exit(exit);
13920
15527
  });
13921
- 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) => {
13922
15529
  const { graphQuery: graphQuery2 } = await Promise.resolve().then(() => (init_graph(), graph_exports));
13923
15530
  const exit = await graphQuery2(term, {
13924
15531
  engine: opts.engine,
13925
15532
  map: opts.map,
13926
15533
  limit: parseInt(opts.limit, 10),
13927
15534
  json: opts.json === true,
13928
- refresh: opts.refresh !== false
15535
+ refresh: opts.refresh !== false,
15536
+ withSource: opts.withSource === true,
15537
+ sourceBudget: parseInt(opts.sourceBudget ?? "400", 10)
13929
15538
  });
13930
15539
  process.exit(exit);
13931
15540
  });
@@ -13952,6 +15561,11 @@ graph.command("context <task>").description("Build task context with native grap
13952
15561
  });
13953
15562
  process.exit(exit);
13954
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
+ });
13955
15569
  program.command("mcp").description("Run the cdd-kit MCP stdio server exposing graph and code-map tools").action(async () => {
13956
15570
  const { runMcpServer: runMcpServer2 } = await Promise.resolve().then(() => (init_server(), server_exports));
13957
15571
  await runMcpServer2({ version: pkg.version });
@@ -13983,6 +15597,19 @@ program.command("list").description("List active changes in specs/changes/").act
13983
15597
  program.command("install-hooks").description("Install pre-commit hook that runs cdd-kit gate on staged changes").action(async () => {
13984
15598
  await installHooks();
13985
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
+ });
13986
15613
  program.command("detect-stack").description("Detect the project tech stack and print the result").action(() => {
13987
15614
  const cwd = process.cwd();
13988
15615
  const result = detectStack(cwd);