contract-driven-delivery 1.12.0 → 1.16.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/CHANGELOG.md +133 -0
- package/README.md +23 -10
- package/assets/CLAUDE.template.md +4 -12
- package/assets/agents/backend-engineer.md +4 -25
- package/assets/agents/change-classifier.md +85 -25
- package/assets/agents/ci-cd-gatekeeper.md +4 -25
- package/assets/agents/contract-reviewer.md +4 -25
- package/assets/agents/dependency-security-reviewer.md +4 -24
- package/assets/agents/e2e-resilience-engineer.md +4 -25
- package/assets/agents/frontend-engineer.md +4 -25
- package/assets/agents/monkey-test-engineer.md +4 -25
- package/assets/agents/qa-reviewer.md +4 -25
- package/assets/agents/repo-context-scanner.md +4 -24
- package/assets/agents/spec-architect.md +4 -25
- package/assets/agents/spec-drift-auditor.md +4 -24
- package/assets/agents/stress-soak-engineer.md +4 -25
- package/assets/agents/test-strategist.md +4 -25
- package/assets/agents/ui-ux-reviewer.md +4 -24
- package/assets/agents/visual-reviewer.md +4 -24
- package/assets/cdd/model-policy.json +20 -1
- package/assets/hooks/post-tool-use-files-read.sh +55 -0
- package/assets/skills/cdd-new/SKILL.md +180 -177
- package/assets/skills/cdd-resume/SKILL.md +1 -1
- package/assets/skills/contract-driven-delivery/references/agent-log-protocol.md +117 -0
- package/assets/specs-templates/context-manifest.md +8 -13
- package/assets/specs-templates/tasks.md +2 -0
- package/dist/cli/index.js +1200 -645
- package/package.json +2 -2
package/dist/cli/index.js
CHANGED
|
@@ -107,21 +107,297 @@ var init_provider = __esm({
|
|
|
107
107
|
}
|
|
108
108
|
});
|
|
109
109
|
|
|
110
|
+
// src/commands/context-scan.ts
|
|
111
|
+
var context_scan_exports = {};
|
|
112
|
+
__export(context_scan_exports, {
|
|
113
|
+
contextScan: () => contextScan
|
|
114
|
+
});
|
|
115
|
+
import { existsSync as existsSync6, mkdirSync as mkdirSync3, readFileSync as readFileSync5, readdirSync as readdirSync4, writeFileSync as writeFileSync2 } from "fs";
|
|
116
|
+
import { createHash as createHash2 } from "crypto";
|
|
117
|
+
import { basename, dirname as dirname3, join as join7, relative as relative2 } from "path";
|
|
118
|
+
function sha256OfFile(path) {
|
|
119
|
+
try {
|
|
120
|
+
return createHash2("sha256").update(readFileSync5(path)).digest("hex");
|
|
121
|
+
} catch {
|
|
122
|
+
return "";
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
function inputsDigest(paths) {
|
|
126
|
+
const combined = paths.slice().sort().map((p) => `${p}:${sha256OfFile(p)}`).join("\n");
|
|
127
|
+
return createHash2("sha256").update(combined).digest("hex");
|
|
128
|
+
}
|
|
129
|
+
function stripGlobSuffix(pattern) {
|
|
130
|
+
return pattern.replace(/\/\*\*$/, "").replace(/\/\*$/, "");
|
|
131
|
+
}
|
|
132
|
+
function getForbiddenPaths(cwd) {
|
|
133
|
+
const forbidden = new Set(DEFAULT_FORBIDDEN);
|
|
134
|
+
const policyPath = join7(cwd, ".cdd", "context-policy.json");
|
|
135
|
+
try {
|
|
136
|
+
if (existsSync6(policyPath)) {
|
|
137
|
+
const policy = JSON.parse(readFileSync5(policyPath, "utf8"));
|
|
138
|
+
for (const pattern of policy.forbiddenPaths ?? []) {
|
|
139
|
+
forbidden.add(stripGlobSuffix(pattern));
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
} catch {
|
|
143
|
+
log.warn("Could not parse .cdd/context-policy.json; using default context-scan excludes.");
|
|
144
|
+
}
|
|
145
|
+
return [...forbidden];
|
|
146
|
+
}
|
|
147
|
+
function isForbidden(relPath, forbidden) {
|
|
148
|
+
const normalized = relPath.replace(/\\/g, "/");
|
|
149
|
+
return forbidden.some((pattern) => normalized === pattern || normalized.startsWith(`${pattern}/`));
|
|
150
|
+
}
|
|
151
|
+
function buildTree(dir, cwd, forbidden, stats, prefix = "", depth = 0) {
|
|
152
|
+
const entries = readdirSync4(dir, { withFileTypes: true }).sort((a, b) => {
|
|
153
|
+
if (a.isDirectory() === b.isDirectory())
|
|
154
|
+
return a.name.localeCompare(b.name);
|
|
155
|
+
return a.isDirectory() ? -1 : 1;
|
|
156
|
+
});
|
|
157
|
+
let output = "";
|
|
158
|
+
const visible = entries.filter((entry) => {
|
|
159
|
+
const relPath = relative2(cwd, join7(dir, entry.name));
|
|
160
|
+
return !isForbidden(relPath, forbidden);
|
|
161
|
+
});
|
|
162
|
+
const truncated = visible.length > PER_DIR_ENTRY_CAP;
|
|
163
|
+
const shown = truncated ? visible.slice(0, PER_DIR_ENTRY_CAP) : visible;
|
|
164
|
+
if (truncated)
|
|
165
|
+
stats.truncatedDirs += 1;
|
|
166
|
+
shown.forEach((entry, index) => {
|
|
167
|
+
const fullPath = join7(dir, entry.name);
|
|
168
|
+
const isLast = index === shown.length - 1 && !truncated;
|
|
169
|
+
const connector = isLast ? "\\-- " : "|-- ";
|
|
170
|
+
output += `${prefix}${connector}${entry.name}${entry.isDirectory() ? "/" : ""}
|
|
171
|
+
`;
|
|
172
|
+
if (entry.isDirectory()) {
|
|
173
|
+
stats.dirs += 1;
|
|
174
|
+
if (depth >= 3) {
|
|
175
|
+
stats.omittedDirs += 1;
|
|
176
|
+
output += `${prefix}${isLast ? " " : "| "}\\-- ... (max depth)
|
|
177
|
+
`;
|
|
178
|
+
} else {
|
|
179
|
+
output += buildTree(fullPath, cwd, forbidden, stats, prefix + (isLast ? " " : "| "), depth + 1);
|
|
180
|
+
}
|
|
181
|
+
} else {
|
|
182
|
+
stats.files += 1;
|
|
183
|
+
}
|
|
184
|
+
});
|
|
185
|
+
if (truncated) {
|
|
186
|
+
output += `${prefix}\\-- ... (${visible.length - PER_DIR_ENTRY_CAP} more entries truncated; cap=${PER_DIR_ENTRY_CAP})
|
|
187
|
+
`;
|
|
188
|
+
}
|
|
189
|
+
return output;
|
|
190
|
+
}
|
|
191
|
+
function firstHeading(content) {
|
|
192
|
+
const match = content.match(/^#\s+(.+)$/m);
|
|
193
|
+
return match?.[1]?.trim();
|
|
194
|
+
}
|
|
195
|
+
function deriveContractType(relPath, metadata) {
|
|
196
|
+
if (metadata.contract)
|
|
197
|
+
return metadata.contract;
|
|
198
|
+
const parts = relPath.split("/");
|
|
199
|
+
return parts.length >= 2 ? parts[1] : "unknown";
|
|
200
|
+
}
|
|
201
|
+
function parseContractMetadata(content) {
|
|
202
|
+
const metadata = {};
|
|
203
|
+
let summary;
|
|
204
|
+
const cddMatch = content.match(/<!--\s*cdd:([\s\S]*?)-->/);
|
|
205
|
+
const yamlMatch = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
|
|
206
|
+
const block = cddMatch?.[1] ?? yamlMatch?.[1];
|
|
207
|
+
if (block) {
|
|
208
|
+
for (const line of block.split(/\r?\n/)) {
|
|
209
|
+
const colon = line.indexOf(":");
|
|
210
|
+
if (colon === -1)
|
|
211
|
+
continue;
|
|
212
|
+
const key = line.slice(0, colon).trim();
|
|
213
|
+
const value = line.slice(colon + 1).trim();
|
|
214
|
+
if (!key || !value)
|
|
215
|
+
continue;
|
|
216
|
+
if (key === "summary")
|
|
217
|
+
summary = value;
|
|
218
|
+
else
|
|
219
|
+
metadata[key] = value;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
if (!summary) {
|
|
223
|
+
const summaryMatch = content.match(/#+\s*Summary\s*\r?\n+([^#\r\n][^\r\n]*)/i);
|
|
224
|
+
summary = summaryMatch?.[1]?.trim();
|
|
225
|
+
}
|
|
226
|
+
return { title: firstHeading(content), summary, metadata };
|
|
227
|
+
}
|
|
228
|
+
function findContractFiles(dir, found = []) {
|
|
229
|
+
if (!existsSync6(dir))
|
|
230
|
+
return found;
|
|
231
|
+
for (const entry of readdirSync4(dir, { withFileTypes: true })) {
|
|
232
|
+
const fullPath = join7(dir, entry.name);
|
|
233
|
+
if (entry.isDirectory())
|
|
234
|
+
findContractFiles(fullPath, found);
|
|
235
|
+
else if (entry.isFile() && entry.name.endsWith(".md") && entry.name !== "INDEX.md" && entry.name !== "CHANGELOG.md")
|
|
236
|
+
found.push(fullPath);
|
|
237
|
+
}
|
|
238
|
+
return found;
|
|
239
|
+
}
|
|
240
|
+
async function contextScan(opts = {}) {
|
|
241
|
+
const cwd = process.cwd();
|
|
242
|
+
const specsContextDir = join7(cwd, "specs", "context");
|
|
243
|
+
mkdirSync3(specsContextDir, { recursive: true });
|
|
244
|
+
const forbidden = getForbiddenPaths(cwd);
|
|
245
|
+
const surface = opts.surface;
|
|
246
|
+
let scanRoot = cwd;
|
|
247
|
+
if (surface) {
|
|
248
|
+
const resolvedSurface = join7(cwd, surface);
|
|
249
|
+
if (!existsSync6(resolvedSurface)) {
|
|
250
|
+
log.error(`--surface path not found: ${surface}`);
|
|
251
|
+
process.exit(1);
|
|
252
|
+
}
|
|
253
|
+
if (!resolvedSurface.startsWith(cwd)) {
|
|
254
|
+
log.error(`--surface must be inside the repo: ${surface}`);
|
|
255
|
+
process.exit(1);
|
|
256
|
+
}
|
|
257
|
+
scanRoot = resolvedSurface;
|
|
258
|
+
}
|
|
259
|
+
const treeStats = { dirs: 0, files: 0, omittedDirs: 0, truncatedDirs: 0 };
|
|
260
|
+
const tree = buildTree(scanRoot, cwd, forbidden, treeStats);
|
|
261
|
+
const policyPath = join7(cwd, ".cdd", "context-policy.json");
|
|
262
|
+
const projectMapInputs = [policyPath].filter(existsSync6);
|
|
263
|
+
writeFileSync2(
|
|
264
|
+
join7(specsContextDir, "project-map.md"),
|
|
265
|
+
[
|
|
266
|
+
"---",
|
|
267
|
+
"artifact: project-map",
|
|
268
|
+
"generated-by: cdd-kit context-scan",
|
|
269
|
+
"schema-version: 1",
|
|
270
|
+
`root: ${basename(cwd)}`,
|
|
271
|
+
...surface ? [`surface: ${surface}`] : [],
|
|
272
|
+
`visible-dirs: ${treeStats.dirs}`,
|
|
273
|
+
`visible-files: ${treeStats.files}`,
|
|
274
|
+
`omitted-dirs: ${treeStats.omittedDirs}`,
|
|
275
|
+
`truncated-dirs: ${treeStats.truncatedDirs}`,
|
|
276
|
+
`inputs-digest: ${inputsDigest(projectMapInputs)}`,
|
|
277
|
+
"---",
|
|
278
|
+
"",
|
|
279
|
+
"# Project Map",
|
|
280
|
+
"",
|
|
281
|
+
"Use this deterministic map to choose candidate context paths before reading files.",
|
|
282
|
+
"",
|
|
283
|
+
"## Excluded Paths",
|
|
284
|
+
...forbidden.map((path) => `- ${path}`),
|
|
285
|
+
"",
|
|
286
|
+
"## Tree",
|
|
287
|
+
"",
|
|
288
|
+
"```",
|
|
289
|
+
`${basename(cwd)}/`,
|
|
290
|
+
tree.trimEnd(),
|
|
291
|
+
"```",
|
|
292
|
+
""
|
|
293
|
+
].join("\n"),
|
|
294
|
+
"utf8"
|
|
295
|
+
);
|
|
296
|
+
log.ok("Created specs/context/project-map.md");
|
|
297
|
+
const contractFiles = findContractFiles(join7(cwd, "contracts")).sort((a, b) => relative2(cwd, a).localeCompare(relative2(cwd, b)));
|
|
298
|
+
const contractEntries = [];
|
|
299
|
+
const inventoryRows = [];
|
|
300
|
+
let missingSummary = 0;
|
|
301
|
+
for (const file of contractFiles) {
|
|
302
|
+
const relPath = relative2(cwd, file).replace(/\\/g, "/");
|
|
303
|
+
const dir = dirname3(relPath).replace(/\\/g, "/");
|
|
304
|
+
const { title, summary, metadata } = parseContractMetadata(readFileSync5(file, "utf8"));
|
|
305
|
+
const contractType = deriveContractType(relPath, metadata);
|
|
306
|
+
const owner = metadata.owner ?? "unknown";
|
|
307
|
+
const surface2 = metadata.surface ?? dir;
|
|
308
|
+
const summaryText = summary ?? "MISSING - add YAML frontmatter `summary:` or `<!-- cdd: summary: ... -->`.";
|
|
309
|
+
inventoryRows.push(`| ${relPath} | ${contractType} | ${surface2} | ${owner} | ${summary ? "yes" : "no"} |`);
|
|
310
|
+
let entry = `## ${relPath}
|
|
311
|
+
`;
|
|
312
|
+
entry += `- path: \`${relPath}\`
|
|
313
|
+
`;
|
|
314
|
+
entry += `- type: ${contractType}
|
|
315
|
+
`;
|
|
316
|
+
entry += `- directory: ${dir}
|
|
317
|
+
`;
|
|
318
|
+
if (title)
|
|
319
|
+
entry += `- title: ${title}
|
|
320
|
+
`;
|
|
321
|
+
for (const [key, value] of Object.entries(metadata)) {
|
|
322
|
+
if (key === "contract")
|
|
323
|
+
continue;
|
|
324
|
+
entry += `- ${key}: ${value}
|
|
325
|
+
`;
|
|
326
|
+
}
|
|
327
|
+
entry += `- summary: ${summaryText}
|
|
328
|
+
|
|
329
|
+
`;
|
|
330
|
+
contractEntries.push(entry);
|
|
331
|
+
if (!summary) {
|
|
332
|
+
missingSummary += 1;
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
const contractIndex = [
|
|
336
|
+
"---",
|
|
337
|
+
"artifact: contracts-index",
|
|
338
|
+
"generated-by: cdd-kit context-scan",
|
|
339
|
+
"schema-version: 1",
|
|
340
|
+
`contract-count: ${contractFiles.length}`,
|
|
341
|
+
`missing-summary-count: ${missingSummary}`,
|
|
342
|
+
`inputs-digest: ${inputsDigest(contractFiles)}`,
|
|
343
|
+
"---",
|
|
344
|
+
"",
|
|
345
|
+
"# Contracts Index",
|
|
346
|
+
"",
|
|
347
|
+
"Generated from deterministic metadata. Add YAML frontmatter fields such as `summary`, `owner`, and `surface` to improve classifier accuracy.",
|
|
348
|
+
"",
|
|
349
|
+
"## Contract Inventory",
|
|
350
|
+
"",
|
|
351
|
+
"| path | type | surface | owner | has-summary |",
|
|
352
|
+
"|---|---|---|---|---|",
|
|
353
|
+
...inventoryRows,
|
|
354
|
+
"",
|
|
355
|
+
"## Contract Details",
|
|
356
|
+
"",
|
|
357
|
+
...contractEntries
|
|
358
|
+
].join("\n");
|
|
359
|
+
writeFileSync2(join7(specsContextDir, "contracts-index.md"), contractIndex, "utf8");
|
|
360
|
+
if (missingSummary > 0) {
|
|
361
|
+
log.warn(`Created specs/context/contracts-index.md with ${missingSummary} missing summary warning(s).`);
|
|
362
|
+
} else {
|
|
363
|
+
log.ok("Created specs/context/contracts-index.md");
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
var DEFAULT_FORBIDDEN, PER_DIR_ENTRY_CAP;
|
|
367
|
+
var init_context_scan = __esm({
|
|
368
|
+
"src/commands/context-scan.ts"() {
|
|
369
|
+
"use strict";
|
|
370
|
+
init_logger();
|
|
371
|
+
DEFAULT_FORBIDDEN = [
|
|
372
|
+
".claude",
|
|
373
|
+
".git",
|
|
374
|
+
"node_modules",
|
|
375
|
+
"dist",
|
|
376
|
+
"build",
|
|
377
|
+
"assets",
|
|
378
|
+
"specs/archive",
|
|
379
|
+
"specs/changes"
|
|
380
|
+
];
|
|
381
|
+
PER_DIR_ENTRY_CAP = 50;
|
|
382
|
+
}
|
|
383
|
+
});
|
|
384
|
+
|
|
110
385
|
// src/commands/doctor.ts
|
|
111
386
|
var doctor_exports = {};
|
|
112
387
|
__export(doctor_exports, {
|
|
113
388
|
doctor: () => doctor
|
|
114
389
|
});
|
|
115
|
-
import { existsSync as
|
|
116
|
-
import {
|
|
390
|
+
import { existsSync as existsSync11, readdirSync as readdirSync7, readFileSync as readFileSync9 } from "fs";
|
|
391
|
+
import { createHash as createHash4 } from "crypto";
|
|
392
|
+
import { join as join12 } from "path";
|
|
117
393
|
function fileExists(cwd, relPath) {
|
|
118
|
-
return
|
|
394
|
+
return existsSync11(join12(cwd, relPath));
|
|
119
395
|
}
|
|
120
396
|
function findFiles(dir, predicate, found = []) {
|
|
121
|
-
if (!
|
|
397
|
+
if (!existsSync11(dir))
|
|
122
398
|
return found;
|
|
123
|
-
for (const entry of
|
|
124
|
-
const fullPath =
|
|
399
|
+
for (const entry of readdirSync7(dir, { withFileTypes: true })) {
|
|
400
|
+
const fullPath = join12(dir, entry.name);
|
|
125
401
|
if (entry.isDirectory())
|
|
126
402
|
findFiles(fullPath, predicate, found);
|
|
127
403
|
else if (entry.isFile() && predicate(entry.name))
|
|
@@ -129,54 +405,76 @@ function findFiles(dir, predicate, found = []) {
|
|
|
129
405
|
}
|
|
130
406
|
return found;
|
|
131
407
|
}
|
|
132
|
-
function
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
} catch {
|
|
138
|
-
}
|
|
408
|
+
function sha256OfFile3(path) {
|
|
409
|
+
try {
|
|
410
|
+
return createHash4("sha256").update(readFileSync9(path)).digest("hex");
|
|
411
|
+
} catch {
|
|
412
|
+
return "";
|
|
139
413
|
}
|
|
140
|
-
return newest;
|
|
141
414
|
}
|
|
142
|
-
function
|
|
143
|
-
const
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
415
|
+
function inputDigest(paths) {
|
|
416
|
+
const combined = paths.slice().sort().map((p) => `${p}:${sha256OfFile3(p)}`).join("\n");
|
|
417
|
+
return createHash4("sha256").update(combined).digest("hex");
|
|
418
|
+
}
|
|
419
|
+
function readContextIndexMetadata(filePath) {
|
|
420
|
+
if (!existsSync11(filePath))
|
|
421
|
+
return {};
|
|
422
|
+
const text = readFileSync9(filePath, "utf8");
|
|
423
|
+
const out = {};
|
|
424
|
+
const digestMatch = text.match(/^inputs-digest:\s*([a-f0-9]+)/m);
|
|
425
|
+
if (digestMatch)
|
|
426
|
+
out.inputsDigest = digestMatch[1];
|
|
427
|
+
const missingMatch = text.match(/^missing-summary-count:\s*(\d+)/m);
|
|
428
|
+
if (missingMatch)
|
|
429
|
+
out.missingSummary = Number(missingMatch[1]);
|
|
430
|
+
return out;
|
|
148
431
|
}
|
|
149
432
|
function checkContextFreshness(cwd) {
|
|
150
433
|
const findings = [];
|
|
151
|
-
const projectMap =
|
|
152
|
-
const contractsIndex =
|
|
153
|
-
const contextPolicy =
|
|
154
|
-
const contractFiles = findFiles(
|
|
155
|
-
|
|
434
|
+
const projectMap = join12(cwd, "specs", "context", "project-map.md");
|
|
435
|
+
const contractsIndex = join12(cwd, "specs", "context", "contracts-index.md");
|
|
436
|
+
const contextPolicy = join12(cwd, ".cdd", "context-policy.json");
|
|
437
|
+
const contractFiles = findFiles(
|
|
438
|
+
join12(cwd, "contracts"),
|
|
439
|
+
(name) => name.endsWith(".md") && name !== "INDEX.md" && name !== "CHANGELOG.md"
|
|
440
|
+
);
|
|
441
|
+
if (!existsSync11(projectMap) || !existsSync11(contractsIndex)) {
|
|
156
442
|
findings.push({
|
|
157
443
|
level: "warning",
|
|
158
444
|
message: "specs/context indexes are missing; run cdd-kit context-scan before classification"
|
|
159
445
|
});
|
|
160
446
|
return findings;
|
|
161
447
|
}
|
|
162
|
-
const
|
|
163
|
-
|
|
448
|
+
const projectMapMeta = readContextIndexMetadata(projectMap);
|
|
449
|
+
const contractsIndexMeta = readContextIndexMetadata(contractsIndex);
|
|
450
|
+
const projectInputDigest = inputDigest([contextPolicy].filter(existsSync11));
|
|
451
|
+
if (projectMapMeta.inputsDigest === void 0) {
|
|
452
|
+
findings.push({
|
|
453
|
+
level: "warning",
|
|
454
|
+
message: "specs/context/project-map.md was generated by an older cdd-kit (no inputs-digest); re-run cdd-kit context-scan"
|
|
455
|
+
});
|
|
456
|
+
} else if (projectInputDigest && projectMapMeta.inputsDigest !== projectInputDigest) {
|
|
164
457
|
findings.push({
|
|
165
458
|
level: "warning",
|
|
166
|
-
message: "specs/context/project-map.md
|
|
459
|
+
message: "specs/context/project-map.md inputs changed (.cdd/context-policy.json); re-run cdd-kit context-scan"
|
|
167
460
|
});
|
|
168
461
|
}
|
|
169
|
-
|
|
462
|
+
const contractsInputDigest = inputDigest(contractFiles);
|
|
463
|
+
if (contractsIndexMeta.inputsDigest === void 0) {
|
|
170
464
|
findings.push({
|
|
171
465
|
level: "warning",
|
|
172
|
-
message: "specs/context/contracts-index.md
|
|
466
|
+
message: "specs/context/contracts-index.md was generated by an older cdd-kit (no inputs-digest); re-run cdd-kit context-scan"
|
|
467
|
+
});
|
|
468
|
+
} else if (contractsInputDigest && contractsIndexMeta.inputsDigest !== contractsInputDigest) {
|
|
469
|
+
findings.push({
|
|
470
|
+
level: "warning",
|
|
471
|
+
message: "specs/context/contracts-index.md inputs changed (contracts/*); re-run cdd-kit context-scan"
|
|
173
472
|
});
|
|
174
473
|
}
|
|
175
|
-
|
|
176
|
-
if (missingSummaryCount !== null && missingSummaryCount > 0) {
|
|
474
|
+
if (contractsIndexMeta.missingSummary !== void 0 && contractsIndexMeta.missingSummary > 0) {
|
|
177
475
|
findings.push({
|
|
178
476
|
level: "warning",
|
|
179
|
-
message: `contracts-index reports ${
|
|
477
|
+
message: `contracts-index reports ${contractsIndexMeta.missingSummary} contract(s) without deterministic summary metadata`
|
|
180
478
|
});
|
|
181
479
|
}
|
|
182
480
|
if (findings.length === 0) {
|
|
@@ -184,6 +482,63 @@ function checkContextFreshness(cwd) {
|
|
|
184
482
|
}
|
|
185
483
|
return findings;
|
|
186
484
|
}
|
|
485
|
+
function readAgentModel(path) {
|
|
486
|
+
try {
|
|
487
|
+
const text = readFileSync9(path, "utf8");
|
|
488
|
+
const m = text.match(/^model:\s*(\S+)/m);
|
|
489
|
+
return m ? m[1] : null;
|
|
490
|
+
} catch {
|
|
491
|
+
return null;
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
function checkModelPolicyDrift(cwd) {
|
|
495
|
+
const policyPath = join12(cwd, ".cdd", "model-policy.json");
|
|
496
|
+
if (!existsSync11(policyPath))
|
|
497
|
+
return [];
|
|
498
|
+
let policy;
|
|
499
|
+
try {
|
|
500
|
+
policy = JSON.parse(readFileSync9(policyPath, "utf8"));
|
|
501
|
+
} catch {
|
|
502
|
+
return [{ level: "warning", message: ".cdd/model-policy.json is not valid JSON" }];
|
|
503
|
+
}
|
|
504
|
+
const roles = policy.roles ?? {};
|
|
505
|
+
if (Object.keys(roles).length === 0) {
|
|
506
|
+
return [{
|
|
507
|
+
level: "warning",
|
|
508
|
+
message: ".cdd/model-policy.json has no role bindings; run cdd-kit upgrade to install defaults"
|
|
509
|
+
}];
|
|
510
|
+
}
|
|
511
|
+
const candidateDirs = [
|
|
512
|
+
join12(cwd, ".claude", "agents"),
|
|
513
|
+
process.env.HOME ? join12(process.env.HOME, ".claude", "agents") : "",
|
|
514
|
+
process.env.USERPROFILE ? join12(process.env.USERPROFILE, ".claude", "agents") : ""
|
|
515
|
+
].filter((p) => p && existsSync11(p));
|
|
516
|
+
if (candidateDirs.length === 0)
|
|
517
|
+
return [];
|
|
518
|
+
const findings = [];
|
|
519
|
+
for (const [role, expected] of Object.entries(roles)) {
|
|
520
|
+
let foundAny = false;
|
|
521
|
+
for (const dir of candidateDirs) {
|
|
522
|
+
const path = join12(dir, `${role}.md`);
|
|
523
|
+
if (!existsSync11(path))
|
|
524
|
+
continue;
|
|
525
|
+
foundAny = true;
|
|
526
|
+
const actual = readAgentModel(path);
|
|
527
|
+
if (actual && actual !== expected) {
|
|
528
|
+
findings.push({
|
|
529
|
+
level: "warning",
|
|
530
|
+
message: `model-policy drift: ${role} expected ${expected}, agent prompt uses ${actual} (${path})`
|
|
531
|
+
});
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
if (!foundAny) {
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
if (findings.length === 0) {
|
|
538
|
+
findings.push({ level: "ok", message: "model-policy roles match installed agent prompts" });
|
|
539
|
+
}
|
|
540
|
+
return findings;
|
|
541
|
+
}
|
|
187
542
|
function buildDoctorReport(cwd, opts) {
|
|
188
543
|
const requestedProvider = opts.provider ?? "auto";
|
|
189
544
|
if (!validateProviderOption(requestedProvider)) {
|
|
@@ -206,6 +561,7 @@ function buildDoctorReport(cwd, opts) {
|
|
|
206
561
|
findings.push({ level: "warning", message: "CODEX.md is missing for Codex provider; run cdd-kit upgrade --provider codex --yes" });
|
|
207
562
|
}
|
|
208
563
|
findings.push(...checkContextFreshness(cwd));
|
|
564
|
+
findings.push(...checkModelPolicyDrift(cwd));
|
|
209
565
|
const errors = findings.filter((finding) => finding.level === "error").length;
|
|
210
566
|
const warnings = findings.filter((finding) => finding.level === "warning").length;
|
|
211
567
|
return {
|
|
@@ -217,8 +573,89 @@ function buildDoctorReport(cwd, opts) {
|
|
|
217
573
|
ok: errors === 0 && (!strict || warnings === 0)
|
|
218
574
|
};
|
|
219
575
|
}
|
|
576
|
+
async function attemptAutoFixes(cwd, report) {
|
|
577
|
+
const fixed = [];
|
|
578
|
+
const remaining = [];
|
|
579
|
+
for (const finding of report.findings) {
|
|
580
|
+
if (finding.level !== "warning") {
|
|
581
|
+
remaining.push(finding);
|
|
582
|
+
continue;
|
|
583
|
+
}
|
|
584
|
+
if (/specs\/context indexes are missing|inputs changed|older cdd-kit|older than/i.test(finding.message)) {
|
|
585
|
+
try {
|
|
586
|
+
const { contextScan: contextScan2 } = await Promise.resolve().then(() => (init_context_scan(), context_scan_exports));
|
|
587
|
+
await contextScan2();
|
|
588
|
+
fixed.push(`ran context-scan to refresh specs/context/`);
|
|
589
|
+
continue;
|
|
590
|
+
} catch (err) {
|
|
591
|
+
remaining.push({ level: "warning", message: `${finding.message} (auto-fix failed: ${err.message})` });
|
|
592
|
+
continue;
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
if (/model-policy\.json has no role bindings/i.test(finding.message)) {
|
|
596
|
+
const policyPath = join12(cwd, ".cdd", "model-policy.json");
|
|
597
|
+
try {
|
|
598
|
+
let existing = {};
|
|
599
|
+
try {
|
|
600
|
+
existing = JSON.parse(readFileSync9(policyPath, "utf8"));
|
|
601
|
+
} catch {
|
|
602
|
+
}
|
|
603
|
+
const merged = {
|
|
604
|
+
...existing,
|
|
605
|
+
roles: {
|
|
606
|
+
"change-classifier": "claude-opus-4-7",
|
|
607
|
+
"spec-architect": "claude-opus-4-7",
|
|
608
|
+
"qa-reviewer": "claude-opus-4-7",
|
|
609
|
+
"contract-reviewer": "claude-sonnet-4-6",
|
|
610
|
+
"test-strategist": "claude-sonnet-4-6",
|
|
611
|
+
"backend-engineer": "claude-sonnet-4-6",
|
|
612
|
+
"frontend-engineer": "claude-sonnet-4-6",
|
|
613
|
+
"ci-cd-gatekeeper": "claude-sonnet-4-6",
|
|
614
|
+
"e2e-resilience-engineer": "claude-sonnet-4-6",
|
|
615
|
+
"monkey-test-engineer": "claude-sonnet-4-6",
|
|
616
|
+
"stress-soak-engineer": "claude-sonnet-4-6",
|
|
617
|
+
"ui-ux-reviewer": "claude-sonnet-4-6",
|
|
618
|
+
"visual-reviewer": "claude-sonnet-4-6",
|
|
619
|
+
"dependency-security-reviewer": "claude-sonnet-4-6",
|
|
620
|
+
"spec-drift-auditor": "claude-sonnet-4-6",
|
|
621
|
+
"repo-context-scanner": "claude-haiku-4-5"
|
|
622
|
+
}
|
|
623
|
+
};
|
|
624
|
+
const { writeFileSync: writeFileSync10 } = await import("fs");
|
|
625
|
+
writeFileSync10(policyPath, JSON.stringify(merged, null, 2) + "\n", "utf8");
|
|
626
|
+
fixed.push(`populated .cdd/model-policy.json with default role bindings`);
|
|
627
|
+
continue;
|
|
628
|
+
} catch (err) {
|
|
629
|
+
remaining.push({ level: "warning", message: `${finding.message} (auto-fix failed: ${err.message})` });
|
|
630
|
+
continue;
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
if (/\.cdd\/.*is missing|run cdd-kit upgrade/i.test(finding.message)) {
|
|
634
|
+
remaining.push({
|
|
635
|
+
level: "warning",
|
|
636
|
+
message: `${finding.message} (run \`cdd-kit upgrade --yes\` manually \u2014 too invasive for --fix)`
|
|
637
|
+
});
|
|
638
|
+
continue;
|
|
639
|
+
}
|
|
640
|
+
remaining.push(finding);
|
|
641
|
+
}
|
|
642
|
+
return { fixed, remaining };
|
|
643
|
+
}
|
|
220
644
|
async function doctor(opts = {}) {
|
|
221
|
-
const
|
|
645
|
+
const cwd = process.cwd();
|
|
646
|
+
let report = buildDoctorReport(cwd, opts);
|
|
647
|
+
if (opts.fix && !opts.json) {
|
|
648
|
+
log.blank();
|
|
649
|
+
log.info("Doctor --fix: attempting safe auto-resolutions\u2026");
|
|
650
|
+
const { fixed, remaining } = await attemptAutoFixes(cwd, report);
|
|
651
|
+
for (const f of fixed)
|
|
652
|
+
log.ok(`fixed: ${f}`);
|
|
653
|
+
if (fixed.length > 0) {
|
|
654
|
+
report = buildDoctorReport(cwd, opts);
|
|
655
|
+
} else {
|
|
656
|
+
log.info("no auto-fixable findings");
|
|
657
|
+
}
|
|
658
|
+
}
|
|
222
659
|
if (opts.json) {
|
|
223
660
|
console.log(JSON.stringify(report, null, 2));
|
|
224
661
|
if (!report.ok)
|
|
@@ -260,14 +697,24 @@ var migrate_exports = {};
|
|
|
260
697
|
__export(migrate_exports, {
|
|
261
698
|
migrate: () => migrate
|
|
262
699
|
});
|
|
263
|
-
import { join as
|
|
264
|
-
import { existsSync as
|
|
700
|
+
import { join as join13 } from "path";
|
|
701
|
+
import { cpSync as cpSync2, existsSync as existsSync12, mkdirSync as mkdirSync5, readdirSync as readdirSync8, readFileSync as readFileSync10, renameSync, rmSync as rmSync2, writeFileSync as writeFileSync5 } from "fs";
|
|
702
|
+
function backupChangeDir(cwd, changeId, sessionStamp) {
|
|
703
|
+
const backupRoot = join13(cwd, ".cdd", "migrate-backup", sessionStamp);
|
|
704
|
+
const backupDir2 = join13(backupRoot, changeId);
|
|
705
|
+
mkdirSync5(backupRoot, { recursive: true });
|
|
706
|
+
const sourceDir = join13(cwd, "specs", "changes", changeId);
|
|
707
|
+
if (existsSync12(sourceDir)) {
|
|
708
|
+
cpSync2(sourceDir, backupDir2, { recursive: true });
|
|
709
|
+
}
|
|
710
|
+
return backupDir2;
|
|
711
|
+
}
|
|
265
712
|
function buildLegacyContextManifest(changeId) {
|
|
266
713
|
return [
|
|
267
714
|
"# Context Manifest",
|
|
268
715
|
"",
|
|
269
716
|
"Generated by `cdd-kit migrate` for an existing change.",
|
|
270
|
-
"
|
|
717
|
+
"Legacy manifest. Forbidden paths come from `.cdd/context-policy.json`.",
|
|
271
718
|
"",
|
|
272
719
|
"## Affected Surfaces",
|
|
273
720
|
"- legacy-unknown",
|
|
@@ -275,16 +722,6 @@ function buildLegacyContextManifest(changeId) {
|
|
|
275
722
|
"## Allowed Paths",
|
|
276
723
|
`- specs/changes/${changeId}/`,
|
|
277
724
|
"",
|
|
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
725
|
"## Required Contracts",
|
|
289
726
|
"- legacy-unknown",
|
|
290
727
|
"",
|
|
@@ -321,6 +758,7 @@ function buildContextGovernedManifest(changeId) {
|
|
|
321
758
|
"",
|
|
322
759
|
"Generated by `cdd-kit migrate --enable-context-governance` for an existing change.",
|
|
323
760
|
"Review and narrow the allowed paths before assigning implementation work.",
|
|
761
|
+
"Forbidden paths come from `.cdd/context-policy.json`.",
|
|
324
762
|
"",
|
|
325
763
|
"## Affected Surfaces",
|
|
326
764
|
"- legacy-unknown",
|
|
@@ -330,16 +768,6 @@ function buildContextGovernedManifest(changeId) {
|
|
|
330
768
|
"- specs/context/project-map.md",
|
|
331
769
|
"- specs/context/contracts-index.md",
|
|
332
770
|
"",
|
|
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
771
|
"## Required Contracts",
|
|
344
772
|
"- legacy-unknown",
|
|
345
773
|
"",
|
|
@@ -372,12 +800,14 @@ function buildContextGovernedManifest(changeId) {
|
|
|
372
800
|
""
|
|
373
801
|
].join("\n");
|
|
374
802
|
}
|
|
375
|
-
function migrateOne(changeId, changeDir,
|
|
803
|
+
function migrateOne(changeId, changeDir, enableContextGovernance) {
|
|
376
804
|
const changed = [];
|
|
377
805
|
const warnings = [];
|
|
378
|
-
const
|
|
379
|
-
|
|
380
|
-
|
|
806
|
+
const pending = [];
|
|
807
|
+
let detectedTier = null;
|
|
808
|
+
const tasksPath = join13(changeDir, "tasks.md");
|
|
809
|
+
if (existsSync12(tasksPath)) {
|
|
810
|
+
let content = readFileSync10(tasksPath, "utf8");
|
|
381
811
|
const norm = content.replace(/\r\n/g, "\n");
|
|
382
812
|
let modified = false;
|
|
383
813
|
const taskChanges = [];
|
|
@@ -413,19 +843,19 @@ status: ${inferredStatus}
|
|
|
413
843
|
}
|
|
414
844
|
if (modified) {
|
|
415
845
|
changed.push(`tasks.md: ${taskChanges.join("; ")}`);
|
|
416
|
-
|
|
417
|
-
writeFileSync4(tasksPath, content, "utf8");
|
|
846
|
+
pending.push({ path: tasksPath, content });
|
|
418
847
|
}
|
|
419
848
|
} else {
|
|
420
849
|
warnings.push("tasks.md not found \u2014 skipping frontmatter migration");
|
|
421
850
|
}
|
|
422
|
-
const classifPath =
|
|
423
|
-
if (
|
|
424
|
-
const content =
|
|
851
|
+
const classifPath = join13(changeDir, "change-classification.md");
|
|
852
|
+
if (existsSync12(classifPath)) {
|
|
853
|
+
const content = readFileSync10(classifPath, "utf8");
|
|
425
854
|
const hasNewTierFormat = /^## Tier\s*\n\s*-\s*\d\s*$/m.test(content);
|
|
855
|
+
const oldMatch = content.match(/\*\*Tier[:\*]+\s*(?:Tier\s*)?(\d)/i) ?? content.match(/^-?\s*Tier:\s*(?:Tier\s*)?(\d)/mi);
|
|
856
|
+
if (oldMatch)
|
|
857
|
+
detectedTier = oldMatch[1];
|
|
426
858
|
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
859
|
if (detectedTier) {
|
|
430
860
|
const addition = `
|
|
431
861
|
## Tier
|
|
@@ -435,54 +865,100 @@ status: ${inferredStatus}
|
|
|
435
865
|
changed.push(
|
|
436
866
|
`change-classification.md: appended "## Tier\\n- ${detectedTier}" (converted from old format)`
|
|
437
867
|
);
|
|
438
|
-
|
|
439
|
-
writeFileSync4(classifPath, content + addition, "utf8");
|
|
868
|
+
pending.push({ path: classifPath, content: content + addition });
|
|
440
869
|
}
|
|
441
870
|
} else {
|
|
442
871
|
warnings.push(
|
|
443
|
-
"change-classification.md: could not detect tier (no **Tier:** N or ## Tier N found).
|
|
872
|
+
"change-classification.md: could not detect tier (no **Tier:** N or ## Tier N found). Set `tier: <0-5>` in tasks.md frontmatter to enable tier-based gate checks."
|
|
444
873
|
);
|
|
445
874
|
}
|
|
875
|
+
} else {
|
|
876
|
+
const structured = content.match(/^## Tier\s*\n\s*-\s*(\d)\s*$/m);
|
|
877
|
+
if (structured)
|
|
878
|
+
detectedTier = structured[1];
|
|
446
879
|
}
|
|
447
880
|
}
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
);
|
|
881
|
+
if (existsSync12(tasksPath)) {
|
|
882
|
+
const tasksWrite = pending.find((p) => p.path === tasksPath);
|
|
883
|
+
let content = tasksWrite ? tasksWrite.content : readFileSync10(tasksPath, "utf8");
|
|
884
|
+
let modified = false;
|
|
885
|
+
const subChanges = [];
|
|
886
|
+
if (detectedTier && !/^tier:\s*\d/m.test(content)) {
|
|
887
|
+
content = upsertFrontmatterField(content, "tier", detectedTier);
|
|
888
|
+
modified = true;
|
|
889
|
+
subChanges.push(`backfilled tier: ${detectedTier}`);
|
|
890
|
+
}
|
|
891
|
+
if (!/^archive-tasks:/m.test(content)) {
|
|
892
|
+
content = upsertFrontmatterField(content, "archive-tasks", '["7.1", "7.2"]');
|
|
893
|
+
modified = true;
|
|
894
|
+
subChanges.push("added default archive-tasks list");
|
|
895
|
+
}
|
|
896
|
+
if (modified) {
|
|
897
|
+
if (tasksWrite) {
|
|
898
|
+
tasksWrite.content = content;
|
|
899
|
+
} else {
|
|
900
|
+
pending.push({ path: tasksPath, content });
|
|
901
|
+
}
|
|
902
|
+
changed.push(`tasks.md: ${subChanges.join("; ")}`);
|
|
457
903
|
}
|
|
904
|
+
}
|
|
905
|
+
const manifestPath = join13(changeDir, "context-manifest.md");
|
|
906
|
+
if (!existsSync12(manifestPath)) {
|
|
907
|
+
changed.push(enableContextGovernance ? "context-manifest.md: added context-governance v1 manifest scaffold" : "context-manifest.md: added legacy context manifest scaffold");
|
|
908
|
+
pending.push({
|
|
909
|
+
path: manifestPath,
|
|
910
|
+
content: enableContextGovernance ? buildContextGovernedManifest(changeId) : buildLegacyContextManifest(changeId)
|
|
911
|
+
});
|
|
458
912
|
} else if (enableContextGovernance) {
|
|
459
913
|
warnings.push("context-manifest.md already exists \u2014 review allowed paths before relying on context-governance: v1");
|
|
460
914
|
}
|
|
461
|
-
return { changed, warnings };
|
|
915
|
+
return { result: { changed, warnings }, pending };
|
|
916
|
+
}
|
|
917
|
+
function commitWritesAtomically(pending) {
|
|
918
|
+
const renames = [];
|
|
919
|
+
try {
|
|
920
|
+
for (const write of pending) {
|
|
921
|
+
const tmp = `${write.path}.cdd-migrate.tmp`;
|
|
922
|
+
writeFileSync5(tmp, write.content, "utf8");
|
|
923
|
+
renames.push({ tmp, final: write.path });
|
|
924
|
+
}
|
|
925
|
+
} catch (err) {
|
|
926
|
+
for (const r of renames) {
|
|
927
|
+
try {
|
|
928
|
+
rmSync2(r.tmp, { force: true });
|
|
929
|
+
} catch {
|
|
930
|
+
}
|
|
931
|
+
}
|
|
932
|
+
throw err;
|
|
933
|
+
}
|
|
934
|
+
for (const r of renames) {
|
|
935
|
+
renameSync(r.tmp, r.final);
|
|
936
|
+
}
|
|
462
937
|
}
|
|
463
938
|
async function migrate(changeId, opts = {}) {
|
|
464
939
|
const cwd = process.cwd();
|
|
465
940
|
const dryRun = opts.dryRun ?? false;
|
|
466
941
|
const enableContextGovernance = opts.enableContextGovernance ?? false;
|
|
942
|
+
const noBackup = opts.noBackup ?? false;
|
|
467
943
|
const idsToMigrate = [];
|
|
468
944
|
if (opts.all) {
|
|
469
|
-
const changesDir =
|
|
470
|
-
if (!
|
|
945
|
+
const changesDir = join13(cwd, "specs", "changes");
|
|
946
|
+
if (!existsSync12(changesDir)) {
|
|
471
947
|
log.info("No specs/changes/ directory found \u2014 nothing to migrate.");
|
|
472
948
|
return;
|
|
473
949
|
}
|
|
474
950
|
idsToMigrate.push(
|
|
475
|
-
...
|
|
951
|
+
...readdirSync8(changesDir, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name)
|
|
476
952
|
);
|
|
477
953
|
} else if (changeId) {
|
|
478
|
-
const specificDir =
|
|
479
|
-
if (!
|
|
954
|
+
const specificDir = join13(cwd, "specs", "changes", changeId);
|
|
955
|
+
if (!existsSync12(specificDir)) {
|
|
480
956
|
log.error(`Change not found: specs/changes/${changeId}`);
|
|
481
957
|
process.exit(1);
|
|
482
958
|
}
|
|
483
959
|
idsToMigrate.push(changeId);
|
|
484
960
|
} else {
|
|
485
|
-
log.error("Usage: cdd-kit migrate <change-id> | cdd-kit migrate --all [--dry-run]");
|
|
961
|
+
log.error("Usage: cdd-kit migrate <change-id> | cdd-kit migrate --all [--dry-run] [--no-backup]");
|
|
486
962
|
process.exit(1);
|
|
487
963
|
}
|
|
488
964
|
if (idsToMigrate.length === 0) {
|
|
@@ -493,35 +969,54 @@ async function migrate(changeId, opts = {}) {
|
|
|
493
969
|
log.info("Dry run \u2014 no files will be written.");
|
|
494
970
|
log.blank();
|
|
495
971
|
}
|
|
972
|
+
const sessionStamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
496
973
|
let migratedCount = 0;
|
|
497
974
|
let upToDateCount = 0;
|
|
975
|
+
const backupRoot = join13(cwd, ".cdd", "migrate-backup", sessionStamp);
|
|
498
976
|
for (const id of idsToMigrate) {
|
|
499
|
-
const changeDir =
|
|
500
|
-
if (!
|
|
977
|
+
const changeDir = join13(cwd, "specs", "changes", id);
|
|
978
|
+
if (!existsSync12(changeDir)) {
|
|
501
979
|
log.warn(` ${id}: directory not found \u2014 skipping`);
|
|
502
980
|
continue;
|
|
503
981
|
}
|
|
504
|
-
const {
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
for (const c of changed)
|
|
508
|
-
log.info(` + ${c}`);
|
|
509
|
-
migratedCount++;
|
|
510
|
-
} else {
|
|
982
|
+
const { result, pending } = migrateOne(id, changeDir, enableContextGovernance);
|
|
983
|
+
const { changed, warnings } = result;
|
|
984
|
+
if (changed.length === 0) {
|
|
511
985
|
log.info(` ${id}: already up to date`);
|
|
512
986
|
upToDateCount++;
|
|
987
|
+
for (const w of warnings)
|
|
988
|
+
log.warn(` ${id}: ${w}`);
|
|
989
|
+
continue;
|
|
513
990
|
}
|
|
514
|
-
|
|
515
|
-
|
|
991
|
+
if (!dryRun) {
|
|
992
|
+
try {
|
|
993
|
+
if (!noBackup)
|
|
994
|
+
backupChangeDir(cwd, id, sessionStamp);
|
|
995
|
+
commitWritesAtomically(pending);
|
|
996
|
+
} catch (err) {
|
|
997
|
+
log.error(` ${id}: migration failed \u2014 ${err.message}`);
|
|
998
|
+
if (!noBackup) {
|
|
999
|
+
log.error(` ${id}: restore from .cdd/migrate-backup/${sessionStamp}/${id}/`);
|
|
1000
|
+
}
|
|
1001
|
+
process.exit(1);
|
|
1002
|
+
}
|
|
516
1003
|
}
|
|
1004
|
+
log.ok(` ${id}: migrated`);
|
|
1005
|
+
for (const c of changed)
|
|
1006
|
+
log.info(` + ${c}`);
|
|
1007
|
+
migratedCount++;
|
|
1008
|
+
for (const w of warnings)
|
|
1009
|
+
log.warn(` ${id}: ${w}`);
|
|
517
1010
|
}
|
|
518
1011
|
log.blank();
|
|
519
1012
|
if (dryRun) {
|
|
520
1013
|
log.info(`Dry run complete: ${migratedCount} change(s) would be updated, ${upToDateCount} already up to date.`);
|
|
521
1014
|
} else {
|
|
522
1015
|
log.ok(`Migration complete: ${migratedCount} updated, ${upToDateCount} already up to date.`);
|
|
523
|
-
if (migratedCount > 0) {
|
|
1016
|
+
if (migratedCount > 0 && !noBackup) {
|
|
1017
|
+
log.info(`Backup: ${backupRoot}`);
|
|
524
1018
|
log.info('Next: git add specs/changes/ && git commit -m "chore: migrate changes to current cdd-kit format"');
|
|
1019
|
+
log.info("When stable, remove backup: rm -rf .cdd/migrate-backup/");
|
|
525
1020
|
}
|
|
526
1021
|
}
|
|
527
1022
|
}
|
|
@@ -537,39 +1032,39 @@ var upgrade_exports = {};
|
|
|
537
1032
|
__export(upgrade_exports, {
|
|
538
1033
|
upgrade: () => upgrade
|
|
539
1034
|
});
|
|
540
|
-
import { existsSync as
|
|
541
|
-
import { dirname as
|
|
1035
|
+
import { existsSync as existsSync13, mkdirSync as mkdirSync6, readdirSync as readdirSync9, copyFileSync as copyFileSync3, readFileSync as readFileSync11, writeFileSync as writeFileSync6 } from "fs";
|
|
1036
|
+
import { dirname as dirname4, join as join14, relative as relative3 } from "path";
|
|
542
1037
|
function planMissingFiles(srcDir, destDir, label, planned) {
|
|
543
|
-
if (!
|
|
1038
|
+
if (!existsSync13(srcDir))
|
|
544
1039
|
return;
|
|
545
|
-
for (const entry of
|
|
546
|
-
const src =
|
|
547
|
-
const dest =
|
|
1040
|
+
for (const entry of readdirSync9(srcDir, { withFileTypes: true })) {
|
|
1041
|
+
const src = join14(srcDir, entry.name);
|
|
1042
|
+
const dest = join14(destDir, entry.name);
|
|
548
1043
|
if (entry.isDirectory()) {
|
|
549
|
-
planMissingFiles(src, dest,
|
|
1044
|
+
planMissingFiles(src, dest, join14(label, entry.name), planned);
|
|
550
1045
|
continue;
|
|
551
1046
|
}
|
|
552
|
-
if (!
|
|
553
|
-
planned.push({ src, dest, rel:
|
|
1047
|
+
if (!existsSync13(dest)) {
|
|
1048
|
+
planned.push({ src, dest, rel: join14(label, relative3(srcDir, src)) });
|
|
554
1049
|
}
|
|
555
1050
|
}
|
|
556
1051
|
}
|
|
557
1052
|
function planProviderGuidance(cwd, provider, planned) {
|
|
558
1053
|
if (provider === "claude" || provider === "both") {
|
|
559
|
-
if (!
|
|
560
|
-
planned.push({ src: ASSET.claudeTemplate, dest:
|
|
1054
|
+
if (!existsSync13(join14(cwd, "CLAUDE.md"))) {
|
|
1055
|
+
planned.push({ src: ASSET.claudeTemplate, dest: join14(cwd, "CLAUDE.md"), rel: "CLAUDE.md" });
|
|
561
1056
|
}
|
|
562
|
-
if (!
|
|
563
|
-
planned.push({ src: ASSET.agentsTemplate, dest:
|
|
1057
|
+
if (!existsSync13(join14(cwd, "AGENTS.md"))) {
|
|
1058
|
+
planned.push({ src: ASSET.agentsTemplate, dest: join14(cwd, "AGENTS.md"), rel: "AGENTS.md" });
|
|
564
1059
|
}
|
|
565
1060
|
}
|
|
566
|
-
if ((provider === "codex" || provider === "both") && !
|
|
567
|
-
planned.push({ src: ASSET.codexTemplate, dest:
|
|
1061
|
+
if ((provider === "codex" || provider === "both") && !existsSync13(join14(cwd, "CODEX.md"))) {
|
|
1062
|
+
planned.push({ src: ASSET.codexTemplate, dest: join14(cwd, "CODEX.md"), rel: "CODEX.md" });
|
|
568
1063
|
}
|
|
569
1064
|
}
|
|
570
1065
|
function applyCopy(plan) {
|
|
571
1066
|
for (const item of plan) {
|
|
572
|
-
|
|
1067
|
+
mkdirSync6(dirname4(item.dest), { recursive: true });
|
|
573
1068
|
copyFileSync3(item.src, item.dest);
|
|
574
1069
|
}
|
|
575
1070
|
}
|
|
@@ -582,12 +1077,12 @@ async function upgrade(opts = {}) {
|
|
|
582
1077
|
}
|
|
583
1078
|
const provider = inferProvider(cwd, requestedProvider);
|
|
584
1079
|
const plan = [];
|
|
585
|
-
planMissingFiles(ASSET.contracts,
|
|
586
|
-
planMissingFiles(ASSET.specsTemplates,
|
|
587
|
-
planMissingFiles(ASSET.testsTemplates,
|
|
588
|
-
planMissingFiles(ASSET.ci,
|
|
589
|
-
planMissingFiles(ASSET.githubWorkflows,
|
|
590
|
-
planMissingFiles(ASSET.cddConfig,
|
|
1080
|
+
planMissingFiles(ASSET.contracts, join14(cwd, "contracts"), "contracts", plan);
|
|
1081
|
+
planMissingFiles(ASSET.specsTemplates, join14(cwd, "specs", "templates"), "specs/templates", plan);
|
|
1082
|
+
planMissingFiles(ASSET.testsTemplates, join14(cwd, "tests", "templates"), "tests/templates", plan);
|
|
1083
|
+
planMissingFiles(ASSET.ci, join14(cwd, "ci"), "ci", plan);
|
|
1084
|
+
planMissingFiles(ASSET.githubWorkflows, join14(cwd, ".github", "workflows"), ".github/workflows", plan);
|
|
1085
|
+
planMissingFiles(ASSET.cddConfig, join14(cwd, ".cdd"), ".cdd", plan);
|
|
591
1086
|
planProviderGuidance(cwd, provider, plan);
|
|
592
1087
|
log.blank();
|
|
593
1088
|
log.info(`Upgrade provider: ${provider}`);
|
|
@@ -624,13 +1119,20 @@ async function upgrade(opts = {}) {
|
|
|
624
1119
|
return;
|
|
625
1120
|
}
|
|
626
1121
|
applyCopy(plan);
|
|
627
|
-
const modelPolicyPath =
|
|
628
|
-
if (
|
|
629
|
-
|
|
1122
|
+
const modelPolicyPath = join14(cwd, ".cdd", "model-policy.json");
|
|
1123
|
+
if (existsSync13(modelPolicyPath)) {
|
|
1124
|
+
let existing = {};
|
|
1125
|
+
try {
|
|
1126
|
+
existing = JSON.parse(readFileSync11(modelPolicyPath, "utf8"));
|
|
1127
|
+
} catch {
|
|
1128
|
+
}
|
|
1129
|
+
const merged = {
|
|
1130
|
+
...existing,
|
|
630
1131
|
provider,
|
|
631
1132
|
generated_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
632
|
-
roles: {}
|
|
633
|
-
}
|
|
1133
|
+
roles: existing.roles && typeof existing.roles === "object" ? existing.roles : {}
|
|
1134
|
+
};
|
|
1135
|
+
writeFileSync6(modelPolicyPath, JSON.stringify(merged, null, 2) + "\n", "utf8");
|
|
634
1136
|
}
|
|
635
1137
|
log.blank();
|
|
636
1138
|
log.ok(`Upgrade complete: ${plan.length} missing file(s) added.`);
|
|
@@ -661,26 +1163,26 @@ var archive_exports = {};
|
|
|
661
1163
|
__export(archive_exports, {
|
|
662
1164
|
archive: () => archive
|
|
663
1165
|
});
|
|
664
|
-
import { join as
|
|
665
|
-
import { existsSync as
|
|
1166
|
+
import { join as join15 } from "path";
|
|
1167
|
+
import { existsSync as existsSync14, mkdirSync as mkdirSync7, renameSync as renameSync2, readFileSync as readFileSync12, writeFileSync as writeFileSync7, appendFileSync, cpSync as cpSync3, rmSync as rmSync3 } from "fs";
|
|
666
1168
|
async function archive(changeId) {
|
|
667
1169
|
const cwd = process.cwd();
|
|
668
|
-
const changeDir =
|
|
1170
|
+
const changeDir = join15(cwd, "specs", "changes", changeId);
|
|
669
1171
|
const archiveYear = (/* @__PURE__ */ new Date()).getFullYear().toString();
|
|
670
|
-
const archiveBase =
|
|
671
|
-
const archiveDir =
|
|
672
|
-
const indexPath =
|
|
673
|
-
if (!
|
|
1172
|
+
const archiveBase = join15(cwd, "specs", "archive", archiveYear);
|
|
1173
|
+
const archiveDir = join15(archiveBase, changeId);
|
|
1174
|
+
const indexPath = join15(cwd, "specs", "archive", "INDEX.md");
|
|
1175
|
+
if (!existsSync14(changeDir)) {
|
|
674
1176
|
log.error(`Change not found: specs/changes/${changeId}`);
|
|
675
1177
|
process.exit(1);
|
|
676
1178
|
}
|
|
677
|
-
if (
|
|
1179
|
+
if (existsSync14(archiveDir)) {
|
|
678
1180
|
log.error(`Already archived: specs/archive/${archiveYear}/${changeId}`);
|
|
679
1181
|
process.exit(1);
|
|
680
1182
|
}
|
|
681
|
-
const tasksPath =
|
|
682
|
-
if (
|
|
683
|
-
const content =
|
|
1183
|
+
const tasksPath = join15(changeDir, "tasks.md");
|
|
1184
|
+
if (existsSync14(tasksPath)) {
|
|
1185
|
+
const content = readFileSync12(tasksPath, "utf8");
|
|
684
1186
|
if (content.includes("status: gate-blocked")) {
|
|
685
1187
|
log.warn("tasks.md has status: gate-blocked \u2014 archiving anyway (change was paused).");
|
|
686
1188
|
}
|
|
@@ -689,15 +1191,15 @@ async function archive(changeId) {
|
|
|
689
1191
|
log.warn(`${pending} task(s) still pending ([ ]). Archive anyway.`);
|
|
690
1192
|
}
|
|
691
1193
|
}
|
|
692
|
-
if (!
|
|
693
|
-
|
|
1194
|
+
if (!existsSync14(archiveBase)) {
|
|
1195
|
+
mkdirSync7(archiveBase, { recursive: true });
|
|
694
1196
|
}
|
|
695
1197
|
try {
|
|
696
|
-
|
|
1198
|
+
renameSync2(changeDir, archiveDir);
|
|
697
1199
|
} catch (err) {
|
|
698
1200
|
if (err.code === "EXDEV") {
|
|
699
|
-
|
|
700
|
-
|
|
1201
|
+
cpSync3(changeDir, archiveDir, { recursive: true });
|
|
1202
|
+
rmSync3(changeDir, { recursive: true, force: true });
|
|
701
1203
|
} else {
|
|
702
1204
|
throw err;
|
|
703
1205
|
}
|
|
@@ -706,8 +1208,8 @@ async function archive(changeId) {
|
|
|
706
1208
|
const today = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
|
|
707
1209
|
const indexLine = `| ${changeId} | ${archiveYear} | ${today} | specs/archive/${archiveYear}/${changeId}/ |
|
|
708
1210
|
`;
|
|
709
|
-
if (!
|
|
710
|
-
|
|
1211
|
+
if (!existsSync14(indexPath)) {
|
|
1212
|
+
writeFileSync7(indexPath, `# Archive Index
|
|
711
1213
|
|
|
712
1214
|
| change-id | year | archived-date | path |
|
|
713
1215
|
|---|---|---|---|
|
|
@@ -720,357 +1222,125 @@ ${indexLine}`, "utf8");
|
|
|
720
1222
|
log.info(`Next: promote durable learnings from archive.md to contracts/ or CLAUDE.md`);
|
|
721
1223
|
}
|
|
722
1224
|
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}
|
|
1225
|
+
"src/commands/archive.ts"() {
|
|
1226
|
+
"use strict";
|
|
1227
|
+
init_logger();
|
|
1228
|
+
}
|
|
1229
|
+
});
|
|
1010
1230
|
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1231
|
+
// src/commands/abandon.ts
|
|
1232
|
+
var abandon_exports = {};
|
|
1233
|
+
__export(abandon_exports, {
|
|
1234
|
+
abandon: () => abandon
|
|
1235
|
+
});
|
|
1236
|
+
import { join as join16 } from "path";
|
|
1237
|
+
import { existsSync as existsSync15, readFileSync as readFileSync13, writeFileSync as writeFileSync8, appendFileSync as appendFileSync2, mkdirSync as mkdirSync8 } from "fs";
|
|
1238
|
+
async function abandon(changeId, opts) {
|
|
1239
|
+
const cwd = process.cwd();
|
|
1240
|
+
const changeDir = join16(cwd, "specs", "changes", changeId);
|
|
1241
|
+
const tasksPath = join16(changeDir, "tasks.md");
|
|
1242
|
+
if (!existsSync15(changeDir)) {
|
|
1243
|
+
log.error(`Change not found: specs/changes/${changeId}`);
|
|
1244
|
+
process.exit(1);
|
|
1245
|
+
}
|
|
1246
|
+
if (existsSync15(tasksPath)) {
|
|
1247
|
+
let content = readFileSync13(tasksPath, "utf8");
|
|
1248
|
+
if (content.match(/^status:/m)) {
|
|
1249
|
+
content = content.replace(/^status: .*/m, "status: abandoned");
|
|
1250
|
+
} else {
|
|
1251
|
+
content = `---
|
|
1252
|
+
change-id: ${changeId}
|
|
1253
|
+
status: abandoned
|
|
1254
|
+
---
|
|
1255
|
+
|
|
1256
|
+
` + content;
|
|
1015
1257
|
}
|
|
1258
|
+
writeFileSync8(tasksPath, content, "utf8");
|
|
1016
1259
|
}
|
|
1017
|
-
const
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
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).`);
|
|
1260
|
+
const today = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
|
|
1261
|
+
const archiveDir = join16(cwd, "specs", "archive");
|
|
1262
|
+
const indexPath = join16(archiveDir, "INDEX.md");
|
|
1263
|
+
const reason = opts.reason ?? "no reason given";
|
|
1264
|
+
const indexLine = `| ${changeId} | abandoned | ${today} | ${reason} |
|
|
1265
|
+
`;
|
|
1266
|
+
if (!existsSync15(archiveDir)) {
|
|
1267
|
+
mkdirSync8(archiveDir, { recursive: true });
|
|
1268
|
+
}
|
|
1269
|
+
if (!existsSync15(indexPath)) {
|
|
1270
|
+
writeFileSync8(indexPath, `# Archive Index
|
|
1271
|
+
|
|
1272
|
+
| change-id | status | date | notes |
|
|
1273
|
+
|---|---|---|---|
|
|
1274
|
+
${indexLine}`, "utf8");
|
|
1043
1275
|
} else {
|
|
1044
|
-
|
|
1276
|
+
appendFileSync2(indexPath, indexLine, "utf8");
|
|
1045
1277
|
}
|
|
1278
|
+
log.ok(`Change ${changeId} marked as abandoned.`);
|
|
1279
|
+
log.info(`specs/changes/${changeId}/ remains on disk (git history preserved).`);
|
|
1280
|
+
log.info(`Run \`cdd-kit archive ${changeId}\` to physically move it, or leave it for git history.`);
|
|
1046
1281
|
}
|
|
1047
|
-
var
|
|
1048
|
-
|
|
1049
|
-
|
|
1282
|
+
var init_abandon = __esm({
|
|
1283
|
+
"src/commands/abandon.ts"() {
|
|
1284
|
+
"use strict";
|
|
1285
|
+
init_logger();
|
|
1286
|
+
}
|
|
1287
|
+
});
|
|
1288
|
+
|
|
1289
|
+
// src/commands/list-changes.ts
|
|
1290
|
+
var list_changes_exports = {};
|
|
1291
|
+
__export(list_changes_exports, {
|
|
1292
|
+
listChanges: () => listChanges
|
|
1293
|
+
});
|
|
1294
|
+
import { join as join17 } from "path";
|
|
1295
|
+
import { existsSync as existsSync16, readdirSync as readdirSync10, readFileSync as readFileSync14 } from "fs";
|
|
1296
|
+
async function listChanges() {
|
|
1297
|
+
const cwd = process.cwd();
|
|
1298
|
+
const changesDir = join17(cwd, "specs", "changes");
|
|
1299
|
+
log.blank();
|
|
1300
|
+
const active = [];
|
|
1301
|
+
if (existsSync16(changesDir)) {
|
|
1302
|
+
active.push(...readdirSync10(changesDir, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name));
|
|
1303
|
+
}
|
|
1304
|
+
if (active.length === 0) {
|
|
1305
|
+
log.info("No active changes in specs/changes/");
|
|
1306
|
+
} else {
|
|
1307
|
+
log.info("Active changes:");
|
|
1308
|
+
for (const id of active) {
|
|
1309
|
+
const tasksPath = join17(changesDir, id, "tasks.md");
|
|
1310
|
+
let status = "in-progress";
|
|
1311
|
+
let pending = 0;
|
|
1312
|
+
if (existsSync16(tasksPath)) {
|
|
1313
|
+
const content = readFileSync14(tasksPath, "utf8");
|
|
1314
|
+
if (content.includes("status: gate-blocked"))
|
|
1315
|
+
status = "gate-blocked";
|
|
1316
|
+
else if (content.includes("status: abandoned"))
|
|
1317
|
+
status = "abandoned";
|
|
1318
|
+
pending = (content.match(/^\s*-\s*\[ \]/gm) || []).length;
|
|
1319
|
+
}
|
|
1320
|
+
const pendingStr = pending > 0 ? ` (${pending} pending)` : "";
|
|
1321
|
+
log.info(` ${id} [${status}]${pendingStr}`);
|
|
1322
|
+
}
|
|
1323
|
+
}
|
|
1324
|
+
log.blank();
|
|
1325
|
+
}
|
|
1326
|
+
var init_list_changes = __esm({
|
|
1327
|
+
"src/commands/list-changes.ts"() {
|
|
1050
1328
|
"use strict";
|
|
1051
1329
|
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
1330
|
}
|
|
1063
1331
|
});
|
|
1064
1332
|
|
|
1065
1333
|
// src/commands/context.ts
|
|
1066
1334
|
var context_exports = {};
|
|
1067
1335
|
__export(context_exports, {
|
|
1336
|
+
approveAllPending: () => approveAllPending,
|
|
1068
1337
|
approveContextExpansion: () => approveContextExpansion,
|
|
1069
1338
|
listContextExpansions: () => listContextExpansions,
|
|
1339
|
+
rejectAllPending: () => rejectAllPending,
|
|
1070
1340
|
rejectContextExpansion: () => rejectContextExpansion,
|
|
1071
1341
|
requestContextExpansion: () => requestContextExpansion
|
|
1072
1342
|
});
|
|
1073
|
-
import { existsSync as existsSync17, readFileSync as
|
|
1343
|
+
import { existsSync as existsSync17, readFileSync as readFileSync15, writeFileSync as writeFileSync9 } from "fs";
|
|
1074
1344
|
import { join as join18 } from "path";
|
|
1075
1345
|
function normalizePath(path) {
|
|
1076
1346
|
return path.replace(/\\/g, "/").replace(/^\.\//, "").trim();
|
|
@@ -1093,7 +1363,7 @@ function readManifest(changeId) {
|
|
|
1093
1363
|
log.error(`context manifest not found: specs/changes/${changeId}/context-manifest.md`);
|
|
1094
1364
|
process.exit(1);
|
|
1095
1365
|
}
|
|
1096
|
-
return
|
|
1366
|
+
return readFileSync15(manifestPath, "utf8");
|
|
1097
1367
|
}
|
|
1098
1368
|
function writeManifest(changeId, content) {
|
|
1099
1369
|
writeFileSync9(manifestPathFor(changeId), content.endsWith("\n") ? content : `${content}
|
|
@@ -1240,6 +1510,21 @@ async function listContextExpansions(changeId, json = false) {
|
|
|
1240
1510
|
log.dim(` ${path}`);
|
|
1241
1511
|
}
|
|
1242
1512
|
}
|
|
1513
|
+
function applyApproval(content, request) {
|
|
1514
|
+
for (const path of request.paths) {
|
|
1515
|
+
const validationError = validateRepoRelativePath(path);
|
|
1516
|
+
if (validationError) {
|
|
1517
|
+
log.error(validationError);
|
|
1518
|
+
process.exit(1);
|
|
1519
|
+
}
|
|
1520
|
+
}
|
|
1521
|
+
const approved = approvedExpansionSet(content);
|
|
1522
|
+
for (const path of request.paths)
|
|
1523
|
+
approved.add(path);
|
|
1524
|
+
let next = replaceSection(content, "Approved Expansions", [...approved].sort().map((p) => `- ${p}`));
|
|
1525
|
+
next = setRequestStatus(next, request.requestId, "approved");
|
|
1526
|
+
return next;
|
|
1527
|
+
}
|
|
1243
1528
|
async function approveContextExpansion(changeId, requestId) {
|
|
1244
1529
|
const content = readManifest(changeId);
|
|
1245
1530
|
const request = parseRequests(content).find((item) => item.requestId === requestId && item.status === "pending");
|
|
@@ -1251,28 +1536,53 @@ async function approveContextExpansion(changeId, requestId) {
|
|
|
1251
1536
|
log.error(`context expansion request has no requested_paths: ${requestId}`);
|
|
1252
1537
|
process.exit(1);
|
|
1253
1538
|
}
|
|
1254
|
-
|
|
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");
|
|
1539
|
+
const next = applyApproval(content, request);
|
|
1266
1540
|
writeManifest(changeId, next);
|
|
1267
1541
|
log.ok(`approved context expansion ${requestId} for ${changeId}`);
|
|
1268
1542
|
for (const path of request.paths)
|
|
1269
1543
|
log.info(` ${path}`);
|
|
1270
1544
|
}
|
|
1545
|
+
async function approveAllPending(changeId) {
|
|
1546
|
+
let content = readManifest(changeId);
|
|
1547
|
+
const pending = parseRequests(content).filter((r) => r.status === "pending");
|
|
1548
|
+
if (pending.length === 0) {
|
|
1549
|
+
log.info(`no pending context expansion requests for ${changeId}`);
|
|
1550
|
+
return;
|
|
1551
|
+
}
|
|
1552
|
+
const skipped = [];
|
|
1553
|
+
let approvedCount = 0;
|
|
1554
|
+
for (const request of pending) {
|
|
1555
|
+
if (request.paths.length === 0) {
|
|
1556
|
+
skipped.push(`${request.requestId} (no requested_paths)`);
|
|
1557
|
+
continue;
|
|
1558
|
+
}
|
|
1559
|
+
content = applyApproval(content, request);
|
|
1560
|
+
approvedCount += 1;
|
|
1561
|
+
}
|
|
1562
|
+
writeManifest(changeId, content);
|
|
1563
|
+
log.ok(`approved ${approvedCount} pending context expansion request(s) for ${changeId}`);
|
|
1564
|
+
for (const reason of skipped) {
|
|
1565
|
+
log.warn(` skipped ${reason}`);
|
|
1566
|
+
}
|
|
1567
|
+
}
|
|
1271
1568
|
async function rejectContextExpansion(changeId, requestId) {
|
|
1272
1569
|
const next = setRequestStatus(readManifest(changeId), requestId, "rejected");
|
|
1273
1570
|
writeManifest(changeId, next);
|
|
1274
1571
|
log.ok(`rejected context expansion ${requestId} for ${changeId}`);
|
|
1275
1572
|
}
|
|
1573
|
+
async function rejectAllPending(changeId) {
|
|
1574
|
+
let content = readManifest(changeId);
|
|
1575
|
+
const pending = parseRequests(content).filter((r) => r.status === "pending");
|
|
1576
|
+
if (pending.length === 0) {
|
|
1577
|
+
log.info(`no pending context expansion requests for ${changeId}`);
|
|
1578
|
+
return;
|
|
1579
|
+
}
|
|
1580
|
+
for (const request of pending) {
|
|
1581
|
+
content = setRequestStatus(content, request.requestId, "rejected");
|
|
1582
|
+
}
|
|
1583
|
+
writeManifest(changeId, content);
|
|
1584
|
+
log.ok(`rejected ${pending.length} pending context expansion request(s) for ${changeId}`);
|
|
1585
|
+
}
|
|
1276
1586
|
var init_context = __esm({
|
|
1277
1587
|
"src/commands/context.ts"() {
|
|
1278
1588
|
"use strict";
|
|
@@ -1281,7 +1591,7 @@ var init_context = __esm({
|
|
|
1281
1591
|
});
|
|
1282
1592
|
|
|
1283
1593
|
// src/cli/index.ts
|
|
1284
|
-
import { readFileSync as
|
|
1594
|
+
import { readFileSync as readFileSync16 } from "fs";
|
|
1285
1595
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
1286
1596
|
import { dirname as dirname5, join as join19 } from "path";
|
|
1287
1597
|
import { Command } from "commander";
|
|
@@ -1590,11 +1900,18 @@ async function init(opts) {
|
|
|
1590
1900
|
log.ok(`.cdd/ - ${cddConfigCount} file(s) written.`);
|
|
1591
1901
|
const modelPolicyPath = join4(cwd, ".cdd", "model-policy.json");
|
|
1592
1902
|
if (existsSync3(modelPolicyPath)) {
|
|
1593
|
-
|
|
1903
|
+
let existing = {};
|
|
1904
|
+
try {
|
|
1905
|
+
existing = JSON.parse(readFileSync2(modelPolicyPath, "utf8"));
|
|
1906
|
+
} catch {
|
|
1907
|
+
}
|
|
1908
|
+
const merged = {
|
|
1909
|
+
...existing,
|
|
1594
1910
|
provider: opts.provider,
|
|
1595
1911
|
generated_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1596
|
-
roles: {}
|
|
1597
|
-
}
|
|
1912
|
+
roles: existing.roles && typeof existing.roles === "object" && Object.keys(existing.roles).length > 0 ? existing.roles : {}
|
|
1913
|
+
};
|
|
1914
|
+
writeFileSync(modelPolicyPath, JSON.stringify(merged, null, 2) + "\n", "utf8");
|
|
1598
1915
|
}
|
|
1599
1916
|
const { count: wfCount, created: wfCreated } = copyDirTracked(
|
|
1600
1917
|
ASSET.githubWorkflows,
|
|
@@ -1823,9 +2140,58 @@ async function update(opts) {
|
|
|
1823
2140
|
|
|
1824
2141
|
// src/commands/new-change.ts
|
|
1825
2142
|
init_paths();
|
|
1826
|
-
import { join as
|
|
1827
|
-
import {
|
|
2143
|
+
import { join as join8 } from "path";
|
|
2144
|
+
import { createHash as createHash3 } from "crypto";
|
|
2145
|
+
import { existsSync as existsSync7, readFileSync as readFileSync6, readdirSync as readdirSync5, writeFileSync as writeFileSync3 } from "fs";
|
|
1828
2146
|
init_logger();
|
|
2147
|
+
init_context_scan();
|
|
2148
|
+
function sha256OfFile2(path) {
|
|
2149
|
+
try {
|
|
2150
|
+
return createHash3("sha256").update(readFileSync6(path)).digest("hex");
|
|
2151
|
+
} catch {
|
|
2152
|
+
return "";
|
|
2153
|
+
}
|
|
2154
|
+
}
|
|
2155
|
+
function inputsDigest2(paths) {
|
|
2156
|
+
const combined = paths.slice().sort().map((p) => `${p}:${sha256OfFile2(p)}`).join("\n");
|
|
2157
|
+
return createHash3("sha256").update(combined).digest("hex");
|
|
2158
|
+
}
|
|
2159
|
+
function findContractFiles2(dir, found = []) {
|
|
2160
|
+
if (!existsSync7(dir))
|
|
2161
|
+
return found;
|
|
2162
|
+
for (const entry of readdirSync5(dir, { withFileTypes: true })) {
|
|
2163
|
+
const fullPath = join8(dir, entry.name);
|
|
2164
|
+
if (entry.isDirectory())
|
|
2165
|
+
findContractFiles2(fullPath, found);
|
|
2166
|
+
else if (entry.isFile() && entry.name.endsWith(".md") && entry.name !== "INDEX.md" && entry.name !== "CHANGELOG.md") {
|
|
2167
|
+
found.push(fullPath);
|
|
2168
|
+
}
|
|
2169
|
+
}
|
|
2170
|
+
return found;
|
|
2171
|
+
}
|
|
2172
|
+
function readIndexDigest(filePath) {
|
|
2173
|
+
if (!existsSync7(filePath))
|
|
2174
|
+
return null;
|
|
2175
|
+
const m = readFileSync6(filePath, "utf8").match(/^inputs-digest:\s*([a-f0-9]+)/m);
|
|
2176
|
+
return m ? m[1] : null;
|
|
2177
|
+
}
|
|
2178
|
+
async function ensureFreshContextIndexes(cwd) {
|
|
2179
|
+
const projectMap = join8(cwd, "specs", "context", "project-map.md");
|
|
2180
|
+
const contractsIndex = join8(cwd, "specs", "context", "contracts-index.md");
|
|
2181
|
+
const policyPath = join8(cwd, ".cdd", "context-policy.json");
|
|
2182
|
+
const policyInputs = [policyPath].filter(existsSync7);
|
|
2183
|
+
const contractFiles = findContractFiles2(join8(cwd, "contracts"));
|
|
2184
|
+
const wantProjectDigest = inputsDigest2(policyInputs);
|
|
2185
|
+
const wantContractsDigest = inputsDigest2(contractFiles);
|
|
2186
|
+
const haveProjectDigest = readIndexDigest(projectMap);
|
|
2187
|
+
const haveContractsDigest = readIndexDigest(contractsIndex);
|
|
2188
|
+
const needsScan = !existsSync7(projectMap) || !existsSync7(contractsIndex) || haveProjectDigest !== wantProjectDigest || haveContractsDigest !== wantContractsDigest;
|
|
2189
|
+
if (!needsScan)
|
|
2190
|
+
return;
|
|
2191
|
+
log.info("context indexes missing or stale \u2014 running cdd-kit context-scan\u2026");
|
|
2192
|
+
await contextScan();
|
|
2193
|
+
log.dim(" (skip with --skip-scan)");
|
|
2194
|
+
}
|
|
1829
2195
|
var REQUIRED_TEMPLATES = [
|
|
1830
2196
|
"change-request.md",
|
|
1831
2197
|
"change-classification.md",
|
|
@@ -1836,7 +2202,7 @@ var REQUIRED_TEMPLATES = [
|
|
|
1836
2202
|
];
|
|
1837
2203
|
function listOptional() {
|
|
1838
2204
|
try {
|
|
1839
|
-
const all =
|
|
2205
|
+
const all = readdirSync5(ASSET.specsTemplates).filter((f) => f.endsWith(".md"));
|
|
1840
2206
|
return all.filter((f) => !REQUIRED_TEMPLATES.includes(f));
|
|
1841
2207
|
} catch {
|
|
1842
2208
|
return [];
|
|
@@ -1866,8 +2232,8 @@ async function newChange(name, opts) {
|
|
|
1866
2232
|
}
|
|
1867
2233
|
}
|
|
1868
2234
|
const cwd = process.cwd();
|
|
1869
|
-
const changeDir =
|
|
1870
|
-
if (
|
|
2235
|
+
const changeDir = join8(cwd, "specs", "changes", name);
|
|
2236
|
+
if (existsSync7(changeDir)) {
|
|
1871
2237
|
if (opts.force) {
|
|
1872
2238
|
log.warn(`Forcing re-scaffold of existing change directory: ${changeDir}`);
|
|
1873
2239
|
log.warn("Existing files will NOT be deleted; only template files will be overwritten.");
|
|
@@ -1877,15 +2243,22 @@ async function newChange(name, opts) {
|
|
|
1877
2243
|
return;
|
|
1878
2244
|
}
|
|
1879
2245
|
}
|
|
2246
|
+
if (!opts.skipScan) {
|
|
2247
|
+
try {
|
|
2248
|
+
await ensureFreshContextIndexes(cwd);
|
|
2249
|
+
} catch (err) {
|
|
2250
|
+
log.warn(`context-scan failed: ${err.message}; continuing without fresh indexes`);
|
|
2251
|
+
}
|
|
2252
|
+
}
|
|
1880
2253
|
log.blank();
|
|
1881
2254
|
log.info(`Creating change scaffold: specs/changes/${name}`);
|
|
1882
2255
|
ensureDir(changeDir);
|
|
1883
2256
|
const templates = opts.all ? [...REQUIRED_TEMPLATES, ...listOptional()] : [...REQUIRED_TEMPLATES];
|
|
1884
2257
|
let written = 0;
|
|
1885
2258
|
for (const tmpl of templates) {
|
|
1886
|
-
const src =
|
|
1887
|
-
const dest =
|
|
1888
|
-
if (!
|
|
2259
|
+
const src = join8(ASSET.specsTemplates, tmpl);
|
|
2260
|
+
const dest = join8(changeDir, tmpl);
|
|
2261
|
+
if (!existsSync7(src)) {
|
|
1889
2262
|
log.warn(`Template not found, skipping: ${tmpl}`);
|
|
1890
2263
|
continue;
|
|
1891
2264
|
}
|
|
@@ -1894,11 +2267,11 @@ async function newChange(name, opts) {
|
|
|
1894
2267
|
written += 1;
|
|
1895
2268
|
}
|
|
1896
2269
|
if (dependencies.length > 0) {
|
|
1897
|
-
const tasksPath =
|
|
1898
|
-
if (
|
|
1899
|
-
const tasks =
|
|
2270
|
+
const tasksPath = join8(changeDir, "tasks.md");
|
|
2271
|
+
if (existsSync7(tasksPath)) {
|
|
2272
|
+
const tasks = readFileSync6(tasksPath, "utf8");
|
|
1900
2273
|
const nextTasks = tasks.replace(/^depends-on:\s*.*$/m, formatDependsOn(dependencies));
|
|
1901
|
-
|
|
2274
|
+
writeFileSync3(tasksPath, nextTasks, "utf8");
|
|
1902
2275
|
log.dim(`depends-on: ${dependencies.join(", ")}`);
|
|
1903
2276
|
}
|
|
1904
2277
|
}
|
|
@@ -1910,8 +2283,8 @@ async function newChange(name, opts) {
|
|
|
1910
2283
|
// src/commands/validate.ts
|
|
1911
2284
|
init_paths();
|
|
1912
2285
|
init_logger();
|
|
1913
|
-
import { join as
|
|
1914
|
-
import { existsSync as
|
|
2286
|
+
import { join as join9 } from "path";
|
|
2287
|
+
import { existsSync as existsSync8 } from "fs";
|
|
1915
2288
|
import { spawnSync } from "child_process";
|
|
1916
2289
|
var VALIDATORS = [
|
|
1917
2290
|
{
|
|
@@ -1944,15 +2317,15 @@ async function validate(opts) {
|
|
|
1944
2317
|
log.error(e instanceof Error ? e.message : String(e));
|
|
1945
2318
|
process.exit(1);
|
|
1946
2319
|
}
|
|
1947
|
-
const scriptsDir =
|
|
2320
|
+
const scriptsDir = join9(ASSET.skill, "scripts");
|
|
1948
2321
|
const runAll = !opts.contracts && !opts.env && !opts.ci && !opts.spec && !opts.versions;
|
|
1949
2322
|
log.blank();
|
|
1950
2323
|
let failed = false;
|
|
1951
2324
|
for (const v of VALIDATORS) {
|
|
1952
2325
|
if (!runAll && !opts[v.flag])
|
|
1953
2326
|
continue;
|
|
1954
|
-
const scriptPath =
|
|
1955
|
-
if (!
|
|
2327
|
+
const scriptPath = join9(scriptsDir, v.script);
|
|
2328
|
+
if (!existsSync8(scriptPath)) {
|
|
1956
2329
|
log.warn(`${v.label}: script not found, skipping (${v.script})`);
|
|
1957
2330
|
log.blank();
|
|
1958
2331
|
continue;
|
|
@@ -1968,8 +2341,8 @@ async function validate(opts) {
|
|
|
1968
2341
|
log.blank();
|
|
1969
2342
|
if (v.chain) {
|
|
1970
2343
|
for (const chained of v.chain) {
|
|
1971
|
-
const chainedPath =
|
|
1972
|
-
if (!
|
|
2344
|
+
const chainedPath = join9(scriptsDir, chained.script);
|
|
2345
|
+
if (!existsSync8(chainedPath)) {
|
|
1973
2346
|
log.warn(`${chained.label}: script not found, skipping (${chained.script})`);
|
|
1974
2347
|
log.blank();
|
|
1975
2348
|
continue;
|
|
@@ -1997,9 +2370,8 @@ async function validate(opts) {
|
|
|
1997
2370
|
|
|
1998
2371
|
// src/commands/gate.ts
|
|
1999
2372
|
init_logger();
|
|
2000
|
-
import { existsSync as
|
|
2001
|
-
import { join as
|
|
2002
|
-
import { spawnSync as spawnSync2 } from "child_process";
|
|
2373
|
+
import { existsSync as existsSync9, readFileSync as readFileSync7, readdirSync as readdirSync6 } from "fs";
|
|
2374
|
+
import { join as join10 } from "path";
|
|
2003
2375
|
var REQUIRED_FILES = [
|
|
2004
2376
|
"change-request.md",
|
|
2005
2377
|
"change-classification.md",
|
|
@@ -2080,11 +2452,11 @@ function loadContextPolicy(cwd) {
|
|
|
2080
2452
|
unknownFilesRead: "warn-for-legacy-fail-for-new"
|
|
2081
2453
|
}
|
|
2082
2454
|
};
|
|
2083
|
-
const policyPath =
|
|
2084
|
-
if (!
|
|
2455
|
+
const policyPath = join10(cwd, ".cdd", "context-policy.json");
|
|
2456
|
+
if (!existsSync9(policyPath))
|
|
2085
2457
|
return defaults;
|
|
2086
2458
|
try {
|
|
2087
|
-
const custom = JSON.parse(
|
|
2459
|
+
const custom = JSON.parse(readFileSync7(policyPath, "utf8"));
|
|
2088
2460
|
return {
|
|
2089
2461
|
...defaults,
|
|
2090
2462
|
...custom,
|
|
@@ -2097,53 +2469,219 @@ function loadContextPolicy(cwd) {
|
|
|
2097
2469
|
}
|
|
2098
2470
|
}
|
|
2099
2471
|
function isContextGovernedChange(changeDir) {
|
|
2100
|
-
const tasksPath =
|
|
2101
|
-
if (!
|
|
2472
|
+
const tasksPath = join10(changeDir, "tasks.md");
|
|
2473
|
+
if (!existsSync9(tasksPath))
|
|
2102
2474
|
return false;
|
|
2103
|
-
return /^context-governance:\s*v1\b/m.test(
|
|
2475
|
+
return /^context-governance:\s*v1\b/m.test(readFileSync7(tasksPath, "utf8"));
|
|
2476
|
+
}
|
|
2477
|
+
var KNOWN_FRONTMATTER_KEYS = /* @__PURE__ */ new Set([
|
|
2478
|
+
"change-id",
|
|
2479
|
+
"status",
|
|
2480
|
+
"tier",
|
|
2481
|
+
"archive-tasks",
|
|
2482
|
+
"context-governance",
|
|
2483
|
+
"depends-on",
|
|
2484
|
+
// Allowed but informational only:
|
|
2485
|
+
"token-budget",
|
|
2486
|
+
"created",
|
|
2487
|
+
"completed"
|
|
2488
|
+
]);
|
|
2489
|
+
var VALID_TASK_STATUSES = /* @__PURE__ */ new Set(["in-progress", "completed", "complete", "done", "gate-blocked", "abandoned", "needs-review"]);
|
|
2490
|
+
function lintFrontmatter(content, errors, warnings) {
|
|
2491
|
+
const fm = parseTaskFrontmatter(content);
|
|
2492
|
+
if (!fm["change-id"]) {
|
|
2493
|
+
errors.push("tasks.md frontmatter: missing required `change-id`");
|
|
2494
|
+
}
|
|
2495
|
+
if (!fm.status) {
|
|
2496
|
+
errors.push("tasks.md frontmatter: missing required `status`");
|
|
2497
|
+
} else if (!VALID_TASK_STATUSES.has(fm.status.toLowerCase())) {
|
|
2498
|
+
errors.push(`tasks.md frontmatter: invalid status \`${fm.status}\` (expected one of: ${[...VALID_TASK_STATUSES].join(", ")})`);
|
|
2499
|
+
}
|
|
2500
|
+
if (fm.tier !== void 0 && fm.tier !== "") {
|
|
2501
|
+
const n = parseInt(fm.tier, 10);
|
|
2502
|
+
if (Number.isNaN(n) || n < 0 || n > 5) {
|
|
2503
|
+
errors.push(`tasks.md frontmatter: invalid tier \`${fm.tier}\` (expected 0-5)`);
|
|
2504
|
+
}
|
|
2505
|
+
}
|
|
2506
|
+
for (const key of Object.keys(fm)) {
|
|
2507
|
+
if (!KNOWN_FRONTMATTER_KEYS.has(key)) {
|
|
2508
|
+
const lower = key.toLowerCase();
|
|
2509
|
+
const suggestion = KNOWN_FRONTMATTER_KEYS.has(lower) ? ` (did you mean \`${lower}\`?)` : "";
|
|
2510
|
+
warnings.push(`tasks.md frontmatter: unknown key \`${key}\`${suggestion}`);
|
|
2511
|
+
}
|
|
2512
|
+
}
|
|
2513
|
+
}
|
|
2514
|
+
function parseTaskFrontmatter(content) {
|
|
2515
|
+
const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
|
|
2516
|
+
if (!match)
|
|
2517
|
+
return {};
|
|
2518
|
+
const out = {};
|
|
2519
|
+
for (const line of match[1].split(/\r?\n/)) {
|
|
2520
|
+
const colon = line.indexOf(":");
|
|
2521
|
+
if (colon === -1)
|
|
2522
|
+
continue;
|
|
2523
|
+
const key = line.slice(0, colon).trim();
|
|
2524
|
+
if (!key)
|
|
2525
|
+
continue;
|
|
2526
|
+
out[key] = line.slice(colon + 1).trim();
|
|
2527
|
+
}
|
|
2528
|
+
return out;
|
|
2104
2529
|
}
|
|
2105
|
-
function
|
|
2106
|
-
|
|
2107
|
-
if (!lineMatch)
|
|
2530
|
+
function parseListField(raw) {
|
|
2531
|
+
if (!raw)
|
|
2108
2532
|
return [];
|
|
2109
|
-
const
|
|
2110
|
-
if (!
|
|
2533
|
+
const trimmed = raw.trim();
|
|
2534
|
+
if (!trimmed || trimmed === "[]")
|
|
2111
2535
|
return [];
|
|
2112
|
-
|
|
2113
|
-
|
|
2114
|
-
|
|
2115
|
-
|
|
2536
|
+
const inner = trimmed.startsWith("[") && trimmed.endsWith("]") ? trimmed.slice(1, -1) : trimmed;
|
|
2537
|
+
return inner.split(",").map((item) => item.trim().replace(/^["']|["']$/g, "")).filter(Boolean);
|
|
2538
|
+
}
|
|
2539
|
+
function parseDependsOn2(content) {
|
|
2540
|
+
return parseListField(parseTaskFrontmatter(content)["depends-on"]);
|
|
2116
2541
|
}
|
|
2117
2542
|
function parseTaskStatus(content) {
|
|
2118
|
-
const
|
|
2119
|
-
return
|
|
2543
|
+
const fm = parseTaskFrontmatter(content);
|
|
2544
|
+
return (fm.status ?? "in-progress").toLowerCase();
|
|
2545
|
+
}
|
|
2546
|
+
function resolveTier(changeDir) {
|
|
2547
|
+
const classifPath = join10(changeDir, "change-classification.md");
|
|
2548
|
+
const classificationPresent = existsSync9(classifPath);
|
|
2549
|
+
const classificationText = classificationPresent ? readFileSync7(classifPath, "utf8") : "";
|
|
2550
|
+
const classificationHasLooseMarker = classificationPresent && TIER_PATTERN.test(classificationText);
|
|
2551
|
+
const tasksPath = join10(changeDir, "tasks.md");
|
|
2552
|
+
if (existsSync9(tasksPath)) {
|
|
2553
|
+
const fm = parseTaskFrontmatter(readFileSync7(tasksPath, "utf8"));
|
|
2554
|
+
const raw = fm.tier;
|
|
2555
|
+
if (raw && raw !== "") {
|
|
2556
|
+
const n = parseInt(raw, 10);
|
|
2557
|
+
if (!Number.isNaN(n) && n >= 0 && n <= 5) {
|
|
2558
|
+
return { tier: n, source: "tasks-frontmatter", classificationPresent, classificationHasLooseMarker };
|
|
2559
|
+
}
|
|
2560
|
+
}
|
|
2561
|
+
}
|
|
2562
|
+
if (classificationPresent) {
|
|
2563
|
+
const structured = classificationText.match(/^## Tier\s*\n\s*-\s*(\d)\s*$/m);
|
|
2564
|
+
if (structured) {
|
|
2565
|
+
const n = parseInt(structured[1], 10);
|
|
2566
|
+
if (!Number.isNaN(n) && n >= 0 && n <= 5) {
|
|
2567
|
+
return { tier: n, source: "classification-structured", classificationPresent, classificationHasLooseMarker };
|
|
2568
|
+
}
|
|
2569
|
+
}
|
|
2570
|
+
const bold = classificationText.match(/\*\*Tier:\*\*\s*Tier\s*(\d)\b/i);
|
|
2571
|
+
if (bold) {
|
|
2572
|
+
const n = parseInt(bold[1], 10);
|
|
2573
|
+
if (!Number.isNaN(n) && n >= 0 && n <= 5) {
|
|
2574
|
+
return { tier: n, source: "classification-bold", classificationPresent, classificationHasLooseMarker };
|
|
2575
|
+
}
|
|
2576
|
+
}
|
|
2577
|
+
}
|
|
2578
|
+
return { tier: null, source: "none", classificationPresent, classificationHasLooseMarker };
|
|
2579
|
+
}
|
|
2580
|
+
var DEFAULT_ARCHIVE_TASKS = ["7.1", "7.2"];
|
|
2581
|
+
function getArchiveTaskIds(content) {
|
|
2582
|
+
const fm = parseTaskFrontmatter(content);
|
|
2583
|
+
const parsed = parseListField(fm["archive-tasks"]);
|
|
2584
|
+
return parsed.length > 0 ? parsed : DEFAULT_ARCHIVE_TASKS;
|
|
2585
|
+
}
|
|
2586
|
+
function enforceTierRequirements(changeDir, agentLogDir, errors, warnings) {
|
|
2587
|
+
const resolution = resolveTier(changeDir);
|
|
2588
|
+
if (resolution.tier === null) {
|
|
2589
|
+
if (resolution.classificationPresent && !resolution.classificationHasLooseMarker) {
|
|
2590
|
+
errors.push(
|
|
2591
|
+
"change-classification.md: missing tier marker. Set `tier: <0-5>` in tasks.md frontmatter (preferred) or include `## Tier\\n- N` in change-classification.md."
|
|
2592
|
+
);
|
|
2593
|
+
}
|
|
2594
|
+
return;
|
|
2595
|
+
}
|
|
2596
|
+
if (resolution.source === "classification-bold") {
|
|
2597
|
+
warnings.push(
|
|
2598
|
+
"tier marker is bold-text only (legacy format); set `tier: <0-5>` in tasks.md frontmatter so tier-specific agent requirements are enforced."
|
|
2599
|
+
);
|
|
2600
|
+
return;
|
|
2601
|
+
}
|
|
2602
|
+
const tier = resolution.tier;
|
|
2603
|
+
const agentLogFiles = agentLogDir && existsSync9(agentLogDir) ? readdirSync6(agentLogDir).map((f) => f.replace(".md", "")) : [];
|
|
2604
|
+
if (tier <= 1) {
|
|
2605
|
+
for (const required of ["e2e-resilience-engineer", "monkey-test-engineer", "stress-soak-engineer"]) {
|
|
2606
|
+
if (!agentLogFiles.includes(required)) {
|
|
2607
|
+
errors.push(`Tier ${tier} change requires agent-log/${required}.md (high-risk change \u2014 E2E/monkey/stress testing mandatory)`);
|
|
2608
|
+
}
|
|
2609
|
+
}
|
|
2610
|
+
}
|
|
2611
|
+
if (tier <= 3) {
|
|
2612
|
+
for (const required of ["contract-reviewer", "qa-reviewer"]) {
|
|
2613
|
+
if (!agentLogFiles.includes(required)) {
|
|
2614
|
+
errors.push(`Tier ${tier} change requires agent-log/${required}.md`);
|
|
2615
|
+
}
|
|
2616
|
+
}
|
|
2617
|
+
}
|
|
2618
|
+
if (resolution.source === "tasks-frontmatter" && resolution.classificationPresent) {
|
|
2619
|
+
const text = readFileSync7(join10(changeDir, "change-classification.md"), "utf8");
|
|
2620
|
+
const structured = text.match(/^## Tier\s*\n\s*-\s*(\d)\s*$/m);
|
|
2621
|
+
const bold = text.match(/\*\*Tier:\*\*\s*Tier\s*(\d)\b/i);
|
|
2622
|
+
const classifTier = structured ? parseInt(structured[1], 10) : bold ? parseInt(bold[1], 10) : NaN;
|
|
2623
|
+
if (!Number.isNaN(classifTier) && classifTier !== tier) {
|
|
2624
|
+
warnings.push(
|
|
2625
|
+
`tier mismatch: tasks.md frontmatter says ${tier}, change-classification.md says ${classifTier} (frontmatter wins; reconcile classification).`
|
|
2626
|
+
);
|
|
2627
|
+
}
|
|
2628
|
+
}
|
|
2120
2629
|
}
|
|
2121
2630
|
function isArchivedChange(cwd, changeId) {
|
|
2122
|
-
const archiveRoot =
|
|
2123
|
-
if (!
|
|
2631
|
+
const archiveRoot = join10(cwd, "specs", "archive");
|
|
2632
|
+
if (!existsSync9(archiveRoot))
|
|
2124
2633
|
return false;
|
|
2125
|
-
const years =
|
|
2126
|
-
return years.some((year) =>
|
|
2634
|
+
const years = readdirSync6(archiveRoot, { withFileTypes: true }).filter((d) => d.isDirectory());
|
|
2635
|
+
return years.some((year) => existsSync9(join10(archiveRoot, year.name, changeId)));
|
|
2636
|
+
}
|
|
2637
|
+
function detectDependencyCycle(cwd, startChangeId) {
|
|
2638
|
+
const visited = /* @__PURE__ */ new Set();
|
|
2639
|
+
const stack = [];
|
|
2640
|
+
function visit(id) {
|
|
2641
|
+
if (stack.includes(id)) {
|
|
2642
|
+
return [...stack.slice(stack.indexOf(id)), id];
|
|
2643
|
+
}
|
|
2644
|
+
if (visited.has(id))
|
|
2645
|
+
return null;
|
|
2646
|
+
visited.add(id);
|
|
2647
|
+
stack.push(id);
|
|
2648
|
+
const tasksPath = join10(cwd, "specs", "changes", id, "tasks.md");
|
|
2649
|
+
if (existsSync9(tasksPath)) {
|
|
2650
|
+
const deps = parseDependsOn2(readFileSync7(tasksPath, "utf8"));
|
|
2651
|
+
for (const dep of deps) {
|
|
2652
|
+
const found = visit(dep);
|
|
2653
|
+
if (found)
|
|
2654
|
+
return found;
|
|
2655
|
+
}
|
|
2656
|
+
}
|
|
2657
|
+
stack.pop();
|
|
2658
|
+
return null;
|
|
2659
|
+
}
|
|
2660
|
+
return visit(startChangeId);
|
|
2127
2661
|
}
|
|
2128
2662
|
function validateDependencies(cwd, changeId, changeDir) {
|
|
2129
|
-
const tasksPath =
|
|
2130
|
-
if (!
|
|
2663
|
+
const tasksPath = join10(changeDir, "tasks.md");
|
|
2664
|
+
if (!existsSync9(tasksPath))
|
|
2131
2665
|
return [];
|
|
2132
|
-
const dependencies = parseDependsOn2(
|
|
2666
|
+
const dependencies = parseDependsOn2(readFileSync7(tasksPath, "utf8"));
|
|
2133
2667
|
const errors = [];
|
|
2668
|
+
const cycle = detectDependencyCycle(cwd, changeId);
|
|
2669
|
+
if (cycle) {
|
|
2670
|
+
errors.push(`depends-on cycle detected: ${cycle.join(" \u2192 ")}`);
|
|
2671
|
+
}
|
|
2134
2672
|
for (const dep of dependencies) {
|
|
2135
2673
|
if (dep === changeId) {
|
|
2136
2674
|
errors.push(`tasks.md: change cannot depend on itself (${dep})`);
|
|
2137
2675
|
continue;
|
|
2138
2676
|
}
|
|
2139
|
-
const upstreamDir =
|
|
2140
|
-
if (
|
|
2141
|
-
const upstreamTasks =
|
|
2142
|
-
if (!
|
|
2677
|
+
const upstreamDir = join10(cwd, "specs", "changes", dep);
|
|
2678
|
+
if (existsSync9(upstreamDir)) {
|
|
2679
|
+
const upstreamTasks = join10(upstreamDir, "tasks.md");
|
|
2680
|
+
if (!existsSync9(upstreamTasks)) {
|
|
2143
2681
|
errors.push(`dependency ${dep}: missing tasks.md`);
|
|
2144
2682
|
continue;
|
|
2145
2683
|
}
|
|
2146
|
-
const status = parseTaskStatus(
|
|
2684
|
+
const status = parseTaskStatus(readFileSync7(upstreamTasks, "utf8"));
|
|
2147
2685
|
if (!["complete", "completed", "done"].includes(status)) {
|
|
2148
2686
|
errors.push(`dependency ${dep}: upstream change is not completed (status: ${status})`);
|
|
2149
2687
|
}
|
|
@@ -2200,9 +2738,10 @@ function parseFilesRead(content) {
|
|
|
2200
2738
|
}
|
|
2201
2739
|
async function gate(changeId, opts = {}) {
|
|
2202
2740
|
const strict = opts.strict ?? false;
|
|
2741
|
+
const lax = opts.lax ?? false;
|
|
2203
2742
|
const cwd = process.cwd();
|
|
2204
|
-
const changeDir =
|
|
2205
|
-
if (!
|
|
2743
|
+
const changeDir = join10(cwd, "specs", "changes", changeId);
|
|
2744
|
+
if (!existsSync9(changeDir)) {
|
|
2206
2745
|
log.error(`change not found: ${changeId} (looked in ${changeDir})`);
|
|
2207
2746
|
process.exit(1);
|
|
2208
2747
|
}
|
|
@@ -2210,13 +2749,13 @@ async function gate(changeId, opts = {}) {
|
|
|
2210
2749
|
const warnings = [];
|
|
2211
2750
|
const contextPolicy = loadContextPolicy(cwd);
|
|
2212
2751
|
const isNewChange = isContextGovernedChange(changeDir);
|
|
2213
|
-
const manifestPath =
|
|
2214
|
-
const hasManifest =
|
|
2752
|
+
const manifestPath = join10(changeDir, "context-manifest.md");
|
|
2753
|
+
const hasManifest = existsSync9(manifestPath);
|
|
2215
2754
|
let allowedPaths = [];
|
|
2216
2755
|
let approvedExpansions = [];
|
|
2217
2756
|
errors.push(...validateDependencies(cwd, changeId, changeDir));
|
|
2218
2757
|
if (hasManifest) {
|
|
2219
|
-
const manifest = parseContextManifest(
|
|
2758
|
+
const manifest = parseContextManifest(readFileSync7(manifestPath, "utf8"));
|
|
2220
2759
|
allowedPaths = manifest.allowedPaths;
|
|
2221
2760
|
approvedExpansions = manifest.approvedExpansions;
|
|
2222
2761
|
if (manifest.pendingExpansions > 0) {
|
|
@@ -2234,7 +2773,7 @@ async function gate(changeId, opts = {}) {
|
|
|
2234
2773
|
}
|
|
2235
2774
|
continue;
|
|
2236
2775
|
}
|
|
2237
|
-
if (!
|
|
2776
|
+
if (!existsSync9(join10(changeDir, f))) {
|
|
2238
2777
|
errors.push(`missing required artifact: ${f}`);
|
|
2239
2778
|
}
|
|
2240
2779
|
}
|
|
@@ -2242,24 +2781,31 @@ async function gate(changeId, opts = {}) {
|
|
|
2242
2781
|
for (const f of REQUIRED_FILES) {
|
|
2243
2782
|
if (f === "context-manifest.md" && !hasManifest)
|
|
2244
2783
|
continue;
|
|
2245
|
-
const content =
|
|
2784
|
+
const content = readFileSync7(join10(changeDir, f), "utf8");
|
|
2246
2785
|
const minChars = MIN_CHARS[f] ?? 100;
|
|
2247
2786
|
if (meaningfulChars(content) < minChars) {
|
|
2248
2787
|
errors.push(`${f}: appears to be a stub (< ${minChars} meaningful chars)`);
|
|
2249
2788
|
}
|
|
2250
2789
|
}
|
|
2251
|
-
const classifPath =
|
|
2252
|
-
|
|
2253
|
-
|
|
2254
|
-
|
|
2255
|
-
|
|
2256
|
-
|
|
2257
|
-
|
|
2258
|
-
|
|
2259
|
-
|
|
2260
|
-
|
|
2261
|
-
const
|
|
2262
|
-
const
|
|
2790
|
+
const classifPath = join10(changeDir, "change-classification.md");
|
|
2791
|
+
const tierResolution = resolveTier(changeDir);
|
|
2792
|
+
if (tierResolution.tier === null && existsSync9(classifPath) && !tierResolution.classificationHasLooseMarker) {
|
|
2793
|
+
errors.push("change-classification.md: missing tier/risk marker (set tier in tasks.md frontmatter, or include Tier 0-5 / low|medium|high|critical in change-classification.md)");
|
|
2794
|
+
}
|
|
2795
|
+
}
|
|
2796
|
+
const tasksPath = join10(changeDir, "tasks.md");
|
|
2797
|
+
if (existsSync9(tasksPath)) {
|
|
2798
|
+
const tasksContent = readFileSync7(tasksPath, "utf8");
|
|
2799
|
+
lintFrontmatter(tasksContent, errors, warnings);
|
|
2800
|
+
const archiveTaskIds = new Set(getArchiveTaskIds(tasksContent));
|
|
2801
|
+
const pendingMatches = tasksContent.match(/^\s*-\s*\[ \]\s+([\d.]+)?[^\n]*/gm) || [];
|
|
2802
|
+
const nonArchivePending = pendingMatches.filter((line) => {
|
|
2803
|
+
const idMatch = line.match(/\[ \]\s+([\d.]+)/);
|
|
2804
|
+
const id = idMatch?.[1];
|
|
2805
|
+
if (!id)
|
|
2806
|
+
return true;
|
|
2807
|
+
return !archiveTaskIds.has(id);
|
|
2808
|
+
}).length;
|
|
2263
2809
|
if (nonArchivePending > 0) {
|
|
2264
2810
|
if (strict) {
|
|
2265
2811
|
errors.push(`${nonArchivePending} task(s) still pending (use [-] for N/A items, [x] for done). Run gate without --strict during development.`);
|
|
@@ -2268,11 +2814,11 @@ async function gate(changeId, opts = {}) {
|
|
|
2268
2814
|
}
|
|
2269
2815
|
}
|
|
2270
2816
|
}
|
|
2271
|
-
const agentLogDir =
|
|
2272
|
-
if (
|
|
2273
|
-
const logFiles =
|
|
2817
|
+
const agentLogDir = join10(changeDir, "agent-log");
|
|
2818
|
+
if (existsSync9(agentLogDir)) {
|
|
2819
|
+
const logFiles = readdirSync6(agentLogDir).filter((f) => f.endsWith(".md"));
|
|
2274
2820
|
for (const f of logFiles) {
|
|
2275
|
-
const content =
|
|
2821
|
+
const content = readFileSync7(join10(agentLogDir, f), "utf8");
|
|
2276
2822
|
const filesRead = parseFilesRead(content);
|
|
2277
2823
|
if (!filesRead.present) {
|
|
2278
2824
|
if (contextPolicy.audit.requireFilesRead) {
|
|
@@ -2295,6 +2841,27 @@ async function gate(changeId, opts = {}) {
|
|
|
2295
2841
|
errors.push(`agent-log/${f}: read unauthorized path -> ${pathRead} (not in allowed paths or approved expansions)`);
|
|
2296
2842
|
}
|
|
2297
2843
|
}
|
|
2844
|
+
const runtimeLog = join10(cwd, ".cdd", "runtime", `${changeId}-files-read.jsonl`);
|
|
2845
|
+
if (existsSync9(runtimeLog)) {
|
|
2846
|
+
const runtimePaths = readFileSync7(runtimeLog, "utf8").split("\n").filter(Boolean).map((line) => {
|
|
2847
|
+
try {
|
|
2848
|
+
return JSON.parse(line).path;
|
|
2849
|
+
} catch {
|
|
2850
|
+
return void 0;
|
|
2851
|
+
}
|
|
2852
|
+
}).filter((p) => Boolean(p)).map((p) => p.replace(/\\/g, "/").replace(/^\.\//, ""));
|
|
2853
|
+
const declared = new Set(filesRead.files);
|
|
2854
|
+
const undeclared = runtimePaths.filter((p) => !declared.has(p));
|
|
2855
|
+
if (undeclared.length > 0) {
|
|
2856
|
+
const sample = undeclared.slice(0, 5).join(", ");
|
|
2857
|
+
const more = undeclared.length > 5 ? ` (+${undeclared.length - 5} more)` : "";
|
|
2858
|
+
const msg = `agent-log/${f}: runtime log shows ${undeclared.length} read(s) not declared in files-read: ${sample}${more}`;
|
|
2859
|
+
if (strict)
|
|
2860
|
+
errors.push(msg);
|
|
2861
|
+
else
|
|
2862
|
+
warnings.push(msg);
|
|
2863
|
+
}
|
|
2864
|
+
}
|
|
2298
2865
|
}
|
|
2299
2866
|
const statusMatch = content.match(/^\s*-\s*status:\s*(complete|needs-review|blocked)\s*$/m);
|
|
2300
2867
|
if (!statusMatch) {
|
|
@@ -2308,7 +2875,7 @@ async function gate(changeId, opts = {}) {
|
|
|
2308
2875
|
errors.push(`agent-log/${f}: status=blocked requires concrete "next-action:" line (>= 10 chars, not "none")`);
|
|
2309
2876
|
}
|
|
2310
2877
|
}
|
|
2311
|
-
if (
|
|
2878
|
+
if (!lax) {
|
|
2312
2879
|
const artifactsMatch = content.match(/- artifacts:([\s\S]*?)(?:\n- |\n#|$)/);
|
|
2313
2880
|
if (artifactsMatch) {
|
|
2314
2881
|
const artifactLines = artifactsMatch[1].split("\n").filter((l) => l.trim().startsWith("-"));
|
|
@@ -2316,8 +2883,8 @@ async function gate(changeId, opts = {}) {
|
|
|
2316
2883
|
const pointer = line.replace(/^\s*-\s*[\w-]+:\s*/, "").trim();
|
|
2317
2884
|
const pathPart = pointer.split(":")[0];
|
|
2318
2885
|
if (pathPart.includes("/") && !pointer.startsWith("http")) {
|
|
2319
|
-
const abs =
|
|
2320
|
-
if (!
|
|
2886
|
+
const abs = join10(cwd, pathPart);
|
|
2887
|
+
if (!existsSync9(abs)) {
|
|
2321
2888
|
errors.push(`agent-log/${f}: artifact pointer not found: ${pathPart}`);
|
|
2322
2889
|
}
|
|
2323
2890
|
}
|
|
@@ -2325,48 +2892,9 @@ async function gate(changeId, opts = {}) {
|
|
|
2325
2892
|
}
|
|
2326
2893
|
}
|
|
2327
2894
|
}
|
|
2328
|
-
|
|
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
|
-
}
|
|
2895
|
+
enforceTierRequirements(changeDir, agentLogDir, errors, warnings);
|
|
2351
2896
|
} else {
|
|
2352
|
-
|
|
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
|
-
}
|
|
2897
|
+
enforceTierRequirements(changeDir, null, errors, warnings);
|
|
2370
2898
|
}
|
|
2371
2899
|
for (const w of warnings) {
|
|
2372
2900
|
log.warn(` ${w}`);
|
|
@@ -2379,12 +2907,10 @@ async function gate(changeId, opts = {}) {
|
|
|
2379
2907
|
process.exit(1);
|
|
2380
2908
|
}
|
|
2381
2909
|
log.info(`gate: running contract validators for ${changeId}\u2026`);
|
|
2382
|
-
|
|
2383
|
-
|
|
2384
|
-
|
|
2385
|
-
|
|
2386
|
-
if (r.status !== 0) {
|
|
2387
|
-
log.error(`gate failed for change: ${changeId} (validators returned non-zero)`);
|
|
2910
|
+
try {
|
|
2911
|
+
await validate({ contracts: true, env: true, ci: true, spec: false, versions: true });
|
|
2912
|
+
} catch (err) {
|
|
2913
|
+
log.error(`gate failed for change: ${changeId} (validators threw): ${err.message}`);
|
|
2388
2914
|
process.exit(1);
|
|
2389
2915
|
}
|
|
2390
2916
|
for (const w of warnings) {
|
|
@@ -2396,26 +2922,26 @@ async function gate(changeId, opts = {}) {
|
|
|
2396
2922
|
// src/commands/install-hooks.ts
|
|
2397
2923
|
init_paths();
|
|
2398
2924
|
init_logger();
|
|
2399
|
-
import { existsSync as
|
|
2400
|
-
import { join as
|
|
2925
|
+
import { existsSync as existsSync10, readFileSync as readFileSync8, writeFileSync as writeFileSync4, chmodSync, mkdirSync as mkdirSync4 } from "fs";
|
|
2926
|
+
import { join as join11 } from "path";
|
|
2401
2927
|
var START_MARKER = "# cdd-kit-managed-block-start";
|
|
2402
2928
|
var END_MARKER = "# cdd-kit-managed-block-end";
|
|
2403
2929
|
async function installHooks() {
|
|
2404
2930
|
const cwd = process.cwd();
|
|
2405
|
-
const gitDir =
|
|
2406
|
-
if (!
|
|
2931
|
+
const gitDir = join11(cwd, ".git");
|
|
2932
|
+
if (!existsSync10(gitDir)) {
|
|
2407
2933
|
log.error("not a git repository (no .git/ found in cwd)");
|
|
2408
2934
|
process.exit(1);
|
|
2409
2935
|
}
|
|
2410
|
-
const hooksDir =
|
|
2411
|
-
|
|
2412
|
-
const dest =
|
|
2413
|
-
const ourHook =
|
|
2936
|
+
const hooksDir = join11(gitDir, "hooks");
|
|
2937
|
+
mkdirSync4(hooksDir, { recursive: true });
|
|
2938
|
+
const dest = join11(hooksDir, "pre-commit");
|
|
2939
|
+
const ourHook = readFileSync8(join11(ASSET.hooks, "pre-commit"), "utf8");
|
|
2414
2940
|
let final;
|
|
2415
|
-
if (!
|
|
2941
|
+
if (!existsSync10(dest)) {
|
|
2416
2942
|
final = ourHook;
|
|
2417
2943
|
} else {
|
|
2418
|
-
const existing =
|
|
2944
|
+
const existing = readFileSync8(dest, "utf8");
|
|
2419
2945
|
const startIdx = existing.indexOf(START_MARKER);
|
|
2420
2946
|
const endIdx = existing.indexOf(END_MARKER);
|
|
2421
2947
|
if (startIdx >= 0 && endIdx > startIdx) {
|
|
@@ -2439,7 +2965,7 @@ async function installHooks() {
|
|
|
2439
2965
|
}
|
|
2440
2966
|
}
|
|
2441
2967
|
}
|
|
2442
|
-
|
|
2968
|
+
writeFileSync4(dest, final, "utf8");
|
|
2443
2969
|
try {
|
|
2444
2970
|
chmodSync(dest, 493);
|
|
2445
2971
|
} catch {
|
|
@@ -2450,7 +2976,7 @@ async function installHooks() {
|
|
|
2450
2976
|
|
|
2451
2977
|
// src/cli/index.ts
|
|
2452
2978
|
var __dirname2 = dirname5(fileURLToPath2(import.meta.url));
|
|
2453
|
-
var pkg = JSON.parse(
|
|
2979
|
+
var pkg = JSON.parse(readFileSync16(join19(__dirname2, "..", "..", "package.json"), "utf8"));
|
|
2454
2980
|
var program = new Command();
|
|
2455
2981
|
program.name("cdd-kit").description("Contract-Driven Delivery Kit CLI").version(pkg.version);
|
|
2456
2982
|
program.command("init").description(
|
|
@@ -2464,9 +2990,9 @@ program.command("init").description(
|
|
|
2464
2990
|
})
|
|
2465
2991
|
);
|
|
2466
2992
|
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) => {
|
|
2993
|
+
program.command("doctor").description("Inspect cdd-kit repo health, provider guidance, and context index freshness").option("--strict", "Treat warnings as errors", false).option("--json", "Print a machine-readable health report", false).option("--provider <provider>", "Provider adapter to inspect: auto, claude, codex, or both", "auto").option("--fix", "Auto-resolve safe warnings (stale context indexes, missing role bindings)", false).action(async (opts) => {
|
|
2468
2994
|
const { doctor: doctor2 } = await Promise.resolve().then(() => (init_doctor(), doctor_exports));
|
|
2469
|
-
await doctor2({ strict: opts.strict, json: opts.json, provider: opts.provider });
|
|
2995
|
+
await doctor2({ strict: opts.strict, json: opts.json, provider: opts.provider, fix: opts.fix });
|
|
2470
2996
|
});
|
|
2471
2997
|
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
2998
|
const { upgrade: upgrade2 } = await Promise.resolve().then(() => (init_upgrade(), upgrade_exports));
|
|
@@ -2477,8 +3003,8 @@ program.command("upgrade").description("Add missing cdd-kit repo-level files wit
|
|
|
2477
3003
|
provider: opts.provider
|
|
2478
3004
|
});
|
|
2479
3005
|
});
|
|
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 })
|
|
3006
|
+
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").option("--skip-scan", "Skip the auto context-scan when indexes are stale (advanced)", false).action(
|
|
3007
|
+
(name, opts) => newChange(name, { all: opts.all, force: opts.force, dependsOn: opts.dependsOn, skipScan: opts.skipScan })
|
|
2482
3008
|
);
|
|
2483
3009
|
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(
|
|
2484
3010
|
(opts) => validate({
|
|
@@ -2489,8 +3015,8 @@ program.command("validate").description("Run validation scripts (defaults to all
|
|
|
2489
3015
|
versions: opts.versions
|
|
2490
3016
|
})
|
|
2491
3017
|
);
|
|
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
|
|
2493
|
-
await gate(id, { strict: opts.strict });
|
|
3018
|
+
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 treat runtime/declared files-read drift as errors", false).option("--lax", "Skip artifact-pointer existence check (for legacy repos with stale logs)", false).action(async (id, opts) => {
|
|
3019
|
+
await gate(id, { strict: opts.strict, lax: opts.lax });
|
|
2494
3020
|
});
|
|
2495
3021
|
program.command("archive <change-id>").description("Move a completed change from specs/changes/ to specs/archive/<year>/").action(async (changeId) => {
|
|
2496
3022
|
const { archive: archive2 } = await Promise.resolve().then(() => (init_archive(), archive_exports));
|
|
@@ -2500,9 +3026,14 @@ program.command("abandon <change-id>").description("Mark a change as abandoned (
|
|
|
2500
3026
|
const { abandon: abandon2 } = await Promise.resolve().then(() => (init_abandon(), abandon_exports));
|
|
2501
3027
|
await abandon2(changeId, opts);
|
|
2502
3028
|
});
|
|
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 = {}) => {
|
|
3029
|
+
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).option("--no-backup", "Skip the per-session backup at .cdd/migrate-backup/<stamp>/ (not recommended)").action(async (changeId, opts = {}) => {
|
|
2504
3030
|
const { migrate: migrate2 } = await Promise.resolve().then(() => (init_migrate(), migrate_exports));
|
|
2505
|
-
await migrate2(changeId,
|
|
3031
|
+
await migrate2(changeId, {
|
|
3032
|
+
all: opts.all,
|
|
3033
|
+
dryRun: opts.dryRun,
|
|
3034
|
+
enableContextGovernance: opts.enableContextGovernance,
|
|
3035
|
+
noBackup: opts.backup === false
|
|
3036
|
+
});
|
|
2506
3037
|
});
|
|
2507
3038
|
program.command("list").description("List active changes in specs/changes/").action(async () => {
|
|
2508
3039
|
const { listChanges: listChanges2 } = await Promise.resolve().then(() => (init_list_changes(), list_changes_exports));
|
|
@@ -2524,22 +3055,46 @@ program.command("detect-stack").description("Detect the project tech stack and p
|
|
|
2524
3055
|
);
|
|
2525
3056
|
}
|
|
2526
3057
|
});
|
|
2527
|
-
program.command("context-scan").description("Deterministically scan project context and generate specs/context maps").action(async () => {
|
|
3058
|
+
program.command("context-scan").description("Deterministically scan project context and generate specs/context maps").option("--surface <path>", "Limit project-map tree to a sub-directory (e.g. --surface src/server)").action(async (opts) => {
|
|
2528
3059
|
const { contextScan: contextScan2 } = await Promise.resolve().then(() => (init_context_scan(), context_scan_exports));
|
|
2529
|
-
await contextScan2();
|
|
3060
|
+
await contextScan2({ surface: opts.surface });
|
|
2530
3061
|
});
|
|
2531
3062
|
var context = program.command("context").description("Manage context governance manifests");
|
|
2532
3063
|
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
3064
|
const { requestContextExpansion: requestContextExpansion2 } = await Promise.resolve().then(() => (init_context(), context_exports));
|
|
2534
3065
|
await requestContextExpansion2(changeId, requestId, opts.path, opts.reason);
|
|
2535
3066
|
});
|
|
2536
|
-
context.command("approve <change-id>
|
|
2537
|
-
const { approveContextExpansion: approveContextExpansion2 } = await Promise.resolve().then(() => (init_context(), context_exports));
|
|
2538
|
-
|
|
3067
|
+
context.command("approve <change-id> [request-id]").description("Approve a pending Context Expansion Request (or all with --all-pending)").option("--all-pending", "Approve every pending Context Expansion Request for this change", false).action(async (changeId, requestId, opts) => {
|
|
3068
|
+
const { approveContextExpansion: approveContextExpansion2, approveAllPending: approveAllPending2 } = await Promise.resolve().then(() => (init_context(), context_exports));
|
|
3069
|
+
if (opts.allPending) {
|
|
3070
|
+
if (requestId) {
|
|
3071
|
+
console.error("--all-pending cannot be combined with a request-id");
|
|
3072
|
+
process.exit(1);
|
|
3073
|
+
}
|
|
3074
|
+
await approveAllPending2(changeId);
|
|
3075
|
+
} else {
|
|
3076
|
+
if (!requestId) {
|
|
3077
|
+
console.error("request-id is required (or pass --all-pending)");
|
|
3078
|
+
process.exit(1);
|
|
3079
|
+
}
|
|
3080
|
+
await approveContextExpansion2(changeId, requestId);
|
|
3081
|
+
}
|
|
2539
3082
|
});
|
|
2540
|
-
context.command("reject <change-id>
|
|
2541
|
-
const { rejectContextExpansion: rejectContextExpansion2 } = await Promise.resolve().then(() => (init_context(), context_exports));
|
|
2542
|
-
|
|
3083
|
+
context.command("reject <change-id> [request-id]").description("Reject a pending Context Expansion Request (or all with --all-pending)").option("--all-pending", "Reject every pending Context Expansion Request for this change", false).action(async (changeId, requestId, opts) => {
|
|
3084
|
+
const { rejectContextExpansion: rejectContextExpansion2, rejectAllPending: rejectAllPending2 } = await Promise.resolve().then(() => (init_context(), context_exports));
|
|
3085
|
+
if (opts.allPending) {
|
|
3086
|
+
if (requestId) {
|
|
3087
|
+
console.error("--all-pending cannot be combined with a request-id");
|
|
3088
|
+
process.exit(1);
|
|
3089
|
+
}
|
|
3090
|
+
await rejectAllPending2(changeId);
|
|
3091
|
+
} else {
|
|
3092
|
+
if (!requestId) {
|
|
3093
|
+
console.error("request-id is required (or pass --all-pending)");
|
|
3094
|
+
process.exit(1);
|
|
3095
|
+
}
|
|
3096
|
+
await rejectContextExpansion2(changeId, requestId);
|
|
3097
|
+
}
|
|
2543
3098
|
});
|
|
2544
3099
|
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
3100
|
const { listContextExpansions: listContextExpansions2 } = await Promise.resolve().then(() => (init_context(), context_exports));
|