@lumenflow/cli 1.6.0 → 2.1.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.
Files changed (42) hide show
  1. package/README.md +19 -0
  2. package/dist/__tests__/backlog-prune.test.js +478 -0
  3. package/dist/__tests__/deps-operations.test.js +206 -0
  4. package/dist/__tests__/file-operations.test.js +906 -0
  5. package/dist/__tests__/git-operations.test.js +668 -0
  6. package/dist/__tests__/guards-validation.test.js +416 -0
  7. package/dist/__tests__/init-plan.test.js +340 -0
  8. package/dist/__tests__/lumenflow-upgrade.test.js +107 -0
  9. package/dist/__tests__/metrics-cli.test.js +619 -0
  10. package/dist/__tests__/rotate-progress.test.js +127 -0
  11. package/dist/__tests__/session-coordinator.test.js +109 -0
  12. package/dist/__tests__/state-bootstrap.test.js +432 -0
  13. package/dist/__tests__/trace-gen.test.js +115 -0
  14. package/dist/backlog-prune.js +299 -0
  15. package/dist/deps-add.js +215 -0
  16. package/dist/deps-remove.js +94 -0
  17. package/dist/docs-sync.js +72 -326
  18. package/dist/file-delete.js +236 -0
  19. package/dist/file-edit.js +247 -0
  20. package/dist/file-read.js +197 -0
  21. package/dist/file-write.js +220 -0
  22. package/dist/git-branch.js +187 -0
  23. package/dist/git-diff.js +177 -0
  24. package/dist/git-log.js +230 -0
  25. package/dist/git-status.js +208 -0
  26. package/dist/guard-locked.js +169 -0
  27. package/dist/guard-main-branch.js +202 -0
  28. package/dist/guard-worktree-commit.js +160 -0
  29. package/dist/init-plan.js +337 -0
  30. package/dist/lumenflow-upgrade.js +178 -0
  31. package/dist/metrics-cli.js +433 -0
  32. package/dist/rotate-progress.js +247 -0
  33. package/dist/session-coordinator.js +300 -0
  34. package/dist/state-bootstrap.js +307 -0
  35. package/dist/sync-templates.js +212 -0
  36. package/dist/trace-gen.js +331 -0
  37. package/dist/validate-agent-skills.js +218 -0
  38. package/dist/validate-agent-sync.js +148 -0
  39. package/dist/validate-backlog-sync.js +152 -0
  40. package/dist/validate-skills-spec.js +206 -0
  41. package/dist/validate.js +230 -0
  42. package/package.json +37 -7
@@ -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
+ }