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