pi-gsd 2.0.1 → 2.0.3
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/dist/pi-gsd-hooks.js +1533 -0
- package/dist/pi-gsd-tools.js +53 -52
- package/package.json +3 -5
- package/.gsd/extensions/pi-gsd-hooks.ts +0 -973
- package/src/cli.ts +0 -644
- package/src/commands/base.ts +0 -67
- package/src/commands/commit.ts +0 -22
- package/src/commands/config.ts +0 -71
- package/src/commands/frontmatter.ts +0 -51
- package/src/commands/index.ts +0 -76
- package/src/commands/init.ts +0 -43
- package/src/commands/milestone.ts +0 -37
- package/src/commands/phase.ts +0 -92
- package/src/commands/progress.ts +0 -71
- package/src/commands/roadmap.ts +0 -40
- package/src/commands/scaffold.ts +0 -19
- package/src/commands/state.ts +0 -102
- package/src/commands/template.ts +0 -52
- package/src/commands/verify.ts +0 -70
- package/src/commands/workstream.ts +0 -98
- package/src/commands/wxp.ts +0 -65
- package/src/lib/commands.ts +0 -1040
- package/src/lib/config.ts +0 -385
- package/src/lib/core.ts +0 -1167
- package/src/lib/frontmatter.ts +0 -462
- package/src/lib/init.ts +0 -517
- package/src/lib/milestone.ts +0 -290
- package/src/lib/model-profiles.ts +0 -272
- package/src/lib/phase.ts +0 -1012
- package/src/lib/profile-output.ts +0 -237
- package/src/lib/profile-pipeline.ts +0 -556
- package/src/lib/roadmap.ts +0 -378
- package/src/lib/schemas.ts +0 -290
- package/src/lib/security.ts +0 -176
- package/src/lib/state.ts +0 -1175
- package/src/lib/template.ts +0 -246
- package/src/lib/uat.ts +0 -289
- package/src/lib/verify.ts +0 -879
- package/src/lib/workstream.ts +0 -524
- package/src/output.ts +0 -45
- package/src/schemas/pi-gsd-settings.schema.json +0 -80
- package/src/schemas/wxp.xsd +0 -619
- package/src/schemas/wxp.zod.ts +0 -318
- package/src/wxp/__tests__/arguments.test.ts +0 -86
- package/src/wxp/__tests__/conditions.test.ts +0 -106
- package/src/wxp/__tests__/executor.test.ts +0 -95
- package/src/wxp/__tests__/helpers.ts +0 -26
- package/src/wxp/__tests__/integration.test.ts +0 -166
- package/src/wxp/__tests__/new-features.test.ts +0 -222
- package/src/wxp/__tests__/parser.test.ts +0 -159
- package/src/wxp/__tests__/paste.test.ts +0 -66
- package/src/wxp/__tests__/schema.test.ts +0 -120
- package/src/wxp/__tests__/security.test.ts +0 -87
- package/src/wxp/__tests__/shell.test.ts +0 -85
- package/src/wxp/__tests__/string-ops.test.ts +0 -25
- package/src/wxp/__tests__/variables.test.ts +0 -65
- package/src/wxp/arguments.ts +0 -89
- package/src/wxp/conditions.ts +0 -78
- package/src/wxp/executor.ts +0 -191
- package/src/wxp/index.ts +0 -191
- package/src/wxp/parser.ts +0 -198
- package/src/wxp/paste.ts +0 -51
- package/src/wxp/security.ts +0 -102
- package/src/wxp/shell.ts +0 -81
- package/src/wxp/string-ops.ts +0 -44
- package/src/wxp/variables.ts +0 -109
package/src/lib/verify.ts
DELETED
|
@@ -1,879 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* verify.ts - Verification suite, consistency, and health validation.
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
import fs from "fs";
|
|
6
|
-
import os from "os";
|
|
7
|
-
import path from "path";
|
|
8
|
-
import {
|
|
9
|
-
checkAgentsInstalled,
|
|
10
|
-
execGit,
|
|
11
|
-
extractCurrentMilestone,
|
|
12
|
-
findPhaseInternal,
|
|
13
|
-
getMilestoneInfo,
|
|
14
|
-
gsdError,
|
|
15
|
-
loadConfig,
|
|
16
|
-
MODEL_PROFILES,
|
|
17
|
-
normalizePhaseName,
|
|
18
|
-
output,
|
|
19
|
-
planningDir,
|
|
20
|
-
planningRoot,
|
|
21
|
-
safeReadFile,
|
|
22
|
-
} from "./core.js";
|
|
23
|
-
import { extractFrontmatter, parseMustHavesBlock } from "./frontmatter.js";
|
|
24
|
-
import { PlanningConfigSchema, StateFrontmatterSchema } from "./schemas.js";
|
|
25
|
-
import { writeStateMd } from "./state.js";
|
|
26
|
-
|
|
27
|
-
// ─── Shared types ─────────────────────────────────────────────────────────────
|
|
28
|
-
|
|
29
|
-
/** A single health-check issue (error, warning, or info). */
|
|
30
|
-
interface HealthIssue {
|
|
31
|
-
code: string;
|
|
32
|
-
message: string;
|
|
33
|
-
fix: string;
|
|
34
|
-
repairable: boolean;
|
|
35
|
-
/** Zod field path, e.g. "workflow.nyquist_validation" */
|
|
36
|
-
field?: string;
|
|
37
|
-
/** Human-readable description of the expected type/value */
|
|
38
|
-
expected?: string;
|
|
39
|
-
/** Actual value found in the file */
|
|
40
|
-
actual?: unknown;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
/** A repair action performed by --repair. */
|
|
44
|
-
interface HealthRepairAction {
|
|
45
|
-
action: string;
|
|
46
|
-
success: boolean;
|
|
47
|
-
path?: string;
|
|
48
|
-
error?: string;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
/** An artifact entry parsed from must_haves.artifacts. */
|
|
52
|
-
interface ArtifactEntry {
|
|
53
|
-
path?: string;
|
|
54
|
-
min_lines?: number;
|
|
55
|
-
contains?: string;
|
|
56
|
-
exports?: string | string[];
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
/** A key-link entry parsed from must_haves.key_links. */
|
|
60
|
-
interface KeyLinkEntry {
|
|
61
|
-
from?: string;
|
|
62
|
-
to?: string;
|
|
63
|
-
via?: string;
|
|
64
|
-
pattern?: string;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
/**
|
|
68
|
-
* Retrieve a nested value from an unknown object by Zod issue path.
|
|
69
|
-
* Returns undefined if any segment is missing or the container is not an object.
|
|
70
|
-
*/
|
|
71
|
-
function getNestedValue(obj: unknown, segments: (string | number)[]): unknown {
|
|
72
|
-
let cur: unknown = obj;
|
|
73
|
-
for (const key of segments) {
|
|
74
|
-
if (cur == null || typeof cur !== "object") return undefined;
|
|
75
|
-
cur = (cur as Record<string | number, unknown>)[key];
|
|
76
|
-
}
|
|
77
|
-
return cur;
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
// ─── cmdVerifySummary ─────────────────────────────────────────────────────────
|
|
81
|
-
|
|
82
|
-
export function cmdVerifySummary(
|
|
83
|
-
cwd: string,
|
|
84
|
-
summaryPath: string | undefined,
|
|
85
|
-
checkFileCount: number,
|
|
86
|
-
raw: boolean,
|
|
87
|
-
): void {
|
|
88
|
-
if (!summaryPath) gsdError("summary-path required");
|
|
89
|
-
const fullPath = path.join(cwd, summaryPath!);
|
|
90
|
-
const checkCount = checkFileCount || 2;
|
|
91
|
-
if (!fs.existsSync(fullPath)) {
|
|
92
|
-
output(
|
|
93
|
-
{
|
|
94
|
-
passed: false,
|
|
95
|
-
checks: {
|
|
96
|
-
summary_exists: false,
|
|
97
|
-
files_created: { checked: 0, found: 0, missing: [] },
|
|
98
|
-
commits_exist: false,
|
|
99
|
-
self_check: "not_found",
|
|
100
|
-
},
|
|
101
|
-
errors: ["SUMMARY.md not found"],
|
|
102
|
-
},
|
|
103
|
-
raw,
|
|
104
|
-
"failed",
|
|
105
|
-
);
|
|
106
|
-
return;
|
|
107
|
-
}
|
|
108
|
-
const content = fs.readFileSync(fullPath, "utf-8"),
|
|
109
|
-
errors: string[] = [];
|
|
110
|
-
const mentionedFiles = new Set<string>();
|
|
111
|
-
for (const pattern of [
|
|
112
|
-
/`([^`]+\.[a-zA-Z]+)`/g,
|
|
113
|
-
/(?:Created|Modified|Added|Updated|Edited):\s*`?([^\s`]+\.[a-zA-Z]+)`?/gi,
|
|
114
|
-
]) {
|
|
115
|
-
let m: RegExpExecArray | null;
|
|
116
|
-
while ((m = pattern.exec(content)) !== null) {
|
|
117
|
-
if (m[1] && !m[1].startsWith("http") && m[1].includes("/"))
|
|
118
|
-
mentionedFiles.add(m[1]);
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
const filesToCheck = Array.from(mentionedFiles).slice(0, checkCount);
|
|
122
|
-
const missing = filesToCheck.filter((f) => !fs.existsSync(path.join(cwd, f)));
|
|
123
|
-
const hashes = content.match(/\b[0-9a-f]{7,40}\b/g) || [];
|
|
124
|
-
let commitsExist = false;
|
|
125
|
-
for (const hash of hashes.slice(0, 3)) {
|
|
126
|
-
if (execGit(cwd, ["cat-file", "-t", hash]).stdout === "commit") {
|
|
127
|
-
commitsExist = true;
|
|
128
|
-
break;
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
let selfCheck = "not_found";
|
|
132
|
-
if (/##\s*(?:Self[- ]?Check|Verification|Quality Check)/i.test(content)) {
|
|
133
|
-
const checkSection = content.slice(
|
|
134
|
-
content.search(/##\s*(?:Self[- ]?Check|Verification|Quality Check)/i),
|
|
135
|
-
);
|
|
136
|
-
if (/(?:fail|✗|❌|incomplete|blocked)/i.test(checkSection))
|
|
137
|
-
selfCheck = "failed";
|
|
138
|
-
else if (/(?:all\s+)?(?:pass|✓|✅|complete|succeeded)/i.test(checkSection))
|
|
139
|
-
selfCheck = "passed";
|
|
140
|
-
}
|
|
141
|
-
if (missing.length > 0) errors.push("Missing files: " + missing.join(", "));
|
|
142
|
-
if (!commitsExist && hashes.length > 0)
|
|
143
|
-
errors.push("Referenced commit hashes not found in git history");
|
|
144
|
-
if (selfCheck === "failed")
|
|
145
|
-
errors.push("Self-check section indicates failure");
|
|
146
|
-
const passed = missing.length === 0 && selfCheck !== "failed";
|
|
147
|
-
output(
|
|
148
|
-
{
|
|
149
|
-
passed,
|
|
150
|
-
checks: {
|
|
151
|
-
summary_exists: true,
|
|
152
|
-
files_created: {
|
|
153
|
-
checked: filesToCheck.length,
|
|
154
|
-
found: filesToCheck.length - missing.length,
|
|
155
|
-
missing,
|
|
156
|
-
},
|
|
157
|
-
commits_exist: commitsExist,
|
|
158
|
-
self_check: selfCheck,
|
|
159
|
-
},
|
|
160
|
-
errors,
|
|
161
|
-
},
|
|
162
|
-
raw,
|
|
163
|
-
passed ? "passed" : "failed",
|
|
164
|
-
);
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
// ─── cmdVerifyPlanStructure ────────────────────────────────────────────────────
|
|
168
|
-
|
|
169
|
-
export function cmdVerifyPlanStructure(
|
|
170
|
-
cwd: string,
|
|
171
|
-
filePath: string | undefined,
|
|
172
|
-
raw: boolean,
|
|
173
|
-
): void {
|
|
174
|
-
if (!filePath) gsdError("file path required");
|
|
175
|
-
const fullPath = path.isAbsolute(filePath!)
|
|
176
|
-
? filePath!
|
|
177
|
-
: path.join(cwd, filePath!);
|
|
178
|
-
const content = safeReadFile(fullPath);
|
|
179
|
-
if (!content) {
|
|
180
|
-
output({ error: "File not found", path: filePath }, raw);
|
|
181
|
-
return;
|
|
182
|
-
}
|
|
183
|
-
const fm = extractFrontmatter(content),
|
|
184
|
-
errors: string[] = [],
|
|
185
|
-
warnings: string[] = [];
|
|
186
|
-
for (const field of [
|
|
187
|
-
"phase",
|
|
188
|
-
"plan",
|
|
189
|
-
"type",
|
|
190
|
-
"wave",
|
|
191
|
-
"depends_on",
|
|
192
|
-
"files_modified",
|
|
193
|
-
"autonomous",
|
|
194
|
-
"must_haves",
|
|
195
|
-
]) {
|
|
196
|
-
if (fm[field] === undefined)
|
|
197
|
-
errors.push(`Missing required frontmatter field: ${field}`);
|
|
198
|
-
}
|
|
199
|
-
const taskPattern = /<task[^>]*>([\s\S]*?)<\/task>/g;
|
|
200
|
-
const tasks: Array<{
|
|
201
|
-
name: string;
|
|
202
|
-
hasFiles: boolean;
|
|
203
|
-
hasAction: boolean;
|
|
204
|
-
hasVerify: boolean;
|
|
205
|
-
hasDone: boolean;
|
|
206
|
-
}> = [];
|
|
207
|
-
let taskMatch: RegExpExecArray | null;
|
|
208
|
-
while ((taskMatch = taskPattern.exec(content)) !== null) {
|
|
209
|
-
const tc = taskMatch[1];
|
|
210
|
-
const nameMatch = tc.match(/<name>([\s\S]*?)<\/name>/);
|
|
211
|
-
const taskName = nameMatch ? nameMatch[1].trim() : "unnamed";
|
|
212
|
-
const hasFiles = /<files>/.test(tc),
|
|
213
|
-
hasAction = /<action>/.test(tc),
|
|
214
|
-
hasVerify = /<verify>/.test(tc),
|
|
215
|
-
hasDone = /<done>/.test(tc);
|
|
216
|
-
if (!nameMatch) errors.push("Task missing <name> element");
|
|
217
|
-
if (!hasAction) errors.push(`Task '${taskName}' missing <action>`);
|
|
218
|
-
if (!hasVerify) warnings.push(`Task '${taskName}' missing <verify>`);
|
|
219
|
-
if (!hasDone) warnings.push(`Task '${taskName}' missing <done>`);
|
|
220
|
-
if (!hasFiles) warnings.push(`Task '${taskName}' missing <files>`);
|
|
221
|
-
tasks.push({ name: taskName, hasFiles, hasAction, hasVerify, hasDone });
|
|
222
|
-
}
|
|
223
|
-
if (tasks.length === 0) warnings.push("No <task> elements found");
|
|
224
|
-
if (
|
|
225
|
-
fm.wave &&
|
|
226
|
-
parseInt(String(fm.wave)) > 1 &&
|
|
227
|
-
(!fm.depends_on ||
|
|
228
|
-
(Array.isArray(fm.depends_on) && fm.depends_on.length === 0))
|
|
229
|
-
)
|
|
230
|
-
warnings.push("Wave > 1 but depends_on is empty");
|
|
231
|
-
const hasCheckpoints = /<task\s+type=["']?checkpoint/.test(content);
|
|
232
|
-
if (hasCheckpoints && fm.autonomous !== "false" && fm.autonomous !== false)
|
|
233
|
-
errors.push("Has checkpoint tasks but autonomous is not false");
|
|
234
|
-
output(
|
|
235
|
-
{
|
|
236
|
-
valid: errors.length === 0,
|
|
237
|
-
errors,
|
|
238
|
-
warnings,
|
|
239
|
-
task_count: tasks.length,
|
|
240
|
-
tasks,
|
|
241
|
-
frontmatter_fields: Object.keys(fm),
|
|
242
|
-
},
|
|
243
|
-
raw,
|
|
244
|
-
errors.length === 0 ? "valid" : "invalid",
|
|
245
|
-
);
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
// ─── cmdVerifyPhaseCompleteness ────────────────────────────────────────────────
|
|
249
|
-
|
|
250
|
-
export function cmdVerifyPhaseCompleteness(
|
|
251
|
-
cwd: string,
|
|
252
|
-
phase: string | undefined,
|
|
253
|
-
raw: boolean,
|
|
254
|
-
): void {
|
|
255
|
-
if (!phase) gsdError("phase required");
|
|
256
|
-
const phaseInfo = findPhaseInternal(cwd, phase!);
|
|
257
|
-
if (!phaseInfo?.found) {
|
|
258
|
-
output({ error: "Phase not found", phase }, raw);
|
|
259
|
-
return;
|
|
260
|
-
}
|
|
261
|
-
const phaseDir = path.join(cwd, phaseInfo.directory);
|
|
262
|
-
const errors: string[] = [],
|
|
263
|
-
warnings: string[] = [];
|
|
264
|
-
let files: string[];
|
|
265
|
-
try {
|
|
266
|
-
files = fs.readdirSync(phaseDir);
|
|
267
|
-
} catch {
|
|
268
|
-
output({ error: "Cannot read phase directory" }, raw);
|
|
269
|
-
return;
|
|
270
|
-
}
|
|
271
|
-
const plans = files.filter((f) => f.match(/-PLAN\.md$/i));
|
|
272
|
-
const summaries = files.filter((f) => f.match(/-SUMMARY\.md$/i));
|
|
273
|
-
const planIds = new Set(plans.map((p) => p.replace(/-PLAN\.md$/i, "")));
|
|
274
|
-
const summaryIds = new Set(
|
|
275
|
-
summaries.map((s) => s.replace(/-SUMMARY\.md$/i, "")),
|
|
276
|
-
);
|
|
277
|
-
const incompletePlans = [...planIds].filter((id) => !summaryIds.has(id));
|
|
278
|
-
const orphanSummaries = [...summaryIds].filter((id) => !planIds.has(id));
|
|
279
|
-
if (incompletePlans.length > 0)
|
|
280
|
-
errors.push(`Plans without summaries: ${incompletePlans.join(", ")}`);
|
|
281
|
-
if (orphanSummaries.length > 0)
|
|
282
|
-
warnings.push(`Summaries without plans: ${orphanSummaries.join(", ")}`);
|
|
283
|
-
output(
|
|
284
|
-
{
|
|
285
|
-
complete: errors.length === 0,
|
|
286
|
-
phase: phaseInfo.phase_number,
|
|
287
|
-
plan_count: plans.length,
|
|
288
|
-
summary_count: summaries.length,
|
|
289
|
-
incomplete_plans: incompletePlans,
|
|
290
|
-
orphan_summaries: orphanSummaries,
|
|
291
|
-
errors,
|
|
292
|
-
warnings,
|
|
293
|
-
},
|
|
294
|
-
raw,
|
|
295
|
-
errors.length === 0 ? "complete" : "incomplete",
|
|
296
|
-
);
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
// ─── cmdVerifyReferences ──────────────────────────────────────────────────────
|
|
300
|
-
|
|
301
|
-
export function cmdVerifyReferences(
|
|
302
|
-
cwd: string,
|
|
303
|
-
filePath: string | undefined,
|
|
304
|
-
raw: boolean,
|
|
305
|
-
): void {
|
|
306
|
-
if (!filePath) gsdError("file path required");
|
|
307
|
-
const fullPath = path.isAbsolute(filePath!)
|
|
308
|
-
? filePath!
|
|
309
|
-
: path.join(cwd, filePath!);
|
|
310
|
-
const content = safeReadFile(fullPath);
|
|
311
|
-
if (!content) {
|
|
312
|
-
output({ error: "File not found", path: filePath }, raw);
|
|
313
|
-
return;
|
|
314
|
-
}
|
|
315
|
-
const found: string[] = [],
|
|
316
|
-
missing: string[] = [];
|
|
317
|
-
for (const ref of content.match(/@([^\s\n,)]+\/[^\s\n,)]+)/g) || []) {
|
|
318
|
-
const cleanRef = ref.slice(1);
|
|
319
|
-
const resolved = cleanRef.startsWith("~/")
|
|
320
|
-
? path.join(process.env["HOME"] ?? "", cleanRef.slice(2))
|
|
321
|
-
: path.join(cwd, cleanRef);
|
|
322
|
-
(fs.existsSync(resolved) ? found : missing).push(cleanRef);
|
|
323
|
-
}
|
|
324
|
-
for (const ref of content.match(/`([^`]+\/[^`]+\.[a-zA-Z]{1,10})`/g) || []) {
|
|
325
|
-
const cleanRef = ref.slice(1, -1);
|
|
326
|
-
if (
|
|
327
|
-
cleanRef.startsWith("http") ||
|
|
328
|
-
cleanRef.includes("${") ||
|
|
329
|
-
cleanRef.includes("{{")
|
|
330
|
-
)
|
|
331
|
-
continue;
|
|
332
|
-
if (found.includes(cleanRef) || missing.includes(cleanRef)) continue;
|
|
333
|
-
(fs.existsSync(path.join(cwd, cleanRef)) ? found : missing).push(cleanRef);
|
|
334
|
-
}
|
|
335
|
-
output(
|
|
336
|
-
{
|
|
337
|
-
valid: missing.length === 0,
|
|
338
|
-
found: found.length,
|
|
339
|
-
missing,
|
|
340
|
-
total: found.length + missing.length,
|
|
341
|
-
},
|
|
342
|
-
raw,
|
|
343
|
-
missing.length === 0 ? "valid" : "invalid",
|
|
344
|
-
);
|
|
345
|
-
}
|
|
346
|
-
|
|
347
|
-
// ─── cmdVerifyCommits ─────────────────────────────────────────────────────────
|
|
348
|
-
|
|
349
|
-
export function cmdVerifyCommits(
|
|
350
|
-
cwd: string,
|
|
351
|
-
hashes: string[],
|
|
352
|
-
raw: boolean,
|
|
353
|
-
): void {
|
|
354
|
-
if (!hashes || hashes.length === 0)
|
|
355
|
-
gsdError("At least one commit hash required");
|
|
356
|
-
const valid: string[] = [],
|
|
357
|
-
invalid: string[] = [];
|
|
358
|
-
for (const hash of hashes) {
|
|
359
|
-
(execGit(cwd, ["cat-file", "-t", hash]).stdout.trim() === "commit"
|
|
360
|
-
? valid
|
|
361
|
-
: invalid
|
|
362
|
-
).push(hash);
|
|
363
|
-
}
|
|
364
|
-
output(
|
|
365
|
-
{ all_valid: invalid.length === 0, valid, invalid, total: hashes.length },
|
|
366
|
-
raw,
|
|
367
|
-
invalid.length === 0 ? "valid" : "invalid",
|
|
368
|
-
);
|
|
369
|
-
}
|
|
370
|
-
|
|
371
|
-
// ─── cmdVerifyArtifacts ────────────────────────────────────────────────────────
|
|
372
|
-
|
|
373
|
-
export function cmdVerifyArtifacts(
|
|
374
|
-
cwd: string,
|
|
375
|
-
planFilePath: string | undefined,
|
|
376
|
-
raw: boolean,
|
|
377
|
-
): void {
|
|
378
|
-
if (!planFilePath) gsdError("plan file path required");
|
|
379
|
-
const fullPath = path.isAbsolute(planFilePath!)
|
|
380
|
-
? planFilePath!
|
|
381
|
-
: path.join(cwd, planFilePath!);
|
|
382
|
-
const content = safeReadFile(fullPath);
|
|
383
|
-
if (!content) {
|
|
384
|
-
output({ error: "File not found", path: planFilePath }, raw);
|
|
385
|
-
return;
|
|
386
|
-
}
|
|
387
|
-
const artifacts = parseMustHavesBlock(content, "artifacts");
|
|
388
|
-
if (artifacts.length === 0) {
|
|
389
|
-
output(
|
|
390
|
-
{
|
|
391
|
-
error: "No must_haves.artifacts found in frontmatter",
|
|
392
|
-
path: planFilePath,
|
|
393
|
-
},
|
|
394
|
-
raw,
|
|
395
|
-
);
|
|
396
|
-
return;
|
|
397
|
-
}
|
|
398
|
-
const results: Array<{
|
|
399
|
-
path: string;
|
|
400
|
-
exists: boolean;
|
|
401
|
-
issues: string[];
|
|
402
|
-
passed: boolean;
|
|
403
|
-
}> = [];
|
|
404
|
-
for (const artifact of artifacts) {
|
|
405
|
-
if (typeof artifact === "string") continue;
|
|
406
|
-
const art = artifact as ArtifactEntry;
|
|
407
|
-
if (!art.path) continue;
|
|
408
|
-
const artFullPath = path.join(cwd, art.path),
|
|
409
|
-
exists = fs.existsSync(artFullPath);
|
|
410
|
-
const check = {
|
|
411
|
-
path: art.path,
|
|
412
|
-
exists,
|
|
413
|
-
issues: [] as string[],
|
|
414
|
-
passed: false,
|
|
415
|
-
};
|
|
416
|
-
if (exists) {
|
|
417
|
-
const fileContent = safeReadFile(artFullPath) ?? "",
|
|
418
|
-
lineCount = fileContent.split("\n").length;
|
|
419
|
-
if (art.min_lines && lineCount < art.min_lines)
|
|
420
|
-
check.issues.push(`Only ${lineCount} lines, need ${art.min_lines}`);
|
|
421
|
-
if (art.contains && !fileContent.includes(art.contains))
|
|
422
|
-
check.issues.push(`Missing pattern: ${art.contains}`);
|
|
423
|
-
if (art.exports) {
|
|
424
|
-
const exps = Array.isArray(art.exports) ? art.exports : [art.exports];
|
|
425
|
-
for (const exp of exps)
|
|
426
|
-
if (!fileContent.includes(exp))
|
|
427
|
-
check.issues.push(`Missing export: ${exp}`);
|
|
428
|
-
}
|
|
429
|
-
check.passed = check.issues.length === 0;
|
|
430
|
-
} else {
|
|
431
|
-
check.issues.push("File not found");
|
|
432
|
-
}
|
|
433
|
-
results.push(check);
|
|
434
|
-
}
|
|
435
|
-
const passed = results.filter((r) => r.passed).length;
|
|
436
|
-
output(
|
|
437
|
-
{
|
|
438
|
-
all_passed: passed === results.length,
|
|
439
|
-
passed,
|
|
440
|
-
total: results.length,
|
|
441
|
-
artifacts: results,
|
|
442
|
-
},
|
|
443
|
-
raw,
|
|
444
|
-
passed === results.length ? "valid" : "invalid",
|
|
445
|
-
);
|
|
446
|
-
}
|
|
447
|
-
|
|
448
|
-
// ─── cmdVerifyKeyLinks ────────────────────────────────────────────────────────
|
|
449
|
-
|
|
450
|
-
export function cmdVerifyKeyLinks(
|
|
451
|
-
cwd: string,
|
|
452
|
-
planFilePath: string | undefined,
|
|
453
|
-
raw: boolean,
|
|
454
|
-
): void {
|
|
455
|
-
if (!planFilePath) gsdError("plan file path required");
|
|
456
|
-
const fullPath = path.isAbsolute(planFilePath!)
|
|
457
|
-
? planFilePath!
|
|
458
|
-
: path.join(cwd, planFilePath!);
|
|
459
|
-
const content = safeReadFile(fullPath);
|
|
460
|
-
if (!content) {
|
|
461
|
-
output({ error: "File not found", path: planFilePath }, raw);
|
|
462
|
-
return;
|
|
463
|
-
}
|
|
464
|
-
const keyLinks = parseMustHavesBlock(content, "key_links");
|
|
465
|
-
if (keyLinks.length === 0) {
|
|
466
|
-
output(
|
|
467
|
-
{
|
|
468
|
-
error: "No must_haves.key_links found in frontmatter",
|
|
469
|
-
path: planFilePath,
|
|
470
|
-
},
|
|
471
|
-
raw,
|
|
472
|
-
);
|
|
473
|
-
return;
|
|
474
|
-
}
|
|
475
|
-
const results: Array<{
|
|
476
|
-
from: string;
|
|
477
|
-
to: string;
|
|
478
|
-
via: string;
|
|
479
|
-
verified: boolean;
|
|
480
|
-
detail: string;
|
|
481
|
-
}> = [];
|
|
482
|
-
for (const link of keyLinks) {
|
|
483
|
-
if (typeof link === "string") continue;
|
|
484
|
-
const l = link as KeyLinkEntry;
|
|
485
|
-
const check = {
|
|
486
|
-
from: l.from ?? "",
|
|
487
|
-
to: l.to ?? "",
|
|
488
|
-
via: l.via || "",
|
|
489
|
-
verified: false,
|
|
490
|
-
detail: "",
|
|
491
|
-
};
|
|
492
|
-
const sourceContent = safeReadFile(path.join(cwd, l.from || ""));
|
|
493
|
-
if (!sourceContent) {
|
|
494
|
-
check.detail = "Source file not found";
|
|
495
|
-
} else if (l.pattern) {
|
|
496
|
-
try {
|
|
497
|
-
const regex = new RegExp(l.pattern);
|
|
498
|
-
if (regex.test(sourceContent)) {
|
|
499
|
-
check.verified = true;
|
|
500
|
-
check.detail = "Pattern found in source";
|
|
501
|
-
} else {
|
|
502
|
-
const targetContent = safeReadFile(path.join(cwd, l.to || ""));
|
|
503
|
-
if (targetContent && regex.test(targetContent)) {
|
|
504
|
-
check.verified = true;
|
|
505
|
-
check.detail = "Pattern found in target";
|
|
506
|
-
} else
|
|
507
|
-
check.detail = `Pattern "${l.pattern}" not found in source or target`;
|
|
508
|
-
}
|
|
509
|
-
} catch {
|
|
510
|
-
check.detail = `Invalid regex pattern: ${l.pattern}`;
|
|
511
|
-
}
|
|
512
|
-
} else {
|
|
513
|
-
if (sourceContent.includes(l.to || "")) {
|
|
514
|
-
check.verified = true;
|
|
515
|
-
check.detail = "Target referenced in source";
|
|
516
|
-
} else check.detail = "Target not referenced in source";
|
|
517
|
-
}
|
|
518
|
-
results.push(check);
|
|
519
|
-
}
|
|
520
|
-
const verified = results.filter((r) => r.verified).length;
|
|
521
|
-
output(
|
|
522
|
-
{
|
|
523
|
-
all_verified: verified === results.length,
|
|
524
|
-
verified,
|
|
525
|
-
total: results.length,
|
|
526
|
-
links: results,
|
|
527
|
-
},
|
|
528
|
-
raw,
|
|
529
|
-
verified === results.length ? "valid" : "invalid",
|
|
530
|
-
);
|
|
531
|
-
}
|
|
532
|
-
|
|
533
|
-
// ─── cmdValidateConsistency ────────────────────────────────────────────────────
|
|
534
|
-
|
|
535
|
-
export function cmdValidateConsistency(cwd: string, raw: boolean): void {
|
|
536
|
-
const roadmapPath = path.join(planningDir(cwd), "ROADMAP.md");
|
|
537
|
-
const phasesDir = path.join(planningDir(cwd), "phases");
|
|
538
|
-
const errors: string[] = [],
|
|
539
|
-
warnings: string[] = [];
|
|
540
|
-
if (!fs.existsSync(roadmapPath)) {
|
|
541
|
-
errors.push("ROADMAP.md not found");
|
|
542
|
-
output({ passed: false, errors, warnings }, raw, "failed");
|
|
543
|
-
return;
|
|
544
|
-
}
|
|
545
|
-
const roadmapContent = extractCurrentMilestone(
|
|
546
|
-
fs.readFileSync(roadmapPath, "utf-8"),
|
|
547
|
-
cwd,
|
|
548
|
-
);
|
|
549
|
-
const roadmapPhases = new Set<string>();
|
|
550
|
-
const phasePattern = /#{2,4}\s*Phase\s+(\d+[A-Z]?(?:\.\d+)*)\s*:/gi;
|
|
551
|
-
let m: RegExpExecArray | null;
|
|
552
|
-
while ((m = phasePattern.exec(roadmapContent)) !== null)
|
|
553
|
-
roadmapPhases.add(m[1]);
|
|
554
|
-
const diskPhases = new Set<string>();
|
|
555
|
-
try {
|
|
556
|
-
fs.readdirSync(phasesDir, { withFileTypes: true })
|
|
557
|
-
.filter((e) => e.isDirectory())
|
|
558
|
-
.map((e) => e.name)
|
|
559
|
-
.forEach((dir) => {
|
|
560
|
-
const dm = dir.match(/^(\d+[A-Z]?(?:\.\d+)*)/i);
|
|
561
|
-
if (dm) diskPhases.add(dm[1]);
|
|
562
|
-
});
|
|
563
|
-
} catch {
|
|
564
|
-
/* ok */
|
|
565
|
-
}
|
|
566
|
-
for (const p of roadmapPhases) {
|
|
567
|
-
if (!diskPhases.has(p) && !diskPhases.has(normalizePhaseName(p)))
|
|
568
|
-
warnings.push(`Phase ${p} in ROADMAP.md but no directory on disk`);
|
|
569
|
-
}
|
|
570
|
-
for (const p of diskPhases) {
|
|
571
|
-
const unpadded = String(parseInt(p, 10));
|
|
572
|
-
if (!roadmapPhases.has(p) && !roadmapPhases.has(unpadded))
|
|
573
|
-
warnings.push(`Phase ${p} exists on disk but not in ROADMAP.md`);
|
|
574
|
-
}
|
|
575
|
-
const config = loadConfig(cwd);
|
|
576
|
-
if (config.phase_naming !== "custom") {
|
|
577
|
-
const integerPhases = [...diskPhases]
|
|
578
|
-
.filter((p) => !p.includes("."))
|
|
579
|
-
.map((p) => parseInt(p, 10))
|
|
580
|
-
.sort((a, b) => a - b);
|
|
581
|
-
for (let i = 1; i < integerPhases.length; i++) {
|
|
582
|
-
if (integerPhases[i] !== integerPhases[i - 1] + 1)
|
|
583
|
-
warnings.push(
|
|
584
|
-
`Gap in phase numbering: ${integerPhases[i - 1]} → ${integerPhases[i]}`,
|
|
585
|
-
);
|
|
586
|
-
}
|
|
587
|
-
}
|
|
588
|
-
const passed = errors.length === 0;
|
|
589
|
-
output(
|
|
590
|
-
{ passed, errors, warnings, warning_count: warnings.length },
|
|
591
|
-
raw,
|
|
592
|
-
passed ? "passed" : "failed",
|
|
593
|
-
);
|
|
594
|
-
}
|
|
595
|
-
|
|
596
|
-
// ─── cmdValidateHealth ────────────────────────────────────────────────────────
|
|
597
|
-
|
|
598
|
-
export function cmdValidateHealth(
|
|
599
|
-
cwd: string,
|
|
600
|
-
options: { repair?: boolean },
|
|
601
|
-
raw: boolean,
|
|
602
|
-
): void {
|
|
603
|
-
const resolved = path.resolve(cwd);
|
|
604
|
-
if (resolved === os.homedir()) {
|
|
605
|
-
output(
|
|
606
|
-
{
|
|
607
|
-
status: "error",
|
|
608
|
-
errors: [
|
|
609
|
-
{
|
|
610
|
-
code: "E010",
|
|
611
|
-
message: `CWD is home directory - health check would read the wrong .planning/ directory.`,
|
|
612
|
-
fix: "cd into your project directory and retry",
|
|
613
|
-
},
|
|
614
|
-
],
|
|
615
|
-
warnings: [],
|
|
616
|
-
info: [{ code: "I010", message: `Resolved CWD: ${resolved}` }],
|
|
617
|
-
repairable_count: 0,
|
|
618
|
-
},
|
|
619
|
-
raw,
|
|
620
|
-
);
|
|
621
|
-
return;
|
|
622
|
-
}
|
|
623
|
-
const planBase = planningDir(cwd),
|
|
624
|
-
planRoot = planningRoot(cwd);
|
|
625
|
-
const projectPath = path.join(planRoot, "PROJECT.md"),
|
|
626
|
-
roadmapPath = path.join(planBase, "ROADMAP.md"),
|
|
627
|
-
statePath = path.join(planBase, "STATE.md"),
|
|
628
|
-
configPath = path.join(planRoot, "config.json"),
|
|
629
|
-
phasesDir = path.join(planBase, "phases");
|
|
630
|
-
const errors: HealthIssue[] = [],
|
|
631
|
-
warnings: HealthIssue[] = [],
|
|
632
|
-
info: HealthIssue[] = [],
|
|
633
|
-
repairs: string[] = [];
|
|
634
|
-
const addIssue = (
|
|
635
|
-
severity: string,
|
|
636
|
-
code: string,
|
|
637
|
-
message: string,
|
|
638
|
-
fix: string,
|
|
639
|
-
repairable = false,
|
|
640
|
-
detail?: { field?: string; expected?: string; actual?: unknown },
|
|
641
|
-
) => {
|
|
642
|
-
const issue: HealthIssue = { code, message, fix, repairable, ...detail };
|
|
643
|
-
if (severity === "error") errors.push(issue);
|
|
644
|
-
else if (severity === "warning") warnings.push(issue);
|
|
645
|
-
else info.push(issue);
|
|
646
|
-
};
|
|
647
|
-
if (!fs.existsSync(planBase)) {
|
|
648
|
-
addIssue(
|
|
649
|
-
"error",
|
|
650
|
-
"E001",
|
|
651
|
-
".planning/ directory not found",
|
|
652
|
-
"Run /gsd-new-project to initialize",
|
|
653
|
-
);
|
|
654
|
-
output(
|
|
655
|
-
{ status: "broken", errors, warnings, info, repairable_count: 0 },
|
|
656
|
-
raw,
|
|
657
|
-
);
|
|
658
|
-
return;
|
|
659
|
-
}
|
|
660
|
-
if (!fs.existsSync(projectPath)) {
|
|
661
|
-
addIssue(
|
|
662
|
-
"error",
|
|
663
|
-
"E002",
|
|
664
|
-
"PROJECT.md not found",
|
|
665
|
-
"Run /gsd-new-project to create",
|
|
666
|
-
);
|
|
667
|
-
} else {
|
|
668
|
-
const content = fs.readFileSync(projectPath, "utf-8");
|
|
669
|
-
for (const s of ["## What This Is", "## Core Value", "## Requirements"])
|
|
670
|
-
if (!content.includes(s))
|
|
671
|
-
addIssue(
|
|
672
|
-
"warning",
|
|
673
|
-
"W001",
|
|
674
|
-
`PROJECT.md missing section: ${s}`,
|
|
675
|
-
"Add section manually",
|
|
676
|
-
);
|
|
677
|
-
}
|
|
678
|
-
if (!fs.existsSync(roadmapPath))
|
|
679
|
-
addIssue(
|
|
680
|
-
"error",
|
|
681
|
-
"E003",
|
|
682
|
-
"ROADMAP.md not found",
|
|
683
|
-
"Run /gsd-new-milestone to create roadmap",
|
|
684
|
-
);
|
|
685
|
-
if (!fs.existsSync(statePath)) {
|
|
686
|
-
addIssue(
|
|
687
|
-
"error",
|
|
688
|
-
"E004",
|
|
689
|
-
"STATE.md not found",
|
|
690
|
-
"Run /gsd-health --repair to regenerate",
|
|
691
|
-
true,
|
|
692
|
-
);
|
|
693
|
-
repairs.push("regenerateState");
|
|
694
|
-
} else {
|
|
695
|
-
// ─── Zod schema validation for STATE.md frontmatter ───────────────────
|
|
696
|
-
try {
|
|
697
|
-
const stateContent = fs.readFileSync(statePath, "utf-8");
|
|
698
|
-
const stateFm = extractFrontmatter(stateContent);
|
|
699
|
-
const stateResult = StateFrontmatterSchema.safeParse(stateFm);
|
|
700
|
-
if (!stateResult.success) {
|
|
701
|
-
for (const issue of stateResult.error.issues) {
|
|
702
|
-
const fieldPath = issue.path.join(".") || "(root)";
|
|
703
|
-
addIssue(
|
|
704
|
-
"warning",
|
|
705
|
-
"W011",
|
|
706
|
-
`STATE.md frontmatter: field "${fieldPath}" — ${issue.message}`,
|
|
707
|
-
"Check STATE.md frontmatter manually or run /gsd-health --repair to regenerate",
|
|
708
|
-
true,
|
|
709
|
-
);
|
|
710
|
-
}
|
|
711
|
-
if (!repairs.includes("regenerateState"))
|
|
712
|
-
repairs.push("regenerateState");
|
|
713
|
-
}
|
|
714
|
-
} catch {
|
|
715
|
-
/* non-blocking — STATE.md parse errors caught by E004 path */
|
|
716
|
-
}
|
|
717
|
-
}
|
|
718
|
-
if (!fs.existsSync(configPath)) {
|
|
719
|
-
addIssue(
|
|
720
|
-
"warning",
|
|
721
|
-
"W003",
|
|
722
|
-
"config.json not found",
|
|
723
|
-
"Run /gsd-health --repair to create with defaults",
|
|
724
|
-
true,
|
|
725
|
-
);
|
|
726
|
-
repairs.push("createConfig");
|
|
727
|
-
} else {
|
|
728
|
-
try {
|
|
729
|
-
const parsed: unknown = JSON.parse(fs.readFileSync(configPath, "utf-8"));
|
|
730
|
-
// ─── Zod schema validation ───────────────────────────────────────────────
|
|
731
|
-
const zodResult = PlanningConfigSchema.safeParse(parsed);
|
|
732
|
-
if (!zodResult.success) {
|
|
733
|
-
for (const issue of zodResult.error.issues) {
|
|
734
|
-
const fieldPath = issue.path.join(".") || "(root)";
|
|
735
|
-
const actual = getNestedValue(parsed, issue.path);
|
|
736
|
-
addIssue(
|
|
737
|
-
"warning",
|
|
738
|
-
"W005",
|
|
739
|
-
`config.json: field "${fieldPath}" - ${issue.message}`,
|
|
740
|
-
"Run pi-gsd-tools validate health --repair to fix using schema defaults",
|
|
741
|
-
true,
|
|
742
|
-
{
|
|
743
|
-
field: fieldPath,
|
|
744
|
-
expected: issue.message,
|
|
745
|
-
actual,
|
|
746
|
-
},
|
|
747
|
-
);
|
|
748
|
-
}
|
|
749
|
-
if (!repairs.includes("fixSchemaDefaults"))
|
|
750
|
-
repairs.push("fixSchemaDefaults");
|
|
751
|
-
}
|
|
752
|
-
} catch (err) {
|
|
753
|
-
addIssue(
|
|
754
|
-
"error",
|
|
755
|
-
"E005",
|
|
756
|
-
`config.json: JSON parse error - ${(err as Error).message}`,
|
|
757
|
-
"Run pi-gsd-tools validate health --repair to reset to defaults",
|
|
758
|
-
true,
|
|
759
|
-
);
|
|
760
|
-
repairs.push("resetConfig");
|
|
761
|
-
}
|
|
762
|
-
}
|
|
763
|
-
try {
|
|
764
|
-
const agentStatus = checkAgentsInstalled();
|
|
765
|
-
if (!agentStatus.agents_installed)
|
|
766
|
-
addIssue(
|
|
767
|
-
"warning",
|
|
768
|
-
"W010",
|
|
769
|
-
agentStatus.installed_agents.length === 0
|
|
770
|
-
? `No GSD agents found in ${agentStatus.agents_dir}`
|
|
771
|
-
: `Missing ${agentStatus.missing_agents.length} GSD agents: ${agentStatus.missing_agents.join(", ")}`,
|
|
772
|
-
"Run the GSD installer: pi install npm:pi-gsd",
|
|
773
|
-
);
|
|
774
|
-
} catch {
|
|
775
|
-
/* non-blocking */
|
|
776
|
-
}
|
|
777
|
-
const repairActions: HealthRepairAction[] = [];
|
|
778
|
-
if (options.repair && repairs.length > 0) {
|
|
779
|
-
for (const repair of repairs) {
|
|
780
|
-
try {
|
|
781
|
-
if (repair === "createConfig" || repair === "resetConfig") {
|
|
782
|
-
// Use PlanningConfigSchema defaults - single source of truth for all fields
|
|
783
|
-
const defaults = PlanningConfigSchema.parse({});
|
|
784
|
-
fs.writeFileSync(
|
|
785
|
-
configPath,
|
|
786
|
-
JSON.stringify(defaults, null, 2),
|
|
787
|
-
"utf-8",
|
|
788
|
-
);
|
|
789
|
-
repairActions.push({
|
|
790
|
-
action: repair,
|
|
791
|
-
success: true,
|
|
792
|
-
path: "config.json",
|
|
793
|
-
});
|
|
794
|
-
} else if (
|
|
795
|
-
repair === "fixSchemaDefaults" &&
|
|
796
|
-
fs.existsSync(configPath)
|
|
797
|
-
) {
|
|
798
|
-
// Merge schema defaults into existing config - fills any missing/invalid fields
|
|
799
|
-
const existing: unknown = JSON.parse(
|
|
800
|
-
fs.readFileSync(configPath, "utf-8"),
|
|
801
|
-
);
|
|
802
|
-
const repaired = PlanningConfigSchema.parse(existing);
|
|
803
|
-
fs.writeFileSync(
|
|
804
|
-
configPath,
|
|
805
|
-
JSON.stringify(repaired, null, 2),
|
|
806
|
-
"utf-8",
|
|
807
|
-
);
|
|
808
|
-
repairActions.push({
|
|
809
|
-
action: repair,
|
|
810
|
-
success: true,
|
|
811
|
-
path: "config.json",
|
|
812
|
-
});
|
|
813
|
-
} else if (repair === "regenerateState") {
|
|
814
|
-
if (fs.existsSync(statePath)) {
|
|
815
|
-
const ts = new Date()
|
|
816
|
-
.toISOString()
|
|
817
|
-
.replace(/[:.]/g, "-")
|
|
818
|
-
.slice(0, 19);
|
|
819
|
-
const bp = `${statePath}.bak-${ts}`;
|
|
820
|
-
fs.copyFileSync(statePath, bp);
|
|
821
|
-
repairActions.push({
|
|
822
|
-
action: "backupState",
|
|
823
|
-
success: true,
|
|
824
|
-
path: bp,
|
|
825
|
-
});
|
|
826
|
-
}
|
|
827
|
-
const milestone = getMilestoneInfo(cwd);
|
|
828
|
-
writeStateMd(
|
|
829
|
-
statePath,
|
|
830
|
-
`# Session State\n\n## Project Reference\n\nSee: .planning/PROJECT.md\n\n## Position\n\n**Milestone:** ${milestone.version} ${milestone.name}\n**Current phase:** (determining...)\n**Status:** Resuming\n\n## Session Log\n\n- ${new Date().toISOString().split("T")[0]}: STATE.md regenerated by /gsd-health --repair\n`,
|
|
831
|
-
cwd,
|
|
832
|
-
);
|
|
833
|
-
repairActions.push({
|
|
834
|
-
action: repair,
|
|
835
|
-
success: true,
|
|
836
|
-
path: "STATE.md",
|
|
837
|
-
});
|
|
838
|
-
}
|
|
839
|
-
} catch (err) {
|
|
840
|
-
repairActions.push({
|
|
841
|
-
action: repair,
|
|
842
|
-
success: false,
|
|
843
|
-
error: (err as Error).message,
|
|
844
|
-
});
|
|
845
|
-
}
|
|
846
|
-
}
|
|
847
|
-
}
|
|
848
|
-
const status =
|
|
849
|
-
errors.length > 0 ? "broken" : warnings.length > 0 ? "degraded" : "healthy";
|
|
850
|
-
output(
|
|
851
|
-
{
|
|
852
|
-
status,
|
|
853
|
-
errors,
|
|
854
|
-
warnings,
|
|
855
|
-
info,
|
|
856
|
-
repairable_count:
|
|
857
|
-
errors.filter((e) => e.repairable).length +
|
|
858
|
-
warnings.filter((w) => w.repairable).length,
|
|
859
|
-
repairs_performed: repairActions.length > 0 ? repairActions : undefined,
|
|
860
|
-
},
|
|
861
|
-
raw,
|
|
862
|
-
);
|
|
863
|
-
}
|
|
864
|
-
|
|
865
|
-
// ─── cmdValidateAgents ────────────────────────────────────────────────────────
|
|
866
|
-
|
|
867
|
-
export function cmdValidateAgents(cwd: string, raw: boolean): void {
|
|
868
|
-
const agentStatus = checkAgentsInstalled();
|
|
869
|
-
output(
|
|
870
|
-
{
|
|
871
|
-
agents_dir: agentStatus.agents_dir,
|
|
872
|
-
agents_found: agentStatus.agents_installed,
|
|
873
|
-
installed: agentStatus.installed_agents,
|
|
874
|
-
missing: agentStatus.missing_agents,
|
|
875
|
-
expected: Object.keys(MODEL_PROFILES),
|
|
876
|
-
},
|
|
877
|
-
raw,
|
|
878
|
-
);
|
|
879
|
-
}
|