@lumenflow/cli 1.6.0 → 2.0.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/dist/__tests__/backlog-prune.test.js +478 -0
- package/dist/__tests__/deps-operations.test.js +206 -0
- package/dist/__tests__/file-operations.test.js +906 -0
- package/dist/__tests__/git-operations.test.js +668 -0
- package/dist/__tests__/guards-validation.test.js +416 -0
- package/dist/__tests__/init-plan.test.js +340 -0
- package/dist/__tests__/lumenflow-upgrade.test.js +107 -0
- package/dist/__tests__/metrics-cli.test.js +619 -0
- package/dist/__tests__/rotate-progress.test.js +127 -0
- package/dist/__tests__/session-coordinator.test.js +109 -0
- package/dist/__tests__/state-bootstrap.test.js +432 -0
- package/dist/__tests__/trace-gen.test.js +115 -0
- package/dist/backlog-prune.js +299 -0
- package/dist/deps-add.js +215 -0
- package/dist/deps-remove.js +94 -0
- package/dist/file-delete.js +236 -0
- package/dist/file-edit.js +247 -0
- package/dist/file-read.js +197 -0
- package/dist/file-write.js +220 -0
- package/dist/git-branch.js +187 -0
- package/dist/git-diff.js +177 -0
- package/dist/git-log.js +230 -0
- package/dist/git-status.js +208 -0
- package/dist/guard-locked.js +169 -0
- package/dist/guard-main-branch.js +202 -0
- package/dist/guard-worktree-commit.js +160 -0
- package/dist/init-plan.js +337 -0
- package/dist/lumenflow-upgrade.js +178 -0
- package/dist/metrics-cli.js +433 -0
- package/dist/rotate-progress.js +247 -0
- package/dist/session-coordinator.js +300 -0
- package/dist/state-bootstrap.js +307 -0
- package/dist/trace-gen.js +331 -0
- package/dist/validate-agent-skills.js +218 -0
- package/dist/validate-agent-sync.js +148 -0
- package/dist/validate-backlog-sync.js +152 -0
- package/dist/validate-skills-spec.js +206 -0
- package/dist/validate.js +230 -0
- package/package.json +34 -6
|
@@ -0,0 +1,331 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Trace Generator CLI Command
|
|
4
|
+
*
|
|
5
|
+
* Creates traceability reports linking WUs to code changes.
|
|
6
|
+
* Useful for audit trails, compliance documentation, and understanding
|
|
7
|
+
* what code was changed as part of each WU.
|
|
8
|
+
*
|
|
9
|
+
* WU-1112: INIT-003 Phase 6 - Migrate remaining Tier 1 tools
|
|
10
|
+
*
|
|
11
|
+
* Usage:
|
|
12
|
+
* pnpm trace:gen --wu WU-1112
|
|
13
|
+
* pnpm trace:gen --since 2024-01-01 --format json
|
|
14
|
+
* pnpm trace:gen --format markdown --output trace.md
|
|
15
|
+
*/
|
|
16
|
+
import { readFileSync, writeFileSync, existsSync, readdirSync } from 'node:fs';
|
|
17
|
+
import { join } from 'node:path';
|
|
18
|
+
import { execSync } from 'node:child_process';
|
|
19
|
+
import { parse as parseYaml } from 'yaml';
|
|
20
|
+
import { EXIT_CODES, DIRECTORIES, FILE_SYSTEM } from '@lumenflow/core/dist/wu-constants.js';
|
|
21
|
+
import { runCLI } from './cli-entry-point.js';
|
|
22
|
+
/** Log prefix for console output */
|
|
23
|
+
const LOG_PREFIX = '[trace:gen]';
|
|
24
|
+
/**
|
|
25
|
+
* Output formats for trace report
|
|
26
|
+
*/
|
|
27
|
+
export var TraceFormat;
|
|
28
|
+
(function (TraceFormat) {
|
|
29
|
+
TraceFormat["JSON"] = "json";
|
|
30
|
+
TraceFormat["MARKDOWN"] = "markdown";
|
|
31
|
+
TraceFormat["CSV"] = "csv";
|
|
32
|
+
})(TraceFormat || (TraceFormat = {}));
|
|
33
|
+
/**
|
|
34
|
+
* Parse command line arguments for trace-gen
|
|
35
|
+
*
|
|
36
|
+
* @param argv - Process argv array
|
|
37
|
+
* @returns Parsed arguments
|
|
38
|
+
*/
|
|
39
|
+
export function parseTraceArgs(argv) {
|
|
40
|
+
const args = {};
|
|
41
|
+
// Skip node and script name
|
|
42
|
+
const cliArgs = argv.slice(2);
|
|
43
|
+
for (let i = 0; i < cliArgs.length; i++) {
|
|
44
|
+
const arg = cliArgs[i];
|
|
45
|
+
if (arg === '--help' || arg === '-h') {
|
|
46
|
+
args.help = true;
|
|
47
|
+
}
|
|
48
|
+
else if (arg === '--wu' || arg === '-w') {
|
|
49
|
+
args.wuId = cliArgs[++i];
|
|
50
|
+
}
|
|
51
|
+
else if (arg === '--format' || arg === '-f') {
|
|
52
|
+
args.format = cliArgs[++i];
|
|
53
|
+
}
|
|
54
|
+
else if (arg === '--output' || arg === '-o') {
|
|
55
|
+
args.output = cliArgs[++i];
|
|
56
|
+
}
|
|
57
|
+
else if (arg === '--since' || arg === '-s') {
|
|
58
|
+
args.since = cliArgs[++i];
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return args;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Build a trace entry from WU and commit data
|
|
65
|
+
*
|
|
66
|
+
* @param input - Input data containing WU info, commits, and files
|
|
67
|
+
* @returns Trace entry with summary statistics
|
|
68
|
+
*/
|
|
69
|
+
export function buildTraceEntry(input) {
|
|
70
|
+
const { wuId, title, status, commits, files } = input;
|
|
71
|
+
const entry = {
|
|
72
|
+
wuId,
|
|
73
|
+
title,
|
|
74
|
+
status,
|
|
75
|
+
commitCount: commits.length,
|
|
76
|
+
fileCount: files.length,
|
|
77
|
+
};
|
|
78
|
+
if (commits.length > 0) {
|
|
79
|
+
// Sort commits by date
|
|
80
|
+
const sorted = [...commits].sort((a, b) => new Date(a.date).getTime() - new Date(b.date).getTime());
|
|
81
|
+
entry.firstCommit = sorted[0].date;
|
|
82
|
+
entry.lastCommit = sorted[sorted.length - 1].date;
|
|
83
|
+
entry.commits = sorted;
|
|
84
|
+
}
|
|
85
|
+
if (files.length > 0) {
|
|
86
|
+
entry.files = files;
|
|
87
|
+
}
|
|
88
|
+
return entry;
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Get commits for a WU by searching git log
|
|
92
|
+
*/
|
|
93
|
+
function getWuCommits(wuId) {
|
|
94
|
+
try {
|
|
95
|
+
const output = execSync(`git log --all --oneline --date=iso-strict --format="%H|%ad|%s" --grep="${wuId}"`, { encoding: FILE_SYSTEM.ENCODING });
|
|
96
|
+
const commits = [];
|
|
97
|
+
for (const line of output.trim().split('\n')) {
|
|
98
|
+
if (!line)
|
|
99
|
+
continue;
|
|
100
|
+
const [sha, date, ...messageParts] = line.split('|');
|
|
101
|
+
commits.push({
|
|
102
|
+
sha: sha.slice(0, 8),
|
|
103
|
+
date,
|
|
104
|
+
message: messageParts.join('|'),
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
return commits;
|
|
108
|
+
}
|
|
109
|
+
catch {
|
|
110
|
+
return [];
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Get files changed by a WU
|
|
115
|
+
*/
|
|
116
|
+
function getWuFiles(wuId) {
|
|
117
|
+
try {
|
|
118
|
+
const output = execSync(`git log --all --name-only --format="" --grep="${wuId}" | sort -u`, {
|
|
119
|
+
encoding: FILE_SYSTEM.ENCODING,
|
|
120
|
+
});
|
|
121
|
+
return output
|
|
122
|
+
.trim()
|
|
123
|
+
.split('\n')
|
|
124
|
+
.filter((f) => f.length > 0);
|
|
125
|
+
}
|
|
126
|
+
catch {
|
|
127
|
+
return [];
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Get WU info from YAML file
|
|
132
|
+
*/
|
|
133
|
+
function getWuInfo(wuId) {
|
|
134
|
+
const yamlPath = join(process.cwd(), DIRECTORIES.WU_DIR, `${wuId}.yaml`);
|
|
135
|
+
if (!existsSync(yamlPath)) {
|
|
136
|
+
return null;
|
|
137
|
+
}
|
|
138
|
+
try {
|
|
139
|
+
const content = readFileSync(yamlPath, { encoding: FILE_SYSTEM.ENCODING });
|
|
140
|
+
const yaml = parseYaml(content);
|
|
141
|
+
return {
|
|
142
|
+
title: yaml?.title || wuId,
|
|
143
|
+
status: yaml?.status || 'unknown',
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
catch {
|
|
147
|
+
return null;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* Format trace report as JSON
|
|
152
|
+
*/
|
|
153
|
+
function formatJson(entries) {
|
|
154
|
+
return JSON.stringify(entries, null, 2);
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* Format trace report as Markdown
|
|
158
|
+
*/
|
|
159
|
+
function formatMarkdown(entries) {
|
|
160
|
+
const lines = [
|
|
161
|
+
'# Traceability Report',
|
|
162
|
+
'',
|
|
163
|
+
`Generated: ${new Date().toISOString()}`,
|
|
164
|
+
'',
|
|
165
|
+
'## WU Summary',
|
|
166
|
+
'',
|
|
167
|
+
'| WU ID | Title | Status | Commits | Files |',
|
|
168
|
+
'|-------|-------|--------|---------|-------|',
|
|
169
|
+
];
|
|
170
|
+
for (const entry of entries) {
|
|
171
|
+
lines.push(`| ${entry.wuId} | ${entry.title.slice(0, 30)} | ${entry.status} | ${entry.commitCount} | ${entry.fileCount} |`);
|
|
172
|
+
}
|
|
173
|
+
lines.push('', '## Details', '');
|
|
174
|
+
for (const entry of entries) {
|
|
175
|
+
lines.push(`### ${entry.wuId}: ${entry.title}`, '');
|
|
176
|
+
lines.push(`- **Status:** ${entry.status}`);
|
|
177
|
+
lines.push(`- **Commits:** ${entry.commitCount}`);
|
|
178
|
+
lines.push(`- **Files:** ${entry.fileCount}`);
|
|
179
|
+
if (entry.firstCommit) {
|
|
180
|
+
lines.push(`- **First commit:** ${entry.firstCommit}`);
|
|
181
|
+
lines.push(`- **Last commit:** ${entry.lastCommit}`);
|
|
182
|
+
}
|
|
183
|
+
if (entry.files && entry.files.length > 0) {
|
|
184
|
+
lines.push('', '**Files changed:**');
|
|
185
|
+
for (const file of entry.files.slice(0, 20)) {
|
|
186
|
+
lines.push(`- ${file}`);
|
|
187
|
+
}
|
|
188
|
+
if (entry.files.length > 20) {
|
|
189
|
+
lines.push(`- ... and ${entry.files.length - 20} more`);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
lines.push('');
|
|
193
|
+
}
|
|
194
|
+
return lines.join('\n');
|
|
195
|
+
}
|
|
196
|
+
/**
|
|
197
|
+
* Format trace report as CSV
|
|
198
|
+
*/
|
|
199
|
+
function formatCsv(entries) {
|
|
200
|
+
const lines = ['WU ID,Title,Status,Commits,Files,First Commit,Last Commit'];
|
|
201
|
+
for (const entry of entries) {
|
|
202
|
+
const title = entry.title.replace(/,/g, ';');
|
|
203
|
+
lines.push(`${entry.wuId},"${title}",${entry.status},${entry.commitCount},${entry.fileCount},${entry.firstCommit || ''},${entry.lastCommit || ''}`);
|
|
204
|
+
}
|
|
205
|
+
return lines.join('\n');
|
|
206
|
+
}
|
|
207
|
+
/**
|
|
208
|
+
* Print help message for trace-gen
|
|
209
|
+
*/
|
|
210
|
+
/* istanbul ignore next -- CLI entry point */
|
|
211
|
+
function printHelp() {
|
|
212
|
+
console.log(`
|
|
213
|
+
Usage: trace-gen [options]
|
|
214
|
+
|
|
215
|
+
Generate traceability reports linking WUs to code changes.
|
|
216
|
+
|
|
217
|
+
Options:
|
|
218
|
+
-w, --wu <id> Trace specific WU (otherwise traces all)
|
|
219
|
+
-f, --format <fmt> Output format: json, markdown, csv (default: json)
|
|
220
|
+
-o, --output <file> Write output to file (default: stdout)
|
|
221
|
+
-s, --since <date> Only trace WUs modified since date (ISO format)
|
|
222
|
+
-h, --help Show this help message
|
|
223
|
+
|
|
224
|
+
Examples:
|
|
225
|
+
trace:gen --wu WU-1112 # Trace single WU
|
|
226
|
+
trace:gen --format markdown --output report.md # Markdown report
|
|
227
|
+
trace:gen --since 2024-01-01 --format csv # CSV report since date
|
|
228
|
+
|
|
229
|
+
Output includes:
|
|
230
|
+
- WU ID and title
|
|
231
|
+
- Status
|
|
232
|
+
- Number of commits
|
|
233
|
+
- Number of files changed
|
|
234
|
+
- First and last commit dates
|
|
235
|
+
- List of changed files
|
|
236
|
+
`);
|
|
237
|
+
}
|
|
238
|
+
/**
|
|
239
|
+
* Main entry point for trace-gen command
|
|
240
|
+
*/
|
|
241
|
+
/* istanbul ignore next -- CLI entry point */
|
|
242
|
+
async function main() {
|
|
243
|
+
const args = parseTraceArgs(process.argv);
|
|
244
|
+
if (args.help) {
|
|
245
|
+
printHelp();
|
|
246
|
+
process.exit(EXIT_CODES.SUCCESS);
|
|
247
|
+
}
|
|
248
|
+
const format = args.format || TraceFormat.JSON;
|
|
249
|
+
const entries = [];
|
|
250
|
+
if (args.wuId) {
|
|
251
|
+
// Trace single WU
|
|
252
|
+
console.error(`${LOG_PREFIX} Tracing ${args.wuId}...`);
|
|
253
|
+
const info = getWuInfo(args.wuId);
|
|
254
|
+
if (!info) {
|
|
255
|
+
console.error(`${LOG_PREFIX} Error: WU ${args.wuId} not found`);
|
|
256
|
+
process.exit(EXIT_CODES.ERROR);
|
|
257
|
+
}
|
|
258
|
+
const commits = getWuCommits(args.wuId);
|
|
259
|
+
const files = getWuFiles(args.wuId);
|
|
260
|
+
entries.push(buildTraceEntry({
|
|
261
|
+
wuId: args.wuId,
|
|
262
|
+
title: info.title,
|
|
263
|
+
status: info.status,
|
|
264
|
+
commits,
|
|
265
|
+
files,
|
|
266
|
+
}));
|
|
267
|
+
}
|
|
268
|
+
else {
|
|
269
|
+
// Trace all WUs
|
|
270
|
+
console.error(`${LOG_PREFIX} Scanning all WUs...`);
|
|
271
|
+
const wuDir = join(process.cwd(), DIRECTORIES.WU_DIR);
|
|
272
|
+
if (!existsSync(wuDir)) {
|
|
273
|
+
console.error(`${LOG_PREFIX} Error: WU directory not found`);
|
|
274
|
+
process.exit(EXIT_CODES.ERROR);
|
|
275
|
+
}
|
|
276
|
+
const files = readdirSync(wuDir);
|
|
277
|
+
for (const file of files) {
|
|
278
|
+
if (!file.endsWith('.yaml') && !file.endsWith('.yml'))
|
|
279
|
+
continue;
|
|
280
|
+
const wuId = file.replace(/\.ya?ml$/, '');
|
|
281
|
+
const info = getWuInfo(wuId);
|
|
282
|
+
if (!info)
|
|
283
|
+
continue;
|
|
284
|
+
// Filter by since date if specified
|
|
285
|
+
if (args.since) {
|
|
286
|
+
const commits = getWuCommits(wuId);
|
|
287
|
+
if (commits.length === 0)
|
|
288
|
+
continue;
|
|
289
|
+
const lastCommitDate = new Date(commits[commits.length - 1]?.date || 0);
|
|
290
|
+
if (lastCommitDate < new Date(args.since))
|
|
291
|
+
continue;
|
|
292
|
+
}
|
|
293
|
+
const commits = getWuCommits(wuId);
|
|
294
|
+
const wuFiles = getWuFiles(wuId);
|
|
295
|
+
entries.push(buildTraceEntry({
|
|
296
|
+
wuId,
|
|
297
|
+
title: info.title,
|
|
298
|
+
status: info.status,
|
|
299
|
+
commits,
|
|
300
|
+
files: wuFiles,
|
|
301
|
+
}));
|
|
302
|
+
}
|
|
303
|
+
console.error(`${LOG_PREFIX} Found ${entries.length} WU(s)`);
|
|
304
|
+
}
|
|
305
|
+
// Format output
|
|
306
|
+
let output;
|
|
307
|
+
switch (format) {
|
|
308
|
+
case TraceFormat.MARKDOWN:
|
|
309
|
+
output = formatMarkdown(entries);
|
|
310
|
+
break;
|
|
311
|
+
case TraceFormat.CSV:
|
|
312
|
+
output = formatCsv(entries);
|
|
313
|
+
break;
|
|
314
|
+
case TraceFormat.JSON:
|
|
315
|
+
default:
|
|
316
|
+
output = formatJson(entries);
|
|
317
|
+
break;
|
|
318
|
+
}
|
|
319
|
+
// Write output
|
|
320
|
+
if (args.output) {
|
|
321
|
+
writeFileSync(args.output, output, { encoding: FILE_SYSTEM.ENCODING });
|
|
322
|
+
console.error(`${LOG_PREFIX} ✅ Report written to ${args.output}`);
|
|
323
|
+
}
|
|
324
|
+
else {
|
|
325
|
+
console.log(output);
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
// Run main if executed directly
|
|
329
|
+
if (import.meta.main) {
|
|
330
|
+
runCLI(main);
|
|
331
|
+
}
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* @file validate-agent-skills.ts
|
|
4
|
+
* @description Validates agent skill definitions (WU-1111)
|
|
5
|
+
*
|
|
6
|
+
* Validates that skill files in .claude/skills/ follow the expected format
|
|
7
|
+
* and contain required sections.
|
|
8
|
+
*
|
|
9
|
+
* Usage:
|
|
10
|
+
* validate-agent-skills # Validate all skills
|
|
11
|
+
* validate-agent-skills --skill wu-lifecycle # Validate specific skill
|
|
12
|
+
*
|
|
13
|
+
* Exit codes:
|
|
14
|
+
* 0 - All skills valid
|
|
15
|
+
* 1 - Validation errors found
|
|
16
|
+
*
|
|
17
|
+
* @see {@link .claude/skills/} - Skill definitions
|
|
18
|
+
*/
|
|
19
|
+
import { existsSync, readFileSync, readdirSync, statSync } from 'node:fs';
|
|
20
|
+
import path from 'node:path';
|
|
21
|
+
import { fileURLToPath } from 'node:url';
|
|
22
|
+
import { FILE_SYSTEM, EMOJI } from '@lumenflow/core/dist/wu-constants.js';
|
|
23
|
+
const LOG_PREFIX = '[validate-agent-skills]';
|
|
24
|
+
/**
|
|
25
|
+
* Required sections in a skill file
|
|
26
|
+
*/
|
|
27
|
+
const REQUIRED_SECTIONS = ['When to Use'];
|
|
28
|
+
/**
|
|
29
|
+
* Recommended sections (produce warnings if missing)
|
|
30
|
+
*/
|
|
31
|
+
const RECOMMENDED_SECTIONS = ['Examples', 'Key Concepts', 'Core Concepts'];
|
|
32
|
+
/**
|
|
33
|
+
* Validate a single skill file
|
|
34
|
+
*
|
|
35
|
+
* @param skillPath - Path to SKILL.md file
|
|
36
|
+
* @returns Validation result
|
|
37
|
+
*/
|
|
38
|
+
export function validateSkillFile(skillPath) {
|
|
39
|
+
const errors = [];
|
|
40
|
+
const warnings = [];
|
|
41
|
+
if (!existsSync(skillPath)) {
|
|
42
|
+
errors.push(`Skill file not found: ${skillPath}`);
|
|
43
|
+
return { valid: false, errors, warnings };
|
|
44
|
+
}
|
|
45
|
+
const content = readFileSync(skillPath, { encoding: FILE_SYSTEM.UTF8 });
|
|
46
|
+
const lines = content.split('\n');
|
|
47
|
+
// Check for title heading
|
|
48
|
+
const hasTitle = lines.some((line) => line.startsWith('# '));
|
|
49
|
+
if (!hasTitle) {
|
|
50
|
+
errors.push('Missing title heading (# Skill Name)');
|
|
51
|
+
}
|
|
52
|
+
// Check for required sections
|
|
53
|
+
for (const section of REQUIRED_SECTIONS) {
|
|
54
|
+
const sectionPattern = new RegExp(`^##\\s+${section}`, 'im');
|
|
55
|
+
if (!sectionPattern.test(content)) {
|
|
56
|
+
errors.push(`Missing required section: "## ${section}"`);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
// Check for recommended sections
|
|
60
|
+
for (const section of RECOMMENDED_SECTIONS) {
|
|
61
|
+
const sectionPattern = new RegExp(`^##\\s+${section}`, 'im');
|
|
62
|
+
if (!sectionPattern.test(content)) {
|
|
63
|
+
warnings.push(`Missing recommended section: "## ${section}"`);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
// Check for minimum content
|
|
67
|
+
if (content.length < 100) {
|
|
68
|
+
warnings.push('Skill content is very short (< 100 characters)');
|
|
69
|
+
}
|
|
70
|
+
return {
|
|
71
|
+
valid: errors.length === 0,
|
|
72
|
+
errors,
|
|
73
|
+
warnings,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Validate all skills in a directory
|
|
78
|
+
*
|
|
79
|
+
* @param skillsDir - Path to skills directory
|
|
80
|
+
* @returns Validation summary
|
|
81
|
+
*/
|
|
82
|
+
export function validateAllSkills(skillsDir) {
|
|
83
|
+
const results = [];
|
|
84
|
+
let totalValid = 0;
|
|
85
|
+
let totalInvalid = 0;
|
|
86
|
+
if (!existsSync(skillsDir)) {
|
|
87
|
+
return {
|
|
88
|
+
totalValid: 0,
|
|
89
|
+
totalInvalid: 1,
|
|
90
|
+
results: [
|
|
91
|
+
{
|
|
92
|
+
skillName: 'DIRECTORY',
|
|
93
|
+
valid: false,
|
|
94
|
+
errors: [`Skills directory not found: ${skillsDir}`],
|
|
95
|
+
warnings: [],
|
|
96
|
+
},
|
|
97
|
+
],
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
const entries = readdirSync(skillsDir);
|
|
101
|
+
for (const entry of entries) {
|
|
102
|
+
const entryPath = path.join(skillsDir, entry);
|
|
103
|
+
const stat = statSync(entryPath);
|
|
104
|
+
if (!stat.isDirectory()) {
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
// Check for SKILL.md in the skill directory
|
|
108
|
+
const skillFile = path.join(entryPath, 'SKILL.md');
|
|
109
|
+
if (!existsSync(skillFile)) {
|
|
110
|
+
results.push({
|
|
111
|
+
skillName: entry,
|
|
112
|
+
valid: false,
|
|
113
|
+
errors: [`Missing SKILL.md in ${entry}/`],
|
|
114
|
+
warnings: [],
|
|
115
|
+
});
|
|
116
|
+
totalInvalid++;
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
const result = validateSkillFile(skillFile);
|
|
120
|
+
results.push({ skillName: entry, ...result });
|
|
121
|
+
if (result.valid) {
|
|
122
|
+
totalValid++;
|
|
123
|
+
}
|
|
124
|
+
else {
|
|
125
|
+
totalInvalid++;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
return { totalValid, totalInvalid, results };
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Get default skills directory based on cwd
|
|
132
|
+
*/
|
|
133
|
+
function getDefaultSkillsDir() {
|
|
134
|
+
return path.join(process.cwd(), '.claude', 'skills');
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* Main CLI entry point
|
|
138
|
+
*/
|
|
139
|
+
async function main() {
|
|
140
|
+
const args = process.argv.slice(2);
|
|
141
|
+
// Parse arguments
|
|
142
|
+
let skillName;
|
|
143
|
+
let skillsDir = getDefaultSkillsDir();
|
|
144
|
+
for (let i = 0; i < args.length; i++) {
|
|
145
|
+
const arg = args[i];
|
|
146
|
+
if (arg === '--skill' || arg === '-s') {
|
|
147
|
+
skillName = args[++i];
|
|
148
|
+
}
|
|
149
|
+
else if (arg === '--dir' || arg === '-d') {
|
|
150
|
+
skillsDir = args[++i];
|
|
151
|
+
}
|
|
152
|
+
else if (arg === '--help' || arg === '-h') {
|
|
153
|
+
console.log(`Usage: validate-agent-skills [options]
|
|
154
|
+
|
|
155
|
+
Validate agent skill definitions.
|
|
156
|
+
|
|
157
|
+
Options:
|
|
158
|
+
--skill, -s NAME Validate specific skill
|
|
159
|
+
--dir, -d DIR Skills directory (default: .claude/skills)
|
|
160
|
+
-h, --help Show this help message
|
|
161
|
+
|
|
162
|
+
Examples:
|
|
163
|
+
validate-agent-skills # Validate all skills
|
|
164
|
+
validate-agent-skills --skill wu-lifecycle # Validate specific skill
|
|
165
|
+
`);
|
|
166
|
+
process.exit(0);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
if (skillName) {
|
|
170
|
+
// Validate specific skill
|
|
171
|
+
const skillPath = path.join(skillsDir, skillName, 'SKILL.md');
|
|
172
|
+
console.log(`${LOG_PREFIX} Validating skill: ${skillName}...`);
|
|
173
|
+
const result = validateSkillFile(skillPath);
|
|
174
|
+
if (result.errors.length > 0) {
|
|
175
|
+
console.log(`${EMOJI.FAILURE} Validation failed:`);
|
|
176
|
+
result.errors.forEach((e) => console.log(` ${e}`));
|
|
177
|
+
}
|
|
178
|
+
if (result.warnings.length > 0) {
|
|
179
|
+
console.log(`${EMOJI.WARNING} Warnings:`);
|
|
180
|
+
result.warnings.forEach((w) => console.log(` ${w}`));
|
|
181
|
+
}
|
|
182
|
+
if (result.valid) {
|
|
183
|
+
console.log(`${EMOJI.SUCCESS} ${skillName} is valid`);
|
|
184
|
+
}
|
|
185
|
+
else {
|
|
186
|
+
process.exit(1);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
else {
|
|
190
|
+
// Validate all skills
|
|
191
|
+
console.log(`${LOG_PREFIX} Validating all skills in ${skillsDir}...`);
|
|
192
|
+
const { totalValid, totalInvalid, results } = validateAllSkills(skillsDir);
|
|
193
|
+
// Print results
|
|
194
|
+
for (const result of results) {
|
|
195
|
+
if (result.errors.length > 0) {
|
|
196
|
+
console.log(`${EMOJI.FAILURE} ${result.skillName}:`);
|
|
197
|
+
result.errors.forEach((e) => console.log(` ${e}`));
|
|
198
|
+
}
|
|
199
|
+
if (result.warnings.length > 0) {
|
|
200
|
+
console.log(`${EMOJI.WARNING} ${result.skillName}: ${result.warnings.length} warning(s)`);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
console.log('');
|
|
204
|
+
console.log(`${LOG_PREFIX} Summary:`);
|
|
205
|
+
console.log(` ${EMOJI.SUCCESS} Valid: ${totalValid}`);
|
|
206
|
+
console.log(` ${EMOJI.FAILURE} Invalid: ${totalInvalid}`);
|
|
207
|
+
if (totalInvalid > 0) {
|
|
208
|
+
process.exit(1);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
// Guard main() for testability
|
|
213
|
+
if (process.argv[1] === fileURLToPath(import.meta.url)) {
|
|
214
|
+
main().catch((error) => {
|
|
215
|
+
console.error(`${LOG_PREFIX} Unexpected error:`, error);
|
|
216
|
+
process.exit(1);
|
|
217
|
+
});
|
|
218
|
+
}
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* @file validate-agent-sync.ts
|
|
4
|
+
* @description Validates agent sync state (WU-1111)
|
|
5
|
+
*
|
|
6
|
+
* Validates that agent configuration files exist and are properly structured.
|
|
7
|
+
* Checks .claude/agents/ for valid agent definitions.
|
|
8
|
+
*
|
|
9
|
+
* Usage:
|
|
10
|
+
* validate-agent-sync # Validate agent configuration
|
|
11
|
+
*
|
|
12
|
+
* Exit codes:
|
|
13
|
+
* 0 - Agent configuration valid
|
|
14
|
+
* 1 - Validation errors found
|
|
15
|
+
*
|
|
16
|
+
* @see {@link .claude/agents/} - Agent definitions
|
|
17
|
+
*/
|
|
18
|
+
import { existsSync, readFileSync, readdirSync } from 'node:fs';
|
|
19
|
+
import path from 'node:path';
|
|
20
|
+
import { fileURLToPath } from 'node:url';
|
|
21
|
+
import { FILE_SYSTEM, EMOJI } from '@lumenflow/core/dist/wu-constants.js';
|
|
22
|
+
const LOG_PREFIX = '[validate-agent-sync]';
|
|
23
|
+
/**
|
|
24
|
+
* Validate agent sync state
|
|
25
|
+
*
|
|
26
|
+
* @param options - Validation options
|
|
27
|
+
* @param options.cwd - Working directory (default: process.cwd())
|
|
28
|
+
* @returns Validation result
|
|
29
|
+
*/
|
|
30
|
+
export async function validateAgentSync(options = {}) {
|
|
31
|
+
const { cwd = process.cwd() } = options;
|
|
32
|
+
const errors = [];
|
|
33
|
+
const warnings = [];
|
|
34
|
+
const agents = [];
|
|
35
|
+
const agentDir = path.join(cwd, '.claude', 'agents');
|
|
36
|
+
// Check if agents directory exists
|
|
37
|
+
if (!existsSync(agentDir)) {
|
|
38
|
+
errors.push(`Agents directory not found: ${agentDir}`);
|
|
39
|
+
return { valid: false, errors, warnings, agents };
|
|
40
|
+
}
|
|
41
|
+
// Read agent definitions
|
|
42
|
+
const files = readdirSync(agentDir).filter((f) => f.endsWith('.json') || f.endsWith('.md'));
|
|
43
|
+
if (files.length === 0) {
|
|
44
|
+
warnings.push('No agent definitions found in .claude/agents/');
|
|
45
|
+
return { valid: true, errors, warnings, agents };
|
|
46
|
+
}
|
|
47
|
+
for (const file of files) {
|
|
48
|
+
const filePath = path.join(agentDir, file);
|
|
49
|
+
const agentName = path.basename(file, path.extname(file));
|
|
50
|
+
agents.push(agentName);
|
|
51
|
+
if (file.endsWith('.json')) {
|
|
52
|
+
// Validate JSON agent definition
|
|
53
|
+
try {
|
|
54
|
+
const content = readFileSync(filePath, { encoding: FILE_SYSTEM.UTF8 });
|
|
55
|
+
const agentDef = JSON.parse(content);
|
|
56
|
+
// Check required fields
|
|
57
|
+
if (!agentDef.name) {
|
|
58
|
+
warnings.push(`${agentName}: Missing "name" field`);
|
|
59
|
+
}
|
|
60
|
+
if (!agentDef.description) {
|
|
61
|
+
warnings.push(`${agentName}: Missing "description" field`);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
catch (e) {
|
|
65
|
+
errors.push(`${agentName}: Failed to parse JSON: ${e.message}`);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
else if (file.endsWith('.md')) {
|
|
69
|
+
// Validate markdown agent definition
|
|
70
|
+
try {
|
|
71
|
+
const content = readFileSync(filePath, { encoding: FILE_SYSTEM.UTF8 });
|
|
72
|
+
// Check for title
|
|
73
|
+
if (!content.includes('# ')) {
|
|
74
|
+
warnings.push(`${agentName}: Missing title heading`);
|
|
75
|
+
}
|
|
76
|
+
// Check for minimum content
|
|
77
|
+
if (content.length < 50) {
|
|
78
|
+
warnings.push(`${agentName}: Agent definition is very short`);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
catch (e) {
|
|
82
|
+
errors.push(`${agentName}: Failed to read file: ${e.message}`);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return {
|
|
87
|
+
valid: errors.length === 0,
|
|
88
|
+
errors,
|
|
89
|
+
warnings,
|
|
90
|
+
agents,
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Main CLI entry point
|
|
95
|
+
*/
|
|
96
|
+
async function main() {
|
|
97
|
+
const args = process.argv.slice(2);
|
|
98
|
+
// Parse arguments
|
|
99
|
+
let cwd = process.cwd();
|
|
100
|
+
for (let i = 0; i < args.length; i++) {
|
|
101
|
+
const arg = args[i];
|
|
102
|
+
if (arg === '--cwd' || arg === '-C') {
|
|
103
|
+
cwd = args[++i];
|
|
104
|
+
}
|
|
105
|
+
else if (arg === '--help' || arg === '-h') {
|
|
106
|
+
console.log(`Usage: validate-agent-sync [options]
|
|
107
|
+
|
|
108
|
+
Validate agent configuration and sync state.
|
|
109
|
+
|
|
110
|
+
Options:
|
|
111
|
+
--cwd, -C DIR Working directory (default: current directory)
|
|
112
|
+
-h, --help Show this help message
|
|
113
|
+
|
|
114
|
+
Examples:
|
|
115
|
+
validate-agent-sync
|
|
116
|
+
validate-agent-sync --cwd /path/to/project
|
|
117
|
+
`);
|
|
118
|
+
process.exit(0);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
console.log(`${LOG_PREFIX} Validating agent sync...`);
|
|
122
|
+
const result = await validateAgentSync({ cwd });
|
|
123
|
+
if (result.errors.length > 0) {
|
|
124
|
+
console.log(`${EMOJI.FAILURE} Validation errors:`);
|
|
125
|
+
result.errors.forEach((e) => console.log(` ${e}`));
|
|
126
|
+
}
|
|
127
|
+
if (result.warnings.length > 0) {
|
|
128
|
+
console.log(`${EMOJI.WARNING} Warnings:`);
|
|
129
|
+
result.warnings.forEach((w) => console.log(` ${w}`));
|
|
130
|
+
}
|
|
131
|
+
if (result.agents.length > 0) {
|
|
132
|
+
console.log(`${LOG_PREFIX} Found ${result.agents.length} agent(s):`);
|
|
133
|
+
result.agents.forEach((a) => console.log(` - ${a}`));
|
|
134
|
+
}
|
|
135
|
+
if (result.valid) {
|
|
136
|
+
console.log(`${EMOJI.SUCCESS} Agent sync validation passed`);
|
|
137
|
+
}
|
|
138
|
+
else {
|
|
139
|
+
process.exit(1);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
// Guard main() for testability
|
|
143
|
+
if (process.argv[1] === fileURLToPath(import.meta.url)) {
|
|
144
|
+
main().catch((error) => {
|
|
145
|
+
console.error(`${LOG_PREFIX} Unexpected error:`, error);
|
|
146
|
+
process.exit(1);
|
|
147
|
+
});
|
|
148
|
+
}
|