contract-driven-delivery 1.9.0 → 1.11.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 (32) hide show
  1. package/README.md +383 -143
  2. package/assets/CLAUDE.template.md +31 -204
  3. package/assets/agents/backend-engineer.md +19 -1
  4. package/assets/agents/change-classifier.md +44 -8
  5. package/assets/agents/ci-cd-gatekeeper.md +13 -0
  6. package/assets/agents/contract-reviewer.md +13 -0
  7. package/assets/agents/e2e-resilience-engineer.md +13 -0
  8. package/assets/agents/frontend-engineer.md +19 -1
  9. package/assets/agents/monkey-test-engineer.md +13 -0
  10. package/assets/agents/qa-reviewer.md +13 -0
  11. package/assets/agents/repo-context-scanner.md +3 -0
  12. package/assets/agents/spec-architect.md +41 -31
  13. package/assets/agents/spec-drift-auditor.md +21 -19
  14. package/assets/agents/stress-soak-engineer.md +13 -0
  15. package/assets/agents/test-strategist.md +36 -26
  16. package/assets/ci-templates/conda.yml +1 -1
  17. package/assets/{ci/github-actions → github-workflows}/contract-driven-gates.yml +12 -17
  18. package/assets/hooks/pre-commit +1 -1
  19. package/assets/skills/cdd-close/SKILL.md +123 -0
  20. package/assets/skills/cdd-init/SKILL.md +6 -0
  21. package/assets/skills/cdd-new/SKILL.md +108 -24
  22. package/assets/skills/cdd-resume/SKILL.md +86 -0
  23. package/assets/skills/contract-driven-delivery/templates/change-classification.md +18 -11
  24. package/assets/skills/contract-driven-delivery/templates/design.md +16 -13
  25. package/assets/skills/contract-driven-delivery/templates/tasks.md +7 -0
  26. package/assets/skills/contract-driven-delivery/templates/test-plan.md +17 -23
  27. package/assets/specs-templates/change-classification.md +18 -11
  28. package/assets/specs-templates/design.md +16 -13
  29. package/assets/specs-templates/tasks.md +7 -0
  30. package/assets/specs-templates/test-plan.md +17 -23
  31. package/dist/cli/index.js +508 -41
  32. package/package.json +8 -5
package/dist/cli/index.js CHANGED
@@ -1,7 +1,368 @@
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/logger.ts
12
+ var RESET, CYAN, GREEN, YELLOW, RED, DIM, log;
13
+ var init_logger = __esm({
14
+ "src/utils/logger.ts"() {
15
+ "use strict";
16
+ RESET = "\x1B[0m";
17
+ CYAN = "\x1B[36m";
18
+ GREEN = "\x1B[32m";
19
+ YELLOW = "\x1B[33m";
20
+ RED = "\x1B[31m";
21
+ DIM = "\x1B[2m";
22
+ log = {
23
+ info(msg) {
24
+ console.log(`${CYAN}\u2139${RESET} ${msg}`);
25
+ },
26
+ ok(msg) {
27
+ console.log(`${GREEN}\u2713${RESET} ${msg}`);
28
+ },
29
+ warn(msg) {
30
+ console.log(`${YELLOW}\u26A0${RESET} ${msg}`);
31
+ },
32
+ error(msg) {
33
+ console.error(`${RED}\u2717${RESET} ${msg}`);
34
+ },
35
+ dim(msg) {
36
+ console.log(`${DIM} ${msg}${RESET}`);
37
+ },
38
+ blank() {
39
+ console.log("");
40
+ }
41
+ };
42
+ }
43
+ });
44
+
45
+ // src/commands/archive.ts
46
+ var archive_exports = {};
47
+ __export(archive_exports, {
48
+ archive: () => archive
49
+ });
50
+ import { join as join10 } from "path";
51
+ import { existsSync as existsSync9, mkdirSync as mkdirSync4, renameSync, readFileSync as readFileSync6, writeFileSync as writeFileSync3, appendFileSync, cpSync as cpSync2, rmSync as rmSync2 } from "fs";
52
+ async function archive(changeId) {
53
+ const cwd = process.cwd();
54
+ const changeDir = join10(cwd, "specs", "changes", changeId);
55
+ const archiveYear = (/* @__PURE__ */ new Date()).getFullYear().toString();
56
+ const archiveBase = join10(cwd, "specs", "archive", archiveYear);
57
+ const archiveDir = join10(archiveBase, changeId);
58
+ const indexPath = join10(cwd, "specs", "archive", "INDEX.md");
59
+ if (!existsSync9(changeDir)) {
60
+ log.error(`Change not found: specs/changes/${changeId}`);
61
+ process.exit(1);
62
+ }
63
+ if (existsSync9(archiveDir)) {
64
+ log.error(`Already archived: specs/archive/${archiveYear}/${changeId}`);
65
+ process.exit(1);
66
+ }
67
+ const tasksPath = join10(changeDir, "tasks.md");
68
+ if (existsSync9(tasksPath)) {
69
+ const content = readFileSync6(tasksPath, "utf8");
70
+ if (content.includes("status: gate-blocked")) {
71
+ log.warn("tasks.md has status: gate-blocked \u2014 archiving anyway (change was paused).");
72
+ }
73
+ const pending = (content.match(/^\s*-\s*\[ \]/gm) || []).length;
74
+ if (pending > 0) {
75
+ log.warn(`${pending} task(s) still pending ([ ]). Archive anyway.`);
76
+ }
77
+ }
78
+ if (!existsSync9(archiveBase)) {
79
+ mkdirSync4(archiveBase, { recursive: true });
80
+ }
81
+ try {
82
+ renameSync(changeDir, archiveDir);
83
+ } catch (err) {
84
+ if (err.code === "EXDEV") {
85
+ cpSync2(changeDir, archiveDir, { recursive: true });
86
+ rmSync2(changeDir, { recursive: true, force: true });
87
+ } else {
88
+ throw err;
89
+ }
90
+ }
91
+ log.ok(`Archived: specs/changes/${changeId} \u2192 specs/archive/${archiveYear}/${changeId}`);
92
+ const today = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
93
+ const indexLine = `| ${changeId} | ${archiveYear} | ${today} | specs/archive/${archiveYear}/${changeId}/ |
94
+ `;
95
+ if (!existsSync9(indexPath)) {
96
+ writeFileSync3(indexPath, `# Archive Index
97
+
98
+ | change-id | year | archived-date | path |
99
+ |---|---|---|---|
100
+ ${indexLine}`, "utf8");
101
+ } else {
102
+ appendFileSync(indexPath, indexLine, "utf8");
103
+ }
104
+ log.ok(`Index updated: specs/archive/INDEX.md`);
105
+ log.blank();
106
+ log.info(`Next: promote durable learnings from archive.md to contracts/ or CLAUDE.md`);
107
+ }
108
+ var init_archive = __esm({
109
+ "src/commands/archive.ts"() {
110
+ "use strict";
111
+ init_logger();
112
+ }
113
+ });
114
+
115
+ // src/commands/abandon.ts
116
+ var abandon_exports = {};
117
+ __export(abandon_exports, {
118
+ abandon: () => abandon
119
+ });
120
+ import { join as join11 } from "path";
121
+ import { existsSync as existsSync10, readFileSync as readFileSync7, writeFileSync as writeFileSync4, appendFileSync as appendFileSync2, mkdirSync as mkdirSync5 } from "fs";
122
+ async function abandon(changeId, opts) {
123
+ const cwd = process.cwd();
124
+ const changeDir = join11(cwd, "specs", "changes", changeId);
125
+ const tasksPath = join11(changeDir, "tasks.md");
126
+ if (!existsSync10(changeDir)) {
127
+ log.error(`Change not found: specs/changes/${changeId}`);
128
+ process.exit(1);
129
+ }
130
+ if (existsSync10(tasksPath)) {
131
+ let content = readFileSync7(tasksPath, "utf8");
132
+ if (content.match(/^status:/m)) {
133
+ content = content.replace(/^status: .*/m, "status: abandoned");
134
+ } else {
135
+ content = `---
136
+ change-id: ${changeId}
137
+ status: abandoned
138
+ ---
139
+
140
+ ` + content;
141
+ }
142
+ writeFileSync4(tasksPath, content, "utf8");
143
+ }
144
+ const today = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
145
+ const archiveDir = join11(cwd, "specs", "archive");
146
+ const indexPath = join11(archiveDir, "INDEX.md");
147
+ const reason = opts.reason ?? "no reason given";
148
+ const indexLine = `| ${changeId} | abandoned | ${today} | ${reason} |
149
+ `;
150
+ if (!existsSync10(archiveDir)) {
151
+ mkdirSync5(archiveDir, { recursive: true });
152
+ }
153
+ if (!existsSync10(indexPath)) {
154
+ writeFileSync4(indexPath, `# Archive Index
155
+
156
+ | change-id | status | date | notes |
157
+ |---|---|---|---|
158
+ ${indexLine}`, "utf8");
159
+ } else {
160
+ appendFileSync2(indexPath, indexLine, "utf8");
161
+ }
162
+ log.ok(`Change ${changeId} marked as abandoned.`);
163
+ log.info(`specs/changes/${changeId}/ remains on disk (git history preserved).`);
164
+ log.info(`Run \`cdd-kit archive ${changeId}\` to physically move it, or leave it for git history.`);
165
+ }
166
+ var init_abandon = __esm({
167
+ "src/commands/abandon.ts"() {
168
+ "use strict";
169
+ init_logger();
170
+ }
171
+ });
172
+
173
+ // src/commands/migrate.ts
174
+ var migrate_exports = {};
175
+ __export(migrate_exports, {
176
+ migrate: () => migrate
177
+ });
178
+ import { join as join12 } from "path";
179
+ import { existsSync as existsSync11, readdirSync as readdirSync6, readFileSync as readFileSync8, writeFileSync as writeFileSync5 } from "fs";
180
+ function migrateOne(changeId, changeDir, dryRun) {
181
+ const changed = [];
182
+ const warnings = [];
183
+ const tasksPath = join12(changeDir, "tasks.md");
184
+ if (existsSync11(tasksPath)) {
185
+ let content = readFileSync8(tasksPath, "utf8");
186
+ const norm = content.replace(/\r\n/g, "\n");
187
+ let modified = false;
188
+ if (!norm.startsWith("---")) {
189
+ const bareStatusMatch = norm.match(/^status:\s*(\S+)/m);
190
+ const inferredStatus = bareStatusMatch ? bareStatusMatch[1] : "in-progress";
191
+ if (bareStatusMatch) {
192
+ content = content.replace(/^status:\s*\S+[ \t]*\n?/m, "");
193
+ }
194
+ content = `---
195
+ change-id: ${changeId}
196
+ status: ${inferredStatus}
197
+ ---
198
+
199
+ ` + content;
200
+ modified = true;
201
+ }
202
+ if (!content.includes("[x]=done")) {
203
+ content = content.replace(
204
+ /^(---\n[\s\S]*?---\n)/,
205
+ `$1
206
+ <!-- [x]=done [-]=N/A [ ]=pending -->
207
+ `
208
+ );
209
+ modified = true;
210
+ }
211
+ if (modified) {
212
+ changed.push("tasks.md: added YAML frontmatter (status: in-progress) + legend comment");
213
+ if (!dryRun)
214
+ writeFileSync5(tasksPath, content, "utf8");
215
+ }
216
+ } else {
217
+ warnings.push("tasks.md not found \u2014 skipping frontmatter migration");
218
+ }
219
+ const classifPath = join12(changeDir, "change-classification.md");
220
+ if (existsSync11(classifPath)) {
221
+ const content = readFileSync8(classifPath, "utf8");
222
+ const hasNewTierFormat = /^## Tier\s*\n\s*-\s*\d\s*$/m.test(content);
223
+ if (!hasNewTierFormat) {
224
+ const oldMatch = content.match(/\*\*Tier[:\*]+\s*(?:Tier\s*)?(\d)/i) ?? content.match(/^-?\s*Tier:\s*(?:Tier\s*)?(\d)/mi);
225
+ const detectedTier = oldMatch ? oldMatch[1] : null;
226
+ if (detectedTier) {
227
+ const addition = `
228
+ ## Tier
229
+ - ${detectedTier}
230
+ `;
231
+ if (!content.includes("\n## Tier\n")) {
232
+ changed.push(
233
+ `change-classification.md: appended "## Tier\\n- ${detectedTier}" (converted from old format)`
234
+ );
235
+ if (!dryRun)
236
+ writeFileSync5(classifPath, content + addition, "utf8");
237
+ }
238
+ } else {
239
+ warnings.push(
240
+ "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."
241
+ );
242
+ }
243
+ }
244
+ }
245
+ return { changed, warnings };
246
+ }
247
+ async function migrate(changeId, opts = {}) {
248
+ const cwd = process.cwd();
249
+ const dryRun = opts.dryRun ?? false;
250
+ const idsToMigrate = [];
251
+ if (opts.all) {
252
+ const changesDir = join12(cwd, "specs", "changes");
253
+ if (!existsSync11(changesDir)) {
254
+ log.info("No specs/changes/ directory found \u2014 nothing to migrate.");
255
+ return;
256
+ }
257
+ idsToMigrate.push(
258
+ ...readdirSync6(changesDir, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name)
259
+ );
260
+ } else if (changeId) {
261
+ const specificDir = join12(cwd, "specs", "changes", changeId);
262
+ if (!existsSync11(specificDir)) {
263
+ log.error(`Change not found: specs/changes/${changeId}`);
264
+ process.exit(1);
265
+ }
266
+ idsToMigrate.push(changeId);
267
+ } else {
268
+ log.error("Usage: cdd-kit migrate <change-id> | cdd-kit migrate --all [--dry-run]");
269
+ process.exit(1);
270
+ }
271
+ if (idsToMigrate.length === 0) {
272
+ log.info("No changes found to migrate.");
273
+ return;
274
+ }
275
+ if (dryRun) {
276
+ log.info("Dry run \u2014 no files will be written.");
277
+ log.blank();
278
+ }
279
+ let migratedCount = 0;
280
+ let upToDateCount = 0;
281
+ for (const id of idsToMigrate) {
282
+ const changeDir = join12(cwd, "specs", "changes", id);
283
+ if (!existsSync11(changeDir)) {
284
+ log.warn(` ${id}: directory not found \u2014 skipping`);
285
+ continue;
286
+ }
287
+ const { changed, warnings } = migrateOne(id, changeDir, dryRun);
288
+ if (changed.length > 0) {
289
+ log.ok(` ${id}: migrated`);
290
+ for (const c of changed)
291
+ log.info(` + ${c}`);
292
+ migratedCount++;
293
+ } else {
294
+ log.info(` ${id}: already up to date`);
295
+ upToDateCount++;
296
+ }
297
+ for (const w of warnings) {
298
+ log.warn(` ${id}: ${w}`);
299
+ }
300
+ }
301
+ log.blank();
302
+ if (dryRun) {
303
+ log.info(`Dry run complete: ${migratedCount} change(s) would be updated, ${upToDateCount} already up to date.`);
304
+ } else {
305
+ log.ok(`Migration complete: ${migratedCount} updated, ${upToDateCount} already up to date.`);
306
+ if (migratedCount > 0) {
307
+ log.info('Next: git add specs/changes/ && git commit -m "chore: migrate changes to v1.11.0 format"');
308
+ }
309
+ }
310
+ }
311
+ var init_migrate = __esm({
312
+ "src/commands/migrate.ts"() {
313
+ "use strict";
314
+ init_logger();
315
+ }
316
+ });
317
+
318
+ // src/commands/list-changes.ts
319
+ var list_changes_exports = {};
320
+ __export(list_changes_exports, {
321
+ listChanges: () => listChanges
322
+ });
323
+ import { join as join13 } from "path";
324
+ import { existsSync as existsSync12, readdirSync as readdirSync7, readFileSync as readFileSync9 } from "fs";
325
+ async function listChanges() {
326
+ const cwd = process.cwd();
327
+ const changesDir = join13(cwd, "specs", "changes");
328
+ log.blank();
329
+ const active = [];
330
+ if (existsSync12(changesDir)) {
331
+ active.push(...readdirSync7(changesDir, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name));
332
+ }
333
+ if (active.length === 0) {
334
+ log.info("No active changes in specs/changes/");
335
+ } else {
336
+ log.info("Active changes:");
337
+ for (const id of active) {
338
+ const tasksPath = join13(changesDir, id, "tasks.md");
339
+ let status = "in-progress";
340
+ let pending = 0;
341
+ if (existsSync12(tasksPath)) {
342
+ const content = readFileSync9(tasksPath, "utf8");
343
+ if (content.includes("status: gate-blocked"))
344
+ status = "gate-blocked";
345
+ else if (content.includes("status: abandoned"))
346
+ status = "abandoned";
347
+ pending = (content.match(/^\s*-\s*\[ \]/gm) || []).length;
348
+ }
349
+ const pendingStr = pending > 0 ? ` (${pending} pending)` : "";
350
+ log.info(` ${id} [${status}]${pendingStr}`);
351
+ }
352
+ }
353
+ log.blank();
354
+ }
355
+ var init_list_changes = __esm({
356
+ "src/commands/list-changes.ts"() {
357
+ "use strict";
358
+ init_logger();
359
+ }
360
+ });
361
+
1
362
  // src/cli/index.ts
2
- import { readFileSync as readFileSync6 } from "fs";
363
+ import { readFileSync as readFileSync10 } from "fs";
3
364
  import { fileURLToPath as fileURLToPath2 } from "url";
4
- import { dirname as dirname3, join as join10 } from "path";
365
+ import { dirname as dirname3, join as join14 } from "path";
5
366
  import { Command } from "commander";
6
367
 
7
368
  // src/commands/init.ts
@@ -26,12 +387,14 @@ var ASSET = {
26
387
  specsTemplates: join(ASSETS_DIR, "specs-templates"),
27
388
  testsTemplates: join(ASSETS_DIR, "tests-templates"),
28
389
  ci: join(ASSETS_DIR, "ci"),
390
+ githubWorkflows: join(ASSETS_DIR, "github-workflows"),
29
391
  hooks: join(ASSETS_DIR, "hooks"),
30
392
  claudeTemplate: join(ASSETS_DIR, "CLAUDE.template.md"),
31
393
  agentsTemplate: join(ASSETS_DIR, "AGENTS.template.md")
32
394
  };
33
395
 
34
396
  // src/utils/copy.ts
397
+ init_logger();
35
398
  import {
36
399
  mkdirSync,
37
400
  existsSync,
@@ -39,36 +402,6 @@ import {
39
402
  copyFileSync
40
403
  } from "fs";
41
404
  import { join as join2, dirname as dirname2, relative } from "path";
42
-
43
- // src/utils/logger.ts
44
- var RESET = "\x1B[0m";
45
- var CYAN = "\x1B[36m";
46
- var GREEN = "\x1B[32m";
47
- var YELLOW = "\x1B[33m";
48
- var RED = "\x1B[31m";
49
- var DIM = "\x1B[2m";
50
- var log = {
51
- info(msg) {
52
- console.log(`${CYAN}\u2139${RESET} ${msg}`);
53
- },
54
- ok(msg) {
55
- console.log(`${GREEN}\u2713${RESET} ${msg}`);
56
- },
57
- warn(msg) {
58
- console.log(`${YELLOW}\u26A0${RESET} ${msg}`);
59
- },
60
- error(msg) {
61
- console.error(`${RED}\u2717${RESET} ${msg}`);
62
- },
63
- dim(msg) {
64
- console.log(`${DIM} ${msg}${RESET}`);
65
- },
66
- blank() {
67
- console.log("");
68
- }
69
- };
70
-
71
- // src/utils/copy.ts
72
405
  function ensureDir(dir) {
73
406
  mkdirSync(dir, { recursive: true });
74
407
  }
@@ -140,6 +473,9 @@ function copyFileTracked(src, dest, opts = {}) {
140
473
  return { written: true, created: isNew };
141
474
  }
142
475
 
476
+ // src/commands/init.ts
477
+ init_logger();
478
+
143
479
  // src/utils/stack-detect.ts
144
480
  import { existsSync as existsSync2, readFileSync } from "fs";
145
481
  import { join as join3 } from "path";
@@ -224,6 +560,18 @@ function detectStack(repoRoot) {
224
560
  }
225
561
 
226
562
  // src/commands/init.ts
563
+ function readCondaEnvName(cwd) {
564
+ const envYml = join4(cwd, "environment.yml");
565
+ if (!existsSync3(envYml))
566
+ return "base";
567
+ try {
568
+ const content = readFileSync2(envYml, "utf8");
569
+ const match = content.match(/^name:\s*(.+)$/m);
570
+ return match ? match[1].trim() : "base";
571
+ } catch {
572
+ return "base";
573
+ }
574
+ }
227
575
  function loadCiTemplate(stack) {
228
576
  const templatePath = join4(ASSETS_DIR, "ci-templates", `${stack}.yml`);
229
577
  if (!existsSync3(templatePath))
@@ -326,6 +674,13 @@ async function init(opts) {
326
674
  );
327
675
  track(ciCreated);
328
676
  log.ok(`ci/ \u2014 ${ciCount} file(s) written.`);
677
+ const { count: wfCount, created: wfCreated } = copyDirTracked(
678
+ ASSET.githubWorkflows,
679
+ join4(cwd, ".github", "workflows"),
680
+ { overwrite: opts.force, label: ".github/workflows" }
681
+ );
682
+ track(wfCreated);
683
+ log.ok(`.github/workflows/ \u2014 ${wfCount} file(s) written.`);
329
684
  const detection = detectStack(cwd);
330
685
  if (detection.polyglot) {
331
686
  const PYTHON_STACKS = ["conda", "poetry", "uv", "pip"];
@@ -345,12 +700,17 @@ async function init(opts) {
345
700
  } else {
346
701
  log.warn("Could not detect stack \u2014 CI placeholder left in place.");
347
702
  }
348
- const ciYmlDest = join4(cwd, "ci", "github-actions", "contract-driven-gates.yml");
703
+ const ciYmlDest = join4(cwd, ".github", "workflows", "contract-driven-gates.yml");
349
704
  if (existsSync3(ciYmlDest)) {
350
705
  const template = loadCiTemplate(detection.primary);
351
706
  if (template) {
352
707
  const original = readFileSync2(ciYmlDest, "utf8");
353
- const patched = patchFastGateYml(original, template, detection.primary);
708
+ let patched = patchFastGateYml(original, template, detection.primary);
709
+ if (detection.primary === "conda" && patched.includes("{{conda-env-name}}")) {
710
+ const envName = readCondaEnvName(cwd);
711
+ patched = patched.replace(/\{\{conda-env-name\}\}/g, envName);
712
+ log.ok(`Conda environment name set to: ${envName}`);
713
+ }
354
714
  if (patched !== original) {
355
715
  writeFileSync(ciYmlDest, patched, "utf8");
356
716
  log.ok(`CI fast-gate patched for stack: ${detection.primary}`);
@@ -392,6 +752,7 @@ async function init(opts) {
392
752
  import { join as join5 } from "path";
393
753
  import { existsSync as existsSync4, mkdirSync as mkdirSync2, readdirSync as readdirSync3, copyFileSync as copyFileSync2, readFileSync as readFileSync3 } from "fs";
394
754
  import { createHash } from "crypto";
755
+ init_logger();
395
756
  import { homedir as homedir2 } from "os";
396
757
  function fileHash(filePath) {
397
758
  const buf = readFileSync3(filePath);
@@ -506,6 +867,7 @@ async function update(opts) {
506
867
  // src/commands/new-change.ts
507
868
  import { join as join6 } from "path";
508
869
  import { existsSync as existsSync5, readdirSync as readdirSync4 } from "fs";
870
+ init_logger();
509
871
  var REQUIRED_TEMPLATES = [
510
872
  "change-request.md",
511
873
  "change-classification.md",
@@ -564,6 +926,7 @@ async function newChange(name, opts) {
564
926
  import { join as join7 } from "path";
565
927
  import { existsSync as existsSync6 } from "fs";
566
928
  import { spawnSync } from "child_process";
929
+ init_logger();
567
930
  var VALIDATORS = [
568
931
  {
569
932
  flag: "contracts",
@@ -647,6 +1010,7 @@ async function validate(opts) {
647
1010
  }
648
1011
 
649
1012
  // src/commands/gate.ts
1013
+ init_logger();
650
1014
  import { existsSync as existsSync7, readFileSync as readFileSync4, readdirSync as readdirSync5 } from "fs";
651
1015
  import { join as join8 } from "path";
652
1016
  import { spawnSync as spawnSync2 } from "child_process";
@@ -658,10 +1022,18 @@ var REQUIRED_FILES = [
658
1022
  "tasks.md"
659
1023
  ];
660
1024
  var TIER_PATTERN = /\b(tier\s*[0-5]|low|medium|high|critical)\b/i;
1025
+ var MIN_CHARS = {
1026
+ "change-classification.md": 200,
1027
+ "test-plan.md": 200,
1028
+ "ci-gates.md": 150,
1029
+ "change-request.md": 100,
1030
+ "tasks.md": 100
1031
+ };
661
1032
  function meaningfulChars(text) {
662
1033
  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;
663
1034
  }
664
- async function gate(changeId) {
1035
+ async function gate(changeId, opts = {}) {
1036
+ const strict = opts.strict ?? false;
665
1037
  const cwd = process.cwd();
666
1038
  const changeDir = join8(cwd, "specs", "changes", changeId);
667
1039
  if (!existsSync7(changeDir)) {
@@ -669,6 +1041,7 @@ async function gate(changeId) {
669
1041
  process.exit(1);
670
1042
  }
671
1043
  const errors = [];
1044
+ const warnings = [];
672
1045
  for (const f of REQUIRED_FILES) {
673
1046
  if (!existsSync7(join8(changeDir, f))) {
674
1047
  errors.push(`missing required artifact: ${f}`);
@@ -677,8 +1050,9 @@ async function gate(changeId) {
677
1050
  if (errors.length === 0) {
678
1051
  for (const f of REQUIRED_FILES) {
679
1052
  const content = readFileSync4(join8(changeDir, f), "utf8");
680
- if (meaningfulChars(content) < 100) {
681
- errors.push(`${f}: appears to be a stub (< 100 meaningful chars)`);
1053
+ const minChars = MIN_CHARS[f] ?? 100;
1054
+ if (meaningfulChars(content) < minChars) {
1055
+ errors.push(`${f}: appears to be a stub (< ${minChars} meaningful chars)`);
682
1056
  }
683
1057
  }
684
1058
  const classifPath = join8(changeDir, "change-classification.md");
@@ -689,6 +1063,18 @@ async function gate(changeId) {
689
1063
  }
690
1064
  }
691
1065
  }
1066
+ const tasksPath = join8(changeDir, "tasks.md");
1067
+ if (existsSync7(tasksPath)) {
1068
+ const tasksContent = readFileSync4(tasksPath, "utf8");
1069
+ const nonArchivePending = (tasksContent.match(/^\s*-\s*\[ \] (?!7\.[12])/gm) || []).length;
1070
+ if (nonArchivePending > 0) {
1071
+ if (strict) {
1072
+ errors.push(`${nonArchivePending} task(s) still pending (use [-] for N/A items, [x] for done). Run gate without --strict during development.`);
1073
+ } else {
1074
+ warnings.push(`${nonArchivePending} task(s) still pending (warning only in non-strict mode)`);
1075
+ }
1076
+ }
1077
+ }
692
1078
  const agentLogDir = join8(changeDir, "agent-log");
693
1079
  if (existsSync7(agentLogDir)) {
694
1080
  const logFiles = readdirSync5(agentLogDir).filter((f) => f.endsWith(".md"));
@@ -706,8 +1092,69 @@ async function gate(changeId) {
706
1092
  errors.push(`agent-log/${f}: status=blocked requires concrete "next-action:" line (>= 10 chars, not "none")`);
707
1093
  }
708
1094
  }
1095
+ if (strict) {
1096
+ const artifactsMatch = content.match(/- artifacts:([\s\S]*?)(?:\n- |\n#|$)/);
1097
+ if (artifactsMatch) {
1098
+ const artifactLines = artifactsMatch[1].split("\n").filter((l) => l.trim().startsWith("-"));
1099
+ for (const line of artifactLines) {
1100
+ const pointer = line.replace(/^\s*-\s*[\w-]+:\s*/, "").trim();
1101
+ const pathPart = pointer.split(":")[0];
1102
+ if (pathPart.includes("/") && !pointer.startsWith("http")) {
1103
+ const abs = join8(cwd, pathPart);
1104
+ if (!existsSync7(abs)) {
1105
+ errors.push(`agent-log/${f}: artifact pointer not found: ${pathPart}`);
1106
+ }
1107
+ }
1108
+ }
1109
+ }
1110
+ }
1111
+ }
1112
+ const classifPath = join8(changeDir, "change-classification.md");
1113
+ if (existsSync7(classifPath)) {
1114
+ const classificationContent = readFileSync4(classifPath, "utf8");
1115
+ const tierMatch = classificationContent.match(/^## Tier\s*\n\s*-\s*(\d)\s*$/m);
1116
+ const tier = tierMatch ? parseInt(tierMatch[1]) : null;
1117
+ if (tier !== null) {
1118
+ const agentLogFiles = readdirSync5(agentLogDir).map((f) => f.replace(".md", ""));
1119
+ if (tier <= 1) {
1120
+ for (const required of ["e2e-resilience-engineer", "monkey-test-engineer", "stress-soak-engineer"]) {
1121
+ if (!agentLogFiles.includes(required)) {
1122
+ errors.push(`Tier ${tier} change requires agent-log/${required}.md (high-risk change \u2014 E2E/monkey/stress testing mandatory)`);
1123
+ }
1124
+ }
1125
+ }
1126
+ if (tier <= 3) {
1127
+ for (const required of ["contract-reviewer", "qa-reviewer"]) {
1128
+ if (!agentLogFiles.includes(required)) {
1129
+ errors.push(`Tier ${tier} change requires agent-log/${required}.md`);
1130
+ }
1131
+ }
1132
+ }
1133
+ }
1134
+ }
1135
+ } else {
1136
+ const classifPath = join8(changeDir, "change-classification.md");
1137
+ if (existsSync7(classifPath)) {
1138
+ const classificationContent = readFileSync4(classifPath, "utf8");
1139
+ const tierMatch = classificationContent.match(/^## Tier\s*\n\s*-\s*(\d)\s*$/m);
1140
+ const tier = tierMatch ? parseInt(tierMatch[1]) : null;
1141
+ if (tier !== null) {
1142
+ if (tier <= 1) {
1143
+ for (const required of ["e2e-resilience-engineer", "monkey-test-engineer", "stress-soak-engineer"]) {
1144
+ errors.push(`Tier ${tier} change requires agent-log/${required}.md (high-risk change \u2014 E2E/monkey/stress testing mandatory)`);
1145
+ }
1146
+ }
1147
+ if (tier <= 3) {
1148
+ for (const required of ["contract-reviewer", "qa-reviewer"]) {
1149
+ errors.push(`Tier ${tier} change requires agent-log/${required}.md`);
1150
+ }
1151
+ }
1152
+ }
709
1153
  }
710
1154
  }
1155
+ for (const w of warnings) {
1156
+ log.warn(` ${w}`);
1157
+ }
711
1158
  if (errors.length > 0) {
712
1159
  log.error(`gate failed for change: ${changeId}`);
713
1160
  for (const e of errors) {
@@ -716,7 +1163,7 @@ async function gate(changeId) {
716
1163
  process.exit(1);
717
1164
  }
718
1165
  log.info(`gate: running contract validators for ${changeId}\u2026`);
719
- const r = spawnSync2(process.execPath, [process.argv[1], "validate"], {
1166
+ const r = spawnSync2(process.execPath, [process.argv[1], "validate", "--contracts", "--env", "--ci", "--versions"], {
720
1167
  cwd,
721
1168
  stdio: "inherit"
722
1169
  });
@@ -724,12 +1171,16 @@ async function gate(changeId) {
724
1171
  log.error(`gate failed for change: ${changeId} (validators returned non-zero)`);
725
1172
  process.exit(1);
726
1173
  }
1174
+ for (const w of warnings) {
1175
+ log.warn(` ${w}`);
1176
+ }
727
1177
  log.ok(`gate passed for change: ${changeId}`);
728
1178
  }
729
1179
 
730
1180
  // src/commands/install-hooks.ts
731
1181
  import { existsSync as existsSync8, readFileSync as readFileSync5, writeFileSync as writeFileSync2, chmodSync, mkdirSync as mkdirSync3 } from "fs";
732
1182
  import { join as join9 } from "path";
1183
+ init_logger();
733
1184
  var START_MARKER = "# cdd-kit-managed-block-start";
734
1185
  var END_MARKER = "# cdd-kit-managed-block-end";
735
1186
  async function installHooks() {
@@ -782,7 +1233,7 @@ async function installHooks() {
782
1233
 
783
1234
  // src/cli/index.ts
784
1235
  var __dirname2 = dirname3(fileURLToPath2(import.meta.url));
785
- var pkg = JSON.parse(readFileSync6(join10(__dirname2, "..", "..", "package.json"), "utf8"));
1236
+ var pkg = JSON.parse(readFileSync10(join14(__dirname2, "..", "..", "package.json"), "utf8"));
786
1237
  var program = new Command();
787
1238
  program.name("cdd-kit").description("Contract-Driven Delivery Kit CLI").version(pkg.version);
788
1239
  program.command("init").description(
@@ -807,8 +1258,24 @@ program.command("validate").description("Run validation scripts (defaults to all
807
1258
  versions: opts.versions
808
1259
  })
809
1260
  );
810
- program.command("gate <change-id>").description("Run full orchestration gate for a change (required artifacts, content, tier, contracts)").action(async (id) => {
811
- await gate(id);
1261
+ 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) => {
1262
+ await gate(id, { strict: opts.strict });
1263
+ });
1264
+ program.command("archive <change-id>").description("Move a completed change from specs/changes/ to specs/archive/<year>/").action(async (changeId) => {
1265
+ const { archive: archive2 } = await Promise.resolve().then(() => (init_archive(), archive_exports));
1266
+ await archive2(changeId);
1267
+ });
1268
+ 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) => {
1269
+ const { abandon: abandon2 } = await Promise.resolve().then(() => (init_abandon(), abandon_exports));
1270
+ await abandon2(changeId, opts);
1271
+ });
1272
+ program.command("migrate [change-id]").description("Upgrade existing change directories to v1.11.0 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).action(async (changeId, opts = {}) => {
1273
+ const { migrate: migrate2 } = await Promise.resolve().then(() => (init_migrate(), migrate_exports));
1274
+ await migrate2(changeId, opts);
1275
+ });
1276
+ program.command("list").description("List active changes in specs/changes/").action(async () => {
1277
+ const { listChanges: listChanges2 } = await Promise.resolve().then(() => (init_list_changes(), list_changes_exports));
1278
+ await listChanges2();
812
1279
  });
813
1280
  program.command("install-hooks").description("Install pre-commit hook that runs cdd-kit gate on staged changes").action(async () => {
814
1281
  await installHooks();