contract-driven-delivery 2.0.7 → 2.0.8

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.
Files changed (3) hide show
  1. package/CHANGELOG.md +61 -0
  2. package/dist/cli/index.js +2255 -1920
  3. package/package.json +1 -1
package/dist/cli/index.js CHANGED
@@ -139,6 +139,13 @@ ${CODE_MAP_BLOCK}
139
139
  chmodSync(dest, 493);
140
140
  } catch {
141
141
  }
142
+ try {
143
+ const cddDir = join4(cwd, ".cdd");
144
+ mkdirSync2(cddDir, { recursive: true });
145
+ writeFileSync(join4(cddDir, ".hooks-installed"), `installed: ${(/* @__PURE__ */ new Date()).toISOString()}
146
+ `, "utf8");
147
+ } catch {
148
+ }
142
149
  log.ok(`code-map pre-commit hook installed at ${dest}`);
143
150
  }
144
151
  var START_MARKER, END_MARKER, CODE_MAP_BLOCK;
@@ -151,7 +158,8 @@ var init_code_map_hook = __esm({
151
158
  CODE_MAP_BLOCK = `${START_MARKER}
152
159
  # Auto-regenerates .cdd/code-map.yml when source files are staged.
153
160
  # Generated by: cdd-kit init --hooks (re-run to update).
154
- staged_src=$(git diff --cached --name-only --diff-filter=ACM | grep -E '\\.(py|js|vue)$' || true)
161
+ # Extension list mirrors src/code-map/config.ts BUILTIN_INCLUDE.
162
+ staged_src=$(git diff --cached --name-only --diff-filter=ACM | grep -E '\\.(py|js|jsx|mjs|cjs|ts|tsx|vue)$' || true)
155
163
  if [ -n "$staged_src" ]; then
156
164
  if cdd-kit code-map --check >/dev/null 2>&1; then
157
165
  : # already up to date
@@ -201,6 +209,173 @@ var init_provider = __esm({
201
209
  }
202
210
  });
203
211
 
212
+ // src/commands/update.ts
213
+ import { join as join7 } from "path";
214
+ import { existsSync as existsSync6, mkdirSync as mkdirSync3, readdirSync as readdirSync3, copyFileSync as copyFileSync2, readFileSync as readFileSync5 } from "fs";
215
+ import { createHash } from "crypto";
216
+ import { homedir as homedir2 } from "os";
217
+ function fileHash(filePath) {
218
+ const buf = readFileSync5(filePath);
219
+ return createHash("sha256").update(buf).digest("hex");
220
+ }
221
+ function diffDir(src, dest) {
222
+ const entries = [];
223
+ if (!existsSync6(src))
224
+ return entries;
225
+ function walk(currentSrc, currentDest) {
226
+ const items = readdirSync3(currentSrc, { withFileTypes: true });
227
+ for (const item of items) {
228
+ const srcPath = join7(currentSrc, item.name);
229
+ const destPath = join7(currentDest, item.name);
230
+ if (item.isDirectory()) {
231
+ walk(srcPath, destPath);
232
+ } else {
233
+ if (!existsSync6(destPath)) {
234
+ entries.push({ src: srcPath, dest: destPath, action: "add" });
235
+ } else if (fileHash(srcPath) !== fileHash(destPath)) {
236
+ entries.push({ src: srcPath, dest: destPath, action: "overwrite" });
237
+ } else {
238
+ entries.push({ src: srcPath, dest: destPath, action: "skip" });
239
+ }
240
+ }
241
+ }
242
+ }
243
+ walk(src, dest);
244
+ return entries;
245
+ }
246
+ function applyDir(entries) {
247
+ let count = 0;
248
+ for (const e of entries) {
249
+ if (e.action === "skip")
250
+ continue;
251
+ mkdirSync3(join7(e.dest, ".."), { recursive: true });
252
+ copyFileSync2(e.src, e.dest);
253
+ count += 1;
254
+ }
255
+ return count;
256
+ }
257
+ function backupDir(dir, backupDest) {
258
+ if (!existsSync6(dir))
259
+ return;
260
+ mkdirSync3(backupDest, { recursive: true });
261
+ function walk(src, dst) {
262
+ const items = readdirSync3(src, { withFileTypes: true });
263
+ for (const item of items) {
264
+ const s = join7(src, item.name);
265
+ const d = join7(dst, item.name);
266
+ if (item.isDirectory()) {
267
+ mkdirSync3(d, { recursive: true });
268
+ walk(s, d);
269
+ } else
270
+ copyFileSync2(s, d);
271
+ }
272
+ }
273
+ walk(dir, backupDest);
274
+ }
275
+ async function update(opts) {
276
+ if (opts.postinstall) {
277
+ if (!existsSync6(join7(SKILLS_HOME, "contract-driven-delivery"))) {
278
+ return;
279
+ }
280
+ opts.yes = true;
281
+ opts.provider = "claude";
282
+ }
283
+ const quiet = !!opts.postinstall;
284
+ if (!quiet)
285
+ log.blank();
286
+ const cwd = process.cwd();
287
+ const requestedProvider = opts.provider ?? "auto";
288
+ if (!validateProviderOption(requestedProvider)) {
289
+ log.error(`Invalid provider: ${requestedProvider}. Use auto, claude, codex, or both.`);
290
+ process.exit(1);
291
+ }
292
+ const provider = inferProvider(cwd, requestedProvider);
293
+ const updateClaudeAssets = provider === "claude" || provider === "both";
294
+ const agentDiff = updateClaudeAssets ? diffDir(ASSET.agents, AGENTS_HOME) : [];
295
+ 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))) : [];
296
+ const toWrite = [...agentDiff, ...skillDiff].filter((e) => e.action !== "skip");
297
+ const toAdd = toWrite.filter((e) => e.action === "add");
298
+ const toOver = toWrite.filter((e) => e.action === "overwrite");
299
+ const toSkip = [...agentDiff, ...skillDiff].filter((e) => e.action === "skip");
300
+ if (!quiet) {
301
+ log.info(`Provider: ${provider}`);
302
+ if (updateClaudeAssets) {
303
+ log.info(`Dry-run diff \u2014 agents: ${AGENTS_HOME}`);
304
+ log.info(`Dry-run diff \u2014 skills: ${SKILLS_HOME}`);
305
+ } else {
306
+ log.info("Codex provider has no global cdd-kit assets to update.");
307
+ log.info("Project files are preserved; run cdd-kit init --local-only --provider codex to add missing local guidance.");
308
+ }
309
+ log.blank();
310
+ if (toAdd.length)
311
+ log.info(` + ${toAdd.length} file(s) would be added`);
312
+ if (toOver.length)
313
+ log.warn(` ~ ${toOver.length} file(s) would be overwritten (user edits lost without backup)`);
314
+ if (toSkip.length)
315
+ log.dim(` ${toSkip.length} file(s) unchanged (skipped)`);
316
+ }
317
+ if (toWrite.length === 0) {
318
+ if (!quiet) {
319
+ log.blank();
320
+ log.ok("Already up to date \u2014 nothing to write.");
321
+ log.blank();
322
+ }
323
+ return;
324
+ }
325
+ if (!quiet && !opts.yes) {
326
+ log.blank();
327
+ log.info("Run with --yes to apply changes. Example:");
328
+ log.dim(" cdd-kit update --yes");
329
+ log.blank();
330
+ return;
331
+ }
332
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
333
+ const backupRoot = join7(homedir2(), ".claude", ".cdd-kit-backup", timestamp);
334
+ if (!quiet) {
335
+ log.blank();
336
+ log.info(`Backing up to ${backupRoot} \u2026`);
337
+ }
338
+ backupDir(AGENTS_HOME, join7(backupRoot, "agents"));
339
+ backupDir(SKILLS_HOME, join7(backupRoot, "skills"));
340
+ if (!quiet)
341
+ log.ok(`Backup complete: ${backupRoot}`);
342
+ if (!quiet)
343
+ log.blank();
344
+ let totalSynced = 0;
345
+ if (updateClaudeAssets) {
346
+ if (!quiet)
347
+ log.info(`Updating agents \u2192 ${AGENTS_HOME}`);
348
+ const agentCount = applyDir(agentDiff);
349
+ if (!quiet)
350
+ log.ok(`${agentCount} agent file(s) updated.`);
351
+ totalSynced += agentCount;
352
+ if (!quiet)
353
+ log.info(`Updating skills \u2192 ${SKILLS_HOME}`);
354
+ const skillCount = applyDir(skillDiff);
355
+ if (!quiet)
356
+ log.ok(`${skillCount} skill file(s) updated.`);
357
+ totalSynced += skillCount;
358
+ }
359
+ if (quiet) {
360
+ if (totalSynced > 0)
361
+ log.ok(`cdd-kit: synced ${totalSynced} file(s) to ~/.claude/`);
362
+ } else {
363
+ log.blank();
364
+ log.info("Project files (contracts/, specs/, tests/, ci/) were not changed.");
365
+ log.ok("Update complete.");
366
+ log.info(`Backup saved to: ${backupRoot}`);
367
+ log.blank();
368
+ }
369
+ }
370
+ var init_update = __esm({
371
+ "src/commands/update.ts"() {
372
+ "use strict";
373
+ init_paths();
374
+ init_logger();
375
+ init_provider();
376
+ }
377
+ });
378
+
204
379
  // src/commands/context-scan.ts
205
380
  var context_scan_exports = {};
206
381
  __export(context_scan_exports, {
@@ -4046,49 +4221,49 @@ var require_fast_uri = __commonJS({
4046
4221
  schemelessOptions.skipEscape = true;
4047
4222
  return serialize(resolved, schemelessOptions);
4048
4223
  }
4049
- function resolveComponent(base, relative5, options, skipNormalization) {
4224
+ function resolveComponent(base, relative6, options, skipNormalization) {
4050
4225
  const target = {};
4051
4226
  if (!skipNormalization) {
4052
4227
  base = parse3(serialize(base, options), options);
4053
- relative5 = parse3(serialize(relative5, options), options);
4228
+ relative6 = parse3(serialize(relative6, options), options);
4054
4229
  }
4055
4230
  options = options || {};
4056
- if (!options.tolerant && relative5.scheme) {
4057
- target.scheme = relative5.scheme;
4058
- target.userinfo = relative5.userinfo;
4059
- target.host = relative5.host;
4060
- target.port = relative5.port;
4061
- target.path = removeDotSegments(relative5.path || "");
4062
- target.query = relative5.query;
4231
+ if (!options.tolerant && relative6.scheme) {
4232
+ target.scheme = relative6.scheme;
4233
+ target.userinfo = relative6.userinfo;
4234
+ target.host = relative6.host;
4235
+ target.port = relative6.port;
4236
+ target.path = removeDotSegments(relative6.path || "");
4237
+ target.query = relative6.query;
4063
4238
  } else {
4064
- if (relative5.userinfo !== void 0 || relative5.host !== void 0 || relative5.port !== void 0) {
4065
- target.userinfo = relative5.userinfo;
4066
- target.host = relative5.host;
4067
- target.port = relative5.port;
4068
- target.path = removeDotSegments(relative5.path || "");
4069
- target.query = relative5.query;
4239
+ if (relative6.userinfo !== void 0 || relative6.host !== void 0 || relative6.port !== void 0) {
4240
+ target.userinfo = relative6.userinfo;
4241
+ target.host = relative6.host;
4242
+ target.port = relative6.port;
4243
+ target.path = removeDotSegments(relative6.path || "");
4244
+ target.query = relative6.query;
4070
4245
  } else {
4071
- if (!relative5.path) {
4246
+ if (!relative6.path) {
4072
4247
  target.path = base.path;
4073
- if (relative5.query !== void 0) {
4074
- target.query = relative5.query;
4248
+ if (relative6.query !== void 0) {
4249
+ target.query = relative6.query;
4075
4250
  } else {
4076
4251
  target.query = base.query;
4077
4252
  }
4078
4253
  } else {
4079
- if (relative5.path[0] === "/") {
4080
- target.path = removeDotSegments(relative5.path);
4254
+ if (relative6.path[0] === "/") {
4255
+ target.path = removeDotSegments(relative6.path);
4081
4256
  } else {
4082
4257
  if ((base.userinfo !== void 0 || base.host !== void 0 || base.port !== void 0) && !base.path) {
4083
- target.path = "/" + relative5.path;
4258
+ target.path = "/" + relative6.path;
4084
4259
  } else if (!base.path) {
4085
- target.path = relative5.path;
4260
+ target.path = relative6.path;
4086
4261
  } else {
4087
- target.path = base.path.slice(0, base.path.lastIndexOf("/") + 1) + relative5.path;
4262
+ target.path = base.path.slice(0, base.path.lastIndexOf("/") + 1) + relative6.path;
4088
4263
  }
4089
4264
  target.path = removeDotSegments(target.path);
4090
4265
  }
4091
- target.query = relative5.query;
4266
+ target.query = relative6.query;
4092
4267
  }
4093
4268
  target.userinfo = base.userinfo;
4094
4269
  target.host = base.host;
@@ -4096,7 +4271,7 @@ var require_fast_uri = __commonJS({
4096
4271
  }
4097
4272
  target.scheme = base.scheme;
4098
4273
  }
4099
- target.fragment = relative5.fragment;
4274
+ target.fragment = relative6.fragment;
4100
4275
  return target;
4101
4276
  }
4102
4277
  function equal(uriA, uriB, options) {
@@ -7468,1877 +7643,2184 @@ var init_freshness = __esm({
7468
7643
  }
7469
7644
  });
7470
7645
 
7471
- // src/code-map/yaml-writer.ts
7472
- function quoteScalar(s) {
7473
- if (s.startsWith(".") || YAML_RESERVED.test(s) || s.includes(" ") || s === "" || s === "null" || s === "true" || s === "false") {
7474
- return `'${s.replace(/'/g, "''")}'`;
7475
- }
7476
- return s;
7477
- }
7478
- function quotePath(p) {
7479
- if (YAML_RESERVED.test(p) || p.includes(" ")) {
7480
- return `'${p.replace(/'/g, "''")}'`;
7481
- }
7482
- return p;
7483
- }
7484
- function renderItems(items) {
7485
- if (items.length === 0)
7486
- return "[]";
7487
- return `[${items.map(quoteScalar).join(", ")}]`;
7488
- }
7489
- function truncateDecorator(d, max = 80) {
7490
- const cleaned = d.replace(/\r?\n/g, " ");
7491
- return cleaned.length <= max ? cleaned : cleaned.slice(0, max) + "...";
7492
- }
7493
- function renderClass(c) {
7494
- const lines = [];
7495
- lines.push(` - name: ${c.name}`);
7496
- lines.push(` lines: ${c.lines[0]}-${c.lines[1]}`);
7497
- if (c.methods.length > 0) {
7498
- lines.push(" methods:");
7499
- for (const m of c.methods) {
7500
- const asyncPrefix = m.async ? "async " : "";
7501
- lines.push(` - { name: ${asyncPrefix}${m.name}, lines: ${m.lines[0]}-${m.lines[1]} }`);
7502
- }
7503
- }
7504
- return lines;
7505
- }
7506
- function renderFunction(f) {
7507
- const asyncPrefix = f.async ? "async " : "";
7508
- let comment = "";
7509
- if (f.decorators.length > 0) {
7510
- const truncated = truncateDecorator(f.decorators[0]);
7511
- comment = ` # @${truncated}`;
7646
+ // src/commands/migrate.ts
7647
+ var migrate_exports = {};
7648
+ __export(migrate_exports, {
7649
+ migrate: () => migrate
7650
+ });
7651
+ import { join as join16 } from "path";
7652
+ import { cpSync as cpSync2, existsSync as existsSync14, mkdirSync as mkdirSync6, readdirSync as readdirSync8, readFileSync as readFileSync11, renameSync, rmSync as rmSync2, writeFileSync as writeFileSync6 } from "fs";
7653
+ import yaml3 from "js-yaml";
7654
+ function backupChangeDir(cwd, changeId, sessionStamp) {
7655
+ const backupRoot = join16(cwd, ".cdd", "migrate-backup", sessionStamp);
7656
+ const backupDir2 = join16(backupRoot, changeId);
7657
+ mkdirSync6(backupRoot, { recursive: true });
7658
+ const sourceDir = join16(cwd, "specs", "changes", changeId);
7659
+ if (existsSync14(sourceDir)) {
7660
+ cpSync2(sourceDir, backupDir2, { recursive: true });
7512
7661
  }
7513
- return ` - { name: ${asyncPrefix}${f.name}, lines: ${f.lines[0]}-${f.lines[1]} }${comment}`;
7662
+ return backupDir2;
7514
7663
  }
7515
- function renderTypeDef(t) {
7516
- const exportedSuffix = t.exported ? "" : " # local";
7517
- return ` - { name: ${t.name}, lines: ${t.lines[0]}-${t.lines[1]} }${exportedSuffix}`;
7664
+ function buildLegacyContextManifest(changeId) {
7665
+ return [
7666
+ "# Context Manifest",
7667
+ "",
7668
+ "Generated by `cdd-kit migrate` for an existing change.",
7669
+ "Legacy manifest. Forbidden paths come from `.cdd/context-policy.json`.",
7670
+ "",
7671
+ "## Affected Surfaces",
7672
+ "- legacy-unknown",
7673
+ "",
7674
+ "## Allowed Paths",
7675
+ `- specs/changes/${changeId}/`,
7676
+ "",
7677
+ "## Required Contracts",
7678
+ "- legacy-unknown",
7679
+ "",
7680
+ "## Required Tests",
7681
+ "- legacy-unknown",
7682
+ "",
7683
+ "## Agent Work Packets",
7684
+ "",
7685
+ "## Context Expansion Requests",
7686
+ "-",
7687
+ "",
7688
+ "## Approved Expansions",
7689
+ "-",
7690
+ ""
7691
+ ].join("\n");
7518
7692
  }
7519
- function renderEnum(e) {
7520
- const lines = [];
7521
- const exportedSuffix = e.exported ? "" : " # local";
7522
- lines.push(` - name: ${e.name}`);
7523
- lines.push(` lines: ${e.lines[0]}-${e.lines[1]}${exportedSuffix}`);
7524
- if (e.members.length > 0) {
7525
- lines.push(` members: [${e.members.join(", ")}]`);
7526
- }
7527
- return lines;
7693
+ function buildContextGovernedManifest(changeId) {
7694
+ return [
7695
+ "# Context Manifest",
7696
+ "",
7697
+ "Generated by `cdd-kit migrate --enable-context-governance` for an existing change.",
7698
+ "Review and narrow the allowed paths before assigning implementation work.",
7699
+ "Forbidden paths come from `.cdd/context-policy.json`.",
7700
+ "",
7701
+ "## Affected Surfaces",
7702
+ "- legacy-unknown",
7703
+ "",
7704
+ "## Allowed Paths",
7705
+ `- specs/changes/${changeId}/`,
7706
+ "- specs/context/project-map.md",
7707
+ "- specs/context/contracts-index.md",
7708
+ "",
7709
+ "## Required Contracts",
7710
+ "- legacy-unknown",
7711
+ "",
7712
+ "## Required Tests",
7713
+ "- legacy-unknown",
7714
+ "",
7715
+ "## Agent Work Packets",
7716
+ "",
7717
+ "### change-classifier",
7718
+ "- allowed:",
7719
+ ` - specs/changes/${changeId}/`,
7720
+ " - specs/context/project-map.md",
7721
+ " - specs/context/contracts-index.md",
7722
+ "",
7723
+ "## Context Expansion Requests",
7724
+ "",
7725
+ "<!--",
7726
+ "Agents must request context expansion instead of reading outside their work packet.",
7727
+ "Use this format only for real requests:",
7728
+ "",
7729
+ "- request-id: CER-001",
7730
+ " requested_paths:",
7731
+ " - src/example.ts",
7732
+ " reason: why this file is required",
7733
+ " status: pending",
7734
+ "-->",
7735
+ "",
7736
+ "## Approved Expansions",
7737
+ "-",
7738
+ ""
7739
+ ].join("\n");
7528
7740
  }
7529
- function renderYaml(entries, opts) {
7530
- const now = (/* @__PURE__ */ new Date()).toISOString().replace(/\.\d+Z$/, "Z");
7531
- const totalSrc = entries.reduce((s, e) => s + e.total_lines, 0);
7532
- const bodyLines = [];
7533
- for (let i = 0; i < entries.length; i++) {
7534
- const e = entries[i];
7535
- if (i > 0)
7536
- bodyLines.push("");
7537
- const pathKey = quotePath(e.path);
7538
- bodyLines.push(`${pathKey}: # ${e.total_lines} lines`);
7539
- if (e.imports.length > 0) {
7540
- bodyLines.push(" imports:");
7541
- for (const imp of e.imports) {
7542
- const mod = quoteScalar(imp.module);
7543
- bodyLines.push(` - { module: ${mod}, items: ${renderItems(imp.items)}, line: ${imp.line} }`);
7544
- }
7545
- }
7546
- if (e.constants.length > 0) {
7547
- bodyLines.push(" constants:");
7548
- for (const c of e.constants) {
7549
- bodyLines.push(` - { name: ${c.name}, line: ${c.line} }`);
7550
- }
7551
- }
7552
- if (e.classes.length > 0) {
7553
- bodyLines.push(" classes:");
7554
- for (const c of e.classes) {
7555
- bodyLines.push(...renderClass(c));
7556
- }
7557
- }
7558
- if (e.functions.length > 0) {
7559
- bodyLines.push(" functions:");
7560
- for (const f of e.functions) {
7561
- bodyLines.push(renderFunction(f));
7562
- }
7563
- }
7564
- if (e.interfaces && e.interfaces.length > 0) {
7565
- bodyLines.push(" interfaces:");
7566
- for (const t of e.interfaces) {
7567
- bodyLines.push(renderTypeDef(t));
7568
- }
7569
- }
7570
- if (e.types && e.types.length > 0) {
7571
- bodyLines.push(" types:");
7572
- for (const t of e.types) {
7573
- bodyLines.push(renderTypeDef(t));
7574
- }
7575
- }
7576
- if (e.enums && e.enums.length > 0) {
7577
- bodyLines.push(" enums:");
7578
- for (const en of e.enums) {
7579
- bodyLines.push(...renderEnum(en));
7580
- }
7581
- }
7741
+ function parseLegacyFrontmatter(content) {
7742
+ const m = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
7743
+ if (!m)
7744
+ return {};
7745
+ const out = {};
7746
+ for (const line of m[1].split(/\r?\n/)) {
7747
+ const colon = line.indexOf(":");
7748
+ if (colon === -1)
7749
+ continue;
7750
+ const key = line.slice(0, colon).trim();
7751
+ if (!key)
7752
+ continue;
7753
+ out[key] = line.slice(colon + 1).trim();
7582
7754
  }
7583
- const mapLines = bodyLines.length + 2;
7584
- const compression = totalSrc === 0 ? 0 : totalSrc / mapLines;
7585
- const fileCount = entries.length;
7586
- const header = [
7587
- `# generated: ${now} by ${opts.generator}`,
7588
- `# files: ${fileCount}, src-lines: ${totalSrc}, map-lines: ${mapLines}, compression: ${compression.toFixed(1)}x`
7589
- ];
7590
- return [...header, ...bodyLines].join("\n") + "\n";
7755
+ return out;
7591
7756
  }
7592
- var YAML_RESERVED;
7593
- var init_yaml_writer = __esm({
7594
- "src/code-map/yaml-writer.ts"() {
7595
- "use strict";
7596
- YAML_RESERVED = /[:[\]{},&*!|>'"%@`#]/;
7597
- }
7598
- });
7599
-
7600
- // src/code-map/orchestrator.ts
7601
- async function scanInProcess(scanner, absolutePaths, repoRoot) {
7602
- const entries = [];
7603
- const warnings = [];
7604
- for (const absPath of absolutePaths) {
7605
- try {
7606
- const entry = await scanner.scan(absPath, repoRoot);
7607
- if (entry === null) {
7608
- const rel = absPath.replace(/\\/g, "/").replace(repoRoot.replace(/\\/g, "/") + "/", "");
7609
- warnings.push({ path: rel, message: "parse error (scanner returned null)" });
7610
- } else {
7611
- entries.push(entry);
7612
- }
7613
- } catch (err) {
7614
- const rel = absPath.replace(/\\/g, "/").replace(repoRoot.replace(/\\/g, "/") + "/", "");
7615
- warnings.push({ path: rel, message: `IO error: ${err.message}` });
7757
+ function parseListField(raw) {
7758
+ if (!raw)
7759
+ return [];
7760
+ const trimmed = raw.trim();
7761
+ if (!trimmed || trimmed === "[]")
7762
+ return [];
7763
+ const inner = trimmed.startsWith("[") && trimmed.endsWith("]") ? trimmed.slice(1, -1) : trimmed;
7764
+ return inner.split(",").map((item) => item.trim().replace(/^["']|["']$/g, "")).filter(Boolean);
7765
+ }
7766
+ function parseLegacyTaskList(body) {
7767
+ const lines = body.split(/\r?\n/);
7768
+ const rows = [];
7769
+ let currentSection;
7770
+ for (const raw of lines) {
7771
+ const headerMatch = raw.match(/^##\s+\d+\.\s+(.*)\s*$/);
7772
+ if (headerMatch) {
7773
+ currentSection = headerMatch[1].trim();
7774
+ continue;
7616
7775
  }
7776
+ const itemMatch = raw.match(/^\s*-\s*\[([ xX\-])\]\s+(\d+(?:\.\d+)*)\s+(.*)\s*$/);
7777
+ if (!itemMatch)
7778
+ continue;
7779
+ const mark = itemMatch[1];
7780
+ const id = itemMatch[2];
7781
+ const title = itemMatch[3].trim();
7782
+ let status = "pending";
7783
+ if (mark === "x" || mark === "X")
7784
+ status = "done";
7785
+ else if (mark === "-")
7786
+ status = "skipped";
7787
+ rows.push({ id, title, status, section: currentSection });
7617
7788
  }
7618
- return { entries, warnings };
7789
+ return rows;
7619
7790
  }
7620
- function bucketByExtension(files) {
7621
- const out = {};
7622
- for (const f of files) {
7623
- const dot = f.lastIndexOf(".");
7624
- const ext = dot >= 0 ? f.slice(dot).toLowerCase() : "";
7625
- if (!out[ext])
7626
- out[ext] = [];
7627
- out[ext].push(f);
7791
+ function migrateTasksFile(changeId, changeDir, enableContextGovernance, detectedTier, changed, warnings, pendingWrites, pendingDeletes) {
7792
+ const newPath = join16(changeDir, "tasks.yml");
7793
+ const legacyPath = join16(changeDir, "tasks.md");
7794
+ if (existsSync14(newPath)) {
7795
+ return;
7628
7796
  }
7629
- return out;
7630
- }
7631
- var init_orchestrator = __esm({
7632
- "src/code-map/orchestrator.ts"() {
7633
- "use strict";
7634
- init_include_exclude();
7797
+ if (!existsSync14(legacyPath)) {
7798
+ warnings.push("tasks.md not found and tasks.yml missing \u2014 skipping tasks migration");
7799
+ return;
7635
7800
  }
7636
- });
7637
-
7638
- // src/code-map/scanners/common.ts
7639
- import { relative as relative3 } from "path";
7640
- function canonicalRelPath(absolutePath, repoRoot) {
7641
- const rel = relative3(repoRoot, absolutePath);
7642
- return rel.replace(/\\/g, "/").normalize("NFC");
7643
- }
7644
- function isAllCapsConst(name) {
7645
- return name.length >= 2 && /^[A-Z][A-Z0-9_]*$/.test(name);
7646
- }
7647
- function isBinary(content) {
7648
- const head = content.slice(0, 4096);
7649
- return head.includes("\0");
7650
- }
7651
- var init_common = __esm({
7652
- "src/code-map/scanners/common.ts"() {
7653
- "use strict";
7801
+ const raw = readFileSync11(legacyPath, "utf8");
7802
+ const fm = parseLegacyFrontmatter(raw);
7803
+ const bodyMatch = raw.match(/^---\r?\n[\s\S]*?\r?\n---\r?\n?([\s\S]*)$/);
7804
+ const body = bodyMatch ? bodyMatch[1] : raw;
7805
+ const tasksRows = parseLegacyTaskList(body);
7806
+ const data = {};
7807
+ data["change-id"] = fm["change-id"] || changeId;
7808
+ data["status"] = fm["status"] || "in-progress";
7809
+ if (fm["tier"] && /^\d+$/.test(fm["tier"])) {
7810
+ data["tier"] = parseInt(fm["tier"], 10);
7811
+ } else if (detectedTier) {
7812
+ data["tier"] = parseInt(detectedTier, 10);
7813
+ } else {
7814
+ data["tier"] = null;
7654
7815
  }
7655
- });
7656
-
7657
- // src/code-map/scanners/python.ts
7658
- var python_exports = {};
7659
- __export(python_exports, {
7660
- pythonScanner: () => pythonScanner
7661
- });
7662
- import { spawnSync as spawnSync2 } from "child_process";
7663
- import { writeFileSync as writeFileSync6, unlinkSync } from "fs";
7664
- import { join as join16 } from "path";
7665
- import { tmpdir } from "os";
7666
- function detectPython2() {
7667
- for (const candidate of ["python3", "python"]) {
7668
- try {
7669
- const result = spawnSync2(candidate, ["--version"], {
7670
- encoding: "utf8",
7671
- timeout: 5e3
7672
- });
7673
- if (result.status === 0)
7674
- return candidate;
7675
- } catch {
7676
- }
7816
+ if (enableContextGovernance || fm["context-governance"] === "v1") {
7817
+ data["context-governance"] = "v1";
7677
7818
  }
7678
- return null;
7679
- }
7680
- var TIMEOUT_MS, PythonScanner, pythonScanner;
7681
- var init_python = __esm({
7682
- "src/code-map/scanners/python.ts"() {
7683
- "use strict";
7684
- init_paths();
7685
- init_common();
7686
- TIMEOUT_MS = parseInt(process.env["CDD_CODE_MAP_TIMEOUT_MS"] ?? "30000", 10);
7687
- PythonScanner = class {
7688
- extensions = [".py"];
7689
- _interpreter = void 0;
7690
- getInterpreter() {
7691
- if (this._interpreter === void 0) {
7692
- this._interpreter = detectPython2();
7693
- }
7694
- return this._interpreter;
7695
- }
7696
- async scan(absolutePath, repoRoot) {
7697
- const result = await this.scanBatch([absolutePath], repoRoot);
7698
- return result.entries[0] ?? null;
7699
- }
7700
- async scanBatch(absolutePaths, repoRoot) {
7701
- const interpreter = this.getInterpreter();
7702
- const entries = [];
7703
- const warnings = [];
7704
- if (!interpreter) {
7705
- const count = absolutePaths.length;
7706
- warnings.push({
7707
- path: "",
7708
- message: `python interpreter not found on PATH; skipping ${count} .py file${count === 1 ? "" : "s"}`
7709
- });
7710
- return { entries, warnings };
7711
- }
7712
- const scriptPath = ASSET.codeMapPython;
7713
- const rand = Math.random().toString(36).slice(2);
7714
- const listFile = join16(tmpdir(), `cdd-codemap-${process.pid}-${rand}.txt`);
7715
- writeFileSync6(listFile, absolutePaths.join("\n") + "\n", "utf8");
7716
- let stdout = "";
7717
- let stderr = "";
7718
- let exitCode = 0;
7719
- try {
7720
- const result = spawnSync2(
7721
- interpreter,
7722
- [scriptPath, "--batch-file", listFile, "--repo-root", repoRoot],
7723
- {
7724
- encoding: "utf8",
7725
- timeout: TIMEOUT_MS,
7726
- maxBuffer: 50 * 1024 * 1024
7727
- // 50MB
7728
- }
7729
- );
7730
- stdout = result.stdout ?? "";
7731
- stderr = result.stderr ?? "";
7732
- exitCode = result.status ?? -1;
7733
- if (result.error) {
7734
- const errMsg = result.error.message ?? String(result.error);
7735
- if (errMsg.includes("ENOENT")) {
7736
- warnings.push({
7737
- path: "",
7738
- message: `python interpreter not found (ENOENT); skipping ${absolutePaths.length} .py file(s)`
7739
- });
7740
- return { entries, warnings };
7741
- }
7742
- if (errMsg.includes("ETIMEDOUT") || errMsg.includes("timeout")) {
7743
- warnings.push({
7744
- path: "",
7745
- message: `python scanner timed out after ${TIMEOUT_MS}ms; skipping .py files`
7746
- });
7747
- return { entries, warnings };
7748
- }
7749
- warnings.push({
7750
- path: "",
7751
- message: `python scanner error: ${errMsg}; skipping .py files`
7752
- });
7753
- return { entries, warnings };
7754
- }
7755
- } finally {
7756
- try {
7757
- unlinkSync(listFile);
7758
- } catch {
7759
- }
7760
- }
7761
- if (exitCode === 3) {
7762
- warnings.push({
7763
- path: "",
7764
- message: "python interpreter is < 3.9 (need ast.unparse); skipping .py files"
7765
- });
7766
- return { entries, warnings };
7767
- }
7768
- for (const line of stdout.split("\n")) {
7769
- const trimmed = line.trim();
7770
- if (!trimmed)
7771
- continue;
7772
- let parsed;
7773
- try {
7774
- parsed = JSON.parse(trimmed);
7775
- } catch {
7776
- continue;
7777
- }
7778
- if (!parsed.ok) {
7779
- warnings.push({ path: parsed.path, message: parsed.error });
7780
- continue;
7781
- }
7782
- const r = parsed;
7783
- entries.push({
7784
- path: canonicalRelPath(join16(repoRoot, r.path), repoRoot),
7785
- total_lines: r.total_lines,
7786
- imports: r.imports ?? [],
7787
- constants: r.constants ?? [],
7788
- classes: (r.classes ?? []).map((c) => ({
7789
- name: c.name,
7790
- lines: [c.lines[0], c.lines[1]],
7791
- methods: (c.methods ?? []).map((m) => ({
7792
- name: m.name,
7793
- lines: [m.lines[0], m.lines[1]],
7794
- async: m.async
7795
- }))
7796
- })),
7797
- functions: (r.functions ?? []).map((f) => ({
7798
- name: f.name,
7799
- lines: [f.lines[0], f.lines[1]],
7800
- decorators: f.decorators ?? [],
7801
- async: f.async
7802
- }))
7803
- });
7804
- }
7805
- if (exitCode === 2) {
7806
- warnings.push({
7807
- path: "",
7808
- message: `python scanner exited with code 2 (fatal); partial results only. stderr: ${stderr.slice(0, 200)}`
7809
- });
7810
- }
7811
- return { entries, warnings };
7812
- }
7813
- };
7814
- pythonScanner = new PythonScanner();
7815
- }
7816
- });
7817
-
7818
- // src/code-map/scanners/javascript.ts
7819
- var javascript_exports = {};
7820
- __export(javascript_exports, {
7821
- COMMON_PLUGINS: () => COMMON_PLUGINS,
7822
- countSourceLines: () => countSourceLines,
7823
- jsScanner: () => jsScanner,
7824
- parseAndExtract: () => parseAndExtract,
7825
- parseJsSource: () => parseJsSource,
7826
- parseSourceWithPlugins: () => parseSourceWithPlugins
7827
- });
7828
- import { readFileSync as readFileSync12 } from "fs";
7829
- import { parse } from "@babel/parser";
7830
- function parseSourceWithPlugins(source, plugins) {
7831
- return parse(source, {
7832
- sourceType: "unambiguous",
7833
- allowReturnOutsideFunction: true,
7834
- allowAwaitOutsideFunction: true,
7835
- errorRecovery: true,
7836
- plugins
7819
+ const archive2 = parseListField(fm["archive-tasks"]);
7820
+ data["archive-tasks"] = archive2.length > 0 ? archive2 : ["7.1", "7.2"];
7821
+ const deps = parseListField(fm["depends-on"]);
7822
+ data["depends-on"] = deps;
7823
+ data["tasks"] = tasksRows.map((r) => {
7824
+ const out = { id: r.id, title: r.title, status: r.status };
7825
+ if (r.section)
7826
+ out["section"] = r.section;
7827
+ return out;
7837
7828
  });
7829
+ const yamlOut = yaml3.dump(data, { lineWidth: -1, noRefs: true });
7830
+ pendingWrites.push({ path: newPath, content: yamlOut });
7831
+ pendingDeletes.push({ path: legacyPath });
7832
+ changed.push(`tasks.md -> tasks.yml (${tasksRows.length} task(s) migrated)`);
7838
7833
  }
7839
- function isCallExpression(node, callee) {
7840
- if (!node || node.type !== "CallExpression")
7841
- return false;
7842
- const c = node.callee;
7843
- return c.type === "Identifier" && c.name === callee;
7844
- }
7845
- function extractRequireModule(node) {
7846
- if (!node || node.type !== "CallExpression")
7847
- return null;
7848
- const c = node.callee;
7849
- if (c.type !== "Identifier" || c.name !== "require")
7850
- return null;
7851
- const arg = node.arguments[0];
7852
- if (!arg || arg.type !== "StringLiteral")
7853
- return null;
7854
- return arg.value;
7855
- }
7856
- function getLineRange(node) {
7857
- const start = node.loc?.start.line ?? 1;
7858
- const end = node.loc?.end.line ?? start;
7859
- return [start, end];
7860
- }
7861
- function processImportDeclaration(node) {
7862
- const items = [];
7863
- for (const spec of node.specifiers) {
7864
- if (spec.type === "ImportDefaultSpecifier") {
7865
- items.push(`default:${spec.local.name}`);
7866
- } else if (spec.type === "ImportNamespaceSpecifier") {
7867
- items.push(`*:${spec.local.name}`);
7868
- } else if (spec.type === "ImportSpecifier") {
7869
- const imported = spec.imported;
7870
- items.push(imported.type === "Identifier" ? imported.name : spec.local.name);
7871
- }
7872
- }
7873
- return {
7874
- module: node.source.value,
7875
- items,
7876
- line: node.loc?.start.line ?? 1
7877
- };
7878
- }
7879
- function processFunctionDeclaration(node, nameOverride) {
7880
- const name = nameOverride ?? node.id?.name;
7881
- if (!name)
7882
- return null;
7883
- return {
7884
- name,
7885
- lines: getLineRange(node),
7886
- decorators: [],
7887
- async: node.async
7888
- };
7889
- }
7890
- function processClassDeclaration(node, nameOverride) {
7891
- const name = nameOverride ?? node.id?.name;
7892
- if (!name)
7893
- return null;
7894
- const methods = [];
7895
- for (const member of node.body.body) {
7896
- if (member.type === "ClassMethod" || member.type === "ClassPrivateMethod") {
7897
- const m = member;
7898
- let methodName;
7899
- if (m.key.type === "Identifier") {
7900
- methodName = m.key.name;
7901
- } else if (m.key.type === "PrivateName") {
7902
- methodName = `#${m.key.id.name}`;
7903
- } else {
7904
- methodName = "<computed>";
7905
- }
7906
- methods.push({
7907
- name: methodName,
7908
- lines: getLineRange(m),
7909
- async: m.async
7910
- });
7911
- }
7912
- }
7913
- return {
7914
- name,
7915
- lines: getLineRange(node),
7916
- methods
7917
- };
7918
- }
7919
- function processVariableDeclaration(node, imports, constants, functions) {
7920
- for (const decl of node.declarations) {
7921
- if (!decl.id || decl.id.type !== "Identifier")
7922
- continue;
7923
- const varName = decl.id.name;
7924
- const init2 = decl.init;
7925
- if (init2 && isCallExpression(init2, "require")) {
7926
- const mod = extractRequireModule(init2);
7927
- if (mod) {
7928
- imports.push({ module: mod, items: [`default:${varName}`], line: node.loc?.start.line ?? 1 });
7929
- }
7930
- continue;
7931
- }
7932
- if (isAllCapsConst(varName) && init2 !== null && init2 !== void 0) {
7933
- constants.push({ name: varName, line: node.loc?.start.line ?? 1 });
7834
+ function parseLegacyAgentLog(content) {
7835
+ const lines = content.split(/\r?\n/);
7836
+ const data = {};
7837
+ let i = 0;
7838
+ const topFieldRe = /^[ ]{0,1}-\s*([\w-]+):\s*(.*)$/;
7839
+ while (i < lines.length) {
7840
+ const line = lines[i];
7841
+ const fieldMatch = line.match(topFieldRe);
7842
+ if (!fieldMatch) {
7843
+ i++;
7934
7844
  continue;
7935
7845
  }
7936
- if (init2 && (init2.type === "ArrowFunctionExpression" || init2.type === "FunctionExpression")) {
7937
- functions.push({
7938
- name: varName,
7939
- lines: getLineRange(node),
7940
- decorators: [],
7941
- async: init2.async
7942
- });
7943
- } else if (init2 && init2.type === "CallExpression" && /^[A-Z]/.test(varName)) {
7944
- functions.push({
7945
- name: varName,
7946
- lines: getLineRange(node),
7947
- decorators: [],
7948
- async: false
7949
- });
7846
+ const key = fieldMatch[1];
7847
+ const inline = fieldMatch[2].trim();
7848
+ if (key === "files-read" || key === "artifacts") {
7849
+ const items = [];
7850
+ let j = i + 1;
7851
+ while (j < lines.length) {
7852
+ const sub = lines[j];
7853
+ if (topFieldRe.test(sub))
7854
+ break;
7855
+ if (/^#/.test(sub))
7856
+ break;
7857
+ const itemMatch = sub.match(/^\s{2,}-\s+(.+?)\s*$/);
7858
+ if (itemMatch) {
7859
+ items.push(itemMatch[1].trim());
7860
+ }
7861
+ j++;
7862
+ }
7863
+ if (key === "files-read") {
7864
+ data["files-read"] = items;
7865
+ } else {
7866
+ data["artifacts"] = items.map((s) => {
7867
+ const idx = s.indexOf(":");
7868
+ if (idx === -1) {
7869
+ return { type: "note", pointer: s };
7870
+ }
7871
+ const type = s.slice(0, idx).trim();
7872
+ const pointer = s.slice(idx + 1).trim();
7873
+ return { type, pointer };
7874
+ });
7875
+ }
7876
+ i = j;
7877
+ continue;
7950
7878
  }
7879
+ data[key] = inline;
7880
+ i++;
7951
7881
  }
7882
+ return data;
7952
7883
  }
7953
- function processTsInterfaceDeclaration(node, exported) {
7954
- if (!node || node.type !== "TSInterfaceDeclaration")
7955
- return null;
7956
- if (!node.id || node.id.type !== "Identifier")
7957
- return null;
7958
- return {
7959
- name: node.id.name,
7960
- lines: getLineRange(node),
7961
- exported
7962
- };
7963
- }
7964
- function processTsTypeAliasDeclaration(node, exported) {
7965
- if (!node || node.type !== "TSTypeAliasDeclaration")
7966
- return null;
7967
- if (!node.id || node.id.type !== "Identifier")
7968
- return null;
7969
- return {
7970
- name: node.id.name,
7971
- lines: getLineRange(node),
7972
- exported
7973
- };
7974
- }
7975
- function processTsEnumDeclaration(node, exported) {
7976
- if (!node || node.type !== "TSEnumDeclaration")
7977
- return null;
7978
- if (!node.id || node.id.type !== "Identifier")
7979
- return null;
7980
- const members = [];
7981
- for (const m of node.members ?? []) {
7982
- if (!m || !m.id)
7884
+ function migrateAgentLogs(changeDir, changed, pendingWrites, pendingDeletes) {
7885
+ const agentLogDir = join16(changeDir, "agent-log");
7886
+ if (!existsSync14(agentLogDir))
7887
+ return;
7888
+ const mdLogs = readdirSync8(agentLogDir).filter((f) => f.endsWith(".md"));
7889
+ for (const f of mdLogs) {
7890
+ const fullPath = join16(agentLogDir, f);
7891
+ const yamlName = f.replace(/\.md$/, ".yml");
7892
+ const yamlFull = join16(agentLogDir, yamlName);
7893
+ if (existsSync14(yamlFull))
7983
7894
  continue;
7984
- if (m.id.type === "Identifier")
7985
- members.push(m.id.name);
7986
- else if (m.id.type === "StringLiteral")
7987
- members.push(m.id.value);
7895
+ const raw = readFileSync11(fullPath, "utf8");
7896
+ const parsed = parseLegacyAgentLog(raw);
7897
+ const yamlOut = yaml3.dump(parsed, { lineWidth: -1, noRefs: true });
7898
+ pendingWrites.push({ path: yamlFull, content: yamlOut });
7899
+ pendingDeletes.push({ path: fullPath });
7900
+ changed.push(`agent-log/${f} -> agent-log/${yamlName}`);
7988
7901
  }
7989
- return {
7990
- name: node.id.name,
7991
- lines: getLineRange(node),
7992
- exported,
7993
- members
7994
- };
7995
7902
  }
7996
- function processStatement(stmt, buckets, extractTsTypes, exportedFromWrapper = false) {
7997
- if (stmt.type === "ExportNamedDeclaration" && stmt.declaration) {
7998
- processStatement(stmt.declaration, buckets, extractTsTypes, true);
7999
- return;
8000
- }
8001
- if (stmt.type === "ExportDefaultDeclaration") {
8002
- const decl = stmt.declaration;
8003
- if (decl.type === "FunctionDeclaration") {
8004
- const fe = processFunctionDeclaration(decl, decl.id?.name ?? "default");
8005
- if (fe)
8006
- buckets.functions.push(fe);
8007
- } else if (decl.type === "ClassDeclaration") {
8008
- const ce = processClassDeclaration(decl, decl.id?.name ?? "default");
8009
- if (ce)
8010
- buckets.classes.push(ce);
7903
+ function migrateOne(changeId, changeDir, enableContextGovernance) {
7904
+ const changed = [];
7905
+ const warnings = [];
7906
+ const pending = [];
7907
+ const deletes = [];
7908
+ let detectedTier = null;
7909
+ const classifPath = join16(changeDir, "change-classification.md");
7910
+ if (existsSync14(classifPath)) {
7911
+ const content = readFileSync11(classifPath, "utf8");
7912
+ const hasNewTierFormat = /^## Tier\s*\n\s*-\s*\d\s*$/m.test(content);
7913
+ const oldMatch = content.match(/\*\*Tier[:\*]+\s*(?:Tier\s*)?(\d)/i) ?? content.match(/^-?\s*Tier:\s*(?:Tier\s*)?(\d)/mi);
7914
+ if (oldMatch)
7915
+ detectedTier = oldMatch[1];
7916
+ if (!hasNewTierFormat) {
7917
+ if (detectedTier) {
7918
+ const addition = `
7919
+ ## Tier
7920
+ - ${detectedTier}
7921
+ `;
7922
+ if (!content.includes("\n## Tier\n")) {
7923
+ changed.push(
7924
+ `change-classification.md: appended "## Tier\\n- ${detectedTier}" (converted from old format)`
7925
+ );
7926
+ pending.push({ path: classifPath, content: content + addition });
7927
+ }
7928
+ } else {
7929
+ warnings.push(
7930
+ "change-classification.md: could not detect tier (no **Tier:** N or ## Tier N found). Set `tier: <0-5>` in tasks.yml frontmatter to enable tier-based gate checks."
7931
+ );
7932
+ }
7933
+ } else {
7934
+ const structured = content.match(/^## Tier\s*\n\s*-\s*(\d)\s*$/m);
7935
+ if (structured)
7936
+ detectedTier = structured[1];
8011
7937
  }
8012
- return;
8013
- }
8014
- if (stmt.type === "ImportDeclaration") {
8015
- buckets.imports.push(processImportDeclaration(stmt));
8016
- return;
8017
7938
  }
8018
- if (stmt.type === "FunctionDeclaration") {
8019
- const fe = processFunctionDeclaration(stmt);
8020
- if (fe)
8021
- buckets.functions.push(fe);
8022
- return;
7939
+ migrateTasksFile(changeId, changeDir, enableContextGovernance, detectedTier, changed, warnings, pending, deletes);
7940
+ migrateAgentLogs(changeDir, changed, pending, deletes);
7941
+ const manifestPath = join16(changeDir, "context-manifest.md");
7942
+ if (!existsSync14(manifestPath)) {
7943
+ changed.push(enableContextGovernance ? "context-manifest.md: added context-governance v1 manifest scaffold" : "context-manifest.md: added legacy context manifest scaffold");
7944
+ pending.push({
7945
+ path: manifestPath,
7946
+ content: enableContextGovernance ? buildContextGovernedManifest(changeId) : buildLegacyContextManifest(changeId)
7947
+ });
7948
+ } else if (enableContextGovernance) {
7949
+ warnings.push("context-manifest.md already exists \u2014 review allowed paths before relying on context-governance: v1");
8023
7950
  }
8024
- if (stmt.type === "ClassDeclaration") {
8025
- const ce = processClassDeclaration(stmt);
8026
- if (ce)
8027
- buckets.classes.push(ce);
8028
- return;
7951
+ return { result: { changed, warnings }, pending, deletes };
7952
+ }
7953
+ function commitWritesAtomically(pending, deletes) {
7954
+ const renames = [];
7955
+ try {
7956
+ for (const write of pending) {
7957
+ const tmp = `${write.path}.cdd-migrate.tmp`;
7958
+ writeFileSync6(tmp, write.content, "utf8");
7959
+ renames.push({ tmp, final: write.path });
7960
+ }
7961
+ } catch (err) {
7962
+ for (const r of renames) {
7963
+ try {
7964
+ rmSync2(r.tmp, { force: true });
7965
+ } catch {
7966
+ }
7967
+ }
7968
+ throw err;
8029
7969
  }
8030
- if (stmt.type === "VariableDeclaration") {
8031
- processVariableDeclaration(stmt, buckets.imports, buckets.constants, buckets.functions);
8032
- return;
7970
+ for (const r of renames) {
7971
+ renameSync(r.tmp, r.final);
8033
7972
  }
8034
- if (extractTsTypes) {
8035
- const anyStmt = stmt;
8036
- if (anyStmt.type === "TSInterfaceDeclaration") {
8037
- const e = processTsInterfaceDeclaration(anyStmt, exportedFromWrapper);
8038
- if (e)
8039
- buckets.interfaces.push(e);
8040
- return;
7973
+ for (const d of deletes) {
7974
+ try {
7975
+ rmSync2(d.path, { force: true });
7976
+ } catch {
8041
7977
  }
8042
- if (anyStmt.type === "TSTypeAliasDeclaration") {
8043
- const e = processTsTypeAliasDeclaration(anyStmt, exportedFromWrapper);
8044
- if (e)
8045
- buckets.types.push(e);
7978
+ }
7979
+ }
7980
+ async function migrate(changeId, opts = {}) {
7981
+ const cwd = process.cwd();
7982
+ const dryRun = opts.dryRun ?? false;
7983
+ const enableContextGovernance = opts.enableContextGovernance ?? false;
7984
+ const noBackup = opts.noBackup ?? false;
7985
+ const idsToMigrate = [];
7986
+ if (opts.all) {
7987
+ const changesDir = join16(cwd, "specs", "changes");
7988
+ if (!existsSync14(changesDir)) {
7989
+ log.info("No specs/changes/ directory found \u2014 nothing to migrate.");
8046
7990
  return;
8047
7991
  }
8048
- if (anyStmt.type === "TSEnumDeclaration") {
8049
- const e = processTsEnumDeclaration(anyStmt, exportedFromWrapper);
8050
- if (e)
8051
- buckets.enums.push(e);
8052
- return;
7992
+ idsToMigrate.push(
7993
+ ...readdirSync8(changesDir, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name)
7994
+ );
7995
+ } else if (changeId) {
7996
+ const specificDir = join16(cwd, "specs", "changes", changeId);
7997
+ if (!existsSync14(specificDir)) {
7998
+ log.error(`Change not found: specs/changes/${changeId}`);
7999
+ process.exit(1);
8053
8000
  }
8001
+ idsToMigrate.push(changeId);
8002
+ } else {
8003
+ log.error("Usage: cdd-kit migrate <change-id> | cdd-kit migrate --all [--dry-run] [--no-backup]");
8004
+ process.exit(1);
8054
8005
  }
8055
- if (stmt.type === "ExpressionStatement") {
8056
- const expr = stmt.expression;
8057
- if (isCallExpression(expr, "require")) {
8058
- const mod = extractRequireModule(expr);
8059
- if (mod) {
8060
- buckets.imports.push({ module: mod, items: [], line: stmt.loc?.start.line ?? 1 });
8006
+ if (idsToMigrate.length === 0) {
8007
+ log.info("No changes found to migrate.");
8008
+ return;
8009
+ }
8010
+ if (dryRun) {
8011
+ log.info("Dry run \u2014 no files will be written.");
8012
+ log.blank();
8013
+ }
8014
+ const sessionStamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
8015
+ let migratedCount = 0;
8016
+ let upToDateCount = 0;
8017
+ const backupRoot = join16(cwd, ".cdd", "migrate-backup", sessionStamp);
8018
+ for (const id of idsToMigrate) {
8019
+ const changeDir = join16(cwd, "specs", "changes", id);
8020
+ if (!existsSync14(changeDir)) {
8021
+ log.warn(` ${id}: directory not found \u2014 skipping`);
8022
+ continue;
8023
+ }
8024
+ const { result, pending, deletes } = migrateOne(id, changeDir, enableContextGovernance);
8025
+ const { changed, warnings } = result;
8026
+ if (changed.length === 0) {
8027
+ log.info(` ${id}: already up to date`);
8028
+ upToDateCount++;
8029
+ for (const w of warnings)
8030
+ log.warn(` ${id}: ${w}`);
8031
+ continue;
8032
+ }
8033
+ if (!dryRun) {
8034
+ try {
8035
+ if (!noBackup)
8036
+ backupChangeDir(cwd, id, sessionStamp);
8037
+ commitWritesAtomically(pending, deletes);
8038
+ } catch (err) {
8039
+ log.error(` ${id}: migration failed \u2014 ${err.message}`);
8040
+ if (!noBackup) {
8041
+ log.error(` ${id}: restore from .cdd/migrate-backup/${sessionStamp}/${id}/`);
8042
+ }
8043
+ process.exit(1);
8061
8044
  }
8062
8045
  }
8046
+ log.ok(` ${id}: migrated`);
8047
+ for (const c of changed)
8048
+ log.info(` + ${c}`);
8049
+ migratedCount++;
8050
+ for (const w of warnings)
8051
+ log.warn(` ${id}: ${w}`);
8063
8052
  }
8064
- }
8065
- function parseAndExtract(source, opts) {
8066
- let ast;
8067
- try {
8068
- ast = parseSourceWithPlugins(source, opts.plugins);
8069
- } catch {
8070
- return null;
8071
- }
8072
- const buckets = {
8073
- imports: [],
8074
- constants: [],
8075
- functions: [],
8076
- classes: [],
8077
- interfaces: [],
8078
- types: [],
8079
- enums: []
8080
- };
8081
- for (const stmt of ast.program.body) {
8082
- processStatement(stmt, buckets, !!opts.extractTsTypes);
8053
+ log.blank();
8054
+ if (dryRun) {
8055
+ log.info(`Dry run complete: ${migratedCount} change(s) would be updated, ${upToDateCount} already up to date.`);
8056
+ } else {
8057
+ log.ok(`Migration complete: ${migratedCount} updated, ${upToDateCount} already up to date.`);
8058
+ if (migratedCount > 0 && !noBackup) {
8059
+ log.info(`Backup: ${backupRoot}`);
8060
+ log.info('Next: git add specs/changes/ && git commit -m "chore: migrate changes to YAML format"');
8061
+ log.info("When stable, remove backup: rm -rf .cdd/migrate-backup/");
8062
+ }
8083
8063
  }
8084
- return buckets;
8085
- }
8086
- function countSourceLines(source) {
8087
- if (source === "")
8088
- return 0;
8089
- return source.split(/\r?\n/).length - (source.endsWith("\n") || source.endsWith("\r\n") ? 1 : 0);
8090
- }
8091
- function parseJsSource(source, relPath) {
8092
- const total_lines = countSourceLines(source);
8093
- const r = parseAndExtract(source, { plugins: JS_PLUGINS, extractTsTypes: false });
8094
- if (!r)
8095
- return null;
8096
- return {
8097
- path: relPath,
8098
- total_lines,
8099
- imports: r.imports,
8100
- constants: r.constants,
8101
- classes: r.classes,
8102
- functions: r.functions
8103
- };
8104
8064
  }
8105
- var COMMON_PLUGINS, JS_PLUGINS, JavaScriptScanner, jsScanner;
8106
- var init_javascript = __esm({
8107
- "src/code-map/scanners/javascript.ts"() {
8065
+ var init_migrate = __esm({
8066
+ "src/commands/migrate.ts"() {
8108
8067
  "use strict";
8109
- init_common();
8110
- COMMON_PLUGINS = [
8111
- "classProperties",
8112
- "classPrivateProperties",
8113
- "classPrivateMethods",
8114
- "decorators-legacy",
8115
- "topLevelAwait",
8116
- "dynamicImport",
8117
- "optionalChaining",
8118
- "nullishCoalescingOperator",
8119
- "objectRestSpread",
8120
- "asyncGenerators",
8121
- "numericSeparator",
8122
- "logicalAssignment"
8123
- ];
8124
- JS_PLUGINS = ["jsx", ...COMMON_PLUGINS];
8125
- JavaScriptScanner = class {
8126
- extensions = [".js"];
8127
- async scan(absolutePath, repoRoot) {
8128
- let source;
8129
- try {
8130
- source = readFileSync12(absolutePath, "utf8");
8131
- } catch (err) {
8132
- throw err;
8133
- }
8134
- if (isBinary(source)) {
8135
- return null;
8136
- }
8137
- const relPath = canonicalRelPath(absolutePath, repoRoot);
8138
- return parseJsSource(source, relPath);
8139
- }
8140
- };
8141
- jsScanner = new JavaScriptScanner();
8068
+ init_logger();
8142
8069
  }
8143
8070
  });
8144
8071
 
8145
- // src/code-map/scanners/typescript.ts
8146
- var typescript_exports = {};
8147
- __export(typescript_exports, {
8148
- tsScanner: () => tsScanner
8072
+ // src/commands/upgrade.ts
8073
+ var upgrade_exports = {};
8074
+ __export(upgrade_exports, {
8075
+ upgrade: () => upgrade
8149
8076
  });
8150
- import { readFileSync as readFileSync13 } from "fs";
8151
- var TypeScriptScanner, tsScanner;
8152
- var init_typescript = __esm({
8153
- "src/code-map/scanners/typescript.ts"() {
8154
- "use strict";
8155
- init_common();
8156
- init_javascript();
8157
- TypeScriptScanner = class {
8158
- extensions = [".ts", ".tsx"];
8159
- async scan(absolutePath, repoRoot) {
8160
- let source;
8161
- try {
8162
- source = readFileSync13(absolutePath, "utf8");
8163
- } catch (err) {
8164
- throw err;
8165
- }
8166
- if (isBinary(source)) {
8167
- return null;
8168
- }
8169
- const isTsx = absolutePath.toLowerCase().endsWith(".tsx");
8170
- const plugins = isTsx ? ["typescript", "jsx", ...COMMON_PLUGINS] : ["typescript", ...COMMON_PLUGINS];
8171
- const r = parseAndExtract(source, { plugins, extractTsTypes: true });
8172
- if (!r)
8173
- return null;
8174
- const relPath = canonicalRelPath(absolutePath, repoRoot);
8175
- const total_lines = countSourceLines(source);
8176
- return {
8177
- path: relPath,
8178
- total_lines,
8179
- imports: r.imports,
8180
- constants: r.constants,
8181
- classes: r.classes,
8182
- functions: r.functions,
8183
- interfaces: r.interfaces,
8184
- types: r.types,
8185
- enums: r.enums
8186
- };
8187
- }
8188
- };
8189
- tsScanner = new TypeScriptScanner();
8077
+ import { existsSync as existsSync15, mkdirSync as mkdirSync7, readdirSync as readdirSync9, copyFileSync as copyFileSync3, readFileSync as readFileSync12, writeFileSync as writeFileSync7 } from "fs";
8078
+ import { dirname as dirname4, join as join17, relative as relative3 } from "path";
8079
+ function planMissingFiles(srcDir, destDir, label, planned) {
8080
+ if (!existsSync15(srcDir))
8081
+ return;
8082
+ for (const entry of readdirSync9(srcDir, { withFileTypes: true })) {
8083
+ const src = join17(srcDir, entry.name);
8084
+ const dest = join17(destDir, entry.name);
8085
+ if (entry.isDirectory()) {
8086
+ planMissingFiles(src, dest, join17(label, entry.name), planned);
8087
+ continue;
8088
+ }
8089
+ if (!existsSync15(dest)) {
8090
+ planned.push({ src, dest, rel: join17(label, relative3(srcDir, src)) });
8091
+ }
8190
8092
  }
8191
- });
8192
-
8193
- // src/code-map/scanners/vue.ts
8194
- var vue_exports = {};
8195
- __export(vue_exports, {
8196
- vueScanner: () => vueScanner
8197
- });
8198
- import { readFileSync as readFileSync14 } from "fs";
8199
- import { parse as parse2 } from "@vue/compiler-sfc";
8200
- var VueScanner, vueScanner;
8201
- var init_vue = __esm({
8202
- "src/code-map/scanners/vue.ts"() {
8203
- "use strict";
8204
- init_common();
8205
- init_javascript();
8206
- VueScanner = class {
8207
- extensions = [".vue"];
8208
- async scan(absolutePath, repoRoot) {
8209
- let source;
8210
- try {
8211
- source = readFileSync14(absolutePath, "utf8");
8212
- } catch (err) {
8213
- throw err;
8214
- }
8215
- if (isBinary(source)) {
8216
- return null;
8217
- }
8218
- const relPath = canonicalRelPath(absolutePath, repoRoot);
8219
- const total_lines = source.split(/\r?\n/).length - (source.endsWith("\n") || source.endsWith("\r\n") ? 1 : 0);
8220
- const { descriptor } = parse2(source, { filename: absolutePath });
8221
- const allImports = [];
8222
- const allConstants = [];
8223
- const allFunctions = [];
8224
- const allClasses = [];
8225
- const warnings = [];
8226
- const scriptBlocks = [descriptor.script, descriptor.scriptSetup].filter(Boolean);
8227
- if (scriptBlocks.length === 0) {
8228
- return {
8229
- path: relPath,
8230
- total_lines,
8231
- imports: [],
8232
- constants: [],
8233
- classes: [],
8234
- functions: []
8235
- };
8236
- }
8237
- for (const block of scriptBlocks) {
8238
- if (!block)
8239
- continue;
8240
- if (block.lang === "ts" || block.lang === "tsx") {
8241
- warnings.push({
8242
- path: relPath,
8243
- message: `skipping <script lang=${block.lang}> block (TypeScript not supported in v1)`
8244
- });
8245
- continue;
8246
- }
8247
- const blockContent = block.content;
8248
- const lineOffset = block.loc.start.line;
8249
- const scriptEntry = parseJsSource(blockContent, relPath);
8250
- if (!scriptEntry) {
8251
- warnings.push({ path: relPath, message: "parse error in script block" });
8252
- continue;
8253
- }
8254
- const offset = lineOffset - 1;
8255
- for (const imp of scriptEntry.imports) {
8256
- allImports.push({ ...imp, line: imp.line + offset });
8257
- }
8258
- for (const c of scriptEntry.constants) {
8259
- allConstants.push({ ...c, line: c.line + offset });
8260
- }
8261
- for (const fn of scriptEntry.functions) {
8262
- allFunctions.push({
8263
- ...fn,
8264
- lines: [fn.lines[0] + offset, fn.lines[1] + offset]
8265
- });
8266
- }
8267
- for (const cls of scriptEntry.classes) {
8268
- allClasses.push({
8269
- ...cls,
8270
- lines: [cls.lines[0] + offset, cls.lines[1] + offset],
8271
- methods: cls.methods.map((m) => ({
8272
- ...m,
8273
- lines: [m.lines[0] + offset, m.lines[1] + offset]
8274
- }))
8275
- });
8276
- }
8277
- }
8278
- return {
8279
- path: relPath,
8280
- total_lines,
8281
- imports: allImports,
8282
- constants: allConstants,
8283
- classes: allClasses,
8284
- functions: allFunctions
8285
- };
8286
- }
8093
+ }
8094
+ function planProviderGuidance(cwd, provider, planned) {
8095
+ if (provider === "claude" || provider === "both") {
8096
+ if (!existsSync15(join17(cwd, "CLAUDE.md"))) {
8097
+ planned.push({ src: ASSET.claudeTemplate, dest: join17(cwd, "CLAUDE.md"), rel: "CLAUDE.md" });
8098
+ }
8099
+ if (!existsSync15(join17(cwd, "AGENTS.md"))) {
8100
+ planned.push({ src: ASSET.agentsTemplate, dest: join17(cwd, "AGENTS.md"), rel: "AGENTS.md" });
8101
+ }
8102
+ }
8103
+ if ((provider === "codex" || provider === "both") && !existsSync15(join17(cwd, "CODEX.md"))) {
8104
+ planned.push({ src: ASSET.codexTemplate, dest: join17(cwd, "CODEX.md"), rel: "CODEX.md" });
8105
+ }
8106
+ }
8107
+ function applyCopy(plan) {
8108
+ for (const item of plan) {
8109
+ mkdirSync7(dirname4(item.dest), { recursive: true });
8110
+ copyFileSync3(item.src, item.dest);
8111
+ }
8112
+ }
8113
+ async function upgrade(opts = {}) {
8114
+ const cwd = process.cwd();
8115
+ const requestedProvider = opts.provider ?? "auto";
8116
+ if (!validateProviderOption(requestedProvider)) {
8117
+ log.error(`Invalid provider: ${requestedProvider}. Use auto, claude, codex, or both.`);
8118
+ process.exit(1);
8119
+ }
8120
+ const provider = inferProvider(cwd, requestedProvider);
8121
+ const plan = [];
8122
+ planMissingFiles(ASSET.contracts, join17(cwd, "contracts"), "contracts", plan);
8123
+ planMissingFiles(ASSET.specsTemplates, join17(cwd, "specs", "templates"), "specs/templates", plan);
8124
+ planMissingFiles(ASSET.testsTemplates, join17(cwd, "tests", "templates"), "tests/templates", plan);
8125
+ planMissingFiles(ASSET.ci, join17(cwd, "ci"), "ci", plan);
8126
+ planMissingFiles(ASSET.githubWorkflows, join17(cwd, ".github", "workflows"), ".github/workflows", plan);
8127
+ planMissingFiles(ASSET.cddConfig, join17(cwd, ".cdd"), ".cdd", plan);
8128
+ planProviderGuidance(cwd, provider, plan);
8129
+ log.blank();
8130
+ log.info(`Upgrade provider: ${provider}`);
8131
+ if (plan.length === 0) {
8132
+ log.ok("No missing cdd-kit project files found.");
8133
+ if (opts.migrateChanges) {
8134
+ log.blank();
8135
+ log.info("Running change migration flow...");
8136
+ await migrate(void 0, {
8137
+ all: true,
8138
+ dryRun: !opts.yes,
8139
+ enableContextGovernance: opts.enableContextGovernance
8140
+ });
8141
+ }
8142
+ log.blank();
8143
+ return;
8144
+ }
8145
+ log.info(`${plan.length} missing file(s) detected:`);
8146
+ for (const item of plan)
8147
+ log.dim(` + ${item.rel.replace(/\\/g, "/")}`);
8148
+ if (!opts.yes) {
8149
+ log.blank();
8150
+ log.info("Dry run only. Re-run with --yes to write missing files.");
8151
+ if (opts.migrateChanges) {
8152
+ log.blank();
8153
+ log.info("Previewing existing change migration because --migrate-changes was requested.");
8154
+ await migrate(void 0, {
8155
+ all: true,
8156
+ dryRun: true,
8157
+ enableContextGovernance: opts.enableContextGovernance
8158
+ });
8159
+ }
8160
+ log.blank();
8161
+ return;
8162
+ }
8163
+ applyCopy(plan);
8164
+ const modelPolicyPath = join17(cwd, ".cdd", "model-policy.json");
8165
+ if (existsSync15(modelPolicyPath)) {
8166
+ let existing = {};
8167
+ try {
8168
+ existing = JSON.parse(readFileSync12(modelPolicyPath, "utf8"));
8169
+ } catch {
8170
+ }
8171
+ const merged = {
8172
+ ...existing,
8173
+ provider,
8174
+ generated_at: (/* @__PURE__ */ new Date()).toISOString(),
8175
+ roles: existing.roles && typeof existing.roles === "object" ? existing.roles : {}
8287
8176
  };
8288
- vueScanner = new VueScanner();
8177
+ writeFileSync7(modelPolicyPath, JSON.stringify(merged, null, 2) + "\n", "utf8");
8178
+ }
8179
+ log.blank();
8180
+ log.ok(`Upgrade complete: ${plan.length} missing file(s) added.`);
8181
+ log.info("Existing project guidance and contracts were preserved.");
8182
+ if (opts.migrateChanges) {
8183
+ log.blank();
8184
+ log.info("Running change migration flow...");
8185
+ await migrate(void 0, {
8186
+ all: true,
8187
+ dryRun: false,
8188
+ enableContextGovernance: opts.enableContextGovernance
8189
+ });
8190
+ }
8191
+ log.blank();
8192
+ }
8193
+ var init_upgrade = __esm({
8194
+ "src/commands/upgrade.ts"() {
8195
+ "use strict";
8196
+ init_paths();
8197
+ init_logger();
8198
+ init_provider();
8199
+ init_migrate();
8289
8200
  }
8290
8201
  });
8291
8202
 
8292
- // src/commands/code-map.ts
8293
- var code_map_exports = {};
8294
- __export(code_map_exports, {
8295
- codeMap: () => codeMap
8296
- });
8297
- import { existsSync as existsSync14, mkdirSync as mkdirSync6, readFileSync as readFileSync15, writeFileSync as writeFileSync7 } from "fs";
8298
- import { resolve, dirname as dirname4 } from "path";
8299
- import { createRequire } from "module";
8300
- import { fileURLToPath as fileURLToPath2 } from "url";
8301
- import { join as join17 } from "path";
8302
- async function codeMap(opts) {
8303
- const root = resolve(process.cwd(), opts.path);
8304
- const start = Date.now();
8305
- let cfg;
8306
- try {
8307
- cfg = loadCodeMapConfig(root);
8308
- } catch (err) {
8309
- log.error(`code-map: ${err.message}`);
8310
- return 1;
8203
+ // src/code-map/yaml-writer.ts
8204
+ function quoteScalar(s) {
8205
+ if (s.startsWith(".") || YAML_RESERVED.test(s) || s.includes(" ") || s === "" || s === "null" || s === "true" || s === "false") {
8206
+ return `'${s.replace(/'/g, "''")}'`;
8311
8207
  }
8312
- const include = [...cfg.include, ...opts.include];
8313
- const exclude = [...cfg.exclude, ...opts.exclude];
8314
- const files = walkRepo(root, { include, exclude });
8315
- const buckets = bucketByExtension(files);
8316
- const result = { entries: [], warnings: [] };
8317
- const tasks = [];
8318
- if (buckets[".py"]?.length) {
8319
- const { pythonScanner: pythonScanner2 } = await Promise.resolve().then(() => (init_python(), python_exports));
8320
- if (pythonScanner2.scanBatch) {
8321
- tasks.push(pythonScanner2.scanBatch(buckets[".py"], root));
8208
+ return s;
8209
+ }
8210
+ function quotePath(p) {
8211
+ if (YAML_RESERVED.test(p) || p.includes(" ")) {
8212
+ return `'${p.replace(/'/g, "''")}'`;
8213
+ }
8214
+ return p;
8215
+ }
8216
+ function renderItems(items) {
8217
+ if (items.length === 0)
8218
+ return "[]";
8219
+ return `[${items.map(quoteScalar).join(", ")}]`;
8220
+ }
8221
+ function truncateDecorator(d, max = 80) {
8222
+ const cleaned = d.replace(/\r?\n/g, " ");
8223
+ return cleaned.length <= max ? cleaned : cleaned.slice(0, max) + "...";
8224
+ }
8225
+ function renderClass(c) {
8226
+ const lines = [];
8227
+ lines.push(` - name: ${c.name}`);
8228
+ lines.push(` lines: ${c.lines[0]}-${c.lines[1]}`);
8229
+ if (c.methods.length > 0) {
8230
+ lines.push(" methods:");
8231
+ for (const m of c.methods) {
8232
+ const asyncPrefix = m.async ? "async " : "";
8233
+ lines.push(` - { name: ${asyncPrefix}${m.name}, lines: ${m.lines[0]}-${m.lines[1]} }`);
8322
8234
  }
8323
8235
  }
8324
- const jsFiles = [
8325
- ...buckets[".js"] ?? [],
8326
- ...buckets[".jsx"] ?? [],
8327
- ...buckets[".mjs"] ?? [],
8328
- ...buckets[".cjs"] ?? []
8329
- ];
8330
- if (jsFiles.length) {
8331
- const { jsScanner: jsScanner2 } = await Promise.resolve().then(() => (init_javascript(), javascript_exports));
8332
- tasks.push(scanInProcess(jsScanner2, jsFiles, root));
8236
+ return lines;
8237
+ }
8238
+ function renderFunction(f) {
8239
+ const asyncPrefix = f.async ? "async " : "";
8240
+ let comment = "";
8241
+ if (f.decorators.length > 0) {
8242
+ const truncated = truncateDecorator(f.decorators[0]);
8243
+ comment = ` # @${truncated}`;
8333
8244
  }
8334
- const tsFiles = [
8335
- ...buckets[".ts"] ?? [],
8336
- ...buckets[".tsx"] ?? []
8337
- ];
8338
- if (tsFiles.length) {
8339
- const { tsScanner: tsScanner2 } = await Promise.resolve().then(() => (init_typescript(), typescript_exports));
8340
- tasks.push(scanInProcess(tsScanner2, tsFiles, root));
8245
+ return ` - { name: ${asyncPrefix}${f.name}, lines: ${f.lines[0]}-${f.lines[1]} }${comment}`;
8246
+ }
8247
+ function renderTypeDef(t) {
8248
+ const exportedSuffix = t.exported ? "" : " # local";
8249
+ return ` - { name: ${t.name}, lines: ${t.lines[0]}-${t.lines[1]} }${exportedSuffix}`;
8250
+ }
8251
+ function renderEnum(e) {
8252
+ const lines = [];
8253
+ const exportedSuffix = e.exported ? "" : " # local";
8254
+ lines.push(` - name: ${e.name}`);
8255
+ lines.push(` lines: ${e.lines[0]}-${e.lines[1]}${exportedSuffix}`);
8256
+ if (e.members.length > 0) {
8257
+ lines.push(` members: [${e.members.join(", ")}]`);
8341
8258
  }
8342
- if (buckets[".vue"]?.length) {
8343
- const { vueScanner: vueScanner2 } = await Promise.resolve().then(() => (init_vue(), vue_exports));
8344
- tasks.push(scanInProcess(vueScanner2, buckets[".vue"], root));
8259
+ return lines;
8260
+ }
8261
+ function renderYaml(entries, opts) {
8262
+ const now = (/* @__PURE__ */ new Date()).toISOString().replace(/\.\d+Z$/, "Z");
8263
+ const totalSrc = entries.reduce((s, e) => s + e.total_lines, 0);
8264
+ const bodyLines = [];
8265
+ for (let i = 0; i < entries.length; i++) {
8266
+ const e = entries[i];
8267
+ if (i > 0)
8268
+ bodyLines.push("");
8269
+ const pathKey = quotePath(e.path);
8270
+ bodyLines.push(`${pathKey}: # ${e.total_lines} lines`);
8271
+ if (e.imports.length > 0) {
8272
+ bodyLines.push(" imports:");
8273
+ for (const imp of e.imports) {
8274
+ const mod = quoteScalar(imp.module);
8275
+ bodyLines.push(` - { module: ${mod}, items: ${renderItems(imp.items)}, line: ${imp.line} }`);
8276
+ }
8277
+ }
8278
+ if (e.constants.length > 0) {
8279
+ bodyLines.push(" constants:");
8280
+ for (const c of e.constants) {
8281
+ bodyLines.push(` - { name: ${c.name}, line: ${c.line} }`);
8282
+ }
8283
+ }
8284
+ if (e.classes.length > 0) {
8285
+ bodyLines.push(" classes:");
8286
+ for (const c of e.classes) {
8287
+ bodyLines.push(...renderClass(c));
8288
+ }
8289
+ }
8290
+ if (e.functions.length > 0) {
8291
+ bodyLines.push(" functions:");
8292
+ for (const f of e.functions) {
8293
+ bodyLines.push(renderFunction(f));
8294
+ }
8295
+ }
8296
+ if (e.interfaces && e.interfaces.length > 0) {
8297
+ bodyLines.push(" interfaces:");
8298
+ for (const t of e.interfaces) {
8299
+ bodyLines.push(renderTypeDef(t));
8300
+ }
8301
+ }
8302
+ if (e.types && e.types.length > 0) {
8303
+ bodyLines.push(" types:");
8304
+ for (const t of e.types) {
8305
+ bodyLines.push(renderTypeDef(t));
8306
+ }
8307
+ }
8308
+ if (e.enums && e.enums.length > 0) {
8309
+ bodyLines.push(" enums:");
8310
+ for (const en of e.enums) {
8311
+ bodyLines.push(...renderEnum(en));
8312
+ }
8313
+ }
8345
8314
  }
8346
- for (const r of await Promise.all(tasks)) {
8347
- result.entries.push(...r.entries);
8348
- result.warnings.push(...r.warnings);
8315
+ const mapLines = bodyLines.length + 2;
8316
+ const compression = totalSrc === 0 ? 0 : totalSrc / mapLines;
8317
+ const fileCount = entries.length;
8318
+ const header = [
8319
+ `# generated: ${now} by ${opts.generator}`,
8320
+ `# files: ${fileCount}, src-lines: ${totalSrc}, map-lines: ${mapLines}, compression: ${compression.toFixed(1)}x`
8321
+ ];
8322
+ return [...header, ...bodyLines].join("\n") + "\n";
8323
+ }
8324
+ var YAML_RESERVED;
8325
+ var init_yaml_writer = __esm({
8326
+ "src/code-map/yaml-writer.ts"() {
8327
+ "use strict";
8328
+ YAML_RESERVED = /[:[\]{},&*!|>'"%@`#]/;
8349
8329
  }
8350
- result.entries.sort((a, b) => a.path < b.path ? -1 : a.path > b.path ? 1 : 0);
8351
- for (const e of result.entries) {
8352
- if (e.total_lines > opts.maxLines) {
8353
- result.warnings.push({
8354
- path: e.path,
8355
- message: `file exceeds --max-lines (${e.total_lines} > ${opts.maxLines})`
8356
- });
8330
+ });
8331
+
8332
+ // src/code-map/orchestrator.ts
8333
+ async function scanInProcess(scanner, absolutePaths, repoRoot) {
8334
+ const entries = [];
8335
+ const warnings = [];
8336
+ for (const absPath of absolutePaths) {
8337
+ try {
8338
+ const entry = await scanner.scan(absPath, repoRoot);
8339
+ if (entry === null) {
8340
+ const rel = absPath.replace(/\\/g, "/").replace(repoRoot.replace(/\\/g, "/") + "/", "");
8341
+ warnings.push({ path: rel, message: "parse error (scanner returned null)" });
8342
+ } else {
8343
+ entries.push(entry);
8344
+ }
8345
+ } catch (err) {
8346
+ const rel = absPath.replace(/\\/g, "/").replace(repoRoot.replace(/\\/g, "/") + "/", "");
8347
+ warnings.push({ path: rel, message: `IO error: ${err.message}` });
8357
8348
  }
8358
8349
  }
8359
- const yamlBody = renderYaml(result.entries, { generator: `cdd-kit ${_pkg.version}` });
8360
- const totalSrc = result.entries.reduce((s, e) => s + e.total_lines, 0);
8361
- const mapLines = yamlBody.split("\n").length;
8362
- const compression = totalSrc === 0 ? 0 : totalSrc / mapLines;
8363
- const summaryLine = `scanned ${result.entries.length} files, ${totalSrc} src lines -> ${opts.out} (${mapLines} lines, compression ${compression.toFixed(1)}x)`;
8364
- for (const w of result.warnings) {
8365
- log.warn(`${w.path}: ${w.message}`);
8350
+ return { entries, warnings };
8351
+ }
8352
+ function bucketByExtension(files) {
8353
+ const out = {};
8354
+ for (const f of files) {
8355
+ const dot = f.lastIndexOf(".");
8356
+ const ext = dot >= 0 ? f.slice(dot).toLowerCase() : "";
8357
+ if (!out[ext])
8358
+ out[ext] = [];
8359
+ out[ext].push(f);
8366
8360
  }
8367
- if (opts.check) {
8368
- const existing = existsSync14(opts.out) ? readFileSync15(opts.out, "utf8") : "";
8369
- const normalize = (s) => s.replace(/^# generated: [^\n]+\n/m, "# generated: <normalized>\n");
8370
- if (normalize(existing) !== normalize(yamlBody)) {
8371
- log.error(`code-map out of date: ${opts.out} would change. Run \`cdd-kit code-map\` to regenerate.`);
8372
- return 1;
8373
- }
8374
- log.ok(`code-map up to date: ${opts.out}`);
8375
- return 0;
8361
+ return out;
8362
+ }
8363
+ var init_orchestrator = __esm({
8364
+ "src/code-map/orchestrator.ts"() {
8365
+ "use strict";
8366
+ init_include_exclude();
8376
8367
  }
8377
- mkdirSync6(dirname4(opts.out), { recursive: true });
8378
- writeFileSync7(opts.out, yamlBody, "utf8");
8379
- log.ok(`${summaryLine} (${Date.now() - start}ms)`);
8380
- return 0;
8368
+ });
8369
+
8370
+ // src/code-map/scanners/common.ts
8371
+ import { relative as relative4 } from "path";
8372
+ function canonicalRelPath(absolutePath, repoRoot) {
8373
+ const rel = relative4(repoRoot, absolutePath);
8374
+ return rel.replace(/\\/g, "/").normalize("NFC");
8381
8375
  }
8382
- var _require, _pkgPath, _pkg;
8383
- var init_code_map = __esm({
8384
- "src/commands/code-map.ts"() {
8376
+ function isAllCapsConst(name) {
8377
+ return name.length >= 2 && /^[A-Z][A-Z0-9_]*$/.test(name);
8378
+ }
8379
+ function isBinary(content) {
8380
+ const head = content.slice(0, 4096);
8381
+ return head.includes("\0");
8382
+ }
8383
+ var init_common = __esm({
8384
+ "src/code-map/scanners/common.ts"() {
8385
8385
  "use strict";
8386
- init_logger();
8387
- init_yaml_writer();
8388
- init_orchestrator();
8389
- init_config();
8390
- _require = createRequire(import.meta.url);
8391
- _pkgPath = join17(fileURLToPath2(import.meta.url), "..", "..", "..", "package.json");
8392
- _pkg = JSON.parse(readFileSync15(_pkgPath, "utf8"));
8393
8386
  }
8394
8387
  });
8395
8388
 
8396
- // src/commands/doctor.ts
8397
- var doctor_exports = {};
8398
- __export(doctor_exports, {
8399
- doctor: () => doctor
8389
+ // src/code-map/scanners/python.ts
8390
+ var python_exports = {};
8391
+ __export(python_exports, {
8392
+ pythonScanner: () => pythonScanner
8400
8393
  });
8401
- import { existsSync as existsSync15, readdirSync as readdirSync8, readFileSync as readFileSync16 } from "fs";
8402
- import { createHash as createHash4 } from "crypto";
8394
+ import { spawnSync as spawnSync2 } from "child_process";
8395
+ import { writeFileSync as writeFileSync8, unlinkSync } from "fs";
8403
8396
  import { join as join18 } from "path";
8404
- function fileExists(cwd, relPath) {
8405
- return existsSync15(join18(cwd, relPath));
8397
+ import { tmpdir } from "os";
8398
+ function detectPython2() {
8399
+ for (const candidate of ["python3", "python"]) {
8400
+ try {
8401
+ const result = spawnSync2(candidate, ["--version"], {
8402
+ encoding: "utf8",
8403
+ timeout: 5e3
8404
+ });
8405
+ if (result.status === 0)
8406
+ return candidate;
8407
+ } catch {
8408
+ }
8409
+ }
8410
+ return null;
8406
8411
  }
8407
- function findFiles(dir, predicate, found = []) {
8408
- if (!existsSync15(dir))
8409
- return found;
8410
- for (const entry of readdirSync8(dir, { withFileTypes: true })) {
8411
- const fullPath = join18(dir, entry.name);
8412
- if (entry.isDirectory())
8413
- findFiles(fullPath, predicate, found);
8414
- else if (entry.isFile() && predicate(entry.name))
8415
- found.push(fullPath);
8412
+ var TIMEOUT_MS, PythonScanner, pythonScanner;
8413
+ var init_python = __esm({
8414
+ "src/code-map/scanners/python.ts"() {
8415
+ "use strict";
8416
+ init_paths();
8417
+ init_common();
8418
+ TIMEOUT_MS = parseInt(process.env["CDD_CODE_MAP_TIMEOUT_MS"] ?? "30000", 10);
8419
+ PythonScanner = class {
8420
+ extensions = [".py"];
8421
+ _interpreter = void 0;
8422
+ getInterpreter() {
8423
+ if (this._interpreter === void 0) {
8424
+ this._interpreter = detectPython2();
8425
+ }
8426
+ return this._interpreter;
8427
+ }
8428
+ async scan(absolutePath, repoRoot) {
8429
+ const result = await this.scanBatch([absolutePath], repoRoot);
8430
+ return result.entries[0] ?? null;
8431
+ }
8432
+ async scanBatch(absolutePaths, repoRoot) {
8433
+ const interpreter = this.getInterpreter();
8434
+ const entries = [];
8435
+ const warnings = [];
8436
+ if (!interpreter) {
8437
+ const count = absolutePaths.length;
8438
+ warnings.push({
8439
+ path: "",
8440
+ message: `python interpreter not found on PATH; skipping ${count} .py file${count === 1 ? "" : "s"}`
8441
+ });
8442
+ return { entries, warnings };
8443
+ }
8444
+ const scriptPath = ASSET.codeMapPython;
8445
+ const rand = Math.random().toString(36).slice(2);
8446
+ const listFile = join18(tmpdir(), `cdd-codemap-${process.pid}-${rand}.txt`);
8447
+ writeFileSync8(listFile, absolutePaths.join("\n") + "\n", "utf8");
8448
+ let stdout = "";
8449
+ let stderr = "";
8450
+ let exitCode = 0;
8451
+ try {
8452
+ const result = spawnSync2(
8453
+ interpreter,
8454
+ [scriptPath, "--batch-file", listFile, "--repo-root", repoRoot],
8455
+ {
8456
+ encoding: "utf8",
8457
+ timeout: TIMEOUT_MS,
8458
+ maxBuffer: 50 * 1024 * 1024
8459
+ // 50MB
8460
+ }
8461
+ );
8462
+ stdout = result.stdout ?? "";
8463
+ stderr = result.stderr ?? "";
8464
+ exitCode = result.status ?? -1;
8465
+ if (result.error) {
8466
+ const errMsg = result.error.message ?? String(result.error);
8467
+ if (errMsg.includes("ENOENT")) {
8468
+ warnings.push({
8469
+ path: "",
8470
+ message: `python interpreter not found (ENOENT); skipping ${absolutePaths.length} .py file(s)`
8471
+ });
8472
+ return { entries, warnings };
8473
+ }
8474
+ if (errMsg.includes("ETIMEDOUT") || errMsg.includes("timeout")) {
8475
+ warnings.push({
8476
+ path: "",
8477
+ message: `python scanner timed out after ${TIMEOUT_MS}ms; skipping .py files`
8478
+ });
8479
+ return { entries, warnings };
8480
+ }
8481
+ warnings.push({
8482
+ path: "",
8483
+ message: `python scanner error: ${errMsg}; skipping .py files`
8484
+ });
8485
+ return { entries, warnings };
8486
+ }
8487
+ } finally {
8488
+ try {
8489
+ unlinkSync(listFile);
8490
+ } catch {
8491
+ }
8492
+ }
8493
+ if (exitCode === 3) {
8494
+ warnings.push({
8495
+ path: "",
8496
+ message: "python interpreter is < 3.9 (need ast.unparse); skipping .py files"
8497
+ });
8498
+ return { entries, warnings };
8499
+ }
8500
+ for (const line of stdout.split("\n")) {
8501
+ const trimmed = line.trim();
8502
+ if (!trimmed)
8503
+ continue;
8504
+ let parsed;
8505
+ try {
8506
+ parsed = JSON.parse(trimmed);
8507
+ } catch {
8508
+ continue;
8509
+ }
8510
+ if (!parsed.ok) {
8511
+ warnings.push({ path: parsed.path, message: parsed.error });
8512
+ continue;
8513
+ }
8514
+ const r = parsed;
8515
+ entries.push({
8516
+ path: canonicalRelPath(join18(repoRoot, r.path), repoRoot),
8517
+ total_lines: r.total_lines,
8518
+ imports: r.imports ?? [],
8519
+ constants: r.constants ?? [],
8520
+ classes: (r.classes ?? []).map((c) => ({
8521
+ name: c.name,
8522
+ lines: [c.lines[0], c.lines[1]],
8523
+ methods: (c.methods ?? []).map((m) => ({
8524
+ name: m.name,
8525
+ lines: [m.lines[0], m.lines[1]],
8526
+ async: m.async
8527
+ }))
8528
+ })),
8529
+ functions: (r.functions ?? []).map((f) => ({
8530
+ name: f.name,
8531
+ lines: [f.lines[0], f.lines[1]],
8532
+ decorators: f.decorators ?? [],
8533
+ async: f.async
8534
+ }))
8535
+ });
8536
+ }
8537
+ if (exitCode === 2) {
8538
+ warnings.push({
8539
+ path: "",
8540
+ message: `python scanner exited with code 2 (fatal); partial results only. stderr: ${stderr.slice(0, 200)}`
8541
+ });
8542
+ }
8543
+ return { entries, warnings };
8544
+ }
8545
+ };
8546
+ pythonScanner = new PythonScanner();
8416
8547
  }
8417
- return found;
8548
+ });
8549
+
8550
+ // src/code-map/scanners/javascript.ts
8551
+ var javascript_exports = {};
8552
+ __export(javascript_exports, {
8553
+ COMMON_PLUGINS: () => COMMON_PLUGINS,
8554
+ countSourceLines: () => countSourceLines,
8555
+ jsScanner: () => jsScanner,
8556
+ parseAndExtract: () => parseAndExtract,
8557
+ parseJsSource: () => parseJsSource,
8558
+ parseSourceWithPlugins: () => parseSourceWithPlugins
8559
+ });
8560
+ import { readFileSync as readFileSync14 } from "fs";
8561
+ import { parse } from "@babel/parser";
8562
+ function parseSourceWithPlugins(source, plugins) {
8563
+ return parse(source, {
8564
+ sourceType: "unambiguous",
8565
+ allowReturnOutsideFunction: true,
8566
+ allowAwaitOutsideFunction: true,
8567
+ errorRecovery: true,
8568
+ plugins
8569
+ });
8418
8570
  }
8419
- function sha256OfFile3(path) {
8420
- try {
8421
- return createHash4("sha256").update(readFileSync16(path)).digest("hex");
8422
- } catch {
8423
- return "";
8424
- }
8571
+ function isCallExpression(node, callee) {
8572
+ if (!node || node.type !== "CallExpression")
8573
+ return false;
8574
+ const c = node.callee;
8575
+ return c.type === "Identifier" && c.name === callee;
8425
8576
  }
8426
- function inputDigest(paths) {
8427
- const combined = paths.slice().sort().map((p) => `${p}:${sha256OfFile3(p)}`).join("\n");
8428
- return createHash4("sha256").update(combined).digest("hex");
8577
+ function extractRequireModule(node) {
8578
+ if (!node || node.type !== "CallExpression")
8579
+ return null;
8580
+ const c = node.callee;
8581
+ if (c.type !== "Identifier" || c.name !== "require")
8582
+ return null;
8583
+ const arg = node.arguments[0];
8584
+ if (!arg || arg.type !== "StringLiteral")
8585
+ return null;
8586
+ return arg.value;
8429
8587
  }
8430
- function readContextIndexMetadata(filePath) {
8431
- if (!existsSync15(filePath))
8432
- return {};
8433
- const text = readFileSync16(filePath, "utf8");
8434
- const out = {};
8435
- const digestMatch = text.match(/^inputs-digest:\s*([a-f0-9]+)/m);
8436
- if (digestMatch)
8437
- out.inputsDigest = digestMatch[1];
8438
- const missingMatch = text.match(/^missing-summary-count:\s*(\d+)/m);
8439
- if (missingMatch)
8440
- out.missingSummary = Number(missingMatch[1]);
8441
- return out;
8588
+ function getLineRange(node) {
8589
+ const start = node.loc?.start.line ?? 1;
8590
+ const end = node.loc?.end.line ?? start;
8591
+ return [start, end];
8442
8592
  }
8443
- function checkContextFreshness(cwd) {
8444
- const findings = [];
8445
- const projectMap = join18(cwd, "specs", "context", "project-map.md");
8446
- const contractsIndex = join18(cwd, "specs", "context", "contracts-index.md");
8447
- const contextPolicy = join18(cwd, ".cdd", "context-policy.json");
8448
- const contractFiles = findFiles(
8449
- join18(cwd, "contracts"),
8450
- (name) => name.endsWith(".md") && name !== "INDEX.md" && name !== "CHANGELOG.md"
8451
- );
8452
- if (!existsSync15(projectMap) || !existsSync15(contractsIndex)) {
8453
- findings.push({
8454
- level: "warning",
8455
- message: "specs/context indexes are missing; run cdd-kit context-scan before classification"
8456
- });
8457
- return findings;
8458
- }
8459
- const projectMapMeta = readContextIndexMetadata(projectMap);
8460
- const contractsIndexMeta = readContextIndexMetadata(contractsIndex);
8461
- const projectInputDigest = inputDigest([contextPolicy].filter(existsSync15));
8462
- if (projectMapMeta.inputsDigest === void 0) {
8463
- findings.push({
8464
- level: "warning",
8465
- message: "specs/context/project-map.md was generated by an older cdd-kit (no inputs-digest); re-run cdd-kit context-scan"
8466
- });
8467
- } else if (projectInputDigest && projectMapMeta.inputsDigest !== projectInputDigest) {
8468
- findings.push({
8469
- level: "warning",
8470
- message: "specs/context/project-map.md inputs changed (.cdd/context-policy.json); re-run cdd-kit context-scan"
8471
- });
8472
- }
8473
- const contractsInputDigest = inputDigest(contractFiles);
8474
- if (contractsIndexMeta.inputsDigest === void 0) {
8475
- findings.push({
8476
- level: "warning",
8477
- message: "specs/context/contracts-index.md was generated by an older cdd-kit (no inputs-digest); re-run cdd-kit context-scan"
8478
- });
8479
- } else if (contractsInputDigest && contractsIndexMeta.inputsDigest !== contractsInputDigest) {
8480
- findings.push({
8481
- level: "warning",
8482
- message: "specs/context/contracts-index.md inputs changed (contracts/*); re-run cdd-kit context-scan"
8483
- });
8484
- }
8485
- if (contractsIndexMeta.missingSummary !== void 0 && contractsIndexMeta.missingSummary > 0) {
8486
- findings.push({
8487
- level: "warning",
8488
- message: `contracts-index reports ${contractsIndexMeta.missingSummary} contract(s) without deterministic summary metadata`
8489
- });
8490
- }
8491
- if (findings.length === 0) {
8492
- findings.push({ level: "ok", message: "context indexes are present and fresh" });
8593
+ function processImportDeclaration(node) {
8594
+ const items = [];
8595
+ for (const spec of node.specifiers) {
8596
+ if (spec.type === "ImportDefaultSpecifier") {
8597
+ items.push(`default:${spec.local.name}`);
8598
+ } else if (spec.type === "ImportNamespaceSpecifier") {
8599
+ items.push(`*:${spec.local.name}`);
8600
+ } else if (spec.type === "ImportSpecifier") {
8601
+ const imported = spec.imported;
8602
+ items.push(imported.type === "Identifier" ? imported.name : spec.local.name);
8603
+ }
8493
8604
  }
8494
- return findings;
8605
+ return {
8606
+ module: node.source.value,
8607
+ items,
8608
+ line: node.loc?.start.line ?? 1
8609
+ };
8495
8610
  }
8496
- function readAgentModel(path) {
8497
- try {
8498
- const text = readFileSync16(path, "utf8");
8499
- const m = text.match(/^model:\s*(\S+)/m);
8500
- return m ? m[1] : null;
8501
- } catch {
8611
+ function processFunctionDeclaration(node, nameOverride) {
8612
+ const name = nameOverride ?? node.id?.name;
8613
+ if (!name)
8502
8614
  return null;
8503
- }
8615
+ return {
8616
+ name,
8617
+ lines: getLineRange(node),
8618
+ decorators: [],
8619
+ async: node.async
8620
+ };
8504
8621
  }
8505
- function checkModelPolicyDrift(cwd) {
8506
- const policyPath = join18(cwd, ".cdd", "model-policy.json");
8507
- if (!existsSync15(policyPath))
8508
- return [];
8509
- let policy;
8510
- try {
8511
- policy = JSON.parse(readFileSync16(policyPath, "utf8"));
8512
- } catch {
8513
- return [{ level: "warning", message: ".cdd/model-policy.json is not valid JSON" }];
8514
- }
8515
- const roles = policy.roles ?? {};
8516
- if (Object.keys(roles).length === 0) {
8517
- return [{
8518
- level: "warning",
8519
- message: ".cdd/model-policy.json has no role bindings; run cdd-kit upgrade to install defaults"
8520
- }];
8521
- }
8522
- const candidateDirs = [
8523
- join18(cwd, ".claude", "agents"),
8524
- process.env.HOME ? join18(process.env.HOME, ".claude", "agents") : "",
8525
- process.env.USERPROFILE ? join18(process.env.USERPROFILE, ".claude", "agents") : ""
8526
- ].filter((p) => p && existsSync15(p));
8527
- if (candidateDirs.length === 0)
8528
- return [];
8529
- const findings = [];
8530
- for (const [role, expected] of Object.entries(roles)) {
8531
- let foundAny = false;
8532
- for (const dir of candidateDirs) {
8533
- const path = join18(dir, `${role}.md`);
8534
- if (!existsSync15(path))
8535
- continue;
8536
- foundAny = true;
8537
- const actual = readAgentModel(path);
8538
- if (actual && actual !== expected) {
8539
- findings.push({
8540
- level: "warning",
8541
- message: `model-policy drift: ${role} expected ${expected}, agent prompt uses ${actual} (${path})`
8542
- });
8622
+ function processClassDeclaration(node, nameOverride) {
8623
+ const name = nameOverride ?? node.id?.name;
8624
+ if (!name)
8625
+ return null;
8626
+ const methods = [];
8627
+ for (const member of node.body.body) {
8628
+ if (member.type === "ClassMethod" || member.type === "ClassPrivateMethod") {
8629
+ const m = member;
8630
+ let methodName;
8631
+ if (m.key.type === "Identifier") {
8632
+ methodName = m.key.name;
8633
+ } else if (m.key.type === "PrivateName") {
8634
+ methodName = `#${m.key.id.name}`;
8635
+ } else {
8636
+ methodName = "<computed>";
8543
8637
  }
8638
+ methods.push({
8639
+ name: methodName,
8640
+ lines: getLineRange(m),
8641
+ async: m.async
8642
+ });
8544
8643
  }
8545
- if (!foundAny) {
8546
- }
8547
- }
8548
- if (findings.length === 0) {
8549
- findings.push({ level: "ok", message: "model-policy roles match installed agent prompts" });
8550
8644
  }
8551
- return findings;
8645
+ return {
8646
+ name,
8647
+ lines: getLineRange(node),
8648
+ methods
8649
+ };
8552
8650
  }
8553
- async function checkAgentLint(cwd) {
8554
- const agentsDir = join18(cwd, ".claude", "agents");
8555
- if (!existsSync15(agentsDir))
8556
- return [];
8557
- try {
8558
- const { readdirSync: rds, readFileSync: rfs } = await import("fs");
8559
- const files = rds(agentsDir).filter((f) => f.endsWith(".md"));
8560
- if (files.length === 0)
8561
- return [];
8562
- const findings = [];
8563
- for (const filename of files) {
8564
- const content = rfs(join18(agentsDir, filename), "utf8");
8565
- const artifactsSection = content.match(
8566
- /### Required artifacts for this agent\s*\n[\s\S]*?(?=\n#{2,3} |\n---|\s*$)/
8567
- )?.[0];
8568
- if (!artifactsSection || !/```ya?ml\s*\nartifacts:/.test(artifactsSection)) {
8569
- findings.push({
8570
- level: "warning",
8571
- message: `lint-agents: ${filename}: missing artifacts YAML block in Required artifacts section`
8572
- });
8573
- }
8574
- const readScopeCount = (content.match(/^## Read scope\s*$/gm) ?? []).length;
8575
- if (readScopeCount > 1) {
8576
- findings.push({
8577
- level: "warning",
8578
- message: `lint-agents: ${filename}: duplicate ## Read scope headings (${readScopeCount})`
8579
- });
8651
+ function processVariableDeclaration(node, imports, constants, functions) {
8652
+ for (const decl of node.declarations) {
8653
+ if (!decl.id || decl.id.type !== "Identifier")
8654
+ continue;
8655
+ const varName = decl.id.name;
8656
+ const init2 = decl.init;
8657
+ if (init2 && isCallExpression(init2, "require")) {
8658
+ const mod = extractRequireModule(init2);
8659
+ if (mod) {
8660
+ imports.push({ module: mod, items: [`default:${varName}`], line: node.loc?.start.line ?? 1 });
8580
8661
  }
8662
+ continue;
8581
8663
  }
8582
- if (findings.length === 0) {
8583
- findings.push({ level: "ok", message: "lint-agents: all agent prompts pass shape checks" });
8664
+ if (isAllCapsConst(varName) && init2 !== null && init2 !== void 0) {
8665
+ constants.push({ name: varName, line: node.loc?.start.line ?? 1 });
8666
+ continue;
8584
8667
  }
8585
- return findings;
8586
- } catch {
8587
- return [];
8668
+ if (init2 && (init2.type === "ArrowFunctionExpression" || init2.type === "FunctionExpression")) {
8669
+ functions.push({
8670
+ name: varName,
8671
+ lines: getLineRange(node),
8672
+ decorators: [],
8673
+ async: init2.async
8674
+ });
8675
+ } else if (init2 && init2.type === "CallExpression" && /^[A-Z]/.test(varName)) {
8676
+ functions.push({
8677
+ name: varName,
8678
+ lines: getLineRange(node),
8679
+ decorators: [],
8680
+ async: false
8681
+ });
8682
+ }
8683
+ }
8684
+ }
8685
+ function processTsInterfaceDeclaration(node, exported) {
8686
+ if (!node || node.type !== "TSInterfaceDeclaration")
8687
+ return null;
8688
+ if (!node.id || node.id.type !== "Identifier")
8689
+ return null;
8690
+ return {
8691
+ name: node.id.name,
8692
+ lines: getLineRange(node),
8693
+ exported
8694
+ };
8695
+ }
8696
+ function processTsTypeAliasDeclaration(node, exported) {
8697
+ if (!node || node.type !== "TSTypeAliasDeclaration")
8698
+ return null;
8699
+ if (!node.id || node.id.type !== "Identifier")
8700
+ return null;
8701
+ return {
8702
+ name: node.id.name,
8703
+ lines: getLineRange(node),
8704
+ exported
8705
+ };
8706
+ }
8707
+ function processTsEnumDeclaration(node, exported) {
8708
+ if (!node || node.type !== "TSEnumDeclaration")
8709
+ return null;
8710
+ if (!node.id || node.id.type !== "Identifier")
8711
+ return null;
8712
+ const members = [];
8713
+ for (const m of node.members ?? []) {
8714
+ if (!m || !m.id)
8715
+ continue;
8716
+ if (m.id.type === "Identifier")
8717
+ members.push(m.id.name);
8718
+ else if (m.id.type === "StringLiteral")
8719
+ members.push(m.id.value);
8588
8720
  }
8721
+ return {
8722
+ name: node.id.name,
8723
+ lines: getLineRange(node),
8724
+ exported,
8725
+ members
8726
+ };
8589
8727
  }
8590
- function checkCodeMap(cwd) {
8591
- const findings = [];
8592
- const mapPath = join18(cwd, ".cdd", "code-map.yml");
8593
- if (!existsSync15(mapPath)) {
8594
- const probe2 = checkCodeMapFreshness(cwd);
8595
- if (probe2.status === "config-error") {
8596
- findings.push({ level: "warning", message: `.cdd/code-map-config.yml is invalid: ${probe2.configError}` });
8597
- } else if (probe2.status === "missing-with-sources") {
8598
- findings.push({ level: "warning", message: ".cdd/code-map.yml is missing; run `cdd-kit code-map`" });
8728
+ function processStatement(stmt, buckets, extractTsTypes, exportedFromWrapper = false) {
8729
+ if (stmt.type === "ExportNamedDeclaration" && stmt.declaration) {
8730
+ processStatement(stmt.declaration, buckets, extractTsTypes, true);
8731
+ return;
8732
+ }
8733
+ if (stmt.type === "ExportDefaultDeclaration") {
8734
+ const decl = stmt.declaration;
8735
+ if (decl.type === "FunctionDeclaration") {
8736
+ const fe = processFunctionDeclaration(decl, decl.id?.name ?? "default");
8737
+ if (fe)
8738
+ buckets.functions.push(fe);
8739
+ } else if (decl.type === "ClassDeclaration") {
8740
+ const ce = processClassDeclaration(decl, decl.id?.name ?? "default");
8741
+ if (ce)
8742
+ buckets.classes.push(ce);
8599
8743
  }
8600
- return findings;
8744
+ return;
8601
8745
  }
8602
- const probe = checkCodeMapFreshness(cwd);
8603
- if (probe.status === "config-error") {
8604
- findings.push({ level: "warning", message: `.cdd/code-map-config.yml is invalid: ${probe.configError}` });
8605
- return findings;
8746
+ if (stmt.type === "ImportDeclaration") {
8747
+ buckets.imports.push(processImportDeclaration(stmt));
8748
+ return;
8606
8749
  }
8607
- if (probe.status === "stale") {
8608
- const top = probe.staleFiles.slice(0, 3).join(", ");
8609
- const more = probe.staleCount > 3 ? ` (+${probe.staleCount - 3} more)` : "";
8610
- findings.push({ level: "warning", message: `code-map stale: ${top}${more}; run \`cdd-kit code-map\`` });
8750
+ if (stmt.type === "FunctionDeclaration") {
8751
+ const fe = processFunctionDeclaration(stmt);
8752
+ if (fe)
8753
+ buckets.functions.push(fe);
8754
+ return;
8611
8755
  }
8612
- const text = readFileSync16(mapPath, "utf8");
8613
- const m = text.match(/^# files: (\d+), src-lines: (\d+), map-lines: (\d+), compression: ([\d.]+)x/m);
8614
- if (m) {
8615
- findings.push({ level: "ok", message: `code-map: ${m[1]} files, ${m[4]}x compression` });
8616
- } else {
8617
- findings.push({ level: "warning", message: ".cdd/code-map.yml has no metadata header (regenerate with cdd-kit 2.0.5+)" });
8756
+ if (stmt.type === "ClassDeclaration") {
8757
+ const ce = processClassDeclaration(stmt);
8758
+ if (ce)
8759
+ buckets.classes.push(ce);
8760
+ return;
8618
8761
  }
8619
- return findings;
8620
- }
8621
- async function buildDoctorReport(cwd, opts) {
8622
- const requestedProvider = opts.provider ?? "auto";
8623
- if (!validateProviderOption(requestedProvider)) {
8624
- log.error(`Invalid provider: ${requestedProvider}. Use auto, claude, codex, or both.`);
8625
- process.exit(1);
8762
+ if (stmt.type === "VariableDeclaration") {
8763
+ processVariableDeclaration(stmt, buckets.imports, buckets.constants, buckets.functions);
8764
+ return;
8626
8765
  }
8627
- const strict = opts.strict ?? false;
8628
- const provider = inferProvider(cwd, requestedProvider);
8629
- const findings = [];
8630
- for (const relPath of ["contracts", "specs/templates", ".cdd/context-policy.json", ".cdd/model-policy.json"]) {
8631
- findings.push(fileExists(cwd, relPath) ? { level: "ok", message: `${relPath} exists` } : { level: "warning", message: `${relPath} is missing; run cdd-kit upgrade --yes` });
8766
+ if (extractTsTypes) {
8767
+ const anyStmt = stmt;
8768
+ if (anyStmt.type === "TSInterfaceDeclaration") {
8769
+ const e = processTsInterfaceDeclaration(anyStmt, exportedFromWrapper);
8770
+ if (e)
8771
+ buckets.interfaces.push(e);
8772
+ return;
8773
+ }
8774
+ if (anyStmt.type === "TSTypeAliasDeclaration") {
8775
+ const e = processTsTypeAliasDeclaration(anyStmt, exportedFromWrapper);
8776
+ if (e)
8777
+ buckets.types.push(e);
8778
+ return;
8779
+ }
8780
+ if (anyStmt.type === "TSEnumDeclaration") {
8781
+ const e = processTsEnumDeclaration(anyStmt, exportedFromWrapper);
8782
+ if (e)
8783
+ buckets.enums.push(e);
8784
+ return;
8785
+ }
8632
8786
  }
8633
- if ((provider === "claude" || provider === "both") && !fileExists(cwd, "CLAUDE.md")) {
8634
- findings.push({ level: "warning", message: "CLAUDE.md is missing for Claude provider; run cdd-kit upgrade --provider claude --yes" });
8787
+ if (stmt.type === "ExpressionStatement") {
8788
+ const expr = stmt.expression;
8789
+ if (isCallExpression(expr, "require")) {
8790
+ const mod = extractRequireModule(expr);
8791
+ if (mod) {
8792
+ buckets.imports.push({ module: mod, items: [], line: stmt.loc?.start.line ?? 1 });
8793
+ }
8794
+ }
8635
8795
  }
8636
- if ((provider === "claude" || provider === "both") && !fileExists(cwd, "AGENTS.md")) {
8637
- findings.push({ level: "warning", message: "AGENTS.md is missing for Claude provider; run cdd-kit upgrade --provider claude --yes" });
8796
+ }
8797
+ function parseAndExtract(source, opts) {
8798
+ let ast;
8799
+ try {
8800
+ ast = parseSourceWithPlugins(source, opts.plugins);
8801
+ } catch {
8802
+ return null;
8638
8803
  }
8639
- if ((provider === "codex" || provider === "both") && !fileExists(cwd, "CODEX.md")) {
8640
- findings.push({ level: "warning", message: "CODEX.md is missing for Codex provider; run cdd-kit upgrade --provider codex --yes" });
8804
+ const buckets = {
8805
+ imports: [],
8806
+ constants: [],
8807
+ functions: [],
8808
+ classes: [],
8809
+ interfaces: [],
8810
+ types: [],
8811
+ enums: []
8812
+ };
8813
+ for (const stmt of ast.program.body) {
8814
+ processStatement(stmt, buckets, !!opts.extractTsTypes);
8641
8815
  }
8642
- findings.push(...checkContextFreshness(cwd));
8643
- findings.push(...checkModelPolicyDrift(cwd));
8644
- findings.push(...await checkAgentLint(cwd));
8645
- findings.push(...checkCodeMap(cwd));
8646
- const errors = findings.filter((finding) => finding.level === "error").length;
8647
- const warnings = findings.filter((finding) => finding.level === "warning").length;
8816
+ return buckets;
8817
+ }
8818
+ function countSourceLines(source) {
8819
+ if (source === "")
8820
+ return 0;
8821
+ return source.split(/\r?\n/).length - (source.endsWith("\n") || source.endsWith("\r\n") ? 1 : 0);
8822
+ }
8823
+ function parseJsSource(source, relPath) {
8824
+ const total_lines = countSourceLines(source);
8825
+ const r = parseAndExtract(source, { plugins: JS_PLUGINS, extractTsTypes: false });
8826
+ if (!r)
8827
+ return null;
8648
8828
  return {
8649
- provider,
8650
- strict,
8651
- findings,
8652
- errors,
8653
- warnings,
8654
- ok: errors === 0 && (!strict || warnings === 0)
8829
+ path: relPath,
8830
+ total_lines,
8831
+ imports: r.imports,
8832
+ constants: r.constants,
8833
+ classes: r.classes,
8834
+ functions: r.functions
8655
8835
  };
8656
8836
  }
8657
- async function attemptAutoFixes(cwd, report) {
8658
- const fixed = [];
8659
- const remaining = [];
8660
- for (const finding of report.findings) {
8661
- if (finding.level !== "warning") {
8662
- remaining.push(finding);
8663
- continue;
8664
- }
8665
- if (/specs\/context indexes are missing|inputs changed|older cdd-kit|older than/i.test(finding.message)) {
8666
- try {
8667
- const { contextScan: contextScan2 } = await Promise.resolve().then(() => (init_context_scan(), context_scan_exports));
8668
- await contextScan2();
8669
- fixed.push(`ran context-scan to refresh specs/context/`);
8670
- continue;
8671
- } catch (err) {
8672
- remaining.push({ level: "warning", message: `${finding.message} (auto-fix failed: ${err.message})` });
8673
- continue;
8837
+ var COMMON_PLUGINS, JS_PLUGINS, JavaScriptScanner, jsScanner;
8838
+ var init_javascript = __esm({
8839
+ "src/code-map/scanners/javascript.ts"() {
8840
+ "use strict";
8841
+ init_common();
8842
+ COMMON_PLUGINS = [
8843
+ "classProperties",
8844
+ "classPrivateProperties",
8845
+ "classPrivateMethods",
8846
+ "decorators-legacy",
8847
+ "topLevelAwait",
8848
+ "dynamicImport",
8849
+ "optionalChaining",
8850
+ "nullishCoalescingOperator",
8851
+ "objectRestSpread",
8852
+ "asyncGenerators",
8853
+ "numericSeparator",
8854
+ "logicalAssignment"
8855
+ ];
8856
+ JS_PLUGINS = ["jsx", ...COMMON_PLUGINS];
8857
+ JavaScriptScanner = class {
8858
+ extensions = [".js"];
8859
+ async scan(absolutePath, repoRoot) {
8860
+ let source;
8861
+ try {
8862
+ source = readFileSync14(absolutePath, "utf8");
8863
+ } catch (err) {
8864
+ throw err;
8865
+ }
8866
+ if (isBinary(source)) {
8867
+ return null;
8868
+ }
8869
+ const relPath = canonicalRelPath(absolutePath, repoRoot);
8870
+ return parseJsSource(source, relPath);
8674
8871
  }
8675
- }
8676
- if (/code-map stale|code-map\.yml is missing/i.test(finding.message)) {
8677
- try {
8678
- const { codeMap: codeMap2 } = await Promise.resolve().then(() => (init_code_map(), code_map_exports));
8679
- await codeMap2({ path: ".", out: ".cdd/code-map.yml", include: [], exclude: [], check: false, maxLines: 1e5 });
8680
- fixed.push("regenerated .cdd/code-map.yml");
8681
- continue;
8682
- } catch (err) {
8683
- remaining.push({ level: "warning", message: `${finding.message} (auto-fix failed: ${err.message})` });
8684
- continue;
8872
+ };
8873
+ jsScanner = new JavaScriptScanner();
8874
+ }
8875
+ });
8876
+
8877
+ // src/code-map/scanners/typescript.ts
8878
+ var typescript_exports = {};
8879
+ __export(typescript_exports, {
8880
+ tsScanner: () => tsScanner
8881
+ });
8882
+ import { readFileSync as readFileSync15 } from "fs";
8883
+ var TypeScriptScanner, tsScanner;
8884
+ var init_typescript = __esm({
8885
+ "src/code-map/scanners/typescript.ts"() {
8886
+ "use strict";
8887
+ init_common();
8888
+ init_javascript();
8889
+ TypeScriptScanner = class {
8890
+ extensions = [".ts", ".tsx"];
8891
+ async scan(absolutePath, repoRoot) {
8892
+ let source;
8893
+ try {
8894
+ source = readFileSync15(absolutePath, "utf8");
8895
+ } catch (err) {
8896
+ throw err;
8897
+ }
8898
+ if (isBinary(source)) {
8899
+ return null;
8900
+ }
8901
+ const isTsx = absolutePath.toLowerCase().endsWith(".tsx");
8902
+ const plugins = isTsx ? ["typescript", "jsx", ...COMMON_PLUGINS] : ["typescript", ...COMMON_PLUGINS];
8903
+ const r = parseAndExtract(source, { plugins, extractTsTypes: true });
8904
+ if (!r)
8905
+ return null;
8906
+ const relPath = canonicalRelPath(absolutePath, repoRoot);
8907
+ const total_lines = countSourceLines(source);
8908
+ return {
8909
+ path: relPath,
8910
+ total_lines,
8911
+ imports: r.imports,
8912
+ constants: r.constants,
8913
+ classes: r.classes,
8914
+ functions: r.functions,
8915
+ interfaces: r.interfaces,
8916
+ types: r.types,
8917
+ enums: r.enums
8918
+ };
8685
8919
  }
8686
- }
8687
- if (/model-policy\.json has no role bindings/i.test(finding.message)) {
8688
- const policyPath = join18(cwd, ".cdd", "model-policy.json");
8689
- try {
8690
- let existing = {};
8920
+ };
8921
+ tsScanner = new TypeScriptScanner();
8922
+ }
8923
+ });
8924
+
8925
+ // src/code-map/scanners/vue.ts
8926
+ var vue_exports = {};
8927
+ __export(vue_exports, {
8928
+ vueScanner: () => vueScanner
8929
+ });
8930
+ import { readFileSync as readFileSync16 } from "fs";
8931
+ import { parse as parse2 } from "@vue/compiler-sfc";
8932
+ var VueScanner, vueScanner;
8933
+ var init_vue = __esm({
8934
+ "src/code-map/scanners/vue.ts"() {
8935
+ "use strict";
8936
+ init_common();
8937
+ init_javascript();
8938
+ VueScanner = class {
8939
+ extensions = [".vue"];
8940
+ async scan(absolutePath, repoRoot) {
8941
+ let source;
8691
8942
  try {
8692
- existing = JSON.parse(readFileSync16(policyPath, "utf8"));
8693
- } catch {
8943
+ source = readFileSync16(absolutePath, "utf8");
8944
+ } catch (err) {
8945
+ throw err;
8694
8946
  }
8695
- const merged = {
8696
- ...existing,
8697
- roles: {
8698
- "change-classifier": "claude-opus-4-7",
8699
- "spec-architect": "claude-opus-4-7",
8700
- "qa-reviewer": "claude-opus-4-7",
8701
- "contract-reviewer": "claude-sonnet-4-6",
8702
- "test-strategist": "claude-sonnet-4-6",
8703
- "backend-engineer": "claude-sonnet-4-6",
8704
- "frontend-engineer": "claude-sonnet-4-6",
8705
- "ci-cd-gatekeeper": "claude-sonnet-4-6",
8706
- "e2e-resilience-engineer": "claude-sonnet-4-6",
8707
- "monkey-test-engineer": "claude-sonnet-4-6",
8708
- "stress-soak-engineer": "claude-sonnet-4-6",
8709
- "ui-ux-reviewer": "claude-sonnet-4-6",
8710
- "visual-reviewer": "claude-haiku-4-5-20251001",
8711
- "dependency-security-reviewer": "claude-sonnet-4-6",
8712
- "spec-drift-auditor": "claude-opus-4-7",
8713
- "repo-context-scanner": "claude-haiku-4-5-20251001"
8947
+ if (isBinary(source)) {
8948
+ return null;
8949
+ }
8950
+ const relPath = canonicalRelPath(absolutePath, repoRoot);
8951
+ const total_lines = source.split(/\r?\n/).length - (source.endsWith("\n") || source.endsWith("\r\n") ? 1 : 0);
8952
+ const { descriptor } = parse2(source, { filename: absolutePath });
8953
+ const allImports = [];
8954
+ const allConstants = [];
8955
+ const allFunctions = [];
8956
+ const allClasses = [];
8957
+ const warnings = [];
8958
+ const scriptBlocks = [descriptor.script, descriptor.scriptSetup].filter(Boolean);
8959
+ if (scriptBlocks.length === 0) {
8960
+ return {
8961
+ path: relPath,
8962
+ total_lines,
8963
+ imports: [],
8964
+ constants: [],
8965
+ classes: [],
8966
+ functions: []
8967
+ };
8968
+ }
8969
+ for (const block of scriptBlocks) {
8970
+ if (!block)
8971
+ continue;
8972
+ if (block.lang === "ts" || block.lang === "tsx") {
8973
+ warnings.push({
8974
+ path: relPath,
8975
+ message: `skipping <script lang=${block.lang}> block (TypeScript not supported in v1)`
8976
+ });
8977
+ continue;
8978
+ }
8979
+ const blockContent = block.content;
8980
+ const lineOffset = block.loc.start.line;
8981
+ const scriptEntry = parseJsSource(blockContent, relPath);
8982
+ if (!scriptEntry) {
8983
+ warnings.push({ path: relPath, message: "parse error in script block" });
8984
+ continue;
8985
+ }
8986
+ const offset = lineOffset - 1;
8987
+ for (const imp of scriptEntry.imports) {
8988
+ allImports.push({ ...imp, line: imp.line + offset });
8989
+ }
8990
+ for (const c of scriptEntry.constants) {
8991
+ allConstants.push({ ...c, line: c.line + offset });
8992
+ }
8993
+ for (const fn of scriptEntry.functions) {
8994
+ allFunctions.push({
8995
+ ...fn,
8996
+ lines: [fn.lines[0] + offset, fn.lines[1] + offset]
8997
+ });
8998
+ }
8999
+ for (const cls of scriptEntry.classes) {
9000
+ allClasses.push({
9001
+ ...cls,
9002
+ lines: [cls.lines[0] + offset, cls.lines[1] + offset],
9003
+ methods: cls.methods.map((m) => ({
9004
+ ...m,
9005
+ lines: [m.lines[0] + offset, m.lines[1] + offset]
9006
+ }))
9007
+ });
8714
9008
  }
9009
+ }
9010
+ return {
9011
+ path: relPath,
9012
+ total_lines,
9013
+ imports: allImports,
9014
+ constants: allConstants,
9015
+ classes: allClasses,
9016
+ functions: allFunctions
8715
9017
  };
8716
- const { writeFileSync: writeFileSync13 } = await import("fs");
8717
- writeFileSync13(policyPath, JSON.stringify(merged, null, 2) + "\n", "utf8");
8718
- fixed.push(`populated .cdd/model-policy.json with default role bindings`);
8719
- continue;
8720
- } catch (err) {
8721
- remaining.push({ level: "warning", message: `${finding.message} (auto-fix failed: ${err.message})` });
8722
- continue;
8723
9018
  }
8724
- }
8725
- if (/\.cdd\/.*is missing|run cdd-kit upgrade/i.test(finding.message)) {
8726
- remaining.push({
8727
- level: "warning",
8728
- message: `${finding.message} (run \`cdd-kit upgrade --yes\` manually \u2014 too invasive for --fix)`
8729
- });
8730
- continue;
8731
- }
8732
- remaining.push(finding);
9019
+ };
9020
+ vueScanner = new VueScanner();
8733
9021
  }
8734
- return { fixed, remaining };
8735
- }
8736
- async function doctor(opts = {}) {
8737
- const cwd = process.cwd();
8738
- let report = await buildDoctorReport(cwd, opts);
8739
- if (opts.fix && !opts.json) {
8740
- log.blank();
8741
- log.info("Doctor --fix: attempting safe auto-resolutions\u2026");
8742
- const { fixed, remaining } = await attemptAutoFixes(cwd, report);
8743
- for (const f of fixed)
8744
- log.ok(`fixed: ${f}`);
8745
- if (fixed.length > 0) {
8746
- report = await buildDoctorReport(cwd, opts);
8747
- } else {
8748
- log.info("no auto-fixable findings");
9022
+ });
9023
+
9024
+ // src/commands/code-map.ts
9025
+ var code_map_exports = {};
9026
+ __export(code_map_exports, {
9027
+ codeMap: () => codeMap
9028
+ });
9029
+ import { existsSync as existsSync16, mkdirSync as mkdirSync8, readFileSync as readFileSync17, writeFileSync as writeFileSync9 } from "fs";
9030
+ import { resolve, dirname as dirname5 } from "path";
9031
+ import { createRequire } from "module";
9032
+ import { fileURLToPath as fileURLToPath2 } from "url";
9033
+ import { join as join19 } from "path";
9034
+ async function codeMap(opts) {
9035
+ const root = resolve(process.cwd(), opts.path);
9036
+ const start = Date.now();
9037
+ let cfg;
9038
+ try {
9039
+ cfg = loadCodeMapConfig(root);
9040
+ } catch (err) {
9041
+ log.error(`code-map: ${err.message}`);
9042
+ return 1;
9043
+ }
9044
+ const include = [...cfg.include, ...opts.include];
9045
+ const exclude = [...cfg.exclude, ...opts.exclude];
9046
+ const files = walkRepo(root, { include, exclude });
9047
+ const buckets = bucketByExtension(files);
9048
+ const result = { entries: [], warnings: [] };
9049
+ const tasks = [];
9050
+ if (buckets[".py"]?.length) {
9051
+ const { pythonScanner: pythonScanner2 } = await Promise.resolve().then(() => (init_python(), python_exports));
9052
+ if (pythonScanner2.scanBatch) {
9053
+ tasks.push(pythonScanner2.scanBatch(buckets[".py"], root));
8749
9054
  }
8750
9055
  }
8751
- if (opts.json) {
8752
- console.log(JSON.stringify(report, null, 2));
8753
- if (!report.ok)
8754
- process.exit(1);
8755
- return;
9056
+ const jsFiles = [
9057
+ ...buckets[".js"] ?? [],
9058
+ ...buckets[".jsx"] ?? [],
9059
+ ...buckets[".mjs"] ?? [],
9060
+ ...buckets[".cjs"] ?? []
9061
+ ];
9062
+ if (jsFiles.length) {
9063
+ const { jsScanner: jsScanner2 } = await Promise.resolve().then(() => (init_javascript(), javascript_exports));
9064
+ tasks.push(scanInProcess(jsScanner2, jsFiles, root));
8756
9065
  }
8757
- log.blank();
8758
- log.info(`Doctor provider: ${report.provider}`);
8759
- for (const finding of report.findings) {
8760
- if (finding.level === "ok")
8761
- log.ok(finding.message);
8762
- else if (finding.level === "warning")
8763
- log.warn(finding.message);
8764
- else
8765
- log.error(finding.message);
9066
+ const tsFiles = [
9067
+ ...buckets[".ts"] ?? [],
9068
+ ...buckets[".tsx"] ?? []
9069
+ ];
9070
+ if (tsFiles.length) {
9071
+ const { tsScanner: tsScanner2 } = await Promise.resolve().then(() => (init_typescript(), typescript_exports));
9072
+ tasks.push(scanInProcess(tsScanner2, tsFiles, root));
8766
9073
  }
8767
- log.blank();
8768
- if (!report.ok) {
8769
- log.error(report.strict && report.errors === 0 ? `doctor failed in strict mode with ${report.warnings} warning(s)` : `doctor failed with ${report.errors} error(s)`);
8770
- process.exit(1);
9074
+ if (buckets[".vue"]?.length) {
9075
+ const { vueScanner: vueScanner2 } = await Promise.resolve().then(() => (init_vue(), vue_exports));
9076
+ tasks.push(scanInProcess(vueScanner2, buckets[".vue"], root));
9077
+ }
9078
+ for (const r of await Promise.all(tasks)) {
9079
+ result.entries.push(...r.entries);
9080
+ result.warnings.push(...r.warnings);
9081
+ }
9082
+ result.entries.sort((a, b) => a.path < b.path ? -1 : a.path > b.path ? 1 : 0);
9083
+ for (const e of result.entries) {
9084
+ if (e.total_lines > opts.maxLines) {
9085
+ result.warnings.push({
9086
+ path: e.path,
9087
+ message: `file exceeds --max-lines (${e.total_lines} > ${opts.maxLines})`
9088
+ });
9089
+ }
9090
+ }
9091
+ const yamlBody = renderYaml(result.entries, { generator: `cdd-kit ${_pkg.version}` });
9092
+ const totalSrc = result.entries.reduce((s, e) => s + e.total_lines, 0);
9093
+ const mapLines = yamlBody.split("\n").length;
9094
+ const compression = totalSrc === 0 ? 0 : totalSrc / mapLines;
9095
+ const summaryLine = `scanned ${result.entries.length} files, ${totalSrc} src lines -> ${opts.out} (${mapLines} lines, compression ${compression.toFixed(1)}x)`;
9096
+ for (const w of result.warnings) {
9097
+ log.warn(`${w.path}: ${w.message}`);
8771
9098
  }
8772
- if (report.warnings > 0) {
8773
- log.warn(`doctor completed with ${report.warnings} warning(s)`);
8774
- } else {
8775
- log.ok("doctor passed");
9099
+ if (opts.check) {
9100
+ const existing = existsSync16(opts.out) ? readFileSync17(opts.out, "utf8") : "";
9101
+ const normalize = (s) => s.replace(/^# generated: [^\n]+\n/m, "# generated: <normalized>\n");
9102
+ if (normalize(existing) !== normalize(yamlBody)) {
9103
+ log.error(`code-map out of date: ${opts.out} would change. Run \`cdd-kit code-map\` to regenerate.`);
9104
+ return 1;
9105
+ }
9106
+ log.ok(`code-map up to date: ${opts.out}`);
9107
+ return 0;
8776
9108
  }
8777
- log.blank();
9109
+ mkdirSync8(dirname5(opts.out), { recursive: true });
9110
+ writeFileSync9(opts.out, yamlBody, "utf8");
9111
+ log.ok(`${summaryLine} (${Date.now() - start}ms)`);
9112
+ return 0;
8778
9113
  }
8779
- var init_doctor = __esm({
8780
- "src/commands/doctor.ts"() {
9114
+ var _require, _pkgPath, _pkg;
9115
+ var init_code_map = __esm({
9116
+ "src/commands/code-map.ts"() {
8781
9117
  "use strict";
8782
9118
  init_logger();
8783
- init_provider();
8784
- init_freshness();
9119
+ init_yaml_writer();
9120
+ init_orchestrator();
9121
+ init_config();
9122
+ _require = createRequire(import.meta.url);
9123
+ _pkgPath = join19(fileURLToPath2(import.meta.url), "..", "..", "..", "package.json");
9124
+ _pkg = JSON.parse(readFileSync17(_pkgPath, "utf8"));
8785
9125
  }
8786
9126
  });
8787
9127
 
8788
- // src/commands/migrate.ts
8789
- var migrate_exports = {};
8790
- __export(migrate_exports, {
8791
- migrate: () => migrate
9128
+ // src/commands/refresh.ts
9129
+ var refresh_exports = {};
9130
+ __export(refresh_exports, {
9131
+ refresh: () => refresh
8792
9132
  });
8793
- import { join as join19 } from "path";
8794
- import { cpSync as cpSync2, existsSync as existsSync16, mkdirSync as mkdirSync7, readdirSync as readdirSync9, readFileSync as readFileSync17, renameSync, rmSync as rmSync2, writeFileSync as writeFileSync8 } from "fs";
8795
- import yaml3 from "js-yaml";
8796
- function backupChangeDir(cwd, changeId, sessionStamp) {
8797
- const backupRoot = join19(cwd, ".cdd", "migrate-backup", sessionStamp);
8798
- const backupDir2 = join19(backupRoot, changeId);
8799
- mkdirSync7(backupRoot, { recursive: true });
8800
- const sourceDir = join19(cwd, "specs", "changes", changeId);
8801
- if (existsSync16(sourceDir)) {
8802
- cpSync2(sourceDir, backupDir2, { recursive: true });
8803
- }
8804
- return backupDir2;
9133
+ import { existsSync as existsSync17, mkdirSync as mkdirSync9, readdirSync as readdirSync10, copyFileSync as copyFileSync4, readFileSync as readFileSync18, writeFileSync as writeFileSync10 } from "fs";
9134
+ import { dirname as dirname6, join as join20, relative as relative5 } from "path";
9135
+ import { createHash as createHash4 } from "crypto";
9136
+ function fileHash2(filePath) {
9137
+ return createHash4("sha256").update(readFileSync18(filePath)).digest("hex");
8805
9138
  }
8806
- function buildLegacyContextManifest(changeId) {
8807
- return [
8808
- "# Context Manifest",
8809
- "",
8810
- "Generated by `cdd-kit migrate` for an existing change.",
8811
- "Legacy manifest. Forbidden paths come from `.cdd/context-policy.json`.",
8812
- "",
8813
- "## Affected Surfaces",
8814
- "- legacy-unknown",
8815
- "",
8816
- "## Allowed Paths",
8817
- `- specs/changes/${changeId}/`,
8818
- "",
8819
- "## Required Contracts",
8820
- "- legacy-unknown",
8821
- "",
8822
- "## Required Tests",
8823
- "- legacy-unknown",
8824
- "",
8825
- "## Agent Work Packets",
8826
- "",
8827
- "## Context Expansion Requests",
8828
- "-",
8829
- "",
8830
- "## Approved Expansions",
8831
- "-",
8832
- ""
8833
- ].join("\n");
9139
+ function planForceRefresh(srcDir, destDir, sectionLabel) {
9140
+ const plan = [];
9141
+ if (!existsSync17(srcDir))
9142
+ return plan;
9143
+ function walk(curSrc, curDest) {
9144
+ let items;
9145
+ try {
9146
+ items = readdirSync10(curSrc, { withFileTypes: true });
9147
+ } catch {
9148
+ return;
9149
+ }
9150
+ for (const item of items) {
9151
+ const sp = join20(curSrc, item.name);
9152
+ const dp = join20(curDest, item.name);
9153
+ if (item.isDirectory()) {
9154
+ walk(sp, dp);
9155
+ continue;
9156
+ }
9157
+ if (!item.isFile())
9158
+ continue;
9159
+ const rel = join20(sectionLabel, relative5(srcDir, sp)).replace(/\\/g, "/");
9160
+ if (!existsSync17(dp)) {
9161
+ plan.push({ src: sp, dest: dp, rel, action: "add" });
9162
+ } else if (fileHash2(sp) !== fileHash2(dp)) {
9163
+ plan.push({ src: sp, dest: dp, rel, action: "overwrite" });
9164
+ } else {
9165
+ plan.push({ src: sp, dest: dp, rel, action: "skip" });
9166
+ }
9167
+ }
9168
+ }
9169
+ walk(srcDir, destDir);
9170
+ return plan;
8834
9171
  }
8835
- function buildContextGovernedManifest(changeId) {
8836
- return [
8837
- "# Context Manifest",
8838
- "",
8839
- "Generated by `cdd-kit migrate --enable-context-governance` for an existing change.",
8840
- "Review and narrow the allowed paths before assigning implementation work.",
8841
- "Forbidden paths come from `.cdd/context-policy.json`.",
8842
- "",
8843
- "## Affected Surfaces",
8844
- "- legacy-unknown",
8845
- "",
8846
- "## Allowed Paths",
8847
- `- specs/changes/${changeId}/`,
8848
- "- specs/context/project-map.md",
8849
- "- specs/context/contracts-index.md",
8850
- "",
8851
- "## Required Contracts",
8852
- "- legacy-unknown",
8853
- "",
8854
- "## Required Tests",
8855
- "- legacy-unknown",
8856
- "",
8857
- "## Agent Work Packets",
8858
- "",
8859
- "### change-classifier",
8860
- "- allowed:",
8861
- ` - specs/changes/${changeId}/`,
8862
- " - specs/context/project-map.md",
8863
- " - specs/context/contracts-index.md",
8864
- "",
8865
- "## Context Expansion Requests",
8866
- "",
8867
- "<!--",
8868
- "Agents must request context expansion instead of reading outside their work packet.",
8869
- "Use this format only for real requests:",
8870
- "",
8871
- "- request-id: CER-001",
8872
- " requested_paths:",
8873
- " - src/example.ts",
8874
- " reason: why this file is required",
8875
- " status: pending",
8876
- "-->",
8877
- "",
8878
- "## Approved Expansions",
8879
- "-",
8880
- ""
8881
- ].join("\n");
9172
+ function planSingleFile(src, dest, rel) {
9173
+ if (!existsSync17(src))
9174
+ return null;
9175
+ if (!existsSync17(dest))
9176
+ return { src, dest, rel, action: "add" };
9177
+ if (fileHash2(src) !== fileHash2(dest))
9178
+ return { src, dest, rel, action: "overwrite" };
9179
+ return { src, dest, rel, action: "skip" };
9180
+ }
9181
+ function applyPlan(plan, backupRoot) {
9182
+ let added = 0;
9183
+ let overwritten = 0;
9184
+ for (const item of plan) {
9185
+ if (item.action === "skip")
9186
+ continue;
9187
+ if (item.action === "overwrite") {
9188
+ const backupPath = join20(backupRoot, item.rel);
9189
+ mkdirSync9(dirname6(backupPath), { recursive: true });
9190
+ copyFileSync4(item.dest, backupPath);
9191
+ overwritten += 1;
9192
+ } else {
9193
+ added += 1;
9194
+ }
9195
+ mkdirSync9(dirname6(item.dest), { recursive: true });
9196
+ copyFileSync4(item.src, item.dest);
9197
+ }
9198
+ return { added, overwritten };
8882
9199
  }
8883
- function parseLegacyFrontmatter(content) {
9200
+ function parseAgentFrontmatter(content) {
8884
9201
  const m = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
8885
9202
  if (!m)
8886
9203
  return {};
8887
- const out = {};
9204
+ const fm = {};
8888
9205
  for (const line of m[1].split(/\r?\n/)) {
8889
- const colon = line.indexOf(":");
8890
- if (colon === -1)
8891
- continue;
8892
- const key = line.slice(0, colon).trim();
8893
- if (!key)
8894
- continue;
8895
- out[key] = line.slice(colon + 1).trim();
8896
- }
8897
- return out;
8898
- }
8899
- function parseListField(raw) {
8900
- if (!raw)
8901
- return [];
8902
- const trimmed = raw.trim();
8903
- if (!trimmed || trimmed === "[]")
8904
- return [];
8905
- const inner = trimmed.startsWith("[") && trimmed.endsWith("]") ? trimmed.slice(1, -1) : trimmed;
8906
- return inner.split(",").map((item) => item.trim().replace(/^["']|["']$/g, "")).filter(Boolean);
8907
- }
8908
- function parseLegacyTaskList(body) {
8909
- const lines = body.split(/\r?\n/);
8910
- const rows = [];
8911
- let currentSection;
8912
- for (const raw of lines) {
8913
- const headerMatch = raw.match(/^##\s+\d+\.\s+(.*)\s*$/);
8914
- if (headerMatch) {
8915
- currentSection = headerMatch[1].trim();
9206
+ const km = line.match(/^([a-zA-Z_-]+):\s*(.+?)\s*$/);
9207
+ if (!km)
8916
9208
  continue;
9209
+ if (km[1] === "name")
9210
+ fm.name = km[2].trim();
9211
+ if (km[1] === "model")
9212
+ fm.model = km[2].trim();
9213
+ }
9214
+ return fm;
9215
+ }
9216
+ function resyncModelPolicy(cwd) {
9217
+ const policyPath = join20(cwd, ".cdd", "model-policy.json");
9218
+ const result = { changed: false, diff: [], policyPath };
9219
+ if (!existsSync17(AGENTS_HOME))
9220
+ return result;
9221
+ const desired = {};
9222
+ const agentFiles = readdirSync10(AGENTS_HOME, { withFileTypes: true }).filter((d) => d.isFile() && d.name.endsWith(".md"));
9223
+ for (const f of agentFiles) {
9224
+ const content = readFileSync18(join20(AGENTS_HOME, f.name), "utf8");
9225
+ const fm = parseAgentFrontmatter(content);
9226
+ if (fm.name && fm.model)
9227
+ desired[fm.name] = fm.model;
9228
+ }
9229
+ if (Object.keys(desired).length === 0)
9230
+ return result;
9231
+ let existing = {};
9232
+ if (existsSync17(policyPath)) {
9233
+ try {
9234
+ existing = JSON.parse(readFileSync18(policyPath, "utf8"));
9235
+ } catch {
8917
9236
  }
8918
- const itemMatch = raw.match(/^\s*-\s*\[([ xX\-])\]\s+(\d+(?:\.\d+)*)\s+(.*)\s*$/);
8919
- if (!itemMatch)
8920
- continue;
8921
- const mark = itemMatch[1];
8922
- const id = itemMatch[2];
8923
- const title = itemMatch[3].trim();
8924
- let status = "pending";
8925
- if (mark === "x" || mark === "X")
8926
- status = "done";
8927
- else if (mark === "-")
8928
- status = "skipped";
8929
- rows.push({ id, title, status, section: currentSection });
8930
9237
  }
8931
- return rows;
8932
- }
8933
- function migrateTasksFile(changeId, changeDir, enableContextGovernance, detectedTier, changed, warnings, pendingWrites, pendingDeletes) {
8934
- const newPath = join19(changeDir, "tasks.yml");
8935
- const legacyPath = join19(changeDir, "tasks.md");
8936
- if (existsSync16(newPath)) {
8937
- return;
9238
+ const existingRoles = existing.roles && typeof existing.roles === "object" ? existing.roles : {};
9239
+ for (const [agent, model] of Object.entries(desired)) {
9240
+ if (existingRoles[agent] !== model) {
9241
+ result.diff.push({ agent, from: existingRoles[agent] ?? null, to: model });
9242
+ }
8938
9243
  }
8939
- if (!existsSync16(legacyPath)) {
8940
- warnings.push("tasks.md not found and tasks.yml missing \u2014 skipping tasks migration");
8941
- return;
9244
+ if (result.diff.length === 0)
9245
+ return result;
9246
+ const merged = {
9247
+ ...existing,
9248
+ roles: desired
9249
+ };
9250
+ if (!("schema-version" in merged))
9251
+ merged["schema-version"] = "0.2.0";
9252
+ if (!("provider" in merged))
9253
+ merged["provider"] = "claude";
9254
+ mkdirSync9(dirname6(policyPath), { recursive: true });
9255
+ writeFileSync10(policyPath, JSON.stringify(merged, null, 2) + "\n", "utf8");
9256
+ result.changed = true;
9257
+ return result;
9258
+ }
9259
+ function planTemplateRefresh(cwd) {
9260
+ const sections = [];
9261
+ sections.push({
9262
+ name: "specs/templates",
9263
+ plan: planForceRefresh(ASSET.specsTemplates, join20(cwd, "specs", "templates"), "specs/templates")
9264
+ });
9265
+ sections.push({
9266
+ name: "tests/templates",
9267
+ plan: planForceRefresh(ASSET.testsTemplates, join20(cwd, "tests", "templates"), "tests/templates")
9268
+ });
9269
+ const ciTemplatesAsset = join20(ASSET.ci, "..", "ci-templates");
9270
+ if (existsSync17(ciTemplatesAsset)) {
9271
+ sections.push({
9272
+ name: "ci-templates",
9273
+ plan: planForceRefresh(ciTemplatesAsset, join20(cwd, "ci-templates"), "ci-templates")
9274
+ });
8942
9275
  }
8943
- const raw = readFileSync17(legacyPath, "utf8");
8944
- const fm = parseLegacyFrontmatter(raw);
8945
- const bodyMatch = raw.match(/^---\r?\n[\s\S]*?\r?\n---\r?\n?([\s\S]*)$/);
8946
- const body = bodyMatch ? bodyMatch[1] : raw;
8947
- const tasksRows = parseLegacyTaskList(body);
8948
- const data = {};
8949
- data["change-id"] = fm["change-id"] || changeId;
8950
- data["status"] = fm["status"] || "in-progress";
8951
- if (fm["tier"] && /^\d+$/.test(fm["tier"])) {
8952
- data["tier"] = parseInt(fm["tier"], 10);
8953
- } else if (detectedTier) {
8954
- data["tier"] = parseInt(detectedTier, 10);
9276
+ const wfAsset = join20(ASSET.githubWorkflows, "contract-driven-gates.yml");
9277
+ const wfDest = join20(cwd, ".github", "workflows", "contract-driven-gates.yml");
9278
+ const wfPlan = planSingleFile(wfAsset, wfDest, ".github/workflows/contract-driven-gates.yml");
9279
+ if (wfPlan)
9280
+ sections.push({ name: ".github/workflows/contract-driven-gates.yml", plan: [wfPlan] });
9281
+ const total = sections.flatMap((s) => s.plan);
9282
+ return { sections, total };
9283
+ }
9284
+ function summarizePlan(items) {
9285
+ return {
9286
+ add: items.filter((i) => i.action === "add").length,
9287
+ overwrite: items.filter((i) => i.action === "overwrite").length,
9288
+ skip: items.filter((i) => i.action === "skip").length
9289
+ };
9290
+ }
9291
+ async function refresh(opts) {
9292
+ const cwd = process.cwd();
9293
+ const apply = !!opts.yes;
9294
+ log.blank();
9295
+ log.info(apply ? "cdd-kit refresh \u2014 applying changes" : "cdd-kit refresh \u2014 dry-run preview (re-run with --yes to apply)");
9296
+ log.blank();
9297
+ if (!opts.noUpdate) {
9298
+ log.info("[1/6] ~/.claude/agents and ~/.claude/skills (via cdd-kit update)");
9299
+ await update({ yes: apply, provider: opts.provider ?? "auto" });
8955
9300
  } else {
8956
- data["tier"] = null;
9301
+ log.dim("[1/6] skipped (--no-update)");
8957
9302
  }
8958
- if (enableContextGovernance || fm["context-governance"] === "v1") {
8959
- data["context-governance"] = "v1";
9303
+ log.blank();
9304
+ if (!opts.noUpgrade) {
9305
+ log.info("[2/6] add missing project files (via cdd-kit upgrade)");
9306
+ await upgrade({ yes: apply, provider: opts.provider ?? "auto" });
9307
+ } else {
9308
+ log.dim("[2/6] skipped (--no-upgrade)");
8960
9309
  }
8961
- const archive2 = parseListField(fm["archive-tasks"]);
8962
- data["archive-tasks"] = archive2.length > 0 ? archive2 : ["7.1", "7.2"];
8963
- const deps = parseListField(fm["depends-on"]);
8964
- data["depends-on"] = deps;
8965
- data["tasks"] = tasksRows.map((r) => {
8966
- const out = { id: r.id, title: r.title, status: r.status };
8967
- if (r.section)
8968
- out["section"] = r.section;
8969
- return out;
8970
- });
8971
- const yamlOut = yaml3.dump(data, { lineWidth: -1, noRefs: true });
8972
- pendingWrites.push({ path: newPath, content: yamlOut });
8973
- pendingDeletes.push({ path: legacyPath });
8974
- changed.push(`tasks.md -> tasks.yml (${tasksRows.length} task(s) migrated)`);
8975
- }
8976
- function parseLegacyAgentLog(content) {
8977
- const lines = content.split(/\r?\n/);
8978
- const data = {};
8979
- let i = 0;
8980
- const topFieldRe = /^[ ]{0,1}-\s*([\w-]+):\s*(.*)$/;
8981
- while (i < lines.length) {
8982
- const line = lines[i];
8983
- const fieldMatch = line.match(topFieldRe);
8984
- if (!fieldMatch) {
8985
- i++;
8986
- continue;
8987
- }
8988
- const key = fieldMatch[1];
8989
- const inline = fieldMatch[2].trim();
8990
- if (key === "files-read" || key === "artifacts") {
8991
- const items = [];
8992
- let j = i + 1;
8993
- while (j < lines.length) {
8994
- const sub = lines[j];
8995
- if (topFieldRe.test(sub))
8996
- break;
8997
- if (/^#/.test(sub))
8998
- break;
8999
- const itemMatch = sub.match(/^\s{2,}-\s+(.+?)\s*$/);
9000
- if (itemMatch) {
9001
- items.push(itemMatch[1].trim());
9310
+ log.blank();
9311
+ let templateAdded = 0;
9312
+ let templateOverwritten = 0;
9313
+ let backupRoot = null;
9314
+ if (!opts.noTemplates) {
9315
+ log.info("[3/6] force-refresh kit-shipped templates");
9316
+ const { sections, total } = planTemplateRefresh(cwd);
9317
+ const summary = summarizePlan(total);
9318
+ if (summary.add === 0 && summary.overwrite === 0) {
9319
+ log.ok(" templates already up to date");
9320
+ } else {
9321
+ for (const s of sections) {
9322
+ const ss = summarizePlan(s.plan);
9323
+ if (ss.add === 0 && ss.overwrite === 0)
9324
+ continue;
9325
+ log.info(` ${s.name}: +${ss.add} ~${ss.overwrite} =${ss.skip}`);
9326
+ for (const item of s.plan.filter((i) => i.action !== "skip")) {
9327
+ log.dim(` ${item.action === "add" ? "+" : "~"} ${item.rel}`);
9002
9328
  }
9003
- j++;
9004
9329
  }
9005
- if (key === "files-read") {
9006
- data["files-read"] = items;
9330
+ if (apply) {
9331
+ const ts = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
9332
+ backupRoot = join20(cwd, ".cdd", ".refresh-backup", ts);
9333
+ const result = applyPlan(total, backupRoot);
9334
+ templateAdded = result.added;
9335
+ templateOverwritten = result.overwritten;
9336
+ log.ok(` applied: +${templateAdded} added, ~${templateOverwritten} overwritten`);
9337
+ if (templateOverwritten > 0) {
9338
+ log.info(` backup saved to: ${relative5(cwd, backupRoot).replace(/\\/g, "/")}`);
9339
+ }
9007
9340
  } else {
9008
- data["artifacts"] = items.map((s) => {
9009
- const idx = s.indexOf(":");
9010
- if (idx === -1) {
9011
- return { type: "note", pointer: s };
9012
- }
9013
- const type = s.slice(0, idx).trim();
9014
- const pointer = s.slice(idx + 1).trim();
9015
- return { type, pointer };
9016
- });
9341
+ log.dim(" (dry-run \u2014 no changes written)");
9017
9342
  }
9018
- i = j;
9019
- continue;
9020
9343
  }
9021
- data[key] = inline;
9022
- i++;
9023
- }
9024
- return data;
9025
- }
9026
- function migrateAgentLogs(changeDir, changed, pendingWrites, pendingDeletes) {
9027
- const agentLogDir = join19(changeDir, "agent-log");
9028
- if (!existsSync16(agentLogDir))
9029
- return;
9030
- const mdLogs = readdirSync9(agentLogDir).filter((f) => f.endsWith(".md"));
9031
- for (const f of mdLogs) {
9032
- const fullPath = join19(agentLogDir, f);
9033
- const yamlName = f.replace(/\.md$/, ".yml");
9034
- const yamlFull = join19(agentLogDir, yamlName);
9035
- if (existsSync16(yamlFull))
9036
- continue;
9037
- const raw = readFileSync17(fullPath, "utf8");
9038
- const parsed = parseLegacyAgentLog(raw);
9039
- const yamlOut = yaml3.dump(parsed, { lineWidth: -1, noRefs: true });
9040
- pendingWrites.push({ path: yamlFull, content: yamlOut });
9041
- pendingDeletes.push({ path: fullPath });
9042
- changed.push(`agent-log/${f} -> agent-log/${yamlName}`);
9344
+ } else {
9345
+ log.dim("[3/6] skipped (--no-templates)");
9043
9346
  }
9044
- }
9045
- function migrateOne(changeId, changeDir, enableContextGovernance) {
9046
- const changed = [];
9047
- const warnings = [];
9048
- const pending = [];
9049
- const deletes = [];
9050
- let detectedTier = null;
9051
- const classifPath = join19(changeDir, "change-classification.md");
9052
- if (existsSync16(classifPath)) {
9053
- const content = readFileSync17(classifPath, "utf8");
9054
- const hasNewTierFormat = /^## Tier\s*\n\s*-\s*\d\s*$/m.test(content);
9055
- const oldMatch = content.match(/\*\*Tier[:\*]+\s*(?:Tier\s*)?(\d)/i) ?? content.match(/^-?\s*Tier:\s*(?:Tier\s*)?(\d)/mi);
9056
- if (oldMatch)
9057
- detectedTier = oldMatch[1];
9058
- if (!hasNewTierFormat) {
9059
- if (detectedTier) {
9060
- const addition = `
9061
- ## Tier
9062
- - ${detectedTier}
9063
- `;
9064
- if (!content.includes("\n## Tier\n")) {
9065
- changed.push(
9066
- `change-classification.md: appended "## Tier\\n- ${detectedTier}" (converted from old format)`
9067
- );
9068
- pending.push({ path: classifPath, content: content + addition });
9347
+ log.blank();
9348
+ if (!opts.noHooks) {
9349
+ const markerPath = join20(cwd, HOOKS_MARKER_PATH);
9350
+ if (existsSync17(markerPath)) {
9351
+ log.info("[4/6] re-install code-map pre-commit hook (marker found)");
9352
+ if (apply) {
9353
+ try {
9354
+ await installCodeMapHook(cwd);
9355
+ } catch (err) {
9356
+ log.warn(` hook re-install skipped: ${err.message}`);
9069
9357
  }
9070
9358
  } else {
9071
- warnings.push(
9072
- "change-classification.md: could not detect tier (no **Tier:** N or ## Tier N found). Set `tier: <0-5>` in tasks.yml frontmatter to enable tier-based gate checks."
9073
- );
9359
+ log.dim(" (dry-run \u2014 would re-install)");
9074
9360
  }
9075
9361
  } else {
9076
- const structured = content.match(/^## Tier\s*\n\s*-\s*(\d)\s*$/m);
9077
- if (structured)
9078
- detectedTier = structured[1];
9362
+ log.dim("[4/6] no .cdd/.hooks-installed marker \u2014 skipping (run `cdd-kit init --hooks` to install)");
9079
9363
  }
9364
+ } else {
9365
+ log.dim("[4/6] skipped (--no-hooks)");
9080
9366
  }
9081
- migrateTasksFile(changeId, changeDir, enableContextGovernance, detectedTier, changed, warnings, pending, deletes);
9082
- migrateAgentLogs(changeDir, changed, pending, deletes);
9083
- const manifestPath = join19(changeDir, "context-manifest.md");
9084
- if (!existsSync16(manifestPath)) {
9085
- changed.push(enableContextGovernance ? "context-manifest.md: added context-governance v1 manifest scaffold" : "context-manifest.md: added legacy context manifest scaffold");
9086
- pending.push({
9087
- path: manifestPath,
9088
- content: enableContextGovernance ? buildContextGovernedManifest(changeId) : buildLegacyContextManifest(changeId)
9089
- });
9090
- } else if (enableContextGovernance) {
9091
- warnings.push("context-manifest.md already exists \u2014 review allowed paths before relying on context-governance: v1");
9092
- }
9093
- return { result: { changed, warnings }, pending, deletes };
9094
- }
9095
- function commitWritesAtomically(pending, deletes) {
9096
- const renames = [];
9097
- try {
9098
- for (const write of pending) {
9099
- const tmp = `${write.path}.cdd-migrate.tmp`;
9100
- writeFileSync8(tmp, write.content, "utf8");
9101
- renames.push({ tmp, final: write.path });
9367
+ log.blank();
9368
+ log.info("[5/6] resync .cdd/model-policy.json roles from agent frontmatter");
9369
+ if (apply) {
9370
+ const r = resyncModelPolicy(cwd);
9371
+ if (r.diff.length === 0) {
9372
+ log.ok(" roles already match agent prompts");
9373
+ } else {
9374
+ for (const d of r.diff) {
9375
+ log.info(` ${d.agent}: ${d.from ?? "(absent)"} \u2192 ${d.to}`);
9376
+ }
9377
+ log.ok(` ${r.diff.length} role binding(s) updated`);
9102
9378
  }
9103
- } catch (err) {
9104
- for (const r of renames) {
9379
+ } else {
9380
+ const fakeApply = () => {
9381
+ };
9382
+ fakeApply();
9383
+ log.dim(" (dry-run \u2014 drift will be reported only when applied)");
9384
+ }
9385
+ log.blank();
9386
+ if (!opts.noCodeMap) {
9387
+ log.info("[6/6] regenerate .cdd/code-map.yml");
9388
+ if (apply) {
9105
9389
  try {
9106
- rmSync2(r.tmp, { force: true });
9107
- } catch {
9390
+ await codeMap({
9391
+ path: ".",
9392
+ out: ".cdd/code-map.yml",
9393
+ include: [],
9394
+ exclude: [],
9395
+ check: false,
9396
+ maxLines: 600
9397
+ });
9398
+ } catch (err) {
9399
+ log.warn(` code-map skipped: ${err.message}`);
9108
9400
  }
9401
+ } else {
9402
+ log.dim(" (dry-run \u2014 would regenerate)");
9109
9403
  }
9110
- throw err;
9404
+ } else {
9405
+ log.dim("[6/6] skipped (--no-code-map)");
9111
9406
  }
9112
- for (const r of renames) {
9113
- renameSync(r.tmp, r.final);
9407
+ log.blank();
9408
+ if (apply) {
9409
+ log.ok("refresh complete.");
9410
+ if (backupRoot) {
9411
+ log.info(`Backup of overwritten templates: ${relative5(cwd, backupRoot).replace(/\\/g, "/")}`);
9412
+ }
9413
+ log.info("Next: review changes with `git diff`, then commit:");
9414
+ log.dim(" git add .cdd/code-map.yml .cdd/model-policy.json specs/templates tests/templates ci-templates .github/workflows");
9415
+ log.dim(' git commit -m "chore: cdd-kit refresh"');
9416
+ } else {
9417
+ log.info("Dry-run finished. Re-run with `--yes` to apply.");
9114
9418
  }
9115
- for (const d of deletes) {
9116
- try {
9117
- rmSync2(d.path, { force: true });
9118
- } catch {
9119
- }
9419
+ log.blank();
9420
+ }
9421
+ var HOOKS_MARKER_PATH;
9422
+ var init_refresh = __esm({
9423
+ "src/commands/refresh.ts"() {
9424
+ "use strict";
9425
+ init_paths();
9426
+ init_logger();
9427
+ init_update();
9428
+ init_upgrade();
9429
+ init_code_map();
9430
+ init_code_map_hook();
9431
+ HOOKS_MARKER_PATH = ".cdd/.hooks-installed";
9120
9432
  }
9433
+ });
9434
+
9435
+ // src/commands/doctor.ts
9436
+ var doctor_exports = {};
9437
+ __export(doctor_exports, {
9438
+ doctor: () => doctor
9439
+ });
9440
+ import { existsSync as existsSync18, readdirSync as readdirSync11, readFileSync as readFileSync19 } from "fs";
9441
+ import { createHash as createHash5 } from "crypto";
9442
+ import { join as join21 } from "path";
9443
+ function fileExists(cwd, relPath) {
9444
+ return existsSync18(join21(cwd, relPath));
9121
9445
  }
9122
- async function migrate(changeId, opts = {}) {
9123
- const cwd = process.cwd();
9124
- const dryRun = opts.dryRun ?? false;
9125
- const enableContextGovernance = opts.enableContextGovernance ?? false;
9126
- const noBackup = opts.noBackup ?? false;
9127
- const idsToMigrate = [];
9128
- if (opts.all) {
9129
- const changesDir = join19(cwd, "specs", "changes");
9130
- if (!existsSync16(changesDir)) {
9131
- log.info("No specs/changes/ directory found \u2014 nothing to migrate.");
9132
- return;
9133
- }
9134
- idsToMigrate.push(
9135
- ...readdirSync9(changesDir, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name)
9136
- );
9137
- } else if (changeId) {
9138
- const specificDir = join19(cwd, "specs", "changes", changeId);
9139
- if (!existsSync16(specificDir)) {
9140
- log.error(`Change not found: specs/changes/${changeId}`);
9141
- process.exit(1);
9142
- }
9143
- idsToMigrate.push(changeId);
9144
- } else {
9145
- log.error("Usage: cdd-kit migrate <change-id> | cdd-kit migrate --all [--dry-run] [--no-backup]");
9146
- process.exit(1);
9446
+ function findFiles(dir, predicate, found = []) {
9447
+ if (!existsSync18(dir))
9448
+ return found;
9449
+ for (const entry of readdirSync11(dir, { withFileTypes: true })) {
9450
+ const fullPath = join21(dir, entry.name);
9451
+ if (entry.isDirectory())
9452
+ findFiles(fullPath, predicate, found);
9453
+ else if (entry.isFile() && predicate(entry.name))
9454
+ found.push(fullPath);
9147
9455
  }
9148
- if (idsToMigrate.length === 0) {
9149
- log.info("No changes found to migrate.");
9150
- return;
9456
+ return found;
9457
+ }
9458
+ function sha256OfFile3(path) {
9459
+ try {
9460
+ return createHash5("sha256").update(readFileSync19(path)).digest("hex");
9461
+ } catch {
9462
+ return "";
9151
9463
  }
9152
- if (dryRun) {
9153
- log.info("Dry run \u2014 no files will be written.");
9154
- log.blank();
9464
+ }
9465
+ function inputDigest(paths) {
9466
+ const combined = paths.slice().sort().map((p) => `${p}:${sha256OfFile3(p)}`).join("\n");
9467
+ return createHash5("sha256").update(combined).digest("hex");
9468
+ }
9469
+ function readContextIndexMetadata(filePath) {
9470
+ if (!existsSync18(filePath))
9471
+ return {};
9472
+ const text = readFileSync19(filePath, "utf8");
9473
+ const out = {};
9474
+ const digestMatch = text.match(/^inputs-digest:\s*([a-f0-9]+)/m);
9475
+ if (digestMatch)
9476
+ out.inputsDigest = digestMatch[1];
9477
+ const missingMatch = text.match(/^missing-summary-count:\s*(\d+)/m);
9478
+ if (missingMatch)
9479
+ out.missingSummary = Number(missingMatch[1]);
9480
+ return out;
9481
+ }
9482
+ function checkContextFreshness(cwd) {
9483
+ const findings = [];
9484
+ const projectMap = join21(cwd, "specs", "context", "project-map.md");
9485
+ const contractsIndex = join21(cwd, "specs", "context", "contracts-index.md");
9486
+ const contextPolicy = join21(cwd, ".cdd", "context-policy.json");
9487
+ const contractFiles = findFiles(
9488
+ join21(cwd, "contracts"),
9489
+ (name) => name.endsWith(".md") && name !== "INDEX.md" && name !== "CHANGELOG.md"
9490
+ );
9491
+ if (!existsSync18(projectMap) || !existsSync18(contractsIndex)) {
9492
+ findings.push({
9493
+ level: "warning",
9494
+ message: "specs/context indexes are missing; run cdd-kit context-scan before classification"
9495
+ });
9496
+ return findings;
9155
9497
  }
9156
- const sessionStamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
9157
- let migratedCount = 0;
9158
- let upToDateCount = 0;
9159
- const backupRoot = join19(cwd, ".cdd", "migrate-backup", sessionStamp);
9160
- for (const id of idsToMigrate) {
9161
- const changeDir = join19(cwd, "specs", "changes", id);
9162
- if (!existsSync16(changeDir)) {
9163
- log.warn(` ${id}: directory not found \u2014 skipping`);
9164
- continue;
9165
- }
9166
- const { result, pending, deletes } = migrateOne(id, changeDir, enableContextGovernance);
9167
- const { changed, warnings } = result;
9168
- if (changed.length === 0) {
9169
- log.info(` ${id}: already up to date`);
9170
- upToDateCount++;
9171
- for (const w of warnings)
9172
- log.warn(` ${id}: ${w}`);
9173
- continue;
9174
- }
9175
- if (!dryRun) {
9176
- try {
9177
- if (!noBackup)
9178
- backupChangeDir(cwd, id, sessionStamp);
9179
- commitWritesAtomically(pending, deletes);
9180
- } catch (err) {
9181
- log.error(` ${id}: migration failed \u2014 ${err.message}`);
9182
- if (!noBackup) {
9183
- log.error(` ${id}: restore from .cdd/migrate-backup/${sessionStamp}/${id}/`);
9184
- }
9185
- process.exit(1);
9498
+ const projectMapMeta = readContextIndexMetadata(projectMap);
9499
+ const contractsIndexMeta = readContextIndexMetadata(contractsIndex);
9500
+ const projectInputDigest = inputDigest([contextPolicy].filter(existsSync18));
9501
+ if (projectMapMeta.inputsDigest === void 0) {
9502
+ findings.push({
9503
+ level: "warning",
9504
+ message: "specs/context/project-map.md was generated by an older cdd-kit (no inputs-digest); re-run cdd-kit context-scan"
9505
+ });
9506
+ } else if (projectInputDigest && projectMapMeta.inputsDigest !== projectInputDigest) {
9507
+ findings.push({
9508
+ level: "warning",
9509
+ message: "specs/context/project-map.md inputs changed (.cdd/context-policy.json); re-run cdd-kit context-scan"
9510
+ });
9511
+ }
9512
+ const contractsInputDigest = inputDigest(contractFiles);
9513
+ if (contractsIndexMeta.inputsDigest === void 0) {
9514
+ findings.push({
9515
+ level: "warning",
9516
+ message: "specs/context/contracts-index.md was generated by an older cdd-kit (no inputs-digest); re-run cdd-kit context-scan"
9517
+ });
9518
+ } else if (contractsInputDigest && contractsIndexMeta.inputsDigest !== contractsInputDigest) {
9519
+ findings.push({
9520
+ level: "warning",
9521
+ message: "specs/context/contracts-index.md inputs changed (contracts/*); re-run cdd-kit context-scan"
9522
+ });
9523
+ }
9524
+ if (contractsIndexMeta.missingSummary !== void 0 && contractsIndexMeta.missingSummary > 0) {
9525
+ findings.push({
9526
+ level: "warning",
9527
+ message: `contracts-index reports ${contractsIndexMeta.missingSummary} contract(s) without deterministic summary metadata`
9528
+ });
9529
+ }
9530
+ if (findings.length === 0) {
9531
+ findings.push({ level: "ok", message: "context indexes are present and fresh" });
9532
+ }
9533
+ return findings;
9534
+ }
9535
+ function readAgentModel(path) {
9536
+ try {
9537
+ const text = readFileSync19(path, "utf8");
9538
+ const m = text.match(/^model:\s*(\S+)/m);
9539
+ return m ? m[1] : null;
9540
+ } catch {
9541
+ return null;
9542
+ }
9543
+ }
9544
+ function checkModelPolicyDrift(cwd) {
9545
+ const policyPath = join21(cwd, ".cdd", "model-policy.json");
9546
+ if (!existsSync18(policyPath))
9547
+ return [];
9548
+ let policy;
9549
+ try {
9550
+ policy = JSON.parse(readFileSync19(policyPath, "utf8"));
9551
+ } catch {
9552
+ return [{ level: "warning", message: ".cdd/model-policy.json is not valid JSON" }];
9553
+ }
9554
+ const roles = policy.roles ?? {};
9555
+ if (Object.keys(roles).length === 0) {
9556
+ return [{
9557
+ level: "warning",
9558
+ message: ".cdd/model-policy.json has no role bindings; run cdd-kit upgrade to install defaults"
9559
+ }];
9560
+ }
9561
+ const candidateDirs = [
9562
+ join21(cwd, ".claude", "agents"),
9563
+ process.env.HOME ? join21(process.env.HOME, ".claude", "agents") : "",
9564
+ process.env.USERPROFILE ? join21(process.env.USERPROFILE, ".claude", "agents") : ""
9565
+ ].filter((p) => p && existsSync18(p));
9566
+ if (candidateDirs.length === 0)
9567
+ return [];
9568
+ const findings = [];
9569
+ for (const [role, expected] of Object.entries(roles)) {
9570
+ let foundAny = false;
9571
+ for (const dir of candidateDirs) {
9572
+ const path = join21(dir, `${role}.md`);
9573
+ if (!existsSync18(path))
9574
+ continue;
9575
+ foundAny = true;
9576
+ const actual = readAgentModel(path);
9577
+ if (actual && actual !== expected) {
9578
+ findings.push({
9579
+ level: "warning",
9580
+ message: `model-policy drift: ${role} expected ${expected}, agent prompt uses ${actual} (${path})`
9581
+ });
9186
9582
  }
9187
9583
  }
9188
- log.ok(` ${id}: migrated`);
9189
- for (const c of changed)
9190
- log.info(` + ${c}`);
9191
- migratedCount++;
9192
- for (const w of warnings)
9193
- log.warn(` ${id}: ${w}`);
9194
- }
9195
- log.blank();
9196
- if (dryRun) {
9197
- log.info(`Dry run complete: ${migratedCount} change(s) would be updated, ${upToDateCount} already up to date.`);
9198
- } else {
9199
- log.ok(`Migration complete: ${migratedCount} updated, ${upToDateCount} already up to date.`);
9200
- if (migratedCount > 0 && !noBackup) {
9201
- log.info(`Backup: ${backupRoot}`);
9202
- log.info('Next: git add specs/changes/ && git commit -m "chore: migrate changes to YAML format"');
9203
- log.info("When stable, remove backup: rm -rf .cdd/migrate-backup/");
9584
+ if (!foundAny) {
9204
9585
  }
9205
9586
  }
9206
- }
9207
- var init_migrate = __esm({
9208
- "src/commands/migrate.ts"() {
9209
- "use strict";
9210
- init_logger();
9587
+ if (findings.length === 0) {
9588
+ findings.push({ level: "ok", message: "model-policy roles match installed agent prompts" });
9211
9589
  }
9212
- });
9213
-
9214
- // src/commands/upgrade.ts
9215
- var upgrade_exports = {};
9216
- __export(upgrade_exports, {
9217
- upgrade: () => upgrade
9218
- });
9219
- import { existsSync as existsSync17, mkdirSync as mkdirSync8, readdirSync as readdirSync10, copyFileSync as copyFileSync3, readFileSync as readFileSync18, writeFileSync as writeFileSync9 } from "fs";
9220
- import { dirname as dirname5, join as join20, relative as relative4 } from "path";
9221
- function planMissingFiles(srcDir, destDir, label, planned) {
9222
- if (!existsSync17(srcDir))
9223
- return;
9224
- for (const entry of readdirSync10(srcDir, { withFileTypes: true })) {
9225
- const src = join20(srcDir, entry.name);
9226
- const dest = join20(destDir, entry.name);
9227
- if (entry.isDirectory()) {
9228
- planMissingFiles(src, dest, join20(label, entry.name), planned);
9229
- continue;
9590
+ return findings;
9591
+ }
9592
+ async function checkAgentLint(cwd) {
9593
+ const agentsDir = join21(cwd, ".claude", "agents");
9594
+ if (!existsSync18(agentsDir))
9595
+ return [];
9596
+ try {
9597
+ const { readdirSync: rds, readFileSync: rfs } = await import("fs");
9598
+ const files = rds(agentsDir).filter((f) => f.endsWith(".md"));
9599
+ if (files.length === 0)
9600
+ return [];
9601
+ const findings = [];
9602
+ for (const filename of files) {
9603
+ const content = rfs(join21(agentsDir, filename), "utf8");
9604
+ const artifactsSection = content.match(
9605
+ /### Required artifacts for this agent\s*\n[\s\S]*?(?=\n#{2,3} |\n---|\s*$)/
9606
+ )?.[0];
9607
+ if (!artifactsSection || !/```ya?ml\s*\nartifacts:/.test(artifactsSection)) {
9608
+ findings.push({
9609
+ level: "warning",
9610
+ message: `lint-agents: ${filename}: missing artifacts YAML block in Required artifacts section`
9611
+ });
9612
+ }
9613
+ const readScopeCount = (content.match(/^## Read scope\s*$/gm) ?? []).length;
9614
+ if (readScopeCount > 1) {
9615
+ findings.push({
9616
+ level: "warning",
9617
+ message: `lint-agents: ${filename}: duplicate ## Read scope headings (${readScopeCount})`
9618
+ });
9619
+ }
9230
9620
  }
9231
- if (!existsSync17(dest)) {
9232
- planned.push({ src, dest, rel: join20(label, relative4(srcDir, src)) });
9621
+ if (findings.length === 0) {
9622
+ findings.push({ level: "ok", message: "lint-agents: all agent prompts pass shape checks" });
9233
9623
  }
9624
+ return findings;
9625
+ } catch {
9626
+ return [];
9234
9627
  }
9235
9628
  }
9236
- function planProviderGuidance(cwd, provider, planned) {
9237
- if (provider === "claude" || provider === "both") {
9238
- if (!existsSync17(join20(cwd, "CLAUDE.md"))) {
9239
- planned.push({ src: ASSET.claudeTemplate, dest: join20(cwd, "CLAUDE.md"), rel: "CLAUDE.md" });
9240
- }
9241
- if (!existsSync17(join20(cwd, "AGENTS.md"))) {
9242
- planned.push({ src: ASSET.agentsTemplate, dest: join20(cwd, "AGENTS.md"), rel: "AGENTS.md" });
9629
+ function checkCodeMap(cwd) {
9630
+ const findings = [];
9631
+ const mapPath = join21(cwd, ".cdd", "code-map.yml");
9632
+ if (!existsSync18(mapPath)) {
9633
+ const probe2 = checkCodeMapFreshness(cwd);
9634
+ if (probe2.status === "config-error") {
9635
+ findings.push({ level: "warning", message: `.cdd/code-map-config.yml is invalid: ${probe2.configError}` });
9636
+ } else if (probe2.status === "missing-with-sources") {
9637
+ findings.push({ level: "warning", message: ".cdd/code-map.yml is missing; run `cdd-kit code-map`" });
9243
9638
  }
9639
+ return findings;
9640
+ }
9641
+ const probe = checkCodeMapFreshness(cwd);
9642
+ if (probe.status === "config-error") {
9643
+ findings.push({ level: "warning", message: `.cdd/code-map-config.yml is invalid: ${probe.configError}` });
9644
+ return findings;
9244
9645
  }
9245
- if ((provider === "codex" || provider === "both") && !existsSync17(join20(cwd, "CODEX.md"))) {
9246
- planned.push({ src: ASSET.codexTemplate, dest: join20(cwd, "CODEX.md"), rel: "CODEX.md" });
9646
+ if (probe.status === "stale") {
9647
+ const top = probe.staleFiles.slice(0, 3).join(", ");
9648
+ const more = probe.staleCount > 3 ? ` (+${probe.staleCount - 3} more)` : "";
9649
+ findings.push({ level: "warning", message: `code-map stale: ${top}${more}; run \`cdd-kit code-map\`` });
9247
9650
  }
9248
- }
9249
- function applyCopy(plan) {
9250
- for (const item of plan) {
9251
- mkdirSync8(dirname5(item.dest), { recursive: true });
9252
- copyFileSync3(item.src, item.dest);
9651
+ const text = readFileSync19(mapPath, "utf8");
9652
+ const m = text.match(/^# files: (\d+), src-lines: (\d+), map-lines: (\d+), compression: ([\d.]+)x/m);
9653
+ if (m) {
9654
+ findings.push({ level: "ok", message: `code-map: ${m[1]} files, ${m[4]}x compression` });
9655
+ } else {
9656
+ findings.push({ level: "warning", message: ".cdd/code-map.yml has no metadata header (regenerate with cdd-kit 2.0.5+)" });
9253
9657
  }
9658
+ return findings;
9254
9659
  }
9255
- async function upgrade(opts = {}) {
9256
- const cwd = process.cwd();
9660
+ async function buildDoctorReport(cwd, opts) {
9257
9661
  const requestedProvider = opts.provider ?? "auto";
9258
9662
  if (!validateProviderOption(requestedProvider)) {
9259
9663
  log.error(`Invalid provider: ${requestedProvider}. Use auto, claude, codex, or both.`);
9260
9664
  process.exit(1);
9261
9665
  }
9666
+ const strict = opts.strict ?? false;
9262
9667
  const provider = inferProvider(cwd, requestedProvider);
9263
- const plan = [];
9264
- planMissingFiles(ASSET.contracts, join20(cwd, "contracts"), "contracts", plan);
9265
- planMissingFiles(ASSET.specsTemplates, join20(cwd, "specs", "templates"), "specs/templates", plan);
9266
- planMissingFiles(ASSET.testsTemplates, join20(cwd, "tests", "templates"), "tests/templates", plan);
9267
- planMissingFiles(ASSET.ci, join20(cwd, "ci"), "ci", plan);
9268
- planMissingFiles(ASSET.githubWorkflows, join20(cwd, ".github", "workflows"), ".github/workflows", plan);
9269
- planMissingFiles(ASSET.cddConfig, join20(cwd, ".cdd"), ".cdd", plan);
9270
- planProviderGuidance(cwd, provider, plan);
9271
- log.blank();
9272
- log.info(`Upgrade provider: ${provider}`);
9273
- if (plan.length === 0) {
9274
- log.ok("No missing cdd-kit project files found.");
9275
- if (opts.migrateChanges) {
9276
- log.blank();
9277
- log.info("Running change migration flow...");
9278
- await migrate(void 0, {
9279
- all: true,
9280
- dryRun: !opts.yes,
9281
- enableContextGovernance: opts.enableContextGovernance
9282
- });
9283
- }
9284
- log.blank();
9285
- return;
9668
+ const findings = [];
9669
+ for (const relPath of ["contracts", "specs/templates", ".cdd/context-policy.json", ".cdd/model-policy.json"]) {
9670
+ findings.push(fileExists(cwd, relPath) ? { level: "ok", message: `${relPath} exists` } : { level: "warning", message: `${relPath} is missing; run cdd-kit upgrade --yes` });
9286
9671
  }
9287
- log.info(`${plan.length} missing file(s) detected:`);
9288
- for (const item of plan)
9289
- log.dim(` + ${item.rel.replace(/\\/g, "/")}`);
9290
- if (!opts.yes) {
9291
- log.blank();
9292
- log.info("Dry run only. Re-run with --yes to write missing files.");
9293
- if (opts.migrateChanges) {
9294
- log.blank();
9295
- log.info("Previewing existing change migration because --migrate-changes was requested.");
9296
- await migrate(void 0, {
9297
- all: true,
9298
- dryRun: true,
9299
- enableContextGovernance: opts.enableContextGovernance
9672
+ if ((provider === "claude" || provider === "both") && !fileExists(cwd, "CLAUDE.md")) {
9673
+ findings.push({ level: "warning", message: "CLAUDE.md is missing for Claude provider; run cdd-kit upgrade --provider claude --yes" });
9674
+ }
9675
+ if ((provider === "claude" || provider === "both") && !fileExists(cwd, "AGENTS.md")) {
9676
+ findings.push({ level: "warning", message: "AGENTS.md is missing for Claude provider; run cdd-kit upgrade --provider claude --yes" });
9677
+ }
9678
+ if ((provider === "codex" || provider === "both") && !fileExists(cwd, "CODEX.md")) {
9679
+ findings.push({ level: "warning", message: "CODEX.md is missing for Codex provider; run cdd-kit upgrade --provider codex --yes" });
9680
+ }
9681
+ findings.push(...checkContextFreshness(cwd));
9682
+ findings.push(...checkModelPolicyDrift(cwd));
9683
+ findings.push(...await checkAgentLint(cwd));
9684
+ findings.push(...checkCodeMap(cwd));
9685
+ const errors = findings.filter((finding) => finding.level === "error").length;
9686
+ const warnings = findings.filter((finding) => finding.level === "warning").length;
9687
+ return {
9688
+ provider,
9689
+ strict,
9690
+ findings,
9691
+ errors,
9692
+ warnings,
9693
+ ok: errors === 0 && (!strict || warnings === 0)
9694
+ };
9695
+ }
9696
+ async function attemptAutoFixes(cwd, report) {
9697
+ const fixed = [];
9698
+ const remaining = [];
9699
+ for (const finding of report.findings) {
9700
+ if (finding.level !== "warning") {
9701
+ remaining.push(finding);
9702
+ continue;
9703
+ }
9704
+ if (/specs\/context indexes are missing|inputs changed|older cdd-kit|older than/i.test(finding.message)) {
9705
+ try {
9706
+ const { contextScan: contextScan2 } = await Promise.resolve().then(() => (init_context_scan(), context_scan_exports));
9707
+ await contextScan2();
9708
+ fixed.push(`ran context-scan to refresh specs/context/`);
9709
+ continue;
9710
+ } catch (err) {
9711
+ remaining.push({ level: "warning", message: `${finding.message} (auto-fix failed: ${err.message})` });
9712
+ continue;
9713
+ }
9714
+ }
9715
+ if (/code-map stale|code-map\.yml is missing/i.test(finding.message)) {
9716
+ try {
9717
+ const { codeMap: codeMap2 } = await Promise.resolve().then(() => (init_code_map(), code_map_exports));
9718
+ await codeMap2({ path: ".", out: ".cdd/code-map.yml", include: [], exclude: [], check: false, maxLines: 1e5 });
9719
+ fixed.push("regenerated .cdd/code-map.yml");
9720
+ continue;
9721
+ } catch (err) {
9722
+ remaining.push({ level: "warning", message: `${finding.message} (auto-fix failed: ${err.message})` });
9723
+ continue;
9724
+ }
9725
+ }
9726
+ if (/model-policy\.json has no role bindings/i.test(finding.message)) {
9727
+ const policyPath = join21(cwd, ".cdd", "model-policy.json");
9728
+ try {
9729
+ let existing = {};
9730
+ try {
9731
+ existing = JSON.parse(readFileSync19(policyPath, "utf8"));
9732
+ } catch {
9733
+ }
9734
+ const merged = {
9735
+ ...existing,
9736
+ roles: {
9737
+ "change-classifier": "claude-opus-4-7",
9738
+ "spec-architect": "claude-opus-4-7",
9739
+ "qa-reviewer": "claude-opus-4-7",
9740
+ "contract-reviewer": "claude-sonnet-4-6",
9741
+ "test-strategist": "claude-sonnet-4-6",
9742
+ "backend-engineer": "claude-sonnet-4-6",
9743
+ "frontend-engineer": "claude-sonnet-4-6",
9744
+ "ci-cd-gatekeeper": "claude-sonnet-4-6",
9745
+ "e2e-resilience-engineer": "claude-sonnet-4-6",
9746
+ "monkey-test-engineer": "claude-sonnet-4-6",
9747
+ "stress-soak-engineer": "claude-sonnet-4-6",
9748
+ "ui-ux-reviewer": "claude-sonnet-4-6",
9749
+ "visual-reviewer": "claude-haiku-4-5-20251001",
9750
+ "dependency-security-reviewer": "claude-sonnet-4-6",
9751
+ "spec-drift-auditor": "claude-opus-4-7",
9752
+ "repo-context-scanner": "claude-haiku-4-5-20251001"
9753
+ }
9754
+ };
9755
+ const { writeFileSync: writeFileSync14 } = await import("fs");
9756
+ writeFileSync14(policyPath, JSON.stringify(merged, null, 2) + "\n", "utf8");
9757
+ fixed.push(`populated .cdd/model-policy.json with default role bindings`);
9758
+ continue;
9759
+ } catch (err) {
9760
+ remaining.push({ level: "warning", message: `${finding.message} (auto-fix failed: ${err.message})` });
9761
+ continue;
9762
+ }
9763
+ }
9764
+ if (/\.cdd\/.*is missing|run cdd-kit upgrade/i.test(finding.message)) {
9765
+ remaining.push({
9766
+ level: "warning",
9767
+ message: `${finding.message} (run \`cdd-kit upgrade --yes\` manually \u2014 too invasive for --fix)`
9300
9768
  });
9769
+ continue;
9301
9770
  }
9771
+ remaining.push(finding);
9772
+ }
9773
+ return { fixed, remaining };
9774
+ }
9775
+ async function doctor(opts = {}) {
9776
+ const cwd = process.cwd();
9777
+ let report = await buildDoctorReport(cwd, opts);
9778
+ if (opts.fix && !opts.json) {
9302
9779
  log.blank();
9780
+ log.info("Doctor --fix: attempting safe auto-resolutions\u2026");
9781
+ const { fixed, remaining } = await attemptAutoFixes(cwd, report);
9782
+ for (const f of fixed)
9783
+ log.ok(`fixed: ${f}`);
9784
+ if (fixed.length > 0) {
9785
+ report = await buildDoctorReport(cwd, opts);
9786
+ } else {
9787
+ log.info("no auto-fixable findings");
9788
+ }
9789
+ }
9790
+ if (opts.json) {
9791
+ console.log(JSON.stringify(report, null, 2));
9792
+ if (!report.ok)
9793
+ process.exit(1);
9303
9794
  return;
9304
9795
  }
9305
- applyCopy(plan);
9306
- const modelPolicyPath = join20(cwd, ".cdd", "model-policy.json");
9307
- if (existsSync17(modelPolicyPath)) {
9308
- let existing = {};
9309
- try {
9310
- existing = JSON.parse(readFileSync18(modelPolicyPath, "utf8"));
9311
- } catch {
9312
- }
9313
- const merged = {
9314
- ...existing,
9315
- provider,
9316
- generated_at: (/* @__PURE__ */ new Date()).toISOString(),
9317
- roles: existing.roles && typeof existing.roles === "object" ? existing.roles : {}
9318
- };
9319
- writeFileSync9(modelPolicyPath, JSON.stringify(merged, null, 2) + "\n", "utf8");
9796
+ log.blank();
9797
+ log.info(`Doctor provider: ${report.provider}`);
9798
+ for (const finding of report.findings) {
9799
+ if (finding.level === "ok")
9800
+ log.ok(finding.message);
9801
+ else if (finding.level === "warning")
9802
+ log.warn(finding.message);
9803
+ else
9804
+ log.error(finding.message);
9320
9805
  }
9321
9806
  log.blank();
9322
- log.ok(`Upgrade complete: ${plan.length} missing file(s) added.`);
9323
- log.info("Existing project guidance and contracts were preserved.");
9324
- if (opts.migrateChanges) {
9325
- log.blank();
9326
- log.info("Running change migration flow...");
9327
- await migrate(void 0, {
9328
- all: true,
9329
- dryRun: false,
9330
- enableContextGovernance: opts.enableContextGovernance
9331
- });
9807
+ if (!report.ok) {
9808
+ log.error(report.strict && report.errors === 0 ? `doctor failed in strict mode with ${report.warnings} warning(s)` : `doctor failed with ${report.errors} error(s)`);
9809
+ process.exit(1);
9810
+ }
9811
+ if (report.warnings > 0) {
9812
+ log.warn(`doctor completed with ${report.warnings} warning(s)`);
9813
+ } else {
9814
+ log.ok("doctor passed");
9332
9815
  }
9333
9816
  log.blank();
9334
9817
  }
9335
- var init_upgrade = __esm({
9336
- "src/commands/upgrade.ts"() {
9818
+ var init_doctor = __esm({
9819
+ "src/commands/doctor.ts"() {
9337
9820
  "use strict";
9338
- init_paths();
9339
9821
  init_logger();
9340
9822
  init_provider();
9341
- init_migrate();
9823
+ init_freshness();
9342
9824
  }
9343
9825
  });
9344
9826
 
@@ -9347,8 +9829,8 @@ var lint_agents_exports = {};
9347
9829
  __export(lint_agents_exports, {
9348
9830
  lintAgents: () => lintAgents
9349
9831
  });
9350
- import { readdirSync as readdirSync11, readFileSync as readFileSync19 } from "fs";
9351
- import { join as join21 } from "path";
9832
+ import { readdirSync as readdirSync12, readFileSync as readFileSync20 } from "fs";
9833
+ import { join as join22 } from "path";
9352
9834
  import { load as yamlLoad2 } from "js-yaml";
9353
9835
  function extractRequiredArtifactsSection(content) {
9354
9836
  const match = content.match(
@@ -9389,20 +9871,20 @@ function hasFlatBacktickKeysWithoutFence(section) {
9389
9871
  }
9390
9872
  async function lintAgents(opts) {
9391
9873
  const cwd = process.cwd();
9392
- const agentsDir = join21(cwd, ".claude", "agents");
9874
+ const agentsDir = join22(cwd, ".claude", "agents");
9393
9875
  let files;
9394
9876
  try {
9395
- files = readdirSync11(agentsDir).filter((f) => f.endsWith(".md")).sort();
9877
+ files = readdirSync12(agentsDir).filter((f) => f.endsWith(".md")).sort();
9396
9878
  } catch {
9397
9879
  log.error(`lint-agents: cannot read ${agentsDir} \u2014 is this a cdd-kit project?`);
9398
9880
  return 1;
9399
9881
  }
9400
9882
  const violations = [];
9401
9883
  for (const filename of files) {
9402
- const filePath = join21(agentsDir, filename);
9884
+ const filePath = join22(agentsDir, filename);
9403
9885
  let content;
9404
9886
  try {
9405
- content = readFileSync19(filePath, "utf8");
9887
+ content = readFileSync20(filePath, "utf8");
9406
9888
  } catch {
9407
9889
  violations.push({
9408
9890
  file: filename,
@@ -9514,28 +9996,28 @@ var archive_exports = {};
9514
9996
  __export(archive_exports, {
9515
9997
  archive: () => archive
9516
9998
  });
9517
- import { join as join22 } from "path";
9518
- import { existsSync as existsSync18, mkdirSync as mkdirSync9, renameSync as renameSync2, readFileSync as readFileSync20, writeFileSync as writeFileSync10, appendFileSync, cpSync as cpSync3, rmSync as rmSync3 } from "fs";
9999
+ import { join as join23 } from "path";
10000
+ import { existsSync as existsSync19, mkdirSync as mkdirSync10, renameSync as renameSync2, readFileSync as readFileSync21, writeFileSync as writeFileSync11, appendFileSync, cpSync as cpSync3, rmSync as rmSync3 } from "fs";
9519
10001
  import yaml4 from "js-yaml";
9520
10002
  async function archive(changeId) {
9521
10003
  const cwd = process.cwd();
9522
- const changeDir = join22(cwd, "specs", "changes", changeId);
10004
+ const changeDir = join23(cwd, "specs", "changes", changeId);
9523
10005
  const archiveYear = (/* @__PURE__ */ new Date()).getFullYear().toString();
9524
- const archiveBase = join22(cwd, "specs", "archive", archiveYear);
9525
- const archiveDir = join22(archiveBase, changeId);
9526
- const indexPath = join22(cwd, "specs", "archive", "INDEX.md");
9527
- if (!existsSync18(changeDir)) {
10006
+ const archiveBase = join23(cwd, "specs", "archive", archiveYear);
10007
+ const archiveDir = join23(archiveBase, changeId);
10008
+ const indexPath = join23(cwd, "specs", "archive", "INDEX.md");
10009
+ if (!existsSync19(changeDir)) {
9528
10010
  log.error(`Change not found: specs/changes/${changeId}`);
9529
10011
  process.exit(1);
9530
10012
  }
9531
- if (existsSync18(archiveDir)) {
10013
+ if (existsSync19(archiveDir)) {
9532
10014
  log.error(`Already archived: specs/archive/${archiveYear}/${changeId}`);
9533
10015
  process.exit(1);
9534
10016
  }
9535
- const tasksPath = join22(changeDir, "tasks.yml");
9536
- if (existsSync18(tasksPath)) {
10017
+ const tasksPath = join23(changeDir, "tasks.yml");
10018
+ if (existsSync19(tasksPath)) {
9537
10019
  try {
9538
- const raw = readFileSync20(tasksPath, "utf8");
10020
+ const raw = readFileSync21(tasksPath, "utf8");
9539
10021
  const data = yaml4.load(raw);
9540
10022
  if (data?.status === "gate-blocked") {
9541
10023
  log.warn("tasks.yml has status: gate-blocked \u2014 archiving anyway (change was paused).");
@@ -9548,8 +10030,8 @@ async function archive(changeId) {
9548
10030
  log.warn("tasks.yml could not be parsed \u2014 archiving anyway.");
9549
10031
  }
9550
10032
  }
9551
- if (!existsSync18(archiveBase)) {
9552
- mkdirSync9(archiveBase, { recursive: true });
10033
+ if (!existsSync19(archiveBase)) {
10034
+ mkdirSync10(archiveBase, { recursive: true });
9553
10035
  }
9554
10036
  try {
9555
10037
  renameSync2(changeDir, archiveDir);
@@ -9565,8 +10047,8 @@ async function archive(changeId) {
9565
10047
  const today = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
9566
10048
  const indexLine = `| ${changeId} | ${archiveYear} | ${today} | specs/archive/${archiveYear}/${changeId}/ |
9567
10049
  `;
9568
- if (!existsSync18(indexPath)) {
9569
- writeFileSync10(indexPath, `# Archive Index
10050
+ if (!existsSync19(indexPath)) {
10051
+ writeFileSync11(indexPath, `# Archive Index
9570
10052
 
9571
10053
  | change-id | year | archived-date | path |
9572
10054
  |---|---|---|---|
@@ -9590,37 +10072,37 @@ var abandon_exports = {};
9590
10072
  __export(abandon_exports, {
9591
10073
  abandon: () => abandon
9592
10074
  });
9593
- import { join as join23 } from "path";
9594
- import { existsSync as existsSync19, readFileSync as readFileSync21, writeFileSync as writeFileSync11, appendFileSync as appendFileSync2, mkdirSync as mkdirSync10 } from "fs";
10075
+ import { join as join24 } from "path";
10076
+ import { existsSync as existsSync20, readFileSync as readFileSync22, writeFileSync as writeFileSync12, appendFileSync as appendFileSync2, mkdirSync as mkdirSync11 } from "fs";
9595
10077
  import yaml5 from "js-yaml";
9596
10078
  async function abandon(changeId, opts) {
9597
10079
  const cwd = process.cwd();
9598
- const changeDir = join23(cwd, "specs", "changes", changeId);
9599
- const tasksPath = join23(changeDir, "tasks.yml");
9600
- if (!existsSync19(changeDir)) {
10080
+ const changeDir = join24(cwd, "specs", "changes", changeId);
10081
+ const tasksPath = join24(changeDir, "tasks.yml");
10082
+ if (!existsSync20(changeDir)) {
9601
10083
  log.error(`Change not found: specs/changes/${changeId}`);
9602
10084
  process.exit(1);
9603
10085
  }
9604
- if (existsSync19(tasksPath)) {
9605
- const raw = readFileSync21(tasksPath, "utf8");
10086
+ if (existsSync20(tasksPath)) {
10087
+ const raw = readFileSync22(tasksPath, "utf8");
9606
10088
  const data = yaml5.load(raw) ?? {};
9607
10089
  data["status"] = "abandoned";
9608
10090
  if (!data["change-id"]) {
9609
10091
  data["change-id"] = changeId;
9610
10092
  }
9611
- writeFileSync11(tasksPath, yaml5.dump(data, { lineWidth: -1, noRefs: true }), "utf8");
10093
+ writeFileSync12(tasksPath, yaml5.dump(data, { lineWidth: -1, noRefs: true }), "utf8");
9612
10094
  }
9613
10095
  const today = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
9614
- const archiveDir = join23(cwd, "specs", "archive");
9615
- const indexPath = join23(archiveDir, "INDEX.md");
10096
+ const archiveDir = join24(cwd, "specs", "archive");
10097
+ const indexPath = join24(archiveDir, "INDEX.md");
9616
10098
  const reason = opts.reason ?? "no reason given";
9617
10099
  const indexLine = `| ${changeId} | abandoned | ${today} | ${reason} |
9618
10100
  `;
9619
- if (!existsSync19(archiveDir)) {
9620
- mkdirSync10(archiveDir, { recursive: true });
10101
+ if (!existsSync20(archiveDir)) {
10102
+ mkdirSync11(archiveDir, { recursive: true });
9621
10103
  }
9622
- if (!existsSync19(indexPath)) {
9623
- writeFileSync11(indexPath, `# Archive Index
10104
+ if (!existsSync20(indexPath)) {
10105
+ writeFileSync12(indexPath, `# Archive Index
9624
10106
 
9625
10107
  | change-id | status | date | notes |
9626
10108
  |---|---|---|---|
@@ -9644,28 +10126,28 @@ var list_changes_exports = {};
9644
10126
  __export(list_changes_exports, {
9645
10127
  listChanges: () => listChanges
9646
10128
  });
9647
- import { join as join24 } from "path";
9648
- import { existsSync as existsSync20, readdirSync as readdirSync12, readFileSync as readFileSync22 } from "fs";
10129
+ import { join as join25 } from "path";
10130
+ import { existsSync as existsSync21, readdirSync as readdirSync13, readFileSync as readFileSync23 } from "fs";
9649
10131
  import yaml6 from "js-yaml";
9650
10132
  async function listChanges() {
9651
10133
  const cwd = process.cwd();
9652
- const changesDir = join24(cwd, "specs", "changes");
10134
+ const changesDir = join25(cwd, "specs", "changes");
9653
10135
  log.blank();
9654
10136
  const active = [];
9655
- if (existsSync20(changesDir)) {
9656
- active.push(...readdirSync12(changesDir, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name));
10137
+ if (existsSync21(changesDir)) {
10138
+ active.push(...readdirSync13(changesDir, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name));
9657
10139
  }
9658
10140
  if (active.length === 0) {
9659
10141
  log.info("No active changes in specs/changes/");
9660
10142
  } else {
9661
10143
  log.info("Active changes:");
9662
10144
  for (const id of active) {
9663
- const tasksPath = join24(changesDir, id, "tasks.yml");
10145
+ const tasksPath = join25(changesDir, id, "tasks.yml");
9664
10146
  let status = "in-progress";
9665
10147
  let pending = 0;
9666
- if (existsSync20(tasksPath)) {
10148
+ if (existsSync21(tasksPath)) {
9667
10149
  try {
9668
- const raw = readFileSync22(tasksPath, "utf8");
10150
+ const raw = readFileSync23(tasksPath, "utf8");
9669
10151
  const data = yaml6.load(raw);
9670
10152
  if (data?.status)
9671
10153
  status = data.status;
@@ -9696,8 +10178,8 @@ __export(context_exports, {
9696
10178
  rejectContextExpansion: () => rejectContextExpansion,
9697
10179
  requestContextExpansion: () => requestContextExpansion
9698
10180
  });
9699
- import { existsSync as existsSync21, readFileSync as readFileSync23, writeFileSync as writeFileSync12 } from "fs";
9700
- import { join as join25 } from "path";
10181
+ import { existsSync as existsSync22, readFileSync as readFileSync24, writeFileSync as writeFileSync13 } from "fs";
10182
+ import { join as join26 } from "path";
9701
10183
  function normalizePath(path) {
9702
10184
  return path.replace(/\\/g, "/").replace(/^\.\//, "").trim();
9703
10185
  }
@@ -9711,18 +10193,18 @@ function validateRepoRelativePath(path) {
9711
10193
  return null;
9712
10194
  }
9713
10195
  function manifestPathFor(changeId) {
9714
- return join25(process.cwd(), "specs", "changes", changeId, "context-manifest.md");
10196
+ return join26(process.cwd(), "specs", "changes", changeId, "context-manifest.md");
9715
10197
  }
9716
10198
  function readManifest(changeId) {
9717
10199
  const manifestPath = manifestPathFor(changeId);
9718
- if (!existsSync21(manifestPath)) {
10200
+ if (!existsSync22(manifestPath)) {
9719
10201
  log.error(`context manifest not found: specs/changes/${changeId}/context-manifest.md`);
9720
10202
  process.exit(1);
9721
10203
  }
9722
- return readFileSync23(manifestPath, "utf8");
10204
+ return readFileSync24(manifestPath, "utf8");
9723
10205
  }
9724
10206
  function writeManifest(changeId, content) {
9725
- writeFileSync12(manifestPathFor(changeId), content.endsWith("\n") ? content : `${content}
10207
+ writeFileSync13(manifestPathFor(changeId), content.endsWith("\n") ? content : `${content}
9726
10208
  `, "utf8");
9727
10209
  }
9728
10210
  function sectionBody(content, heading) {
@@ -9947,9 +10429,9 @@ var init_context = __esm({
9947
10429
  });
9948
10430
 
9949
10431
  // src/cli/index.ts
9950
- import { readFileSync as readFileSync24 } from "fs";
10432
+ import { readFileSync as readFileSync25 } from "fs";
9951
10433
  import { fileURLToPath as fileURLToPath3 } from "url";
9952
- import { dirname as dirname6, join as join26 } from "path";
10434
+ import { dirname as dirname7, join as join27 } from "path";
9953
10435
  import { Command } from "commander";
9954
10436
 
9955
10437
  // src/commands/init.ts
@@ -10364,167 +10846,8 @@ async function init(opts) {
10364
10846
  log.blank();
10365
10847
  }
10366
10848
 
10367
- // src/commands/update.ts
10368
- init_paths();
10369
- init_logger();
10370
- init_provider();
10371
- import { join as join7 } from "path";
10372
- import { existsSync as existsSync6, mkdirSync as mkdirSync3, readdirSync as readdirSync3, copyFileSync as copyFileSync2, readFileSync as readFileSync5 } from "fs";
10373
- import { createHash } from "crypto";
10374
- import { homedir as homedir2 } from "os";
10375
- function fileHash(filePath) {
10376
- const buf = readFileSync5(filePath);
10377
- return createHash("sha256").update(buf).digest("hex");
10378
- }
10379
- function diffDir(src, dest) {
10380
- const entries = [];
10381
- if (!existsSync6(src))
10382
- return entries;
10383
- function walk(currentSrc, currentDest) {
10384
- const items = readdirSync3(currentSrc, { withFileTypes: true });
10385
- for (const item of items) {
10386
- const srcPath = join7(currentSrc, item.name);
10387
- const destPath = join7(currentDest, item.name);
10388
- if (item.isDirectory()) {
10389
- walk(srcPath, destPath);
10390
- } else {
10391
- if (!existsSync6(destPath)) {
10392
- entries.push({ src: srcPath, dest: destPath, action: "add" });
10393
- } else if (fileHash(srcPath) !== fileHash(destPath)) {
10394
- entries.push({ src: srcPath, dest: destPath, action: "overwrite" });
10395
- } else {
10396
- entries.push({ src: srcPath, dest: destPath, action: "skip" });
10397
- }
10398
- }
10399
- }
10400
- }
10401
- walk(src, dest);
10402
- return entries;
10403
- }
10404
- function applyDir(entries) {
10405
- let count = 0;
10406
- for (const e of entries) {
10407
- if (e.action === "skip")
10408
- continue;
10409
- mkdirSync3(join7(e.dest, ".."), { recursive: true });
10410
- copyFileSync2(e.src, e.dest);
10411
- count += 1;
10412
- }
10413
- return count;
10414
- }
10415
- function backupDir(dir, backupDest) {
10416
- if (!existsSync6(dir))
10417
- return;
10418
- mkdirSync3(backupDest, { recursive: true });
10419
- function walk(src, dst) {
10420
- const items = readdirSync3(src, { withFileTypes: true });
10421
- for (const item of items) {
10422
- const s = join7(src, item.name);
10423
- const d = join7(dst, item.name);
10424
- if (item.isDirectory()) {
10425
- mkdirSync3(d, { recursive: true });
10426
- walk(s, d);
10427
- } else
10428
- copyFileSync2(s, d);
10429
- }
10430
- }
10431
- walk(dir, backupDest);
10432
- }
10433
- async function update(opts) {
10434
- if (opts.postinstall) {
10435
- if (!existsSync6(join7(SKILLS_HOME, "contract-driven-delivery"))) {
10436
- return;
10437
- }
10438
- opts.yes = true;
10439
- opts.provider = "claude";
10440
- }
10441
- const quiet = !!opts.postinstall;
10442
- if (!quiet)
10443
- log.blank();
10444
- const cwd = process.cwd();
10445
- const requestedProvider = opts.provider ?? "auto";
10446
- if (!validateProviderOption(requestedProvider)) {
10447
- log.error(`Invalid provider: ${requestedProvider}. Use auto, claude, codex, or both.`);
10448
- process.exit(1);
10449
- }
10450
- const provider = inferProvider(cwd, requestedProvider);
10451
- const updateClaudeAssets = provider === "claude" || provider === "both";
10452
- const agentDiff = updateClaudeAssets ? diffDir(ASSET.agents, AGENTS_HOME) : [];
10453
- 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))) : [];
10454
- const toWrite = [...agentDiff, ...skillDiff].filter((e) => e.action !== "skip");
10455
- const toAdd = toWrite.filter((e) => e.action === "add");
10456
- const toOver = toWrite.filter((e) => e.action === "overwrite");
10457
- const toSkip = [...agentDiff, ...skillDiff].filter((e) => e.action === "skip");
10458
- if (!quiet) {
10459
- log.info(`Provider: ${provider}`);
10460
- if (updateClaudeAssets) {
10461
- log.info(`Dry-run diff \u2014 agents: ${AGENTS_HOME}`);
10462
- log.info(`Dry-run diff \u2014 skills: ${SKILLS_HOME}`);
10463
- } else {
10464
- log.info("Codex provider has no global cdd-kit assets to update.");
10465
- log.info("Project files are preserved; run cdd-kit init --local-only --provider codex to add missing local guidance.");
10466
- }
10467
- log.blank();
10468
- if (toAdd.length)
10469
- log.info(` + ${toAdd.length} file(s) would be added`);
10470
- if (toOver.length)
10471
- log.warn(` ~ ${toOver.length} file(s) would be overwritten (user edits lost without backup)`);
10472
- if (toSkip.length)
10473
- log.dim(` ${toSkip.length} file(s) unchanged (skipped)`);
10474
- }
10475
- if (toWrite.length === 0) {
10476
- if (!quiet) {
10477
- log.blank();
10478
- log.ok("Already up to date \u2014 nothing to write.");
10479
- log.blank();
10480
- }
10481
- return;
10482
- }
10483
- if (!quiet && !opts.yes) {
10484
- log.blank();
10485
- log.info("Run with --yes to apply changes. Example:");
10486
- log.dim(" cdd-kit update --yes");
10487
- log.blank();
10488
- return;
10489
- }
10490
- const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
10491
- const backupRoot = join7(homedir2(), ".claude", ".cdd-kit-backup", timestamp);
10492
- if (!quiet) {
10493
- log.blank();
10494
- log.info(`Backing up to ${backupRoot} \u2026`);
10495
- }
10496
- backupDir(AGENTS_HOME, join7(backupRoot, "agents"));
10497
- backupDir(SKILLS_HOME, join7(backupRoot, "skills"));
10498
- if (!quiet)
10499
- log.ok(`Backup complete: ${backupRoot}`);
10500
- if (!quiet)
10501
- log.blank();
10502
- let totalSynced = 0;
10503
- if (updateClaudeAssets) {
10504
- if (!quiet)
10505
- log.info(`Updating agents \u2192 ${AGENTS_HOME}`);
10506
- const agentCount = applyDir(agentDiff);
10507
- if (!quiet)
10508
- log.ok(`${agentCount} agent file(s) updated.`);
10509
- totalSynced += agentCount;
10510
- if (!quiet)
10511
- log.info(`Updating skills \u2192 ${SKILLS_HOME}`);
10512
- const skillCount = applyDir(skillDiff);
10513
- if (!quiet)
10514
- log.ok(`${skillCount} skill file(s) updated.`);
10515
- totalSynced += skillCount;
10516
- }
10517
- if (quiet) {
10518
- if (totalSynced > 0)
10519
- log.ok(`cdd-kit: synced ${totalSynced} file(s) to ~/.claude/`);
10520
- } else {
10521
- log.blank();
10522
- log.info("Project files (contracts/, specs/, tests/, ci/) were not changed.");
10523
- log.ok("Update complete.");
10524
- log.info(`Backup saved to: ${backupRoot}`);
10525
- log.blank();
10526
- }
10527
- }
10849
+ // src/cli/index.ts
10850
+ init_update();
10528
10851
 
10529
10852
  // src/commands/new-change.ts
10530
10853
  init_paths();
@@ -11513,8 +11836,8 @@ async function installHooks() {
11513
11836
  }
11514
11837
 
11515
11838
  // src/cli/index.ts
11516
- var __dirname2 = dirname6(fileURLToPath3(import.meta.url));
11517
- var pkg = JSON.parse(readFileSync24(join26(__dirname2, "..", "..", "package.json"), "utf8"));
11839
+ var __dirname2 = dirname7(fileURLToPath3(import.meta.url));
11840
+ var pkg = JSON.parse(readFileSync25(join27(__dirname2, "..", "..", "package.json"), "utf8"));
11518
11841
  var program = new Command();
11519
11842
  program.name("cdd-kit").description("Contract-Driven Delivery Kit CLI").version(pkg.version);
11520
11843
  program.command("init").description(
@@ -11529,6 +11852,18 @@ program.command("init").description(
11529
11852
  })
11530
11853
  );
11531
11854
  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 }));
11855
+ program.command("refresh").description("Complete upgrade: refresh agents, skills, templates, hooks, model-policy, and code-map in one command").option("--yes", "Apply changes (default is dry-run)", false).option("--no-templates", "Skip force-refresh of specs/templates, tests/templates, ci-templates, .github/workflows").option("--no-hooks", "Skip pre-commit hook re-installation").option("--no-code-map", "Skip code-map regeneration").option("--no-update", "Skip ~/.claude update step").option("--no-upgrade", "Skip project add-missing step").option("--provider <provider>", "Provider adapter: auto, claude, codex, or both", "auto").action(async (opts) => {
11856
+ const { refresh: refresh2 } = await Promise.resolve().then(() => (init_refresh(), refresh_exports));
11857
+ await refresh2({
11858
+ yes: opts.yes,
11859
+ noTemplates: opts.templates === false,
11860
+ noHooks: opts.hooks === false,
11861
+ noCodeMap: opts.codeMap === false,
11862
+ noUpdate: opts.update === false,
11863
+ noUpgrade: opts.upgrade === false,
11864
+ provider: opts.provider
11865
+ });
11866
+ });
11532
11867
  program.command("doctor").description("Inspect cdd-kit repo health, provider guidance, and context index freshness").option("--strict", "Treat warnings as errors", false).option("--json", "Print a machine-readable health report", false).option("--provider <provider>", "Provider adapter to inspect: auto, claude, codex, or both", "auto").option("--fix", "Auto-resolve safe warnings (stale context indexes, missing role bindings)", false).action(async (opts) => {
11533
11868
  const { doctor: doctor2 } = await Promise.resolve().then(() => (init_doctor(), doctor_exports));
11534
11869
  await doctor2({ strict: opts.strict, json: opts.json, provider: opts.provider, fix: opts.fix });