contract-driven-delivery 1.10.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.
@@ -1,3 +1,10 @@
1
+ ---
2
+ change-id: <change-id>
3
+ status: in-progress
4
+ ---
5
+
6
+ <!-- [x]=done [-]=N/A [ ]=pending -->
7
+
1
8
  # Tasks
2
9
 
3
10
  ## 1. Preparation
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
@@ -33,6 +394,7 @@ var ASSET = {
33
394
  };
34
395
 
35
396
  // src/utils/copy.ts
397
+ init_logger();
36
398
  import {
37
399
  mkdirSync,
38
400
  existsSync,
@@ -40,36 +402,6 @@ import {
40
402
  copyFileSync
41
403
  } from "fs";
42
404
  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
405
  function ensureDir(dir) {
74
406
  mkdirSync(dir, { recursive: true });
75
407
  }
@@ -141,6 +473,9 @@ function copyFileTracked(src, dest, opts = {}) {
141
473
  return { written: true, created: isNew };
142
474
  }
143
475
 
476
+ // src/commands/init.ts
477
+ init_logger();
478
+
144
479
  // src/utils/stack-detect.ts
145
480
  import { existsSync as existsSync2, readFileSync } from "fs";
146
481
  import { join as join3 } from "path";
@@ -417,6 +752,7 @@ async function init(opts) {
417
752
  import { join as join5 } from "path";
418
753
  import { existsSync as existsSync4, mkdirSync as mkdirSync2, readdirSync as readdirSync3, copyFileSync as copyFileSync2, readFileSync as readFileSync3 } from "fs";
419
754
  import { createHash } from "crypto";
755
+ init_logger();
420
756
  import { homedir as homedir2 } from "os";
421
757
  function fileHash(filePath) {
422
758
  const buf = readFileSync3(filePath);
@@ -531,6 +867,7 @@ async function update(opts) {
531
867
  // src/commands/new-change.ts
532
868
  import { join as join6 } from "path";
533
869
  import { existsSync as existsSync5, readdirSync as readdirSync4 } from "fs";
870
+ init_logger();
534
871
  var REQUIRED_TEMPLATES = [
535
872
  "change-request.md",
536
873
  "change-classification.md",
@@ -589,6 +926,7 @@ async function newChange(name, opts) {
589
926
  import { join as join7 } from "path";
590
927
  import { existsSync as existsSync6 } from "fs";
591
928
  import { spawnSync } from "child_process";
929
+ init_logger();
592
930
  var VALIDATORS = [
593
931
  {
594
932
  flag: "contracts",
@@ -672,6 +1010,7 @@ async function validate(opts) {
672
1010
  }
673
1011
 
674
1012
  // src/commands/gate.ts
1013
+ init_logger();
675
1014
  import { existsSync as existsSync7, readFileSync as readFileSync4, readdirSync as readdirSync5 } from "fs";
676
1015
  import { join as join8 } from "path";
677
1016
  import { spawnSync as spawnSync2 } from "child_process";
@@ -683,10 +1022,18 @@ var REQUIRED_FILES = [
683
1022
  "tasks.md"
684
1023
  ];
685
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
+ };
686
1032
  function meaningfulChars(text) {
687
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;
688
1034
  }
689
- async function gate(changeId) {
1035
+ async function gate(changeId, opts = {}) {
1036
+ const strict = opts.strict ?? false;
690
1037
  const cwd = process.cwd();
691
1038
  const changeDir = join8(cwd, "specs", "changes", changeId);
692
1039
  if (!existsSync7(changeDir)) {
@@ -694,6 +1041,7 @@ async function gate(changeId) {
694
1041
  process.exit(1);
695
1042
  }
696
1043
  const errors = [];
1044
+ const warnings = [];
697
1045
  for (const f of REQUIRED_FILES) {
698
1046
  if (!existsSync7(join8(changeDir, f))) {
699
1047
  errors.push(`missing required artifact: ${f}`);
@@ -702,8 +1050,9 @@ async function gate(changeId) {
702
1050
  if (errors.length === 0) {
703
1051
  for (const f of REQUIRED_FILES) {
704
1052
  const content = readFileSync4(join8(changeDir, f), "utf8");
705
- if (meaningfulChars(content) < 100) {
706
- 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)`);
707
1056
  }
708
1057
  }
709
1058
  const classifPath = join8(changeDir, "change-classification.md");
@@ -714,6 +1063,18 @@ async function gate(changeId) {
714
1063
  }
715
1064
  }
716
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
+ }
717
1078
  const agentLogDir = join8(changeDir, "agent-log");
718
1079
  if (existsSync7(agentLogDir)) {
719
1080
  const logFiles = readdirSync5(agentLogDir).filter((f) => f.endsWith(".md"));
@@ -731,8 +1092,69 @@ async function gate(changeId) {
731
1092
  errors.push(`agent-log/${f}: status=blocked requires concrete "next-action:" line (>= 10 chars, not "none")`);
732
1093
  }
733
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
+ }
734
1153
  }
735
1154
  }
1155
+ for (const w of warnings) {
1156
+ log.warn(` ${w}`);
1157
+ }
736
1158
  if (errors.length > 0) {
737
1159
  log.error(`gate failed for change: ${changeId}`);
738
1160
  for (const e of errors) {
@@ -741,7 +1163,7 @@ async function gate(changeId) {
741
1163
  process.exit(1);
742
1164
  }
743
1165
  log.info(`gate: running contract validators for ${changeId}\u2026`);
744
- const r = spawnSync2(process.execPath, [process.argv[1], "validate"], {
1166
+ const r = spawnSync2(process.execPath, [process.argv[1], "validate", "--contracts", "--env", "--ci", "--versions"], {
745
1167
  cwd,
746
1168
  stdio: "inherit"
747
1169
  });
@@ -749,12 +1171,16 @@ async function gate(changeId) {
749
1171
  log.error(`gate failed for change: ${changeId} (validators returned non-zero)`);
750
1172
  process.exit(1);
751
1173
  }
1174
+ for (const w of warnings) {
1175
+ log.warn(` ${w}`);
1176
+ }
752
1177
  log.ok(`gate passed for change: ${changeId}`);
753
1178
  }
754
1179
 
755
1180
  // src/commands/install-hooks.ts
756
1181
  import { existsSync as existsSync8, readFileSync as readFileSync5, writeFileSync as writeFileSync2, chmodSync, mkdirSync as mkdirSync3 } from "fs";
757
1182
  import { join as join9 } from "path";
1183
+ init_logger();
758
1184
  var START_MARKER = "# cdd-kit-managed-block-start";
759
1185
  var END_MARKER = "# cdd-kit-managed-block-end";
760
1186
  async function installHooks() {
@@ -807,7 +1233,7 @@ async function installHooks() {
807
1233
 
808
1234
  // src/cli/index.ts
809
1235
  var __dirname2 = dirname3(fileURLToPath2(import.meta.url));
810
- var pkg = JSON.parse(readFileSync6(join10(__dirname2, "..", "..", "package.json"), "utf8"));
1236
+ var pkg = JSON.parse(readFileSync10(join14(__dirname2, "..", "..", "package.json"), "utf8"));
811
1237
  var program = new Command();
812
1238
  program.name("cdd-kit").description("Contract-Driven Delivery Kit CLI").version(pkg.version);
813
1239
  program.command("init").description(
@@ -832,8 +1258,24 @@ program.command("validate").description("Run validation scripts (defaults to all
832
1258
  versions: opts.versions
833
1259
  })
834
1260
  );
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);
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();
837
1279
  });
838
1280
  program.command("install-hooks").description("Install pre-commit hook that runs cdd-kit gate on staged changes").action(async () => {
839
1281
  await installHooks();
package/package.json CHANGED
@@ -1,15 +1,18 @@
1
1
  {
2
2
  "name": "contract-driven-delivery",
3
- "version": "1.10.0",
4
- "description": "Contract-driven delivery kit CLI spec-first, test-first AI-assisted delivery for brownfield systems",
3
+ "version": "1.11.0",
4
+ "description": "Claude Code kit that turns AI agents into a disciplined engineering team: contracts-first, test-first, spec-first. Every change is classified, contracted, TDD-ed, implemented, and gate-verified by an orchestrated multi-agent flow.",
5
5
  "keywords": [
6
6
  "contract-driven",
7
+ "claude-code",
8
+ "ai-agents",
7
9
  "sdd",
8
10
  "tdd",
9
- "claude-code",
10
- "ai",
11
11
  "specs",
12
- "contracts"
12
+ "contracts",
13
+ "brownfield",
14
+ "multi-agent",
15
+ "cdd-kit"
13
16
  ],
14
17
  "license": "MIT",
15
18
  "author": "Contract-Driven Delivery Contributors",