@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,527 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * `duk doctor` — environment validation for the development-utility-kit harness.
5
+ *
6
+ * Mechanical, deterministic, no LLM. Per ADR-034:
7
+ * "Mecânico, determinístico, scriptável → CLI duk."
8
+ *
9
+ * Checks:
10
+ * 1. environment — Node version, git, npx, python3 (warn-only for hooks)
11
+ * 2. harness — root directory, .claude/ structure, CLAUDE.md, package.json
12
+ * 3. settings — ~/.claude/settings.json exists, parseable, hook paths live
13
+ * 4. stacks — packs available in .claude/stacks/, README index present
14
+ * 5. credentials — C:/development/tools/credentials/vps.txt (warn if missing)
15
+ * 6. project — only if CWD is an adopted project: MANIFEST + Project Identity
16
+ *
17
+ * Output: aligned table (Category | Check | Status | Detail).
18
+ * Exit code: 0 if no FAIL, 1 if any FAIL. WARN never fails unless --strict.
19
+ *
20
+ * Flags:
21
+ * --json machine-readable output
22
+ * --strict exit non-zero on WARN too
23
+ * --help show help
24
+ */
25
+
26
+ const fs = require('fs');
27
+ const path = require('path');
28
+ const os = require('os');
29
+ const { spawnSync } = require('child_process');
30
+
31
+ // ── Status constants ────────────────────────────────────────────────────────
32
+
33
+ const PASS = 'PASS';
34
+ const WARN = 'WARN';
35
+ const FAIL = 'FAIL';
36
+
37
+ // ── Helpers ─────────────────────────────────────────────────────────────────
38
+
39
+ /**
40
+ * Run a command and return { stdout, status }. Never throws.
41
+ * @param {string} cmd
42
+ * @param {string[]} args
43
+ * @returns {{ stdout: string, status: number|null }}
44
+ */
45
+ function runCmd(cmd, args) {
46
+ try {
47
+ const r = spawnSync(cmd, args, {
48
+ encoding: 'utf8',
49
+ shell: process.platform === 'win32',
50
+ timeout: 5000,
51
+ });
52
+ return { stdout: (r.stdout || '').trim(), status: r.status };
53
+ } catch {
54
+ return { stdout: '', status: null };
55
+ }
56
+ }
57
+
58
+ function fileExists(p) {
59
+ try {
60
+ return fs.existsSync(p);
61
+ } catch {
62
+ return false;
63
+ }
64
+ }
65
+
66
+ function isDir(p) {
67
+ try {
68
+ return fs.statSync(p).isDirectory();
69
+ } catch {
70
+ return false;
71
+ }
72
+ }
73
+
74
+ function readJsonSafe(p) {
75
+ try {
76
+ return JSON.parse(fs.readFileSync(p, 'utf8'));
77
+ } catch {
78
+ return null;
79
+ }
80
+ }
81
+
82
+ function packageRoot() {
83
+ return path.resolve(__dirname, '..', '..');
84
+ }
85
+
86
+ function harnessVersion() {
87
+ const pkg = readJsonSafe(path.join(packageRoot(), 'package.json'));
88
+ return pkg && pkg.version ? pkg.version : '0.0.0';
89
+ }
90
+
91
+ // ── Individual checks ───────────────────────────────────────────────────────
92
+
93
+ /**
94
+ * @returns {Array<{category: string, name: string, status: string, detail: string}>}
95
+ */
96
+ function checkEnvironment() {
97
+ const out = [];
98
+
99
+ // Node version
100
+ const nodeVer = process.version.replace(/^v/, '');
101
+ const major = parseInt(nodeVer.split('.')[0], 10);
102
+ out.push({
103
+ category: 'environment',
104
+ name: 'node',
105
+ status: major >= 18 ? PASS : FAIL,
106
+ detail: `${process.version} (need >= 18)`,
107
+ });
108
+
109
+ // git
110
+ const git = runCmd('git', ['--version']);
111
+ out.push({
112
+ category: 'environment',
113
+ name: 'git',
114
+ status: git.status === 0 ? PASS : FAIL,
115
+ detail: git.status === 0 ? git.stdout : 'not in PATH',
116
+ });
117
+
118
+ // npx
119
+ const npx = runCmd('npx', ['--version']);
120
+ out.push({
121
+ category: 'environment',
122
+ name: 'npx',
123
+ status: npx.status === 0 ? PASS : WARN,
124
+ detail: npx.status === 0 ? `v${npx.stdout}` : 'not in PATH (some skills need it)',
125
+ });
126
+
127
+ // python3 (used by some plugin hooks)
128
+ let py = runCmd('python3', ['--version']);
129
+ if (py.status !== 0) py = runCmd('python', ['--version']);
130
+ out.push({
131
+ category: 'environment',
132
+ name: 'python',
133
+ status: py.status === 0 ? PASS : WARN,
134
+ detail: py.status === 0 ? py.stdout : 'not in PATH (some hooks need it)',
135
+ });
136
+
137
+ return out;
138
+ }
139
+
140
+ function checkHarness() {
141
+ const out = [];
142
+ const root = packageRoot();
143
+
144
+ // Root exists and is a git repo
145
+ const isGitRepo = isDir(path.join(root, '.git'));
146
+ out.push({
147
+ category: 'harness',
148
+ name: 'root',
149
+ status: isGitRepo ? PASS : FAIL,
150
+ detail: `${root} ${isGitRepo ? '(git repo)' : '(NOT a git repo)'}`,
151
+ });
152
+
153
+ // Required structure
154
+ const required = [
155
+ { rel: '.claude', kind: 'dir' },
156
+ { rel: '.claude/agents', kind: 'dir' },
157
+ { rel: '.claude/skills', kind: 'dir' },
158
+ { rel: '.claude/stacks', kind: 'dir' },
159
+ { rel: 'CLAUDE.md', kind: 'file' },
160
+ { rel: 'package.json', kind: 'file' },
161
+ { rel: 'bin/cli.js', kind: 'file' },
162
+ ];
163
+
164
+ for (const r of required) {
165
+ const full = path.join(root, r.rel);
166
+ const ok = r.kind === 'dir' ? isDir(full) : fileExists(full);
167
+ out.push({
168
+ category: 'harness',
169
+ name: r.rel,
170
+ status: ok ? PASS : FAIL,
171
+ detail: ok ? r.kind : `missing (${r.kind})`,
172
+ });
173
+ }
174
+
175
+ // Harness version
176
+ out.push({
177
+ category: 'harness',
178
+ name: 'version',
179
+ status: PASS,
180
+ detail: harnessVersion(),
181
+ });
182
+
183
+ return out;
184
+ }
185
+
186
+ function checkSettings() {
187
+ const out = [];
188
+ const settingsPath = path.join(os.homedir(), '.claude', 'settings.json');
189
+
190
+ if (!fileExists(settingsPath)) {
191
+ out.push({
192
+ category: 'settings',
193
+ name: '~/.claude/settings.json',
194
+ status: WARN,
195
+ detail: 'not found (hooks/permissions empty)',
196
+ });
197
+ return out;
198
+ }
199
+
200
+ const settings = readJsonSafe(settingsPath);
201
+ if (!settings) {
202
+ out.push({
203
+ category: 'settings',
204
+ name: '~/.claude/settings.json',
205
+ status: FAIL,
206
+ detail: 'exists but not valid JSON',
207
+ });
208
+ return out;
209
+ }
210
+
211
+ out.push({
212
+ category: 'settings',
213
+ name: '~/.claude/settings.json',
214
+ status: PASS,
215
+ detail: 'valid JSON',
216
+ });
217
+
218
+ // Walk hooks looking for absolute paths that don't exist on disk.
219
+ const brokenHooks = [];
220
+ const allHooks = [];
221
+ try {
222
+ const hooks = settings.hooks || {};
223
+ for (const eventName of Object.keys(hooks)) {
224
+ const arr = Array.isArray(hooks[eventName]) ? hooks[eventName] : [];
225
+ for (const entry of arr) {
226
+ const hookList = (entry && Array.isArray(entry.hooks)) ? entry.hooks : [];
227
+ for (const h of hookList) {
228
+ if (h && typeof h.command === 'string') {
229
+ // Extract first absolute-looking path from the command.
230
+ const m = h.command.match(/[A-Za-z]:[/\\][^\s"']+|\/[^\s"']+/);
231
+ if (m) {
232
+ const p = m[0];
233
+ allHooks.push({ event: eventName, path: p });
234
+ if (!fileExists(p)) brokenHooks.push({ event: eventName, path: p });
235
+ }
236
+ }
237
+ }
238
+ }
239
+ }
240
+ } catch {
241
+ // ignore — best-effort scan
242
+ }
243
+
244
+ out.push({
245
+ category: 'settings',
246
+ name: 'hooks.total',
247
+ status: PASS,
248
+ detail: `${allHooks.length} hook command(s) detected`,
249
+ });
250
+
251
+ if (brokenHooks.length > 0) {
252
+ for (const b of brokenHooks) {
253
+ out.push({
254
+ category: 'settings',
255
+ name: `hook.${b.event}`,
256
+ status: FAIL,
257
+ detail: `path does not exist: ${b.path}`,
258
+ });
259
+ }
260
+ } else if (allHooks.length > 0) {
261
+ out.push({
262
+ category: 'settings',
263
+ name: 'hooks.paths',
264
+ status: PASS,
265
+ detail: 'all hook paths exist',
266
+ });
267
+ }
268
+
269
+ return out;
270
+ }
271
+
272
+ function checkStacks() {
273
+ const out = [];
274
+ const stacksDir = path.join(packageRoot(), '.claude', 'stacks');
275
+
276
+ if (!isDir(stacksDir)) {
277
+ out.push({
278
+ category: 'stacks',
279
+ name: 'directory',
280
+ status: FAIL,
281
+ detail: 'missing .claude/stacks/',
282
+ });
283
+ return out;
284
+ }
285
+
286
+ // Index
287
+ const readme = path.join(stacksDir, 'README.md');
288
+ out.push({
289
+ category: 'stacks',
290
+ name: 'README.md',
291
+ status: fileExists(readme) ? PASS : WARN,
292
+ detail: fileExists(readme) ? 'present' : 'missing (index lost)',
293
+ });
294
+
295
+ // Walk languages/<framework>-<major>.md
296
+ let packs = [];
297
+ try {
298
+ const langs = fs.readdirSync(stacksDir, { withFileTypes: true })
299
+ .filter((d) => d.isDirectory())
300
+ .map((d) => d.name);
301
+ for (const lang of langs) {
302
+ const langDir = path.join(stacksDir, lang);
303
+ const files = fs.readdirSync(langDir).filter((f) => f.endsWith('.md'));
304
+ for (const f of files) {
305
+ packs.push(`${lang}/${f.replace(/\.md$/, '')}`);
306
+ }
307
+ }
308
+ } catch {
309
+ // ignore
310
+ }
311
+
312
+ out.push({
313
+ category: 'stacks',
314
+ name: 'packs.count',
315
+ status: packs.length > 0 ? PASS : WARN,
316
+ detail: packs.length === 0 ? 'no packs found' : `${packs.length} pack(s): ${packs.join(', ')}`,
317
+ });
318
+
319
+ return out;
320
+ }
321
+
322
+ function checkOperationalGuardRail() {
323
+ return [{
324
+ category: 'guidance',
325
+ name: 'mass-edits',
326
+ status: PASS,
327
+ detail: 'Use Cowork/sandbox for mass reads & validates — avoid mass native-git edits in Claude Code CLI on Windows (OS locks, AV scans).',
328
+ }];
329
+ }
330
+
331
+ function checkCredentials() {
332
+ const out = [];
333
+ const credPath = process.platform === 'win32'
334
+ ? 'C:/development/tools/credentials/vps.txt'
335
+ : path.join(os.homedir(), 'development/tools/credentials/vps.txt');
336
+
337
+ out.push({
338
+ category: 'credentials',
339
+ name: 'vps.txt',
340
+ status: fileExists(credPath) ? PASS : WARN,
341
+ detail: fileExists(credPath) ? credPath : `not found at ${credPath} (skills that need VPS access will prompt)`,
342
+ });
343
+
344
+ return out;
345
+ }
346
+
347
+ /**
348
+ * Check whether a .claude.bak/ directory exists and is restorable
349
+ * (i.e., it contains at least one .md file — not an empty dir).
350
+ * @param {string} dir Project directory to inspect.
351
+ * @returns {Array<{category,name,status,detail}>}
352
+ */
353
+ function checkBackupRestore(dir) {
354
+ const out = [];
355
+ const bakDir = path.join(dir, '.claude.bak');
356
+
357
+ if (!isDir(bakDir)) {
358
+ // No backup present — this is fine (first install or intentionally cleaned).
359
+ out.push({
360
+ category: 'backup',
361
+ name: '.claude.bak',
362
+ status: PASS,
363
+ detail: 'no backup present (normal for first install)',
364
+ });
365
+ return out;
366
+ }
367
+
368
+ // Backup exists — verify it is restorable (contains agent/skill files).
369
+ let fileCount = 0;
370
+ try {
371
+ const walk = (d) => {
372
+ for (const entry of fs.readdirSync(d, { withFileTypes: true })) {
373
+ const full = path.join(d, entry.name);
374
+ if (entry.isDirectory()) walk(full);
375
+ else if (entry.isFile()) fileCount++;
376
+ }
377
+ };
378
+ walk(bakDir);
379
+ } catch {
380
+ // ignore
381
+ }
382
+
383
+ if (fileCount === 0) {
384
+ out.push({
385
+ category: 'backup',
386
+ name: '.claude.bak',
387
+ status: WARN,
388
+ detail: `backup directory exists but is empty — not restorable (rm -rf .claude.bak to clear)`,
389
+ });
390
+ } else {
391
+ out.push({
392
+ category: 'backup',
393
+ name: '.claude.bak',
394
+ status: PASS,
395
+ detail: `backup present and restorable (${fileCount} file(s) — restore: cp -r .claude.bak .claude)`,
396
+ });
397
+ }
398
+
399
+ return out;
400
+ }
401
+
402
+ function checkProject() {
403
+ // Only check the current working directory — and only if it looks like an adopted project (not the harness itself).
404
+ const out = [];
405
+ const cwd = process.cwd();
406
+ const root = packageRoot();
407
+
408
+ // Skip when invoked from the harness root itself.
409
+ if (path.resolve(cwd) === path.resolve(root)) {
410
+ return out;
411
+ }
412
+
413
+ // Always check backup regardless of whether .claude/ exists.
414
+ out.push(...checkBackupRestore(cwd));
415
+
416
+ const cwdClaude = path.join(cwd, '.claude');
417
+ if (!isDir(cwdClaude)) {
418
+ out.push({
419
+ category: 'project',
420
+ name: 'cwd',
421
+ status: WARN,
422
+ detail: 'CWD is not an adopted project (no .claude/) — run `duk install` to adopt',
423
+ });
424
+ return out;
425
+ }
426
+
427
+ // MANIFEST baseline
428
+ const manifest = path.join(cwdClaude, '.MANIFEST');
429
+ out.push({
430
+ category: 'project',
431
+ name: '.MANIFEST',
432
+ status: fileExists(manifest) ? PASS : WARN,
433
+ detail: fileExists(manifest) ? 'baseline present' : 'missing (run `duk install` to create)',
434
+ });
435
+
436
+ // Project Identity in CLAUDE.md (not the placeholder)
437
+ const claudeMd = path.join(cwd, 'CLAUDE.md');
438
+ if (fileExists(claudeMd)) {
439
+ const txt = fs.readFileSync(claudeMd, 'utf8');
440
+ const filled = !/Project name.*<project-name>/i.test(txt) && /## Project Identity/i.test(txt);
441
+ out.push({
442
+ category: 'project',
443
+ name: 'Project Identity',
444
+ status: filled ? PASS : WARN,
445
+ detail: filled ? 'filled' : 'placeholder `<project-name>` still present — run stack discovery',
446
+ });
447
+ } else {
448
+ out.push({
449
+ category: 'project',
450
+ name: 'CLAUDE.md',
451
+ status: FAIL,
452
+ detail: 'missing',
453
+ });
454
+ }
455
+
456
+ return out;
457
+ }
458
+
459
+ // ── Formatting ──────────────────────────────────────────────────────────────
460
+
461
+ function formatTable(rows) {
462
+ if (rows.length === 0) return '';
463
+ const headers = ['CATEGORY', 'CHECK', 'STATUS', 'DETAIL'];
464
+ const widths = headers.map((h, i) => {
465
+ const colKey = ['category', 'name', 'status', 'detail'][i];
466
+ return Math.max(h.length, ...rows.map((r) => String(r[colKey] || '').length));
467
+ });
468
+
469
+ const pad = (s, w) => String(s || '').padEnd(w);
470
+ const sep = widths.map((w) => '─'.repeat(w)).join('─┼─');
471
+ const header = headers.map((h, i) => pad(h, widths[i])).join(' │ ');
472
+ const body = rows.map((r) =>
473
+ [r.category, r.name, r.status, r.detail].map((v, i) => pad(v, widths[i])).join(' │ ')
474
+ ).join('\n');
475
+
476
+ return `${header}\n${sep}\n${body}`;
477
+ }
478
+
479
+ function summarize(rows) {
480
+ const counts = { PASS: 0, WARN: 0, FAIL: 0 };
481
+ for (const r of rows) counts[r.status] = (counts[r.status] || 0) + 1;
482
+ return counts;
483
+ }
484
+
485
+ // ── Entry point ─────────────────────────────────────────────────────────────
486
+
487
+ /**
488
+ * @param {{ json?: boolean, strict?: boolean }} [options]
489
+ * @returns {number} exit code
490
+ */
491
+ function runDoctor(options) {
492
+ const opts = options || {};
493
+ const rows = [
494
+ ...checkEnvironment(),
495
+ ...checkHarness(),
496
+ ...checkSettings(),
497
+ ...checkStacks(),
498
+ ...checkCredentials(),
499
+ ...checkOperationalGuardRail(),
500
+ ...checkProject(),
501
+ ];
502
+
503
+ const counts = summarize(rows);
504
+
505
+ if (opts.json) {
506
+ process.stdout.write(JSON.stringify({ summary: counts, checks: rows }, null, 2) + '\n');
507
+ } else {
508
+ process.stdout.write('duk doctor — environment validation\n\n');
509
+ process.stdout.write(formatTable(rows) + '\n\n');
510
+ process.stdout.write(
511
+ `Summary: ${counts.PASS} PASS, ${counts.WARN} WARN, ${counts.FAIL} FAIL\n`
512
+ );
513
+ if (counts.FAIL > 0) {
514
+ process.stdout.write('\nAction required: fix FAIL items before continuing.\n');
515
+ } else if (counts.WARN > 0) {
516
+ process.stdout.write('\nNon-blocking warnings present. Review when convenient.\n');
517
+ } else {
518
+ process.stdout.write('\nAll clear.\n');
519
+ }
520
+ }
521
+
522
+ if (counts.FAIL > 0) return 1;
523
+ if (opts.strict && counts.WARN > 0) return 1;
524
+ return 0;
525
+ }
526
+
527
+ module.exports = { runDoctor };