meto-cli 0.9.3 → 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +40 -7
- package/dist/cli/audit/blueprint.d.ts +53 -0
- package/dist/cli/audit/blueprint.d.ts.map +1 -0
- package/dist/cli/audit/blueprint.js +386 -0
- package/dist/cli/audit/blueprint.js.map +1 -0
- package/dist/cli/audit/detect-stack.d.ts +31 -0
- package/dist/cli/audit/detect-stack.d.ts.map +1 -0
- package/dist/cli/audit/detect-stack.js +154 -0
- package/dist/cli/audit/detect-stack.js.map +1 -0
- package/dist/cli/audit/fixer.d.ts +75 -0
- package/dist/cli/audit/fixer.d.ts.map +1 -0
- package/dist/cli/audit/fixer.js +802 -0
- package/dist/cli/audit/fixer.js.map +1 -0
- package/dist/cli/audit/index.d.ts +24 -0
- package/dist/cli/audit/index.d.ts.map +1 -0
- package/dist/cli/audit/index.js +212 -0
- package/dist/cli/audit/index.js.map +1 -0
- package/dist/cli/audit/reporter.d.ts +42 -0
- package/dist/cli/audit/reporter.d.ts.map +1 -0
- package/dist/cli/audit/reporter.js +101 -0
- package/dist/cli/audit/reporter.js.map +1 -0
- package/dist/cli/audit/scanner.d.ts +48 -0
- package/dist/cli/audit/scanner.d.ts.map +1 -0
- package/dist/cli/audit/scanner.js +202 -0
- package/dist/cli/audit/scanner.js.map +1 -0
- package/dist/cli/index.js +6 -0
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/renderer.d.ts.map +1 -1
- package/dist/cli/renderer.js +4 -1
- package/dist/cli/renderer.js.map +1 -1
- package/dist/cli/stacks.d.ts +11 -0
- package/dist/cli/stacks.d.ts.map +1 -1
- package/dist/cli/stacks.js +82 -0
- package/dist/cli/stacks.js.map +1 -1
- package/package.json +1 -1
- package/templates/.claude/agent-memory/meto-community/MEMORY.md +23 -0
- package/templates/.claude/agents/community-manager-agent.md +72 -0
- package/templates/.claude/agents/developer-agent.md +6 -3
- package/templates/.claude/agents/pm-agent.md +2 -2
- package/templates/.claude/agents/tester-agent.md +5 -2
- package/templates/CLAUDE.md +10 -6
- package/templates/ai/workflows/code-guidelines.md +46 -0
- package/templates/ai/workflows/session-checkpoint.md +1 -2
|
@@ -0,0 +1,802 @@
|
|
|
1
|
+
import { appendFile, mkdir, readFile, stat, writeFile } from "node:fs/promises";
|
|
2
|
+
import { dirname, join } from "node:path";
|
|
3
|
+
import * as p from "@clack/prompts";
|
|
4
|
+
import { buildWorkflowAgentsSection, replaceTokens, resolveTemplatesDir, } from "../renderer.js";
|
|
5
|
+
// ---------------------------------------------------------------------------
|
|
6
|
+
// Helpers
|
|
7
|
+
// ---------------------------------------------------------------------------
|
|
8
|
+
/**
|
|
9
|
+
* Checks whether a file exists at the given path.
|
|
10
|
+
* Used to prevent overwriting existing files.
|
|
11
|
+
*/
|
|
12
|
+
async function fileExists(filePath) {
|
|
13
|
+
try {
|
|
14
|
+
await stat(filePath);
|
|
15
|
+
return true;
|
|
16
|
+
}
|
|
17
|
+
catch {
|
|
18
|
+
return false;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Reads a template file from the templates directory and applies token substitution.
|
|
23
|
+
* Returns undefined if the template does not exist.
|
|
24
|
+
*/
|
|
25
|
+
async function readAndRenderTemplate(templateRelativePath, tokens) {
|
|
26
|
+
const templatesDir = resolveTemplatesDir();
|
|
27
|
+
const templatePath = join(templatesDir, templateRelativePath);
|
|
28
|
+
try {
|
|
29
|
+
const raw = await readFile(templatePath, "utf-8");
|
|
30
|
+
return replaceTokens(raw, tokens);
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
return undefined;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Writes content to a file, creating parent directories as needed.
|
|
38
|
+
* Never overwrites existing files.
|
|
39
|
+
*/
|
|
40
|
+
async function writeFileIfMissing(filePath, content) {
|
|
41
|
+
if (await fileExists(filePath)) {
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
const dir = dirname(filePath);
|
|
45
|
+
await mkdir(dir, { recursive: true });
|
|
46
|
+
await writeFile(filePath, content, "utf-8");
|
|
47
|
+
return true;
|
|
48
|
+
}
|
|
49
|
+
// ---------------------------------------------------------------------------
|
|
50
|
+
// Fixer
|
|
51
|
+
// ---------------------------------------------------------------------------
|
|
52
|
+
/**
|
|
53
|
+
* Determines the template path for a given blueprint expectation path.
|
|
54
|
+
* Most paths map directly (e.g. "CLAUDE.md" -> "CLAUDE.md" in templates/).
|
|
55
|
+
* Directories don't have a template -- they are created implicitly.
|
|
56
|
+
*/
|
|
57
|
+
function getTemplatePath(expectationPath, checkType) {
|
|
58
|
+
// Directories are created implicitly when files inside them are created.
|
|
59
|
+
// For standalone dir-exists checks, we create the directory directly.
|
|
60
|
+
if (checkType === "dir-exists") {
|
|
61
|
+
return undefined;
|
|
62
|
+
}
|
|
63
|
+
// Custom checks (like README glob, alternative dirs) are not fixable via templates
|
|
64
|
+
if (checkType === "custom") {
|
|
65
|
+
return undefined;
|
|
66
|
+
}
|
|
67
|
+
// Direct path mapping for file-exists and file-contains
|
|
68
|
+
return expectationPath;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Attempts to fix a single failed expectation by creating the missing file or directory.
|
|
72
|
+
* Prompts the user for confirmation before each creation.
|
|
73
|
+
*
|
|
74
|
+
* This function is layer-agnostic -- it works with any failed ScanResult.
|
|
75
|
+
*/
|
|
76
|
+
async function fixSingleExpectation(projectDir, scanResult, tokens) {
|
|
77
|
+
const { expectation } = scanResult;
|
|
78
|
+
// Not fixable according to blueprint
|
|
79
|
+
if (!expectation.fixable) {
|
|
80
|
+
return {
|
|
81
|
+
scanResult,
|
|
82
|
+
outcome: "skipped",
|
|
83
|
+
message: `${expectation.description} is not auto-fixable`,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
const targetPath = join(projectDir, expectation.path);
|
|
87
|
+
// Already exists -- should not happen for failed checks, but be safe
|
|
88
|
+
if (await fileExists(targetPath)) {
|
|
89
|
+
return {
|
|
90
|
+
scanResult,
|
|
91
|
+
outcome: "skipped",
|
|
92
|
+
message: `${expectation.path} already exists`,
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
// Prompt user
|
|
96
|
+
const shouldFix = await p.confirm({
|
|
97
|
+
message: `Create ${expectation.path}? (${expectation.description})`,
|
|
98
|
+
initialValue: true,
|
|
99
|
+
});
|
|
100
|
+
if (p.isCancel(shouldFix) || !shouldFix) {
|
|
101
|
+
return {
|
|
102
|
+
scanResult,
|
|
103
|
+
outcome: "declined",
|
|
104
|
+
message: `Skipped ${expectation.path}`,
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
// Handle directory creation
|
|
108
|
+
if (expectation.checkType === "dir-exists") {
|
|
109
|
+
try {
|
|
110
|
+
await mkdir(targetPath, { recursive: true });
|
|
111
|
+
return {
|
|
112
|
+
scanResult,
|
|
113
|
+
outcome: "created",
|
|
114
|
+
message: `Created directory ${expectation.path}/`,
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
catch (err) {
|
|
118
|
+
const reason = err instanceof Error ? err.message : "Unknown error";
|
|
119
|
+
return {
|
|
120
|
+
scanResult,
|
|
121
|
+
outcome: "error",
|
|
122
|
+
message: `Failed to create ${expectation.path}/: ${reason}`,
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
// Handle file creation from template
|
|
127
|
+
const templatePath = getTemplatePath(expectation.path, expectation.checkType);
|
|
128
|
+
if (templatePath === undefined) {
|
|
129
|
+
return {
|
|
130
|
+
scanResult,
|
|
131
|
+
outcome: "skipped",
|
|
132
|
+
message: `No template available for ${expectation.path}`,
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
const rendered = await readAndRenderTemplate(templatePath, tokens);
|
|
136
|
+
if (rendered === undefined) {
|
|
137
|
+
return {
|
|
138
|
+
scanResult,
|
|
139
|
+
outcome: "error",
|
|
140
|
+
message: `Template not found for ${expectation.path}`,
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
try {
|
|
144
|
+
const written = await writeFileIfMissing(targetPath, rendered);
|
|
145
|
+
if (written) {
|
|
146
|
+
return {
|
|
147
|
+
scanResult,
|
|
148
|
+
outcome: "created",
|
|
149
|
+
message: `Created ${expectation.path}`,
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
return {
|
|
153
|
+
scanResult,
|
|
154
|
+
outcome: "skipped",
|
|
155
|
+
message: `${expectation.path} already exists (race condition)`,
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
catch (err) {
|
|
159
|
+
const reason = err instanceof Error ? err.message : "Unknown error";
|
|
160
|
+
return {
|
|
161
|
+
scanResult,
|
|
162
|
+
outcome: "error",
|
|
163
|
+
message: `Failed to create ${expectation.path}: ${reason}`,
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* Runs the interactive fixer for all failed expectations in a layer scan result.
|
|
169
|
+
* Prompts the user for each missing file/directory and creates it from templates.
|
|
170
|
+
*
|
|
171
|
+
* This function is layer-agnostic -- it works with any LayerScanResult.
|
|
172
|
+
* The fixer never overwrites existing files -- only creates missing ones.
|
|
173
|
+
*
|
|
174
|
+
* @param projectDir - Absolute path to the project root
|
|
175
|
+
* @param layerResult - The scan result containing failures to fix
|
|
176
|
+
* @param tokens - Token map for template rendering
|
|
177
|
+
* @returns Fix results for each failed expectation
|
|
178
|
+
*/
|
|
179
|
+
export async function fixLayer(projectDir, layerResult, tokens) {
|
|
180
|
+
const failedResults = layerResult.results.filter((r) => r.status === "fail");
|
|
181
|
+
const fixes = [];
|
|
182
|
+
if (failedResults.length === 0) {
|
|
183
|
+
return {
|
|
184
|
+
layerId: layerResult.layer.id,
|
|
185
|
+
fixes,
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
p.log.info(`Layer ${layerResult.layer.id} (${layerResult.layer.name}): ${failedResults.length} issue${failedResults.length === 1 ? "" : "s"} found`);
|
|
189
|
+
for (const failed of failedResults) {
|
|
190
|
+
const fixResult = await fixSingleExpectation(projectDir, failed, tokens);
|
|
191
|
+
fixes.push(fixResult);
|
|
192
|
+
// Display result
|
|
193
|
+
switch (fixResult.outcome) {
|
|
194
|
+
case "created":
|
|
195
|
+
p.log.success(fixResult.message);
|
|
196
|
+
break;
|
|
197
|
+
case "declined":
|
|
198
|
+
p.log.info(fixResult.message);
|
|
199
|
+
break;
|
|
200
|
+
case "skipped":
|
|
201
|
+
p.log.warning(fixResult.message);
|
|
202
|
+
break;
|
|
203
|
+
case "error":
|
|
204
|
+
p.log.error(fixResult.message);
|
|
205
|
+
break;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
return {
|
|
209
|
+
layerId: layerResult.layer.id,
|
|
210
|
+
fixes,
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
// ---------------------------------------------------------------------------
|
|
214
|
+
// Layer 2 (Agents) -- workflow-aware fixer
|
|
215
|
+
// ---------------------------------------------------------------------------
|
|
216
|
+
/** Expectation IDs that represent agent definition files (not dirs or memory). */
|
|
217
|
+
const AGENT_FILE_IDS = new Set([
|
|
218
|
+
"L2-pm-agent",
|
|
219
|
+
"L2-developer-agent",
|
|
220
|
+
"L2-tester-agent",
|
|
221
|
+
]);
|
|
222
|
+
/** Expectation IDs that represent agent memory MEMORY.md files inside dirs. */
|
|
223
|
+
const AGENT_MEMORY_DIR_IDS = new Set([
|
|
224
|
+
"L2-pm-memory",
|
|
225
|
+
"L2-developer-memory",
|
|
226
|
+
"L2-tester-memory",
|
|
227
|
+
]);
|
|
228
|
+
/** Maps memory directory expectation IDs to the MEMORY.md template path. */
|
|
229
|
+
const MEMORY_TEMPLATE_PATHS = {
|
|
230
|
+
"L2-pm-memory": ".claude/agent-memory/meto-pm/MEMORY.md",
|
|
231
|
+
"L2-developer-memory": ".claude/agent-memory/meto-developer/MEMORY.md",
|
|
232
|
+
"L2-tester-memory": ".claude/agent-memory/meto-tester/MEMORY.md",
|
|
233
|
+
};
|
|
234
|
+
/**
|
|
235
|
+
* Checks whether any failed expectations in the layer involve agent files
|
|
236
|
+
* that would require a workflow choice (Sprint vs Swarm).
|
|
237
|
+
*/
|
|
238
|
+
function hasAgentFileFailures(layerResult) {
|
|
239
|
+
return layerResult.results.some((r) => r.status === "fail" && AGENT_FILE_IDS.has(r.expectation.id));
|
|
240
|
+
}
|
|
241
|
+
/**
|
|
242
|
+
* Prompts the user to choose between Sprint and Swarm workflow modes.
|
|
243
|
+
* Returns the chosen mode, or undefined if the user cancels.
|
|
244
|
+
*/
|
|
245
|
+
async function promptWorkflowChoice() {
|
|
246
|
+
const choice = await p.select({
|
|
247
|
+
message: "Which workflow mode should the agents use?",
|
|
248
|
+
options: [
|
|
249
|
+
{
|
|
250
|
+
value: "sprint",
|
|
251
|
+
label: "Sprint",
|
|
252
|
+
hint: "3 agents (PM, Developer, Tester) with human orchestration",
|
|
253
|
+
},
|
|
254
|
+
{
|
|
255
|
+
value: "swarm",
|
|
256
|
+
label: "Swarm",
|
|
257
|
+
hint: "Parallel epic agents with autonomous orchestration",
|
|
258
|
+
},
|
|
259
|
+
],
|
|
260
|
+
});
|
|
261
|
+
if (p.isCancel(choice)) {
|
|
262
|
+
return undefined;
|
|
263
|
+
}
|
|
264
|
+
return choice;
|
|
265
|
+
}
|
|
266
|
+
/**
|
|
267
|
+
* Runs the interactive fixer for Layer 2 (Agents).
|
|
268
|
+
*
|
|
269
|
+
* Layer 2 differs from other layers because:
|
|
270
|
+
* - When creating agent files from scratch, the user is asked to choose
|
|
271
|
+
* Sprint vs Swarm workflow mode.
|
|
272
|
+
* - The workflow choice affects the WORKFLOW_AGENTS_SECTION token in CLAUDE.md
|
|
273
|
+
* (CLAUDE.md is Layer 1, but the token content depends on the Layer 2 choice).
|
|
274
|
+
* - Agent memory directories are created with a MEMORY.md file from templates.
|
|
275
|
+
*
|
|
276
|
+
* The fixer never overwrites existing agent definitions or settings.
|
|
277
|
+
*
|
|
278
|
+
* @param projectDir - Absolute path to the project root
|
|
279
|
+
* @param layerResult - The Layer 2 scan result containing failures to fix
|
|
280
|
+
* @param tokens - Token map for template rendering
|
|
281
|
+
* @returns Fix results for each failed expectation
|
|
282
|
+
*/
|
|
283
|
+
export async function fixLayerTwo(projectDir, layerResult, tokens) {
|
|
284
|
+
const failedResults = layerResult.results.filter((r) => r.status === "fail");
|
|
285
|
+
const fixes = [];
|
|
286
|
+
if (failedResults.length === 0) {
|
|
287
|
+
return {
|
|
288
|
+
layerId: layerResult.layer.id,
|
|
289
|
+
fixes,
|
|
290
|
+
};
|
|
291
|
+
}
|
|
292
|
+
p.log.info(`Layer ${layerResult.layer.id} (${layerResult.layer.name}): ${failedResults.length} issue${failedResults.length === 1 ? "" : "s"} found`);
|
|
293
|
+
// If any agent files need creating, ask for workflow choice up front
|
|
294
|
+
let workflowMode = "sprint";
|
|
295
|
+
if (hasAgentFileFailures(layerResult)) {
|
|
296
|
+
const choice = await promptWorkflowChoice();
|
|
297
|
+
if (choice === undefined) {
|
|
298
|
+
// User cancelled -- decline all fixes
|
|
299
|
+
for (const failed of failedResults) {
|
|
300
|
+
fixes.push({
|
|
301
|
+
scanResult: failed,
|
|
302
|
+
outcome: "declined",
|
|
303
|
+
message: `Skipped ${failed.expectation.path} (cancelled workflow choice)`,
|
|
304
|
+
});
|
|
305
|
+
}
|
|
306
|
+
return { layerId: layerResult.layer.id, fixes };
|
|
307
|
+
}
|
|
308
|
+
workflowMode = choice;
|
|
309
|
+
}
|
|
310
|
+
// Update the token map with the chosen workflow section
|
|
311
|
+
const updatedTokens = {
|
|
312
|
+
...tokens,
|
|
313
|
+
WORKFLOW_AGENTS_SECTION: buildWorkflowAgentsSection(workflowMode),
|
|
314
|
+
};
|
|
315
|
+
for (const failed of failedResults) {
|
|
316
|
+
const { expectation } = failed;
|
|
317
|
+
// Not fixable according to blueprint
|
|
318
|
+
if (!expectation.fixable) {
|
|
319
|
+
const result = {
|
|
320
|
+
scanResult: failed,
|
|
321
|
+
outcome: "skipped",
|
|
322
|
+
message: `${expectation.description} is not auto-fixable`,
|
|
323
|
+
};
|
|
324
|
+
fixes.push(result);
|
|
325
|
+
p.log.warning(result.message);
|
|
326
|
+
continue;
|
|
327
|
+
}
|
|
328
|
+
const targetPath = join(projectDir, expectation.path);
|
|
329
|
+
// Already exists -- safety check
|
|
330
|
+
if (await fileExists(targetPath)) {
|
|
331
|
+
const result = {
|
|
332
|
+
scanResult: failed,
|
|
333
|
+
outcome: "skipped",
|
|
334
|
+
message: `${expectation.path} already exists`,
|
|
335
|
+
};
|
|
336
|
+
fixes.push(result);
|
|
337
|
+
p.log.warning(result.message);
|
|
338
|
+
continue;
|
|
339
|
+
}
|
|
340
|
+
// Prompt user for each item
|
|
341
|
+
const shouldFix = await p.confirm({
|
|
342
|
+
message: `Create ${expectation.path}? (${expectation.description})`,
|
|
343
|
+
initialValue: true,
|
|
344
|
+
});
|
|
345
|
+
if (p.isCancel(shouldFix) || !shouldFix) {
|
|
346
|
+
const result = {
|
|
347
|
+
scanResult: failed,
|
|
348
|
+
outcome: "declined",
|
|
349
|
+
message: `Skipped ${expectation.path}`,
|
|
350
|
+
};
|
|
351
|
+
fixes.push(result);
|
|
352
|
+
p.log.info(result.message);
|
|
353
|
+
continue;
|
|
354
|
+
}
|
|
355
|
+
// Handle directory creation (with MEMORY.md for agent memory dirs)
|
|
356
|
+
if (expectation.checkType === "dir-exists") {
|
|
357
|
+
try {
|
|
358
|
+
await mkdir(targetPath, { recursive: true });
|
|
359
|
+
// For agent memory directories, also create the MEMORY.md file
|
|
360
|
+
const memoryTemplatePath = MEMORY_TEMPLATE_PATHS[expectation.id];
|
|
361
|
+
if (memoryTemplatePath !== undefined) {
|
|
362
|
+
const rendered = await readAndRenderTemplate(memoryTemplatePath, updatedTokens);
|
|
363
|
+
if (rendered !== undefined) {
|
|
364
|
+
const memoryFilePath = join(targetPath, "MEMORY.md");
|
|
365
|
+
await writeFileIfMissing(memoryFilePath, rendered);
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
const result = {
|
|
369
|
+
scanResult: failed,
|
|
370
|
+
outcome: "created",
|
|
371
|
+
message: `Created directory ${expectation.path}/`,
|
|
372
|
+
};
|
|
373
|
+
fixes.push(result);
|
|
374
|
+
p.log.success(result.message);
|
|
375
|
+
}
|
|
376
|
+
catch (err) {
|
|
377
|
+
const reason = err instanceof Error ? err.message : "Unknown error";
|
|
378
|
+
const result = {
|
|
379
|
+
scanResult: failed,
|
|
380
|
+
outcome: "error",
|
|
381
|
+
message: `Failed to create ${expectation.path}/: ${reason}`,
|
|
382
|
+
};
|
|
383
|
+
fixes.push(result);
|
|
384
|
+
p.log.error(result.message);
|
|
385
|
+
}
|
|
386
|
+
continue;
|
|
387
|
+
}
|
|
388
|
+
// Handle file creation from template
|
|
389
|
+
const templatePath = getTemplatePath(expectation.path, expectation.checkType);
|
|
390
|
+
if (templatePath === undefined) {
|
|
391
|
+
const result = {
|
|
392
|
+
scanResult: failed,
|
|
393
|
+
outcome: "skipped",
|
|
394
|
+
message: `No template available for ${expectation.path}`,
|
|
395
|
+
};
|
|
396
|
+
fixes.push(result);
|
|
397
|
+
p.log.warning(result.message);
|
|
398
|
+
continue;
|
|
399
|
+
}
|
|
400
|
+
const rendered = await readAndRenderTemplate(templatePath, updatedTokens);
|
|
401
|
+
if (rendered === undefined) {
|
|
402
|
+
const result = {
|
|
403
|
+
scanResult: failed,
|
|
404
|
+
outcome: "error",
|
|
405
|
+
message: `Template not found for ${expectation.path}`,
|
|
406
|
+
};
|
|
407
|
+
fixes.push(result);
|
|
408
|
+
p.log.error(result.message);
|
|
409
|
+
continue;
|
|
410
|
+
}
|
|
411
|
+
try {
|
|
412
|
+
const written = await writeFileIfMissing(targetPath, rendered);
|
|
413
|
+
if (written) {
|
|
414
|
+
const result = {
|
|
415
|
+
scanResult: failed,
|
|
416
|
+
outcome: "created",
|
|
417
|
+
message: `Created ${expectation.path}`,
|
|
418
|
+
};
|
|
419
|
+
fixes.push(result);
|
|
420
|
+
p.log.success(result.message);
|
|
421
|
+
}
|
|
422
|
+
else {
|
|
423
|
+
const result = {
|
|
424
|
+
scanResult: failed,
|
|
425
|
+
outcome: "skipped",
|
|
426
|
+
message: `${expectation.path} already exists (race condition)`,
|
|
427
|
+
};
|
|
428
|
+
fixes.push(result);
|
|
429
|
+
p.log.warning(result.message);
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
catch (err) {
|
|
433
|
+
const reason = err instanceof Error ? err.message : "Unknown error";
|
|
434
|
+
const result = {
|
|
435
|
+
scanResult: failed,
|
|
436
|
+
outcome: "error",
|
|
437
|
+
message: `Failed to create ${expectation.path}: ${reason}`,
|
|
438
|
+
};
|
|
439
|
+
fixes.push(result);
|
|
440
|
+
p.log.error(result.message);
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
return {
|
|
444
|
+
layerId: layerResult.layer.id,
|
|
445
|
+
fixes,
|
|
446
|
+
};
|
|
447
|
+
}
|
|
448
|
+
// ---------------------------------------------------------------------------
|
|
449
|
+
// Layer 3 (Governance) -- content-aware fixer
|
|
450
|
+
// ---------------------------------------------------------------------------
|
|
451
|
+
/**
|
|
452
|
+
* Expectation IDs for governance file-exists checks (create from templates).
|
|
453
|
+
*/
|
|
454
|
+
const GOVERNANCE_FILE_IDS = new Set([
|
|
455
|
+
"L3-dod-exists",
|
|
456
|
+
"L3-session-checkpoint",
|
|
457
|
+
]);
|
|
458
|
+
/**
|
|
459
|
+
* Expectation IDs for agent file-contains checks (append references).
|
|
460
|
+
*/
|
|
461
|
+
const AGENT_REFERENCE_IDS = new Set([
|
|
462
|
+
"L3-pm-agent-refs-dod",
|
|
463
|
+
"L3-developer-agent-refs-commit",
|
|
464
|
+
"L3-tester-agent-refs-dod",
|
|
465
|
+
"L3-pm-agent-refs-memory",
|
|
466
|
+
"L3-developer-agent-refs-memory",
|
|
467
|
+
"L3-tester-agent-refs-memory",
|
|
468
|
+
]);
|
|
469
|
+
/**
|
|
470
|
+
* Maps agent reference expectation IDs to the text that should be appended
|
|
471
|
+
* to the agent definition file when the reference is missing.
|
|
472
|
+
* Each snippet is a self-contained section that can be appended at the end.
|
|
473
|
+
*/
|
|
474
|
+
const AGENT_REFERENCE_PATCHES = {
|
|
475
|
+
"L3-pm-agent-refs-dod": [
|
|
476
|
+
"",
|
|
477
|
+
"## Governance References",
|
|
478
|
+
"- Read `/ai/workflows/definition-of-done.md` before closing any task",
|
|
479
|
+
"",
|
|
480
|
+
].join("\n"),
|
|
481
|
+
"L3-developer-agent-refs-commit": [
|
|
482
|
+
"",
|
|
483
|
+
"## Commit Conventions",
|
|
484
|
+
"- Follow commit conventions in `/ai/workflows/commit-conventions.md`",
|
|
485
|
+
"- Format: `<type>(<scope>): <description> [<agent-tag>]`",
|
|
486
|
+
"",
|
|
487
|
+
].join("\n"),
|
|
488
|
+
"L3-tester-agent-refs-dod": [
|
|
489
|
+
"",
|
|
490
|
+
"## Governance References",
|
|
491
|
+
"- Validate against `/ai/workflows/definition-of-done.md` for every task",
|
|
492
|
+
"",
|
|
493
|
+
].join("\n"),
|
|
494
|
+
"L3-pm-agent-refs-memory": [
|
|
495
|
+
"",
|
|
496
|
+
"## Memory",
|
|
497
|
+
"- Read `.claude/agent-memory/meto-pm/MEMORY.md` at session start",
|
|
498
|
+
"- Update `.claude/agent-memory/meto-pm/MEMORY.md` at session end",
|
|
499
|
+
"",
|
|
500
|
+
].join("\n"),
|
|
501
|
+
"L3-developer-agent-refs-memory": [
|
|
502
|
+
"",
|
|
503
|
+
"## Memory",
|
|
504
|
+
"- Read `.claude/agent-memory/meto-developer/MEMORY.md` at session start",
|
|
505
|
+
"- Update `.claude/agent-memory/meto-developer/MEMORY.md` at session end",
|
|
506
|
+
"",
|
|
507
|
+
].join("\n"),
|
|
508
|
+
"L3-tester-agent-refs-memory": [
|
|
509
|
+
"",
|
|
510
|
+
"## Memory",
|
|
511
|
+
"- Read `.claude/agent-memory/meto-tester/MEMORY.md` at session start",
|
|
512
|
+
"- Update `.claude/agent-memory/meto-tester/MEMORY.md` at session end",
|
|
513
|
+
"",
|
|
514
|
+
].join("\n"),
|
|
515
|
+
};
|
|
516
|
+
/**
|
|
517
|
+
* The commit conventions section appended to CLAUDE.md when the
|
|
518
|
+
* L3-commit-conventions-defined check fails.
|
|
519
|
+
*/
|
|
520
|
+
const COMMIT_CONVENTIONS_PATCH = [
|
|
521
|
+
"",
|
|
522
|
+
"## Commit Format",
|
|
523
|
+
"",
|
|
524
|
+
"```",
|
|
525
|
+
"feat(scope): description [dev-agent]",
|
|
526
|
+
"fix(scope): description [dev-agent]",
|
|
527
|
+
"docs(scope): description [pm-agent]",
|
|
528
|
+
"test(scope): description [tester-agent]",
|
|
529
|
+
"chore(scope): description [bootstrap]",
|
|
530
|
+
"```",
|
|
531
|
+
"",
|
|
532
|
+
].join("\n");
|
|
533
|
+
/**
|
|
534
|
+
* Appends content to the end of an existing file.
|
|
535
|
+
* Returns false if the file does not exist.
|
|
536
|
+
*/
|
|
537
|
+
async function appendToFile(filePath, content) {
|
|
538
|
+
try {
|
|
539
|
+
await stat(filePath);
|
|
540
|
+
await appendFile(filePath, content, "utf-8");
|
|
541
|
+
return true;
|
|
542
|
+
}
|
|
543
|
+
catch {
|
|
544
|
+
return false;
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
/**
|
|
548
|
+
* Runs the interactive fixer for Layer 3 (Governance).
|
|
549
|
+
*
|
|
550
|
+
* Layer 3 differs from other layers because it has three fix strategies:
|
|
551
|
+
* 1. Missing governance files (definition-of-done, session-checkpoint):
|
|
552
|
+
* created from templates, same as Layer 1.
|
|
553
|
+
* 2. CLAUDE.md missing commit conventions section:
|
|
554
|
+
* appends a commit format section to the existing file.
|
|
555
|
+
* 3. Agent definitions missing governance references:
|
|
556
|
+
* appends reference sections to existing agent files.
|
|
557
|
+
*
|
|
558
|
+
* All fixes are additive -- existing content is never modified or removed.
|
|
559
|
+
*
|
|
560
|
+
* @param projectDir - Absolute path to the project root
|
|
561
|
+
* @param layerResult - The Layer 3 scan result containing failures to fix
|
|
562
|
+
* @param tokens - Token map for template rendering
|
|
563
|
+
* @returns Fix results for each failed expectation
|
|
564
|
+
*/
|
|
565
|
+
export async function fixLayerThree(projectDir, layerResult, tokens) {
|
|
566
|
+
const failedResults = layerResult.results.filter((r) => r.status === "fail");
|
|
567
|
+
const fixes = [];
|
|
568
|
+
if (failedResults.length === 0) {
|
|
569
|
+
return {
|
|
570
|
+
layerId: layerResult.layer.id,
|
|
571
|
+
fixes,
|
|
572
|
+
};
|
|
573
|
+
}
|
|
574
|
+
p.log.info(`Layer ${layerResult.layer.id} (${layerResult.layer.name}): ${failedResults.length} issue${failedResults.length === 1 ? "" : "s"} found`);
|
|
575
|
+
for (const failed of failedResults) {
|
|
576
|
+
const { expectation } = failed;
|
|
577
|
+
// Not fixable according to blueprint
|
|
578
|
+
if (!expectation.fixable) {
|
|
579
|
+
const result = {
|
|
580
|
+
scanResult: failed,
|
|
581
|
+
outcome: "skipped",
|
|
582
|
+
message: `${expectation.description} is not auto-fixable`,
|
|
583
|
+
};
|
|
584
|
+
fixes.push(result);
|
|
585
|
+
p.log.warning(result.message);
|
|
586
|
+
continue;
|
|
587
|
+
}
|
|
588
|
+
const targetPath = join(projectDir, expectation.path);
|
|
589
|
+
// Strategy 1: Missing governance files -- create from templates
|
|
590
|
+
if (GOVERNANCE_FILE_IDS.has(expectation.id)) {
|
|
591
|
+
if (await fileExists(targetPath)) {
|
|
592
|
+
const result = {
|
|
593
|
+
scanResult: failed,
|
|
594
|
+
outcome: "skipped",
|
|
595
|
+
message: `${expectation.path} already exists`,
|
|
596
|
+
};
|
|
597
|
+
fixes.push(result);
|
|
598
|
+
p.log.warning(result.message);
|
|
599
|
+
continue;
|
|
600
|
+
}
|
|
601
|
+
const shouldFix = await p.confirm({
|
|
602
|
+
message: `Create ${expectation.path}? (${expectation.description})`,
|
|
603
|
+
initialValue: true,
|
|
604
|
+
});
|
|
605
|
+
if (p.isCancel(shouldFix) || !shouldFix) {
|
|
606
|
+
const result = {
|
|
607
|
+
scanResult: failed,
|
|
608
|
+
outcome: "declined",
|
|
609
|
+
message: `Skipped ${expectation.path}`,
|
|
610
|
+
};
|
|
611
|
+
fixes.push(result);
|
|
612
|
+
p.log.info(result.message);
|
|
613
|
+
continue;
|
|
614
|
+
}
|
|
615
|
+
const rendered = await readAndRenderTemplate(expectation.path, tokens);
|
|
616
|
+
if (rendered === undefined) {
|
|
617
|
+
const result = {
|
|
618
|
+
scanResult: failed,
|
|
619
|
+
outcome: "error",
|
|
620
|
+
message: `Template not found for ${expectation.path}`,
|
|
621
|
+
};
|
|
622
|
+
fixes.push(result);
|
|
623
|
+
p.log.error(result.message);
|
|
624
|
+
continue;
|
|
625
|
+
}
|
|
626
|
+
try {
|
|
627
|
+
const written = await writeFileIfMissing(targetPath, rendered);
|
|
628
|
+
if (written) {
|
|
629
|
+
const result = {
|
|
630
|
+
scanResult: failed,
|
|
631
|
+
outcome: "created",
|
|
632
|
+
message: `Created ${expectation.path}`,
|
|
633
|
+
};
|
|
634
|
+
fixes.push(result);
|
|
635
|
+
p.log.success(result.message);
|
|
636
|
+
}
|
|
637
|
+
else {
|
|
638
|
+
const result = {
|
|
639
|
+
scanResult: failed,
|
|
640
|
+
outcome: "skipped",
|
|
641
|
+
message: `${expectation.path} already exists (race condition)`,
|
|
642
|
+
};
|
|
643
|
+
fixes.push(result);
|
|
644
|
+
p.log.warning(result.message);
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
catch (err) {
|
|
648
|
+
const reason = err instanceof Error ? err.message : "Unknown error";
|
|
649
|
+
const result = {
|
|
650
|
+
scanResult: failed,
|
|
651
|
+
outcome: "error",
|
|
652
|
+
message: `Failed to create ${expectation.path}: ${reason}`,
|
|
653
|
+
};
|
|
654
|
+
fixes.push(result);
|
|
655
|
+
p.log.error(result.message);
|
|
656
|
+
}
|
|
657
|
+
continue;
|
|
658
|
+
}
|
|
659
|
+
// Strategy 2: CLAUDE.md missing commit conventions -- append section
|
|
660
|
+
if (expectation.id === "L3-commit-conventions-defined") {
|
|
661
|
+
if (!(await fileExists(targetPath))) {
|
|
662
|
+
const result = {
|
|
663
|
+
scanResult: failed,
|
|
664
|
+
outcome: "skipped",
|
|
665
|
+
message: `${expectation.path} does not exist -- create it first via Layer 1`,
|
|
666
|
+
};
|
|
667
|
+
fixes.push(result);
|
|
668
|
+
p.log.warning(result.message);
|
|
669
|
+
continue;
|
|
670
|
+
}
|
|
671
|
+
const shouldFix = await p.confirm({
|
|
672
|
+
message: `Append commit conventions section to ${expectation.path}?`,
|
|
673
|
+
initialValue: true,
|
|
674
|
+
});
|
|
675
|
+
if (p.isCancel(shouldFix) || !shouldFix) {
|
|
676
|
+
const result = {
|
|
677
|
+
scanResult: failed,
|
|
678
|
+
outcome: "declined",
|
|
679
|
+
message: `Skipped adding commit conventions to ${expectation.path}`,
|
|
680
|
+
};
|
|
681
|
+
fixes.push(result);
|
|
682
|
+
p.log.info(result.message);
|
|
683
|
+
continue;
|
|
684
|
+
}
|
|
685
|
+
try {
|
|
686
|
+
const appended = await appendToFile(targetPath, COMMIT_CONVENTIONS_PATCH);
|
|
687
|
+
if (appended) {
|
|
688
|
+
const result = {
|
|
689
|
+
scanResult: failed,
|
|
690
|
+
outcome: "created",
|
|
691
|
+
message: `Appended commit conventions section to ${expectation.path}`,
|
|
692
|
+
};
|
|
693
|
+
fixes.push(result);
|
|
694
|
+
p.log.success(result.message);
|
|
695
|
+
}
|
|
696
|
+
else {
|
|
697
|
+
const result = {
|
|
698
|
+
scanResult: failed,
|
|
699
|
+
outcome: "error",
|
|
700
|
+
message: `Could not append to ${expectation.path}`,
|
|
701
|
+
};
|
|
702
|
+
fixes.push(result);
|
|
703
|
+
p.log.error(result.message);
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
catch (err) {
|
|
707
|
+
const reason = err instanceof Error ? err.message : "Unknown error";
|
|
708
|
+
const result = {
|
|
709
|
+
scanResult: failed,
|
|
710
|
+
outcome: "error",
|
|
711
|
+
message: `Failed to append to ${expectation.path}: ${reason}`,
|
|
712
|
+
};
|
|
713
|
+
fixes.push(result);
|
|
714
|
+
p.log.error(result.message);
|
|
715
|
+
}
|
|
716
|
+
continue;
|
|
717
|
+
}
|
|
718
|
+
// Strategy 3: Agent definitions missing governance references -- append
|
|
719
|
+
if (AGENT_REFERENCE_IDS.has(expectation.id)) {
|
|
720
|
+
if (!(await fileExists(targetPath))) {
|
|
721
|
+
const result = {
|
|
722
|
+
scanResult: failed,
|
|
723
|
+
outcome: "skipped",
|
|
724
|
+
message: `${expectation.path} does not exist -- create it first via Layer 2`,
|
|
725
|
+
};
|
|
726
|
+
fixes.push(result);
|
|
727
|
+
p.log.warning(result.message);
|
|
728
|
+
continue;
|
|
729
|
+
}
|
|
730
|
+
const patch = AGENT_REFERENCE_PATCHES[expectation.id];
|
|
731
|
+
if (patch === undefined) {
|
|
732
|
+
const result = {
|
|
733
|
+
scanResult: failed,
|
|
734
|
+
outcome: "skipped",
|
|
735
|
+
message: `No patch defined for ${expectation.id}`,
|
|
736
|
+
};
|
|
737
|
+
fixes.push(result);
|
|
738
|
+
p.log.warning(result.message);
|
|
739
|
+
continue;
|
|
740
|
+
}
|
|
741
|
+
const shouldFix = await p.confirm({
|
|
742
|
+
message: `Append ${expectation.description.toLowerCase()} to ${expectation.path}?`,
|
|
743
|
+
initialValue: true,
|
|
744
|
+
});
|
|
745
|
+
if (p.isCancel(shouldFix) || !shouldFix) {
|
|
746
|
+
const result = {
|
|
747
|
+
scanResult: failed,
|
|
748
|
+
outcome: "declined",
|
|
749
|
+
message: `Skipped patching ${expectation.path}`,
|
|
750
|
+
};
|
|
751
|
+
fixes.push(result);
|
|
752
|
+
p.log.info(result.message);
|
|
753
|
+
continue;
|
|
754
|
+
}
|
|
755
|
+
try {
|
|
756
|
+
const appended = await appendToFile(targetPath, patch);
|
|
757
|
+
if (appended) {
|
|
758
|
+
const result = {
|
|
759
|
+
scanResult: failed,
|
|
760
|
+
outcome: "created",
|
|
761
|
+
message: `Appended ${expectation.description.toLowerCase()} to ${expectation.path}`,
|
|
762
|
+
};
|
|
763
|
+
fixes.push(result);
|
|
764
|
+
p.log.success(result.message);
|
|
765
|
+
}
|
|
766
|
+
else {
|
|
767
|
+
const result = {
|
|
768
|
+
scanResult: failed,
|
|
769
|
+
outcome: "error",
|
|
770
|
+
message: `Could not append to ${expectation.path}`,
|
|
771
|
+
};
|
|
772
|
+
fixes.push(result);
|
|
773
|
+
p.log.error(result.message);
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
catch (err) {
|
|
777
|
+
const reason = err instanceof Error ? err.message : "Unknown error";
|
|
778
|
+
const result = {
|
|
779
|
+
scanResult: failed,
|
|
780
|
+
outcome: "error",
|
|
781
|
+
message: `Failed to patch ${expectation.path}: ${reason}`,
|
|
782
|
+
};
|
|
783
|
+
fixes.push(result);
|
|
784
|
+
p.log.error(result.message);
|
|
785
|
+
}
|
|
786
|
+
continue;
|
|
787
|
+
}
|
|
788
|
+
// Fallback: unknown expectation type for Layer 3
|
|
789
|
+
const result = {
|
|
790
|
+
scanResult: failed,
|
|
791
|
+
outcome: "skipped",
|
|
792
|
+
message: `No fix strategy for ${expectation.id}`,
|
|
793
|
+
};
|
|
794
|
+
fixes.push(result);
|
|
795
|
+
p.log.warning(result.message);
|
|
796
|
+
}
|
|
797
|
+
return {
|
|
798
|
+
layerId: layerResult.layer.id,
|
|
799
|
+
fixes,
|
|
800
|
+
};
|
|
801
|
+
}
|
|
802
|
+
//# sourceMappingURL=fixer.js.map
|