@eltonssouza/development-utility-kit 0.10.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/.claude/agents/README.md +24 -0
- package/.claude/agents/analyst.md +198 -0
- package/.claude/agents/backend-developer.md +126 -0
- package/.claude/agents/brain-keeper.md +229 -0
- package/.claude/agents/code-reviewer.md +181 -0
- package/.claude/agents/database-engineer.md +94 -0
- package/.claude/agents/devops-engineer.md +141 -0
- package/.claude/agents/frontend-developer.md +97 -0
- package/.claude/agents/gate-keeper.md +118 -0
- package/.claude/agents/migrator.md +291 -0
- package/.claude/agents/mobile-developer.md +80 -0
- package/.claude/agents/n8n-specialist.md +94 -0
- package/.claude/agents/product-owner.md +115 -0
- package/.claude/agents/qa-engineer.md +232 -0
- package/.claude/agents/release-engineer.md +204 -0
- package/.claude/agents/scaffold.md +87 -0
- package/.claude/agents/security-engineer.md +199 -0
- package/.claude/agents/sprint-runner.md +46 -0
- package/.claude/agents/stack-resolver.md +104 -0
- package/.claude/agents/tech-lead.md +182 -0
- package/.claude/agents/update-template.md +54 -0
- package/.claude/agents/ux-designer.md +118 -0
- package/.claude/hooks/flow-guard.js +261 -0
- package/.claude/hooks/flow-state.js +197 -0
- package/.claude/local/CLAUDE.md +71 -0
- package/.claude/settings.json +55 -0
- package/.claude/skills/README.md +331 -0
- package/.claude/skills/active-project/SKILL.md +131 -0
- package/.claude/skills/api-integration-test/SKILL.md +84 -0
- package/.claude/skills/auto-test-guard/SKILL.md +239 -0
- package/.claude/skills/auto-test-guard/resources/backend-tests.md +20 -0
- package/.claude/skills/auto-test-guard/resources/e2e-tests.md +24 -0
- package/.claude/skills/auto-test-guard/resources/execution-report.md +49 -0
- package/.claude/skills/auto-test-guard/resources/frontend-tests.md +18 -0
- package/.claude/skills/auto-test-guard/resources/initial-setup.md +108 -0
- package/.claude/skills/auto-test-guard/resources/run-suite.md +48 -0
- package/.claude/skills/auto-test-guard/resources/senior-gate.md +19 -0
- package/.claude/skills/brain-keeper/SKILL.md +62 -0
- package/.claude/skills/brain-keeper/obsidian/app.json +9 -0
- package/.claude/skills/brain-keeper/obsidian/appearance.json +4 -0
- package/.claude/skills/brain-keeper/obsidian/core-plugins.json +20 -0
- package/.claude/skills/brain-keeper/obsidian/daily-notes.json +5 -0
- package/.claude/skills/brain-keeper/obsidian/graph.json +32 -0
- package/.claude/skills/brain-keeper/obsidian/snippets/folder-colors.css +90 -0
- package/.claude/skills/brain-keeper/obsidian/templates.json +5 -0
- package/.claude/skills/brain-keeper/templates/README.md +51 -0
- package/.claude/skills/brain-keeper/templates/adr.md +40 -0
- package/.claude/skills/brain-keeper/templates/bug.md +35 -0
- package/.claude/skills/brain-keeper/templates/daily.md +38 -0
- package/.claude/skills/brain-keeper/templates/feature.md +62 -0
- package/.claude/skills/brain-keeper/templates/meeting.md +34 -0
- package/.claude/skills/brain-keeper/templates/tech-debt.md +21 -0
- package/.claude/skills/caveman/SKILL.md +189 -0
- package/.claude/skills/create-stack-pack/SKILL.md +281 -0
- package/.claude/skills/grill-me/SKILL.md +80 -0
- package/.claude/skills/pair-debug/SKILL.md +288 -0
- package/.claude/skills/prd-ready-check/SKILL.md +86 -0
- package/.claude/skills/project-manager/SKILL.md +334 -0
- package/.claude/skills/quality-standards/SKILL.md +203 -0
- package/.claude/skills/quick-feature/SKILL.md +266 -0
- package/.claude/skills/run-sprint/SKILL.md +41 -0
- package/.claude/skills/scaffold/SKILL.md +60 -0
- package/.claude/skills/stack-discovery/SKILL.md +161 -0
- package/.claude/skills/test-coverage-auditor/SKILL.md +87 -0
- package/.claude/skills/to-issues/SKILL.md +163 -0
- package/.claude/skills/to-prd/SKILL.md +130 -0
- package/.claude/skills/update-template/SKILL.md +256 -0
- package/.claude/stacks/CODEOWNERS +30 -0
- package/.claude/stacks/README.md +97 -0
- package/.claude/stacks/_template.md +116 -0
- package/.claude/stacks/dotnet/aspire-9.md +528 -0
- package/.claude/stacks/go/gin-1.10.md +570 -0
- package/.claude/stacks/java/spring-boot-3.md +376 -0
- package/.claude/stacks/java/spring-boot-4.md +438 -0
- package/.claude/stacks/node/express-5.md +538 -0
- package/.claude/stacks/python/django-5.md +483 -0
- package/.claude/stacks/python/fastapi-0.115.md +522 -0
- package/.claude/stacks/typescript/angular-18.md +420 -0
- package/.claude/stacks/typescript/angular-19.md +397 -0
- package/.claude/stacks/typescript/angular-21.md +494 -0
- package/CLAUDE.md +472 -0
- package/README.md +412 -0
- package/bin/cli.js +848 -0
- package/bin/lib/adr.js +146 -0
- package/bin/lib/backup.js +62 -0
- package/bin/lib/detect-stack.js +476 -0
- package/bin/lib/doctor.js +527 -0
- package/bin/lib/help.js +328 -0
- package/bin/lib/identity.js +108 -0
- package/bin/lib/lint-allowlist.json +15 -0
- package/bin/lib/lint.js +798 -0
- package/bin/lib/local-dir.js +68 -0
- package/bin/lib/manifest.js +236 -0
- package/bin/lib/sync-all.js +394 -0
- package/bin/lib/version-check.js +398 -0
- package/dashboard/db.js +321 -0
- package/dashboard/package.json +22 -0
- package/dashboard/public/app.js +853 -0
- package/dashboard/public/content/docs/agents-reference.en.md +911 -0
- package/dashboard/public/content/docs/architecture-overview.en.md +252 -0
- package/dashboard/public/content/docs/autonomy-matrix.en.md +186 -0
- package/dashboard/public/content/docs/cli-reference.en.md +538 -0
- package/dashboard/public/content/docs/git-flow.en.md +525 -0
- package/dashboard/public/content/docs/honcho-memory.en.md +394 -0
- package/dashboard/public/content/docs/hooks-reference.en.md +404 -0
- package/dashboard/public/content/docs/pipeline.en.md +414 -0
- package/dashboard/public/content/docs/plugins.en.md +289 -0
- package/dashboard/public/content/docs/quality-gate.en.md +315 -0
- package/dashboard/public/content/docs/skills-reference.en.md +484 -0
- package/dashboard/public/content/docs/stack-rules.en.md +362 -0
- package/dashboard/public/content/docs/troubleshooting.en.md +565 -0
- package/dashboard/public/content/manifest.json +114 -0
- package/dashboard/public/content/manual/backend.en.md +1053 -0
- package/dashboard/public/content/manual/existing-project.en.md +848 -0
- package/dashboard/public/content/manual/frontend.en.md +1008 -0
- package/dashboard/public/content/manual/fullstack.en.md +1459 -0
- package/dashboard/public/content/manual/mobile.en.md +837 -0
- package/dashboard/public/content/manual/quickstart.en.md +169 -0
- package/dashboard/public/index.html +217 -0
- package/dashboard/public/style.css +857 -0
- package/dashboard/public/vendor/marked.min.js +69 -0
- package/dashboard/rtk.js +143 -0
- package/dashboard/server-app.js +421 -0
- package/dashboard/server.js +104 -0
- package/dashboard/test/sprint1.test.js +406 -0
- package/dashboard/test/sprint2.test.js +571 -0
- package/dashboard/test/sprint3.test.js +560 -0
- package/package.json +33 -0
- package/scripts/hooks/subagent-telemetry.sh +14 -0
- package/scripts/hooks/telemetry-writer.js +250 -0
- package/scripts/latest-versions.json +56 -0
package/bin/lib/lint.js
ADDED
|
@@ -0,0 +1,798 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* `duk lint` — structural validation of the harness `.claude/` directory.
|
|
5
|
+
*
|
|
6
|
+
* Mechanical, deterministic, no LLM. Per ADR-034:
|
|
7
|
+
* "Mecânico, determinístico, scriptável → CLI duk."
|
|
8
|
+
*
|
|
9
|
+
* Consolidates and expands scripts/lint-harness.mjs (deprecated by this command).
|
|
10
|
+
*
|
|
11
|
+
* Categories:
|
|
12
|
+
* 1. skills — SKILL.md frontmatter (name, description, tools, model)
|
|
13
|
+
* 2. agents — agent frontmatter (name, description, model)
|
|
14
|
+
* 3. refs — agents referenced by skills must exist in .claude/agents/
|
|
15
|
+
* 4. adrs — ADR cross-refs (ADR-XYZ mentions must point to existing files)
|
|
16
|
+
* 5. stacks — pack files have required sections
|
|
17
|
+
* 6. d1-contract — skill body forbids "checklist|golden rule|inviolable rules";
|
|
18
|
+
* agent body forbids "PT triggers:" (from scripts/lint-harness.mjs)
|
|
19
|
+
*
|
|
20
|
+
* Output: violations grouped by category + summary.
|
|
21
|
+
* Exit code: 0 if no FAIL, 1 if any FAIL.
|
|
22
|
+
*
|
|
23
|
+
* Flags:
|
|
24
|
+
* --json machine-readable output
|
|
25
|
+
* --category <name> run only one category (comma-separated for many)
|
|
26
|
+
* --help show help
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
const fs = require('fs');
|
|
30
|
+
const path = require('path');
|
|
31
|
+
|
|
32
|
+
// ── Severities ──────────────────────────────────────────────────────────────
|
|
33
|
+
|
|
34
|
+
const ERROR = 'ERROR';
|
|
35
|
+
const WARN = 'WARN';
|
|
36
|
+
|
|
37
|
+
// ── Helpers ─────────────────────────────────────────────────────────────────
|
|
38
|
+
|
|
39
|
+
function packageRoot() {
|
|
40
|
+
return path.resolve(__dirname, '..', '..');
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function readSafe(p) {
|
|
44
|
+
try {
|
|
45
|
+
return fs.readFileSync(p, 'utf8');
|
|
46
|
+
} catch {
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function isDir(p) {
|
|
52
|
+
try {
|
|
53
|
+
return fs.statSync(p).isDirectory();
|
|
54
|
+
} catch {
|
|
55
|
+
return false;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function listDir(p) {
|
|
60
|
+
try {
|
|
61
|
+
return fs.readdirSync(p, { withFileTypes: true });
|
|
62
|
+
} catch {
|
|
63
|
+
return [];
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Extract a YAML-ish frontmatter block as a key→value object. Best-effort:
|
|
69
|
+
* supports `key: value`, multi-line values get the first line only, lists are
|
|
70
|
+
* captured as raw string. Returns { frontmatter, body, bodyStartLine }.
|
|
71
|
+
*/
|
|
72
|
+
function extractFrontmatter(content) {
|
|
73
|
+
const lines = content.split('\n');
|
|
74
|
+
if (lines[0].trim() !== '---') {
|
|
75
|
+
return { frontmatter: {}, body: content, bodyStartLine: 1 };
|
|
76
|
+
}
|
|
77
|
+
let end = -1;
|
|
78
|
+
for (let i = 1; i < lines.length; i++) {
|
|
79
|
+
if (lines[i].trim() === '---') { end = i; break; }
|
|
80
|
+
}
|
|
81
|
+
if (end === -1) {
|
|
82
|
+
return { frontmatter: {}, body: content, bodyStartLine: 1 };
|
|
83
|
+
}
|
|
84
|
+
const fmBlock = lines.slice(1, end);
|
|
85
|
+
const fm = {};
|
|
86
|
+
for (const rawLine of fmBlock) {
|
|
87
|
+
// Strip trailing \r so the regex anchor `$` works on CRLF files.
|
|
88
|
+
// Without this, `(.*)$` is unsatisfiable on lines ending in \r:
|
|
89
|
+
// - `.` does not match \r (no /s flag)
|
|
90
|
+
// - `$` matches end-of-string or before final \n, never before \r
|
|
91
|
+
const line = rawLine.replace(/\r$/, '');
|
|
92
|
+
const m = line.match(/^([A-Za-z0-9_-]+)\s*:\s*(.*)$/);
|
|
93
|
+
if (m) fm[m[1]] = m[2].trim();
|
|
94
|
+
}
|
|
95
|
+
return {
|
|
96
|
+
frontmatter: fm,
|
|
97
|
+
body: lines.slice(end + 1).join('\n'),
|
|
98
|
+
bodyStartLine: end + 2,
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// ── Walkers ─────────────────────────────────────────────────────────────────
|
|
103
|
+
|
|
104
|
+
function findSkills() {
|
|
105
|
+
const dir = path.join(packageRoot(), '.claude', 'skills');
|
|
106
|
+
const out = [];
|
|
107
|
+
for (const entry of listDir(dir)) {
|
|
108
|
+
if (entry.isDirectory()) {
|
|
109
|
+
const skillPath = path.join(dir, entry.name, 'SKILL.md');
|
|
110
|
+
if (fs.existsSync(skillPath)) {
|
|
111
|
+
out.push({ name: entry.name, path: skillPath });
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
return out;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function findAgents() {
|
|
119
|
+
const dir = path.join(packageRoot(), '.claude', 'agents');
|
|
120
|
+
const out = [];
|
|
121
|
+
for (const entry of listDir(dir)) {
|
|
122
|
+
// Skip README.md (directory index, not an agent definition) — per tech-debt Item 22.
|
|
123
|
+
if (
|
|
124
|
+
entry.isFile() &&
|
|
125
|
+
entry.name.endsWith('.md') &&
|
|
126
|
+
!entry.name.startsWith('_') &&
|
|
127
|
+
entry.name !== 'README.md'
|
|
128
|
+
) {
|
|
129
|
+
out.push({
|
|
130
|
+
name: entry.name.replace(/\.md$/, ''),
|
|
131
|
+
path: path.join(dir, entry.name),
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
return out;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function findADRs() {
|
|
139
|
+
const dir = path.join(packageRoot(), 'docs', 'brain', 'decisions');
|
|
140
|
+
const out = [];
|
|
141
|
+
for (const entry of listDir(dir)) {
|
|
142
|
+
if (entry.isFile() && /^ADR-\d{3}.*\.md$/.test(entry.name)) {
|
|
143
|
+
const m = entry.name.match(/^ADR-(\d{3})/);
|
|
144
|
+
out.push({
|
|
145
|
+
id: m ? m[1] : null,
|
|
146
|
+
filename: entry.name,
|
|
147
|
+
path: path.join(dir, entry.name),
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
return out;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function findStackPacks() {
|
|
155
|
+
const dir = path.join(packageRoot(), '.claude', 'stacks');
|
|
156
|
+
const out = [];
|
|
157
|
+
for (const langEntry of listDir(dir)) {
|
|
158
|
+
if (langEntry.isDirectory()) {
|
|
159
|
+
const langDir = path.join(dir, langEntry.name);
|
|
160
|
+
for (const file of listDir(langDir)) {
|
|
161
|
+
if (file.isFile() && file.name.endsWith('.md') && file.name !== 'README.md' && file.name !== '_template.md') {
|
|
162
|
+
out.push({
|
|
163
|
+
lang: langEntry.name,
|
|
164
|
+
framework: file.name.replace(/\.md$/, ''),
|
|
165
|
+
path: path.join(langDir, file.name),
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
return out;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// ── Category 1: skills.frontmatter ──────────────────────────────────────────
|
|
175
|
+
|
|
176
|
+
const SKILL_REQUIRED_FIELDS = ['name', 'description', 'tools', 'model'];
|
|
177
|
+
|
|
178
|
+
function checkSkillsFrontmatter() {
|
|
179
|
+
const violations = [];
|
|
180
|
+
for (const skill of findSkills()) {
|
|
181
|
+
const content = readSafe(skill.path);
|
|
182
|
+
if (content === null) {
|
|
183
|
+
violations.push({ category: 'skills', severity: ERROR, file: skill.path, line: 0, msg: 'cannot read file' });
|
|
184
|
+
continue;
|
|
185
|
+
}
|
|
186
|
+
const { frontmatter } = extractFrontmatter(content);
|
|
187
|
+
for (const f of SKILL_REQUIRED_FIELDS) {
|
|
188
|
+
if (!frontmatter[f]) {
|
|
189
|
+
violations.push({
|
|
190
|
+
category: 'skills', severity: ERROR, file: skill.path, line: 1,
|
|
191
|
+
msg: `frontmatter missing required field: ${f}`,
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
if (frontmatter.name && frontmatter.name !== skill.name) {
|
|
196
|
+
violations.push({
|
|
197
|
+
category: 'skills', severity: ERROR, file: skill.path, line: 1,
|
|
198
|
+
msg: `frontmatter name "${frontmatter.name}" != directory name "${skill.name}"`,
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
return violations;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// ── Category 2: agents.frontmatter ──────────────────────────────────────────
|
|
206
|
+
|
|
207
|
+
const AGENT_REQUIRED_FIELDS = ['name', 'description', 'model'];
|
|
208
|
+
|
|
209
|
+
function checkAgentsFrontmatter() {
|
|
210
|
+
const violations = [];
|
|
211
|
+
for (const agent of findAgents()) {
|
|
212
|
+
const content = readSafe(agent.path);
|
|
213
|
+
if (content === null) {
|
|
214
|
+
violations.push({ category: 'agents', severity: ERROR, file: agent.path, line: 0, msg: 'cannot read file' });
|
|
215
|
+
continue;
|
|
216
|
+
}
|
|
217
|
+
const { frontmatter } = extractFrontmatter(content);
|
|
218
|
+
for (const f of AGENT_REQUIRED_FIELDS) {
|
|
219
|
+
if (!frontmatter[f]) {
|
|
220
|
+
violations.push({
|
|
221
|
+
category: 'agents', severity: ERROR, file: agent.path, line: 1,
|
|
222
|
+
msg: `frontmatter missing required field: ${f}`,
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
if (frontmatter.name && frontmatter.name !== agent.name) {
|
|
227
|
+
violations.push({
|
|
228
|
+
category: 'agents', severity: ERROR, file: agent.path, line: 1,
|
|
229
|
+
msg: `frontmatter name "${frontmatter.name}" != filename "${agent.name}"`,
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
return violations;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// ── Category 3: refs (skills → agents) ──────────────────────────────────────
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Skills reference agents via backticks: `agent-name`. We extract candidates
|
|
240
|
+
* from SKILL.md bodies and check they exist on disk. Known "agent" word lists
|
|
241
|
+
* help us narrow down — we only flag references that look like agent names
|
|
242
|
+
* AND appear in the project-manager routing table (CLAUDE.md) or the skill's
|
|
243
|
+
* own SKILL.md body.
|
|
244
|
+
*/
|
|
245
|
+
function checkSkillAgentRefs() {
|
|
246
|
+
const violations = [];
|
|
247
|
+
const agentNames = new Set(findAgents().map((a) => a.name));
|
|
248
|
+
// Allow skills as "subagent_type" targets too (e.g., grill-me dispatches to analyst).
|
|
249
|
+
const skillNames = new Set(findSkills().map((s) => s.name));
|
|
250
|
+
|
|
251
|
+
// Common false-positives: code keywords, generic words. We only flag backticks
|
|
252
|
+
// matching the kebab-case pattern that look like agent names.
|
|
253
|
+
for (const skill of findSkills()) {
|
|
254
|
+
const content = readSafe(skill.path);
|
|
255
|
+
if (content === null) continue;
|
|
256
|
+
const { body, bodyStartLine } = extractFrontmatter(content);
|
|
257
|
+
const lines = body.split('\n');
|
|
258
|
+
|
|
259
|
+
// Look only at section "## 3. Routing table" or any table-row with explicit
|
|
260
|
+
// `subagent_type` mentions. Generic prose with backticks would generate
|
|
261
|
+
// too many false positives.
|
|
262
|
+
let inRoutingTable = false;
|
|
263
|
+
for (let i = 0; i < lines.length; i++) {
|
|
264
|
+
const line = lines[i];
|
|
265
|
+
if (/^##\s+\d?\.?\s*Routing table/i.test(line)) inRoutingTable = true;
|
|
266
|
+
else if (/^##\s+/.test(line)) inRoutingTable = false;
|
|
267
|
+
if (!inRoutingTable) continue;
|
|
268
|
+
|
|
269
|
+
// Extract `name` backtick refs in the line
|
|
270
|
+
const matches = line.match(/`([a-z][a-z0-9-]+)`/g);
|
|
271
|
+
if (!matches) continue;
|
|
272
|
+
for (const m of matches) {
|
|
273
|
+
const candidate = m.replace(/`/g, '');
|
|
274
|
+
// Skip if it's a skill name (legitimate cross-skill ref like `grill-me`).
|
|
275
|
+
if (skillNames.has(candidate)) continue;
|
|
276
|
+
// Heuristic: only flag candidates that look like agent role names
|
|
277
|
+
// (contain hyphen and end with -er, -ist, -owner, -lead, etc.)
|
|
278
|
+
if (!/-(er|ist|owner|lead|engineer|reviewer|analyst|architect|developer|specialist|designer|runner|keeper|migrator|resolver|auditor)$/.test(candidate)) {
|
|
279
|
+
continue;
|
|
280
|
+
}
|
|
281
|
+
if (!agentNames.has(candidate)) {
|
|
282
|
+
violations.push({
|
|
283
|
+
category: 'refs', severity: ERROR, file: skill.path,
|
|
284
|
+
line: bodyStartLine + i,
|
|
285
|
+
msg: `skill references unknown agent: \`${candidate}\``,
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
return violations;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// ── Category 4: adr cross-refs ──────────────────────────────────────────────
|
|
295
|
+
|
|
296
|
+
// Items 19 + 20 — robustness against false positives:
|
|
297
|
+
// - Item 19: ignore refs inside blockquote (`> ...`) or inside quoted phrases
|
|
298
|
+
// (e.g. ADR-031 line 51 quotes a hypothetical decision mentioning ADR-040).
|
|
299
|
+
// - Item 20: ignore documented gaps in `lint-allowlist.json` (e.g. ADR-001..006
|
|
300
|
+
// and ADR-009 deleted by design — recorded in tech-debt Items 4 and 20).
|
|
301
|
+
function loadAllowlist() {
|
|
302
|
+
const file = path.join(__dirname, 'lint-allowlist.json');
|
|
303
|
+
const raw = readSafe(file);
|
|
304
|
+
if (raw === null) return {};
|
|
305
|
+
try {
|
|
306
|
+
return JSON.parse(raw);
|
|
307
|
+
} catch {
|
|
308
|
+
return {};
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
function isMatchInsideQuotes(line, matchStr) {
|
|
313
|
+
const idx = line.indexOf(matchStr);
|
|
314
|
+
if (idx < 0) return false;
|
|
315
|
+
const before = line.substring(0, idx);
|
|
316
|
+
const dq = (before.match(/"/g) || []).length;
|
|
317
|
+
const sq = (before.match(/'/g) || []).length;
|
|
318
|
+
const bt = (before.match(/`/g) || []).length;
|
|
319
|
+
// Odd count of any quote char before the match means the match sits inside
|
|
320
|
+
// an open quoted span.
|
|
321
|
+
return dq % 2 === 1 || sq % 2 === 1 || bt % 2 === 1;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
function checkADRCrossRefs() {
|
|
325
|
+
const violations = [];
|
|
326
|
+
const adrs = findADRs();
|
|
327
|
+
const known = new Set(adrs.map((a) => a.id).filter(Boolean));
|
|
328
|
+
const allowlist = loadAllowlist();
|
|
329
|
+
const knownGaps = new Set(allowlist['adr-gaps-known'] || []);
|
|
330
|
+
|
|
331
|
+
for (const adr of adrs) {
|
|
332
|
+
const content = readSafe(adr.path);
|
|
333
|
+
if (content === null) continue;
|
|
334
|
+
const lines = content.split('\n');
|
|
335
|
+
for (let i = 0; i < lines.length; i++) {
|
|
336
|
+
const line = lines[i];
|
|
337
|
+
// Item 19: skip blockquote lines — citation context, not a real cross-ref.
|
|
338
|
+
if (/^\s*>/.test(line)) continue;
|
|
339
|
+
// Match ADR-XYZ (3 digits)
|
|
340
|
+
const matches = line.match(/\bADR-(\d{3})\b/g);
|
|
341
|
+
if (!matches) continue;
|
|
342
|
+
for (const m of matches) {
|
|
343
|
+
// Item 19: skip matches inside quoted spans (", ', or `).
|
|
344
|
+
if (isMatchInsideQuotes(line, m)) continue;
|
|
345
|
+
const id = m.replace(/^ADR-/, '');
|
|
346
|
+
// Self-reference is fine.
|
|
347
|
+
if (id === adr.id) continue;
|
|
348
|
+
if (!known.has(id)) {
|
|
349
|
+
violations.push({
|
|
350
|
+
category: 'adrs', severity: WARN, file: adr.path, line: i + 1,
|
|
351
|
+
msg: `references missing ${m}`,
|
|
352
|
+
});
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
// Detect gaps in ADR numbering (informational).
|
|
359
|
+
// Item 20: skip gaps listed in lint-allowlist.json["adr-gaps-known"].
|
|
360
|
+
const ids = Array.from(known).map((s) => parseInt(s, 10)).sort((a, b) => a - b);
|
|
361
|
+
for (let i = 1; i < ids.length; i++) {
|
|
362
|
+
if (ids[i] - ids[i - 1] > 1) {
|
|
363
|
+
// Gap: ADR-(prev+1)..ADR-(curr-1) missing
|
|
364
|
+
for (let g = ids[i - 1] + 1; g < ids[i]; g++) {
|
|
365
|
+
const padded = String(g).padStart(3, '0');
|
|
366
|
+
if (knownGaps.has(padded)) continue;
|
|
367
|
+
violations.push({
|
|
368
|
+
category: 'adrs', severity: WARN,
|
|
369
|
+
file: path.join(packageRoot(), 'docs/brain/decisions/'),
|
|
370
|
+
line: 0,
|
|
371
|
+
msg: `gap in ADR numbering: ADR-${padded} not found`,
|
|
372
|
+
});
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
return violations;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
// ── Category 5: stacks (basic structure) ───────────────────────────────────
|
|
381
|
+
|
|
382
|
+
// Section headings may carry an optional numeric prefix (e.g. "## 6. Build & run commands").
|
|
383
|
+
// Required for every active stack pack (per ADR-027).
|
|
384
|
+
const STACK_REQUIRED_SECTIONS = [
|
|
385
|
+
/^##\s+(?:\d+\.\s+)?Build\s*&\s*run/i,
|
|
386
|
+
/^##\s+(?:\d+\.\s+)?Code\s+patterns/i,
|
|
387
|
+
/^##\s+(?:\d+\.\s+)?Anti-patterns/i,
|
|
388
|
+
/^##\s+(?:\d+\.\s+)?Security/i,
|
|
389
|
+
/^##\s+(?:\d+\.\s+)?Testing/i,
|
|
390
|
+
];
|
|
391
|
+
|
|
392
|
+
function checkStackPacks() {
|
|
393
|
+
const violations = [];
|
|
394
|
+
const packs = findStackPacks();
|
|
395
|
+
if (packs.length === 0) {
|
|
396
|
+
violations.push({
|
|
397
|
+
category: 'stacks', severity: WARN,
|
|
398
|
+
file: path.join(packageRoot(), '.claude/stacks'),
|
|
399
|
+
line: 0,
|
|
400
|
+
msg: 'no stack packs found',
|
|
401
|
+
});
|
|
402
|
+
return violations;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
for (const pack of packs) {
|
|
406
|
+
const content = readSafe(pack.path);
|
|
407
|
+
if (content === null) continue;
|
|
408
|
+
const { body } = extractFrontmatter(content);
|
|
409
|
+
const lines = body.split('\n');
|
|
410
|
+
for (const required of STACK_REQUIRED_SECTIONS) {
|
|
411
|
+
if (!lines.some((l) => required.test(l))) {
|
|
412
|
+
violations.push({
|
|
413
|
+
category: 'stacks', severity: WARN, file: pack.path, line: 0,
|
|
414
|
+
msg: `pack missing recommended section matching: ${required}`,
|
|
415
|
+
});
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
return violations;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
// ── Category 6: D1 contract (from scripts/lint-harness.mjs) ────────────────
|
|
424
|
+
|
|
425
|
+
const PATTERN1_PAIRS = [
|
|
426
|
+
'brain-keeper',
|
|
427
|
+
'update-template',
|
|
428
|
+
'scaffold',
|
|
429
|
+
];
|
|
430
|
+
|
|
431
|
+
const SKILL_FORBIDDEN_SECTION_PATTERNS = [
|
|
432
|
+
/^#{1,6}\s+.*\bchecklist\b/i,
|
|
433
|
+
/^#{1,6}\s+.*\bgolden rule\b/i,
|
|
434
|
+
/^#{1,6}\s+.*\binviolable rules?\b/i,
|
|
435
|
+
];
|
|
436
|
+
|
|
437
|
+
const AGENT_FORBIDDEN_BODY_PATTERNS = [
|
|
438
|
+
/^PT triggers:/i,
|
|
439
|
+
/^##\s+PT triggers/i,
|
|
440
|
+
];
|
|
441
|
+
|
|
442
|
+
function checkD1Contract() {
|
|
443
|
+
const violations = [];
|
|
444
|
+
for (const name of PATTERN1_PAIRS) {
|
|
445
|
+
// Skill side
|
|
446
|
+
const skillPath = path.join(packageRoot(), '.claude', 'skills', name, 'SKILL.md');
|
|
447
|
+
if (!fs.existsSync(skillPath)) {
|
|
448
|
+
violations.push({
|
|
449
|
+
category: 'd1-contract', severity: WARN, file: skillPath, line: 0,
|
|
450
|
+
msg: `SKILL.md missing for Pattern 1 pair '${name}'`,
|
|
451
|
+
});
|
|
452
|
+
} else {
|
|
453
|
+
const content = readSafe(skillPath);
|
|
454
|
+
const { body, bodyStartLine } = extractFrontmatter(content || '');
|
|
455
|
+
const lines = body.split('\n');
|
|
456
|
+
for (let i = 0; i < lines.length; i++) {
|
|
457
|
+
for (const pattern of SKILL_FORBIDDEN_SECTION_PATTERNS) {
|
|
458
|
+
if (pattern.test(lines[i])) {
|
|
459
|
+
violations.push({
|
|
460
|
+
category: 'd1-contract', severity: WARN, file: skillPath,
|
|
461
|
+
line: bodyStartLine + i,
|
|
462
|
+
msg: `skill body contains forbidden section: "${lines[i].trim()}"`,
|
|
463
|
+
});
|
|
464
|
+
break;
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
// Agent side
|
|
470
|
+
const agentPath = path.join(packageRoot(), '.claude', 'agents', `${name}.md`);
|
|
471
|
+
if (!fs.existsSync(agentPath)) {
|
|
472
|
+
violations.push({
|
|
473
|
+
category: 'd1-contract', severity: WARN, file: agentPath, line: 0,
|
|
474
|
+
msg: `agent file missing for Pattern 1 pair '${name}'`,
|
|
475
|
+
});
|
|
476
|
+
} else {
|
|
477
|
+
const content = readSafe(agentPath);
|
|
478
|
+
const { body, bodyStartLine } = extractFrontmatter(content || '');
|
|
479
|
+
const lines = body.split('\n');
|
|
480
|
+
for (let i = 0; i < lines.length; i++) {
|
|
481
|
+
for (const pattern of AGENT_FORBIDDEN_BODY_PATTERNS) {
|
|
482
|
+
if (pattern.test(lines[i])) {
|
|
483
|
+
violations.push({
|
|
484
|
+
category: 'd1-contract', severity: WARN, file: agentPath,
|
|
485
|
+
line: bodyStartLine + i,
|
|
486
|
+
msg: `agent body contains PT trigger section: "${lines[i].trim()}"`,
|
|
487
|
+
});
|
|
488
|
+
break;
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
return violations;
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
// ── Category 7: hook-registry (T2.4 / Sprint 2) ───────────────────────────
|
|
498
|
+
// Asserts that flow-guard.js is registered as a UserPromptSubmit hook in
|
|
499
|
+
// .claude/settings.json. Fails if the hook file is absent or not wired.
|
|
500
|
+
|
|
501
|
+
function checkHookRegistry() {
|
|
502
|
+
const violations = [];
|
|
503
|
+
const root = packageRoot();
|
|
504
|
+
const settingsPath = path.join(root, '.claude', 'settings.json');
|
|
505
|
+
const hookPath = path.join(root, '.claude', 'hooks', 'flow-guard.js');
|
|
506
|
+
|
|
507
|
+
// 1. Hook file must exist
|
|
508
|
+
if (!fs.existsSync(hookPath)) {
|
|
509
|
+
violations.push({
|
|
510
|
+
category: 'hook-registry', severity: ERROR, file: hookPath, line: 0,
|
|
511
|
+
msg: 'flow-guard.js not found at .claude/hooks/flow-guard.js',
|
|
512
|
+
});
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
// 2. settings.json must exist and contain UserPromptSubmit + flow-guard reference
|
|
516
|
+
const settingsContent = readSafe(settingsPath);
|
|
517
|
+
if (!settingsContent) {
|
|
518
|
+
violations.push({
|
|
519
|
+
category: 'hook-registry', severity: ERROR, file: settingsPath, line: 0,
|
|
520
|
+
msg: 'settings.json not found — flow-guard cannot be registered',
|
|
521
|
+
});
|
|
522
|
+
return violations;
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
let settings;
|
|
526
|
+
try {
|
|
527
|
+
settings = JSON.parse(settingsContent);
|
|
528
|
+
} catch {
|
|
529
|
+
violations.push({
|
|
530
|
+
category: 'hook-registry', severity: ERROR, file: settingsPath, line: 0,
|
|
531
|
+
msg: 'settings.json is not valid JSON',
|
|
532
|
+
});
|
|
533
|
+
return violations;
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
const hooks = settings && settings.hooks;
|
|
537
|
+
const userPromptHooks = hooks && hooks.UserPromptSubmit;
|
|
538
|
+
if (!Array.isArray(userPromptHooks) || userPromptHooks.length === 0) {
|
|
539
|
+
violations.push({
|
|
540
|
+
category: 'hook-registry', severity: ERROR, file: settingsPath, line: 0,
|
|
541
|
+
msg: 'settings.json has no UserPromptSubmit hooks — flow-guard must be registered',
|
|
542
|
+
});
|
|
543
|
+
return violations;
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
const hasFlowGuard = userPromptHooks.some((entry) => {
|
|
547
|
+
if (!entry || !Array.isArray(entry.hooks)) return false;
|
|
548
|
+
return entry.hooks.some((h) => h && h.command && h.command.includes('flow-guard'));
|
|
549
|
+
});
|
|
550
|
+
|
|
551
|
+
if (!hasFlowGuard) {
|
|
552
|
+
violations.push({
|
|
553
|
+
category: 'hook-registry', severity: ERROR, file: settingsPath, line: 0,
|
|
554
|
+
msg: 'flow-guard.js is not referenced in any UserPromptSubmit hook in settings.json',
|
|
555
|
+
});
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
return violations;
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
// ── Category 8: legacy-path-ref (T0.1 / WS-7) ─────────────────────────────
|
|
562
|
+
// Fails if any active .md file under .claude/ or dashboard/public/ still
|
|
563
|
+
// references the old repo name `claude-code-agents`.
|
|
564
|
+
|
|
565
|
+
const LEGACY_PATH_PATTERN = /claude-code-agents/;
|
|
566
|
+
|
|
567
|
+
function checkLegacyPathRefs() {
|
|
568
|
+
const violations = [];
|
|
569
|
+
const root = packageRoot();
|
|
570
|
+
|
|
571
|
+
function scanMdFiles(dir) {
|
|
572
|
+
let entries;
|
|
573
|
+
try { entries = fs.readdirSync(dir, { withFileTypes: true }); }
|
|
574
|
+
catch { return; }
|
|
575
|
+
for (const entry of entries) {
|
|
576
|
+
const full = path.join(dir, entry.name);
|
|
577
|
+
if (entry.isDirectory() && entry.name !== 'node_modules') {
|
|
578
|
+
scanMdFiles(full);
|
|
579
|
+
} else if (entry.isFile() && entry.name.endsWith('.md')) {
|
|
580
|
+
const content = readSafe(full);
|
|
581
|
+
if (!content) continue;
|
|
582
|
+
const lines = content.split('\n');
|
|
583
|
+
for (let i = 0; i < lines.length; i++) {
|
|
584
|
+
if (LEGACY_PATH_PATTERN.test(lines[i])) {
|
|
585
|
+
violations.push({
|
|
586
|
+
category: 'legacy-path-ref', severity: ERROR,
|
|
587
|
+
file: full, line: i + 1,
|
|
588
|
+
msg: `legacy name "claude-code-agents" found — replace with "development-utility-kit"`,
|
|
589
|
+
});
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
scanMdFiles(path.join(root, '.claude'));
|
|
597
|
+
scanMdFiles(path.join(root, 'dashboard', 'public'));
|
|
598
|
+
return violations;
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
// ── Category 8: pattern1-thin-skill (T0.2 / ADR-042) ──────────────────────
|
|
602
|
+
// run-sprint/SKILL.md must be a thin trigger (≤ 60 lines of body after
|
|
603
|
+
// frontmatter). The full execution contract lives in sprint-runner.md.
|
|
604
|
+
|
|
605
|
+
const THIN_SKILL_MAX_LINES = 60;
|
|
606
|
+
|
|
607
|
+
function checkPattern1ThinSkill() {
|
|
608
|
+
const violations = [];
|
|
609
|
+
const skillPath = path.join(packageRoot(), '.claude', 'skills', 'run-sprint', 'SKILL.md');
|
|
610
|
+
const content = readSafe(skillPath);
|
|
611
|
+
if (!content) {
|
|
612
|
+
violations.push({
|
|
613
|
+
category: 'pattern1-thin-skill', severity: ERROR, file: skillPath, line: 0,
|
|
614
|
+
msg: 'run-sprint/SKILL.md not found',
|
|
615
|
+
});
|
|
616
|
+
return violations;
|
|
617
|
+
}
|
|
618
|
+
const { body } = extractFrontmatter(content);
|
|
619
|
+
const bodyLines = body.split('\n').filter((l) => l.trim() !== '').length;
|
|
620
|
+
if (bodyLines > THIN_SKILL_MAX_LINES) {
|
|
621
|
+
violations.push({
|
|
622
|
+
category: 'pattern1-thin-skill', severity: ERROR, file: skillPath, line: 1,
|
|
623
|
+
msg: `run-sprint/SKILL.md has ${bodyLines} non-empty body lines (max ${THIN_SKILL_MAX_LINES}). Move the execution contract to .claude/agents/sprint-runner.md per ADR-042.`,
|
|
624
|
+
});
|
|
625
|
+
}
|
|
626
|
+
return violations;
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
// ── Category 9: moc-gap (T0.3 / WS-11) ────────────────────────────────────
|
|
630
|
+
// Every docs/brain/decisions/ADR-*.md must appear in docs/brain/README.md (MOC).
|
|
631
|
+
// Removing an ADR from the MOC = lint failure (ADR-044).
|
|
632
|
+
|
|
633
|
+
function checkMocGap() {
|
|
634
|
+
const violations = [];
|
|
635
|
+
const root = packageRoot();
|
|
636
|
+
const mocPath = path.join(root, 'docs', 'brain', 'README.md');
|
|
637
|
+
const mocContent = readSafe(mocPath);
|
|
638
|
+
if (!mocContent) {
|
|
639
|
+
violations.push({
|
|
640
|
+
category: 'moc-gap', severity: WARN, file: mocPath, line: 0,
|
|
641
|
+
msg: 'docs/brain/README.md (MOC) not found',
|
|
642
|
+
});
|
|
643
|
+
return violations;
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
const adrs = findADRs();
|
|
647
|
+
for (const adr of adrs) {
|
|
648
|
+
const id = adr.id;
|
|
649
|
+
if (!id) continue;
|
|
650
|
+
// Check MOC references this ADR by its full ID (e.g. ADR-041 or ADR-007)
|
|
651
|
+
if (!mocContent.includes(`ADR-${id}`)) {
|
|
652
|
+
violations.push({
|
|
653
|
+
category: 'moc-gap', severity: ERROR, file: mocPath, line: 0,
|
|
654
|
+
msg: `ADR-${id} (${adr.filename}) exists in decisions/ but is not listed in MOC (README.md)`,
|
|
655
|
+
});
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
return violations;
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
// ── Category 10: unversioned-referenced-adr (T0.4 / ADR-044) ──────────────
|
|
662
|
+
// Any ADR referenced by its ID in .claude/**/*.md must be tracked by git.
|
|
663
|
+
// Runs only when inside a git repo.
|
|
664
|
+
|
|
665
|
+
function checkUnversionedReferencedAdr() {
|
|
666
|
+
const violations = [];
|
|
667
|
+
const root = packageRoot();
|
|
668
|
+
|
|
669
|
+
// Build set of tracked ADR IDs via git ls-files
|
|
670
|
+
let trackedIds;
|
|
671
|
+
try {
|
|
672
|
+
const { execSync } = require('child_process');
|
|
673
|
+
const out = execSync('git ls-files docs/brain/decisions/', {
|
|
674
|
+
cwd: root, encoding: 'utf8', stdio: ['pipe', 'pipe', 'ignore'],
|
|
675
|
+
});
|
|
676
|
+
trackedIds = new Set(
|
|
677
|
+
out.split('\n')
|
|
678
|
+
.map((l) => { const m = l.match(/ADR-(\d{3})/); return m ? m[1] : null; })
|
|
679
|
+
.filter(Boolean)
|
|
680
|
+
);
|
|
681
|
+
} catch {
|
|
682
|
+
// Not in a git repo or git not available — skip silently.
|
|
683
|
+
return violations;
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
// Scan all .md files under .claude/ for ADR-XYZ references
|
|
687
|
+
function scanForRefs(dir) {
|
|
688
|
+
let entries;
|
|
689
|
+
try { entries = fs.readdirSync(dir, { withFileTypes: true }); }
|
|
690
|
+
catch { return; }
|
|
691
|
+
for (const entry of entries) {
|
|
692
|
+
const full = path.join(dir, entry.name);
|
|
693
|
+
if (entry.isDirectory() && entry.name !== 'node_modules') {
|
|
694
|
+
scanForRefs(full);
|
|
695
|
+
} else if (entry.isFile() && entry.name.endsWith('.md')) {
|
|
696
|
+
const content = readSafe(full);
|
|
697
|
+
if (!content) continue;
|
|
698
|
+
const lines = content.split('\n');
|
|
699
|
+
for (let i = 0; i < lines.length; i++) {
|
|
700
|
+
const refs = lines[i].match(/\bADR-(\d{3})\b/g);
|
|
701
|
+
if (!refs) continue;
|
|
702
|
+
for (const ref of refs) {
|
|
703
|
+
const id = ref.replace('ADR-', '');
|
|
704
|
+
if (!trackedIds.has(id)) {
|
|
705
|
+
violations.push({
|
|
706
|
+
category: 'unversioned-referenced-adr', severity: ERROR,
|
|
707
|
+
file: full, line: i + 1,
|
|
708
|
+
msg: `references ${ref} which is not tracked in git (run git add docs/brain/decisions/ADR-${id}-*.md)`,
|
|
709
|
+
});
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
scanForRefs(path.join(root, '.claude'));
|
|
718
|
+
return violations;
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
// ── Entry point ─────────────────────────────────────────────────────────────
|
|
722
|
+
|
|
723
|
+
const CATEGORIES = {
|
|
724
|
+
skills: { fn: checkSkillsFrontmatter, label: 'skills.frontmatter' },
|
|
725
|
+
agents: { fn: checkAgentsFrontmatter, label: 'agents.frontmatter' },
|
|
726
|
+
refs: { fn: checkSkillAgentRefs, label: 'skill → agent refs' },
|
|
727
|
+
adrs: { fn: checkADRCrossRefs, label: 'ADR cross-refs' },
|
|
728
|
+
stacks: { fn: checkStackPacks, label: 'stack pack structure' },
|
|
729
|
+
'd1-contract': { fn: checkD1Contract, label: 'D1 contract (skill/agent body)' },
|
|
730
|
+
'hook-registry': { fn: checkHookRegistry, label: 'hook registry (flow-guard wired)' },
|
|
731
|
+
'legacy-path-ref': { fn: checkLegacyPathRefs, label: 'legacy path refs (claude-code-agents)' },
|
|
732
|
+
'pattern1-thin-skill': { fn: checkPattern1ThinSkill, label: 'run-sprint thin trigger (ADR-042)' },
|
|
733
|
+
'moc-gap': { fn: checkMocGap, label: 'MOC ↔ decisions gap (ADR-044)' },
|
|
734
|
+
'unversioned-referenced-adr': { fn: checkUnversionedReferencedAdr, label: 'unversioned referenced ADR (ADR-044)' },
|
|
735
|
+
};
|
|
736
|
+
|
|
737
|
+
function summarize(violations) {
|
|
738
|
+
const counts = { ERROR: 0, WARN: 0 };
|
|
739
|
+
for (const v of violations) counts[v.severity] = (counts[v.severity] || 0) + 1;
|
|
740
|
+
return counts;
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
/**
|
|
744
|
+
* @param {{ json?: boolean, categories?: string[] }} [options]
|
|
745
|
+
* @returns {number} exit code
|
|
746
|
+
*/
|
|
747
|
+
function runLint(options) {
|
|
748
|
+
const opts = options || {};
|
|
749
|
+
const selected = opts.categories && opts.categories.length > 0
|
|
750
|
+
? opts.categories.filter((c) => CATEGORIES[c])
|
|
751
|
+
: Object.keys(CATEGORIES);
|
|
752
|
+
|
|
753
|
+
if (opts.categories && selected.length === 0) {
|
|
754
|
+
process.stderr.write(`Error: unknown category. Valid: ${Object.keys(CATEGORIES).join(', ')}\n`);
|
|
755
|
+
return 1;
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
let allViolations = [];
|
|
759
|
+
for (const cat of selected) {
|
|
760
|
+
allViolations = allViolations.concat(CATEGORIES[cat].fn());
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
const counts = summarize(allViolations);
|
|
764
|
+
|
|
765
|
+
if (opts.json) {
|
|
766
|
+
process.stdout.write(JSON.stringify({
|
|
767
|
+
summary: counts,
|
|
768
|
+
categories: selected,
|
|
769
|
+
violations: allViolations,
|
|
770
|
+
}, null, 2) + '\n');
|
|
771
|
+
} else {
|
|
772
|
+
process.stdout.write(`duk lint — harness structure validation\n`);
|
|
773
|
+
process.stdout.write(`Categories: ${selected.map((c) => CATEGORIES[c].label).join(', ')}\n\n`);
|
|
774
|
+
if (allViolations.length === 0) {
|
|
775
|
+
process.stdout.write('No violations.\n');
|
|
776
|
+
} else {
|
|
777
|
+
// Group by category for readable output
|
|
778
|
+
const grouped = {};
|
|
779
|
+
for (const v of allViolations) {
|
|
780
|
+
if (!grouped[v.category]) grouped[v.category] = [];
|
|
781
|
+
grouped[v.category].push(v);
|
|
782
|
+
}
|
|
783
|
+
for (const cat of Object.keys(grouped)) {
|
|
784
|
+
process.stdout.write(`── ${CATEGORIES[cat].label} (${grouped[cat].length}) ──\n`);
|
|
785
|
+
for (const v of grouped[cat]) {
|
|
786
|
+
const rel = path.relative(packageRoot(), v.file) || v.file;
|
|
787
|
+
process.stdout.write(` [${v.severity}] ${rel}:${v.line} — ${v.msg}\n`);
|
|
788
|
+
}
|
|
789
|
+
process.stdout.write('\n');
|
|
790
|
+
}
|
|
791
|
+
process.stdout.write(`Summary: ${counts.ERROR} ERROR, ${counts.WARN} WARN\n`);
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
return counts.ERROR > 0 ? 1 : 0;
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
module.exports = { runLint };
|