cc-dev-template 0.1.31 → 0.1.33
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/bin/install.js +5 -9
- package/package.json +1 -1
- package/src/commands/done.md +52 -0
- package/src/commands/prime.md +40 -4
- package/src/hooks/bash-precheck.sh +2 -2
- package/src/mcp-servers/qa-server/README.md +10 -8
- package/src/mcp-servers/qa-server/src/index.ts +20 -27
- package/src/skills/initialize-project/SKILL.md +88 -0
- package/src/agents/adr-agent.md +0 -167
- package/src/agents/claude-md-agent.md +0 -124
- package/src/agents/decomposition-agent.md +0 -170
- package/src/agents/execution-agent.md +0 -232
- package/src/agents/rca-agent.md +0 -192
- package/src/agents/tdd-agent.md +0 -205
- package/src/commands/create-agent-skill.md +0 -11
- package/src/commands/finalize.md +0 -48
- package/src/commands/heal-skill.md +0 -69
- package/src/hooks/orchestration-guidance.sh +0 -56
- package/src/hooks/orchestration-hook.json +0 -14
- package/src/scripts/adr-list.js +0 -298
- package/src/scripts/adr-tags.js +0 -242
- package/src/scripts/validate-yaml.js +0 -142
- package/src/scripts/yaml-validation-hook.json +0 -15
- package/src/skills/orchestration/SKILL.md +0 -161
- package/src/skills/orchestration/references/debugging/describe.md +0 -144
- package/src/skills/orchestration/references/debugging/fix.md +0 -117
- package/src/skills/orchestration/references/debugging/learn.md +0 -185
- package/src/skills/orchestration/references/debugging/rca.md +0 -92
- package/src/skills/orchestration/references/debugging/verify.md +0 -102
- package/src/skills/orchestration/references/execution/complete.md +0 -175
- package/src/skills/orchestration/references/execution/start.md +0 -77
- package/src/skills/orchestration/references/execution/tasks.md +0 -114
- package/src/skills/orchestration/references/planning/draft.md +0 -269
- package/src/skills/orchestration/references/planning/explore.md +0 -160
- package/src/skills/orchestration/references/planning/finalize.md +0 -184
- package/src/skills/orchestration/references/planning/start.md +0 -119
- package/src/skills/orchestration/scripts/plan-status.js +0 -355
package/src/scripts/adr-list.js
DELETED
|
@@ -1,298 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* adr-list.js - List ADRs with optional filtering by tags and status
|
|
5
|
-
*
|
|
6
|
-
* This CLI script scans all YAML ADR files in the project's .claude/adrs/
|
|
7
|
-
* and returns their metadata, with optional filtering capabilities. Designed
|
|
8
|
-
* to support progressive ADR filtering (per ADR-007) by providing descriptions
|
|
9
|
-
* without full content.
|
|
10
|
-
*
|
|
11
|
-
* Usage:
|
|
12
|
-
* node ~/.claude/scripts/adr-list.js # All ADRs
|
|
13
|
-
* node ~/.claude/scripts/adr-list.js --tags=architecture # Filter by single tag
|
|
14
|
-
* node ~/.claude/scripts/adr-list.js --tags=agents,planning # Filter by multiple tags (OR logic)
|
|
15
|
-
* node ~/.claude/scripts/adr-list.js --status=Accepted # Filter by status
|
|
16
|
-
* node ~/.claude/scripts/adr-list.js --tags=adrs --status=Accepted # Combine filters
|
|
17
|
-
* node ~/.claude/scripts/adr-list.js --include-parent # Include parent repo ADRs (in submodule)
|
|
18
|
-
*
|
|
19
|
-
* Flags:
|
|
20
|
-
* --tags=tag1,tag2 Filter ADRs that have ANY of the specified tags (OR logic)
|
|
21
|
-
* --status=Status Filter ADRs that match the exact status string
|
|
22
|
-
* --include-parent When in a git submodule, also scan parent repo's .claude/adrs/
|
|
23
|
-
*
|
|
24
|
-
* Output:
|
|
25
|
-
* JSON array to stdout: [{ id, title, description, tags, status, date, source, sourcePath }]
|
|
26
|
-
*
|
|
27
|
-
* Example output:
|
|
28
|
-
* [
|
|
29
|
-
* {
|
|
30
|
-
* "id": "ADR-001",
|
|
31
|
-
* "title": "Use YAML ADRs for Architectural Decisions",
|
|
32
|
-
* "description": "Establishes YAML-formatted ADRs as the primary mechanism...",
|
|
33
|
-
* "tags": ["architecture", "adrs", "format"],
|
|
34
|
-
* "status": "Accepted",
|
|
35
|
-
* "date": "2025-11-22",
|
|
36
|
-
* "source": "local",
|
|
37
|
-
* "sourcePath": "/path/to/project"
|
|
38
|
-
* }
|
|
39
|
-
* ]
|
|
40
|
-
*
|
|
41
|
-
* Note: This script supplements the adr-agent (per ADR-009), providing
|
|
42
|
-
* quick CLI access to ADR listings without invoking the full agent.
|
|
43
|
-
*/
|
|
44
|
-
|
|
45
|
-
const fs = require('fs');
|
|
46
|
-
const path = require('path');
|
|
47
|
-
const yaml = require('js-yaml');
|
|
48
|
-
const { execSync } = require('child_process');
|
|
49
|
-
|
|
50
|
-
// ADRs are in the project's .claude/adrs/ directory (relative to CWD)
|
|
51
|
-
const ADRS_DIR = path.join(process.cwd(), '.claude', 'adrs');
|
|
52
|
-
|
|
53
|
-
/**
|
|
54
|
-
* Parse command line arguments for --tags, --status, and --include-parent flags
|
|
55
|
-
* @returns {{ tags: string[] | null, status: string | null, includeParent: boolean }}
|
|
56
|
-
*/
|
|
57
|
-
function parseArgs() {
|
|
58
|
-
const args = process.argv.slice(2);
|
|
59
|
-
let tags = null;
|
|
60
|
-
let status = null;
|
|
61
|
-
let includeParent = false;
|
|
62
|
-
|
|
63
|
-
for (const arg of args) {
|
|
64
|
-
// Handle --tags=value or --tags value
|
|
65
|
-
if (arg.startsWith('--tags=')) {
|
|
66
|
-
const value = arg.slice('--tags='.length);
|
|
67
|
-
tags = value.split(',').map(t => t.trim()).filter(t => t.length > 0);
|
|
68
|
-
}
|
|
69
|
-
// Handle --status=value or --status value
|
|
70
|
-
else if (arg.startsWith('--status=')) {
|
|
71
|
-
status = arg.slice('--status='.length).trim();
|
|
72
|
-
}
|
|
73
|
-
// Handle --include-parent flag
|
|
74
|
-
else if (arg === '--include-parent') {
|
|
75
|
-
includeParent = true;
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
return { tags, status, includeParent };
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
/**
|
|
83
|
-
* Detect submodule context using git commands
|
|
84
|
-
* @returns {{ isInSubmodule: boolean, parentRepoPath: string | null, currentSubmodulePath: string | null }}
|
|
85
|
-
*/
|
|
86
|
-
function detectSubmoduleContext() {
|
|
87
|
-
const result = {
|
|
88
|
-
isInSubmodule: false,
|
|
89
|
-
parentRepoPath: null,
|
|
90
|
-
currentSubmodulePath: null
|
|
91
|
-
};
|
|
92
|
-
|
|
93
|
-
try {
|
|
94
|
-
// Check if we're in a git repo at all
|
|
95
|
-
execSync('git rev-parse --git-dir', { stdio: 'pipe' });
|
|
96
|
-
|
|
97
|
-
// Check if we're in a submodule by looking for a superproject
|
|
98
|
-
const superprojectPath = execSync('git rev-parse --show-superproject-working-tree', {
|
|
99
|
-
encoding: 'utf8',
|
|
100
|
-
stdio: ['pipe', 'pipe', 'pipe']
|
|
101
|
-
}).trim();
|
|
102
|
-
|
|
103
|
-
if (superprojectPath) {
|
|
104
|
-
result.isInSubmodule = true;
|
|
105
|
-
result.parentRepoPath = superprojectPath;
|
|
106
|
-
|
|
107
|
-
// Get the current submodule path relative to parent
|
|
108
|
-
// This is the path from the parent repo root to the submodule
|
|
109
|
-
const currentPath = process.cwd();
|
|
110
|
-
result.currentSubmodulePath = path.relative(superprojectPath, currentPath);
|
|
111
|
-
}
|
|
112
|
-
} catch (error) {
|
|
113
|
-
// Not in a git repo or git not available - silently ignore
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
return result;
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
/**
|
|
120
|
-
* Read and parse a YAML ADR file, extracting metadata fields
|
|
121
|
-
* @param {string} filePath - Absolute path to the YAML file
|
|
122
|
-
* @param {string} source - "local" or "inherited"
|
|
123
|
-
* @param {string} sourcePath - Path to the repo containing this ADR
|
|
124
|
-
* @returns {{ id: string, title: string, description: string, tags: string[], status: string, date: string, scope: string[] | null, source: string, sourcePath: string } | null}
|
|
125
|
-
*/
|
|
126
|
-
function parseAdrFile(filePath, source, sourcePath) {
|
|
127
|
-
try {
|
|
128
|
-
const content = fs.readFileSync(filePath, 'utf8');
|
|
129
|
-
const data = yaml.load(content);
|
|
130
|
-
|
|
131
|
-
// Extract fields, handling missing optional fields gracefully
|
|
132
|
-
const id = data?.id || path.basename(filePath, '.yaml');
|
|
133
|
-
const title = data?.title || '';
|
|
134
|
-
// Clean up description - remove trailing newlines from YAML multi-line strings
|
|
135
|
-
const description = (data?.description || '').trim();
|
|
136
|
-
const tags = Array.isArray(data?.tags) ? data.tags : [];
|
|
137
|
-
const status = data?.status || '';
|
|
138
|
-
const date = data?.date || '';
|
|
139
|
-
// Scope is optional - null means global (applies everywhere)
|
|
140
|
-
const scope = Array.isArray(data?.scope) ? data.scope : null;
|
|
141
|
-
|
|
142
|
-
return { id, title, description, tags, status, date, scope, source, sourcePath };
|
|
143
|
-
} catch (error) {
|
|
144
|
-
// Log to stderr so it doesn't pollute JSON output
|
|
145
|
-
console.error(`Warning: Failed to parse ${filePath}: ${error.message}`);
|
|
146
|
-
return null;
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
/**
|
|
151
|
-
* Check if an ADR matches the given tag filter (OR logic)
|
|
152
|
-
* @param {string[]} adrTags - Tags from the ADR
|
|
153
|
-
* @param {string[]} filterTags - Tags to filter by
|
|
154
|
-
* @returns {boolean} - True if ADR has ANY of the filter tags
|
|
155
|
-
*/
|
|
156
|
-
function matchesTags(adrTags, filterTags) {
|
|
157
|
-
if (!filterTags || filterTags.length === 0) {
|
|
158
|
-
return true; // No filter = match all
|
|
159
|
-
}
|
|
160
|
-
// OR logic: return true if ADR has ANY of the filter tags
|
|
161
|
-
return filterTags.some(filterTag => adrTags.includes(filterTag));
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
/**
|
|
165
|
-
* Check if an ADR matches the given status filter (exact match)
|
|
166
|
-
* @param {string} adrStatus - Status from the ADR
|
|
167
|
-
* @param {string | null} filterStatus - Status to filter by
|
|
168
|
-
* @returns {boolean} - True if status matches exactly
|
|
169
|
-
*/
|
|
170
|
-
function matchesStatus(adrStatus, filterStatus) {
|
|
171
|
-
if (!filterStatus) {
|
|
172
|
-
return true; // No filter = match all
|
|
173
|
-
}
|
|
174
|
-
return adrStatus === filterStatus;
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
/**
|
|
178
|
-
* Check if an inherited ADR's scope applies to the current submodule
|
|
179
|
-
* @param {string[] | null} scope - ADR's scope field (null = global, array = specific submodules)
|
|
180
|
-
* @param {string | null} currentSubmodulePath - Current submodule path relative to parent
|
|
181
|
-
* @returns {boolean} - True if ADR applies to current context
|
|
182
|
-
*/
|
|
183
|
-
function matchesScope(scope, currentSubmodulePath) {
|
|
184
|
-
// null scope = global, applies everywhere
|
|
185
|
-
if (scope === null) {
|
|
186
|
-
return true;
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
// If we don't know the current submodule path, be conservative and include it
|
|
190
|
-
if (!currentSubmodulePath) {
|
|
191
|
-
return true;
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
// Check if current submodule path matches any scope entry
|
|
195
|
-
return scope.some(scopePath => {
|
|
196
|
-
// Exact match or prefix match (for nested submodules)
|
|
197
|
-
return currentSubmodulePath === scopePath ||
|
|
198
|
-
currentSubmodulePath.startsWith(scopePath + '/');
|
|
199
|
-
});
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
/**
|
|
203
|
-
* Collect ADRs from a specific directory
|
|
204
|
-
* @param {string} adrsDir - Directory to scan for ADRs
|
|
205
|
-
* @param {string} source - "local" or "inherited"
|
|
206
|
-
* @param {string} sourcePath - Path to the repo containing this ADR
|
|
207
|
-
* @param {{ tags: string[] | null, status: string | null }} filters
|
|
208
|
-
* @param {string | null} currentSubmodulePath - For inherited ADRs, used for scope filtering
|
|
209
|
-
* @returns {Array<object>}
|
|
210
|
-
*/
|
|
211
|
-
function collectAdrsFromDir(adrsDir, source, sourcePath, filters, currentSubmodulePath) {
|
|
212
|
-
const adrs = [];
|
|
213
|
-
|
|
214
|
-
// Check if ADRs directory exists
|
|
215
|
-
if (!fs.existsSync(adrsDir)) {
|
|
216
|
-
return adrs;
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
// Read all .yaml files in the ADRs directory
|
|
220
|
-
const files = fs.readdirSync(adrsDir)
|
|
221
|
-
.filter(file => file.endsWith('.yaml'))
|
|
222
|
-
.sort() // Sort alphabetically for consistent ordering
|
|
223
|
-
.map(file => path.join(adrsDir, file));
|
|
224
|
-
|
|
225
|
-
// Process each ADR file
|
|
226
|
-
for (const filePath of files) {
|
|
227
|
-
const adr = parseAdrFile(filePath, source, sourcePath);
|
|
228
|
-
if (!adr) continue;
|
|
229
|
-
|
|
230
|
-
// Apply tag and status filters
|
|
231
|
-
if (!matchesTags(adr.tags, filters.tags)) continue;
|
|
232
|
-
if (!matchesStatus(adr.status, filters.status)) continue;
|
|
233
|
-
|
|
234
|
-
// For inherited ADRs, apply scope filtering
|
|
235
|
-
if (source === 'inherited' && !matchesScope(adr.scope, currentSubmodulePath)) {
|
|
236
|
-
continue;
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
adrs.push(adr);
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
return adrs;
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
/**
|
|
246
|
-
* Scan ADRs directory and collect all ADR metadata with optional filtering
|
|
247
|
-
* @param {{ tags: string[] | null, status: string | null, includeParent: boolean }} options
|
|
248
|
-
* @returns {Array<{ id: string, title: string, description: string, tags: string[], status: string, date: string, source: string, sourcePath: string }>}
|
|
249
|
-
*/
|
|
250
|
-
function collectAdrs(options) {
|
|
251
|
-
const { tags, status, includeParent } = options;
|
|
252
|
-
const filters = { tags, status };
|
|
253
|
-
let allAdrs = [];
|
|
254
|
-
|
|
255
|
-
// Always collect local ADRs
|
|
256
|
-
const localAdrs = collectAdrsFromDir(ADRS_DIR, 'local', process.cwd(), filters, null);
|
|
257
|
-
allAdrs = allAdrs.concat(localAdrs);
|
|
258
|
-
|
|
259
|
-
// If --include-parent flag is set and we're in a submodule, also collect parent ADRs
|
|
260
|
-
if (includeParent) {
|
|
261
|
-
const submoduleContext = detectSubmoduleContext();
|
|
262
|
-
|
|
263
|
-
if (submoduleContext.isInSubmodule && submoduleContext.parentRepoPath) {
|
|
264
|
-
const parentAdrsDir = path.join(submoduleContext.parentRepoPath, '.claude', 'adrs');
|
|
265
|
-
const parentAdrs = collectAdrsFromDir(
|
|
266
|
-
parentAdrsDir,
|
|
267
|
-
'inherited',
|
|
268
|
-
submoduleContext.parentRepoPath,
|
|
269
|
-
filters,
|
|
270
|
-
submoduleContext.currentSubmodulePath
|
|
271
|
-
);
|
|
272
|
-
allAdrs = allAdrs.concat(parentAdrs);
|
|
273
|
-
}
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
// For backward compatibility, remove scope field from output (internal use only)
|
|
277
|
-
// and only include source/sourcePath when --include-parent is used
|
|
278
|
-
return allAdrs.map(adr => {
|
|
279
|
-
const { scope, ...rest } = adr;
|
|
280
|
-
if (!includeParent) {
|
|
281
|
-
// For backward compatibility, don't include source fields when not using --include-parent
|
|
282
|
-
const { source, sourcePath, ...backwardCompatible } = rest;
|
|
283
|
-
return backwardCompatible;
|
|
284
|
-
}
|
|
285
|
-
return rest;
|
|
286
|
-
});
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
// Main execution
|
|
290
|
-
function main() {
|
|
291
|
-
const options = parseArgs();
|
|
292
|
-
const adrs = collectAdrs(options);
|
|
293
|
-
|
|
294
|
-
// Output JSON to stdout
|
|
295
|
-
console.log(JSON.stringify(adrs, null, 2));
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
main();
|
package/src/scripts/adr-tags.js
DELETED
|
@@ -1,242 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* adr-tags.js - List all distinct tags across all ADRs with usage counts
|
|
5
|
-
*
|
|
6
|
-
* This CLI script scans all YAML ADR files in the project's .claude/adrs/
|
|
7
|
-
* and extracts tag information, producing a sorted summary of tag usage.
|
|
8
|
-
*
|
|
9
|
-
* Usage:
|
|
10
|
-
* node ~/.claude/scripts/adr-tags.js # Tags from local ADRs only
|
|
11
|
-
* node ~/.claude/scripts/adr-tags.js --include-parent # Include parent repo ADRs (in submodule)
|
|
12
|
-
*
|
|
13
|
-
* Flags:
|
|
14
|
-
* --include-parent When in a git submodule, also include tags from parent repo's ADRs
|
|
15
|
-
*
|
|
16
|
-
* Output:
|
|
17
|
-
* JSON array to stdout: [{ tag, count, adrs }] sorted by count descending
|
|
18
|
-
*
|
|
19
|
-
* Example output:
|
|
20
|
-
* [
|
|
21
|
-
* { "tag": "architecture", "count": 8, "adrs": ["ADR-001", "ADR-003"] },
|
|
22
|
-
* { "tag": "agents", "count": 4, "adrs": ["ADR-003", "ADR-004"] }
|
|
23
|
-
* ]
|
|
24
|
-
*
|
|
25
|
-
* Note: This script supplements the adr-agent (per ADR-009), providing
|
|
26
|
-
* quick CLI access to tag information without invoking the full agent.
|
|
27
|
-
*/
|
|
28
|
-
|
|
29
|
-
const fs = require('fs');
|
|
30
|
-
const path = require('path');
|
|
31
|
-
const yaml = require('js-yaml');
|
|
32
|
-
const { execSync } = require('child_process');
|
|
33
|
-
|
|
34
|
-
// ADRs are in the project's .claude/adrs/ directory (relative to CWD)
|
|
35
|
-
const ADRS_DIR = path.join(process.cwd(), '.claude', 'adrs');
|
|
36
|
-
|
|
37
|
-
/**
|
|
38
|
-
* Parse command line arguments for --include-parent flag
|
|
39
|
-
* @returns {{ includeParent: boolean }}
|
|
40
|
-
*/
|
|
41
|
-
function parseArgs() {
|
|
42
|
-
const args = process.argv.slice(2);
|
|
43
|
-
let includeParent = false;
|
|
44
|
-
|
|
45
|
-
for (const arg of args) {
|
|
46
|
-
if (arg === '--include-parent') {
|
|
47
|
-
includeParent = true;
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
return { includeParent };
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
/**
|
|
55
|
-
* Detect submodule context using git commands
|
|
56
|
-
* @returns {{ isInSubmodule: boolean, parentRepoPath: string | null, currentSubmodulePath: string | null }}
|
|
57
|
-
*/
|
|
58
|
-
function detectSubmoduleContext() {
|
|
59
|
-
const result = {
|
|
60
|
-
isInSubmodule: false,
|
|
61
|
-
parentRepoPath: null,
|
|
62
|
-
currentSubmodulePath: null
|
|
63
|
-
};
|
|
64
|
-
|
|
65
|
-
try {
|
|
66
|
-
// Check if we're in a git repo at all
|
|
67
|
-
execSync('git rev-parse --git-dir', { stdio: 'pipe' });
|
|
68
|
-
|
|
69
|
-
// Check if we're in a submodule by looking for a superproject
|
|
70
|
-
const superprojectPath = execSync('git rev-parse --show-superproject-working-tree', {
|
|
71
|
-
encoding: 'utf8',
|
|
72
|
-
stdio: ['pipe', 'pipe', 'pipe']
|
|
73
|
-
}).trim();
|
|
74
|
-
|
|
75
|
-
if (superprojectPath) {
|
|
76
|
-
result.isInSubmodule = true;
|
|
77
|
-
result.parentRepoPath = superprojectPath;
|
|
78
|
-
|
|
79
|
-
// Get the current submodule path relative to parent
|
|
80
|
-
const currentPath = process.cwd();
|
|
81
|
-
result.currentSubmodulePath = path.relative(superprojectPath, currentPath);
|
|
82
|
-
}
|
|
83
|
-
} catch (error) {
|
|
84
|
-
// Not in a git repo or git not available - silently ignore
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
return result;
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
/**
|
|
91
|
-
* Check if an inherited ADR's scope applies to the current submodule
|
|
92
|
-
* @param {string[] | null} scope - ADR's scope field (null = global, array = specific submodules)
|
|
93
|
-
* @param {string | null} currentSubmodulePath - Current submodule path relative to parent
|
|
94
|
-
* @returns {boolean} - True if ADR applies to current context
|
|
95
|
-
*/
|
|
96
|
-
function matchesScope(scope, currentSubmodulePath) {
|
|
97
|
-
// null scope = global, applies everywhere
|
|
98
|
-
if (scope === null) {
|
|
99
|
-
return true;
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
// If we don't know the current submodule path, be conservative and include it
|
|
103
|
-
if (!currentSubmodulePath) {
|
|
104
|
-
return true;
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
// Check if current submodule path matches any scope entry
|
|
108
|
-
return scope.some(scopePath => {
|
|
109
|
-
// Exact match or prefix match (for nested submodules)
|
|
110
|
-
return currentSubmodulePath === scopePath ||
|
|
111
|
-
currentSubmodulePath.startsWith(scopePath + '/');
|
|
112
|
-
});
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
/**
|
|
116
|
-
* Read and parse a YAML ADR file, extracting id, tags, and scope
|
|
117
|
-
* @param {string} filePath - Absolute path to the YAML file
|
|
118
|
-
* @returns {{ id: string, tags: string[], scope: string[] | null } | null} - Parsed data or null on error
|
|
119
|
-
*/
|
|
120
|
-
function parseAdrFile(filePath) {
|
|
121
|
-
try {
|
|
122
|
-
const content = fs.readFileSync(filePath, 'utf8');
|
|
123
|
-
const data = yaml.load(content);
|
|
124
|
-
|
|
125
|
-
// Extract id, tags, and scope, handling missing fields gracefully
|
|
126
|
-
const id = data?.id || path.basename(filePath, '.yaml');
|
|
127
|
-
const tags = Array.isArray(data?.tags) ? data.tags : [];
|
|
128
|
-
const scope = Array.isArray(data?.scope) ? data.scope : null;
|
|
129
|
-
|
|
130
|
-
return { id, tags, scope };
|
|
131
|
-
} catch (error) {
|
|
132
|
-
// Log to stderr so it doesn't pollute JSON output
|
|
133
|
-
console.error(`Warning: Failed to parse ${filePath}: ${error.message}`);
|
|
134
|
-
return null;
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
/**
|
|
139
|
-
* Collect tags from a specific ADRs directory
|
|
140
|
-
* @param {string} adrsDir - Directory to scan for ADRs
|
|
141
|
-
* @param {Map<string, string[]>} tagMap - Map to add tags to
|
|
142
|
-
* @param {boolean} isInherited - Whether these are inherited ADRs
|
|
143
|
-
* @param {string | null} currentSubmodulePath - For scope filtering on inherited ADRs
|
|
144
|
-
*/
|
|
145
|
-
function collectTagsFromDir(adrsDir, tagMap, isInherited, currentSubmodulePath) {
|
|
146
|
-
// Check if ADRs directory exists
|
|
147
|
-
if (!fs.existsSync(adrsDir)) {
|
|
148
|
-
return;
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
// Read all .yaml files in the ADRs directory
|
|
152
|
-
const files = fs.readdirSync(adrsDir)
|
|
153
|
-
.filter(file => file.endsWith('.yaml'))
|
|
154
|
-
.map(file => path.join(adrsDir, file));
|
|
155
|
-
|
|
156
|
-
// Process each ADR file
|
|
157
|
-
for (const filePath of files) {
|
|
158
|
-
const adr = parseAdrFile(filePath);
|
|
159
|
-
if (!adr) continue;
|
|
160
|
-
|
|
161
|
-
// For inherited ADRs, apply scope filtering
|
|
162
|
-
if (isInherited && !matchesScope(adr.scope, currentSubmodulePath)) {
|
|
163
|
-
continue;
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
// Add this ADR's id to each of its tags
|
|
167
|
-
for (const tag of adr.tags) {
|
|
168
|
-
if (!tagMap.has(tag)) {
|
|
169
|
-
tagMap.set(tag, []);
|
|
170
|
-
}
|
|
171
|
-
// Avoid duplicates (in case same ADR id exists in both local and parent)
|
|
172
|
-
const adrList = tagMap.get(tag);
|
|
173
|
-
if (!adrList.includes(adr.id)) {
|
|
174
|
-
adrList.push(adr.id);
|
|
175
|
-
}
|
|
176
|
-
}
|
|
177
|
-
}
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
/**
|
|
181
|
-
* Scan ADRs directories and collect all tag information
|
|
182
|
-
* @param {{ includeParent: boolean }} options
|
|
183
|
-
* @returns {Map<string, string[]>} - Map of tag -> array of ADR ids
|
|
184
|
-
*/
|
|
185
|
-
function collectTags(options) {
|
|
186
|
-
const tagMap = new Map();
|
|
187
|
-
const { includeParent } = options;
|
|
188
|
-
|
|
189
|
-
// Collect tags from local ADRs
|
|
190
|
-
collectTagsFromDir(ADRS_DIR, tagMap, false, null);
|
|
191
|
-
|
|
192
|
-
// If --include-parent flag is set and we're in a submodule, also collect parent tags
|
|
193
|
-
if (includeParent) {
|
|
194
|
-
const submoduleContext = detectSubmoduleContext();
|
|
195
|
-
|
|
196
|
-
if (submoduleContext.isInSubmodule && submoduleContext.parentRepoPath) {
|
|
197
|
-
const parentAdrsDir = path.join(submoduleContext.parentRepoPath, '.claude', 'adrs');
|
|
198
|
-
collectTagsFromDir(parentAdrsDir, tagMap, true, submoduleContext.currentSubmodulePath);
|
|
199
|
-
}
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
return tagMap;
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
/**
|
|
206
|
-
* Format tag data as sorted output array
|
|
207
|
-
* @param {Map<string, string[]>} tagMap - Map of tag -> array of ADR ids
|
|
208
|
-
* @returns {Array<{ tag: string, count: number, adrs: string[] }>}
|
|
209
|
-
*/
|
|
210
|
-
function formatOutput(tagMap) {
|
|
211
|
-
const result = [];
|
|
212
|
-
|
|
213
|
-
for (const [tag, adrs] of tagMap.entries()) {
|
|
214
|
-
result.push({
|
|
215
|
-
tag,
|
|
216
|
-
count: adrs.length,
|
|
217
|
-
adrs: adrs.sort() // Sort ADR ids alphabetically within each tag
|
|
218
|
-
});
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
// Sort by count descending, then alphabetically by tag name for ties
|
|
222
|
-
result.sort((a, b) => {
|
|
223
|
-
if (b.count !== a.count) {
|
|
224
|
-
return b.count - a.count;
|
|
225
|
-
}
|
|
226
|
-
return a.tag.localeCompare(b.tag);
|
|
227
|
-
});
|
|
228
|
-
|
|
229
|
-
return result;
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
// Main execution
|
|
233
|
-
function main() {
|
|
234
|
-
const options = parseArgs();
|
|
235
|
-
const tagMap = collectTags(options);
|
|
236
|
-
const output = formatOutput(tagMap);
|
|
237
|
-
|
|
238
|
-
// Output JSON to stdout
|
|
239
|
-
console.log(JSON.stringify(output, null, 2));
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
main();
|
|
@@ -1,142 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* validate-yaml.js - Validate YAML syntax in files
|
|
5
|
-
*
|
|
6
|
-
* This script validates YAML files after they are created or modified through Claude Code's
|
|
7
|
-
* Edit or Write tools. It's invoked by the PostToolUse hook system with JSON input via stdin
|
|
8
|
-
* containing the file path that was just modified.
|
|
9
|
-
*
|
|
10
|
-
* Usage:
|
|
11
|
-
* echo '{"tool_input":{"file_path":"/path/to/file.yaml"}}' | node validate-yaml.js
|
|
12
|
-
*
|
|
13
|
-
* Input format (JSON via stdin):
|
|
14
|
-
* {
|
|
15
|
-
* "tool_input": {
|
|
16
|
-
* "file_path": "/absolute/path/to/file.yaml"
|
|
17
|
-
* }
|
|
18
|
-
* }
|
|
19
|
-
*
|
|
20
|
-
* Exit codes:
|
|
21
|
-
* 0 - File is valid YAML or not a YAML file (silent success)
|
|
22
|
-
* 2 - File is invalid YAML (parsing error written to stderr)
|
|
23
|
-
*
|
|
24
|
-
* Behavior:
|
|
25
|
-
* - Non-YAML files (.yaml and .yml extensions) pass through silently with exit 0
|
|
26
|
-
* - Valid YAML files produce no output and exit 0
|
|
27
|
-
* - Invalid YAML files produce parsing error on stderr and exit 2
|
|
28
|
-
*
|
|
29
|
-
* Dependencies:
|
|
30
|
-
* - js-yaml (for YAML parsing)
|
|
31
|
-
* - Node.js built-ins (fs, path)
|
|
32
|
-
*/
|
|
33
|
-
|
|
34
|
-
const fs = require('fs');
|
|
35
|
-
const path = require('path');
|
|
36
|
-
const yaml = require('js-yaml');
|
|
37
|
-
|
|
38
|
-
/**
|
|
39
|
-
* Read JSON from stdin and extract file_path
|
|
40
|
-
* @returns {Promise<string | null>} - The file path from tool_input, or null if invalid
|
|
41
|
-
*/
|
|
42
|
-
async function readStdin() {
|
|
43
|
-
return new Promise((resolve, reject) => {
|
|
44
|
-
let data = '';
|
|
45
|
-
|
|
46
|
-
process.stdin.setEncoding('utf8');
|
|
47
|
-
process.stdin.on('data', chunk => {
|
|
48
|
-
data += chunk;
|
|
49
|
-
});
|
|
50
|
-
|
|
51
|
-
process.stdin.on('end', () => {
|
|
52
|
-
try {
|
|
53
|
-
const parsed = JSON.parse(data);
|
|
54
|
-
const filePath = parsed?.tool_input?.file_path;
|
|
55
|
-
resolve(filePath || null);
|
|
56
|
-
} catch (error) {
|
|
57
|
-
// If we can't parse the input, still exit cleanly - don't interrupt the user's work
|
|
58
|
-
resolve(null);
|
|
59
|
-
}
|
|
60
|
-
});
|
|
61
|
-
|
|
62
|
-
process.stdin.on('error', () => {
|
|
63
|
-
// If there's an issue reading stdin, exit cleanly
|
|
64
|
-
resolve(null);
|
|
65
|
-
});
|
|
66
|
-
});
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
/**
|
|
70
|
-
* Check if a file has a YAML extension
|
|
71
|
-
* @param {string} filePath - The file path to check
|
|
72
|
-
* @returns {boolean} - True if the file ends with .yaml or .yml
|
|
73
|
-
*/
|
|
74
|
-
function isYamlFile(filePath) {
|
|
75
|
-
return filePath.endsWith('.yaml') || filePath.endsWith('.yml');
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
/**
|
|
79
|
-
* Check if a file is in a Helm charts directory (uses Go templating, not valid YAML)
|
|
80
|
-
* @param {string} filePath - The file path to check
|
|
81
|
-
* @returns {boolean} - True if the file is in a charts directory
|
|
82
|
-
*/
|
|
83
|
-
function isHelmChartFile(filePath) {
|
|
84
|
-
return filePath.includes('/charts/');
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
/**
|
|
88
|
-
* Validate YAML syntax by attempting to parse the file
|
|
89
|
-
* @param {string} filePath - The absolute path to the YAML file
|
|
90
|
-
* @returns {{ valid: boolean, error?: string }} - Validation result with optional error message
|
|
91
|
-
*/
|
|
92
|
-
function validateYaml(filePath) {
|
|
93
|
-
try {
|
|
94
|
-
const content = fs.readFileSync(filePath, 'utf8');
|
|
95
|
-
yaml.load(content);
|
|
96
|
-
return { valid: true };
|
|
97
|
-
} catch (error) {
|
|
98
|
-
return {
|
|
99
|
-
valid: false,
|
|
100
|
-
error: error.message
|
|
101
|
-
};
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
/**
|
|
106
|
-
* Main execution - validate YAML file from tool input
|
|
107
|
-
*/
|
|
108
|
-
async function main() {
|
|
109
|
-
const filePath = await readStdin();
|
|
110
|
-
|
|
111
|
-
// If we couldn't extract a file path, exit silently
|
|
112
|
-
if (!filePath) {
|
|
113
|
-
process.exit(0);
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
// If it's not a YAML file, pass through silently
|
|
117
|
-
if (!isYamlFile(filePath)) {
|
|
118
|
-
process.exit(0);
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
// If it's a Helm chart file, skip validation (uses Go templating syntax)
|
|
122
|
-
if (isHelmChartFile(filePath)) {
|
|
123
|
-
process.exit(0);
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
// Validate the YAML file
|
|
127
|
-
const result = validateYaml(filePath);
|
|
128
|
-
|
|
129
|
-
if (result.valid) {
|
|
130
|
-
// Valid YAML - silent success
|
|
131
|
-
process.exit(0);
|
|
132
|
-
} else {
|
|
133
|
-
// Invalid YAML - report error to stderr
|
|
134
|
-
console.error(`YAML validation error in ${filePath}:\n${result.error}`);
|
|
135
|
-
process.exit(2);
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
main().catch(() => {
|
|
140
|
-
// If anything goes wrong, exit silently to avoid disrupting the user's workflow
|
|
141
|
-
process.exit(0);
|
|
142
|
-
});
|