@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.
Files changed (131) hide show
  1. package/.claude/agents/README.md +24 -0
  2. package/.claude/agents/analyst.md +198 -0
  3. package/.claude/agents/backend-developer.md +126 -0
  4. package/.claude/agents/brain-keeper.md +229 -0
  5. package/.claude/agents/code-reviewer.md +181 -0
  6. package/.claude/agents/database-engineer.md +94 -0
  7. package/.claude/agents/devops-engineer.md +141 -0
  8. package/.claude/agents/frontend-developer.md +97 -0
  9. package/.claude/agents/gate-keeper.md +118 -0
  10. package/.claude/agents/migrator.md +291 -0
  11. package/.claude/agents/mobile-developer.md +80 -0
  12. package/.claude/agents/n8n-specialist.md +94 -0
  13. package/.claude/agents/product-owner.md +115 -0
  14. package/.claude/agents/qa-engineer.md +232 -0
  15. package/.claude/agents/release-engineer.md +204 -0
  16. package/.claude/agents/scaffold.md +87 -0
  17. package/.claude/agents/security-engineer.md +199 -0
  18. package/.claude/agents/sprint-runner.md +46 -0
  19. package/.claude/agents/stack-resolver.md +104 -0
  20. package/.claude/agents/tech-lead.md +182 -0
  21. package/.claude/agents/update-template.md +54 -0
  22. package/.claude/agents/ux-designer.md +118 -0
  23. package/.claude/hooks/flow-guard.js +261 -0
  24. package/.claude/hooks/flow-state.js +197 -0
  25. package/.claude/local/CLAUDE.md +71 -0
  26. package/.claude/settings.json +55 -0
  27. package/.claude/skills/README.md +331 -0
  28. package/.claude/skills/active-project/SKILL.md +131 -0
  29. package/.claude/skills/api-integration-test/SKILL.md +84 -0
  30. package/.claude/skills/auto-test-guard/SKILL.md +239 -0
  31. package/.claude/skills/auto-test-guard/resources/backend-tests.md +20 -0
  32. package/.claude/skills/auto-test-guard/resources/e2e-tests.md +24 -0
  33. package/.claude/skills/auto-test-guard/resources/execution-report.md +49 -0
  34. package/.claude/skills/auto-test-guard/resources/frontend-tests.md +18 -0
  35. package/.claude/skills/auto-test-guard/resources/initial-setup.md +108 -0
  36. package/.claude/skills/auto-test-guard/resources/run-suite.md +48 -0
  37. package/.claude/skills/auto-test-guard/resources/senior-gate.md +19 -0
  38. package/.claude/skills/brain-keeper/SKILL.md +62 -0
  39. package/.claude/skills/brain-keeper/obsidian/app.json +9 -0
  40. package/.claude/skills/brain-keeper/obsidian/appearance.json +4 -0
  41. package/.claude/skills/brain-keeper/obsidian/core-plugins.json +20 -0
  42. package/.claude/skills/brain-keeper/obsidian/daily-notes.json +5 -0
  43. package/.claude/skills/brain-keeper/obsidian/graph.json +32 -0
  44. package/.claude/skills/brain-keeper/obsidian/snippets/folder-colors.css +90 -0
  45. package/.claude/skills/brain-keeper/obsidian/templates.json +5 -0
  46. package/.claude/skills/brain-keeper/templates/README.md +51 -0
  47. package/.claude/skills/brain-keeper/templates/adr.md +40 -0
  48. package/.claude/skills/brain-keeper/templates/bug.md +35 -0
  49. package/.claude/skills/brain-keeper/templates/daily.md +38 -0
  50. package/.claude/skills/brain-keeper/templates/feature.md +62 -0
  51. package/.claude/skills/brain-keeper/templates/meeting.md +34 -0
  52. package/.claude/skills/brain-keeper/templates/tech-debt.md +21 -0
  53. package/.claude/skills/caveman/SKILL.md +189 -0
  54. package/.claude/skills/create-stack-pack/SKILL.md +281 -0
  55. package/.claude/skills/grill-me/SKILL.md +80 -0
  56. package/.claude/skills/pair-debug/SKILL.md +288 -0
  57. package/.claude/skills/prd-ready-check/SKILL.md +86 -0
  58. package/.claude/skills/project-manager/SKILL.md +334 -0
  59. package/.claude/skills/quality-standards/SKILL.md +203 -0
  60. package/.claude/skills/quick-feature/SKILL.md +266 -0
  61. package/.claude/skills/run-sprint/SKILL.md +41 -0
  62. package/.claude/skills/scaffold/SKILL.md +60 -0
  63. package/.claude/skills/stack-discovery/SKILL.md +161 -0
  64. package/.claude/skills/test-coverage-auditor/SKILL.md +87 -0
  65. package/.claude/skills/to-issues/SKILL.md +163 -0
  66. package/.claude/skills/to-prd/SKILL.md +130 -0
  67. package/.claude/skills/update-template/SKILL.md +256 -0
  68. package/.claude/stacks/CODEOWNERS +30 -0
  69. package/.claude/stacks/README.md +97 -0
  70. package/.claude/stacks/_template.md +116 -0
  71. package/.claude/stacks/dotnet/aspire-9.md +528 -0
  72. package/.claude/stacks/go/gin-1.10.md +570 -0
  73. package/.claude/stacks/java/spring-boot-3.md +376 -0
  74. package/.claude/stacks/java/spring-boot-4.md +438 -0
  75. package/.claude/stacks/node/express-5.md +538 -0
  76. package/.claude/stacks/python/django-5.md +483 -0
  77. package/.claude/stacks/python/fastapi-0.115.md +522 -0
  78. package/.claude/stacks/typescript/angular-18.md +420 -0
  79. package/.claude/stacks/typescript/angular-19.md +397 -0
  80. package/.claude/stacks/typescript/angular-21.md +494 -0
  81. package/CLAUDE.md +472 -0
  82. package/README.md +412 -0
  83. package/bin/cli.js +848 -0
  84. package/bin/lib/adr.js +146 -0
  85. package/bin/lib/backup.js +62 -0
  86. package/bin/lib/detect-stack.js +476 -0
  87. package/bin/lib/doctor.js +527 -0
  88. package/bin/lib/help.js +328 -0
  89. package/bin/lib/identity.js +108 -0
  90. package/bin/lib/lint-allowlist.json +15 -0
  91. package/bin/lib/lint.js +798 -0
  92. package/bin/lib/local-dir.js +68 -0
  93. package/bin/lib/manifest.js +236 -0
  94. package/bin/lib/sync-all.js +394 -0
  95. package/bin/lib/version-check.js +398 -0
  96. package/dashboard/db.js +321 -0
  97. package/dashboard/package.json +22 -0
  98. package/dashboard/public/app.js +853 -0
  99. package/dashboard/public/content/docs/agents-reference.en.md +911 -0
  100. package/dashboard/public/content/docs/architecture-overview.en.md +252 -0
  101. package/dashboard/public/content/docs/autonomy-matrix.en.md +186 -0
  102. package/dashboard/public/content/docs/cli-reference.en.md +538 -0
  103. package/dashboard/public/content/docs/git-flow.en.md +525 -0
  104. package/dashboard/public/content/docs/honcho-memory.en.md +394 -0
  105. package/dashboard/public/content/docs/hooks-reference.en.md +404 -0
  106. package/dashboard/public/content/docs/pipeline.en.md +414 -0
  107. package/dashboard/public/content/docs/plugins.en.md +289 -0
  108. package/dashboard/public/content/docs/quality-gate.en.md +315 -0
  109. package/dashboard/public/content/docs/skills-reference.en.md +484 -0
  110. package/dashboard/public/content/docs/stack-rules.en.md +362 -0
  111. package/dashboard/public/content/docs/troubleshooting.en.md +565 -0
  112. package/dashboard/public/content/manifest.json +114 -0
  113. package/dashboard/public/content/manual/backend.en.md +1053 -0
  114. package/dashboard/public/content/manual/existing-project.en.md +848 -0
  115. package/dashboard/public/content/manual/frontend.en.md +1008 -0
  116. package/dashboard/public/content/manual/fullstack.en.md +1459 -0
  117. package/dashboard/public/content/manual/mobile.en.md +837 -0
  118. package/dashboard/public/content/manual/quickstart.en.md +169 -0
  119. package/dashboard/public/index.html +217 -0
  120. package/dashboard/public/style.css +857 -0
  121. package/dashboard/public/vendor/marked.min.js +69 -0
  122. package/dashboard/rtk.js +143 -0
  123. package/dashboard/server-app.js +421 -0
  124. package/dashboard/server.js +104 -0
  125. package/dashboard/test/sprint1.test.js +406 -0
  126. package/dashboard/test/sprint2.test.js +571 -0
  127. package/dashboard/test/sprint3.test.js +560 -0
  128. package/package.json +33 -0
  129. package/scripts/hooks/subagent-telemetry.sh +14 -0
  130. package/scripts/hooks/telemetry-writer.js +250 -0
  131. package/scripts/latest-versions.json +56 -0
@@ -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 };