cc-dev-template 0.1.4 → 0.1.6

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.
@@ -0,0 +1,206 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * find-skills.js
5
+ *
6
+ * Discovers all Claude Code skills at user and project levels.
7
+ * Returns JSON with skill locations and basic metadata.
8
+ *
9
+ * Usage: node find-skills.js [--json] [--verbose]
10
+ */
11
+
12
+ const fs = require('fs');
13
+ const path = require('path');
14
+ const os = require('os');
15
+
16
+ // Parse arguments
17
+ const args = process.argv.slice(2);
18
+ const jsonOutput = args.includes('--json');
19
+ const verbose = args.includes('--verbose');
20
+
21
+ // Skill locations to scan
22
+ const locations = [
23
+ {
24
+ name: 'user',
25
+ path: path.join(os.homedir(), '.claude', 'skills'),
26
+ description: 'User-level skills (~/.claude/skills/)'
27
+ },
28
+ {
29
+ name: 'project',
30
+ path: path.join(process.cwd(), '.claude', 'skills'),
31
+ description: 'Project-level skills (.claude/skills/)'
32
+ },
33
+ {
34
+ name: 'source',
35
+ path: path.join(process.cwd(), 'src', 'skills'),
36
+ description: 'Source skills (src/skills/)'
37
+ }
38
+ ];
39
+
40
+ /**
41
+ * Parse YAML frontmatter from SKILL.md content
42
+ */
43
+ function parseFrontmatter(content) {
44
+ const match = content.match(/^---\n([\s\S]*?)\n---/);
45
+ if (!match) return null;
46
+
47
+ const frontmatter = {};
48
+ const lines = match[1].split('\n');
49
+
50
+ for (const line of lines) {
51
+ const colonIndex = line.indexOf(':');
52
+ if (colonIndex > 0) {
53
+ const key = line.slice(0, colonIndex).trim();
54
+ let value = line.slice(colonIndex + 1).trim();
55
+
56
+ // Remove quotes if present
57
+ if ((value.startsWith('"') && value.endsWith('"')) ||
58
+ (value.startsWith("'") && value.endsWith("'"))) {
59
+ value = value.slice(1, -1);
60
+ }
61
+
62
+ frontmatter[key] = value;
63
+ }
64
+ }
65
+
66
+ return frontmatter;
67
+ }
68
+
69
+ /**
70
+ * Get basic stats about a skill
71
+ */
72
+ function getSkillStats(skillPath) {
73
+ const skillMdPath = path.join(skillPath, 'SKILL.md');
74
+
75
+ if (!fs.existsSync(skillMdPath)) {
76
+ return null;
77
+ }
78
+
79
+ const content = fs.readFileSync(skillMdPath, 'utf-8');
80
+ const frontmatter = parseFrontmatter(content);
81
+
82
+ // Count words in body (after frontmatter)
83
+ const bodyMatch = content.match(/^---\n[\s\S]*?\n---\n([\s\S]*)$/);
84
+ const body = bodyMatch ? bodyMatch[1] : content;
85
+ const wordCount = body.split(/\s+/).filter(w => w.length > 0).length;
86
+
87
+ // Check for subdirectories
88
+ const hasReferences = fs.existsSync(path.join(skillPath, 'references'));
89
+ const hasScripts = fs.existsSync(path.join(skillPath, 'scripts'));
90
+ const hasAssets = fs.existsSync(path.join(skillPath, 'assets'));
91
+
92
+ return {
93
+ name: frontmatter?.name || path.basename(skillPath),
94
+ description: frontmatter?.description || null,
95
+ wordCount,
96
+ hasReferences,
97
+ hasScripts,
98
+ hasAssets,
99
+ frontmatterValid: !!(frontmatter?.name && frontmatter?.description)
100
+ };
101
+ }
102
+
103
+ /**
104
+ * Scan a location for skills
105
+ */
106
+ function scanLocation(location) {
107
+ const skills = [];
108
+
109
+ if (!fs.existsSync(location.path)) {
110
+ return skills;
111
+ }
112
+
113
+ const entries = fs.readdirSync(location.path, { withFileTypes: true });
114
+
115
+ for (const entry of entries) {
116
+ if (!entry.isDirectory()) continue;
117
+
118
+ const skillPath = path.join(location.path, entry.name);
119
+ const skillMdPath = path.join(skillPath, 'SKILL.md');
120
+
121
+ if (fs.existsSync(skillMdPath)) {
122
+ const stats = getSkillStats(skillPath);
123
+
124
+ skills.push({
125
+ directory: entry.name,
126
+ path: skillPath,
127
+ location: location.name,
128
+ ...stats
129
+ });
130
+ }
131
+ }
132
+
133
+ return skills;
134
+ }
135
+
136
+ // Main execution
137
+ const results = {
138
+ timestamp: new Date().toISOString(),
139
+ cwd: process.cwd(),
140
+ locations: {},
141
+ skills: [],
142
+ summary: {
143
+ total: 0,
144
+ byLocation: {}
145
+ }
146
+ };
147
+
148
+ for (const location of locations) {
149
+ const skills = scanLocation(location);
150
+
151
+ results.locations[location.name] = {
152
+ path: location.path,
153
+ exists: fs.existsSync(location.path),
154
+ count: skills.length
155
+ };
156
+
157
+ results.skills.push(...skills);
158
+ results.summary.byLocation[location.name] = skills.length;
159
+ }
160
+
161
+ results.summary.total = results.skills.length;
162
+
163
+ // Output
164
+ if (jsonOutput) {
165
+ console.log(JSON.stringify(results, null, 2));
166
+ } else {
167
+ console.log('\n=== Claude Code Skills Discovery ===\n');
168
+
169
+ for (const location of locations) {
170
+ const info = results.locations[location.name];
171
+ console.log(`${location.description}`);
172
+ console.log(` Path: ${info.path}`);
173
+ console.log(` Exists: ${info.exists ? 'Yes' : 'No'}`);
174
+ console.log(` Skills found: ${info.count}`);
175
+ console.log();
176
+ }
177
+
178
+ if (results.skills.length === 0) {
179
+ console.log('No skills found.\n');
180
+ } else {
181
+ console.log('=== Skills ===\n');
182
+
183
+ for (const skill of results.skills) {
184
+ console.log(`[${skill.location}] ${skill.name}`);
185
+ console.log(` Path: ${skill.path}`);
186
+
187
+ if (verbose) {
188
+ console.log(` Words: ${skill.wordCount}`);
189
+ console.log(` Valid frontmatter: ${skill.frontmatterValid ? 'Yes' : 'No'}`);
190
+ console.log(` Has references/: ${skill.hasReferences ? 'Yes' : 'No'}`);
191
+ console.log(` Has scripts/: ${skill.hasScripts ? 'Yes' : 'No'}`);
192
+
193
+ if (skill.description) {
194
+ const truncated = skill.description.length > 80
195
+ ? skill.description.slice(0, 80) + '...'
196
+ : skill.description;
197
+ console.log(` Description: ${truncated}`);
198
+ }
199
+ }
200
+
201
+ console.log();
202
+ }
203
+
204
+ console.log(`Total: ${results.summary.total} skill(s)\n`);
205
+ }
206
+ }
@@ -0,0 +1,386 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * validate-skill.js
5
+ *
6
+ * Validates a Claude Code skill against best practices.
7
+ * Returns detailed findings with severity levels.
8
+ *
9
+ * Usage: node validate-skill.js <skill-path> [--json] [--fix]
10
+ */
11
+
12
+ const fs = require('fs');
13
+ const path = require('path');
14
+
15
+ // Parse arguments
16
+ const args = process.argv.slice(2);
17
+ const skillPath = args.find(a => !a.startsWith('--'));
18
+ const jsonOutput = args.includes('--json');
19
+ const autoFix = args.includes('--fix');
20
+
21
+ if (!skillPath) {
22
+ console.error('Usage: node validate-skill.js <skill-path> [--json] [--fix]');
23
+ process.exit(1);
24
+ }
25
+
26
+ // Resolve skill path
27
+ const resolvedPath = path.resolve(skillPath);
28
+ const skillMdPath = path.join(resolvedPath, 'SKILL.md');
29
+
30
+ if (!fs.existsSync(skillMdPath)) {
31
+ console.error(`Error: SKILL.md not found at ${skillMdPath}`);
32
+ process.exit(1);
33
+ }
34
+
35
+ // Findings storage
36
+ const findings = [];
37
+
38
+ function addFinding(category, severity, message, line = null, suggestion = null) {
39
+ findings.push({ category, severity, message, line, suggestion });
40
+ }
41
+
42
+ /**
43
+ * Parse YAML frontmatter
44
+ */
45
+ function parseFrontmatter(content) {
46
+ const match = content.match(/^---\n([\s\S]*?)\n---/);
47
+ if (!match) return { valid: false, data: null, raw: null };
48
+
49
+ const raw = match[1];
50
+ const data = {};
51
+ const lines = raw.split('\n');
52
+
53
+ for (const line of lines) {
54
+ const colonIndex = line.indexOf(':');
55
+ if (colonIndex > 0) {
56
+ const key = line.slice(0, colonIndex).trim();
57
+ let value = line.slice(colonIndex + 1).trim();
58
+
59
+ // Handle multi-line values (basic support)
60
+ if (value === '' || value === '|' || value === '>') {
61
+ // Skip complex multi-line for now
62
+ continue;
63
+ }
64
+
65
+ // Remove quotes if present
66
+ if ((value.startsWith('"') && value.endsWith('"')) ||
67
+ (value.startsWith("'") && value.endsWith("'"))) {
68
+ value = value.slice(1, -1);
69
+ }
70
+
71
+ data[key] = value;
72
+ }
73
+ }
74
+
75
+ return { valid: true, data, raw };
76
+ }
77
+
78
+ /**
79
+ * Get body content (after frontmatter)
80
+ */
81
+ function getBody(content) {
82
+ const match = content.match(/^---\n[\s\S]*?\n---\n([\s\S]*)$/);
83
+ return match ? match[1] : content;
84
+ }
85
+
86
+ // Read skill content
87
+ const content = fs.readFileSync(skillMdPath, 'utf-8');
88
+ const frontmatter = parseFrontmatter(content);
89
+ const body = getBody(content);
90
+ const lines = content.split('\n');
91
+
92
+ // === VALIDATION CHECKS ===
93
+
94
+ // Check 1: Frontmatter exists
95
+ if (!frontmatter.valid) {
96
+ addFinding('STRUCTURE', 'error', 'Missing or invalid YAML frontmatter');
97
+ } else {
98
+ // Check 2: Required fields
99
+ if (!frontmatter.data.name) {
100
+ addFinding('STRUCTURE', 'error', 'Missing required field: name');
101
+ } else {
102
+ // Check name format (hyphen-case)
103
+ const namePattern = /^[a-z0-9]+(-[a-z0-9]+)*$/;
104
+ if (!namePattern.test(frontmatter.data.name)) {
105
+ addFinding('STRUCTURE', 'error',
106
+ `Invalid name format: "${frontmatter.data.name}" - must be hyphen-case (lowercase letters, numbers, hyphens)`);
107
+ }
108
+
109
+ // Check name matches directory
110
+ const dirName = path.basename(resolvedPath);
111
+ if (frontmatter.data.name !== dirName) {
112
+ addFinding('STRUCTURE', 'warning',
113
+ `Name "${frontmatter.data.name}" doesn't match directory "${dirName}"`);
114
+ }
115
+
116
+ // Check name length
117
+ if (frontmatter.data.name.length > 64) {
118
+ addFinding('STRUCTURE', 'error',
119
+ `Name exceeds 64 characters (${frontmatter.data.name.length})`);
120
+ }
121
+ }
122
+
123
+ if (!frontmatter.data.description) {
124
+ addFinding('STRUCTURE', 'error', 'Missing required field: description');
125
+ } else {
126
+ // Check description length
127
+ if (frontmatter.data.description.length > 1024) {
128
+ addFinding('DESCRIPTION', 'error',
129
+ `Description exceeds 1024 characters (${frontmatter.data.description.length})`);
130
+ }
131
+
132
+ // Check for third person
133
+ const desc = frontmatter.data.description.toLowerCase();
134
+ if (desc.startsWith('use this') || desc.startsWith('use when')) {
135
+ addFinding('DESCRIPTION', 'warning',
136
+ 'Description should use third person ("This skill should be used when...") not second person',
137
+ null,
138
+ 'Start with "This skill should be used when..." or similar third-person phrasing');
139
+ }
140
+
141
+ // Check for trigger phrases (quoted text)
142
+ const hasQuotedPhrases = /"[^"]+"|'[^']+'/.test(frontmatter.data.description);
143
+ if (!hasQuotedPhrases) {
144
+ addFinding('DESCRIPTION', 'warning',
145
+ 'Description lacks quoted trigger phrases',
146
+ null,
147
+ 'Add specific phrases users might say, e.g., "create a skill", "build a new skill"');
148
+ }
149
+
150
+ // Check if description is too detailed (might prevent activation)
151
+ if (frontmatter.data.description.length > 500 && !hasQuotedPhrases) {
152
+ addFinding('DESCRIPTION', 'info',
153
+ 'Long description without trigger phrases may reduce activation reliability');
154
+ }
155
+ }
156
+ }
157
+
158
+ // Check 3: Second person violations
159
+ const secondPersonPatterns = [
160
+ /\byou should\b/gi,
161
+ /\byou can\b/gi,
162
+ /\byou need\b/gi,
163
+ /\byou might\b/gi,
164
+ /\byou will\b/gi,
165
+ /\byou must\b/gi,
166
+ /\byour\b/gi
167
+ ];
168
+
169
+ let secondPersonCount = 0;
170
+ const secondPersonExamples = [];
171
+
172
+ for (let i = 0; i < lines.length; i++) {
173
+ const line = lines[i];
174
+ // Skip frontmatter
175
+ if (i < 3) continue;
176
+
177
+ for (const pattern of secondPersonPatterns) {
178
+ const matches = line.match(pattern);
179
+ if (matches) {
180
+ secondPersonCount += matches.length;
181
+ if (secondPersonExamples.length < 3) {
182
+ secondPersonExamples.push({ line: i + 1, text: line.trim().slice(0, 60) });
183
+ }
184
+ }
185
+ }
186
+ }
187
+
188
+ if (secondPersonCount > 0) {
189
+ addFinding('STYLE', 'error',
190
+ `Found ${secondPersonCount} instance(s) of second person language`,
191
+ secondPersonExamples[0]?.line,
192
+ 'Convert to imperative form: "You should parse" → "Parse"');
193
+ }
194
+
195
+ // Check 4: Negative framing
196
+ const negativePatterns = [
197
+ /\bdon't\b/gi,
198
+ /\bdo not\b/gi,
199
+ /\bnever\b/gi,
200
+ /\bavoid\b/gi,
201
+ /\bshould not\b/gi,
202
+ /\bshouldn't\b/gi,
203
+ /\bcan't\b/gi,
204
+ /\bcannot\b/gi
205
+ ];
206
+
207
+ let negativeCount = 0;
208
+ const negativeExamples = [];
209
+
210
+ for (let i = 0; i < lines.length; i++) {
211
+ const line = lines[i];
212
+ if (i < 3) continue;
213
+
214
+ for (const pattern of negativePatterns) {
215
+ const matches = line.match(pattern);
216
+ if (matches) {
217
+ negativeCount += matches.length;
218
+ if (negativeExamples.length < 3) {
219
+ negativeExamples.push({ line: i + 1, text: line.trim().slice(0, 60) });
220
+ }
221
+ }
222
+ }
223
+ }
224
+
225
+ if (negativeCount > 3) {
226
+ addFinding('STYLE', 'warning',
227
+ `Found ${negativeCount} instance(s) of negative framing`,
228
+ negativeExamples[0]?.line,
229
+ 'Use positive framing: "Don\'t do X" → "Do Y instead"');
230
+ }
231
+
232
+ // Check 5: Body size
233
+ const wordCount = body.split(/\s+/).filter(w => w.length > 0).length;
234
+ const lineCount = body.split('\n').length;
235
+
236
+ if (wordCount > 5000) {
237
+ addFinding('SIZE', 'error',
238
+ `SKILL.md body is ${wordCount} words - strongly exceeds 2,000 word recommendation`,
239
+ null,
240
+ 'Move detailed content to references/ directory');
241
+ } else if (wordCount > 3500) {
242
+ addFinding('SIZE', 'warning',
243
+ `SKILL.md body is ${wordCount} words - exceeds 2,000 word recommendation`,
244
+ null,
245
+ 'Consider moving detailed content to references/ directory');
246
+ } else if (wordCount > 2000) {
247
+ addFinding('SIZE', 'info',
248
+ `SKILL.md body is ${wordCount} words - at upper limit of recommendation`);
249
+ }
250
+
251
+ // Check 6: Progressive disclosure
252
+ const hasReferences = fs.existsSync(path.join(resolvedPath, 'references'));
253
+ const hasScripts = fs.existsSync(path.join(resolvedPath, 'scripts'));
254
+
255
+ if (wordCount > 2000 && !hasReferences) {
256
+ addFinding('DISCLOSURE', 'warning',
257
+ 'Large SKILL.md without references/ directory - not using progressive disclosure',
258
+ null,
259
+ 'Create references/ and move detailed content there');
260
+ }
261
+
262
+ // Check 7: Reference integrity
263
+ const referencePaths = body.match(/`references\/[^`]+`|references\/[\w.-]+/g) || [];
264
+ const scriptPaths = body.match(/`scripts\/[^`]+`|scripts\/[\w.-]+/g) || [];
265
+
266
+ for (const ref of referencePaths) {
267
+ const cleanPath = ref.replace(/`/g, '');
268
+ const fullPath = path.join(resolvedPath, cleanPath);
269
+
270
+ if (!fs.existsSync(fullPath)) {
271
+ addFinding('BROKEN', 'error',
272
+ `Referenced file doesn't exist: ${cleanPath}`);
273
+ }
274
+ }
275
+
276
+ for (const ref of scriptPaths) {
277
+ const cleanPath = ref.replace(/`/g, '');
278
+ const fullPath = path.join(resolvedPath, cleanPath);
279
+
280
+ if (!fs.existsSync(fullPath)) {
281
+ addFinding('BROKEN', 'error',
282
+ `Referenced script doesn't exist: ${cleanPath}`);
283
+ }
284
+ }
285
+
286
+ // Check 8: Unreferenced resources
287
+ if (hasReferences) {
288
+ const refDir = path.join(resolvedPath, 'references');
289
+ const refFiles = fs.readdirSync(refDir);
290
+
291
+ for (const file of refFiles) {
292
+ const isReferenced = body.includes(`references/${file}`) ||
293
+ body.includes(`references/${path.basename(file, '.md')}`);
294
+ if (!isReferenced && file.endsWith('.md')) {
295
+ addFinding('DISCLOSURE', 'info',
296
+ `File references/${file} exists but may not be referenced in SKILL.md`);
297
+ }
298
+ }
299
+ }
300
+
301
+ if (hasScripts) {
302
+ const scriptDir = path.join(resolvedPath, 'scripts');
303
+ const scriptFiles = fs.readdirSync(scriptDir);
304
+
305
+ for (const file of scriptFiles) {
306
+ const isReferenced = body.includes(`scripts/${file}`);
307
+ if (!isReferenced) {
308
+ addFinding('DISCLOSURE', 'info',
309
+ `File scripts/${file} exists but may not be referenced in SKILL.md`);
310
+ }
311
+ }
312
+ }
313
+
314
+ // === OUTPUT ===
315
+
316
+ const result = {
317
+ skill: {
318
+ path: resolvedPath,
319
+ name: frontmatter.data?.name || path.basename(resolvedPath),
320
+ wordCount,
321
+ lineCount,
322
+ hasReferences,
323
+ hasScripts
324
+ },
325
+ findings,
326
+ summary: {
327
+ errors: findings.filter(f => f.severity === 'error').length,
328
+ warnings: findings.filter(f => f.severity === 'warning').length,
329
+ info: findings.filter(f => f.severity === 'info').length,
330
+ passed: findings.filter(f => f.severity === 'error').length === 0
331
+ }
332
+ };
333
+
334
+ if (jsonOutput) {
335
+ console.log(JSON.stringify(result, null, 2));
336
+ } else {
337
+ console.log(`\n=== Skill Validation: ${result.skill.name} ===\n`);
338
+ console.log(`Path: ${result.skill.path}`);
339
+ console.log(`Words: ${result.skill.wordCount}`);
340
+ console.log(`Has references/: ${result.skill.hasReferences ? 'Yes' : 'No'}`);
341
+ console.log(`Has scripts/: ${result.skill.hasScripts ? 'Yes' : 'No'}`);
342
+ console.log();
343
+
344
+ if (findings.length === 0) {
345
+ console.log('✓ All checks passed!\n');
346
+ } else {
347
+ // Group by severity
348
+ const errors = findings.filter(f => f.severity === 'error');
349
+ const warnings = findings.filter(f => f.severity === 'warning');
350
+ const infos = findings.filter(f => f.severity === 'info');
351
+
352
+ if (errors.length > 0) {
353
+ console.log('ERRORS:');
354
+ for (const f of errors) {
355
+ console.log(` ✗ [${f.category}] ${f.message}`);
356
+ if (f.line) console.log(` Line ${f.line}`);
357
+ if (f.suggestion) console.log(` Suggestion: ${f.suggestion}`);
358
+ }
359
+ console.log();
360
+ }
361
+
362
+ if (warnings.length > 0) {
363
+ console.log('WARNINGS:');
364
+ for (const f of warnings) {
365
+ console.log(` ! [${f.category}] ${f.message}`);
366
+ if (f.line) console.log(` Line ${f.line}`);
367
+ if (f.suggestion) console.log(` Suggestion: ${f.suggestion}`);
368
+ }
369
+ console.log();
370
+ }
371
+
372
+ if (infos.length > 0) {
373
+ console.log('INFO:');
374
+ for (const f of infos) {
375
+ console.log(` i [${f.category}] ${f.message}`);
376
+ }
377
+ console.log();
378
+ }
379
+
380
+ console.log(`Summary: ${result.summary.errors} error(s), ${result.summary.warnings} warning(s), ${result.summary.info} info(s)`);
381
+ console.log(`Status: ${result.summary.passed ? 'PASSED' : 'FAILED'}\n`);
382
+ }
383
+ }
384
+
385
+ // Exit with error code if validation failed
386
+ process.exit(result.summary.passed ? 0 : 1);
@@ -23,7 +23,7 @@ Complete each step in order before proceeding to the next.
23
23
  Run the plan status script to find existing plans:
24
24
 
25
25
  ```bash
26
- node ~/.claude/scripts/plan-status.js
26
+ node ~/.claude/skills/orchestration/scripts/plan-status.js
27
27
  ```
28
28
 
29
29
  This returns a list of plans with their current status (draft, approved, in_progress, completed).
@@ -94,14 +94,37 @@ relevant_adrs:
94
94
  - id: ADR-[XXX]
95
95
  title: [title]
96
96
  constraint: [What this ADR requires us to do or avoid]
97
+
98
+ # Submodules affected by this work (for repos with git submodules)
99
+ affected_submodules:
100
+ - path: [relative/path/to/submodule]
101
+ reason: [Why this submodule is affected by the plan]
97
102
  ```
98
103
 
99
104
  **Field guidance:**
100
105
 
101
106
  - **Required**: id, title, type, status, created, problem_statement, goals, success_criteria
102
- - **Include if relevant**: integration_points, data_models, api_contracts, relevant_adrs
107
+ - **Include if relevant**: integration_points, data_models, api_contracts, relevant_adrs, affected_submodules
103
108
  - **Only include fields that were discussed**: The plan reflects the conversation from Phases 1 and 2
104
109
 
110
+ **affected_submodules field:**
111
+
112
+ Include this field when working in repositories with git submodules and the plan's scope touches specific submodules. This enables scope-aware ADR discovery—the adr-agent can filter to only show ADRs that are either global or scoped to the affected submodules.
113
+
114
+ - `path` (required): Relative path to the submodule, matching what appears in `.gitmodules`
115
+ - `reason` (optional): Brief explanation of why this submodule is affected by the plan
116
+
117
+ Example:
118
+ ```yaml
119
+ affected_submodules:
120
+ - path: packages/auth
121
+ reason: Adding new authentication method
122
+ - path: packages/shared
123
+ reason: Shared types need updating for new auth flow
124
+ ```
125
+
126
+ When a plan declares affected_submodules, ADRs scoped to those submodules become relevant, while ADRs scoped to unaffected submodules can be filtered out. ADRs with no scope field are always treated as global and remain relevant regardless of which submodules are affected.
127
+
105
128
  **Why no approach or risks?** The plan defines WHAT we're building, not HOW. The approach (phases, task breakdown) gets figured out during decomposition in execution. Risks emerge naturally during exploration or decomposition.
106
129
  </step>
107
130
 
@@ -56,10 +56,13 @@ While codebase exploration is happening, also spawn the adr-agent to find archit
56
56
  ```
57
57
  Spawn adr-agent: "Find ADRs that would be relevant to [summary of requirements].
58
58
  For each relevant ADR, explain why it matters and what
59
- constraints it imposes on this work."
59
+ constraints it imposes on this work.
60
+
61
+ If this work affects specific submodules, mention which ones
62
+ so the agent can filter ADRs by scope."
60
63
  ```
61
64
 
62
- ADRs discovered now will inform the plan you write. Better to know the rules before you design than to discover violations later.
65
+ ADRs discovered now will inform the plan you write. Better to know the rules before you design than to discover violations later. When specific submodules are involved, scope-aware discovery surfaces the most relevant ADRs while filtering out ADRs that only apply elsewhere.
63
66
  </step>
64
67
 
65
68
  <step name="Synthesize Findings">
@@ -18,6 +18,7 @@ Complete each step in order before proceeding to the next.
18
18
  Spawn the adr-agent to validate the plan. The agent will:
19
19
  - Read the plan.yaml
20
20
  - Find ALL ADRs that might be relevant (not just the ones declared in the plan)
21
+ - Use the plan's `affected_submodules` field for scope-aware filtering
21
22
  - Check compliance for each: COMPLIANT, TENSION, or CONFLICT
22
23
  - Report what needs attention
23
24
 
@@ -28,12 +29,16 @@ Find all ADRs that could be relevant to this work—check beyond what's
28
29
  declared in the plan's relevant_adrs field. Better to surface extra
29
30
  ADRs than miss important ones.
30
31
 
32
+ Use the plan's affected_submodules field to focus on ADRs that apply
33
+ to the relevant submodules. Global ADRs always apply; submodule-scoped
34
+ ADRs only apply when their scope matches the affected submodules.
35
+
31
36
  For each relevant ADR, report:
32
37
  - COMPLIANT: Plan follows this ADR
33
38
  - TENSION: Potential friction, worth noting
34
39
  - CONFLICT: Plan violates this ADR, must resolve
35
40
 
36
- Return a validation report with findings for each ADR."
41
+ Note in the report when ADRs were filtered by scope."
37
42
  ```
38
43
 
39
44
  **Show the validation report to the user** before proceeding.
@@ -10,7 +10,7 @@
10
10
  * file scanning.
11
11
  *
12
12
  * Usage:
13
- * node ~/.claude/scripts/plan-status.js
13
+ * node ~/.claude/skills/orchestration/scripts/plan-status.js
14
14
  *
15
15
  * Output:
16
16
  * JSON object to stdout: