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.
- package/README.md +383 -143
- package/assets/CLAUDE.template.md +16 -0
- package/assets/agents/backend-engineer.md +13 -0
- package/assets/agents/change-classifier.md +10 -0
- package/assets/agents/ci-cd-gatekeeper.md +13 -0
- package/assets/agents/contract-reviewer.md +13 -0
- package/assets/agents/e2e-resilience-engineer.md +13 -0
- package/assets/agents/frontend-engineer.md +13 -0
- package/assets/agents/monkey-test-engineer.md +13 -0
- package/assets/agents/qa-reviewer.md +13 -0
- package/assets/agents/spec-architect.md +13 -0
- package/assets/agents/stress-soak-engineer.md +13 -0
- package/assets/agents/test-strategist.md +13 -0
- package/assets/hooks/pre-commit +1 -1
- package/assets/skills/cdd-close/SKILL.md +123 -0
- package/assets/skills/cdd-new/SKILL.md +50 -24
- package/assets/skills/cdd-resume/SKILL.md +86 -0
- package/assets/skills/contract-driven-delivery/templates/change-classification.md +9 -8
- package/assets/skills/contract-driven-delivery/templates/tasks.md +7 -0
- package/assets/specs-templates/change-classification.md +9 -8
- package/assets/specs-templates/tasks.md +7 -0
- package/dist/cli/index.js +481 -39
- 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
|
|
363
|
+
import { readFileSync as readFileSync10 } from "fs";
|
|
3
364
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
4
|
-
import { dirname as dirname3, join as
|
|
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
|
-
|
|
706
|
-
|
|
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(
|
|
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.
|
|
4
|
-
"description": "
|
|
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",
|