contract-driven-delivery 1.10.0 → 1.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/CHANGELOG.md +72 -0
  2. package/README.md +532 -135
  3. package/assets/CLAUDE.template.md +34 -0
  4. package/assets/CODEX.template.md +39 -0
  5. package/assets/agents/backend-engineer.md +15 -0
  6. package/assets/agents/change-classifier.md +57 -1
  7. package/assets/agents/ci-cd-gatekeeper.md +15 -0
  8. package/assets/agents/contract-reviewer.md +15 -0
  9. package/assets/agents/dependency-security-reviewer.md +2 -0
  10. package/assets/agents/e2e-resilience-engineer.md +15 -0
  11. package/assets/agents/frontend-engineer.md +15 -0
  12. package/assets/agents/monkey-test-engineer.md +15 -0
  13. package/assets/agents/qa-reviewer.md +15 -0
  14. package/assets/agents/repo-context-scanner.md +2 -0
  15. package/assets/agents/spec-architect.md +15 -0
  16. package/assets/agents/spec-drift-auditor.md +2 -0
  17. package/assets/agents/stress-soak-engineer.md +15 -0
  18. package/assets/agents/test-strategist.md +15 -0
  19. package/assets/agents/ui-ux-reviewer.md +2 -0
  20. package/assets/agents/visual-reviewer.md +2 -0
  21. package/assets/cdd/context-policy.json +25 -0
  22. package/assets/cdd/model-policy.json +5 -0
  23. package/assets/contracts/api/api-contract.md +3 -0
  24. package/assets/contracts/api/api-inventory.md +7 -0
  25. package/assets/contracts/api/error-format.md +7 -0
  26. package/assets/contracts/business/business-rules.md +3 -0
  27. package/assets/contracts/ci/ci-gate-contract.md +3 -0
  28. package/assets/contracts/css/css-contract.md +3 -0
  29. package/assets/contracts/css/design-tokens.md +7 -0
  30. package/assets/contracts/data/data-shape-contract.md +3 -0
  31. package/assets/contracts/env/env-contract.md +3 -0
  32. package/assets/hooks/pre-commit +1 -1
  33. package/assets/skills/cdd-close/SKILL.md +150 -0
  34. package/assets/skills/cdd-new/SKILL.md +94 -35
  35. package/assets/skills/cdd-resume/SKILL.md +114 -0
  36. package/assets/skills/contract-driven-delivery/templates/change-classification.md +9 -8
  37. package/assets/skills/contract-driven-delivery/templates/tasks.md +7 -0
  38. package/assets/specs-templates/change-classification.md +9 -8
  39. package/assets/specs-templates/context-manifest.md +49 -0
  40. package/assets/specs-templates/tasks.md +9 -0
  41. package/dist/cli/index.js +1846 -152
  42. package/docs/release-checklist.md +39 -0
  43. package/package.json +12 -6
package/dist/cli/index.js CHANGED
@@ -1,38 +1,1298 @@
1
+ var __defProp = Object.defineProperty;
2
+ var __getOwnPropNames = Object.getOwnPropertyNames;
3
+ var __esm = (fn, res) => function __init() {
4
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
5
+ };
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+
11
+ // src/utils/paths.ts
12
+ import { join, dirname } from "path";
13
+ import { fileURLToPath } from "url";
14
+ import { homedir } from "os";
15
+ var __dirname, PACKAGE_ROOT, ASSETS_DIR, CLAUDE_HOME, AGENTS_HOME, SKILLS_HOME, ASSET;
16
+ var init_paths = __esm({
17
+ "src/utils/paths.ts"() {
18
+ "use strict";
19
+ __dirname = dirname(fileURLToPath(import.meta.url));
20
+ PACKAGE_ROOT = join(__dirname, "..", "..");
21
+ ASSETS_DIR = join(PACKAGE_ROOT, "assets");
22
+ CLAUDE_HOME = join(homedir(), ".claude");
23
+ AGENTS_HOME = join(CLAUDE_HOME, "agents");
24
+ SKILLS_HOME = join(CLAUDE_HOME, "skills");
25
+ ASSET = {
26
+ agents: join(ASSETS_DIR, "agents"),
27
+ skills: join(ASSETS_DIR, "skills"),
28
+ skill: join(ASSETS_DIR, "skills", "contract-driven-delivery"),
29
+ contracts: join(ASSETS_DIR, "contracts"),
30
+ specsTemplates: join(ASSETS_DIR, "specs-templates"),
31
+ testsTemplates: join(ASSETS_DIR, "tests-templates"),
32
+ ci: join(ASSETS_DIR, "ci"),
33
+ githubWorkflows: join(ASSETS_DIR, "github-workflows"),
34
+ hooks: join(ASSETS_DIR, "hooks"),
35
+ claudeTemplate: join(ASSETS_DIR, "CLAUDE.template.md"),
36
+ codexTemplate: join(ASSETS_DIR, "CODEX.template.md"),
37
+ agentsTemplate: join(ASSETS_DIR, "AGENTS.template.md"),
38
+ cddConfig: join(ASSETS_DIR, "cdd")
39
+ };
40
+ }
41
+ });
42
+
43
+ // src/utils/logger.ts
44
+ var RESET, CYAN, GREEN, YELLOW, RED, DIM, log;
45
+ var init_logger = __esm({
46
+ "src/utils/logger.ts"() {
47
+ "use strict";
48
+ RESET = "\x1B[0m";
49
+ CYAN = "\x1B[36m";
50
+ GREEN = "\x1B[32m";
51
+ YELLOW = "\x1B[33m";
52
+ RED = "\x1B[31m";
53
+ DIM = "\x1B[2m";
54
+ log = {
55
+ info(msg) {
56
+ console.log(`${CYAN}\u2139${RESET} ${msg}`);
57
+ },
58
+ ok(msg) {
59
+ console.log(`${GREEN}\u2713${RESET} ${msg}`);
60
+ },
61
+ warn(msg) {
62
+ console.log(`${YELLOW}\u26A0${RESET} ${msg}`);
63
+ },
64
+ error(msg) {
65
+ console.error(`${RED}\u2717${RESET} ${msg}`);
66
+ },
67
+ dim(msg) {
68
+ console.log(`${DIM} ${msg}${RESET}`);
69
+ },
70
+ blank() {
71
+ console.log("");
72
+ }
73
+ };
74
+ }
75
+ });
76
+
77
+ // src/utils/provider.ts
78
+ import { existsSync as existsSync4, readFileSync as readFileSync3 } from "fs";
79
+ import { join as join5 } from "path";
80
+ function validateProviderOption(provider) {
81
+ return provider === "auto" || provider === "claude" || provider === "codex" || provider === "both";
82
+ }
83
+ function inferProvider(cwd, requested = "auto") {
84
+ if (requested !== "auto")
85
+ return requested;
86
+ const modelPolicyPath = join5(cwd, ".cdd", "model-policy.json");
87
+ if (existsSync4(modelPolicyPath)) {
88
+ try {
89
+ const policy = JSON.parse(readFileSync3(modelPolicyPath, "utf8"));
90
+ if (policy.provider === "claude" || policy.provider === "codex" || policy.provider === "both") {
91
+ return policy.provider;
92
+ }
93
+ } catch {
94
+ }
95
+ }
96
+ const hasClaude = existsSync4(join5(cwd, "CLAUDE.md")) || existsSync4(join5(cwd, "AGENTS.md"));
97
+ const hasCodex = existsSync4(join5(cwd, "CODEX.md"));
98
+ if (hasClaude && hasCodex)
99
+ return "both";
100
+ if (hasCodex)
101
+ return "codex";
102
+ return "claude";
103
+ }
104
+ var init_provider = __esm({
105
+ "src/utils/provider.ts"() {
106
+ "use strict";
107
+ }
108
+ });
109
+
110
+ // src/commands/doctor.ts
111
+ var doctor_exports = {};
112
+ __export(doctor_exports, {
113
+ doctor: () => doctor
114
+ });
115
+ import { existsSync as existsSync10, readdirSync as readdirSync6, readFileSync as readFileSync8, statSync as statSync2 } from "fs";
116
+ import { join as join11 } from "path";
117
+ function fileExists(cwd, relPath) {
118
+ return existsSync10(join11(cwd, relPath));
119
+ }
120
+ function findFiles(dir, predicate, found = []) {
121
+ if (!existsSync10(dir))
122
+ return found;
123
+ for (const entry of readdirSync6(dir, { withFileTypes: true })) {
124
+ const fullPath = join11(dir, entry.name);
125
+ if (entry.isDirectory())
126
+ findFiles(fullPath, predicate, found);
127
+ else if (entry.isFile() && predicate(entry.name))
128
+ found.push(fullPath);
129
+ }
130
+ return found;
131
+ }
132
+ function newestMtime(paths) {
133
+ let newest = 0;
134
+ for (const path of paths) {
135
+ try {
136
+ newest = Math.max(newest, statSync2(path).mtimeMs);
137
+ } catch {
138
+ }
139
+ }
140
+ return newest;
141
+ }
142
+ function readMissingSummaryCount(cwd) {
143
+ const indexPath = join11(cwd, "specs", "context", "contracts-index.md");
144
+ if (!existsSync10(indexPath))
145
+ return null;
146
+ const match = readFileSync8(indexPath, "utf8").match(/^missing-summary-count:\s*(\d+)/m);
147
+ return match ? Number(match[1]) : null;
148
+ }
149
+ function checkContextFreshness(cwd) {
150
+ const findings = [];
151
+ const projectMap = join11(cwd, "specs", "context", "project-map.md");
152
+ const contractsIndex = join11(cwd, "specs", "context", "contracts-index.md");
153
+ const contextPolicy = join11(cwd, ".cdd", "context-policy.json");
154
+ const contractFiles = findFiles(join11(cwd, "contracts"), (name) => name.endsWith(".md"));
155
+ if (!existsSync10(projectMap) || !existsSync10(contractsIndex)) {
156
+ findings.push({
157
+ level: "warning",
158
+ message: "specs/context indexes are missing; run cdd-kit context-scan before classification"
159
+ });
160
+ return findings;
161
+ }
162
+ const projectInputs = [contextPolicy].filter(existsSync10);
163
+ if (projectInputs.length > 0 && statSync2(projectMap).mtimeMs < newestMtime(projectInputs)) {
164
+ findings.push({
165
+ level: "warning",
166
+ message: "specs/context/project-map.md is older than .cdd/context-policy.json; run cdd-kit context-scan"
167
+ });
168
+ }
169
+ if (contractFiles.length > 0 && statSync2(contractsIndex).mtimeMs < newestMtime(contractFiles)) {
170
+ findings.push({
171
+ level: "warning",
172
+ message: "specs/context/contracts-index.md is older than contracts/; run cdd-kit context-scan"
173
+ });
174
+ }
175
+ const missingSummaryCount = readMissingSummaryCount(cwd);
176
+ if (missingSummaryCount !== null && missingSummaryCount > 0) {
177
+ findings.push({
178
+ level: "warning",
179
+ message: `contracts-index reports ${missingSummaryCount} contract(s) without deterministic summary metadata`
180
+ });
181
+ }
182
+ if (findings.length === 0) {
183
+ findings.push({ level: "ok", message: "context indexes are present and fresh" });
184
+ }
185
+ return findings;
186
+ }
187
+ function buildDoctorReport(cwd, opts) {
188
+ const requestedProvider = opts.provider ?? "auto";
189
+ if (!validateProviderOption(requestedProvider)) {
190
+ log.error(`Invalid provider: ${requestedProvider}. Use auto, claude, codex, or both.`);
191
+ process.exit(1);
192
+ }
193
+ const strict = opts.strict ?? false;
194
+ const provider = inferProvider(cwd, requestedProvider);
195
+ const findings = [];
196
+ for (const relPath of ["contracts", "specs/templates", ".cdd/context-policy.json", ".cdd/model-policy.json"]) {
197
+ findings.push(fileExists(cwd, relPath) ? { level: "ok", message: `${relPath} exists` } : { level: "warning", message: `${relPath} is missing; run cdd-kit upgrade --yes` });
198
+ }
199
+ if ((provider === "claude" || provider === "both") && !fileExists(cwd, "CLAUDE.md")) {
200
+ findings.push({ level: "warning", message: "CLAUDE.md is missing for Claude provider; run cdd-kit upgrade --provider claude --yes" });
201
+ }
202
+ if ((provider === "claude" || provider === "both") && !fileExists(cwd, "AGENTS.md")) {
203
+ findings.push({ level: "warning", message: "AGENTS.md is missing for Claude provider; run cdd-kit upgrade --provider claude --yes" });
204
+ }
205
+ if ((provider === "codex" || provider === "both") && !fileExists(cwd, "CODEX.md")) {
206
+ findings.push({ level: "warning", message: "CODEX.md is missing for Codex provider; run cdd-kit upgrade --provider codex --yes" });
207
+ }
208
+ findings.push(...checkContextFreshness(cwd));
209
+ const errors = findings.filter((finding) => finding.level === "error").length;
210
+ const warnings = findings.filter((finding) => finding.level === "warning").length;
211
+ return {
212
+ provider,
213
+ strict,
214
+ findings,
215
+ errors,
216
+ warnings,
217
+ ok: errors === 0 && (!strict || warnings === 0)
218
+ };
219
+ }
220
+ async function doctor(opts = {}) {
221
+ const report = buildDoctorReport(process.cwd(), opts);
222
+ if (opts.json) {
223
+ console.log(JSON.stringify(report, null, 2));
224
+ if (!report.ok)
225
+ process.exit(1);
226
+ return;
227
+ }
228
+ log.blank();
229
+ log.info(`Doctor provider: ${report.provider}`);
230
+ for (const finding of report.findings) {
231
+ if (finding.level === "ok")
232
+ log.ok(finding.message);
233
+ else if (finding.level === "warning")
234
+ log.warn(finding.message);
235
+ else
236
+ log.error(finding.message);
237
+ }
238
+ log.blank();
239
+ if (!report.ok) {
240
+ log.error(report.strict && report.errors === 0 ? `doctor failed in strict mode with ${report.warnings} warning(s)` : `doctor failed with ${report.errors} error(s)`);
241
+ process.exit(1);
242
+ }
243
+ if (report.warnings > 0) {
244
+ log.warn(`doctor completed with ${report.warnings} warning(s)`);
245
+ } else {
246
+ log.ok("doctor passed");
247
+ }
248
+ log.blank();
249
+ }
250
+ var init_doctor = __esm({
251
+ "src/commands/doctor.ts"() {
252
+ "use strict";
253
+ init_logger();
254
+ init_provider();
255
+ }
256
+ });
257
+
258
+ // src/commands/migrate.ts
259
+ var migrate_exports = {};
260
+ __export(migrate_exports, {
261
+ migrate: () => migrate
262
+ });
263
+ import { join as join12 } from "path";
264
+ import { existsSync as existsSync11, readdirSync as readdirSync7, readFileSync as readFileSync9, writeFileSync as writeFileSync4 } from "fs";
265
+ function buildLegacyContextManifest(changeId) {
266
+ return [
267
+ "# Context Manifest",
268
+ "",
269
+ "Generated by `cdd-kit migrate` for an existing change.",
270
+ "This legacy manifest records a conservative default context boundary without enabling context-governance: v1.",
271
+ "",
272
+ "## Affected Surfaces",
273
+ "- legacy-unknown",
274
+ "",
275
+ "## Allowed Paths",
276
+ `- specs/changes/${changeId}/`,
277
+ "",
278
+ "## Forbidden Paths",
279
+ "- .claude/worktrees/**",
280
+ "- .git/**",
281
+ "- node_modules/**",
282
+ "- dist/**",
283
+ "- build/**",
284
+ "- assets/**",
285
+ "- specs/archive/**",
286
+ `- specs/changes/* except specs/changes/${changeId}/`,
287
+ "",
288
+ "## Required Contracts",
289
+ "- legacy-unknown",
290
+ "",
291
+ "## Required Tests",
292
+ "- legacy-unknown",
293
+ "",
294
+ "## Agent Work Packets",
295
+ "",
296
+ "## Context Expansion Requests",
297
+ "-",
298
+ "",
299
+ "## Approved Expansions",
300
+ "-",
301
+ ""
302
+ ].join("\n");
303
+ }
304
+ function upsertFrontmatterField(content, field, value) {
305
+ if (!content.startsWith("---\n"))
306
+ return content;
307
+ const closing = content.indexOf("\n---", 4);
308
+ if (closing === -1)
309
+ return content;
310
+ const frontmatter = content.slice(4, closing);
311
+ const body = content.slice(closing);
312
+ const fieldPattern = new RegExp(`^${field}:.*$`, "m");
313
+ const nextFrontmatter = fieldPattern.test(frontmatter) ? frontmatter.replace(fieldPattern, `${field}: ${value}`) : `${frontmatter.trimEnd()}
314
+ ${field}: ${value}`;
315
+ return `---
316
+ ${nextFrontmatter}${body}`;
317
+ }
318
+ function buildContextGovernedManifest(changeId) {
319
+ return [
320
+ "# Context Manifest",
321
+ "",
322
+ "Generated by `cdd-kit migrate --enable-context-governance` for an existing change.",
323
+ "Review and narrow the allowed paths before assigning implementation work.",
324
+ "",
325
+ "## Affected Surfaces",
326
+ "- legacy-unknown",
327
+ "",
328
+ "## Allowed Paths",
329
+ `- specs/changes/${changeId}/`,
330
+ "- specs/context/project-map.md",
331
+ "- specs/context/contracts-index.md",
332
+ "",
333
+ "## Forbidden Paths",
334
+ "- .claude/worktrees/**",
335
+ "- .git/**",
336
+ "- node_modules/**",
337
+ "- dist/**",
338
+ "- build/**",
339
+ "- assets/**",
340
+ "- specs/archive/**",
341
+ `- specs/changes/* except specs/changes/${changeId}/`,
342
+ "",
343
+ "## Required Contracts",
344
+ "- legacy-unknown",
345
+ "",
346
+ "## Required Tests",
347
+ "- legacy-unknown",
348
+ "",
349
+ "## Agent Work Packets",
350
+ "",
351
+ "### change-classifier",
352
+ "- allowed:",
353
+ ` - specs/changes/${changeId}/`,
354
+ " - specs/context/project-map.md",
355
+ " - specs/context/contracts-index.md",
356
+ "",
357
+ "## Context Expansion Requests",
358
+ "",
359
+ "<!--",
360
+ "Agents must request context expansion instead of reading outside their work packet.",
361
+ "Use this format only for real requests:",
362
+ "",
363
+ "- request-id: CER-001",
364
+ " requested_paths:",
365
+ " - src/example.ts",
366
+ " reason: why this file is required",
367
+ " status: pending",
368
+ "-->",
369
+ "",
370
+ "## Approved Expansions",
371
+ "-",
372
+ ""
373
+ ].join("\n");
374
+ }
375
+ function migrateOne(changeId, changeDir, dryRun, enableContextGovernance) {
376
+ const changed = [];
377
+ const warnings = [];
378
+ const tasksPath = join12(changeDir, "tasks.md");
379
+ if (existsSync11(tasksPath)) {
380
+ let content = readFileSync9(tasksPath, "utf8");
381
+ const norm = content.replace(/\r\n/g, "\n");
382
+ let modified = false;
383
+ const taskChanges = [];
384
+ if (!norm.startsWith("---")) {
385
+ const bareStatusMatch = norm.match(/^status:\s*(\S+)/m);
386
+ const inferredStatus = bareStatusMatch ? bareStatusMatch[1] : "in-progress";
387
+ if (bareStatusMatch) {
388
+ content = content.replace(/^status:\s*\S+[ \t]*\n?/m, "");
389
+ }
390
+ content = `---
391
+ change-id: ${changeId}
392
+ status: ${inferredStatus}
393
+ ---
394
+
395
+ ` + content;
396
+ modified = true;
397
+ taskChanges.push("added YAML frontmatter");
398
+ }
399
+ if (!content.includes("[x]=done")) {
400
+ content = content.replace(
401
+ /^(---\n[\s\S]*?---\n)/,
402
+ `$1
403
+ <!-- [x]=done [-]=N/A [ ]=pending -->
404
+ `
405
+ );
406
+ modified = true;
407
+ taskChanges.push("added [x]/[-]/[ ] legend comment");
408
+ }
409
+ if (enableContextGovernance && !/^context-governance:\s*v1\b/m.test(content)) {
410
+ content = upsertFrontmatterField(content, "context-governance", "v1");
411
+ modified = true;
412
+ taskChanges.push("enabled context-governance: v1");
413
+ }
414
+ if (modified) {
415
+ changed.push(`tasks.md: ${taskChanges.join("; ")}`);
416
+ if (!dryRun)
417
+ writeFileSync4(tasksPath, content, "utf8");
418
+ }
419
+ } else {
420
+ warnings.push("tasks.md not found \u2014 skipping frontmatter migration");
421
+ }
422
+ const classifPath = join12(changeDir, "change-classification.md");
423
+ if (existsSync11(classifPath)) {
424
+ const content = readFileSync9(classifPath, "utf8");
425
+ const hasNewTierFormat = /^## Tier\s*\n\s*-\s*\d\s*$/m.test(content);
426
+ if (!hasNewTierFormat) {
427
+ const oldMatch = content.match(/\*\*Tier[:\*]+\s*(?:Tier\s*)?(\d)/i) ?? content.match(/^-?\s*Tier:\s*(?:Tier\s*)?(\d)/mi);
428
+ const detectedTier = oldMatch ? oldMatch[1] : null;
429
+ if (detectedTier) {
430
+ const addition = `
431
+ ## Tier
432
+ - ${detectedTier}
433
+ `;
434
+ if (!content.includes("\n## Tier\n")) {
435
+ changed.push(
436
+ `change-classification.md: appended "## Tier\\n- ${detectedTier}" (converted from old format)`
437
+ );
438
+ if (!dryRun)
439
+ writeFileSync4(classifPath, content + addition, "utf8");
440
+ }
441
+ } else {
442
+ warnings.push(
443
+ "change-classification.md: could not detect tier (no **Tier:** N or ## Tier N found). gate tier-based agent-log checks will be skipped for this change."
444
+ );
445
+ }
446
+ }
447
+ }
448
+ const manifestPath = join12(changeDir, "context-manifest.md");
449
+ if (!existsSync11(manifestPath)) {
450
+ changed.push(enableContextGovernance ? "context-manifest.md: added context-governance v1 manifest scaffold" : "context-manifest.md: added legacy context manifest scaffold");
451
+ if (!dryRun) {
452
+ writeFileSync4(
453
+ manifestPath,
454
+ enableContextGovernance ? buildContextGovernedManifest(changeId) : buildLegacyContextManifest(changeId),
455
+ "utf8"
456
+ );
457
+ }
458
+ } else if (enableContextGovernance) {
459
+ warnings.push("context-manifest.md already exists \u2014 review allowed paths before relying on context-governance: v1");
460
+ }
461
+ return { changed, warnings };
462
+ }
463
+ async function migrate(changeId, opts = {}) {
464
+ const cwd = process.cwd();
465
+ const dryRun = opts.dryRun ?? false;
466
+ const enableContextGovernance = opts.enableContextGovernance ?? false;
467
+ const idsToMigrate = [];
468
+ if (opts.all) {
469
+ const changesDir = join12(cwd, "specs", "changes");
470
+ if (!existsSync11(changesDir)) {
471
+ log.info("No specs/changes/ directory found \u2014 nothing to migrate.");
472
+ return;
473
+ }
474
+ idsToMigrate.push(
475
+ ...readdirSync7(changesDir, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name)
476
+ );
477
+ } else if (changeId) {
478
+ const specificDir = join12(cwd, "specs", "changes", changeId);
479
+ if (!existsSync11(specificDir)) {
480
+ log.error(`Change not found: specs/changes/${changeId}`);
481
+ process.exit(1);
482
+ }
483
+ idsToMigrate.push(changeId);
484
+ } else {
485
+ log.error("Usage: cdd-kit migrate <change-id> | cdd-kit migrate --all [--dry-run]");
486
+ process.exit(1);
487
+ }
488
+ if (idsToMigrate.length === 0) {
489
+ log.info("No changes found to migrate.");
490
+ return;
491
+ }
492
+ if (dryRun) {
493
+ log.info("Dry run \u2014 no files will be written.");
494
+ log.blank();
495
+ }
496
+ let migratedCount = 0;
497
+ let upToDateCount = 0;
498
+ for (const id of idsToMigrate) {
499
+ const changeDir = join12(cwd, "specs", "changes", id);
500
+ if (!existsSync11(changeDir)) {
501
+ log.warn(` ${id}: directory not found \u2014 skipping`);
502
+ continue;
503
+ }
504
+ const { changed, warnings } = migrateOne(id, changeDir, dryRun, enableContextGovernance);
505
+ if (changed.length > 0) {
506
+ log.ok(` ${id}: migrated`);
507
+ for (const c of changed)
508
+ log.info(` + ${c}`);
509
+ migratedCount++;
510
+ } else {
511
+ log.info(` ${id}: already up to date`);
512
+ upToDateCount++;
513
+ }
514
+ for (const w of warnings) {
515
+ log.warn(` ${id}: ${w}`);
516
+ }
517
+ }
518
+ log.blank();
519
+ if (dryRun) {
520
+ log.info(`Dry run complete: ${migratedCount} change(s) would be updated, ${upToDateCount} already up to date.`);
521
+ } else {
522
+ log.ok(`Migration complete: ${migratedCount} updated, ${upToDateCount} already up to date.`);
523
+ if (migratedCount > 0) {
524
+ log.info('Next: git add specs/changes/ && git commit -m "chore: migrate changes to current cdd-kit format"');
525
+ }
526
+ }
527
+ }
528
+ var init_migrate = __esm({
529
+ "src/commands/migrate.ts"() {
530
+ "use strict";
531
+ init_logger();
532
+ }
533
+ });
534
+
535
+ // src/commands/upgrade.ts
536
+ var upgrade_exports = {};
537
+ __export(upgrade_exports, {
538
+ upgrade: () => upgrade
539
+ });
540
+ import { existsSync as existsSync12, mkdirSync as mkdirSync4, readdirSync as readdirSync8, copyFileSync as copyFileSync3, writeFileSync as writeFileSync5 } from "fs";
541
+ import { dirname as dirname3, join as join13, relative as relative2 } from "path";
542
+ function planMissingFiles(srcDir, destDir, label, planned) {
543
+ if (!existsSync12(srcDir))
544
+ return;
545
+ for (const entry of readdirSync8(srcDir, { withFileTypes: true })) {
546
+ const src = join13(srcDir, entry.name);
547
+ const dest = join13(destDir, entry.name);
548
+ if (entry.isDirectory()) {
549
+ planMissingFiles(src, dest, join13(label, entry.name), planned);
550
+ continue;
551
+ }
552
+ if (!existsSync12(dest)) {
553
+ planned.push({ src, dest, rel: join13(label, relative2(srcDir, src)) });
554
+ }
555
+ }
556
+ }
557
+ function planProviderGuidance(cwd, provider, planned) {
558
+ if (provider === "claude" || provider === "both") {
559
+ if (!existsSync12(join13(cwd, "CLAUDE.md"))) {
560
+ planned.push({ src: ASSET.claudeTemplate, dest: join13(cwd, "CLAUDE.md"), rel: "CLAUDE.md" });
561
+ }
562
+ if (!existsSync12(join13(cwd, "AGENTS.md"))) {
563
+ planned.push({ src: ASSET.agentsTemplate, dest: join13(cwd, "AGENTS.md"), rel: "AGENTS.md" });
564
+ }
565
+ }
566
+ if ((provider === "codex" || provider === "both") && !existsSync12(join13(cwd, "CODEX.md"))) {
567
+ planned.push({ src: ASSET.codexTemplate, dest: join13(cwd, "CODEX.md"), rel: "CODEX.md" });
568
+ }
569
+ }
570
+ function applyCopy(plan) {
571
+ for (const item of plan) {
572
+ mkdirSync4(dirname3(item.dest), { recursive: true });
573
+ copyFileSync3(item.src, item.dest);
574
+ }
575
+ }
576
+ async function upgrade(opts = {}) {
577
+ const cwd = process.cwd();
578
+ const requestedProvider = opts.provider ?? "auto";
579
+ if (!validateProviderOption(requestedProvider)) {
580
+ log.error(`Invalid provider: ${requestedProvider}. Use auto, claude, codex, or both.`);
581
+ process.exit(1);
582
+ }
583
+ const provider = inferProvider(cwd, requestedProvider);
584
+ const plan = [];
585
+ planMissingFiles(ASSET.contracts, join13(cwd, "contracts"), "contracts", plan);
586
+ planMissingFiles(ASSET.specsTemplates, join13(cwd, "specs", "templates"), "specs/templates", plan);
587
+ planMissingFiles(ASSET.testsTemplates, join13(cwd, "tests", "templates"), "tests/templates", plan);
588
+ planMissingFiles(ASSET.ci, join13(cwd, "ci"), "ci", plan);
589
+ planMissingFiles(ASSET.githubWorkflows, join13(cwd, ".github", "workflows"), ".github/workflows", plan);
590
+ planMissingFiles(ASSET.cddConfig, join13(cwd, ".cdd"), ".cdd", plan);
591
+ planProviderGuidance(cwd, provider, plan);
592
+ log.blank();
593
+ log.info(`Upgrade provider: ${provider}`);
594
+ if (plan.length === 0) {
595
+ log.ok("No missing cdd-kit project files found.");
596
+ if (opts.migrateChanges) {
597
+ log.blank();
598
+ log.info("Running change migration flow...");
599
+ await migrate(void 0, {
600
+ all: true,
601
+ dryRun: !opts.yes,
602
+ enableContextGovernance: opts.enableContextGovernance
603
+ });
604
+ }
605
+ log.blank();
606
+ return;
607
+ }
608
+ log.info(`${plan.length} missing file(s) detected:`);
609
+ for (const item of plan)
610
+ log.dim(` + ${item.rel.replace(/\\/g, "/")}`);
611
+ if (!opts.yes) {
612
+ log.blank();
613
+ log.info("Dry run only. Re-run with --yes to write missing files.");
614
+ if (opts.migrateChanges) {
615
+ log.blank();
616
+ log.info("Previewing existing change migration because --migrate-changes was requested.");
617
+ await migrate(void 0, {
618
+ all: true,
619
+ dryRun: true,
620
+ enableContextGovernance: opts.enableContextGovernance
621
+ });
622
+ }
623
+ log.blank();
624
+ return;
625
+ }
626
+ applyCopy(plan);
627
+ const modelPolicyPath = join13(cwd, ".cdd", "model-policy.json");
628
+ if (existsSync12(modelPolicyPath)) {
629
+ writeFileSync5(modelPolicyPath, JSON.stringify({
630
+ provider,
631
+ generated_at: (/* @__PURE__ */ new Date()).toISOString(),
632
+ roles: {}
633
+ }, null, 2) + "\n", "utf8");
634
+ }
635
+ log.blank();
636
+ log.ok(`Upgrade complete: ${plan.length} missing file(s) added.`);
637
+ log.info("Existing project guidance and contracts were preserved.");
638
+ if (opts.migrateChanges) {
639
+ log.blank();
640
+ log.info("Running change migration flow...");
641
+ await migrate(void 0, {
642
+ all: true,
643
+ dryRun: false,
644
+ enableContextGovernance: opts.enableContextGovernance
645
+ });
646
+ }
647
+ log.blank();
648
+ }
649
+ var init_upgrade = __esm({
650
+ "src/commands/upgrade.ts"() {
651
+ "use strict";
652
+ init_paths();
653
+ init_logger();
654
+ init_provider();
655
+ init_migrate();
656
+ }
657
+ });
658
+
659
+ // src/commands/archive.ts
660
+ var archive_exports = {};
661
+ __export(archive_exports, {
662
+ archive: () => archive
663
+ });
664
+ import { join as join14 } from "path";
665
+ import { existsSync as existsSync13, mkdirSync as mkdirSync5, renameSync, readFileSync as readFileSync10, writeFileSync as writeFileSync6, appendFileSync, cpSync as cpSync2, rmSync as rmSync2 } from "fs";
666
+ async function archive(changeId) {
667
+ const cwd = process.cwd();
668
+ const changeDir = join14(cwd, "specs", "changes", changeId);
669
+ const archiveYear = (/* @__PURE__ */ new Date()).getFullYear().toString();
670
+ const archiveBase = join14(cwd, "specs", "archive", archiveYear);
671
+ const archiveDir = join14(archiveBase, changeId);
672
+ const indexPath = join14(cwd, "specs", "archive", "INDEX.md");
673
+ if (!existsSync13(changeDir)) {
674
+ log.error(`Change not found: specs/changes/${changeId}`);
675
+ process.exit(1);
676
+ }
677
+ if (existsSync13(archiveDir)) {
678
+ log.error(`Already archived: specs/archive/${archiveYear}/${changeId}`);
679
+ process.exit(1);
680
+ }
681
+ const tasksPath = join14(changeDir, "tasks.md");
682
+ if (existsSync13(tasksPath)) {
683
+ const content = readFileSync10(tasksPath, "utf8");
684
+ if (content.includes("status: gate-blocked")) {
685
+ log.warn("tasks.md has status: gate-blocked \u2014 archiving anyway (change was paused).");
686
+ }
687
+ const pending = (content.match(/^\s*-\s*\[ \]/gm) || []).length;
688
+ if (pending > 0) {
689
+ log.warn(`${pending} task(s) still pending ([ ]). Archive anyway.`);
690
+ }
691
+ }
692
+ if (!existsSync13(archiveBase)) {
693
+ mkdirSync5(archiveBase, { recursive: true });
694
+ }
695
+ try {
696
+ renameSync(changeDir, archiveDir);
697
+ } catch (err) {
698
+ if (err.code === "EXDEV") {
699
+ cpSync2(changeDir, archiveDir, { recursive: true });
700
+ rmSync2(changeDir, { recursive: true, force: true });
701
+ } else {
702
+ throw err;
703
+ }
704
+ }
705
+ log.ok(`Archived: specs/changes/${changeId} \u2192 specs/archive/${archiveYear}/${changeId}`);
706
+ const today = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
707
+ const indexLine = `| ${changeId} | ${archiveYear} | ${today} | specs/archive/${archiveYear}/${changeId}/ |
708
+ `;
709
+ if (!existsSync13(indexPath)) {
710
+ writeFileSync6(indexPath, `# Archive Index
711
+
712
+ | change-id | year | archived-date | path |
713
+ |---|---|---|---|
714
+ ${indexLine}`, "utf8");
715
+ } else {
716
+ appendFileSync(indexPath, indexLine, "utf8");
717
+ }
718
+ log.ok(`Index updated: specs/archive/INDEX.md`);
719
+ log.blank();
720
+ log.info(`Next: promote durable learnings from archive.md to contracts/ or CLAUDE.md`);
721
+ }
722
+ var init_archive = __esm({
723
+ "src/commands/archive.ts"() {
724
+ "use strict";
725
+ init_logger();
726
+ }
727
+ });
728
+
729
+ // src/commands/abandon.ts
730
+ var abandon_exports = {};
731
+ __export(abandon_exports, {
732
+ abandon: () => abandon
733
+ });
734
+ import { join as join15 } from "path";
735
+ import { existsSync as existsSync14, readFileSync as readFileSync11, writeFileSync as writeFileSync7, appendFileSync as appendFileSync2, mkdirSync as mkdirSync6 } from "fs";
736
+ async function abandon(changeId, opts) {
737
+ const cwd = process.cwd();
738
+ const changeDir = join15(cwd, "specs", "changes", changeId);
739
+ const tasksPath = join15(changeDir, "tasks.md");
740
+ if (!existsSync14(changeDir)) {
741
+ log.error(`Change not found: specs/changes/${changeId}`);
742
+ process.exit(1);
743
+ }
744
+ if (existsSync14(tasksPath)) {
745
+ let content = readFileSync11(tasksPath, "utf8");
746
+ if (content.match(/^status:/m)) {
747
+ content = content.replace(/^status: .*/m, "status: abandoned");
748
+ } else {
749
+ content = `---
750
+ change-id: ${changeId}
751
+ status: abandoned
752
+ ---
753
+
754
+ ` + content;
755
+ }
756
+ writeFileSync7(tasksPath, content, "utf8");
757
+ }
758
+ const today = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
759
+ const archiveDir = join15(cwd, "specs", "archive");
760
+ const indexPath = join15(archiveDir, "INDEX.md");
761
+ const reason = opts.reason ?? "no reason given";
762
+ const indexLine = `| ${changeId} | abandoned | ${today} | ${reason} |
763
+ `;
764
+ if (!existsSync14(archiveDir)) {
765
+ mkdirSync6(archiveDir, { recursive: true });
766
+ }
767
+ if (!existsSync14(indexPath)) {
768
+ writeFileSync7(indexPath, `# Archive Index
769
+
770
+ | change-id | status | date | notes |
771
+ |---|---|---|---|
772
+ ${indexLine}`, "utf8");
773
+ } else {
774
+ appendFileSync2(indexPath, indexLine, "utf8");
775
+ }
776
+ log.ok(`Change ${changeId} marked as abandoned.`);
777
+ log.info(`specs/changes/${changeId}/ remains on disk (git history preserved).`);
778
+ log.info(`Run \`cdd-kit archive ${changeId}\` to physically move it, or leave it for git history.`);
779
+ }
780
+ var init_abandon = __esm({
781
+ "src/commands/abandon.ts"() {
782
+ "use strict";
783
+ init_logger();
784
+ }
785
+ });
786
+
787
+ // src/commands/list-changes.ts
788
+ var list_changes_exports = {};
789
+ __export(list_changes_exports, {
790
+ listChanges: () => listChanges
791
+ });
792
+ import { join as join16 } from "path";
793
+ import { existsSync as existsSync15, readdirSync as readdirSync9, readFileSync as readFileSync12 } from "fs";
794
+ async function listChanges() {
795
+ const cwd = process.cwd();
796
+ const changesDir = join16(cwd, "specs", "changes");
797
+ log.blank();
798
+ const active = [];
799
+ if (existsSync15(changesDir)) {
800
+ active.push(...readdirSync9(changesDir, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name));
801
+ }
802
+ if (active.length === 0) {
803
+ log.info("No active changes in specs/changes/");
804
+ } else {
805
+ log.info("Active changes:");
806
+ for (const id of active) {
807
+ const tasksPath = join16(changesDir, id, "tasks.md");
808
+ let status = "in-progress";
809
+ let pending = 0;
810
+ if (existsSync15(tasksPath)) {
811
+ const content = readFileSync12(tasksPath, "utf8");
812
+ if (content.includes("status: gate-blocked"))
813
+ status = "gate-blocked";
814
+ else if (content.includes("status: abandoned"))
815
+ status = "abandoned";
816
+ pending = (content.match(/^\s*-\s*\[ \]/gm) || []).length;
817
+ }
818
+ const pendingStr = pending > 0 ? ` (${pending} pending)` : "";
819
+ log.info(` ${id} [${status}]${pendingStr}`);
820
+ }
821
+ }
822
+ log.blank();
823
+ }
824
+ var init_list_changes = __esm({
825
+ "src/commands/list-changes.ts"() {
826
+ "use strict";
827
+ init_logger();
828
+ }
829
+ });
830
+
831
+ // src/commands/context-scan.ts
832
+ var context_scan_exports = {};
833
+ __export(context_scan_exports, {
834
+ contextScan: () => contextScan
835
+ });
836
+ import { existsSync as existsSync16, mkdirSync as mkdirSync7, readFileSync as readFileSync13, readdirSync as readdirSync10, writeFileSync as writeFileSync8 } from "fs";
837
+ import { basename, dirname as dirname4, join as join17, relative as relative3 } from "path";
838
+ function stripGlobSuffix(pattern) {
839
+ return pattern.replace(/\/\*\*$/, "").replace(/\/\*$/, "");
840
+ }
841
+ function getForbiddenPaths(cwd) {
842
+ const forbidden = new Set(DEFAULT_FORBIDDEN);
843
+ const policyPath = join17(cwd, ".cdd", "context-policy.json");
844
+ try {
845
+ if (existsSync16(policyPath)) {
846
+ const policy = JSON.parse(readFileSync13(policyPath, "utf8"));
847
+ for (const pattern of policy.forbiddenPaths ?? []) {
848
+ forbidden.add(stripGlobSuffix(pattern));
849
+ }
850
+ }
851
+ } catch {
852
+ log.warn("Could not parse .cdd/context-policy.json; using default context-scan excludes.");
853
+ }
854
+ return [...forbidden];
855
+ }
856
+ function isForbidden(relPath, forbidden) {
857
+ const normalized = relPath.replace(/\\/g, "/");
858
+ return forbidden.some((pattern) => normalized === pattern || normalized.startsWith(`${pattern}/`));
859
+ }
860
+ function buildTree(dir, cwd, forbidden, stats, prefix = "", depth = 0) {
861
+ const entries = readdirSync10(dir, { withFileTypes: true }).sort((a, b) => {
862
+ if (a.isDirectory() === b.isDirectory())
863
+ return a.name.localeCompare(b.name);
864
+ return a.isDirectory() ? -1 : 1;
865
+ });
866
+ let output = "";
867
+ const visible = entries.filter((entry) => {
868
+ const relPath = relative3(cwd, join17(dir, entry.name));
869
+ return !isForbidden(relPath, forbidden);
870
+ });
871
+ visible.forEach((entry, index) => {
872
+ const fullPath = join17(dir, entry.name);
873
+ const isLast = index === visible.length - 1;
874
+ const connector = isLast ? "\\-- " : "|-- ";
875
+ output += `${prefix}${connector}${entry.name}${entry.isDirectory() ? "/" : ""}
876
+ `;
877
+ if (entry.isDirectory()) {
878
+ stats.dirs += 1;
879
+ if (depth >= 3) {
880
+ stats.omittedDirs += 1;
881
+ output += `${prefix}${isLast ? " " : "| "}\\-- ... (max depth)
882
+ `;
883
+ } else {
884
+ output += buildTree(fullPath, cwd, forbidden, stats, prefix + (isLast ? " " : "| "), depth + 1);
885
+ }
886
+ } else {
887
+ stats.files += 1;
888
+ }
889
+ });
890
+ return output;
891
+ }
892
+ function firstHeading(content) {
893
+ const match = content.match(/^#\s+(.+)$/m);
894
+ return match?.[1]?.trim();
895
+ }
896
+ function deriveContractType(relPath, metadata) {
897
+ if (metadata.contract)
898
+ return metadata.contract;
899
+ const parts = relPath.split("/");
900
+ return parts.length >= 2 ? parts[1] : "unknown";
901
+ }
902
+ function parseContractMetadata(content) {
903
+ const metadata = {};
904
+ let summary;
905
+ const cddMatch = content.match(/<!--\s*cdd:([\s\S]*?)-->/);
906
+ const yamlMatch = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
907
+ const block = cddMatch?.[1] ?? yamlMatch?.[1];
908
+ if (block) {
909
+ for (const line of block.split(/\r?\n/)) {
910
+ const colon = line.indexOf(":");
911
+ if (colon === -1)
912
+ continue;
913
+ const key = line.slice(0, colon).trim();
914
+ const value = line.slice(colon + 1).trim();
915
+ if (!key || !value)
916
+ continue;
917
+ if (key === "summary")
918
+ summary = value;
919
+ else
920
+ metadata[key] = value;
921
+ }
922
+ }
923
+ if (!summary) {
924
+ const summaryMatch = content.match(/#+\s*Summary\s*\r?\n+([^#\r\n][^\r\n]*)/i);
925
+ summary = summaryMatch?.[1]?.trim();
926
+ }
927
+ return { title: firstHeading(content), summary, metadata };
928
+ }
929
+ function findContractFiles(dir, found = []) {
930
+ if (!existsSync16(dir))
931
+ return found;
932
+ for (const entry of readdirSync10(dir, { withFileTypes: true })) {
933
+ const fullPath = join17(dir, entry.name);
934
+ if (entry.isDirectory())
935
+ findContractFiles(fullPath, found);
936
+ else if (entry.isFile() && entry.name.endsWith(".md") && entry.name !== "INDEX.md" && entry.name !== "CHANGELOG.md")
937
+ found.push(fullPath);
938
+ }
939
+ return found;
940
+ }
941
+ async function contextScan() {
942
+ const cwd = process.cwd();
943
+ const specsContextDir = join17(cwd, "specs", "context");
944
+ mkdirSync7(specsContextDir, { recursive: true });
945
+ const forbidden = getForbiddenPaths(cwd);
946
+ const treeStats = { dirs: 0, files: 0, omittedDirs: 0 };
947
+ const tree = buildTree(cwd, cwd, forbidden, treeStats);
948
+ writeFileSync8(
949
+ join17(specsContextDir, "project-map.md"),
950
+ [
951
+ "---",
952
+ "artifact: project-map",
953
+ "generated-by: cdd-kit context-scan",
954
+ "schema-version: 1",
955
+ `root: ${basename(cwd)}`,
956
+ `visible-dirs: ${treeStats.dirs}`,
957
+ `visible-files: ${treeStats.files}`,
958
+ `omitted-dirs: ${treeStats.omittedDirs}`,
959
+ "---",
960
+ "",
961
+ "# Project Map",
962
+ "",
963
+ "Use this deterministic map to choose candidate context paths before reading files.",
964
+ "",
965
+ "## Excluded Paths",
966
+ ...forbidden.map((path) => `- ${path}`),
967
+ "",
968
+ "## Tree",
969
+ "",
970
+ "```",
971
+ `${basename(cwd)}/`,
972
+ tree.trimEnd(),
973
+ "```",
974
+ ""
975
+ ].join("\n"),
976
+ "utf8"
977
+ );
978
+ log.ok("Created specs/context/project-map.md");
979
+ const contractFiles = findContractFiles(join17(cwd, "contracts")).sort((a, b) => relative3(cwd, a).localeCompare(relative3(cwd, b)));
980
+ const contractEntries = [];
981
+ const inventoryRows = [];
982
+ let missingSummary = 0;
983
+ for (const file of contractFiles) {
984
+ const relPath = relative3(cwd, file).replace(/\\/g, "/");
985
+ const dir = dirname4(relPath).replace(/\\/g, "/");
986
+ const { title, summary, metadata } = parseContractMetadata(readFileSync13(file, "utf8"));
987
+ const contractType = deriveContractType(relPath, metadata);
988
+ const owner = metadata.owner ?? "unknown";
989
+ const surface = metadata.surface ?? dir;
990
+ const summaryText = summary ?? "MISSING - add YAML frontmatter `summary:` or `<!-- cdd: summary: ... -->`.";
991
+ inventoryRows.push(`| ${relPath} | ${contractType} | ${surface} | ${owner} | ${summary ? "yes" : "no"} |`);
992
+ let entry = `## ${relPath}
993
+ `;
994
+ entry += `- path: \`${relPath}\`
995
+ `;
996
+ entry += `- type: ${contractType}
997
+ `;
998
+ entry += `- directory: ${dir}
999
+ `;
1000
+ if (title)
1001
+ entry += `- title: ${title}
1002
+ `;
1003
+ for (const [key, value] of Object.entries(metadata)) {
1004
+ if (key === "contract")
1005
+ continue;
1006
+ entry += `- ${key}: ${value}
1007
+ `;
1008
+ }
1009
+ entry += `- summary: ${summaryText}
1010
+
1011
+ `;
1012
+ contractEntries.push(entry);
1013
+ if (!summary) {
1014
+ missingSummary += 1;
1015
+ }
1016
+ }
1017
+ const contractIndex = [
1018
+ "---",
1019
+ "artifact: contracts-index",
1020
+ "generated-by: cdd-kit context-scan",
1021
+ "schema-version: 1",
1022
+ `contract-count: ${contractFiles.length}`,
1023
+ `missing-summary-count: ${missingSummary}`,
1024
+ "---",
1025
+ "",
1026
+ "# Contracts Index",
1027
+ "",
1028
+ "Generated from deterministic metadata. Add YAML frontmatter fields such as `summary`, `owner`, and `surface` to improve classifier accuracy.",
1029
+ "",
1030
+ "## Contract Inventory",
1031
+ "",
1032
+ "| path | type | surface | owner | has-summary |",
1033
+ "|---|---|---|---|---|",
1034
+ ...inventoryRows,
1035
+ "",
1036
+ "## Contract Details",
1037
+ "",
1038
+ ...contractEntries
1039
+ ].join("\n");
1040
+ writeFileSync8(join17(specsContextDir, "contracts-index.md"), contractIndex, "utf8");
1041
+ if (missingSummary > 0) {
1042
+ log.warn(`Created specs/context/contracts-index.md with ${missingSummary} missing summary warning(s).`);
1043
+ } else {
1044
+ log.ok("Created specs/context/contracts-index.md");
1045
+ }
1046
+ }
1047
+ var DEFAULT_FORBIDDEN;
1048
+ var init_context_scan = __esm({
1049
+ "src/commands/context-scan.ts"() {
1050
+ "use strict";
1051
+ init_logger();
1052
+ DEFAULT_FORBIDDEN = [
1053
+ ".claude",
1054
+ ".git",
1055
+ "node_modules",
1056
+ "dist",
1057
+ "build",
1058
+ "assets",
1059
+ "specs/archive",
1060
+ "specs/changes"
1061
+ ];
1062
+ }
1063
+ });
1064
+
1065
+ // src/commands/context.ts
1066
+ var context_exports = {};
1067
+ __export(context_exports, {
1068
+ approveContextExpansion: () => approveContextExpansion,
1069
+ listContextExpansions: () => listContextExpansions,
1070
+ rejectContextExpansion: () => rejectContextExpansion,
1071
+ requestContextExpansion: () => requestContextExpansion
1072
+ });
1073
+ import { existsSync as existsSync17, readFileSync as readFileSync14, writeFileSync as writeFileSync9 } from "fs";
1074
+ import { join as join18 } from "path";
1075
+ function normalizePath(path) {
1076
+ return path.replace(/\\/g, "/").replace(/^\.\//, "").trim();
1077
+ }
1078
+ function validateRepoRelativePath(path) {
1079
+ if (/^[a-zA-Z]:\//.test(path) || path.startsWith("/")) {
1080
+ return `requested path must be repo-relative: ${path}`;
1081
+ }
1082
+ if (path.split("/").includes("..")) {
1083
+ return `requested path must not contain "..": ${path}`;
1084
+ }
1085
+ return null;
1086
+ }
1087
+ function manifestPathFor(changeId) {
1088
+ return join18(process.cwd(), "specs", "changes", changeId, "context-manifest.md");
1089
+ }
1090
+ function readManifest(changeId) {
1091
+ const manifestPath = manifestPathFor(changeId);
1092
+ if (!existsSync17(manifestPath)) {
1093
+ log.error(`context manifest not found: specs/changes/${changeId}/context-manifest.md`);
1094
+ process.exit(1);
1095
+ }
1096
+ return readFileSync14(manifestPath, "utf8");
1097
+ }
1098
+ function writeManifest(changeId, content) {
1099
+ writeFileSync9(manifestPathFor(changeId), content.endsWith("\n") ? content : `${content}
1100
+ `, "utf8");
1101
+ }
1102
+ function sectionBody(content, heading) {
1103
+ const match = content.match(new RegExp(`## ${heading}\\s*\\n([\\s\\S]*?)(?=\\n## |$)`));
1104
+ return match?.[1] ?? "";
1105
+ }
1106
+ function parseRequests(content) {
1107
+ const body = sectionBody(content, "Context Expansion Requests");
1108
+ if (!body.trim())
1109
+ return [];
1110
+ const requests = [];
1111
+ const blocks = body.split(/(?=^\s*-\s*request-id:\s*)/m);
1112
+ for (const block of blocks) {
1113
+ const idMatch = block.match(/^\s*-\s*request-id:\s*(\S+)/m);
1114
+ if (!idMatch)
1115
+ continue;
1116
+ const statusMatch = block.match(/^\s*status:\s*(\S+)/im);
1117
+ const reasonMatch = block.match(/^\s*reason:\s*(.+)$/im);
1118
+ const paths = [];
1119
+ let inPaths = false;
1120
+ for (const line of block.split(/\r?\n/)) {
1121
+ if (/^\s*requested_paths:\s*$/.test(line)) {
1122
+ inPaths = true;
1123
+ continue;
1124
+ }
1125
+ if (!inPaths)
1126
+ continue;
1127
+ const item = line.match(/^\s*-\s+(.+?)\s*$/);
1128
+ if (item) {
1129
+ paths.push(normalizePath(item[1]));
1130
+ continue;
1131
+ }
1132
+ if (/^\s*[a-zA-Z_-]+:\s*/.test(line))
1133
+ break;
1134
+ }
1135
+ requests.push({
1136
+ requestId: idMatch[1],
1137
+ paths,
1138
+ reason: reasonMatch?.[1]?.trim(),
1139
+ status: statusMatch?.[1]?.trim().toLowerCase() ?? "unknown"
1140
+ });
1141
+ }
1142
+ return requests;
1143
+ }
1144
+ function approvedExpansionSet(content) {
1145
+ const body = sectionBody(content, "Approved Expansions");
1146
+ const approved = /* @__PURE__ */ new Set();
1147
+ for (const line of body.split(/\r?\n/)) {
1148
+ const item = line.match(/^\s*-\s+(.+?)\s*$/);
1149
+ if (!item)
1150
+ continue;
1151
+ const value = normalizePath(item[1]);
1152
+ if (value && value !== "-")
1153
+ approved.add(value);
1154
+ }
1155
+ return approved;
1156
+ }
1157
+ function replaceSection(content, heading, lines) {
1158
+ const nextSection = [`## ${heading}`, ...lines, ""].join("\n");
1159
+ const pattern = new RegExp(`## ${heading}\\s*\\n[\\s\\S]*?(?=\\n## |$)`);
1160
+ if (pattern.test(content))
1161
+ return content.replace(pattern, nextSection.trimEnd());
1162
+ return `${content.trimEnd()}
1163
+
1164
+ ${nextSection}`;
1165
+ }
1166
+ function renderRequests(requests) {
1167
+ if (requests.length === 0)
1168
+ return ["-"];
1169
+ const lines = [];
1170
+ for (const request of requests) {
1171
+ lines.push(`- request-id: ${request.requestId}`);
1172
+ lines.push(" requested_paths:");
1173
+ for (const path of request.paths)
1174
+ lines.push(` - ${path}`);
1175
+ if (request.reason)
1176
+ lines.push(` reason: ${request.reason}`);
1177
+ lines.push(` status: ${request.status}`);
1178
+ lines.push("");
1179
+ }
1180
+ if (lines[lines.length - 1] === "")
1181
+ lines.pop();
1182
+ return lines;
1183
+ }
1184
+ function setRequestStatus(content, requestId, status) {
1185
+ const requests = parseRequests(content);
1186
+ const target = requests.find((request) => request.requestId === requestId);
1187
+ if (!target) {
1188
+ log.error(`context expansion request not found: ${requestId}`);
1189
+ process.exit(1);
1190
+ }
1191
+ if (target.status !== "pending") {
1192
+ log.error(`pending context expansion request not found: ${requestId}`);
1193
+ process.exit(1);
1194
+ }
1195
+ const next = requests.map((request) => request.requestId === requestId ? { ...request, status } : request);
1196
+ return replaceSection(content, "Context Expansion Requests", renderRequests(next));
1197
+ }
1198
+ async function requestContextExpansion(changeId, requestId, paths, reason) {
1199
+ if (paths.length === 0) {
1200
+ log.error("at least one --path value is required");
1201
+ process.exit(1);
1202
+ }
1203
+ const normalizedPaths = [...new Set(paths.map(normalizePath).filter(Boolean))];
1204
+ for (const path of normalizedPaths) {
1205
+ const validationError = validateRepoRelativePath(path);
1206
+ if (validationError) {
1207
+ log.error(validationError);
1208
+ process.exit(1);
1209
+ }
1210
+ }
1211
+ const content = readManifest(changeId);
1212
+ const requests = parseRequests(content);
1213
+ if (requests.some((request) => request.requestId === requestId)) {
1214
+ log.error(`context expansion request already exists: ${requestId}`);
1215
+ process.exit(1);
1216
+ }
1217
+ const next = replaceSection(content, "Context Expansion Requests", renderRequests([
1218
+ ...requests,
1219
+ { requestId, paths: normalizedPaths, reason, status: "pending" }
1220
+ ]));
1221
+ writeManifest(changeId, next);
1222
+ log.ok(`recorded context expansion request ${requestId} for ${changeId}`);
1223
+ for (const path of normalizedPaths)
1224
+ log.info(` ${path}`);
1225
+ }
1226
+ async function listContextExpansions(changeId, json = false) {
1227
+ const requests = parseRequests(readManifest(changeId));
1228
+ if (json) {
1229
+ console.log(JSON.stringify({ changeId, requests }, null, 2));
1230
+ return;
1231
+ }
1232
+ if (requests.length === 0) {
1233
+ log.info(`no context expansion requests for ${changeId}`);
1234
+ return;
1235
+ }
1236
+ log.info(`context expansion requests for ${changeId}`);
1237
+ for (const request of requests) {
1238
+ log.info(`- ${request.requestId} [${request.status}] ${request.reason ?? ""}`.trimEnd());
1239
+ for (const path of request.paths)
1240
+ log.dim(` ${path}`);
1241
+ }
1242
+ }
1243
+ async function approveContextExpansion(changeId, requestId) {
1244
+ const content = readManifest(changeId);
1245
+ const request = parseRequests(content).find((item) => item.requestId === requestId && item.status === "pending");
1246
+ if (!request) {
1247
+ log.error(`pending context expansion request not found: ${requestId}`);
1248
+ process.exit(1);
1249
+ }
1250
+ if (request.paths.length === 0) {
1251
+ log.error(`context expansion request has no requested_paths: ${requestId}`);
1252
+ process.exit(1);
1253
+ }
1254
+ for (const path of request.paths) {
1255
+ const validationError = validateRepoRelativePath(path);
1256
+ if (validationError) {
1257
+ log.error(validationError);
1258
+ process.exit(1);
1259
+ }
1260
+ }
1261
+ const approved = approvedExpansionSet(content);
1262
+ for (const path of request.paths)
1263
+ approved.add(path);
1264
+ let next = replaceSection(content, "Approved Expansions", [...[...approved].sort().map((path) => `- ${path}`)]);
1265
+ next = setRequestStatus(next, requestId, "approved");
1266
+ writeManifest(changeId, next);
1267
+ log.ok(`approved context expansion ${requestId} for ${changeId}`);
1268
+ for (const path of request.paths)
1269
+ log.info(` ${path}`);
1270
+ }
1271
+ async function rejectContextExpansion(changeId, requestId) {
1272
+ const next = setRequestStatus(readManifest(changeId), requestId, "rejected");
1273
+ writeManifest(changeId, next);
1274
+ log.ok(`rejected context expansion ${requestId} for ${changeId}`);
1275
+ }
1276
+ var init_context = __esm({
1277
+ "src/commands/context.ts"() {
1278
+ "use strict";
1279
+ init_logger();
1280
+ }
1281
+ });
1282
+
1
1283
  // src/cli/index.ts
2
- import { readFileSync as readFileSync6 } from "fs";
1284
+ import { readFileSync as readFileSync15 } from "fs";
3
1285
  import { fileURLToPath as fileURLToPath2 } from "url";
4
- import { dirname as dirname3, join as join10 } from "path";
1286
+ import { dirname as dirname5, join as join19 } from "path";
5
1287
  import { Command } from "commander";
6
1288
 
7
1289
  // src/commands/init.ts
1290
+ init_paths();
8
1291
  import { join as join4 } from "path";
9
1292
  import { rmSync, readFileSync as readFileSync2, writeFileSync, existsSync as existsSync3, readdirSync as readdirSync2 } from "fs";
10
1293
 
11
- // src/utils/paths.ts
12
- import { join, dirname } from "path";
13
- import { fileURLToPath } from "url";
14
- import { homedir } from "os";
15
- var __dirname = dirname(fileURLToPath(import.meta.url));
16
- var PACKAGE_ROOT = join(__dirname, "..", "..");
17
- var ASSETS_DIR = join(PACKAGE_ROOT, "assets");
18
- var CLAUDE_HOME = join(homedir(), ".claude");
19
- var AGENTS_HOME = join(CLAUDE_HOME, "agents");
20
- var SKILLS_HOME = join(CLAUDE_HOME, "skills");
21
- var ASSET = {
22
- agents: join(ASSETS_DIR, "agents"),
23
- skills: join(ASSETS_DIR, "skills"),
24
- skill: join(ASSETS_DIR, "skills", "contract-driven-delivery"),
25
- contracts: join(ASSETS_DIR, "contracts"),
26
- specsTemplates: join(ASSETS_DIR, "specs-templates"),
27
- testsTemplates: join(ASSETS_DIR, "tests-templates"),
28
- ci: join(ASSETS_DIR, "ci"),
29
- githubWorkflows: join(ASSETS_DIR, "github-workflows"),
30
- hooks: join(ASSETS_DIR, "hooks"),
31
- claudeTemplate: join(ASSETS_DIR, "CLAUDE.template.md"),
32
- agentsTemplate: join(ASSETS_DIR, "AGENTS.template.md")
33
- };
34
-
35
1294
  // src/utils/copy.ts
1295
+ init_logger();
36
1296
  import {
37
1297
  mkdirSync,
38
1298
  existsSync,
@@ -40,36 +1300,6 @@ import {
40
1300
  copyFileSync
41
1301
  } from "fs";
42
1302
  import { join as join2, dirname as dirname2, relative } from "path";
43
-
44
- // src/utils/logger.ts
45
- var RESET = "\x1B[0m";
46
- var CYAN = "\x1B[36m";
47
- var GREEN = "\x1B[32m";
48
- var YELLOW = "\x1B[33m";
49
- var RED = "\x1B[31m";
50
- var DIM = "\x1B[2m";
51
- var log = {
52
- info(msg) {
53
- console.log(`${CYAN}\u2139${RESET} ${msg}`);
54
- },
55
- ok(msg) {
56
- console.log(`${GREEN}\u2713${RESET} ${msg}`);
57
- },
58
- warn(msg) {
59
- console.log(`${YELLOW}\u26A0${RESET} ${msg}`);
60
- },
61
- error(msg) {
62
- console.error(`${RED}\u2717${RESET} ${msg}`);
63
- },
64
- dim(msg) {
65
- console.log(`${DIM} ${msg}${RESET}`);
66
- },
67
- blank() {
68
- console.log("");
69
- }
70
- };
71
-
72
- // src/utils/copy.ts
73
1303
  function ensureDir(dir) {
74
1304
  mkdirSync(dir, { recursive: true });
75
1305
  }
@@ -141,6 +1371,9 @@ function copyFileTracked(src, dest, opts = {}) {
141
1371
  return { written: true, created: isNew };
142
1372
  }
143
1373
 
1374
+ // src/commands/init.ts
1375
+ init_logger();
1376
+
144
1377
  // src/utils/stack-detect.ts
145
1378
  import { existsSync as existsSync2, readFileSync } from "fs";
146
1379
  import { join as join3 } from "path";
@@ -272,8 +1505,14 @@ async function init(opts) {
272
1505
  log.error("--global-only and --local-only are mutually exclusive.");
273
1506
  process.exit(1);
274
1507
  }
1508
+ if (!["claude", "codex", "both"].includes(opts.provider)) {
1509
+ log.error(`Invalid provider: ${opts.provider}. Use claude, codex, or both.`);
1510
+ process.exit(1);
1511
+ }
275
1512
  const cwd = process.cwd();
276
1513
  const createdPaths = [];
1514
+ const installClaude = opts.provider === "claude" || opts.provider === "both";
1515
+ const installCodex = opts.provider === "codex" || opts.provider === "both";
277
1516
  function track(paths) {
278
1517
  createdPaths.push(...paths);
279
1518
  }
@@ -292,7 +1531,7 @@ async function init(opts) {
292
1531
  log.info("Initialising contract-driven-delivery kit\u2026");
293
1532
  log.blank();
294
1533
  try {
295
- if (!opts.localOnly) {
1534
+ if (!opts.localOnly && installClaude) {
296
1535
  log.info(`Installing agents \u2192 ${AGENTS_HOME}`);
297
1536
  const { count: agentCount, created: agentCreated } = copyDirTracked(ASSET.agents, AGENTS_HOME, { overwrite: true });
298
1537
  track(agentCreated);
@@ -308,6 +1547,9 @@ async function init(opts) {
308
1547
  }
309
1548
  log.ok(`${totalSkillFiles} skill file(s) installed (${skillDirs.length} skills).`);
310
1549
  log.blank();
1550
+ } else if (!opts.localOnly && installCodex) {
1551
+ log.info("No global assets for provider: codex.");
1552
+ log.blank();
311
1553
  }
312
1554
  if (!opts.globalOnly) {
313
1555
  log.info(`Scaffolding project files in ${cwd}`);
@@ -339,6 +1581,21 @@ async function init(opts) {
339
1581
  );
340
1582
  track(ciCreated);
341
1583
  log.ok(`ci/ \u2014 ${ciCount} file(s) written.`);
1584
+ const { count: cddConfigCount, created: cddConfigCreated } = copyDirTracked(
1585
+ ASSET.cddConfig,
1586
+ join4(cwd, ".cdd"),
1587
+ { overwrite: opts.force, label: ".cdd" }
1588
+ );
1589
+ track(cddConfigCreated);
1590
+ log.ok(`.cdd/ - ${cddConfigCount} file(s) written.`);
1591
+ const modelPolicyPath = join4(cwd, ".cdd", "model-policy.json");
1592
+ if (existsSync3(modelPolicyPath)) {
1593
+ writeFileSync(modelPolicyPath, JSON.stringify({
1594
+ provider: opts.provider,
1595
+ generated_at: (/* @__PURE__ */ new Date()).toISOString(),
1596
+ roles: {}
1597
+ }, null, 2) + "\n", "utf8");
1598
+ }
342
1599
  const { count: wfCount, created: wfCreated } = copyDirTracked(
343
1600
  ASSET.githubWorkflows,
344
1601
  join4(cwd, ".github", "workflows"),
@@ -382,24 +1639,37 @@ async function init(opts) {
382
1639
  }
383
1640
  }
384
1641
  }
385
- const { written: claudeWritten, created: claudeCreated } = copyFileTracked(
386
- ASSET.claudeTemplate,
387
- join4(cwd, "CLAUDE.md"),
388
- { overwrite: false, label: "CLAUDE.md" }
389
- );
390
- if (claudeCreated)
391
- track([join4(cwd, "CLAUDE.md")]);
392
- if (claudeWritten)
393
- log.ok("CLAUDE.md created.");
394
- const { written: agentsWritten, created: agentsCreated } = copyFileTracked(
395
- ASSET.agentsTemplate,
396
- join4(cwd, "AGENTS.md"),
397
- { overwrite: false, label: "AGENTS.md" }
398
- );
399
- if (agentsCreated)
400
- track([join4(cwd, "AGENTS.md")]);
401
- if (agentsWritten)
402
- log.ok("AGENTS.md created.");
1642
+ if (installClaude) {
1643
+ const { written: claudeWritten, created: claudeCreated } = copyFileTracked(
1644
+ ASSET.claudeTemplate,
1645
+ join4(cwd, "CLAUDE.md"),
1646
+ { overwrite: false, label: "CLAUDE.md" }
1647
+ );
1648
+ if (claudeCreated)
1649
+ track([join4(cwd, "CLAUDE.md")]);
1650
+ if (claudeWritten)
1651
+ log.ok("CLAUDE.md created.");
1652
+ const { written: agentsWritten, created: agentsCreated } = copyFileTracked(
1653
+ ASSET.agentsTemplate,
1654
+ join4(cwd, "AGENTS.md"),
1655
+ { overwrite: false, label: "AGENTS.md" }
1656
+ );
1657
+ if (agentsCreated)
1658
+ track([join4(cwd, "AGENTS.md")]);
1659
+ if (agentsWritten)
1660
+ log.ok("AGENTS.md created.");
1661
+ }
1662
+ if (installCodex) {
1663
+ const { written: codexWritten, created: codexCreated } = copyFileTracked(
1664
+ ASSET.codexTemplate,
1665
+ join4(cwd, "CODEX.md"),
1666
+ { overwrite: false, label: "CODEX.md" }
1667
+ );
1668
+ if (codexCreated)
1669
+ track([join4(cwd, "CODEX.md")]);
1670
+ if (codexWritten)
1671
+ log.ok("CODEX.md created.");
1672
+ }
403
1673
  log.blank();
404
1674
  }
405
1675
  } catch (err) {
@@ -409,32 +1679,39 @@ async function init(opts) {
409
1679
  }
410
1680
  log.ok("Done.");
411
1681
  log.blank();
412
- log.info("Use the contract-driven-delivery skill in Claude Code to scan this repo.");
1682
+ if (opts.provider === "codex") {
1683
+ log.info("Use CODEX.md and cdd-kit commands to run the contract-driven workflow.");
1684
+ } else {
1685
+ log.info("Use the contract-driven-delivery skill in Claude Code to scan this repo.");
1686
+ }
413
1687
  log.blank();
414
1688
  }
415
1689
 
416
1690
  // src/commands/update.ts
417
- import { join as join5 } from "path";
418
- import { existsSync as existsSync4, mkdirSync as mkdirSync2, readdirSync as readdirSync3, copyFileSync as copyFileSync2, readFileSync as readFileSync3 } from "fs";
1691
+ init_paths();
1692
+ init_logger();
1693
+ init_provider();
1694
+ import { join as join6 } from "path";
1695
+ import { existsSync as existsSync5, mkdirSync as mkdirSync2, readdirSync as readdirSync3, copyFileSync as copyFileSync2, readFileSync as readFileSync4 } from "fs";
419
1696
  import { createHash } from "crypto";
420
1697
  import { homedir as homedir2 } from "os";
421
1698
  function fileHash(filePath) {
422
- const buf = readFileSync3(filePath);
1699
+ const buf = readFileSync4(filePath);
423
1700
  return createHash("sha256").update(buf).digest("hex");
424
1701
  }
425
1702
  function diffDir(src, dest) {
426
1703
  const entries = [];
427
- if (!existsSync4(src))
1704
+ if (!existsSync5(src))
428
1705
  return entries;
429
1706
  function walk(currentSrc, currentDest) {
430
1707
  const items = readdirSync3(currentSrc, { withFileTypes: true });
431
1708
  for (const item of items) {
432
- const srcPath = join5(currentSrc, item.name);
433
- const destPath = join5(currentDest, item.name);
1709
+ const srcPath = join6(currentSrc, item.name);
1710
+ const destPath = join6(currentDest, item.name);
434
1711
  if (item.isDirectory()) {
435
1712
  walk(srcPath, destPath);
436
1713
  } else {
437
- if (!existsSync4(destPath)) {
1714
+ if (!existsSync5(destPath)) {
438
1715
  entries.push({ src: srcPath, dest: destPath, action: "add" });
439
1716
  } else if (fileHash(srcPath) !== fileHash(destPath)) {
440
1717
  entries.push({ src: srcPath, dest: destPath, action: "overwrite" });
@@ -452,21 +1729,21 @@ function applyDir(entries) {
452
1729
  for (const e of entries) {
453
1730
  if (e.action === "skip")
454
1731
  continue;
455
- mkdirSync2(join5(e.dest, ".."), { recursive: true });
1732
+ mkdirSync2(join6(e.dest, ".."), { recursive: true });
456
1733
  copyFileSync2(e.src, e.dest);
457
1734
  count += 1;
458
1735
  }
459
1736
  return count;
460
1737
  }
461
1738
  function backupDir(dir, backupDest) {
462
- if (!existsSync4(dir))
1739
+ if (!existsSync5(dir))
463
1740
  return;
464
1741
  mkdirSync2(backupDest, { recursive: true });
465
1742
  function walk(src, dst) {
466
1743
  const items = readdirSync3(src, { withFileTypes: true });
467
1744
  for (const item of items) {
468
- const s = join5(src, item.name);
469
- const d = join5(dst, item.name);
1745
+ const s = join6(src, item.name);
1746
+ const d = join6(dst, item.name);
470
1747
  if (item.isDirectory()) {
471
1748
  mkdirSync2(d, { recursive: true });
472
1749
  walk(s, d);
@@ -478,15 +1755,29 @@ function backupDir(dir, backupDest) {
478
1755
  }
479
1756
  async function update(opts) {
480
1757
  log.blank();
481
- const skillDest = join5(SKILLS_HOME, "contract-driven-delivery");
482
- const agentDiff = diffDir(ASSET.agents, AGENTS_HOME);
483
- const skillDiff = diffDir(ASSET.skill, skillDest);
1758
+ const cwd = process.cwd();
1759
+ const requestedProvider = opts.provider ?? "auto";
1760
+ if (!validateProviderOption(requestedProvider)) {
1761
+ log.error(`Invalid provider: ${requestedProvider}. Use auto, claude, codex, or both.`);
1762
+ process.exit(1);
1763
+ }
1764
+ const provider = inferProvider(cwd, requestedProvider);
1765
+ const updateClaudeAssets = provider === "claude" || provider === "both";
1766
+ const skillDest = join6(SKILLS_HOME, "contract-driven-delivery");
1767
+ const agentDiff = updateClaudeAssets ? diffDir(ASSET.agents, AGENTS_HOME) : [];
1768
+ const skillDiff = updateClaudeAssets ? diffDir(ASSET.skill, skillDest) : [];
484
1769
  const toWrite = [...agentDiff, ...skillDiff].filter((e) => e.action !== "skip");
485
1770
  const toAdd = toWrite.filter((e) => e.action === "add");
486
1771
  const toOver = toWrite.filter((e) => e.action === "overwrite");
487
1772
  const toSkip = [...agentDiff, ...skillDiff].filter((e) => e.action === "skip");
488
- log.info(`Dry-run diff \u2014 agents: ${AGENTS_HOME}`);
489
- log.info(`Dry-run diff \u2014 skill: ${skillDest}`);
1773
+ log.info(`Provider: ${provider}`);
1774
+ if (updateClaudeAssets) {
1775
+ log.info(`Dry-run diff \u2014 agents: ${AGENTS_HOME}`);
1776
+ log.info(`Dry-run diff \u2014 skill: ${skillDest}`);
1777
+ } else {
1778
+ log.info("Codex provider has no global cdd-kit assets to update.");
1779
+ log.info("Project files are preserved; run cdd-kit init --local-only --provider codex to add missing local guidance.");
1780
+ }
490
1781
  log.blank();
491
1782
  if (toAdd.length)
492
1783
  log.info(` + ${toAdd.length} file(s) would be added`);
@@ -508,19 +1799,21 @@ async function update(opts) {
508
1799
  return;
509
1800
  }
510
1801
  const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
511
- const backupRoot = join5(homedir2(), ".claude", ".cdd-kit-backup", timestamp);
1802
+ const backupRoot = join6(homedir2(), ".claude", ".cdd-kit-backup", timestamp);
512
1803
  log.blank();
513
1804
  log.info(`Backing up to ${backupRoot} \u2026`);
514
- backupDir(AGENTS_HOME, join5(backupRoot, "agents"));
515
- backupDir(skillDest, join5(backupRoot, "skill"));
1805
+ backupDir(AGENTS_HOME, join6(backupRoot, "agents"));
1806
+ backupDir(skillDest, join6(backupRoot, "skill"));
516
1807
  log.ok(`Backup complete: ${backupRoot}`);
517
1808
  log.blank();
518
- log.info(`Updating agents \u2192 ${AGENTS_HOME}`);
519
- const agentCount = applyDir(agentDiff);
520
- log.ok(`${agentCount} agent file(s) updated.`);
521
- log.info(`Updating skill \u2192 ${skillDest}`);
522
- const skillCount = applyDir(skillDiff);
523
- log.ok(`${skillCount} skill file(s) updated.`);
1809
+ if (updateClaudeAssets) {
1810
+ log.info(`Updating agents \u2192 ${AGENTS_HOME}`);
1811
+ const agentCount = applyDir(agentDiff);
1812
+ log.ok(`${agentCount} agent file(s) updated.`);
1813
+ log.info(`Updating skill \u2192 ${skillDest}`);
1814
+ const skillCount = applyDir(skillDiff);
1815
+ log.ok(`${skillCount} skill file(s) updated.`);
1816
+ }
524
1817
  log.blank();
525
1818
  log.info("Project files (contracts/, specs/, tests/, ci/) were not changed.");
526
1819
  log.ok("Update complete.");
@@ -529,14 +1822,17 @@ async function update(opts) {
529
1822
  }
530
1823
 
531
1824
  // src/commands/new-change.ts
532
- import { join as join6 } from "path";
533
- import { existsSync as existsSync5, readdirSync as readdirSync4 } from "fs";
1825
+ init_paths();
1826
+ import { join as join7 } from "path";
1827
+ import { existsSync as existsSync6, readFileSync as readFileSync5, readdirSync as readdirSync4, writeFileSync as writeFileSync2 } from "fs";
1828
+ init_logger();
534
1829
  var REQUIRED_TEMPLATES = [
535
1830
  "change-request.md",
536
1831
  "change-classification.md",
537
1832
  "test-plan.md",
538
1833
  "ci-gates.md",
539
- "tasks.md"
1834
+ "tasks.md",
1835
+ "context-manifest.md"
540
1836
  ];
541
1837
  function listOptional() {
542
1838
  try {
@@ -547,14 +1843,31 @@ function listOptional() {
547
1843
  }
548
1844
  }
549
1845
  var SAFE_NAME = /^[a-z0-9][a-z0-9_-]{0,63}$/i;
1846
+ function parseDependsOn(raw) {
1847
+ if (!raw)
1848
+ return [];
1849
+ return raw.split(",").map((item) => item.trim()).filter(Boolean);
1850
+ }
1851
+ function formatDependsOn(ids) {
1852
+ if (ids.length === 0)
1853
+ return "depends-on: []";
1854
+ return `depends-on: [${ids.join(", ")}]`;
1855
+ }
550
1856
  async function newChange(name, opts) {
551
1857
  if (!SAFE_NAME.test(name)) {
552
1858
  log.error(`Invalid change name: "${name}". Use letters, numbers, hyphens, or underscores (max 64 chars).`);
553
1859
  process.exit(1);
554
1860
  }
1861
+ const dependencies = parseDependsOn(opts.dependsOn);
1862
+ for (const dep of dependencies) {
1863
+ if (!SAFE_NAME.test(dep)) {
1864
+ log.error(`Invalid dependency name: "${dep}". Use letters, numbers, hyphens, or underscores (max 64 chars).`);
1865
+ process.exit(1);
1866
+ }
1867
+ }
555
1868
  const cwd = process.cwd();
556
- const changeDir = join6(cwd, "specs", "changes", name);
557
- if (existsSync5(changeDir)) {
1869
+ const changeDir = join7(cwd, "specs", "changes", name);
1870
+ if (existsSync6(changeDir)) {
558
1871
  if (opts.force) {
559
1872
  log.warn(`Forcing re-scaffold of existing change directory: ${changeDir}`);
560
1873
  log.warn("Existing files will NOT be deleted; only template files will be overwritten.");
@@ -570,9 +1883,9 @@ async function newChange(name, opts) {
570
1883
  const templates = opts.all ? [...REQUIRED_TEMPLATES, ...listOptional()] : [...REQUIRED_TEMPLATES];
571
1884
  let written = 0;
572
1885
  for (const tmpl of templates) {
573
- const src = join6(ASSET.specsTemplates, tmpl);
574
- const dest = join6(changeDir, tmpl);
575
- if (!existsSync5(src)) {
1886
+ const src = join7(ASSET.specsTemplates, tmpl);
1887
+ const dest = join7(changeDir, tmpl);
1888
+ if (!existsSync6(src)) {
576
1889
  log.warn(`Template not found, skipping: ${tmpl}`);
577
1890
  continue;
578
1891
  }
@@ -580,14 +1893,25 @@ async function newChange(name, opts) {
580
1893
  log.dim(tmpl);
581
1894
  written += 1;
582
1895
  }
1896
+ if (dependencies.length > 0) {
1897
+ const tasksPath = join7(changeDir, "tasks.md");
1898
+ if (existsSync6(tasksPath)) {
1899
+ const tasks = readFileSync5(tasksPath, "utf8");
1900
+ const nextTasks = tasks.replace(/^depends-on:\s*.*$/m, formatDependsOn(dependencies));
1901
+ writeFileSync2(tasksPath, nextTasks, "utf8");
1902
+ log.dim(`depends-on: ${dependencies.join(", ")}`);
1903
+ }
1904
+ }
583
1905
  log.blank();
584
1906
  log.ok(`${written} template(s) created in specs/changes/${name}`);
585
1907
  log.blank();
586
1908
  }
587
1909
 
588
1910
  // src/commands/validate.ts
589
- import { join as join7 } from "path";
590
- import { existsSync as existsSync6 } from "fs";
1911
+ init_paths();
1912
+ init_logger();
1913
+ import { join as join8 } from "path";
1914
+ import { existsSync as existsSync7 } from "fs";
591
1915
  import { spawnSync } from "child_process";
592
1916
  var VALIDATORS = [
593
1917
  {
@@ -620,15 +1944,15 @@ async function validate(opts) {
620
1944
  log.error(e instanceof Error ? e.message : String(e));
621
1945
  process.exit(1);
622
1946
  }
623
- const scriptsDir = join7(ASSET.skill, "scripts");
1947
+ const scriptsDir = join8(ASSET.skill, "scripts");
624
1948
  const runAll = !opts.contracts && !opts.env && !opts.ci && !opts.spec && !opts.versions;
625
1949
  log.blank();
626
1950
  let failed = false;
627
1951
  for (const v of VALIDATORS) {
628
1952
  if (!runAll && !opts[v.flag])
629
1953
  continue;
630
- const scriptPath = join7(scriptsDir, v.script);
631
- if (!existsSync6(scriptPath)) {
1954
+ const scriptPath = join8(scriptsDir, v.script);
1955
+ if (!existsSync7(scriptPath)) {
632
1956
  log.warn(`${v.label}: script not found, skipping (${v.script})`);
633
1957
  log.blank();
634
1958
  continue;
@@ -644,8 +1968,8 @@ async function validate(opts) {
644
1968
  log.blank();
645
1969
  if (v.chain) {
646
1970
  for (const chained of v.chain) {
647
- const chainedPath = join7(scriptsDir, chained.script);
648
- if (!existsSync6(chainedPath)) {
1971
+ const chainedPath = join8(scriptsDir, chained.script);
1972
+ if (!existsSync7(chainedPath)) {
649
1973
  log.warn(`${chained.label}: script not found, skipping (${chained.script})`);
650
1974
  log.blank();
651
1975
  continue;
@@ -672,53 +1996,306 @@ async function validate(opts) {
672
1996
  }
673
1997
 
674
1998
  // src/commands/gate.ts
675
- import { existsSync as existsSync7, readFileSync as readFileSync4, readdirSync as readdirSync5 } from "fs";
676
- import { join as join8 } from "path";
1999
+ init_logger();
2000
+ import { existsSync as existsSync8, readFileSync as readFileSync6, readdirSync as readdirSync5 } from "fs";
2001
+ import { join as join9 } from "path";
677
2002
  import { spawnSync as spawnSync2 } from "child_process";
678
2003
  var REQUIRED_FILES = [
679
2004
  "change-request.md",
680
2005
  "change-classification.md",
681
2006
  "test-plan.md",
682
2007
  "ci-gates.md",
683
- "tasks.md"
2008
+ "tasks.md",
2009
+ "context-manifest.md"
684
2010
  ];
685
2011
  var TIER_PATTERN = /\b(tier\s*[0-5]|low|medium|high|critical)\b/i;
2012
+ var MIN_CHARS = {
2013
+ "change-classification.md": 200,
2014
+ "test-plan.md": 200,
2015
+ "ci-gates.md": 150,
2016
+ "change-request.md": 100,
2017
+ "tasks.md": 100,
2018
+ "context-manifest.md": 50
2019
+ };
686
2020
  function meaningfulChars(text) {
687
2021
  return text.split("\n").map((l) => l.trim()).filter((l) => l).filter((l) => !l.startsWith("#")).filter((l) => !/^[|\s\-:]+$/.test(l)).filter((l) => !l.startsWith("<!--")).join("").length;
688
2022
  }
689
- async function gate(changeId) {
2023
+ function stripHtmlComments(text) {
2024
+ return text.replace(/<!--[\s\S]*?-->/g, "");
2025
+ }
2026
+ function pathMatches(relPath, patterns, currentChangeId) {
2027
+ const normalized = relPath.replace(/\\/g, "/").replace(/^\.\//, "");
2028
+ return patterns.some((rawPattern) => {
2029
+ const pattern = rawPattern.replace(/\\/g, "/").replace(/^\.\//, "");
2030
+ if (pattern === "specs/changes/*" && currentChangeId) {
2031
+ const current = `specs/changes/${currentChangeId}`;
2032
+ if (normalized === current || normalized.startsWith(`${current}/`))
2033
+ return false;
2034
+ return normalized.startsWith("specs/changes/");
2035
+ }
2036
+ if (pattern.endsWith("/**")) {
2037
+ const base = pattern.slice(0, -3);
2038
+ return normalized === base || normalized.startsWith(`${base}/`);
2039
+ }
2040
+ if (pattern.endsWith("/*")) {
2041
+ const base = pattern.slice(0, -2);
2042
+ if (!normalized.startsWith(`${base}/`))
2043
+ return false;
2044
+ return !normalized.slice(base.length + 1).includes("/");
2045
+ }
2046
+ return normalized === pattern || normalized.startsWith(`${pattern}/`);
2047
+ });
2048
+ }
2049
+ function parseListSection(content, heading) {
2050
+ const clean = stripHtmlComments(content);
2051
+ const match = clean.match(new RegExp(`## ${heading}\\s*\\n([\\s\\S]*?)(?:\\n## |$)`));
2052
+ if (!match)
2053
+ return [];
2054
+ return match[1].split(/\r?\n/).map((line) => line.replace(/^\s*-\s*/, "").trim()).filter((item) => item && item !== "-" && item.toLowerCase() !== "none");
2055
+ }
2056
+ function parseContextManifest(content) {
2057
+ const clean = stripHtmlComments(content);
2058
+ const requestMatch = clean.match(/## Context Expansion Requests\s*\n([\s\S]*?)(?:\n## |$)/);
2059
+ const pendingExpansions = requestMatch ? (requestMatch[1].match(/^\s*-\s*status:\s*pending\b/gim) || []).length : 0;
2060
+ return {
2061
+ allowedPaths: parseListSection(content, "Allowed Paths"),
2062
+ approvedExpansions: parseListSection(content, "Approved Expansions"),
2063
+ pendingExpansions
2064
+ };
2065
+ }
2066
+ function loadContextPolicy(cwd) {
2067
+ const defaults = {
2068
+ forbiddenPaths: [
2069
+ ".claude/worktrees/**",
2070
+ ".git/**",
2071
+ "node_modules/**",
2072
+ "dist/**",
2073
+ "build/**",
2074
+ "assets/**",
2075
+ "specs/archive/**",
2076
+ "specs/changes/*"
2077
+ ],
2078
+ audit: {
2079
+ requireFilesRead: true,
2080
+ unknownFilesRead: "warn-for-legacy-fail-for-new"
2081
+ }
2082
+ };
2083
+ const policyPath = join9(cwd, ".cdd", "context-policy.json");
2084
+ if (!existsSync8(policyPath))
2085
+ return defaults;
2086
+ try {
2087
+ const custom = JSON.parse(readFileSync6(policyPath, "utf8"));
2088
+ return {
2089
+ ...defaults,
2090
+ ...custom,
2091
+ forbiddenPaths: Array.from(/* @__PURE__ */ new Set([...defaults.forbiddenPaths, ...custom.forbiddenPaths ?? []])),
2092
+ audit: { ...defaults.audit, ...custom.audit ?? {} }
2093
+ };
2094
+ } catch {
2095
+ log.warn("could not parse .cdd/context-policy.json; using default context policy");
2096
+ return defaults;
2097
+ }
2098
+ }
2099
+ function isContextGovernedChange(changeDir) {
2100
+ const tasksPath = join9(changeDir, "tasks.md");
2101
+ if (!existsSync8(tasksPath))
2102
+ return false;
2103
+ return /^context-governance:\s*v1\b/m.test(readFileSync6(tasksPath, "utf8"));
2104
+ }
2105
+ function parseDependsOn2(content) {
2106
+ const lineMatch = content.match(/^depends-on:\s*(.+)$/m);
2107
+ if (!lineMatch)
2108
+ return [];
2109
+ const raw = lineMatch[1].trim();
2110
+ if (!raw || raw === "[]")
2111
+ return [];
2112
+ if (raw.startsWith("[") && raw.endsWith("]")) {
2113
+ return raw.slice(1, -1).split(",").map((item) => item.trim()).filter(Boolean);
2114
+ }
2115
+ return raw.split(",").map((item) => item.trim()).filter(Boolean);
2116
+ }
2117
+ function parseTaskStatus(content) {
2118
+ const match = content.match(/^status:\s*([a-zA-Z0-9_-]+)/m);
2119
+ return match ? match[1].trim().toLowerCase() : "in-progress";
2120
+ }
2121
+ function isArchivedChange(cwd, changeId) {
2122
+ const archiveRoot = join9(cwd, "specs", "archive");
2123
+ if (!existsSync8(archiveRoot))
2124
+ return false;
2125
+ const years = readdirSync5(archiveRoot, { withFileTypes: true }).filter((d) => d.isDirectory());
2126
+ return years.some((year) => existsSync8(join9(archiveRoot, year.name, changeId)));
2127
+ }
2128
+ function validateDependencies(cwd, changeId, changeDir) {
2129
+ const tasksPath = join9(changeDir, "tasks.md");
2130
+ if (!existsSync8(tasksPath))
2131
+ return [];
2132
+ const dependencies = parseDependsOn2(readFileSync6(tasksPath, "utf8"));
2133
+ const errors = [];
2134
+ for (const dep of dependencies) {
2135
+ if (dep === changeId) {
2136
+ errors.push(`tasks.md: change cannot depend on itself (${dep})`);
2137
+ continue;
2138
+ }
2139
+ const upstreamDir = join9(cwd, "specs", "changes", dep);
2140
+ if (existsSync8(upstreamDir)) {
2141
+ const upstreamTasks = join9(upstreamDir, "tasks.md");
2142
+ if (!existsSync8(upstreamTasks)) {
2143
+ errors.push(`dependency ${dep}: missing tasks.md`);
2144
+ continue;
2145
+ }
2146
+ const status = parseTaskStatus(readFileSync6(upstreamTasks, "utf8"));
2147
+ if (!["complete", "completed", "done"].includes(status)) {
2148
+ errors.push(`dependency ${dep}: upstream change is not completed (status: ${status})`);
2149
+ }
2150
+ continue;
2151
+ }
2152
+ if (!isArchivedChange(cwd, dep)) {
2153
+ errors.push(`dependency ${dep}: upstream change not found in specs/changes/ or specs/archive/`);
2154
+ }
2155
+ }
2156
+ return errors;
2157
+ }
2158
+ function parseFilesRead(content) {
2159
+ const clean = stripHtmlComments(content);
2160
+ const allLines = clean.split(/\r?\n/);
2161
+ const startIndex = allLines.findIndex((line) => /^\s*-\s*files-read:\s*$/.test(line));
2162
+ if (startIndex === -1)
2163
+ return { present: false, files: [], errors: [] };
2164
+ const files = [];
2165
+ const errors = [];
2166
+ const lines = [];
2167
+ for (let i = startIndex + 1; i < allLines.length; i++) {
2168
+ const line = allLines[i];
2169
+ if (/^-\s*[a-zA-Z][\w-]*:\s*/.test(line) || /^#/.test(line))
2170
+ break;
2171
+ lines.push(line);
2172
+ }
2173
+ for (const rawLine of lines) {
2174
+ if (!rawLine.trim())
2175
+ continue;
2176
+ const itemMatch = rawLine.match(/^\s{2,}-\s+(.+?)\s*$/);
2177
+ if (!itemMatch) {
2178
+ errors.push(`invalid files-read entry format: ${rawLine.trim()}`);
2179
+ continue;
2180
+ }
2181
+ const item = itemMatch[1].trim();
2182
+ if (!item || item === "-" || item.toLowerCase() === "none" || item.toLowerCase() === "unknown") {
2183
+ continue;
2184
+ }
2185
+ const normalized = item.replace(/\\/g, "/").replace(/^\.\//, "");
2186
+ if (/^[a-zA-Z]:\//.test(normalized) || normalized.startsWith("/")) {
2187
+ errors.push(`files-read path must be repo-relative: ${item}`);
2188
+ continue;
2189
+ }
2190
+ if (normalized.split("/").includes("..")) {
2191
+ errors.push(`files-read path must not contain "..": ${item}`);
2192
+ continue;
2193
+ }
2194
+ files.push(normalized);
2195
+ }
2196
+ if (files.length === 0 && errors.length === 0) {
2197
+ errors.push("files-read section must list repo-relative paths or omit the section for legacy changes");
2198
+ }
2199
+ return { present: true, files, errors };
2200
+ }
2201
+ async function gate(changeId, opts = {}) {
2202
+ const strict = opts.strict ?? false;
690
2203
  const cwd = process.cwd();
691
- const changeDir = join8(cwd, "specs", "changes", changeId);
692
- if (!existsSync7(changeDir)) {
2204
+ const changeDir = join9(cwd, "specs", "changes", changeId);
2205
+ if (!existsSync8(changeDir)) {
693
2206
  log.error(`change not found: ${changeId} (looked in ${changeDir})`);
694
2207
  process.exit(1);
695
2208
  }
696
2209
  const errors = [];
2210
+ const warnings = [];
2211
+ const contextPolicy = loadContextPolicy(cwd);
2212
+ const isNewChange = isContextGovernedChange(changeDir);
2213
+ const manifestPath = join9(changeDir, "context-manifest.md");
2214
+ const hasManifest = existsSync8(manifestPath);
2215
+ let allowedPaths = [];
2216
+ let approvedExpansions = [];
2217
+ errors.push(...validateDependencies(cwd, changeId, changeDir));
2218
+ if (hasManifest) {
2219
+ const manifest = parseContextManifest(readFileSync6(manifestPath, "utf8"));
2220
+ allowedPaths = manifest.allowedPaths;
2221
+ approvedExpansions = manifest.approvedExpansions;
2222
+ if (manifest.pendingExpansions > 0) {
2223
+ errors.push(`context-manifest.md: has ${manifest.pendingExpansions} pending context expansion request(s)`);
2224
+ }
2225
+ }
697
2226
  for (const f of REQUIRED_FILES) {
698
- if (!existsSync7(join8(changeDir, f))) {
2227
+ if (f === "context-manifest.md") {
2228
+ if (!hasManifest) {
2229
+ if (isNewChange || strict) {
2230
+ errors.push("missing required artifact: context-manifest.md");
2231
+ } else {
2232
+ warnings.push("missing context-manifest.md (legacy change; run cdd-kit migrate after upgrading)");
2233
+ }
2234
+ }
2235
+ continue;
2236
+ }
2237
+ if (!existsSync8(join9(changeDir, f))) {
699
2238
  errors.push(`missing required artifact: ${f}`);
700
2239
  }
701
2240
  }
702
2241
  if (errors.length === 0) {
703
2242
  for (const f of REQUIRED_FILES) {
704
- const content = readFileSync4(join8(changeDir, f), "utf8");
705
- if (meaningfulChars(content) < 100) {
706
- errors.push(`${f}: appears to be a stub (< 100 meaningful chars)`);
2243
+ if (f === "context-manifest.md" && !hasManifest)
2244
+ continue;
2245
+ const content = readFileSync6(join9(changeDir, f), "utf8");
2246
+ const minChars = MIN_CHARS[f] ?? 100;
2247
+ if (meaningfulChars(content) < minChars) {
2248
+ errors.push(`${f}: appears to be a stub (< ${minChars} meaningful chars)`);
707
2249
  }
708
2250
  }
709
- const classifPath = join8(changeDir, "change-classification.md");
710
- if (existsSync7(classifPath)) {
711
- const text = readFileSync4(classifPath, "utf8");
2251
+ const classifPath = join9(changeDir, "change-classification.md");
2252
+ if (existsSync8(classifPath)) {
2253
+ const text = readFileSync6(classifPath, "utf8");
712
2254
  if (!TIER_PATTERN.test(text)) {
713
2255
  errors.push("change-classification.md: missing tier/risk marker (Tier 0-5 or low/medium/high/critical)");
714
2256
  }
715
2257
  }
716
2258
  }
717
- const agentLogDir = join8(changeDir, "agent-log");
718
- if (existsSync7(agentLogDir)) {
2259
+ const tasksPath = join9(changeDir, "tasks.md");
2260
+ if (existsSync8(tasksPath)) {
2261
+ const tasksContent = readFileSync6(tasksPath, "utf8");
2262
+ const nonArchivePending = (tasksContent.match(/^\s*-\s*\[ \] (?!7\.[12])/gm) || []).length;
2263
+ if (nonArchivePending > 0) {
2264
+ if (strict) {
2265
+ errors.push(`${nonArchivePending} task(s) still pending (use [-] for N/A items, [x] for done). Run gate without --strict during development.`);
2266
+ } else {
2267
+ warnings.push(`${nonArchivePending} task(s) still pending (warning only in non-strict mode)`);
2268
+ }
2269
+ }
2270
+ }
2271
+ const agentLogDir = join9(changeDir, "agent-log");
2272
+ if (existsSync8(agentLogDir)) {
719
2273
  const logFiles = readdirSync5(agentLogDir).filter((f) => f.endsWith(".md"));
720
2274
  for (const f of logFiles) {
721
- const content = readFileSync4(join8(agentLogDir, f), "utf8");
2275
+ const content = readFileSync6(join9(agentLogDir, f), "utf8");
2276
+ const filesRead = parseFilesRead(content);
2277
+ if (!filesRead.present) {
2278
+ if (contextPolicy.audit.requireFilesRead) {
2279
+ const msg = `agent-log/${f}: missing "- files-read:" section`;
2280
+ if (isNewChange || strict || contextPolicy.audit.unknownFilesRead !== "warn-for-legacy-fail-for-new") {
2281
+ errors.push(msg);
2282
+ } else {
2283
+ warnings.push(`${msg} (legacy warning only)`);
2284
+ }
2285
+ }
2286
+ } else {
2287
+ for (const parseError of filesRead.errors) {
2288
+ errors.push(`agent-log/${f}: ${parseError}`);
2289
+ }
2290
+ for (const pathRead of filesRead.files) {
2291
+ if (pathMatches(pathRead, contextPolicy.forbiddenPaths, changeId)) {
2292
+ errors.push(`agent-log/${f}: read forbidden path -> ${pathRead}`);
2293
+ }
2294
+ if (hasManifest && allowedPaths.length > 0 && !pathMatches(pathRead, allowedPaths) && !pathMatches(pathRead, approvedExpansions)) {
2295
+ errors.push(`agent-log/${f}: read unauthorized path -> ${pathRead} (not in allowed paths or approved expansions)`);
2296
+ }
2297
+ }
2298
+ }
722
2299
  const statusMatch = content.match(/^\s*-\s*status:\s*(complete|needs-review|blocked)\s*$/m);
723
2300
  if (!statusMatch) {
724
2301
  errors.push(`agent-log/${f}: missing or invalid "status:" line (must be complete | needs-review | blocked)`);
@@ -731,7 +2308,68 @@ async function gate(changeId) {
731
2308
  errors.push(`agent-log/${f}: status=blocked requires concrete "next-action:" line (>= 10 chars, not "none")`);
732
2309
  }
733
2310
  }
2311
+ if (strict) {
2312
+ const artifactsMatch = content.match(/- artifacts:([\s\S]*?)(?:\n- |\n#|$)/);
2313
+ if (artifactsMatch) {
2314
+ const artifactLines = artifactsMatch[1].split("\n").filter((l) => l.trim().startsWith("-"));
2315
+ for (const line of artifactLines) {
2316
+ const pointer = line.replace(/^\s*-\s*[\w-]+:\s*/, "").trim();
2317
+ const pathPart = pointer.split(":")[0];
2318
+ if (pathPart.includes("/") && !pointer.startsWith("http")) {
2319
+ const abs = join9(cwd, pathPart);
2320
+ if (!existsSync8(abs)) {
2321
+ errors.push(`agent-log/${f}: artifact pointer not found: ${pathPart}`);
2322
+ }
2323
+ }
2324
+ }
2325
+ }
2326
+ }
734
2327
  }
2328
+ const classifPath = join9(changeDir, "change-classification.md");
2329
+ if (existsSync8(classifPath)) {
2330
+ const classificationContent = readFileSync6(classifPath, "utf8");
2331
+ const tierMatch = classificationContent.match(/^## Tier\s*\n\s*-\s*(\d)\s*$/m);
2332
+ const tier = tierMatch ? parseInt(tierMatch[1]) : null;
2333
+ if (tier !== null) {
2334
+ const agentLogFiles = readdirSync5(agentLogDir).map((f) => f.replace(".md", ""));
2335
+ if (tier <= 1) {
2336
+ for (const required of ["e2e-resilience-engineer", "monkey-test-engineer", "stress-soak-engineer"]) {
2337
+ if (!agentLogFiles.includes(required)) {
2338
+ errors.push(`Tier ${tier} change requires agent-log/${required}.md (high-risk change \u2014 E2E/monkey/stress testing mandatory)`);
2339
+ }
2340
+ }
2341
+ }
2342
+ if (tier <= 3) {
2343
+ for (const required of ["contract-reviewer", "qa-reviewer"]) {
2344
+ if (!agentLogFiles.includes(required)) {
2345
+ errors.push(`Tier ${tier} change requires agent-log/${required}.md`);
2346
+ }
2347
+ }
2348
+ }
2349
+ }
2350
+ }
2351
+ } else {
2352
+ const classifPath = join9(changeDir, "change-classification.md");
2353
+ if (existsSync8(classifPath)) {
2354
+ const classificationContent = readFileSync6(classifPath, "utf8");
2355
+ const tierMatch = classificationContent.match(/^## Tier\s*\n\s*-\s*(\d)\s*$/m);
2356
+ const tier = tierMatch ? parseInt(tierMatch[1]) : null;
2357
+ if (tier !== null) {
2358
+ if (tier <= 1) {
2359
+ for (const required of ["e2e-resilience-engineer", "monkey-test-engineer", "stress-soak-engineer"]) {
2360
+ errors.push(`Tier ${tier} change requires agent-log/${required}.md (high-risk change \u2014 E2E/monkey/stress testing mandatory)`);
2361
+ }
2362
+ }
2363
+ if (tier <= 3) {
2364
+ for (const required of ["contract-reviewer", "qa-reviewer"]) {
2365
+ errors.push(`Tier ${tier} change requires agent-log/${required}.md`);
2366
+ }
2367
+ }
2368
+ }
2369
+ }
2370
+ }
2371
+ for (const w of warnings) {
2372
+ log.warn(` ${w}`);
735
2373
  }
736
2374
  if (errors.length > 0) {
737
2375
  log.error(`gate failed for change: ${changeId}`);
@@ -741,7 +2379,7 @@ async function gate(changeId) {
741
2379
  process.exit(1);
742
2380
  }
743
2381
  log.info(`gate: running contract validators for ${changeId}\u2026`);
744
- const r = spawnSync2(process.execPath, [process.argv[1], "validate"], {
2382
+ const r = spawnSync2(process.execPath, [process.argv[1], "validate", "--contracts", "--env", "--ci", "--versions"], {
745
2383
  cwd,
746
2384
  stdio: "inherit"
747
2385
  });
@@ -749,30 +2387,35 @@ async function gate(changeId) {
749
2387
  log.error(`gate failed for change: ${changeId} (validators returned non-zero)`);
750
2388
  process.exit(1);
751
2389
  }
2390
+ for (const w of warnings) {
2391
+ log.warn(` ${w}`);
2392
+ }
752
2393
  log.ok(`gate passed for change: ${changeId}`);
753
2394
  }
754
2395
 
755
2396
  // src/commands/install-hooks.ts
756
- import { existsSync as existsSync8, readFileSync as readFileSync5, writeFileSync as writeFileSync2, chmodSync, mkdirSync as mkdirSync3 } from "fs";
757
- import { join as join9 } from "path";
2397
+ init_paths();
2398
+ init_logger();
2399
+ import { existsSync as existsSync9, readFileSync as readFileSync7, writeFileSync as writeFileSync3, chmodSync, mkdirSync as mkdirSync3 } from "fs";
2400
+ import { join as join10 } from "path";
758
2401
  var START_MARKER = "# cdd-kit-managed-block-start";
759
2402
  var END_MARKER = "# cdd-kit-managed-block-end";
760
2403
  async function installHooks() {
761
2404
  const cwd = process.cwd();
762
- const gitDir = join9(cwd, ".git");
763
- if (!existsSync8(gitDir)) {
2405
+ const gitDir = join10(cwd, ".git");
2406
+ if (!existsSync9(gitDir)) {
764
2407
  log.error("not a git repository (no .git/ found in cwd)");
765
2408
  process.exit(1);
766
2409
  }
767
- const hooksDir = join9(gitDir, "hooks");
2410
+ const hooksDir = join10(gitDir, "hooks");
768
2411
  mkdirSync3(hooksDir, { recursive: true });
769
- const dest = join9(hooksDir, "pre-commit");
770
- const ourHook = readFileSync5(join9(ASSET.hooks, "pre-commit"), "utf8");
2412
+ const dest = join10(hooksDir, "pre-commit");
2413
+ const ourHook = readFileSync7(join10(ASSET.hooks, "pre-commit"), "utf8");
771
2414
  let final;
772
- if (!existsSync8(dest)) {
2415
+ if (!existsSync9(dest)) {
773
2416
  final = ourHook;
774
2417
  } else {
775
- const existing = readFileSync5(dest, "utf8");
2418
+ const existing = readFileSync7(dest, "utf8");
776
2419
  const startIdx = existing.indexOf(START_MARKER);
777
2420
  const endIdx = existing.indexOf(END_MARKER);
778
2421
  if (startIdx >= 0 && endIdx > startIdx) {
@@ -796,7 +2439,7 @@ async function installHooks() {
796
2439
  }
797
2440
  }
798
2441
  }
799
- writeFileSync2(dest, final, "utf8");
2442
+ writeFileSync3(dest, final, "utf8");
800
2443
  try {
801
2444
  chmodSync(dest, 493);
802
2445
  } catch {
@@ -806,22 +2449,36 @@ async function installHooks() {
806
2449
  }
807
2450
 
808
2451
  // src/cli/index.ts
809
- var __dirname2 = dirname3(fileURLToPath2(import.meta.url));
810
- var pkg = JSON.parse(readFileSync6(join10(__dirname2, "..", "..", "package.json"), "utf8"));
2452
+ var __dirname2 = dirname5(fileURLToPath2(import.meta.url));
2453
+ var pkg = JSON.parse(readFileSync15(join19(__dirname2, "..", "..", "package.json"), "utf8"));
811
2454
  var program = new Command();
812
2455
  program.name("cdd-kit").description("Contract-Driven Delivery Kit CLI").version(pkg.version);
813
2456
  program.command("init").description(
814
2457
  "Install agents/skill into ~/.claude and scaffold project files in cwd"
815
- ).option("--global-only", "Only install into ~/.claude, skip project files", false).option("--local-only", "Only scaffold project files, skip ~/.claude", false).option("--force", "Overwrite existing project files", false).action(
2458
+ ).option("--global-only", "Only install into ~/.claude, skip project files", false).option("--local-only", "Only scaffold project files, skip ~/.claude", false).option("--force", "Overwrite existing project files", false).option("--provider <provider>", "Provider adapter to scaffold: claude, codex, or both", "claude").action(
816
2459
  (opts) => init({
817
2460
  globalOnly: opts.globalOnly,
818
2461
  localOnly: opts.localOnly,
819
- force: opts.force
2462
+ force: opts.force,
2463
+ provider: opts.provider
820
2464
  })
821
2465
  );
822
- program.command("update").description("Update ~/.claude agents and skill (does not touch project files)").option("--yes", "Apply changes (default is dry-run)", false).action((opts) => update({ yes: opts.yes }));
823
- program.command("new <name>").description("Scaffold a new change directory under specs/changes/<name>").option("--all", "Include optional templates in addition to required ones", false).option("--force", "Overwrite existing template files in the change folder", false).action(
824
- (name, opts) => newChange(name, { all: opts.all, force: opts.force })
2466
+ 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").action((opts) => update({ yes: opts.yes, provider: opts.provider }));
2467
+ 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").action(async (opts) => {
2468
+ const { doctor: doctor2 } = await Promise.resolve().then(() => (init_doctor(), doctor_exports));
2469
+ await doctor2({ strict: opts.strict, json: opts.json, provider: opts.provider });
2470
+ });
2471
+ program.command("upgrade").description("Add missing cdd-kit repo-level files without overwriting existing project files").option("--yes", "Apply changes (default is dry-run)", false).option("--migrate-changes", "Also migrate existing specs/changes/* directories", false).option("--enable-context-governance", "When migrating changes, opt them into context-governance: v1", false).option("--provider <provider>", "Provider adapter to scaffold: auto, claude, codex, or both", "auto").action(async (opts) => {
2472
+ const { upgrade: upgrade2 } = await Promise.resolve().then(() => (init_upgrade(), upgrade_exports));
2473
+ await upgrade2({
2474
+ yes: opts.yes,
2475
+ migrateChanges: opts.migrateChanges,
2476
+ enableContextGovernance: opts.enableContextGovernance,
2477
+ provider: opts.provider
2478
+ });
2479
+ });
2480
+ program.command("new <name>").description("Scaffold a new change directory under specs/changes/<name>").option("--all", "Include optional templates in addition to required ones", false).option("--force", "Overwrite existing template files in the change folder", false).option("--depends-on <change-ids>", "Comma-separated upstream change ids that must complete first").action(
2481
+ (name, opts) => newChange(name, { all: opts.all, force: opts.force, dependsOn: opts.dependsOn })
825
2482
  );
826
2483
  program.command("validate").description("Run validation scripts (defaults to all)").option("--contracts", "Validate API/data/CSS contracts (use --env separately for env)", false).option("--env", "Validate env contract", false).option("--ci", "Validate CI gate policy", false).option("--spec", "Validate spec traceability", false).option("--versions", "Validate contract frontmatter and version bumps", false).action(
827
2484
  (opts) => validate({
@@ -832,8 +2489,24 @@ program.command("validate").description("Run validation scripts (defaults to all
832
2489
  versions: opts.versions
833
2490
  })
834
2491
  );
835
- program.command("gate <change-id>").description("Run full orchestration gate for a change (required artifacts, content, tier, contracts)").action(async (id) => {
836
- await gate(id);
2492
+ program.command("gate <change-id>").description("Run full orchestration gate for a change (required artifacts, content, tier, contracts)").option("--strict", "Treat pending tasks (except section 7) as errors, and validate artifact pointers", false).action(async (id, opts) => {
2493
+ await gate(id, { strict: opts.strict });
2494
+ });
2495
+ program.command("archive <change-id>").description("Move a completed change from specs/changes/ to specs/archive/<year>/").action(async (changeId) => {
2496
+ const { archive: archive2 } = await Promise.resolve().then(() => (init_archive(), archive_exports));
2497
+ await archive2(changeId);
2498
+ });
2499
+ program.command("abandon <change-id>").description("Mark a change as abandoned (updates tasks.md status, records in INDEX.md)").option("--reason <text>", "reason for abandonment").action(async (changeId, opts) => {
2500
+ const { abandon: abandon2 } = await Promise.resolve().then(() => (init_abandon(), abandon_exports));
2501
+ await abandon2(changeId, opts);
2502
+ });
2503
+ program.command("migrate [change-id]").description("Upgrade existing change directories to the current cdd-kit format (tasks.md frontmatter + tier format)").option("--all", "Migrate all changes in specs/changes/", false).option("--dry-run", "Show what would change without writing files", false).option("--enable-context-governance", "Opt legacy changes into context-governance: v1 hard gate behavior", false).action(async (changeId, opts = {}) => {
2504
+ const { migrate: migrate2 } = await Promise.resolve().then(() => (init_migrate(), migrate_exports));
2505
+ await migrate2(changeId, opts);
2506
+ });
2507
+ program.command("list").description("List active changes in specs/changes/").action(async () => {
2508
+ const { listChanges: listChanges2 } = await Promise.resolve().then(() => (init_list_changes(), list_changes_exports));
2509
+ await listChanges2();
837
2510
  });
838
2511
  program.command("install-hooks").description("Install pre-commit hook that runs cdd-kit gate on staged changes").action(async () => {
839
2512
  await installHooks();
@@ -851,4 +2524,25 @@ program.command("detect-stack").description("Detect the project tech stack and p
851
2524
  );
852
2525
  }
853
2526
  });
2527
+ program.command("context-scan").description("Deterministically scan project context and generate specs/context maps").action(async () => {
2528
+ const { contextScan: contextScan2 } = await Promise.resolve().then(() => (init_context_scan(), context_scan_exports));
2529
+ await contextScan2();
2530
+ });
2531
+ var context = program.command("context").description("Manage context governance manifests");
2532
+ context.command("request <change-id> <request-id>").description("Record a new pending Context Expansion Request").requiredOption("--path <paths...>", "Repo-relative path(s) requested by the agent").option("--reason <text>", "Reason the extra context is required").action(async (changeId, requestId, opts) => {
2533
+ const { requestContextExpansion: requestContextExpansion2 } = await Promise.resolve().then(() => (init_context(), context_exports));
2534
+ await requestContextExpansion2(changeId, requestId, opts.path, opts.reason);
2535
+ });
2536
+ context.command("approve <change-id> <request-id>").description("Approve a pending Context Expansion Request and add its paths to Approved Expansions").action(async (changeId, requestId) => {
2537
+ const { approveContextExpansion: approveContextExpansion2 } = await Promise.resolve().then(() => (init_context(), context_exports));
2538
+ await approveContextExpansion2(changeId, requestId);
2539
+ });
2540
+ context.command("reject <change-id> <request-id>").description("Reject a pending Context Expansion Request").action(async (changeId, requestId) => {
2541
+ const { rejectContextExpansion: rejectContextExpansion2 } = await Promise.resolve().then(() => (init_context(), context_exports));
2542
+ await rejectContextExpansion2(changeId, requestId);
2543
+ });
2544
+ context.command("list <change-id>").description("List Context Expansion Requests for a change").option("--json", "Print machine-readable JSON", false).action(async (changeId, opts) => {
2545
+ const { listContextExpansions: listContextExpansions2 } = await Promise.resolve().then(() => (init_context(), context_exports));
2546
+ await listContextExpansions2(changeId, opts.json);
2547
+ });
854
2548
  program.parse();