thachvd-kit 1.0.36 → 1.0.37

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/bin/cli.js CHANGED
@@ -1,1735 +1,1785 @@
1
- #!/usr/bin/env node
2
-
3
- const fs = require('fs');
4
- const path = require('path');
5
- const { spawnSync } = require('child_process');
6
- const prompts = require('prompts');
7
- const pc = require('picocolors');
8
- const packageJson = require('../package.json');
9
- const mattSkills = require('./matt-skills');
10
- const {
11
- QUALITY_FLOOR_SECTION,
12
- AGENT_RULES_SECTION,
13
- CONVENTIONS_STANDARDS_SECTION,
14
- FAST_PATH_SECTION,
15
- GETTING_STARTED_FAST_PATH_SECTION,
16
- TOOL_ROUTING_SECTION
17
- } = require('./policy');
18
-
19
- const sourceDir = path.resolve(__dirname, '..');
20
- const targetDir = process.cwd();
21
- const CODEBASE_MEMORY_PACKAGE = 'codebase-memory-mcp';
22
- const CODEBASE_MEMORY_BINARY = 'codebase-memory-mcp';
23
- const RTK_INSTALL_COMMAND = 'cargo install --git https://github.com/rtk-ai/rtk --branch master rtk';
24
-
25
- const FRAMEWORK_CHOICES = [
26
- 'vue', 'react', 'next', 'nuxt', 'svelte', 'angular',
27
- 'laravel', 'cakephp', 'symfony', 'codeigniter',
28
- 'express', 'fastapi', 'django', 'flask', 'gin', 'fiber', 'echo', 'nestjs',
29
- 'rails', 'spring', 'aspnet',
30
- 'tauri', 'axum', 'actix', 'flutter', 'react-native'
31
- ];
32
-
33
- const LANGUAGE_CHOICES = ['typescript', 'javascript', 'python', 'go', 'php', 'rust', 'java', 'ruby', 'swift', 'kotlin', 'csharp', 'cpp'];
34
- const PROJECT_TYPE_CHOICES = ['web-app', 'api', 'mobile', 'desktop', 'cli', 'fullstack'];
35
- const DATABASE_CHOICES = ['postgres', 'mysql', 'sqlite', 'mongodb', 'redis', 'dynamodb', 'none'];
36
- const PACKAGE_MANAGER_CHOICES = ['npm', 'pnpm', 'yarn', 'bun', 'pip', 'uv', 'go mod', 'cargo', 'composer', 'auto-detect'];
37
- const TEST_FRAMEWORK_CHOICES = ['jest', 'vitest', 'pytest', 'go test', 'phpunit', 'pest', 'rspec', 'junit', 'auto-detect'];
38
- const CLOUD_CHOICES = ['aws', 'gcp', 'azure', 'vercel', 'fly', 'railway', 'none'];
39
- const FRONTEND_FRAMEWORKS = ['vue', 'react', 'next', 'nuxt', 'svelte', 'angular'];
40
- const BACKEND_FRAMEWORKS = ['laravel', 'cakephp', 'symfony', 'codeigniter', 'express', 'fastapi', 'django', 'flask', 'gin', 'fiber', 'echo', 'nestjs', 'rails', 'spring', 'aspnet', 'axum', 'actix'];
41
- const MOBILE_DESKTOP_FRAMEWORKS = ['tauri', 'flutter', 'react-native'];
42
- const IGNORED_SCAN_DIRS = new Set(['.git', 'node_modules', 'vendor', 'dist', 'build', 'coverage', '.next', '.nuxt', '.turbo', '.cache', 'storage', 'tmp']);
43
-
44
- function printHelp() {
45
- console.log(`
46
- ${pc.bold('Usage:')}
47
- thachvd-kit init [--yes]
48
- thachvd-kit upgrade [--dry-run]
49
- thachvd-kit global [--dry-run] [--antigravity-only|--codex-only]
50
- thachvd-kit setup [--yes] [--no-install-skills]
51
- thachvd-kit doctor
52
- thachvd-kit prompt
53
- thachvd-kit skills install [--dry-run]
54
- thachvd-kit skills check
55
- thachvd-kit skills update [--dry-run]
56
- thachvd-kit --version
57
- thachvd-kit --help
58
-
59
- ${pc.bold('Commands:')}
60
- init Generate project context files and documentation (new projects)
61
- upgrade Safely add/update thachvd-kit's managed policy blocks in an existing project (does not rescan or regenerate)
62
- global Patch a managed tool-routing block into the current user's Antigravity/Codex global instructions
63
- setup Install/configure MCP, Matt Pocock skills, and recommended local tooling
64
- doctor Check project context and local integrations
65
- prompt Print the saved project-indexing prompt again
66
- skills Install, check, or update the promoted Matt Pocock skill set
67
-
68
- ${pc.bold('Options:')}
69
- --yes Overwrite generated files or accept setup defaults
70
- --no-setup-mcp Skip MCP setup (legacy init compatibility)
71
- --no-setup-hook Skip the Claude Code Definition-of-Done Stop hook during setup
72
- --no-install-rtk Skip auto-installing rtk via cargo during setup
73
- --no-index Skip auto-indexing the repository with codebase-memory-mcp during setup
74
- --no-install-skills Skip installing Matt Pocock skills during setup
75
- --dry-run Preview a change without writing it (upgrade/global/skills install/skills update)
76
- --antigravity-only Limit global to the Antigravity global instructions
77
- --codex-only Limit global to the Codex global instructions
78
- --version Show the installed CLI version
79
- --help Show this help message
80
-
81
- ${pc.bold('What gets generated:')}
82
- AGENTS.md Shared instructions for Codex, Antigravity, Claude Code, and Cursor
83
- CLAUDE.md Claude Code entry file that imports AGENTS.md
84
- GEMINI.md Antigravity entry file that points to AGENTS.md
85
- .cursorrules Cursor entry file that points to AGENTS.md
86
- .agent/docs/ Project context, architecture, workflow, tooling, and durable onboarding prompt
87
-
88
- ${pc.bold('Workflow:')}
89
- Matt Pocock's promoted skills (installed under .agents/skills/) are the
90
- workflow layer. Use the matching skill when a specialized workflow is
91
- useful; if unsure which one, use /ask-matt. Do not invoke a heavyweight
92
- skill for a localized, obvious change.
93
-
94
- ${pc.bold('Tip:')}
95
- Run thachvd-kit setup to install the promoted skill set and configure
96
- local tooling, then thachvd-kit doctor to verify the machine.
97
- `);
98
- }
99
-
100
- function formatScanPreviewLines(scanned) {
101
- return [
102
- ['App root', scanned.app_root || '.'],
103
- ['Project type', scanned.project_type || 'unknown'],
104
- ['Primary language', scanned.primary_language || 'unknown'],
105
- ['Frameworks', (scanned.frameworks || []).join(', ') || 'no framework'],
106
- ['Database', scanned.database || 'none'],
107
- ['Package manager', scanned.package_manager || 'auto-detect'],
108
- ['Test framework', scanned.test_framework || 'auto-detect'],
109
- ['Cloud', scanned.cloud || 'none'],
110
- ['Infrastructure', (scanned.infrastructure || []).join(', ') || 'none'],
111
- ['CI/CD', scanned.ci_cd || 'none']
112
- ];
113
- }
114
-
115
- async function askQuestions(questions) {
116
- return prompts(questions, {
117
- onCancel: () => {
118
- console.log(pc.yellow('\nCancelled.'));
119
- process.exit(130);
120
- }
121
- });
122
- }
123
-
124
- function fileExists(...parts) {
125
- return fs.existsSync(path.join(targetDir, ...parts));
126
- }
127
-
128
- function fileExistsAt(baseDir, ...parts) {
129
- return fs.existsSync(path.join(baseDir, ...parts));
130
- }
131
-
132
- function readJsonIfExists(...parts) {
133
- const fullPath = path.join(targetDir, ...parts);
134
- if (!fs.existsSync(fullPath)) return null;
135
- try {
136
- return JSON.parse(fs.readFileSync(fullPath, 'utf8'));
137
- } catch {
138
- return null;
139
- }
140
- }
141
-
142
- function readJsonIfExistsAt(baseDir, ...parts) {
143
- const fullPath = path.join(baseDir, ...parts);
144
- if (!fs.existsSync(fullPath)) return null;
145
- try {
146
- return JSON.parse(fs.readFileSync(fullPath, 'utf8'));
147
- } catch {
148
- return null;
149
- }
150
- }
151
-
152
- function readTextIfExists(...parts) {
153
- const fullPath = path.join(targetDir, ...parts);
154
- if (!fs.existsSync(fullPath)) return '';
155
- try {
156
- return fs.readFileSync(fullPath, 'utf8');
157
- } catch {
158
- return '';
159
- }
160
- }
161
-
162
- function readTextIfExistsAt(baseDir, ...parts) {
163
- const fullPath = path.join(baseDir, ...parts);
164
- if (!fs.existsSync(fullPath)) return '';
165
- try {
166
- return fs.readFileSync(fullPath, 'utf8');
167
- } catch {
168
- return '';
169
- }
170
- }
171
-
172
- function detectPackageManager(baseDir = targetDir) {
173
- if (fileExistsAt(baseDir, 'pnpm-lock.yaml')) return 'pnpm';
174
- if (fileExistsAt(baseDir, 'yarn.lock')) return 'yarn';
175
- if (fileExistsAt(baseDir, 'bun.lockb') || fileExistsAt(baseDir, 'bun.lock')) return 'bun';
176
- if (fileExistsAt(baseDir, 'package-lock.json')) return 'npm';
177
- if (fileExistsAt(baseDir, 'uv.lock')) return 'uv';
178
- if (fileExistsAt(baseDir, 'poetry.lock')) return 'pip';
179
- if (fileExistsAt(baseDir, 'requirements.txt') || fileExistsAt(baseDir, 'pyproject.toml')) return 'pip';
180
- if (fileExistsAt(baseDir, 'go.mod')) return 'go mod';
181
- if (fileExistsAt(baseDir, 'Cargo.toml')) return 'cargo';
182
- if (fileExistsAt(baseDir, 'composer.json')) return 'composer';
183
- return 'auto-detect';
184
- }
185
-
186
- function uniq(values) {
187
- return Array.from(new Set(values.filter(Boolean)));
188
- }
189
-
190
- function detectAppRoot(baseDir) {
191
- const candidates = [baseDir];
192
- const queue = [{ dir: baseDir, depth: 0 }];
193
-
194
- while (queue.length > 0) {
195
- const { dir, depth } = queue.shift();
196
- if (depth >= 2) continue;
197
- let entries = [];
198
- try {
199
- entries = fs.readdirSync(dir, { withFileTypes: true });
200
- } catch {
201
- continue;
202
- }
203
- for (const entry of entries) {
204
- if (!entry.isDirectory()) continue;
205
- if (IGNORED_SCAN_DIRS.has(entry.name)) continue;
206
- const nextDir = path.join(dir, entry.name);
207
- candidates.push(nextDir);
208
- queue.push({ dir: nextDir, depth: depth + 1 });
209
- }
210
- }
211
-
212
- const scoreCandidate = dir => {
213
- let score = 0;
214
- if (fileExistsAt(dir, 'composer.json')) score += 5;
215
- if (fileExistsAt(dir, 'package.json')) score += 4;
216
- if (fileExistsAt(dir, 'Cargo.toml')) score += 4;
217
- if (fileExistsAt(dir, 'go.mod')) score += 4;
218
- if (fileExistsAt(dir, 'pyproject.toml') || fileExistsAt(dir, 'requirements.txt')) score += 4;
219
- if (fileExistsAt(dir, 'artisan')) score += 5;
220
- if (fileExistsAt(dir, 'bin', 'cake') || fileExistsAt(dir, 'app', 'Config', 'core.php')) score += 5;
221
- if (fileExistsAt(dir, 'src-tauri', 'tauri.conf.json') || fileExistsAt(dir, 'src-tauri', 'tauri.conf.json5')) score += 5;
222
- if (fileExistsAt(dir, 'manage.py')) score += 4;
223
- if (fileExistsAt(dir, 'Gemfile')) score += 4;
224
- if (fileExistsAt(dir, 'pom.xml') || fileExistsAt(dir, 'build.gradle') || fileExistsAt(dir, 'build.gradle.kts')) score += 4;
225
- if (fileExistsAt(dir, 'app', 'View') || fileExistsAt(dir, 'resources', 'views') || fileExistsAt(dir, 'templates') || fileExistsAt(dir, 'app', 'views')) score += 2;
226
- return score;
227
- };
228
-
229
- const ranked = candidates
230
- .map(dir => ({ dir, score: scoreCandidate(dir) }))
231
- .filter(item => item.score > 0)
232
- .sort((a, b) => (b.score - a.score) || (a.dir.length - b.dir.length));
233
-
234
- return ranked[0]?.dir || baseDir;
235
- }
236
-
237
- function repoHasAny(baseDir, relativePaths, maxDepth = 3) {
238
- const queue = [{ dir: baseDir, depth: 0 }];
239
- while (queue.length > 0) {
240
- const { dir, depth } = queue.shift();
241
- for (const relativePath of relativePaths) {
242
- if (fileExistsAt(dir, ...relativePath)) return true;
243
- }
244
- if (depth >= maxDepth) continue;
245
- let entries = [];
246
- try {
247
- entries = fs.readdirSync(dir, { withFileTypes: true });
248
- } catch {
249
- continue;
250
- }
251
- for (const entry of entries) {
252
- if (!entry.isDirectory()) continue;
253
- if (IGNORED_SCAN_DIRS.has(entry.name)) continue;
254
- queue.push({ dir: path.join(dir, entry.name), depth: depth + 1 });
255
- }
256
- }
257
- return false;
258
- }
259
-
260
- function evidenceIf(condition, label, weight = 1) {
261
- return condition ? { label, weight } : null;
262
- }
263
-
264
- function buildDetection(id, evidence, meta = {}) {
265
- const hits = evidence.filter(Boolean);
266
- const score = hits.reduce((sum, hit) => sum + hit.weight, 0);
267
- return {
268
- id,
269
- score,
270
- evidence: hits.map(hit => hit.label),
271
- ...meta
272
- };
273
- }
274
-
275
- function confidenceLabel(score) {
276
- if (score >= 6) return 'high';
277
- if (score >= 3) return 'medium';
278
- return 'low';
279
- }
280
-
281
- function hasDependency(depNames, ...names) {
282
- return names.some(name => depNames.has(name));
283
- }
284
-
285
- function hasComposerDependency(composerDepNames, ...names) {
286
- return names.some(name => composerDepNames.has(name));
287
- }
288
-
289
- function hasRootFile(rootEntries, ...filenames) {
290
- return filenames.some(name => rootEntries.has(name));
291
- }
292
-
293
- function textMatches(text, pattern) {
294
- return Boolean(text && pattern.test(text));
295
- }
296
-
297
- const LANGUAGE_DETECTORS = [
298
- {
299
- id: 'typescript',
300
- detect: ctx => buildDetection('typescript', [
301
- evidenceIf(ctx.fileExists('tsconfig.json'), 'found tsconfig.json', 4),
302
- evidenceIf(hasDependency(ctx.depNames, 'typescript', 'ts-node', 'tsx'), 'package.json -> typescript toolchain', 2)
303
- ])
304
- },
305
- {
306
- id: 'javascript',
307
- detect: ctx => buildDetection('javascript', [
308
- evidenceIf(ctx.fileExists('package.json'), 'found package.json', 2),
309
- evidenceIf(ctx.depNames.size > 0, 'package.json -> JavaScript dependency graph', 1)
310
- ])
311
- },
312
- {
313
- id: 'python',
314
- detect: ctx => buildDetection('python', [
315
- evidenceIf(ctx.fileExists('requirements.txt'), 'found requirements.txt', 3),
316
- evidenceIf(ctx.fileExists('pyproject.toml'), 'found pyproject.toml', 2),
317
- evidenceIf(textMatches(ctx.pyprojectText, /\[tool\.(poetry|uv|hatch|pdm)\]/i), 'pyproject.toml -> Python tool section', 2)
318
- ])
319
- },
320
- {
321
- id: 'go',
322
- detect: ctx => buildDetection('go', [
323
- evidenceIf(ctx.fileExists('go.mod'), 'found go.mod', 4)
324
- ])
325
- },
326
- {
327
- id: 'php',
328
- detect: ctx => buildDetection('php', [
329
- evidenceIf(ctx.fileExists('composer.json'), 'found composer.json', 4),
330
- evidenceIf(ctx.composerDepNames.size > 0, 'composer.json -> PHP dependency graph', 1)
331
- ])
332
- },
333
- {
334
- id: 'rust',
335
- detect: ctx => buildDetection('rust', [
336
- evidenceIf(ctx.fileExists('Cargo.toml'), 'found Cargo.toml', 4),
337
- evidenceIf(ctx.fileExists('src-tauri', 'Cargo.toml'), 'found src-tauri/Cargo.toml', 5)
338
- ])
339
- },
340
- {
341
- id: 'java',
342
- detect: ctx => buildDetection('java', [
343
- evidenceIf(ctx.fileExists('pom.xml'), 'found pom.xml', 4),
344
- evidenceIf(ctx.fileExists('build.gradle') || ctx.fileExists('build.gradle.kts'), 'found Gradle build file', 3)
345
- ])
346
- },
347
- {
348
- id: 'ruby',
349
- detect: ctx => buildDetection('ruby', [
350
- evidenceIf(ctx.fileExists('Gemfile'), 'found Gemfile', 4),
351
- evidenceIf(ctx.fileExists('config.ru'), 'found config.ru', 1)
352
- ])
353
- },
354
- {
355
- id: 'csharp',
356
- detect: ctx => buildDetection('csharp', [
357
- evidenceIf(ctx.rootEntries.some(name => name.endsWith('.sln') || name.endsWith('.csproj')), 'found .sln/.csproj', 4),
358
- evidenceIf(ctx.fileExists('Program.cs'), 'found Program.cs', 1)
359
- ])
360
- }
361
- ];
362
-
363
- const FRAMEWORK_DETECTORS = [
364
- {
365
- id: 'next',
366
- kind: 'frontend',
367
- projectType: 'web-app',
368
- detect: ctx => buildDetection('next', [
369
- evidenceIf(hasDependency(ctx.depNames, 'next'), 'package.json -> next', 4),
370
- evidenceIf(ctx.fileExists('next.config.js') || ctx.fileExists('next.config.mjs') || ctx.fileExists('next.config.ts'), 'found next.config.*', 3),
371
- evidenceIf(ctx.fileExists('app') || ctx.fileExists('pages'), 'found app/ or pages/', 1)
372
- ])
373
- },
374
- {
375
- id: 'react',
376
- kind: 'frontend',
377
- projectType: 'web-app',
378
- detect: ctx => buildDetection('react', [
379
- evidenceIf(hasDependency(ctx.depNames, 'react'), 'package.json -> react', 4),
380
- evidenceIf(hasDependency(ctx.depNames, 'react-dom'), 'package.json -> react-dom', 2)
381
- ])
382
- },
383
- {
384
- id: 'vue',
385
- kind: 'frontend',
386
- projectType: 'web-app',
387
- detect: ctx => buildDetection('vue', [
388
- evidenceIf(hasDependency(ctx.depNames, 'vue'), 'package.json -> vue', 4)
389
- ])
390
- },
391
- {
392
- id: 'nuxt',
393
- kind: 'frontend',
394
- projectType: 'web-app',
395
- detect: ctx => buildDetection('nuxt', [
396
- evidenceIf(hasDependency(ctx.depNames, 'nuxt'), 'package.json -> nuxt', 4),
397
- evidenceIf(ctx.fileExists('nuxt.config.ts') || ctx.fileExists('nuxt.config.js'), 'found nuxt.config.*', 3)
398
- ])
399
- },
400
- {
401
- id: 'svelte',
402
- kind: 'frontend',
403
- projectType: 'web-app',
404
- detect: ctx => buildDetection('svelte', [
405
- evidenceIf(hasDependency(ctx.depNames, 'svelte', '@sveltejs/kit'), 'package.json -> svelte', 4)
406
- ])
407
- },
408
- {
409
- id: 'angular',
410
- kind: 'frontend',
411
- projectType: 'web-app',
412
- detect: ctx => buildDetection('angular', [
413
- evidenceIf(hasDependency(ctx.depNames, '@angular/core'), 'package.json -> @angular/core', 4),
414
- evidenceIf(ctx.fileExists('angular.json'), 'found angular.json', 3)
415
- ])
416
- },
417
- {
418
- id: 'express',
419
- kind: 'backend',
420
- language: 'javascript',
421
- projectType: 'api',
422
- detect: ctx => buildDetection('express', [
423
- evidenceIf(hasDependency(ctx.depNames, 'express'), 'package.json -> express', 4)
424
- ])
425
- },
426
- {
427
- id: 'nestjs',
428
- kind: 'backend',
429
- language: 'javascript',
430
- projectType: 'api',
431
- detect: ctx => buildDetection('nestjs', [
432
- evidenceIf(hasDependency(ctx.depNames, '@nestjs/core'), 'package.json -> @nestjs/core', 4)
433
- ])
434
- },
435
- {
436
- id: 'fastapi',
437
- kind: 'backend',
438
- language: 'python',
439
- projectType: 'api',
440
- detect: ctx => buildDetection('fastapi', [
441
- evidenceIf(textMatches(ctx.requirementsText, /\bfastapi\b/i) || textMatches(ctx.pyprojectText, /\bfastapi\b/i), 'Python deps -> fastapi', 4)
442
- ])
443
- },
444
- {
445
- id: 'django',
446
- kind: 'backend',
447
- language: 'python',
448
- projectType: 'web-app',
449
- detect: ctx => buildDetection('django', [
450
- evidenceIf(textMatches(ctx.requirementsText, /\bdjango\b/i) || textMatches(ctx.pyprojectText, /\bdjango\b/i), 'Python deps -> django', 4),
451
- evidenceIf(ctx.fileExists('manage.py'), 'found manage.py', 2),
452
- evidenceIf(ctx.fileExists('templates'), 'found templates/', 1)
453
- ])
454
- },
455
- {
456
- id: 'flask',
457
- kind: 'backend',
458
- language: 'python',
459
- projectType: 'api',
460
- detect: ctx => buildDetection('flask', [
461
- evidenceIf(textMatches(ctx.requirementsText, /\bflask\b/i) || textMatches(ctx.pyprojectText, /\bflask\b/i), 'Python deps -> flask', 4)
462
- ])
463
- },
464
- {
465
- id: 'laravel',
466
- kind: 'backend',
467
- language: 'php',
468
- projectType: 'web-app',
469
- detect: ctx => buildDetection('laravel', [
470
- evidenceIf(hasComposerDependency(ctx.composerDepNames, 'laravel/framework'), 'composer.json -> laravel/framework', 4),
471
- evidenceIf(ctx.fileExists('artisan'), 'found artisan', 3),
472
- evidenceIf(ctx.fileExists('resources', 'views'), 'found resources/views', 1)
473
- ])
474
- },
475
- {
476
- id: 'cakephp',
477
- kind: 'backend',
478
- language: 'php',
479
- projectType: 'web-app',
480
- detect: ctx => buildDetection('cakephp', [
481
- evidenceIf(hasComposerDependency(ctx.composerDepNames, 'cakephp/cakephp'), 'composer.json -> cakephp/cakephp', 4),
482
- evidenceIf(ctx.fileExists('bin', 'cake'), 'found bin/cake', 3),
483
- evidenceIf(ctx.fileExists('config', 'app.php'), 'found config/app.php', 2),
484
- evidenceIf(ctx.fileExists('app', 'Config', 'core.php'), 'found app/Config/core.php (CakePHP legacy)', 3),
485
- evidenceIf(ctx.fileExists('app', 'Controller', 'AppController.php'), 'found app/Controller/AppController.php', 2),
486
- evidenceIf(ctx.fileExists('app', 'Model', 'AppModel.php'), 'found app/Model/AppModel.php', 2),
487
- evidenceIf(ctx.fileExists('lib', 'Cake'), 'found lib/Cake', 3),
488
- evidenceIf(ctx.fileExists('cake', 'console', 'cake'), 'found cake/console/cake', 3),
489
- evidenceIf(ctx.fileExists('app', 'View'), 'found app/View', 2),
490
- evidenceIf(textMatches(ctx.viewFilesText, /\.ctp$/im), 'found .ctp view templates', 2)
491
- ])
492
- },
493
- {
494
- id: 'symfony',
495
- kind: 'backend',
496
- language: 'php',
497
- projectType: 'web-app',
498
- detect: ctx => buildDetection('symfony', [
499
- evidenceIf(hasComposerDependency(ctx.composerDepNames, 'symfony/framework-bundle'), 'composer.json -> symfony/framework-bundle', 4),
500
- evidenceIf(ctx.fileExists('bin', 'console'), 'found bin/console', 2),
501
- evidenceIf(ctx.fileExists('config', 'bundles.php'), 'found config/bundles.php', 1),
502
- evidenceIf(ctx.fileExists('templates'), 'found templates/', 1)
503
- ])
504
- },
505
- {
506
- id: 'codeigniter',
507
- kind: 'backend',
508
- language: 'php',
509
- projectType: 'api',
510
- detect: ctx => buildDetection('codeigniter', [
511
- evidenceIf(hasComposerDependency(ctx.composerDepNames, 'codeigniter4/framework'), 'composer.json -> codeigniter4/framework', 4),
512
- evidenceIf(ctx.fileExists('spark'), 'found spark', 2),
513
- evidenceIf(ctx.fileExists('app', 'Config', 'App.php'), 'found app/Config/App.php', 1)
514
- ])
515
- },
516
- {
517
- id: 'gin',
518
- kind: 'backend',
519
- language: 'go',
520
- projectType: 'api',
521
- detect: ctx => buildDetection('gin', [
522
- evidenceIf(textMatches(ctx.goModText, /\bgithub\.com\/gin-gonic\/gin\b/i), 'go.mod -> github.com/gin-gonic/gin', 4)
523
- ])
524
- },
525
- {
526
- id: 'fiber',
527
- kind: 'backend',
528
- language: 'go',
529
- projectType: 'api',
530
- detect: ctx => buildDetection('fiber', [
531
- evidenceIf(textMatches(ctx.goModText, /\bgithub\.com\/gofiber\/fiber\b/i), 'go.mod -> github.com/gofiber/fiber', 4)
532
- ])
533
- },
534
- {
535
- id: 'echo',
536
- kind: 'backend',
537
- language: 'go',
538
- projectType: 'api',
539
- detect: ctx => buildDetection('echo', [
540
- evidenceIf(textMatches(ctx.goModText, /\bgithub\.com\/labstack\/echo\b/i), 'go.mod -> github.com/labstack/echo', 4)
541
- ])
542
- },
543
- {
544
- id: 'rails',
545
- kind: 'backend',
546
- language: 'ruby',
547
- projectType: 'web-app',
548
- detect: ctx => buildDetection('rails', [
549
- evidenceIf(textMatches(ctx.gemfileText, /^\s*gem\s+["']rails["']/im), 'Gemfile -> rails', 4),
550
- evidenceIf(ctx.fileExists('bin', 'rails'), 'found bin/rails', 2),
551
- evidenceIf(ctx.fileExists('app', 'views'), 'found app/views', 1)
552
- ])
553
- },
554
- {
555
- id: 'spring',
556
- kind: 'backend',
557
- language: 'java',
558
- projectType: 'api',
559
- detect: ctx => buildDetection('spring', [
560
- evidenceIf(textMatches(ctx.pomText, /spring-boot/i) || textMatches(ctx.gradleText, /spring-boot/i), 'Java build -> spring-boot', 4)
561
- ])
562
- },
563
- {
564
- id: 'aspnet',
565
- kind: 'backend',
566
- language: 'csharp',
567
- projectType: 'api',
568
- detect: ctx => buildDetection('aspnet', [
569
- evidenceIf(textMatches(ctx.csprojText, /Microsoft\.NET\.Sdk\.Web/i), '.csproj -> Microsoft.NET.Sdk.Web', 4),
570
- evidenceIf(textMatches(ctx.programCsText, /WebApplication\.CreateBuilder/i), 'Program.cs -> WebApplication.CreateBuilder', 2)
571
- ])
572
- },
573
- {
574
- id: 'tauri',
575
- kind: 'desktop',
576
- language: 'rust',
577
- projectType: 'desktop',
578
- detect: ctx => buildDetection('tauri', [
579
- evidenceIf(hasDependency(ctx.depNames, '@tauri-apps/api'), 'package.json -> @tauri-apps/api', 4),
580
- evidenceIf(ctx.fileExists('src-tauri', 'tauri.conf.json') || ctx.fileExists('src-tauri', 'tauri.conf.json5'), 'found src-tauri/tauri.conf.*', 4),
581
- evidenceIf(ctx.fileExists('src-tauri', 'Cargo.toml'), 'found src-tauri/Cargo.toml', 2)
582
- ])
583
- },
584
- {
585
- id: 'axum',
586
- kind: 'backend',
587
- language: 'rust',
588
- projectType: 'api',
589
- detect: ctx => buildDetection('axum', [
590
- evidenceIf(textMatches(ctx.cargoText, /\baxum\b/i), 'Cargo.toml -> axum', 4)
591
- ])
592
- },
593
- {
594
- id: 'actix',
595
- kind: 'backend',
596
- language: 'rust',
597
- projectType: 'api',
598
- detect: ctx => buildDetection('actix', [
599
- evidenceIf(textMatches(ctx.cargoText, /\bactix-web\b/i), 'Cargo.toml -> actix-web', 4)
600
- ])
601
- },
602
- {
603
- id: 'flutter',
604
- kind: 'mobile',
605
- projectType: 'mobile',
606
- detect: ctx => buildDetection('flutter', [
607
- evidenceIf(textMatches(ctx.pubspecText, /^\s*flutter\s*:/im), 'pubspec.yaml -> flutter', 4)
608
- ])
609
- },
610
- {
611
- id: 'react-native',
612
- kind: 'mobile',
613
- projectType: 'mobile',
614
- detect: ctx => buildDetection('react-native', [
615
- evidenceIf(hasDependency(ctx.depNames, 'react-native', 'expo'), 'package.json -> react-native/expo', 4)
616
- ])
617
- }
618
- ];
619
-
620
- function scanProject() {
621
- const appRoot = detectAppRoot(targetDir);
622
- const pkg = readJsonIfExistsAt(appRoot, 'package.json') || {};
623
- const composer = readJsonIfExistsAt(appRoot, 'composer.json') || {};
624
- const rootEntries = fs.existsSync(appRoot) ? fs.readdirSync(appRoot) : [];
625
- const rootCsproj = rootEntries.find(name => name.endsWith('.csproj')) || '';
626
- const deps = {
627
- ...(pkg.dependencies || {}),
628
- ...(pkg.devDependencies || {}),
629
- ...(pkg.peerDependencies || {})
630
- };
631
- const depNames = new Set(Object.keys(deps));
632
- const composerDeps = {
633
- ...(composer.require || {}),
634
- ...(composer['require-dev'] || {})
635
- };
636
- const composerDepNames = new Set(Object.keys(composerDeps));
637
- const ctx = {
638
- pkg,
639
- composer,
640
- depNames,
641
- composerDepNames,
642
- rootEntries,
643
- fileExists: (...parts) => fileExistsAt(appRoot, ...parts),
644
- requirementsText: readTextIfExistsAt(appRoot, 'requirements.txt'),
645
- pyprojectText: readTextIfExistsAt(appRoot, 'pyproject.toml'),
646
- goModText: readTextIfExistsAt(appRoot, 'go.mod'),
647
- pubspecText: readTextIfExistsAt(appRoot, 'pubspec.yaml'),
648
- gemfileText: readTextIfExistsAt(appRoot, 'Gemfile'),
649
- pomText: readTextIfExistsAt(appRoot, 'pom.xml'),
650
- gradleText: `${readTextIfExistsAt(appRoot, 'build.gradle')}\n${readTextIfExistsAt(appRoot, 'build.gradle.kts')}`,
651
- cargoText: `${readTextIfExistsAt(appRoot, 'Cargo.toml')}\n${readTextIfExistsAt(appRoot, 'src-tauri', 'Cargo.toml')}`,
652
- csprojText: rootCsproj ? readTextIfExistsAt(appRoot, rootCsproj) : '',
653
- programCsText: readTextIfExistsAt(appRoot, 'Program.cs'),
654
- viewFilesText: fs.existsSync(path.join(appRoot, 'app', 'View'))
655
- ? fs.readdirSync(path.join(appRoot, 'app', 'View'), { recursive: true }).join('\n')
656
- : ''
657
- };
658
-
659
- const languageDetections = LANGUAGE_DETECTORS
660
- .map(detector => detector.detect(ctx))
661
- .filter(result => result.score >= 2)
662
- .sort((a, b) => b.score - a.score);
663
-
664
- const frameworkDetails = FRAMEWORK_DETECTORS
665
- .map(detector => {
666
- const result = detector.detect(ctx);
667
- if (result.score < 3) return null;
668
- return {
669
- ...result,
670
- kind: detector.kind,
671
- language: detector.language,
672
- projectType: detector.projectType,
673
- confidence: confidenceLabel(result.score)
674
- };
675
- })
676
- .filter(Boolean)
677
- .sort((a, b) => b.score - a.score);
678
-
679
- const frameworkLanguageScores = frameworkDetails.reduce((acc, item) => {
680
- if (!item.language) return acc;
681
- acc[item.language] = (acc[item.language] || 0) + item.score;
682
- return acc;
683
- }, {});
684
- const primaryLanguage = Object.keys(frameworkLanguageScores).sort((a, b) => frameworkLanguageScores[b] - frameworkLanguageScores[a])[0]
685
- || languageDetections[0]?.id
686
- || 'typescript';
687
- const frameworks = uniq(frameworkDetails.map(item => item.id)).filter(name => FRAMEWORK_CHOICES.includes(name));
688
- const infrastructure = [];
689
- let projectType = '';
690
- let database = 'none';
691
- let testFramework = 'auto-detect';
692
- let cloud = 'none';
693
- let ciCd = 'none';
694
-
695
- if (frameworkDetails.some(item => item.projectType === 'desktop')) projectType = 'desktop';
696
- else if (frameworkDetails.some(item => item.projectType === 'mobile')) projectType = 'mobile';
697
- else if (frameworkDetails.some(item => item.kind === 'frontend') && frameworkDetails.some(item => item.kind === 'backend')) projectType = 'fullstack';
698
- else if (frameworkDetails.some(item => item.projectType === 'web-app')) projectType = 'web-app';
699
- else if (frameworkDetails.some(item => item.kind === 'backend')) projectType = 'api';
700
- else if (frameworkDetails.some(item => item.kind === 'frontend')) projectType = 'web-app';
701
- else if (fileExistsAt(appRoot, 'package.json') && pkg.bin) projectType = 'cli';
702
- else if (['php', 'python', 'go', 'java', 'ruby', 'csharp', 'rust'].includes(primaryLanguage)) projectType = 'api';
703
-
704
- if (hasDependency(depNames, 'pg', 'postgres', '@prisma/client') || /postgres/i.test(JSON.stringify(pkg))) database = 'postgres';
705
- else if (hasDependency(depNames, 'mysql', 'mysql2')) database = 'mysql';
706
- else if (hasDependency(depNames, 'sqlite3', 'better-sqlite3', 'rusqlite')) database = 'sqlite';
707
- else if (hasDependency(depNames, 'mongodb', 'mongoose')) database = 'mongodb';
708
- else if (hasDependency(depNames, 'redis', 'ioredis')) database = 'redis';
709
- else if (hasDependency(depNames, '@aws-sdk/client-dynamodb')) database = 'dynamodb';
710
-
711
- if (hasDependency(depNames, 'vitest')) testFramework = 'vitest';
712
- else if (hasDependency(depNames, 'jest')) testFramework = 'jest';
713
- else if (hasDependency(depNames, 'pytest') || textMatches(ctx.requirementsText, /\bpytest\b/i) || textMatches(ctx.pyprojectText, /\bpytest\b/i)) testFramework = 'pytest';
714
- else if (hasComposerDependency(composerDepNames, 'phpunit/phpunit')) testFramework = 'phpunit';
715
- else if (hasComposerDependency(composerDepNames, 'pestphp/pest')) testFramework = 'pest';
716
- else if (fileExistsAt(appRoot, 'go.mod')) testFramework = 'go test';
717
- else if (textMatches(ctx.gemfileText, /^\s*gem\s+["']rspec["']/im)) testFramework = 'rspec';
718
-
719
- if (repoHasAny(targetDir, [['Dockerfile'], ['docker-compose.yml'], ['docker-compose.yaml']])) {
720
- infrastructure.push('docker');
721
- }
722
-
723
- if (repoHasAny(targetDir, [['vercel.json']])) {
724
- cloud = 'vercel';
725
- infrastructure.push('vercel');
726
- } else if (repoHasAny(targetDir, [['fly.toml']])) {
727
- cloud = 'fly';
728
- infrastructure.push('fly');
729
- } else if (repoHasAny(targetDir, [['railway.json'], ['railway.toml']])) {
730
- cloud = 'railway';
731
- infrastructure.push('railway');
732
- }
733
-
734
- if (repoHasAny(targetDir, [['.github', 'workflows'], ['.gitlab-ci.yml'], ['.circleci', 'config.yml'], ['Jenkinsfile']], 2)) {
735
- if (repoHasAny(targetDir, [['.github', 'workflows']], 2)) ciCd = 'github-actions';
736
- else if (repoHasAny(targetDir, [['.gitlab-ci.yml']], 2)) ciCd = 'gitlab-ci';
737
- else if (repoHasAny(targetDir, [['.circleci', 'config.yml']], 2)) ciCd = 'circleci';
738
- else if (repoHasAny(targetDir, [['Jenkinsfile']], 2)) ciCd = 'jenkins';
739
- }
740
-
741
- const scanEvidence = [
742
- ...(languageDetections[0] ? [`language:${languageDetections[0].id} => ${languageDetections[0].evidence.join('; ')}`] : []),
743
- ...frameworkDetails.map(item => `${item.id} [${item.confidence}] => ${item.evidence.join('; ')}`)
744
- ];
745
-
746
- return {
747
- project_name: path.basename(targetDir),
748
- app_root: path.relative(targetDir, appRoot) || '.',
749
- description: pkg.description || composer.description || 'A software project',
750
- project_type: PROJECT_TYPE_CHOICES.includes(projectType) ? projectType : 'web-app',
751
- primary_language: LANGUAGE_CHOICES.includes(primaryLanguage) ? primaryLanguage : 'typescript',
752
- frameworks,
753
- framework_details: frameworkDetails,
754
- scan_evidence: scanEvidence,
755
- database: DATABASE_CHOICES.includes(database) ? database : 'none',
756
- package_manager: PACKAGE_MANAGER_CHOICES.includes(detectPackageManager(appRoot)) ? detectPackageManager(appRoot) : 'auto-detect',
757
- uses_docker: infrastructure.includes('docker'),
758
- cloud,
759
- infrastructure: uniq(infrastructure),
760
- ci_cd: ciCd,
761
- test_framework: TEST_FRAMEWORK_CHOICES.includes(testFramework) ? testFramework : 'auto-detect',
762
- code_style: 'project-standard',
763
- architecture_notes: '<!-- Add notes -->',
764
- current_phase: 'Initial setup',
765
- next_phase: '-',
766
- known_issues: '<!-- None yet -->'
767
- };
768
- }
769
- // --- File Generators ---
770
-
771
- function generateSharedAgentsMd(data) {
772
- return `# AGENTS.md
773
-
774
- Shared operating instructions for Codex, Antigravity, Claude Code, and Cursor.
775
-
776
- ## Startup
777
-
778
- 1. Read this file first.
779
- 2. Read \`.agent/docs/project.md\` for detected stack, commands, and routing.
780
- 3. Read \`.agent/docs/workflow.md\` for the task flow before editing.
781
- 4. Read \`.agent/docs/architecture.md\` and \`.agent/docs/conventions.md\` before planning code changes.
782
- 5. Read \`.agent/docs/tooling.md\` when the task needs MCP, RTK, or browser verification.
783
-
784
- If any \`.agent/docs/*.md\` file still contains \`TODO: refine\`, update the docs by scanning the project before making product code changes.
785
-
786
- ## Workflow
787
-
788
- Matt Pocock's promoted skills (installed under \`.agents/skills/\` by \`thachvd-kit setup\`) are the workflow layer. Use the matching skill when a specialized workflow is useful:
789
-
790
- - Clear, localized change: inspect -> edit -> focused verification. Do not force a heavyweight skill.
791
- - Ambiguous feature or design: \`/grill-with-docs\`, then optionally \`/to-spec\` for a durable contract.
792
- - Normal feature: \`/grill-with-docs\` -> \`/to-spec\` -> \`/implement\`.
793
- - Large feature needing decomposition: add \`/to-tickets\` before \`/implement\`.
794
- - Huge, multi-session uncertainty: \`/wayfinder\`.
795
- - Bug or failing behavior: \`/diagnosing-bugs\`.
796
- - Test-driven implementation: \`/tdd\` (skip the ceremony for trivial config/text changes).
797
- - Architecture survey: \`/improve-codebase-architecture\`; use \`/codebase-design\` as the design vocabulary.
798
- - Pre-completion review: \`/code-review\` once when code risk warrants it.
799
-
800
- If unsure which skill fits, use \`/ask-matt\`.
801
-
802
- Questions and research do not edit product code. A fast path is allowed only when the change is localized, mechanically obvious, low-risk, and has focused verification; file count alone is not a gate. State the scope and verification before editing. If the task becomes ambiguous or its behavior, contract, or risk surface grows, use the matching planning skill above instead.
803
-
804
- See \`.agent/docs/workflow.md\` for the full route matrix. This kit does not implement a second workflow engine; it only installs and checks the promoted skill set (\`thachvd-kit skills install|check|update\`).
805
-
806
- ${QUALITY_FLOOR_SECTION}
807
-
808
- ${AGENT_RULES_SECTION}
809
-
810
- ## Shared Knowledge
811
-
812
- - Project context: \`.agent/docs/\`
813
- - Tooling setup: \`.agent/docs/tooling.md\`
814
- - Matt Pocock skills: promoted set installed under \`.agents/skills/\` via \`thachvd-kit skills install\`
815
- - RTK: recommended local CLI for compact command output
816
- - Antigravity and Cursor entry rules: \`GEMINI.md\` and \`.cursorrules\`
817
- `;
818
- }
819
-
820
- function generateIndexProjectPrompt() {
821
- return `# Project Indexing Prompt
822
-
823
- Read CLAUDE.md (if using Claude Code), AGENTS.md, and all files under .agent/docs/.
824
- Use the project's Matt Pocock skills for workflow (see AGENTS.md; run /ask-matt if unsure which fits) and use codebase-memory MCP for structural code discovery when available.
825
- Ask me what this project does and any important conventions I want preserved.
826
- Then scan this repository and refine the generated context docs with factual rules.
827
- Do not implement product code during onboarding.
828
- `;
829
- }
830
-
831
- function printProjectPrompt() {
832
- const promptPath = path.join(targetDir, '.agent', 'docs', 'index-project-prompt.md');
833
- if (!fs.existsSync(promptPath)) {
834
- console.error(pc.yellow('No saved project prompt found. Run thachvd-kit init first.'));
835
- process.exitCode = 1;
836
- return;
837
- }
838
- console.log(fs.readFileSync(promptPath, 'utf8'));
839
- }
840
-
841
- function generateSharedClaudeMd() {
842
- return `@AGENTS.md
843
-
844
- ## Claude Code
845
-
846
- This repository uses AGENTS.md as the shared cross-agent entry file. Follow the imported instructions and keep Claude-specific notes here only when they cannot apply to Codex or Antigravity.
847
- `;
848
- }
849
-
850
- function generateSharedGeminiMd() {
851
- return `# GEMINI.md
852
-
853
- Antigravity entry file for this project.
854
-
855
- Read AGENTS.md first, then follow the shared docs under .agent/docs/.
856
- `;
857
- }
858
-
859
- function generateSharedCursorrules(data) {
860
- return `# Cursor Rules
861
-
862
- This repository uses AGENTS.md as the shared cross-agent entry file. Follow the instructions in AGENTS.md and keep Cursor-specific notes here only when they cannot apply to Codex, Antigravity, or Claude Code.
863
-
864
- Read AGENTS.md first, then follow the shared docs under .agent/docs/.
865
-
866
- ---
867
-
868
- ${generateSharedAgentsMd(data).trimEnd()}
869
- `;
870
- }
871
-
872
- function resolveCommands(data) {
873
- const frameworks = data.frameworks || [];
874
- const primaryLang = data.primary_language || 'javascript';
875
- const pkgManager = data.package_manager || 'npm';
876
- const testFramework = data.test_framework || 'auto-detect';
877
-
878
- const isPython = primaryLang === 'python';
879
- const isGo = primaryLang === 'go';
880
- const isRust = primaryLang === 'rust';
881
- const isPhp = primaryLang === 'php';
882
-
883
- let install = 'npm install';
884
- let dev = 'npm run dev';
885
- let build = 'npm run build';
886
- let test = 'npm test';
887
- let lint = 'npm run lint';
888
-
889
- if (pkgManager === 'pnpm') { install = 'pnpm install'; dev = 'pnpm dev'; build = 'pnpm build'; test = 'pnpm test'; lint = 'pnpm lint'; }
890
- else if (pkgManager === 'yarn') { install = 'yarn'; dev = 'yarn dev'; build = 'yarn build'; test = 'yarn test'; lint = 'yarn lint'; }
891
- else if (pkgManager === 'bun') { install = 'bun install'; dev = 'bun dev'; build = 'bun run build'; test = 'bun test'; lint = 'bun run lint'; }
892
- else if (isPython) { install = 'pip install -r requirements.txt'; dev = 'python manage.py runserver'; build = '# no build step'; test = 'pytest'; lint = 'ruff check .'; }
893
- else if (isGo) { install = 'go mod download'; dev = 'go run .'; build = 'go build ./...'; test = 'go test ./...'; lint = 'golangci-lint run'; }
894
- else if (isRust) { install = 'cargo build'; dev = 'cargo run'; build = 'cargo build --release'; test = 'cargo test'; lint = 'cargo clippy'; }
895
- else if (isPhp) {
896
- install = 'composer install';
897
- build = '# no build step';
898
- test = testFramework === 'pest' ? './vendor/bin/pest' : './vendor/bin/phpunit';
899
- const isCakePHP = frameworks.some(f => f.toLowerCase().includes('cakephp'));
900
- const isSymfony = frameworks.some(f => f.toLowerCase().includes('symfony'));
901
- const isCodeIgniter = frameworks.some(f => f.toLowerCase().includes('codeigniter'));
902
- if (isCakePHP) {
903
- dev = 'bin/cake server';
904
- lint = './vendor/bin/phpcs';
905
- } else if (isSymfony) {
906
- dev = 'bin/console server:run';
907
- lint = './vendor/bin/phpcs';
908
- } else if (isCodeIgniter) {
909
- dev = 'php spark serve';
910
- lint = './vendor/bin/phpcs';
911
- } else {
912
- dev = 'php artisan serve';
913
- lint = 'php artisan pint';
914
- }
915
- }
916
-
917
- if (testFramework === 'vitest') test = `${pkgManager === 'pnpm' ? 'pnpm' : pkgManager === 'yarn' ? 'yarn' : pkgManager === 'bun' ? 'bun run' : 'npx'} vitest`;
918
- else if (testFramework === 'jest') test = `${pkgManager === 'pnpm' ? 'pnpm' : pkgManager === 'yarn' ? 'yarn' : 'npx'} jest`;
919
-
920
- return { install, dev, build, test, lint };
921
- }
922
-
923
- function generateProjectDoc(data) {
924
- const commands = resolveCommands(data);
925
- const evidence = (data.scan_evidence || []).length > 0
926
- ? (data.scan_evidence || []).map(line => `- ${line}`).join('\n')
927
- : '- TODO: refine scan evidence by inspecting the project.';
928
-
929
- return `# Project Rules
930
-
931
- Generated by thachvd-kit on ${new Date().toISOString().split('T')[0]}.
932
-
933
- ## Summary
934
-
935
- - Name: ${data.project_name}
936
- - Description: ${data.description}
937
- - Type: ${data.project_type}
938
- - App root: ${data.app_root || '.'}
939
-
940
- ## Stack
941
-
942
- - Language: ${data.primary_language}
943
- - Frameworks: ${(data.frameworks || []).join(', ') || 'none'}
944
- - Database: ${data.database || 'none'}
945
- - Infrastructure: ${(data.infrastructure || []).join(', ') || 'none'}
946
- - Package manager: ${data.package_manager || 'auto-detect'}
947
- - Test framework: ${data.test_framework || 'auto-detect'}
948
-
949
- ## Commands
950
-
951
- - Install: \`${commands.install}\`
952
- - Dev: \`${commands.dev}\`
953
- - Build: \`${commands.build}\`
954
- - Test: \`${commands.test}\`
955
- - Lint: \`${commands.lint}\`
956
-
957
- ## Available Integrations
958
-
959
- Use these integrations when available. Run thachvd-kit doctor to check the current machine.
960
-
961
- | MCP | When to Use |
962
- |-----|-------------|
963
- | \`codebase-memory-mcp\` | Explore symbols, trace call chains, architecture, and impact |
964
- | \`context7\` | Look up library/framework docs, API signatures, migration guides |
965
- | \`playwright\` | Verify UI behavior, take screenshots, test forms and navigation |
966
- | \`rtk\` | Recommended proxy for noisy git, tests, builds, and package commands |
967
-
968
- > Check \`.agent/docs/tooling.md\` for setup details and additional MCP servers.
969
-
970
- ## Scan Evidence
971
-
972
- ${evidence}
973
- `;
974
- }
975
-
976
- function generateArchitectureDoc(data) {
977
- const appRootLine = data.app_root && data.app_root !== '.'
978
- ? `- App root is currently detected as \`${data.app_root}\`.`
979
- : '- App root is the repository root unless refined below.';
980
- const evidence = (data.scan_evidence || []).length > 0
981
- ? (data.scan_evidence || []).map(line => `- ${line}`).join('\n')
982
- : '- TODO: refine architecture by scanning source directories.';
983
-
984
- return `# Architecture Notes
985
-
986
- ## Current Map
987
-
988
- ${appRootLine}
989
- - TODO: refine major directories and responsibilities.
990
- - TODO: refine main entry points, routing, state boundaries, and integration boundaries.
991
-
992
- ## Detected Evidence
993
-
994
- ${evidence}
995
-
996
- ## AI Maintenance Rule
997
-
998
- Before non-trivial implementation work, inspect this file. If it is stale or still contains TODO items relevant to the task, update it from the code before editing product code.
999
- `;
1000
- }
1001
-
1002
- function generateConventionsDoc() {
1003
- return `# Coding Conventions
1004
-
1005
- ${CONVENTIONS_STANDARDS_SECTION}
1006
-
1007
- ${QUALITY_FLOOR_SECTION}
1008
-
1009
- ## Verification
1010
-
1011
- - Run the focused test or lint command that matches the touched area.
1012
- - If no automated check exists, document the manual verification performed.
1013
- - For UI/web changes, prioritize using Playwright MCP to automatically verify client-side behavior and capture screenshots.
1014
- - Do not claim completion without verification evidence.
1015
- `;
1016
- }
1017
-
1018
- function generateWorkflowDoc() {
1019
- return `# Agent Workflow
1020
-
1021
- This project uses Matt Pocock's promoted skills (installed under .agents/skills/ by \`thachvd-kit setup\`) as the workflow layer. thachvd-kit only provides project context, the skill manifest, and integration setup; it is not a second workflow engine.
1022
-
1023
- ## Before Every Task
1024
-
1025
- 1. Read AGENTS.md and the relevant files under .agent/docs/.
1026
- 2. Use the promoted skill that matches the request, or run /ask-matt if unsure which one fits.
1027
- 3. Use codebase-memory MCP for structural discovery when available.
1028
- 4. Make changes only after the required planning gate for that situation.
1029
- 5. Verify the change and request review before completion.
1030
-
1031
- ## Route Matrix
1032
-
1033
- | Situation | Skill | Gate |
1034
- |---|---|---|
1035
- | Question or research only | direct answer or research | no product-code edits |
1036
- | Clear, localized change | fast path (no skill) | inspect -> edit -> focused verify |
1037
- | Ambiguous feature or design | /grill-with-docs, then optionally /to-spec | durable contract before implementation when useful |
1038
- | Normal feature | /grill-with-docs -> /to-spec -> /implement | spec agreed before implementation |
1039
- | Large feature needing decomposition | /grill-with-docs -> /to-spec -> /to-tickets -> /implement | tickets agreed before implementation |
1040
- | Huge, multi-session uncertainty | /wayfinder | shared decision map before implementation |
1041
- | Bug or failing behavior | /diagnosing-bugs | reproduce -> root cause -> regression protection -> fix -> verify |
1042
- | Test-driven implementation | /tdd | red -> green -> refactor per slice |
1043
- | Architecture survey | /improve-codebase-architecture (+ /codebase-design vocabulary) | findings reviewed before large refactors |
1044
- | Pre-merge | /code-review | blocking findings resolved |
1045
-
1046
- Not sure which row applies? Run /ask-matt instead of guessing.
1047
-
1048
- ## Standard Feature Flow
1049
-
1050
- /grill-with-docs -> /to-spec -> (/to-tickets for large work) -> /implement (with /tdd where it helps) -> /code-review.
1051
-
1052
- ${FAST_PATH_SECTION}
1053
-
1054
- ## Documentation Maintenance
1055
-
1056
- - Update architecture.md when structure or boundaries change.
1057
- - Update conventions.md when repeated project patterns become clear.
1058
- - Update project.md when stack, scripts, or test tooling changes.
1059
- - Keep AGENTS.md concise and put project-specific detail under .agent/docs/.
1060
- `;
1061
- }
1062
-
1063
- function generateGettingStartedDoc() {
1064
- return `# Getting Started
1065
-
1066
- thachvd-kit creates project context. Matt Pocock's promoted skills own the development workflow.
1067
-
1068
- ## Setup
1069
-
1070
- 1. Run \`thachvd-kit init\` in the repository.
1071
- 2. Open or print \`.agent/docs/index-project-prompt.md\` with \`thachvd-kit prompt\`.
1072
- 3. Run \`thachvd-kit setup\` (installs the promoted skill set by default; add \`--no-install-skills\` to skip) and RTK.
1073
- 4. Inside the AI client, run \`/setup-matt-pocock-skills\` once to configure the issue tracker, triage labels, and doc layout.
1074
- 5. Run \`thachvd-kit doctor\` and restart the AI client.
1075
-
1076
- ## Standard Flow
1077
-
1078
- 1. \`/grill-with-docs\` for unclear requirements or design.
1079
- 2. \`/to-spec\` when a durable implementation contract is useful.
1080
- 3. \`/to-tickets\` for large work that benefits from decomposition.
1081
- 4. \`/implement\`, with \`/tdd\` where red-green-refactor helps.
1082
- 5. \`/code-review\` once before completion.
1083
-
1084
- Not sure which skill fits? Run \`/ask-matt\`.
1085
-
1086
- ## Bug Flow
1087
-
1088
- Use \`/diagnosing-bugs\`: reproduce the symptom, inspect the path with codebase-memory MCP, test hypotheses, add regression protection, fix the root cause, and verify.
1089
-
1090
- ${GETTING_STARTED_FAST_PATH_SECTION}
1091
- `;
1092
- }
1093
-
1094
- function resolveToolingSetup(data) {
1095
- const primaryLang = data.primary_language || 'javascript';
1096
- const packageManager = data.package_manager || 'npm';
1097
- const isPython = primaryLang === 'python';
1098
- const runner = packageManager === 'pnpm'
1099
- ? 'pnpm dlx'
1100
- : packageManager === 'yarn'
1101
- ? 'yarn dlx'
1102
- : packageManager === 'bun'
1103
- ? 'bunx'
1104
- : 'npx';
1105
- const playwrightCommand = isPython
1106
- ? 'pip install playwright && playwright install chromium'
1107
- : `${runner} playwright install`;
1108
-
1109
- return { runner, playwrightCommand };
1110
- }
1111
-
1112
- function generateToolingDoc(data) {
1113
- const { runner, playwrightCommand } = resolveToolingSetup(data);
1114
-
1115
- return `# Optional Tooling
1116
-
1117
- Run \`thachvd-kit doctor\` to check the current machine.
1118
-
1119
- ## Matt Pocock Skills
1120
-
1121
- Matt Pocock's promoted engineering and productivity skills are the workflow layer for this project, installed project-locally under \`.agents/skills/\` (never globally):
1122
-
1123
- - Install the promoted set: \`thachvd-kit skills install\` (\`--dry-run\` to preview the command without running it)
1124
- - Check what is installed: \`thachvd-kit skills check\` (read-only)
1125
- - Update the promoted set: \`thachvd-kit skills update\` (re-adds each skill individually for safety, so it is slower than install — one network clone per skill)
1126
- - The full manifest lives in \`bin/matt-skills.js\` (\`PROMOTED_SKILLS\`); it mirrors upstream's \`skills/engineering/\` + \`skills/productivity/\` catalog and never includes \`in-progress\`, \`misc\`, or \`deprecated\` skills.
1127
- - After the first install, run \`/setup-matt-pocock-skills\` once inside the AI client to configure the issue tracker, triage labels, and generated docs location. thachvd-kit does not simulate that skill.
1128
- - Unsure which skill fits a task? Run \`/ask-matt\`.
1129
-
1130
- ${TOOL_ROUTING_SECTION}
1131
-
1132
- ## Codebase Memory MCP
1133
-
1134
- Use \`codebase-memory-mcp\` for symbol search, call paths, architecture, and impact analysis.
1135
-
1136
- - Install: \`npm install -g codebase-memory-mcp\`
1137
- - Index a repository: \`codebase-memory-mcp cli index_repository --repo-path .\`
1138
- - Keep local index state out of git unless the team explicitly chooses a shared snapshot.
1139
-
1140
- ## RTK
1141
-
1142
- RTK is the recommended command-output layer, not a workflow engine. Prefer it by default for verbose shell commands.
1143
-
1144
- - Install: \`cargo install --git https://github.com/rtk-ai/rtk --branch master rtk\`
1145
- - Examples: \`rtk git status\`, \`rtk npm test\`, \`rtk git log\`.
1146
- - If it is unavailable, run the underlying command directly.
1147
-
1148
- ## Playwright
1149
-
1150
- - Check: \`${runner} playwright --version\`
1151
- - Browser install: \`${playwrightCommand}\`
1152
- - Use the project's existing Playwright scripts when available.
1153
- `;
1154
- }
1155
-
1156
- async function writeGeneratedFile(relativePath, content, assumeYes) {
1157
- const outputPath = path.join(targetDir, relativePath);
1158
- const exists = fs.existsSync(outputPath);
1159
- if (exists && !assumeYes) {
1160
- const response = await prompts({
1161
- type: 'confirm',
1162
- name: 'overwrite',
1163
- message: `File ${pc.bold(relativePath)} already exists. Do you want to overwrite it?`,
1164
- initial: false
1165
- });
1166
- if (response.overwrite === undefined) {
1167
- console.log(pc.yellow('\nCancelled.'));
1168
- process.exit(130);
1169
- }
1170
- if (!response.overwrite) return false;
1171
- }
1172
-
1173
- fs.mkdirSync(path.dirname(outputPath), { recursive: true });
1174
- fs.writeFileSync(outputPath, content, 'utf8');
1175
- return true;
1176
- }
1177
-
1178
- function homePath(...parts) {
1179
- const home = process.env.USERPROFILE || process.env.HOME;
1180
- return home ? path.join(home, ...parts) : null;
1181
- }
1182
-
1183
- function printMattSkillsSetupReminder() {
1184
- console.log(` Run ${pc.bold('/setup-matt-pocock-skills')} once inside your AI client to configure the issue tracker, triage labels, and generated docs location.`);
1185
- }
1186
-
1187
- function runDoctor() {
1188
- console.log(pc.bold(pc.cyan('thachvd-kit doctor')));
1189
- const requiredFiles = ['AGENTS.md', 'CLAUDE.md', 'GEMINI.md', '.cursorrules',
1190
- path.join('.agent', 'docs', 'project.md'), path.join('.agent', 'docs', 'workflow.md')];
1191
- for (const relativePath of requiredFiles) {
1192
- const ok = fs.existsSync(path.join(targetDir, relativePath));
1193
- console.log(` ${ok ? pc.green('OK') : pc.yellow('WARN')} ${relativePath}`);
1194
- }
1195
-
1196
- const tools = [
1197
- [CODEBASE_MEMORY_BINARY, `npm install -g ${CODEBASE_MEMORY_PACKAGE}`],
1198
- ['rtk', `recommended: ${RTK_INSTALL_COMMAND}`]
1199
- ];
1200
- console.log(`\n${pc.bold('Local integrations (RTK recommended)')}`);
1201
- for (const [binary, hint] of tools) {
1202
- const ok = commandExists(binary);
1203
- console.log(` ${ok ? pc.green('OK') : pc.yellow('WARN')} ${binary}${ok ? '' : ` - ${hint}`}`);
1204
- }
1205
-
1206
- console.log(`\n${pc.bold('Matt Pocock skills')}`);
1207
- const skillStatus = mattSkills.checkSkills(targetDir);
1208
- if (skillStatus.missing.length === 0) {
1209
- console.log(` ${pc.green('OK')} promoted skill set installed (${skillStatus.total}/${skillStatus.total})`);
1210
- } else {
1211
- console.log(` ${pc.yellow('WARN')} ${skillStatus.missing.length} promoted skill(s) missing`);
1212
- console.log(` Run: ${pc.bold('thachvd-kit skills install')}`);
1213
- }
1214
- const setupSkillOk = mattSkills.isSkillInstalled(targetDir, mattSkills.SETUP_SKILL_NAME);
1215
- console.log(` ${setupSkillOk ? pc.green('OK') : pc.yellow('WARN')} ${mattSkills.SETUP_SKILL_NAME} available`);
1216
- const contextMdExists = fs.existsSync(path.join(targetDir, 'CONTEXT.md'));
1217
- const docsAdrExists = fs.existsSync(path.join(targetDir, 'docs', 'adr'));
1218
- console.log(` ${pc.dim('-')} CONTEXT.md ${contextMdExists ? 'detected' : 'not created yet (informational only)'}`);
1219
- console.log(` ${pc.dim('-')} docs/adr ${docsAdrExists ? 'detected' : 'not created yet (informational only)'}`);
1220
-
1221
- console.log(`\n${pc.bold('Next step')}`);
1222
- console.log(' Run thachvd-kit prompt to print the saved project-indexing prompt.');
1223
- }
1224
-
1225
- async function runSetup(args) {
1226
- const skipMcpSetup = args.includes('--no-setup-mcp');
1227
- const skipHookSetup = args.includes('--no-setup-hook');
1228
- const skipRtkInstall = args.includes('--no-install-rtk');
1229
- const skipIndex = args.includes('--no-index');
1230
- const skipSkillsInstall = args.includes('--no-install-skills');
1231
- console.log(pc.bold(pc.cyan('thachvd-kit setup')));
1232
- if (!skipSkillsInstall) {
1233
- console.log(pc.bold('Matt Pocock skills'));
1234
- const skillStatus = mattSkills.checkSkills(targetDir);
1235
- if (skillStatus.missing.length === 0) {
1236
- // Already fully installed: `setup` only ensures presence, it
1237
- // never re-installs or updates on its own. Explicit
1238
- // `thachvd-kit skills update` is required for that.
1239
- console.log(` ${pc.green('OK')} promoted skill set already installed (${skillStatus.total}/${skillStatus.total})`);
1240
- } else {
1241
- const skillsResult = mattSkills.installSkills(targetDir);
1242
- const skillsMarker = skillsResult.ok ? pc.green('OK') : pc.yellow('WARN');
1243
- console.log(` ${skillsMarker} ${skillsResult.message}`);
1244
- }
1245
- printMattSkillsSetupReminder();
1246
- } else {
1247
- console.log(`${pc.bold('Matt Pocock skills')}\n ${pc.dim('skipped (--no-install-skills)')}`);
1248
- }
1249
- if (!skipMcpSetup) {
1250
- console.log(pc.bold('MCP setup'));
1251
- for (const result of setupMcpServers({ install: true })) {
1252
- const marker = result.ok ? pc.green('OK') : pc.yellow('WARN');
1253
- console.log(` ${marker} ${result.message}`);
1254
- }
1255
- if (!skipIndex) {
1256
- const indexResult = indexRepository();
1257
- const marker = indexResult.ok ? pc.green('OK') : pc.yellow('WARN');
1258
- console.log(` ${marker} ${indexResult.message}`);
1259
- }
1260
- }
1261
- if (!skipHookSetup) {
1262
- console.log(`\n${pc.bold('Claude Code Definition-of-Done hook')}`);
1263
- const claudeProjectSettings = path.join(targetDir, '.claude', 'settings.json');
1264
- const result = mergeClaudeProjectHooks(claudeProjectSettings);
1265
- const marker = result.ok ? pc.green('OK') : pc.yellow('WARN');
1266
- console.log(` ${marker} ${result.message}`);
1267
- console.log(' This hook only checks for plan/review/verification evidence; it never prescribes which skill produces it.');
1268
- }
1269
- console.log(`\n${pc.bold('RTK (recommended)')}`);
1270
- const rtkResult = ensureRtkInstalled(!skipRtkInstall);
1271
- const rtkMarker = rtkResult.ok ? pc.green('OK') : pc.yellow('WARN');
1272
- console.log(` ${rtkMarker} ${rtkResult.message}`);
1273
- console.log('\nRestart your AI client after installing plugins or changing MCP configuration.');
1274
- console.log('Your onboarding prompt is saved at .agent/docs/index-project-prompt.md. Run thachvd-kit prompt to print it again.');
1275
- }
1276
-
1277
- function commandExists(command) {
1278
- const checker = process.platform === 'win32' ? 'where' : 'command';
1279
- const args = process.platform === 'win32' ? [command] : ['-v', command];
1280
- const result = spawnSync(checker, args, { encoding: 'utf8', shell: process.platform !== 'win32' });
1281
- return result.status === 0;
1282
- }
1283
-
1284
- function ensureGlobalNpmPackage(binaryName, packageName, install = false) {
1285
- if (commandExists(binaryName)) {
1286
- return { ok: true, message: `${binaryName} already installed` };
1287
- }
1288
- if (!install) {
1289
- return { ok: false, message: `${binaryName} is missing; run: npm install -g ${packageName}` };
1290
- }
1291
- if (!commandExists('npm')) {
1292
- return { ok: false, message: `npm not found; run: npm install -g ${packageName}` };
1293
- }
1294
- const result = spawnSync('npm', ['install', '-g', packageName], {
1295
- encoding: 'utf8',
1296
- stdio: 'pipe',
1297
- shell: process.platform === 'win32'
1298
- });
1299
- if (result.status !== 0) {
1300
- return { ok: false, message: `failed to install ${binaryName}; run: npm install -g ${packageName}` };
1301
- }
1302
- return { ok: true, message: `installed ${binaryName}` };
1303
- }
1304
-
1305
- function ensureCodebaseMemoryCli(install = false) {
1306
- return ensureGlobalNpmPackage(CODEBASE_MEMORY_BINARY, CODEBASE_MEMORY_PACKAGE, install);
1307
- }
1308
-
1309
- function ensureRtkInstalled(install = false) {
1310
- if (commandExists('rtk')) {
1311
- return { ok: true, message: 'rtk already installed' };
1312
- }
1313
- if (!install) {
1314
- return { ok: false, message: `rtk is missing; run: ${RTK_INSTALL_COMMAND}` };
1315
- }
1316
- if (!commandExists('cargo')) {
1317
- return { ok: false, message: `cargo not found; install Rust, then run: ${RTK_INSTALL_COMMAND}` };
1318
- }
1319
- console.log(pc.dim(' Installing rtk via cargo (compiles from source; this can take a few minutes)...'));
1320
- const result = spawnSync('cargo', ['install', '--git', 'https://github.com/rtk-ai/rtk', '--branch', 'master', 'rtk'], {
1321
- encoding: 'utf8',
1322
- stdio: 'inherit',
1323
- shell: process.platform === 'win32'
1324
- });
1325
- if (result.status !== 0) {
1326
- return { ok: false, message: `failed to install rtk; run: ${RTK_INSTALL_COMMAND}` };
1327
- }
1328
- return { ok: true, message: 'installed rtk' };
1329
- }
1330
-
1331
- function indexRepository() {
1332
- if (!commandExists(CODEBASE_MEMORY_BINARY)) {
1333
- return { ok: false, message: `${CODEBASE_MEMORY_BINARY} is not installed; skipped indexing` };
1334
- }
1335
- console.log(pc.dim(` Running ${CODEBASE_MEMORY_BINARY} cli index_repository --repo-path . (first index of a large repo can take a while)...`));
1336
- const result = spawnSync(CODEBASE_MEMORY_BINARY, ['cli', 'index_repository', '--repo-path', '.'], {
1337
- cwd: targetDir,
1338
- encoding: 'utf8',
1339
- stdio: 'inherit',
1340
- shell: process.platform === 'win32'
1341
- });
1342
- if (result.status !== 0) {
1343
- return { ok: false, message: `indexing failed; run manually: ${CODEBASE_MEMORY_BINARY} cli index_repository --repo-path .` };
1344
- }
1345
- return { ok: true, message: 'indexed repository with codebase-memory-mcp' };
1346
- }
1347
-
1348
- function readJsonFile(filePath) {
1349
- if (!fs.existsSync(filePath)) return {};
1350
- try {
1351
- return JSON.parse(fs.readFileSync(filePath, 'utf8'));
1352
- } catch {
1353
- return null;
1354
- }
1355
- }
1356
-
1357
- function writeJsonFile(filePath, data) {
1358
- fs.mkdirSync(path.dirname(filePath), { recursive: true });
1359
- fs.writeFileSync(filePath, JSON.stringify(data, null, 2) + '\n', 'utf8');
1360
- }
1361
-
1362
- function mergeMcpJson(filePath, withType = false) {
1363
- const data = readJsonFile(filePath);
1364
- if (data === null) {
1365
- return { ok: false, message: `${filePath} is not valid JSON; skipped` };
1366
- }
1367
- data.mcpServers = data.mcpServers || {};
1368
- const baseServers = {
1369
- 'codebase-memory-mcp': { command: CODEBASE_MEMORY_BINARY, args: [] },
1370
- playwright: { command: 'playwright-mcp' },
1371
- context7: { command: 'context7-mcp' }
1372
- };
1373
- if (data.permissions && Array.isArray(data.permissions.allow)) {
1374
- const requiredPermissions = [
1375
- "mcp__context7__resolve-library-id",
1376
- "mcp__context7__query-docs"
1377
- ];
1378
- for (const perm of requiredPermissions) {
1379
- if (!data.permissions.allow.includes(perm)) {
1380
- data.permissions.allow.push(perm);
1381
- }
1382
- }
1383
- }
1384
- for (const [name, server] of Object.entries(baseServers)) {
1385
- if (!data.mcpServers[name]) {
1386
- data.mcpServers[name] = withType ? { type: 'stdio', ...server } : server;
1387
- }
1388
- }
1389
- writeJsonFile(filePath, data);
1390
- const tools = Object.keys(baseServers).join(', ');
1391
- return { ok: true, message: `configured ${tools} MCP in ${filePath}` };
1392
- }
1393
-
1394
- function appendMcpTomlIfMissing(filePath) {
1395
- fs.mkdirSync(path.dirname(filePath), { recursive: true });
1396
- const existing = fs.existsSync(filePath) ? fs.readFileSync(filePath, 'utf8') : '';
1397
- const blocks = [];
1398
- if (!/\[mcp_servers\.context7\]/.test(existing)) {
1399
- blocks.push(`[mcp_servers.context7]
1400
- command = "context7-mcp"
1401
- startup_timeout_sec = 20
1402
- tool_timeout_sec = 120
1403
- `);
1404
- }
1405
- if (!/\[mcp_servers\.codebase-memory-mcp\]/.test(existing)) {
1406
- blocks.push(`[mcp_servers.codebase-memory-mcp]
1407
- command = "${CODEBASE_MEMORY_BINARY}"
1408
- startup_timeout_sec = 20
1409
- tool_timeout_sec = 120
1410
- `);
1411
- }
1412
- if (!/\[mcp_servers\.playwright\]/.test(existing)) {
1413
- blocks.push(`[mcp_servers.playwright]
1414
- command = "playwright-mcp"
1415
- startup_timeout_sec = 20
1416
- tool_timeout_sec = 120
1417
- `);
1418
- }
1419
- if (blocks.length === 0) { return { ok: true, message: `already configured ${filePath}` };
1420
- }
1421
- const separator = existing && !existing.endsWith('\n') ? '\n\n' : existing ? '\n' : '';
1422
- fs.writeFileSync(filePath, existing + separator + blocks.join('\n'), 'utf8');
1423
- return { ok: true, message: `configured codebase-memory-mcp, context7, playwright MCP in ${filePath}` };
1424
- }
1425
-
1426
- const DOD_HOOK_MARKER = 'feature/refactor Definition-of-Done gate';
1427
-
1428
- function generateDodStopHookPrompt() {
1429
- return `Input JSON (Stop hook payload): $ARGUMENTS\n\nCheck this project's ${DOD_HOOK_MARKER}. This kit does not define its own workflow steps or require any specific workflow skill; it only verifies that evidence of the gate exists, however that evidence was produced (a documented planning/review skill, a manual process, or anything equivalent). Use the \`last_assistant_message\` field as the authoritative final response. Use \`transcript_path\` only for additional context because the transcript may not contain the final response yet.\n\nVerification is never optional: every change, fast path included, requires semantic evidence in the final response or visible transcript that focused tests or equivalent verification actually ran and passed (for example test/build output, or a described manual verification). Do not require a specific command; judge the evidence on its meaning.\n\nFor any change that is not a verified fast path (see below), completion additionally requires evidence of:\n- Plan: a plan was written and explicitly approved by the human before implementation started.\n- Review: the change was reviewed (correctness, simplicity, architecture, security, performance) and blocking issues were resolved.\n\nFast-path eligibility is based on risk and contract surface, not file count. A change is fast path only when it is localized, mechanically obvious, low-risk, and has no meaningful public API, schema, auth/security, data-integrity, payment, concurrency, dependency, CI/release, or architectural risk, with focused verification available. A source file plus its own focused test can still be fast path; a one-file auth, payment, schema, security, or concurrency change is not fast path no matter how small. A verified fast-path change is exempt from the Plan and Review evidence only when the final response states FAST_PATH: simple-fix, its reason, and its narrow scope — Verification evidence is still mandatory in that case, not optional.\n\nSteps:\n1. If the input JSON has \`stop_hook_active: true\`, allow stopping immediately (already checked once this cycle; never block twice in a row).\n2. Inspect the changed files and the \`last_assistant_message\` field.\n3. If no source files changed or the final response does not claim the task is done/complete, allow stopping.\n4. If the final response explicitly contains FAST_PATH: simple-fix, first check the claim against the risk criteria above, not file count. If it actually touches one of those risk areas regardless of file count, the FAST_PATH claim is invalid; treat it as a full feature/refactor change and continue to step 5. Otherwise, look for semantic evidence that focused verification actually ran and passed. If that evidence is present, allow stopping. If it is missing, block with {"ok": false, "reason": "Missing Verification evidence: ..."}.\n5. If the final response claims completion for a change that is not a verified fast path, check all three gates: (a) a plan was written and explicitly approved by the human before implementation started; (b) the change was reviewed and blocking issues were resolved; (c) tests or equivalent verification ran and passed. If any one of the three is missing, block naming exactly that gate; a generic statement that the task was simple, or that it touched only one file, is not enough on its own.\n6. Return exactly JSON: {"ok": true} to allow stopping, or {"ok": false, "reason": "..."} to block.\n\nWhen blocking, name exactly which gate is missing without prescribing which skill or workflow must produce it.`;
1430
- }
1431
-
1432
- function isDefinitionOfDoneHook(hook) {
1433
- const prompt = hook && typeof hook.prompt === 'string' ? hook.prompt : '';
1434
- return prompt.includes(DOD_HOOK_MARKER);
1435
- }
1436
-
1437
- function mergeClaudeProjectHooks(filePath) {
1438
- const data = readJsonFile(filePath);
1439
- if (data === null) {
1440
- return { ok: false, message: `${filePath} is not valid JSON; skipped hook setup` };
1441
- }
1442
- if (!data.hooks || typeof data.hooks !== 'object') data.hooks = {};
1443
- if (!Array.isArray(data.hooks.Stop)) data.hooks.Stop = [];
1444
-
1445
- let primaryHook = null;
1446
- let definitionHookCount = 0;
1447
- for (const entry of data.hooks.Stop) {
1448
- for (const hook of Array.isArray(entry?.hooks) ? entry.hooks : []) {
1449
- if (isDefinitionOfDoneHook(hook)) {
1450
- definitionHookCount++;
1451
- if (!primaryHook) primaryHook = hook;
1452
- }
1453
- }
1454
- }
1455
-
1456
- if (primaryHook) {
1457
- primaryHook.prompt = generateDodStopHookPrompt();
1458
- primaryHook.timeout = 60;
1459
- primaryHook.statusMessage = 'Checking feature/refactor Definition-of-Done gate...';
1460
-
1461
- if (definitionHookCount > 1) {
1462
- data.hooks.Stop = data.hooks.Stop
1463
- .map(entry => ({
1464
- ...entry,
1465
- hooks: (Array.isArray(entry?.hooks) ? entry.hooks : [])
1466
- .filter(hook => !isDefinitionOfDoneHook(hook) || hook === primaryHook)
1467
- }))
1468
- .filter(entry => entry.hooks.length > 0);
1469
- }
1470
-
1471
- writeJsonFile(filePath, data);
1472
- return {
1473
- ok: true,
1474
- message: definitionHookCount > 1
1475
- ? `consolidated Definition-of-Done Stop hooks in ${filePath}`
1476
- : `updated Definition-of-Done Stop hook in ${filePath}`
1477
- };
1478
- }
1479
-
1480
- data.hooks.Stop.push({
1481
- hooks: [
1482
- {
1483
- type: 'agent',
1484
- prompt: generateDodStopHookPrompt(),
1485
- timeout: 60,
1486
- statusMessage: 'Checking feature/refactor Definition-of-Done gate...'
1487
- }
1488
- ]
1489
- });
1490
- writeJsonFile(filePath, data);
1491
- return { ok: true, message: `configured Definition-of-Done Stop hook in ${filePath}` };
1492
- }
1493
-
1494
- function setupMcpServers(options = {}) {
1495
- const install = options.install === true;
1496
- const results = [];
1497
- results.push(ensureCodebaseMemoryCli(install));
1498
- results.push(ensureGlobalNpmPackage('context7-mcp', '@upstash/context7-mcp', install));
1499
- results.push(ensureGlobalNpmPackage('playwright-mcp', '@playwright/mcp', install));
1500
-
1501
- const codexConfig = homePath('.codex', 'config.toml');
1502
- if (codexConfig) results.push(appendMcpTomlIfMissing(codexConfig));
1503
-
1504
- const geminiConfig = homePath('.gemini', 'config', 'mcp_config.json');
1505
- if (geminiConfig) results.push(mergeMcpJson(geminiConfig, false));
1506
-
1507
- const antigravityIdeConfig = homePath('.gemini', 'antigravity-ide', 'mcp_config.json');
1508
- if (antigravityIdeConfig) results.push(mergeMcpJson(antigravityIdeConfig, false));
1509
-
1510
- const antigravityConfig = homePath('.gemini', 'antigravity', 'mcp_config.json');
1511
- if (antigravityConfig && fs.existsSync(path.dirname(antigravityConfig))) {
1512
- results.push(mergeMcpJson(antigravityConfig, false));
1513
- }
1514
-
1515
- const claudeConfig = homePath('.claude.json');
1516
- if (claudeConfig) results.push(mergeMcpJson(claudeConfig, true));
1517
-
1518
- const claudeSettings = homePath('.claude', 'settings.json');
1519
- if (claudeSettings) results.push(mergeMcpJson(claudeSettings, true));
1520
-
1521
- return results;
1522
- }
1523
-
1524
- function runSkillsCommand(args) {
1525
- const subcommand = args[0];
1526
- const dryRun = args.includes('--dry-run');
1527
-
1528
- if (subcommand === 'install') {
1529
- console.log(pc.bold(pc.cyan('thachvd-kit skills install')));
1530
- const result = mattSkills.installSkills(targetDir, { dryRun });
1531
- console.log(` ${result.ok ? pc.green('OK') : pc.yellow('WARN')} ${result.message}`);
1532
- if (!dryRun && result.ok) printMattSkillsSetupReminder();
1533
- if (!result.ok) process.exitCode = 1;
1534
- return;
1535
- }
1536
-
1537
- if (subcommand === 'check') {
1538
- console.log(pc.bold(pc.cyan('thachvd-kit skills check')));
1539
- const status = mattSkills.checkSkills(targetDir);
1540
- for (const name of mattSkills.PROMOTED_SKILLS) {
1541
- const installed = status.installed.includes(name);
1542
- console.log(` ${installed ? pc.green('OK') : pc.yellow('WARN')} ${name}`);
1543
- }
1544
- console.log(`\n ${status.installed.length}/${status.total} promoted skills installed in ${mattSkills.SKILLS_RELATIVE_DIR}`);
1545
- if (status.missing.length > 0) {
1546
- console.log(` Run: ${pc.bold('thachvd-kit skills install')}`);
1547
- }
1548
- return;
1549
- }
1550
-
1551
- if (subcommand === 'update') {
1552
- console.log(pc.bold(pc.cyan('thachvd-kit skills update')));
1553
- const result = mattSkills.updateSkills(targetDir, { dryRun });
1554
- if (dryRun) {
1555
- for (const item of result.results) console.log(` ${pc.dim('would run')} ${item.command}`);
1556
- } else {
1557
- for (const item of result.results) console.log(` ${item.ok ? pc.green('OK') : pc.yellow('WARN')} ${item.name}`);
1558
- }
1559
- console.log(`\n ${result.ok ? pc.green('OK') : pc.yellow('WARN')} ${result.message}`);
1560
- if (!result.ok) process.exitCode = 1;
1561
- return;
1562
- }
1563
-
1564
- console.error(pc.red(`Unknown skills subcommand: ${subcommand || '(none)'}`));
1565
- console.error('Usage: thachvd-kit skills install|check|update [--dry-run]');
1566
- process.exit(1);
1567
- }
1568
-
1569
- // --- Main CLI ---
1570
-
1571
- async function main() {
1572
- // Note: 'upgrade' and 'global' are handled directly by bin/entry.js
1573
- // (bin/upgrade.js / bin/global.js) before this module is even required,
1574
- // so they never reach here.
1575
- const rawArgs = process.argv.slice(2);
1576
- const command = rawArgs[0] && !rawArgs[0].startsWith('-') ? rawArgs[0] : 'init';
1577
- const args = command === 'init' ? rawArgs.slice(rawArgs[0] === 'init' ? 1 : 0) : rawArgs.slice(1);
1578
-
1579
- if (rawArgs.includes('--version') || rawArgs.includes('-v')) {
1580
- console.log(packageJson.version);
1581
- return;
1582
- }
1583
-
1584
- if (rawArgs.includes('--help') || rawArgs.includes('-h')) {
1585
- printHelp();
1586
- return;
1587
- }
1588
-
1589
- if (command === 'doctor') {
1590
- runDoctor();
1591
- return;
1592
- }
1593
-
1594
- if (command === 'prompt') {
1595
- printProjectPrompt();
1596
- return;
1597
- }
1598
-
1599
- if (command === 'setup') {
1600
- await runSetup(rawArgs.slice(1));
1601
- return;
1602
- }
1603
-
1604
- if (command === 'skills') {
1605
- runSkillsCommand(rawArgs.slice(1));
1606
- return;
1607
- }
1608
-
1609
- if (command !== 'init') {
1610
- console.error(pc.red(`Unknown command: ${command}`));
1611
- printHelp();
1612
- process.exit(1);
1613
- }
1614
-
1615
- const assumeYes = args.includes('--yes') || args.includes('-y');
1616
- const skipMcpSetup = args.includes('--no-setup-mcp') || process.env.THACHVD_KIT_SKIP_MCP_SETUP === '1';
1617
-
1618
- console.log(pc.bold(pc.cyan('='.repeat(50))));
1619
- console.log(' ' + pc.bold('thachvd-kit') + ' - AI Project Bootstrap');
1620
- console.log(' Generates shared agent rules and configures MCP tooling');
1621
- console.log(pc.bold(pc.cyan('='.repeat(50))));
1622
-
1623
- console.log(pc.dim(' Scanning current project files for a baseline...\n'));
1624
- const scanned = scanProject();
1625
-
1626
- console.log(` ${pc.bold('Detected context:')}`);
1627
- formatScanPreviewLines(scanned).forEach(([label, value]) => {
1628
- console.log(` - ${label}: ${value}`);
1629
- });
1630
- console.log('');
1631
- if ((scanned.scan_evidence || []).length > 0) {
1632
- console.log(` ${pc.bold('Evidence:')}`);
1633
- scanned.scan_evidence.forEach(line => console.log(` - ${line}`));
1634
- console.log('');
1635
- }
1636
-
1637
- // Combine data
1638
- const data = {
1639
- project_name: scanned?.project_name || path.basename(targetDir),
1640
- description: scanned?.description || 'A software project',
1641
- project_type: scanned?.project_type || 'web-app',
1642
- primary_language: scanned?.primary_language || 'typescript',
1643
- frameworks: scanned?.frameworks || [],
1644
- database: scanned?.database || 'none',
1645
- uses_docker: scanned?.uses_docker ?? true,
1646
- cloud: scanned?.cloud || 'none',
1647
- package_manager: scanned?.package_manager || 'auto-detect',
1648
- test_framework: scanned?.test_framework || 'auto-detect'
1649
- };
1650
-
1651
- data.database = data.database || scanned?.database || 'none';
1652
- data.uses_docker = typeof data.uses_docker === 'boolean' ? data.uses_docker : (scanned?.uses_docker ?? true);
1653
- data.cloud = data.cloud || scanned?.cloud || 'none';
1654
- data.package_manager = data.package_manager || scanned?.package_manager || 'auto-detect';
1655
- data.test_framework = data.test_framework || scanned?.test_framework || 'auto-detect';
1656
- data.framework_details = scanned?.framework_details || [];
1657
- data.scan_evidence = scanned?.scan_evidence || [];
1658
- data.app_root = scanned?.app_root || '.';
1659
-
1660
- data.infrastructure = [];
1661
- if (data.uses_docker) data.infrastructure.push('docker');
1662
- if (data.cloud !== 'none') data.infrastructure.push(data.cloud);
1663
-
1664
- const shouldSetupMcp = args.includes('--setup-mcp') && !skipMcpSetup;
1665
-
1666
- console.log(`\n${pc.bold(pc.cyan('Generating Files'))}`);
1667
-
1668
- // Shared entry files are written after the .agent folder is available.
1669
-
1670
- console.log(` ${pc.green('OK')} Project context generation enabled; run thachvd-kit setup to install the promoted Matt Pocock skill set`);
1671
-
1672
-
1673
- const generatedFiles = [
1674
- ['AGENTS.md', generateSharedAgentsMd(data)],
1675
- ['CLAUDE.md', generateSharedClaudeMd()],
1676
- ['GEMINI.md', generateSharedGeminiMd()],
1677
- ['.cursorrules', generateSharedCursorrules(data)],
1678
- [path.join('.agent', 'docs', 'project.md'), generateProjectDoc(data)],
1679
- [path.join('.agent', 'docs', 'architecture.md'), generateArchitectureDoc(data)],
1680
- [path.join('.agent', 'docs', 'conventions.md'), generateConventionsDoc()],
1681
- [path.join('.agent', 'docs', 'workflow.md'), generateWorkflowDoc()],
1682
- [path.join('.agent', 'docs', 'tooling.md'), generateToolingDoc(data)],
1683
- [path.join('.agent', 'docs', 'getting-started.md'), generateGettingStartedDoc()],
1684
- [path.join('.agent', 'docs', 'index-project-prompt.md'), generateIndexProjectPrompt()]
1685
- ];
1686
-
1687
- for (const [relativePath, content] of generatedFiles) {
1688
- const wrote = await writeGeneratedFile(relativePath, content, assumeYes);
1689
- if (wrote) console.log(` ${pc.green('OK')} Generated ${pc.bold(relativePath)}`);
1690
- else console.log(` ${pc.yellow('~')} Skipped ${pc.bold(relativePath)}`);
1691
- }
1692
-
1693
- if (shouldSetupMcp) {
1694
- console.log(`\n${pc.bold(pc.cyan('Configuring MCP'))}`);
1695
- for (const result of setupMcpServers()) {
1696
- const marker = result.ok ? pc.green('OK') : pc.yellow('~');
1697
- console.log(` ${marker} ${result.message}`);
1698
- }
1699
- }
1700
-
1701
- console.log(`\n${pc.bold(pc.green('Done!'))}
1702
-
1703
- ${pc.bold('Project:')} ${data.project_name}
1704
- ${pc.bold('Stack:')} ${data.primary_language} | ${(data.frameworks || []).join(', ') || 'no framework'}
1705
-
1706
- ${pc.bold('Generated:')}
1707
- - ${pc.bold('AGENTS.md')} shared entry for Codex, Antigravity, Claude Code, and Cursor
1708
- - ${pc.bold('CLAUDE.md')} Claude Code entry that imports AGENTS.md
1709
- - ${pc.bold('GEMINI.md')} Antigravity entry
1710
- - ${pc.bold('.cursorrules')} Cursor entry
1711
- - ${pc.bold('.agent/docs/')} scan-based project rules and local operating notes
1712
- - ${pc.bold('.agent/docs/index-project-prompt.md')} durable onboarding prompt
1713
-
1714
- ${pc.bold('MCP/tooling setup:')}
1715
- - Matt Pocock's promoted skills (.agents/skills/) are the workflow layer
1716
- - Run ${pc.bold('thachvd-kit setup')} to install them and configure codebase-memory MCP and recommended RTK
1717
-
1718
- ${pc.bold('Next steps:')}
1719
- 1. Run ${pc.bold('thachvd-kit prompt')} and paste the saved prompt into your AI editor
1720
- 2. Run ${pc.bold('thachvd-kit setup')} to install the promoted skill set and RTK
1721
- 3. Run ${pc.bold('thachvd-kit doctor')} to verify the machine
1722
- 4. Re-run ${pc.bold('thachvd-kit prompt')} whenever you need the onboarding prompt again
1723
-
1724
- ${pc.bold('Onboarding prompt saved to .agent/docs/index-project-prompt.md:')}
1725
- ${pc.dim('---')}
1726
- ${generateIndexProjectPrompt().trim()}
1727
- ${pc.dim('---')}
1728
- `);
1729
- }
1730
-
1731
- main().catch(err => {
1732
- console.error(pc.red('\nError during init:'), err);
1733
- process.exit(1);
1734
- });
1735
-
1
+ #!/usr/bin/env node
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const { spawnSync } = require('child_process');
6
+ const prompts = require('prompts');
7
+ const pc = require('picocolors');
8
+ const packageJson = require('../package.json');
9
+ const mattSkills = require('./matt-skills');
10
+ const nativeSkills = require('./native-skills');
11
+ const {
12
+ QUALITY_FLOOR_SECTION,
13
+ AGENT_RULES_SECTION,
14
+ CONVENTIONS_STANDARDS_SECTION,
15
+ FAST_PATH_SECTION,
16
+ GETTING_STARTED_FAST_PATH_SECTION,
17
+ TOOL_ROUTING_SECTION
18
+ } = require('./policy');
19
+
20
+ const sourceDir = path.resolve(__dirname, '..');
21
+ const targetDir = process.cwd();
22
+ const CODEBASE_MEMORY_PACKAGE = 'codebase-memory-mcp';
23
+ const CODEBASE_MEMORY_BINARY = 'codebase-memory-mcp';
24
+ const RTK_INSTALL_COMMAND = 'cargo install --git https://github.com/rtk-ai/rtk --branch master rtk';
25
+
26
+ const FRAMEWORK_CHOICES = [
27
+ 'vue', 'react', 'next', 'nuxt', 'svelte', 'angular',
28
+ 'laravel', 'cakephp', 'symfony', 'codeigniter',
29
+ 'express', 'fastapi', 'django', 'flask', 'gin', 'fiber', 'echo', 'nestjs',
30
+ 'rails', 'spring', 'aspnet',
31
+ 'tauri', 'axum', 'actix', 'flutter', 'react-native'
32
+ ];
33
+
34
+ const LANGUAGE_CHOICES = ['typescript', 'javascript', 'python', 'go', 'php', 'rust', 'java', 'ruby', 'swift', 'kotlin', 'csharp', 'cpp'];
35
+ const PROJECT_TYPE_CHOICES = ['web-app', 'api', 'mobile', 'desktop', 'cli', 'fullstack'];
36
+ const DATABASE_CHOICES = ['postgres', 'mysql', 'sqlite', 'mongodb', 'redis', 'dynamodb', 'none'];
37
+ const PACKAGE_MANAGER_CHOICES = ['npm', 'pnpm', 'yarn', 'bun', 'pip', 'uv', 'go mod', 'cargo', 'composer', 'auto-detect'];
38
+ const TEST_FRAMEWORK_CHOICES = ['jest', 'vitest', 'pytest', 'go test', 'phpunit', 'pest', 'rspec', 'junit', 'auto-detect'];
39
+ const CLOUD_CHOICES = ['aws', 'gcp', 'azure', 'vercel', 'fly', 'railway', 'none'];
40
+ const FRONTEND_FRAMEWORKS = ['vue', 'react', 'next', 'nuxt', 'svelte', 'angular'];
41
+ const BACKEND_FRAMEWORKS = ['laravel', 'cakephp', 'symfony', 'codeigniter', 'express', 'fastapi', 'django', 'flask', 'gin', 'fiber', 'echo', 'nestjs', 'rails', 'spring', 'aspnet', 'axum', 'actix'];
42
+ const MOBILE_DESKTOP_FRAMEWORKS = ['tauri', 'flutter', 'react-native'];
43
+ const IGNORED_SCAN_DIRS = new Set(['.git', 'node_modules', 'vendor', 'dist', 'build', 'coverage', '.next', '.nuxt', '.turbo', '.cache', 'storage', 'tmp']);
44
+
45
+ function printHelp() {
46
+ console.log(`
47
+ ${pc.bold('Usage:')}
48
+ thachvd-kit init [--yes]
49
+ thachvd-kit upgrade [--dry-run]
50
+ thachvd-kit global [--dry-run] [--antigravity-only|--codex-only]
51
+ thachvd-kit setup [--yes] [--no-install-skills]
52
+ thachvd-kit doctor
53
+ thachvd-kit prompt
54
+ thachvd-kit skills install [--dry-run]
55
+ thachvd-kit skills check
56
+ thachvd-kit skills update [--dry-run]
57
+ thachvd-kit --version
58
+ thachvd-kit --help
59
+
60
+ ${pc.bold('Commands:')}
61
+ init Generate project context files and documentation (new projects)
62
+ upgrade Safely add/update thachvd-kit's managed policy blocks in an existing project (does not rescan or regenerate)
63
+ global Patch a managed tool-routing block into the current user's Antigravity/Codex global instructions
64
+ setup Install/configure MCP, Matt Pocock skills, and recommended local tooling
65
+ doctor Check project context and local integrations
66
+ prompt Print the saved project-indexing prompt again
67
+ skills Install, check, or update the promoted Matt Pocock skill set
68
+
69
+ ${pc.bold('Options:')}
70
+ --yes Overwrite generated files or accept setup defaults
71
+ --no-setup-mcp Skip MCP setup (legacy init compatibility)
72
+ --no-setup-hook Skip the Claude Code Definition-of-Done Stop hook during setup
73
+ --no-install-rtk Skip auto-installing rtk via cargo during setup
74
+ --no-index Skip auto-indexing the repository with codebase-memory-mcp during setup
75
+ --no-install-skills Skip installing Matt Pocock and thachvd-kit native skills during setup
76
+ --dry-run Preview a change without writing it (upgrade/global/skills install/skills update)
77
+ --antigravity-only Limit global to the Antigravity global instructions
78
+ --codex-only Limit global to the Codex global instructions
79
+ --version Show the installed CLI version
80
+ --help Show this help message
81
+
82
+ ${pc.bold('What gets generated:')}
83
+ AGENTS.md Shared instructions for Codex, Antigravity, Claude Code, and Cursor
84
+ CLAUDE.md Claude Code entry file that imports AGENTS.md
85
+ GEMINI.md Antigravity entry file that points to AGENTS.md
86
+ .cursorrules Cursor entry file that points to AGENTS.md
87
+ .agent/docs/ Project context, architecture, workflow, tooling, and durable onboarding prompt
88
+
89
+ ${pc.bold('Workflow:')}
90
+ Matt Pocock's promoted skills plus thachvd-kit's native worktree/SDD skills
91
+ form the workflow layer. Use /using-git-worktrees only when isolation is
92
+ intended; choose /implement or /subagent-driven-development as alternative
93
+ executors for a plan, then finish with /code-review and
94
+ /finishing-a-development-branch.
95
+ Unsure which planning or implementation skill fits? Use /ask-matt.
96
+
97
+ ${pc.bold('Tip:')}
98
+ Run thachvd-kit setup to install the promoted skill set and configure
99
+ local tooling, then thachvd-kit doctor to verify the machine.
100
+ `);
101
+ }
102
+
103
+ function formatScanPreviewLines(scanned) {
104
+ return [
105
+ ['App root', scanned.app_root || '.'],
106
+ ['Project type', scanned.project_type || 'unknown'],
107
+ ['Primary language', scanned.primary_language || 'unknown'],
108
+ ['Frameworks', (scanned.frameworks || []).join(', ') || 'no framework'],
109
+ ['Database', scanned.database || 'none'],
110
+ ['Package manager', scanned.package_manager || 'auto-detect'],
111
+ ['Test framework', scanned.test_framework || 'auto-detect'],
112
+ ['Cloud', scanned.cloud || 'none'],
113
+ ['Infrastructure', (scanned.infrastructure || []).join(', ') || 'none'],
114
+ ['CI/CD', scanned.ci_cd || 'none']
115
+ ];
116
+ }
117
+
118
+ async function askQuestions(questions) {
119
+ return prompts(questions, {
120
+ onCancel: () => {
121
+ console.log(pc.yellow('\nCancelled.'));
122
+ process.exit(130);
123
+ }
124
+ });
125
+ }
126
+
127
+ function fileExists(...parts) {
128
+ return fs.existsSync(path.join(targetDir, ...parts));
129
+ }
130
+
131
+ function fileExistsAt(baseDir, ...parts) {
132
+ return fs.existsSync(path.join(baseDir, ...parts));
133
+ }
134
+
135
+ function readJsonIfExists(...parts) {
136
+ const fullPath = path.join(targetDir, ...parts);
137
+ if (!fs.existsSync(fullPath)) return null;
138
+ try {
139
+ return JSON.parse(fs.readFileSync(fullPath, 'utf8'));
140
+ } catch {
141
+ return null;
142
+ }
143
+ }
144
+
145
+ function readJsonIfExistsAt(baseDir, ...parts) {
146
+ const fullPath = path.join(baseDir, ...parts);
147
+ if (!fs.existsSync(fullPath)) return null;
148
+ try {
149
+ return JSON.parse(fs.readFileSync(fullPath, 'utf8'));
150
+ } catch {
151
+ return null;
152
+ }
153
+ }
154
+
155
+ function readTextIfExists(...parts) {
156
+ const fullPath = path.join(targetDir, ...parts);
157
+ if (!fs.existsSync(fullPath)) return '';
158
+ try {
159
+ return fs.readFileSync(fullPath, 'utf8');
160
+ } catch {
161
+ return '';
162
+ }
163
+ }
164
+
165
+ function readTextIfExistsAt(baseDir, ...parts) {
166
+ const fullPath = path.join(baseDir, ...parts);
167
+ if (!fs.existsSync(fullPath)) return '';
168
+ try {
169
+ return fs.readFileSync(fullPath, 'utf8');
170
+ } catch {
171
+ return '';
172
+ }
173
+ }
174
+
175
+ function detectPackageManager(baseDir = targetDir) {
176
+ if (fileExistsAt(baseDir, 'pnpm-lock.yaml')) return 'pnpm';
177
+ if (fileExistsAt(baseDir, 'yarn.lock')) return 'yarn';
178
+ if (fileExistsAt(baseDir, 'bun.lockb') || fileExistsAt(baseDir, 'bun.lock')) return 'bun';
179
+ if (fileExistsAt(baseDir, 'package-lock.json')) return 'npm';
180
+ if (fileExistsAt(baseDir, 'uv.lock')) return 'uv';
181
+ if (fileExistsAt(baseDir, 'poetry.lock')) return 'pip';
182
+ if (fileExistsAt(baseDir, 'requirements.txt') || fileExistsAt(baseDir, 'pyproject.toml')) return 'pip';
183
+ if (fileExistsAt(baseDir, 'go.mod')) return 'go mod';
184
+ if (fileExistsAt(baseDir, 'Cargo.toml')) return 'cargo';
185
+ if (fileExistsAt(baseDir, 'composer.json')) return 'composer';
186
+ return 'auto-detect';
187
+ }
188
+
189
+ function uniq(values) {
190
+ return Array.from(new Set(values.filter(Boolean)));
191
+ }
192
+
193
+ function detectAppRoot(baseDir) {
194
+ const candidates = [baseDir];
195
+ const queue = [{ dir: baseDir, depth: 0 }];
196
+
197
+ while (queue.length > 0) {
198
+ const { dir, depth } = queue.shift();
199
+ if (depth >= 2) continue;
200
+ let entries = [];
201
+ try {
202
+ entries = fs.readdirSync(dir, { withFileTypes: true });
203
+ } catch {
204
+ continue;
205
+ }
206
+ for (const entry of entries) {
207
+ if (!entry.isDirectory()) continue;
208
+ if (IGNORED_SCAN_DIRS.has(entry.name)) continue;
209
+ const nextDir = path.join(dir, entry.name);
210
+ candidates.push(nextDir);
211
+ queue.push({ dir: nextDir, depth: depth + 1 });
212
+ }
213
+ }
214
+
215
+ const scoreCandidate = dir => {
216
+ let score = 0;
217
+ if (fileExistsAt(dir, 'composer.json')) score += 5;
218
+ if (fileExistsAt(dir, 'package.json')) score += 4;
219
+ if (fileExistsAt(dir, 'Cargo.toml')) score += 4;
220
+ if (fileExistsAt(dir, 'go.mod')) score += 4;
221
+ if (fileExistsAt(dir, 'pyproject.toml') || fileExistsAt(dir, 'requirements.txt')) score += 4;
222
+ if (fileExistsAt(dir, 'artisan')) score += 5;
223
+ if (fileExistsAt(dir, 'bin', 'cake') || fileExistsAt(dir, 'app', 'Config', 'core.php')) score += 5;
224
+ if (fileExistsAt(dir, 'src-tauri', 'tauri.conf.json') || fileExistsAt(dir, 'src-tauri', 'tauri.conf.json5')) score += 5;
225
+ if (fileExistsAt(dir, 'manage.py')) score += 4;
226
+ if (fileExistsAt(dir, 'Gemfile')) score += 4;
227
+ if (fileExistsAt(dir, 'pom.xml') || fileExistsAt(dir, 'build.gradle') || fileExistsAt(dir, 'build.gradle.kts')) score += 4;
228
+ if (fileExistsAt(dir, 'app', 'View') || fileExistsAt(dir, 'resources', 'views') || fileExistsAt(dir, 'templates') || fileExistsAt(dir, 'app', 'views')) score += 2;
229
+ return score;
230
+ };
231
+
232
+ const ranked = candidates
233
+ .map(dir => ({ dir, score: scoreCandidate(dir) }))
234
+ .filter(item => item.score > 0)
235
+ .sort((a, b) => (b.score - a.score) || (a.dir.length - b.dir.length));
236
+
237
+ return ranked[0]?.dir || baseDir;
238
+ }
239
+
240
+ function repoHasAny(baseDir, relativePaths, maxDepth = 3) {
241
+ const queue = [{ dir: baseDir, depth: 0 }];
242
+ while (queue.length > 0) {
243
+ const { dir, depth } = queue.shift();
244
+ for (const relativePath of relativePaths) {
245
+ if (fileExistsAt(dir, ...relativePath)) return true;
246
+ }
247
+ if (depth >= maxDepth) continue;
248
+ let entries = [];
249
+ try {
250
+ entries = fs.readdirSync(dir, { withFileTypes: true });
251
+ } catch {
252
+ continue;
253
+ }
254
+ for (const entry of entries) {
255
+ if (!entry.isDirectory()) continue;
256
+ if (IGNORED_SCAN_DIRS.has(entry.name)) continue;
257
+ queue.push({ dir: path.join(dir, entry.name), depth: depth + 1 });
258
+ }
259
+ }
260
+ return false;
261
+ }
262
+
263
+ function evidenceIf(condition, label, weight = 1) {
264
+ return condition ? { label, weight } : null;
265
+ }
266
+
267
+ function buildDetection(id, evidence, meta = {}) {
268
+ const hits = evidence.filter(Boolean);
269
+ const score = hits.reduce((sum, hit) => sum + hit.weight, 0);
270
+ return {
271
+ id,
272
+ score,
273
+ evidence: hits.map(hit => hit.label),
274
+ ...meta
275
+ };
276
+ }
277
+
278
+ function confidenceLabel(score) {
279
+ if (score >= 6) return 'high';
280
+ if (score >= 3) return 'medium';
281
+ return 'low';
282
+ }
283
+
284
+ function hasDependency(depNames, ...names) {
285
+ return names.some(name => depNames.has(name));
286
+ }
287
+
288
+ function hasComposerDependency(composerDepNames, ...names) {
289
+ return names.some(name => composerDepNames.has(name));
290
+ }
291
+
292
+ function hasRootFile(rootEntries, ...filenames) {
293
+ return filenames.some(name => rootEntries.has(name));
294
+ }
295
+
296
+ function textMatches(text, pattern) {
297
+ return Boolean(text && pattern.test(text));
298
+ }
299
+
300
+ const LANGUAGE_DETECTORS = [
301
+ {
302
+ id: 'typescript',
303
+ detect: ctx => buildDetection('typescript', [
304
+ evidenceIf(ctx.fileExists('tsconfig.json'), 'found tsconfig.json', 4),
305
+ evidenceIf(hasDependency(ctx.depNames, 'typescript', 'ts-node', 'tsx'), 'package.json -> typescript toolchain', 2)
306
+ ])
307
+ },
308
+ {
309
+ id: 'javascript',
310
+ detect: ctx => buildDetection('javascript', [
311
+ evidenceIf(ctx.fileExists('package.json'), 'found package.json', 2),
312
+ evidenceIf(ctx.depNames.size > 0, 'package.json -> JavaScript dependency graph', 1)
313
+ ])
314
+ },
315
+ {
316
+ id: 'python',
317
+ detect: ctx => buildDetection('python', [
318
+ evidenceIf(ctx.fileExists('requirements.txt'), 'found requirements.txt', 3),
319
+ evidenceIf(ctx.fileExists('pyproject.toml'), 'found pyproject.toml', 2),
320
+ evidenceIf(textMatches(ctx.pyprojectText, /\[tool\.(poetry|uv|hatch|pdm)\]/i), 'pyproject.toml -> Python tool section', 2)
321
+ ])
322
+ },
323
+ {
324
+ id: 'go',
325
+ detect: ctx => buildDetection('go', [
326
+ evidenceIf(ctx.fileExists('go.mod'), 'found go.mod', 4)
327
+ ])
328
+ },
329
+ {
330
+ id: 'php',
331
+ detect: ctx => buildDetection('php', [
332
+ evidenceIf(ctx.fileExists('composer.json'), 'found composer.json', 4),
333
+ evidenceIf(ctx.composerDepNames.size > 0, 'composer.json -> PHP dependency graph', 1)
334
+ ])
335
+ },
336
+ {
337
+ id: 'rust',
338
+ detect: ctx => buildDetection('rust', [
339
+ evidenceIf(ctx.fileExists('Cargo.toml'), 'found Cargo.toml', 4),
340
+ evidenceIf(ctx.fileExists('src-tauri', 'Cargo.toml'), 'found src-tauri/Cargo.toml', 5)
341
+ ])
342
+ },
343
+ {
344
+ id: 'java',
345
+ detect: ctx => buildDetection('java', [
346
+ evidenceIf(ctx.fileExists('pom.xml'), 'found pom.xml', 4),
347
+ evidenceIf(ctx.fileExists('build.gradle') || ctx.fileExists('build.gradle.kts'), 'found Gradle build file', 3)
348
+ ])
349
+ },
350
+ {
351
+ id: 'ruby',
352
+ detect: ctx => buildDetection('ruby', [
353
+ evidenceIf(ctx.fileExists('Gemfile'), 'found Gemfile', 4),
354
+ evidenceIf(ctx.fileExists('config.ru'), 'found config.ru', 1)
355
+ ])
356
+ },
357
+ {
358
+ id: 'csharp',
359
+ detect: ctx => buildDetection('csharp', [
360
+ evidenceIf(ctx.rootEntries.some(name => name.endsWith('.sln') || name.endsWith('.csproj')), 'found .sln/.csproj', 4),
361
+ evidenceIf(ctx.fileExists('Program.cs'), 'found Program.cs', 1)
362
+ ])
363
+ }
364
+ ];
365
+
366
+ const FRAMEWORK_DETECTORS = [
367
+ {
368
+ id: 'next',
369
+ kind: 'frontend',
370
+ projectType: 'web-app',
371
+ detect: ctx => buildDetection('next', [
372
+ evidenceIf(hasDependency(ctx.depNames, 'next'), 'package.json -> next', 4),
373
+ evidenceIf(ctx.fileExists('next.config.js') || ctx.fileExists('next.config.mjs') || ctx.fileExists('next.config.ts'), 'found next.config.*', 3),
374
+ evidenceIf(ctx.fileExists('app') || ctx.fileExists('pages'), 'found app/ or pages/', 1)
375
+ ])
376
+ },
377
+ {
378
+ id: 'react',
379
+ kind: 'frontend',
380
+ projectType: 'web-app',
381
+ detect: ctx => buildDetection('react', [
382
+ evidenceIf(hasDependency(ctx.depNames, 'react'), 'package.json -> react', 4),
383
+ evidenceIf(hasDependency(ctx.depNames, 'react-dom'), 'package.json -> react-dom', 2)
384
+ ])
385
+ },
386
+ {
387
+ id: 'vue',
388
+ kind: 'frontend',
389
+ projectType: 'web-app',
390
+ detect: ctx => buildDetection('vue', [
391
+ evidenceIf(hasDependency(ctx.depNames, 'vue'), 'package.json -> vue', 4)
392
+ ])
393
+ },
394
+ {
395
+ id: 'nuxt',
396
+ kind: 'frontend',
397
+ projectType: 'web-app',
398
+ detect: ctx => buildDetection('nuxt', [
399
+ evidenceIf(hasDependency(ctx.depNames, 'nuxt'), 'package.json -> nuxt', 4),
400
+ evidenceIf(ctx.fileExists('nuxt.config.ts') || ctx.fileExists('nuxt.config.js'), 'found nuxt.config.*', 3)
401
+ ])
402
+ },
403
+ {
404
+ id: 'svelte',
405
+ kind: 'frontend',
406
+ projectType: 'web-app',
407
+ detect: ctx => buildDetection('svelte', [
408
+ evidenceIf(hasDependency(ctx.depNames, 'svelte', '@sveltejs/kit'), 'package.json -> svelte', 4)
409
+ ])
410
+ },
411
+ {
412
+ id: 'angular',
413
+ kind: 'frontend',
414
+ projectType: 'web-app',
415
+ detect: ctx => buildDetection('angular', [
416
+ evidenceIf(hasDependency(ctx.depNames, '@angular/core'), 'package.json -> @angular/core', 4),
417
+ evidenceIf(ctx.fileExists('angular.json'), 'found angular.json', 3)
418
+ ])
419
+ },
420
+ {
421
+ id: 'express',
422
+ kind: 'backend',
423
+ language: 'javascript',
424
+ projectType: 'api',
425
+ detect: ctx => buildDetection('express', [
426
+ evidenceIf(hasDependency(ctx.depNames, 'express'), 'package.json -> express', 4)
427
+ ])
428
+ },
429
+ {
430
+ id: 'nestjs',
431
+ kind: 'backend',
432
+ language: 'javascript',
433
+ projectType: 'api',
434
+ detect: ctx => buildDetection('nestjs', [
435
+ evidenceIf(hasDependency(ctx.depNames, '@nestjs/core'), 'package.json -> @nestjs/core', 4)
436
+ ])
437
+ },
438
+ {
439
+ id: 'fastapi',
440
+ kind: 'backend',
441
+ language: 'python',
442
+ projectType: 'api',
443
+ detect: ctx => buildDetection('fastapi', [
444
+ evidenceIf(textMatches(ctx.requirementsText, /\bfastapi\b/i) || textMatches(ctx.pyprojectText, /\bfastapi\b/i), 'Python deps -> fastapi', 4)
445
+ ])
446
+ },
447
+ {
448
+ id: 'django',
449
+ kind: 'backend',
450
+ language: 'python',
451
+ projectType: 'web-app',
452
+ detect: ctx => buildDetection('django', [
453
+ evidenceIf(textMatches(ctx.requirementsText, /\bdjango\b/i) || textMatches(ctx.pyprojectText, /\bdjango\b/i), 'Python deps -> django', 4),
454
+ evidenceIf(ctx.fileExists('manage.py'), 'found manage.py', 2),
455
+ evidenceIf(ctx.fileExists('templates'), 'found templates/', 1)
456
+ ])
457
+ },
458
+ {
459
+ id: 'flask',
460
+ kind: 'backend',
461
+ language: 'python',
462
+ projectType: 'api',
463
+ detect: ctx => buildDetection('flask', [
464
+ evidenceIf(textMatches(ctx.requirementsText, /\bflask\b/i) || textMatches(ctx.pyprojectText, /\bflask\b/i), 'Python deps -> flask', 4)
465
+ ])
466
+ },
467
+ {
468
+ id: 'laravel',
469
+ kind: 'backend',
470
+ language: 'php',
471
+ projectType: 'web-app',
472
+ detect: ctx => buildDetection('laravel', [
473
+ evidenceIf(hasComposerDependency(ctx.composerDepNames, 'laravel/framework'), 'composer.json -> laravel/framework', 4),
474
+ evidenceIf(ctx.fileExists('artisan'), 'found artisan', 3),
475
+ evidenceIf(ctx.fileExists('resources', 'views'), 'found resources/views', 1)
476
+ ])
477
+ },
478
+ {
479
+ id: 'cakephp',
480
+ kind: 'backend',
481
+ language: 'php',
482
+ projectType: 'web-app',
483
+ detect: ctx => buildDetection('cakephp', [
484
+ evidenceIf(hasComposerDependency(ctx.composerDepNames, 'cakephp/cakephp'), 'composer.json -> cakephp/cakephp', 4),
485
+ evidenceIf(ctx.fileExists('bin', 'cake'), 'found bin/cake', 3),
486
+ evidenceIf(ctx.fileExists('config', 'app.php'), 'found config/app.php', 2),
487
+ evidenceIf(ctx.fileExists('app', 'Config', 'core.php'), 'found app/Config/core.php (CakePHP legacy)', 3),
488
+ evidenceIf(ctx.fileExists('app', 'Controller', 'AppController.php'), 'found app/Controller/AppController.php', 2),
489
+ evidenceIf(ctx.fileExists('app', 'Model', 'AppModel.php'), 'found app/Model/AppModel.php', 2),
490
+ evidenceIf(ctx.fileExists('lib', 'Cake'), 'found lib/Cake', 3),
491
+ evidenceIf(ctx.fileExists('cake', 'console', 'cake'), 'found cake/console/cake', 3),
492
+ evidenceIf(ctx.fileExists('app', 'View'), 'found app/View', 2),
493
+ evidenceIf(textMatches(ctx.viewFilesText, /\.ctp$/im), 'found .ctp view templates', 2)
494
+ ])
495
+ },
496
+ {
497
+ id: 'symfony',
498
+ kind: 'backend',
499
+ language: 'php',
500
+ projectType: 'web-app',
501
+ detect: ctx => buildDetection('symfony', [
502
+ evidenceIf(hasComposerDependency(ctx.composerDepNames, 'symfony/framework-bundle'), 'composer.json -> symfony/framework-bundle', 4),
503
+ evidenceIf(ctx.fileExists('bin', 'console'), 'found bin/console', 2),
504
+ evidenceIf(ctx.fileExists('config', 'bundles.php'), 'found config/bundles.php', 1),
505
+ evidenceIf(ctx.fileExists('templates'), 'found templates/', 1)
506
+ ])
507
+ },
508
+ {
509
+ id: 'codeigniter',
510
+ kind: 'backend',
511
+ language: 'php',
512
+ projectType: 'api',
513
+ detect: ctx => buildDetection('codeigniter', [
514
+ evidenceIf(hasComposerDependency(ctx.composerDepNames, 'codeigniter4/framework'), 'composer.json -> codeigniter4/framework', 4),
515
+ evidenceIf(ctx.fileExists('spark'), 'found spark', 2),
516
+ evidenceIf(ctx.fileExists('app', 'Config', 'App.php'), 'found app/Config/App.php', 1)
517
+ ])
518
+ },
519
+ {
520
+ id: 'gin',
521
+ kind: 'backend',
522
+ language: 'go',
523
+ projectType: 'api',
524
+ detect: ctx => buildDetection('gin', [
525
+ evidenceIf(textMatches(ctx.goModText, /\bgithub\.com\/gin-gonic\/gin\b/i), 'go.mod -> github.com/gin-gonic/gin', 4)
526
+ ])
527
+ },
528
+ {
529
+ id: 'fiber',
530
+ kind: 'backend',
531
+ language: 'go',
532
+ projectType: 'api',
533
+ detect: ctx => buildDetection('fiber', [
534
+ evidenceIf(textMatches(ctx.goModText, /\bgithub\.com\/gofiber\/fiber\b/i), 'go.mod -> github.com/gofiber/fiber', 4)
535
+ ])
536
+ },
537
+ {
538
+ id: 'echo',
539
+ kind: 'backend',
540
+ language: 'go',
541
+ projectType: 'api',
542
+ detect: ctx => buildDetection('echo', [
543
+ evidenceIf(textMatches(ctx.goModText, /\bgithub\.com\/labstack\/echo\b/i), 'go.mod -> github.com/labstack/echo', 4)
544
+ ])
545
+ },
546
+ {
547
+ id: 'rails',
548
+ kind: 'backend',
549
+ language: 'ruby',
550
+ projectType: 'web-app',
551
+ detect: ctx => buildDetection('rails', [
552
+ evidenceIf(textMatches(ctx.gemfileText, /^\s*gem\s+["']rails["']/im), 'Gemfile -> rails', 4),
553
+ evidenceIf(ctx.fileExists('bin', 'rails'), 'found bin/rails', 2),
554
+ evidenceIf(ctx.fileExists('app', 'views'), 'found app/views', 1)
555
+ ])
556
+ },
557
+ {
558
+ id: 'spring',
559
+ kind: 'backend',
560
+ language: 'java',
561
+ projectType: 'api',
562
+ detect: ctx => buildDetection('spring', [
563
+ evidenceIf(textMatches(ctx.pomText, /spring-boot/i) || textMatches(ctx.gradleText, /spring-boot/i), 'Java build -> spring-boot', 4)
564
+ ])
565
+ },
566
+ {
567
+ id: 'aspnet',
568
+ kind: 'backend',
569
+ language: 'csharp',
570
+ projectType: 'api',
571
+ detect: ctx => buildDetection('aspnet', [
572
+ evidenceIf(textMatches(ctx.csprojText, /Microsoft\.NET\.Sdk\.Web/i), '.csproj -> Microsoft.NET.Sdk.Web', 4),
573
+ evidenceIf(textMatches(ctx.programCsText, /WebApplication\.CreateBuilder/i), 'Program.cs -> WebApplication.CreateBuilder', 2)
574
+ ])
575
+ },
576
+ {
577
+ id: 'tauri',
578
+ kind: 'desktop',
579
+ language: 'rust',
580
+ projectType: 'desktop',
581
+ detect: ctx => buildDetection('tauri', [
582
+ evidenceIf(hasDependency(ctx.depNames, '@tauri-apps/api'), 'package.json -> @tauri-apps/api', 4),
583
+ evidenceIf(ctx.fileExists('src-tauri', 'tauri.conf.json') || ctx.fileExists('src-tauri', 'tauri.conf.json5'), 'found src-tauri/tauri.conf.*', 4),
584
+ evidenceIf(ctx.fileExists('src-tauri', 'Cargo.toml'), 'found src-tauri/Cargo.toml', 2)
585
+ ])
586
+ },
587
+ {
588
+ id: 'axum',
589
+ kind: 'backend',
590
+ language: 'rust',
591
+ projectType: 'api',
592
+ detect: ctx => buildDetection('axum', [
593
+ evidenceIf(textMatches(ctx.cargoText, /\baxum\b/i), 'Cargo.toml -> axum', 4)
594
+ ])
595
+ },
596
+ {
597
+ id: 'actix',
598
+ kind: 'backend',
599
+ language: 'rust',
600
+ projectType: 'api',
601
+ detect: ctx => buildDetection('actix', [
602
+ evidenceIf(textMatches(ctx.cargoText, /\bactix-web\b/i), 'Cargo.toml -> actix-web', 4)
603
+ ])
604
+ },
605
+ {
606
+ id: 'flutter',
607
+ kind: 'mobile',
608
+ projectType: 'mobile',
609
+ detect: ctx => buildDetection('flutter', [
610
+ evidenceIf(textMatches(ctx.pubspecText, /^\s*flutter\s*:/im), 'pubspec.yaml -> flutter', 4)
611
+ ])
612
+ },
613
+ {
614
+ id: 'react-native',
615
+ kind: 'mobile',
616
+ projectType: 'mobile',
617
+ detect: ctx => buildDetection('react-native', [
618
+ evidenceIf(hasDependency(ctx.depNames, 'react-native', 'expo'), 'package.json -> react-native/expo', 4)
619
+ ])
620
+ }
621
+ ];
622
+
623
+ function scanProject() {
624
+ const appRoot = detectAppRoot(targetDir);
625
+ const pkg = readJsonIfExistsAt(appRoot, 'package.json') || {};
626
+ const composer = readJsonIfExistsAt(appRoot, 'composer.json') || {};
627
+ const rootEntries = fs.existsSync(appRoot) ? fs.readdirSync(appRoot) : [];
628
+ const rootCsproj = rootEntries.find(name => name.endsWith('.csproj')) || '';
629
+ const deps = {
630
+ ...(pkg.dependencies || {}),
631
+ ...(pkg.devDependencies || {}),
632
+ ...(pkg.peerDependencies || {})
633
+ };
634
+ const depNames = new Set(Object.keys(deps));
635
+ const composerDeps = {
636
+ ...(composer.require || {}),
637
+ ...(composer['require-dev'] || {})
638
+ };
639
+ const composerDepNames = new Set(Object.keys(composerDeps));
640
+ const ctx = {
641
+ pkg,
642
+ composer,
643
+ depNames,
644
+ composerDepNames,
645
+ rootEntries,
646
+ fileExists: (...parts) => fileExistsAt(appRoot, ...parts),
647
+ requirementsText: readTextIfExistsAt(appRoot, 'requirements.txt'),
648
+ pyprojectText: readTextIfExistsAt(appRoot, 'pyproject.toml'),
649
+ goModText: readTextIfExistsAt(appRoot, 'go.mod'),
650
+ pubspecText: readTextIfExistsAt(appRoot, 'pubspec.yaml'),
651
+ gemfileText: readTextIfExistsAt(appRoot, 'Gemfile'),
652
+ pomText: readTextIfExistsAt(appRoot, 'pom.xml'),
653
+ gradleText: `${readTextIfExistsAt(appRoot, 'build.gradle')}\n${readTextIfExistsAt(appRoot, 'build.gradle.kts')}`,
654
+ cargoText: `${readTextIfExistsAt(appRoot, 'Cargo.toml')}\n${readTextIfExistsAt(appRoot, 'src-tauri', 'Cargo.toml')}`,
655
+ csprojText: rootCsproj ? readTextIfExistsAt(appRoot, rootCsproj) : '',
656
+ programCsText: readTextIfExistsAt(appRoot, 'Program.cs'),
657
+ viewFilesText: fs.existsSync(path.join(appRoot, 'app', 'View'))
658
+ ? fs.readdirSync(path.join(appRoot, 'app', 'View'), { recursive: true }).join('\n')
659
+ : ''
660
+ };
661
+
662
+ const languageDetections = LANGUAGE_DETECTORS
663
+ .map(detector => detector.detect(ctx))
664
+ .filter(result => result.score >= 2)
665
+ .sort((a, b) => b.score - a.score);
666
+
667
+ const frameworkDetails = FRAMEWORK_DETECTORS
668
+ .map(detector => {
669
+ const result = detector.detect(ctx);
670
+ if (result.score < 3) return null;
671
+ return {
672
+ ...result,
673
+ kind: detector.kind,
674
+ language: detector.language,
675
+ projectType: detector.projectType,
676
+ confidence: confidenceLabel(result.score)
677
+ };
678
+ })
679
+ .filter(Boolean)
680
+ .sort((a, b) => b.score - a.score);
681
+
682
+ const frameworkLanguageScores = frameworkDetails.reduce((acc, item) => {
683
+ if (!item.language) return acc;
684
+ acc[item.language] = (acc[item.language] || 0) + item.score;
685
+ return acc;
686
+ }, {});
687
+ const primaryLanguage = Object.keys(frameworkLanguageScores).sort((a, b) => frameworkLanguageScores[b] - frameworkLanguageScores[a])[0]
688
+ || languageDetections[0]?.id
689
+ || 'typescript';
690
+ const frameworks = uniq(frameworkDetails.map(item => item.id)).filter(name => FRAMEWORK_CHOICES.includes(name));
691
+ const infrastructure = [];
692
+ let projectType = '';
693
+ let database = 'none';
694
+ let testFramework = 'auto-detect';
695
+ let cloud = 'none';
696
+ let ciCd = 'none';
697
+
698
+ if (frameworkDetails.some(item => item.projectType === 'desktop')) projectType = 'desktop';
699
+ else if (frameworkDetails.some(item => item.projectType === 'mobile')) projectType = 'mobile';
700
+ else if (frameworkDetails.some(item => item.kind === 'frontend') && frameworkDetails.some(item => item.kind === 'backend')) projectType = 'fullstack';
701
+ else if (frameworkDetails.some(item => item.projectType === 'web-app')) projectType = 'web-app';
702
+ else if (frameworkDetails.some(item => item.kind === 'backend')) projectType = 'api';
703
+ else if (frameworkDetails.some(item => item.kind === 'frontend')) projectType = 'web-app';
704
+ else if (fileExistsAt(appRoot, 'package.json') && pkg.bin) projectType = 'cli';
705
+ else if (['php', 'python', 'go', 'java', 'ruby', 'csharp', 'rust'].includes(primaryLanguage)) projectType = 'api';
706
+
707
+ if (hasDependency(depNames, 'pg', 'postgres', '@prisma/client') || /postgres/i.test(JSON.stringify(pkg))) database = 'postgres';
708
+ else if (hasDependency(depNames, 'mysql', 'mysql2')) database = 'mysql';
709
+ else if (hasDependency(depNames, 'sqlite3', 'better-sqlite3', 'rusqlite')) database = 'sqlite';
710
+ else if (hasDependency(depNames, 'mongodb', 'mongoose')) database = 'mongodb';
711
+ else if (hasDependency(depNames, 'redis', 'ioredis')) database = 'redis';
712
+ else if (hasDependency(depNames, '@aws-sdk/client-dynamodb')) database = 'dynamodb';
713
+
714
+ if (hasDependency(depNames, 'vitest')) testFramework = 'vitest';
715
+ else if (hasDependency(depNames, 'jest')) testFramework = 'jest';
716
+ else if (hasDependency(depNames, 'pytest') || textMatches(ctx.requirementsText, /\bpytest\b/i) || textMatches(ctx.pyprojectText, /\bpytest\b/i)) testFramework = 'pytest';
717
+ else if (hasComposerDependency(composerDepNames, 'phpunit/phpunit')) testFramework = 'phpunit';
718
+ else if (hasComposerDependency(composerDepNames, 'pestphp/pest')) testFramework = 'pest';
719
+ else if (fileExistsAt(appRoot, 'go.mod')) testFramework = 'go test';
720
+ else if (textMatches(ctx.gemfileText, /^\s*gem\s+["']rspec["']/im)) testFramework = 'rspec';
721
+
722
+ if (repoHasAny(targetDir, [['Dockerfile'], ['docker-compose.yml'], ['docker-compose.yaml']])) {
723
+ infrastructure.push('docker');
724
+ }
725
+
726
+ if (repoHasAny(targetDir, [['vercel.json']])) {
727
+ cloud = 'vercel';
728
+ infrastructure.push('vercel');
729
+ } else if (repoHasAny(targetDir, [['fly.toml']])) {
730
+ cloud = 'fly';
731
+ infrastructure.push('fly');
732
+ } else if (repoHasAny(targetDir, [['railway.json'], ['railway.toml']])) {
733
+ cloud = 'railway';
734
+ infrastructure.push('railway');
735
+ }
736
+
737
+ if (repoHasAny(targetDir, [['.github', 'workflows'], ['.gitlab-ci.yml'], ['.circleci', 'config.yml'], ['Jenkinsfile']], 2)) {
738
+ if (repoHasAny(targetDir, [['.github', 'workflows']], 2)) ciCd = 'github-actions';
739
+ else if (repoHasAny(targetDir, [['.gitlab-ci.yml']], 2)) ciCd = 'gitlab-ci';
740
+ else if (repoHasAny(targetDir, [['.circleci', 'config.yml']], 2)) ciCd = 'circleci';
741
+ else if (repoHasAny(targetDir, [['Jenkinsfile']], 2)) ciCd = 'jenkins';
742
+ }
743
+
744
+ const scanEvidence = [
745
+ ...(languageDetections[0] ? [`language:${languageDetections[0].id} => ${languageDetections[0].evidence.join('; ')}`] : []),
746
+ ...frameworkDetails.map(item => `${item.id} [${item.confidence}] => ${item.evidence.join('; ')}`)
747
+ ];
748
+
749
+ return {
750
+ project_name: path.basename(targetDir),
751
+ app_root: path.relative(targetDir, appRoot) || '.',
752
+ description: pkg.description || composer.description || 'A software project',
753
+ project_type: PROJECT_TYPE_CHOICES.includes(projectType) ? projectType : 'web-app',
754
+ primary_language: LANGUAGE_CHOICES.includes(primaryLanguage) ? primaryLanguage : 'typescript',
755
+ frameworks,
756
+ framework_details: frameworkDetails,
757
+ scan_evidence: scanEvidence,
758
+ database: DATABASE_CHOICES.includes(database) ? database : 'none',
759
+ package_manager: PACKAGE_MANAGER_CHOICES.includes(detectPackageManager(appRoot)) ? detectPackageManager(appRoot) : 'auto-detect',
760
+ uses_docker: infrastructure.includes('docker'),
761
+ cloud,
762
+ infrastructure: uniq(infrastructure),
763
+ ci_cd: ciCd,
764
+ test_framework: TEST_FRAMEWORK_CHOICES.includes(testFramework) ? testFramework : 'auto-detect',
765
+ code_style: 'project-standard',
766
+ architecture_notes: '<!-- Add notes -->',
767
+ current_phase: 'Initial setup',
768
+ next_phase: '-',
769
+ known_issues: '<!-- None yet -->'
770
+ };
771
+ }
772
+ // --- File Generators ---
773
+
774
+ function generateSharedAgentsMd(data) {
775
+ return `# AGENTS.md
776
+
777
+ Shared operating instructions for Codex, Antigravity, Claude Code, and Cursor.
778
+
779
+ ## Startup
780
+
781
+ 1. Read this file first.
782
+ 2. Read \`.agent/docs/project.md\` for detected stack, commands, and routing.
783
+ 3. Read \`.agent/docs/workflow.md\` for the task flow before editing.
784
+ 4. Read \`.agent/docs/architecture.md\` and \`.agent/docs/conventions.md\` before planning code changes.
785
+ 5. Read \`.agent/docs/tooling.md\` when the task needs MCP, RTK, or browser verification.
786
+
787
+ If any \`.agent/docs/*.md\` file still contains \`TODO: refine\`, update the docs by scanning the project before making product code changes.
788
+
789
+ ## Workflow
790
+
791
+ Matt Pocock's promoted skills (installed under \`.agents/skills/\` by \`thachvd-kit setup\`) are the workflow layer. Use the matching skill when a specialized workflow is useful:
792
+
793
+ - Clear, localized change: inspect -> edit -> focused verification. Do not force a heavyweight skill.
794
+ - Ambiguous feature or design: \`/grill-with-docs\`, then optionally \`/to-spec\` for a durable contract.
795
+ - Normal feature: \`/grill-with-docs\` -> \`/to-spec\` -> \`/implement\`.
796
+ - Large feature needing decomposition: add \`/to-tickets\` before \`/implement\`.
797
+ - Huge, multi-session uncertainty: \`/wayfinder\`.
798
+ - Bug or failing behavior: \`/diagnosing-bugs\`.
799
+ - Test-driven implementation: \`/tdd\` (skip the ceremony for trivial config/text changes).
800
+ - Architecture survey: \`/improve-codebase-architecture\`; use \`/codebase-design\` as the design vocabulary.
801
+ - Pre-completion review: \`/code-review\` once when code risk warrants it.
802
+
803
+ If unsure which skill fits, use \`/ask-matt\`.
804
+
805
+ Questions and research do not edit product code. A fast path is allowed only when the change is localized, mechanically obvious, low-risk, and has focused verification; file count alone is not a gate. State the scope and verification before editing. If the task becomes ambiguous or its behavior, contract, or risk surface grows, use the matching planning skill above instead.
806
+
807
+ See \`.agent/docs/workflow.md\` for the full route matrix. This kit does not implement a second workflow engine; it only installs and checks the promoted skill set (\`thachvd-kit skills install|check|update\`).
808
+
809
+ ${QUALITY_FLOOR_SECTION}
810
+
811
+ ${AGENT_RULES_SECTION}
812
+
813
+ ## Shared Knowledge
814
+
815
+ - Project context: \`.agent/docs/\`
816
+ - Tooling setup: \`.agent/docs/tooling.md\`
817
+ - Matt Pocock skills: promoted set installed under \`.agents/skills/\` via \`thachvd-kit skills install\`
818
+ - RTK: recommended local CLI for compact command output
819
+ - Antigravity and Cursor entry rules: \`GEMINI.md\` and \`.cursorrules\`
820
+ `;
821
+ }
822
+
823
+ function generateIndexProjectPrompt() {
824
+ return `# Project Indexing Prompt
825
+
826
+ Read CLAUDE.md (if using Claude Code), AGENTS.md, and all files under .agent/docs/.
827
+ Use the project's Matt Pocock skills for workflow (see AGENTS.md; run /ask-matt if unsure which fits) and use codebase-memory MCP for structural code discovery when available.
828
+ Ask me what this project does and any important conventions I want preserved.
829
+ Then scan this repository and refine the generated context docs with factual rules.
830
+ Do not implement product code during onboarding.
831
+ `;
832
+ }
833
+
834
+ function printProjectPrompt() {
835
+ const promptPath = path.join(targetDir, '.agent', 'docs', 'index-project-prompt.md');
836
+ if (!fs.existsSync(promptPath)) {
837
+ console.error(pc.yellow('No saved project prompt found. Run thachvd-kit init first.'));
838
+ process.exitCode = 1;
839
+ return;
840
+ }
841
+ console.log(fs.readFileSync(promptPath, 'utf8'));
842
+ }
843
+
844
+ function generateSharedClaudeMd() {
845
+ return `@AGENTS.md
846
+
847
+ ## Claude Code
848
+
849
+ This repository uses AGENTS.md as the shared cross-agent entry file. Follow the imported instructions and keep Claude-specific notes here only when they cannot apply to Codex or Antigravity.
850
+ `;
851
+ }
852
+
853
+ function generateSharedGeminiMd() {
854
+ return `# GEMINI.md
855
+
856
+ Antigravity entry file for this project.
857
+
858
+ Read AGENTS.md first, then follow the shared docs under .agent/docs/.
859
+ `;
860
+ }
861
+
862
+ function generateSharedCursorrules(data) {
863
+ return `# Cursor Rules
864
+
865
+ This repository uses AGENTS.md as the shared cross-agent entry file. Follow the instructions in AGENTS.md and keep Cursor-specific notes here only when they cannot apply to Codex, Antigravity, or Claude Code.
866
+
867
+ Read AGENTS.md first, then follow the shared docs under .agent/docs/.
868
+
869
+ ---
870
+
871
+ ${generateSharedAgentsMd(data).trimEnd()}
872
+ `;
873
+ }
874
+
875
+ function resolveCommands(data) {
876
+ const frameworks = data.frameworks || [];
877
+ const primaryLang = data.primary_language || 'javascript';
878
+ const pkgManager = data.package_manager || 'npm';
879
+ const testFramework = data.test_framework || 'auto-detect';
880
+
881
+ const isPython = primaryLang === 'python';
882
+ const isGo = primaryLang === 'go';
883
+ const isRust = primaryLang === 'rust';
884
+ const isPhp = primaryLang === 'php';
885
+
886
+ let install = 'npm install';
887
+ let dev = 'npm run dev';
888
+ let build = 'npm run build';
889
+ let test = 'npm test';
890
+ let lint = 'npm run lint';
891
+
892
+ if (pkgManager === 'pnpm') { install = 'pnpm install'; dev = 'pnpm dev'; build = 'pnpm build'; test = 'pnpm test'; lint = 'pnpm lint'; }
893
+ else if (pkgManager === 'yarn') { install = 'yarn'; dev = 'yarn dev'; build = 'yarn build'; test = 'yarn test'; lint = 'yarn lint'; }
894
+ else if (pkgManager === 'bun') { install = 'bun install'; dev = 'bun dev'; build = 'bun run build'; test = 'bun test'; lint = 'bun run lint'; }
895
+ else if (isPython) { install = 'pip install -r requirements.txt'; dev = 'python manage.py runserver'; build = '# no build step'; test = 'pytest'; lint = 'ruff check .'; }
896
+ else if (isGo) { install = 'go mod download'; dev = 'go run .'; build = 'go build ./...'; test = 'go test ./...'; lint = 'golangci-lint run'; }
897
+ else if (isRust) { install = 'cargo build'; dev = 'cargo run'; build = 'cargo build --release'; test = 'cargo test'; lint = 'cargo clippy'; }
898
+ else if (isPhp) {
899
+ install = 'composer install';
900
+ build = '# no build step';
901
+ test = testFramework === 'pest' ? './vendor/bin/pest' : './vendor/bin/phpunit';
902
+ const isCakePHP = frameworks.some(f => f.toLowerCase().includes('cakephp'));
903
+ const isSymfony = frameworks.some(f => f.toLowerCase().includes('symfony'));
904
+ const isCodeIgniter = frameworks.some(f => f.toLowerCase().includes('codeigniter'));
905
+ if (isCakePHP) {
906
+ dev = 'bin/cake server';
907
+ lint = './vendor/bin/phpcs';
908
+ } else if (isSymfony) {
909
+ dev = 'bin/console server:run';
910
+ lint = './vendor/bin/phpcs';
911
+ } else if (isCodeIgniter) {
912
+ dev = 'php spark serve';
913
+ lint = './vendor/bin/phpcs';
914
+ } else {
915
+ dev = 'php artisan serve';
916
+ lint = 'php artisan pint';
917
+ }
918
+ }
919
+
920
+ if (testFramework === 'vitest') test = `${pkgManager === 'pnpm' ? 'pnpm' : pkgManager === 'yarn' ? 'yarn' : pkgManager === 'bun' ? 'bun run' : 'npx'} vitest`;
921
+ else if (testFramework === 'jest') test = `${pkgManager === 'pnpm' ? 'pnpm' : pkgManager === 'yarn' ? 'yarn' : 'npx'} jest`;
922
+
923
+ return { install, dev, build, test, lint };
924
+ }
925
+
926
+ function generateProjectDoc(data) {
927
+ const commands = resolveCommands(data);
928
+ const evidence = (data.scan_evidence || []).length > 0
929
+ ? (data.scan_evidence || []).map(line => `- ${line}`).join('\n')
930
+ : '- TODO: refine scan evidence by inspecting the project.';
931
+
932
+ return `# Project Rules
933
+
934
+ Generated by thachvd-kit on ${new Date().toISOString().split('T')[0]}.
935
+
936
+ ## Summary
937
+
938
+ - Name: ${data.project_name}
939
+ - Description: ${data.description}
940
+ - Type: ${data.project_type}
941
+ - App root: ${data.app_root || '.'}
942
+
943
+ ## Stack
944
+
945
+ - Language: ${data.primary_language}
946
+ - Frameworks: ${(data.frameworks || []).join(', ') || 'none'}
947
+ - Database: ${data.database || 'none'}
948
+ - Infrastructure: ${(data.infrastructure || []).join(', ') || 'none'}
949
+ - Package manager: ${data.package_manager || 'auto-detect'}
950
+ - Test framework: ${data.test_framework || 'auto-detect'}
951
+
952
+ ## Commands
953
+
954
+ - Install: \`${commands.install}\`
955
+ - Dev: \`${commands.dev}\`
956
+ - Build: \`${commands.build}\`
957
+ - Test: \`${commands.test}\`
958
+ - Lint: \`${commands.lint}\`
959
+
960
+ ## Available Integrations
961
+
962
+ Use these integrations when available. Run thachvd-kit doctor to check the current machine.
963
+
964
+ | MCP | When to Use |
965
+ |-----|-------------|
966
+ | \`codebase-memory-mcp\` | Explore symbols, trace call chains, architecture, and impact |
967
+ | \`context7\` | Look up library/framework docs, API signatures, migration guides |
968
+ | \`playwright\` | Verify UI behavior, take screenshots, test forms and navigation |
969
+ | \`rtk\` | Recommended proxy for noisy git, tests, builds, and package commands |
970
+
971
+ > Check \`.agent/docs/tooling.md\` for setup details and additional MCP servers.
972
+
973
+ ## Scan Evidence
974
+
975
+ ${evidence}
976
+ `;
977
+ }
978
+
979
+ function generateArchitectureDoc(data) {
980
+ const appRootLine = data.app_root && data.app_root !== '.'
981
+ ? `- App root is currently detected as \`${data.app_root}\`.`
982
+ : '- App root is the repository root unless refined below.';
983
+ const evidence = (data.scan_evidence || []).length > 0
984
+ ? (data.scan_evidence || []).map(line => `- ${line}`).join('\n')
985
+ : '- TODO: refine architecture by scanning source directories.';
986
+
987
+ return `# Architecture Notes
988
+
989
+ ## Current Map
990
+
991
+ ${appRootLine}
992
+ - TODO: refine major directories and responsibilities.
993
+ - TODO: refine main entry points, routing, state boundaries, and integration boundaries.
994
+
995
+ ## Detected Evidence
996
+
997
+ ${evidence}
998
+
999
+ ## AI Maintenance Rule
1000
+
1001
+ Before non-trivial implementation work, inspect this file. If it is stale or still contains TODO items relevant to the task, update it from the code before editing product code.
1002
+ `;
1003
+ }
1004
+
1005
+ function generateConventionsDoc() {
1006
+ return `# Coding Conventions
1007
+
1008
+ ${CONVENTIONS_STANDARDS_SECTION}
1009
+
1010
+ ${QUALITY_FLOOR_SECTION}
1011
+
1012
+ ## Verification
1013
+
1014
+ - Run the focused test or lint command that matches the touched area.
1015
+ - If no automated check exists, document the manual verification performed.
1016
+ - For UI/web changes, prioritize using Playwright MCP to automatically verify client-side behavior and capture screenshots.
1017
+ - Do not claim completion without verification evidence.
1018
+ `;
1019
+ }
1020
+
1021
+ function generateWorkflowDoc() {
1022
+ return `# Agent Workflow
1023
+
1024
+ This project uses Matt Pocock's promoted skills plus thachvd-kit's native workflow skills (installed under .agents/skills/ and .claude/skills/ by \`thachvd-kit setup\`) as the workflow layer. Native skills add opt-in workspace isolation, subagent-driven execution, and branch finishing; they do not replace Matt's planning, implementation, testing, or review skills.
1025
+
1026
+ ## Before Every Task
1027
+
1028
+ 1. Read AGENTS.md and the relevant files under .agent/docs/.
1029
+ 2. Use the promoted skill that matches the request, or run /ask-matt if unsure which one fits.
1030
+ 3. Use codebase-memory MCP for structural discovery when available.
1031
+ 4. Make changes only after the required planning gate for that situation.
1032
+ 5. Verify the change and request review before completion.
1033
+
1034
+ ## Route Matrix
1035
+
1036
+ | Situation | Skill | Gate |
1037
+ |---|---|---|
1038
+ | Question or research only | direct answer or research | no product-code edits |
1039
+ | Clear, localized change | fast path (no skill) | inspect -> edit -> focused verify |
1040
+ | Ambiguous feature or design | /grill-with-docs, then optionally /to-spec | durable contract before implementation when useful |
1041
+ | Normal feature | /grill-with-docs -> /to-spec -> choose workspace -> /implement | spec agreed before implementation |
1042
+ | Large feature needing decomposition | /grill-with-docs -> /to-spec -> /to-tickets -> choose workspace -> executor | tickets agreed before implementation |
1043
+ | Huge, multi-session uncertainty | /wayfinder | shared decision map before implementation |
1044
+ | Bug or failing behavior | /diagnosing-bugs | reproduce -> root cause -> regression protection -> fix -> verify |
1045
+ | Test-driven implementation | /tdd | red -> green -> refactor per slice |
1046
+ | Architecture survey | /improve-codebase-architecture (+ /codebase-design vocabulary) | findings reviewed before large refactors |
1047
+ | Pre-merge | /code-review | blocking findings resolved |
1048
+
1049
+ Not sure which row applies? Run /ask-matt instead of guessing.
1050
+
1051
+ ## Standard Feature Flow
1052
+
1053
+ /grill-with-docs -> /to-spec -> (/to-tickets for large work) -> choose workspace -> choose executor (/implement or /subagent-driven-development) -> /code-review -> /finishing-a-development-branch.
1054
+
1055
+ Choose the current branch/workspace when the user explicitly wants to work there. Use /using-git-worktrees only when isolation is requested or appropriate workspace isolation already exists. SDD is for plans with multiple relatively independent tasks and falls back to /implement when subagent dispatch is unavailable; it never forces a worktree.
1056
+
1057
+ ${FAST_PATH_SECTION}
1058
+
1059
+ ## Documentation Maintenance
1060
+
1061
+ - Update architecture.md when structure or boundaries change.
1062
+ - Update conventions.md when repeated project patterns become clear.
1063
+ - Update project.md when stack, scripts, or test tooling changes.
1064
+ - Keep AGENTS.md concise and put project-specific detail under .agent/docs/.
1065
+ `;
1066
+ }
1067
+
1068
+ function generateGettingStartedDoc() {
1069
+ return `# Getting Started
1070
+
1071
+ thachvd-kit creates project context. Matt Pocock's promoted and thachvd-kit native skills own the development workflow.
1072
+
1073
+ ## Setup
1074
+
1075
+ 1. Run \`thachvd-kit init\` in the repository.
1076
+ 2. Open or print \`.agent/docs/index-project-prompt.md\` with \`thachvd-kit prompt\`.
1077
+ 3. Run \`thachvd-kit setup\` (installs promoted and native skill sets by default; add \`--no-install-skills\` to skip) and RTK.
1078
+ 4. Inside the AI client, run \`/setup-matt-pocock-skills\` once to configure the issue tracker, triage labels, and doc layout.
1079
+ 5. Run \`thachvd-kit doctor\` and restart the AI client.
1080
+
1081
+ ## Standard Flow
1082
+
1083
+ 1. \`/grill-with-docs\` for unclear requirements or design.
1084
+ 2. \`/to-spec\` when a durable implementation contract is useful.
1085
+ 3. \`/to-tickets\` for large work that benefits from decomposition.
1086
+ 4. \`/implement\`, with \`/tdd\` where red-green-refactor helps.
1087
+ 5. \`/code-review\` once before completion.
1088
+
1089
+ Not sure which skill fits? Run \`/ask-matt\`.
1090
+
1091
+ ## Bug Flow
1092
+
1093
+ Use \`/diagnosing-bugs\`: reproduce the symptom, inspect the path with codebase-memory MCP, test hypotheses, add regression protection, fix the root cause, and verify.
1094
+
1095
+ ${GETTING_STARTED_FAST_PATH_SECTION}
1096
+ `;
1097
+ }
1098
+
1099
+ function resolveToolingSetup(data) {
1100
+ const primaryLang = data.primary_language || 'javascript';
1101
+ const packageManager = data.package_manager || 'npm';
1102
+ const isPython = primaryLang === 'python';
1103
+ const runner = packageManager === 'pnpm'
1104
+ ? 'pnpm dlx'
1105
+ : packageManager === 'yarn'
1106
+ ? 'yarn dlx'
1107
+ : packageManager === 'bun'
1108
+ ? 'bunx'
1109
+ : 'npx';
1110
+ const playwrightCommand = isPython
1111
+ ? 'pip install playwright && playwright install chromium'
1112
+ : `${runner} playwright install`;
1113
+
1114
+ return { runner, playwrightCommand };
1115
+ }
1116
+
1117
+ function generateToolingDoc(data) {
1118
+ const { runner, playwrightCommand } = resolveToolingSetup(data);
1119
+
1120
+ return `# Optional Tooling
1121
+
1122
+ Run \`thachvd-kit doctor\` to check the current machine.
1123
+
1124
+ ## Matt Pocock Skills + Native Workflow Skills
1125
+
1126
+ Matt Pocock's promoted engineering and productivity skills are installed project-locally under \`.agents/skills/\` (never globally). thachvd-kit also bundles native workflow skills to both \`.agents/skills/\` and \`.claude/skills/\`:
1127
+
1128
+ - Install the promoted set: \`thachvd-kit skills install\` (\`--dry-run\` to preview the command without running it)
1129
+ - Check what is installed: \`thachvd-kit skills check\` (read-only)
1130
+ - Update the promoted set: \`thachvd-kit skills update\` (re-adds each skill individually for safety, so it is slower than install — one network clone per skill)
1131
+ - The full manifest lives in \`bin/matt-skills.js\` (\`PROMOTED_SKILLS\`); it mirrors upstream's \`skills/engineering/\` + \`skills/productivity/\` catalog and never includes \`in-progress\`, \`misc\`, or \`deprecated\` skills.
1132
+ - After the first install, run \`/setup-matt-pocock-skills\` once inside the AI client to configure the issue tracker, triage labels, and generated docs location. thachvd-kit does not simulate that skill.
1133
+ - Unsure which skill fits a task? Run \`/ask-matt\`.
1134
+ - Native skills: \`/using-git-worktrees\` (opt-in isolation), \`/subagent-driven-development\` (alternative executor for multi-task plans), and \`/finishing-a-development-branch\`.
1135
+ - SDD's upstream shell scripts are retained for provenance; cross-platform Node entrypoints are used by the adapted workflow on Windows.
1136
+
1137
+ ${TOOL_ROUTING_SECTION}
1138
+
1139
+ ## Codebase Memory MCP
1140
+
1141
+ Use \`codebase-memory-mcp\` for symbol search, call paths, architecture, and impact analysis.
1142
+
1143
+ - Install: \`npm install -g codebase-memory-mcp\`
1144
+ - Index a repository: \`codebase-memory-mcp cli index_repository --repo-path .\`
1145
+ - Keep local index state out of git unless the team explicitly chooses a shared snapshot.
1146
+
1147
+ ## RTK
1148
+
1149
+ RTK is the recommended command-output layer, not a workflow engine. Prefer it by default for verbose shell commands.
1150
+
1151
+ - Install: \`cargo install --git https://github.com/rtk-ai/rtk --branch master rtk\`
1152
+ - Examples: \`rtk git status\`, \`rtk npm test\`, \`rtk git log\`.
1153
+ - If it is unavailable, run the underlying command directly.
1154
+
1155
+ ## Playwright
1156
+
1157
+ - Check: \`${runner} playwright --version\`
1158
+ - Browser install: \`${playwrightCommand}\`
1159
+ - Use the project's existing Playwright scripts when available.
1160
+ `;
1161
+ }
1162
+
1163
+ async function writeGeneratedFile(relativePath, content, assumeYes) {
1164
+ const outputPath = path.join(targetDir, relativePath);
1165
+ const exists = fs.existsSync(outputPath);
1166
+ if (exists && !assumeYes) {
1167
+ const response = await prompts({
1168
+ type: 'confirm',
1169
+ name: 'overwrite',
1170
+ message: `File ${pc.bold(relativePath)} already exists. Do you want to overwrite it?`,
1171
+ initial: false
1172
+ });
1173
+ if (response.overwrite === undefined) {
1174
+ console.log(pc.yellow('\nCancelled.'));
1175
+ process.exit(130);
1176
+ }
1177
+ if (!response.overwrite) return false;
1178
+ }
1179
+
1180
+ fs.mkdirSync(path.dirname(outputPath), { recursive: true });
1181
+ fs.writeFileSync(outputPath, content, 'utf8');
1182
+ return true;
1183
+ }
1184
+
1185
+ function homePath(...parts) {
1186
+ const home = process.env.USERPROFILE || process.env.HOME;
1187
+ return home ? path.join(home, ...parts) : null;
1188
+ }
1189
+
1190
+ function printMattSkillsSetupReminder() {
1191
+ console.log(` Run ${pc.bold('/setup-matt-pocock-skills')} once inside your AI client to configure the issue tracker, triage labels, and generated docs location.`);
1192
+ }
1193
+
1194
+ function runDoctor() {
1195
+ console.log(pc.bold(pc.cyan('thachvd-kit doctor')));
1196
+ const requiredFiles = ['AGENTS.md', 'CLAUDE.md', 'GEMINI.md', '.cursorrules',
1197
+ path.join('.agent', 'docs', 'project.md'), path.join('.agent', 'docs', 'workflow.md')];
1198
+ for (const relativePath of requiredFiles) {
1199
+ const ok = fs.existsSync(path.join(targetDir, relativePath));
1200
+ console.log(` ${ok ? pc.green('OK') : pc.yellow('WARN')} ${relativePath}`);
1201
+ }
1202
+
1203
+ const tools = [
1204
+ [CODEBASE_MEMORY_BINARY, `npm install -g ${CODEBASE_MEMORY_PACKAGE}`],
1205
+ ['rtk', `recommended: ${RTK_INSTALL_COMMAND}`]
1206
+ ];
1207
+ console.log(`\n${pc.bold('Local integrations (RTK recommended)')}`);
1208
+ for (const [binary, hint] of tools) {
1209
+ const ok = commandExists(binary);
1210
+ console.log(` ${ok ? pc.green('OK') : pc.yellow('WARN')} ${binary}${ok ? '' : ` - ${hint}`}`);
1211
+ }
1212
+
1213
+ console.log(`\n${pc.bold('Matt Pocock skills')}`);
1214
+ const skillStatus = mattSkills.checkSkills(targetDir);
1215
+ if (skillStatus.missing.length === 0) {
1216
+ console.log(` ${pc.green('OK')} promoted skill set installed (${skillStatus.total}/${skillStatus.total})`);
1217
+ } else {
1218
+ console.log(` ${pc.yellow('WARN')} ${skillStatus.missing.length} promoted skill(s) missing`);
1219
+ console.log(` Run: ${pc.bold('thachvd-kit skills install')}`);
1220
+ }
1221
+ const setupSkillOk = mattSkills.isSkillInstalled(targetDir, mattSkills.SETUP_SKILL_NAME);
1222
+ console.log(` ${setupSkillOk ? pc.green('OK') : pc.yellow('WARN')} ${mattSkills.SETUP_SKILL_NAME} available`);
1223
+ console.log(`\n${pc.bold('thachvd-kit native skills')}`);
1224
+ const nativeStatus = nativeSkills.checkNativeSkills(targetDir);
1225
+ for (const name of nativeSkills.NATIVE_SKILLS) {
1226
+ const status = nativeStatus.skills[name];
1227
+ console.log(` ${status.agents && status.claude ? pc.green('OK') : pc.yellow('WARN')} ${name} (.agents: ${status.agents ? 'installed' : 'missing'}, .claude: ${status.claude ? 'installed' : 'missing'})`);
1228
+ }
1229
+ for (const surface of nativeSkills.NATIVE_SKILL_SURFACES) {
1230
+ const support = nativeStatus.surfaces[surface.id].support;
1231
+ const status = support.missing.length === 0 ? pc.green('OK') : pc.yellow('WARN');
1232
+ console.log(` ${status} support files (${surface.relativeDir}: ${support.missing.length === 0 ? 'installed' : 'missing'})`);
1233
+ }
1234
+ const contextMdExists = fs.existsSync(path.join(targetDir, 'CONTEXT.md'));
1235
+ const docsAdrExists = fs.existsSync(path.join(targetDir, 'docs', 'adr'));
1236
+ console.log(` ${pc.dim('-')} CONTEXT.md ${contextMdExists ? 'detected' : 'not created yet (informational only)'}`);
1237
+ console.log(` ${pc.dim('-')} docs/adr ${docsAdrExists ? 'detected' : 'not created yet (informational only)'}`);
1238
+
1239
+ console.log(`\n${pc.bold('Next step')}`);
1240
+ console.log(' Run thachvd-kit prompt to print the saved project-indexing prompt.');
1241
+ }
1242
+
1243
+ async function runSetup(args) {
1244
+ const skipMcpSetup = args.includes('--no-setup-mcp');
1245
+ const skipHookSetup = args.includes('--no-setup-hook');
1246
+ const skipRtkInstall = args.includes('--no-install-rtk');
1247
+ const skipIndex = args.includes('--no-index');
1248
+ const skipSkillsInstall = args.includes('--no-install-skills');
1249
+ console.log(pc.bold(pc.cyan('thachvd-kit setup')));
1250
+ if (!skipSkillsInstall) {
1251
+ console.log(pc.bold('Matt Pocock skills'));
1252
+ const skillStatus = mattSkills.checkSkills(targetDir);
1253
+ if (skillStatus.missing.length === 0) {
1254
+ // Already fully installed: `setup` only ensures presence, it
1255
+ // never re-installs or updates on its own. Explicit
1256
+ // `thachvd-kit skills update` is required for that.
1257
+ console.log(` ${pc.green('OK')} promoted skill set already installed (${skillStatus.total}/${skillStatus.total})`);
1258
+ } else {
1259
+ const skillsResult = mattSkills.installSkills(targetDir);
1260
+ const skillsMarker = skillsResult.ok ? pc.green('OK') : pc.yellow('WARN');
1261
+ console.log(` ${skillsMarker} ${skillsResult.message}`);
1262
+ }
1263
+ printMattSkillsSetupReminder();
1264
+ console.log(`\n${pc.bold('thachvd-kit native skills')}`);
1265
+ const nativeStatus = nativeSkills.checkNativeSkills(targetDir);
1266
+ if (nativeStatus.allInstalled) {
1267
+ console.log(` ${pc.green('OK')} native skill set already installed (${nativeStatus.total}/${nativeStatus.total})`);
1268
+ } else {
1269
+ const nativeResult = nativeSkills.installNativeSkills(targetDir);
1270
+ const nativeMarker = nativeResult.ok ? pc.green('OK') : pc.yellow('WARN');
1271
+ console.log(` ${nativeMarker} ${nativeResult.message}`);
1272
+ }
1273
+ } else {
1274
+ console.log(`${pc.bold('Matt Pocock skills')}\n ${pc.dim('skipped (--no-install-skills)')}`);
1275
+ console.log(`${pc.bold('thachvd-kit native skills')}\n ${pc.dim('skipped (--no-install-skills)')}`);
1276
+ }
1277
+ if (!skipMcpSetup) {
1278
+ console.log(pc.bold('MCP setup'));
1279
+ for (const result of setupMcpServers({ install: true })) {
1280
+ const marker = result.ok ? pc.green('OK') : pc.yellow('WARN');
1281
+ console.log(` ${marker} ${result.message}`);
1282
+ }
1283
+ if (!skipIndex) {
1284
+ const indexResult = indexRepository();
1285
+ const marker = indexResult.ok ? pc.green('OK') : pc.yellow('WARN');
1286
+ console.log(` ${marker} ${indexResult.message}`);
1287
+ }
1288
+ }
1289
+ if (!skipHookSetup) {
1290
+ console.log(`\n${pc.bold('Claude Code Definition-of-Done hook')}`);
1291
+ const claudeProjectSettings = path.join(targetDir, '.claude', 'settings.json');
1292
+ const result = mergeClaudeProjectHooks(claudeProjectSettings);
1293
+ const marker = result.ok ? pc.green('OK') : pc.yellow('WARN');
1294
+ console.log(` ${marker} ${result.message}`);
1295
+ console.log(' This hook only checks for plan/review/verification evidence; it never prescribes which skill produces it.');
1296
+ }
1297
+ console.log(`\n${pc.bold('RTK (recommended)')}`);
1298
+ const rtkResult = ensureRtkInstalled(!skipRtkInstall);
1299
+ const rtkMarker = rtkResult.ok ? pc.green('OK') : pc.yellow('WARN');
1300
+ console.log(` ${rtkMarker} ${rtkResult.message}`);
1301
+ console.log('\nRestart your AI client after installing plugins or changing MCP configuration.');
1302
+ console.log('Your onboarding prompt is saved at .agent/docs/index-project-prompt.md. Run thachvd-kit prompt to print it again.');
1303
+ }
1304
+
1305
+ function commandExists(command) {
1306
+ const checker = process.platform === 'win32' ? 'where' : 'command';
1307
+ const args = process.platform === 'win32' ? [command] : ['-v', command];
1308
+ const result = spawnSync(checker, args, { encoding: 'utf8', shell: process.platform !== 'win32' });
1309
+ return result.status === 0;
1310
+ }
1311
+
1312
+ function ensureGlobalNpmPackage(binaryName, packageName, install = false) {
1313
+ if (commandExists(binaryName)) {
1314
+ return { ok: true, message: `${binaryName} already installed` };
1315
+ }
1316
+ if (!install) {
1317
+ return { ok: false, message: `${binaryName} is missing; run: npm install -g ${packageName}` };
1318
+ }
1319
+ if (!commandExists('npm')) {
1320
+ return { ok: false, message: `npm not found; run: npm install -g ${packageName}` };
1321
+ }
1322
+ const result = spawnSync('npm', ['install', '-g', packageName], {
1323
+ encoding: 'utf8',
1324
+ stdio: 'pipe',
1325
+ shell: process.platform === 'win32'
1326
+ });
1327
+ if (result.status !== 0) {
1328
+ return { ok: false, message: `failed to install ${binaryName}; run: npm install -g ${packageName}` };
1329
+ }
1330
+ return { ok: true, message: `installed ${binaryName}` };
1331
+ }
1332
+
1333
+ function ensureCodebaseMemoryCli(install = false) {
1334
+ return ensureGlobalNpmPackage(CODEBASE_MEMORY_BINARY, CODEBASE_MEMORY_PACKAGE, install);
1335
+ }
1336
+
1337
+ function ensureRtkInstalled(install = false) {
1338
+ if (commandExists('rtk')) {
1339
+ return { ok: true, message: 'rtk already installed' };
1340
+ }
1341
+ if (!install) {
1342
+ return { ok: false, message: `rtk is missing; run: ${RTK_INSTALL_COMMAND}` };
1343
+ }
1344
+ if (!commandExists('cargo')) {
1345
+ return { ok: false, message: `cargo not found; install Rust, then run: ${RTK_INSTALL_COMMAND}` };
1346
+ }
1347
+ console.log(pc.dim(' Installing rtk via cargo (compiles from source; this can take a few minutes)...'));
1348
+ const result = spawnSync('cargo', ['install', '--git', 'https://github.com/rtk-ai/rtk', '--branch', 'master', 'rtk'], {
1349
+ encoding: 'utf8',
1350
+ stdio: 'inherit',
1351
+ shell: process.platform === 'win32'
1352
+ });
1353
+ if (result.status !== 0) {
1354
+ return { ok: false, message: `failed to install rtk; run: ${RTK_INSTALL_COMMAND}` };
1355
+ }
1356
+ return { ok: true, message: 'installed rtk' };
1357
+ }
1358
+
1359
+ function indexRepository() {
1360
+ if (!commandExists(CODEBASE_MEMORY_BINARY)) {
1361
+ return { ok: false, message: `${CODEBASE_MEMORY_BINARY} is not installed; skipped indexing` };
1362
+ }
1363
+ console.log(pc.dim(` Running ${CODEBASE_MEMORY_BINARY} cli index_repository --repo-path . (first index of a large repo can take a while)...`));
1364
+ const result = spawnSync(CODEBASE_MEMORY_BINARY, ['cli', 'index_repository', '--repo-path', '.'], {
1365
+ cwd: targetDir,
1366
+ encoding: 'utf8',
1367
+ stdio: 'inherit',
1368
+ shell: process.platform === 'win32'
1369
+ });
1370
+ if (result.status !== 0) {
1371
+ return { ok: false, message: `indexing failed; run manually: ${CODEBASE_MEMORY_BINARY} cli index_repository --repo-path .` };
1372
+ }
1373
+ return { ok: true, message: 'indexed repository with codebase-memory-mcp' };
1374
+ }
1375
+
1376
+ function readJsonFile(filePath) {
1377
+ if (!fs.existsSync(filePath)) return {};
1378
+ try {
1379
+ return JSON.parse(fs.readFileSync(filePath, 'utf8'));
1380
+ } catch {
1381
+ return null;
1382
+ }
1383
+ }
1384
+
1385
+ function writeJsonFile(filePath, data) {
1386
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
1387
+ fs.writeFileSync(filePath, JSON.stringify(data, null, 2) + '\n', 'utf8');
1388
+ }
1389
+
1390
+ function mergeMcpJson(filePath, withType = false) {
1391
+ const data = readJsonFile(filePath);
1392
+ if (data === null) {
1393
+ return { ok: false, message: `${filePath} is not valid JSON; skipped` };
1394
+ }
1395
+ data.mcpServers = data.mcpServers || {};
1396
+ const baseServers = {
1397
+ 'codebase-memory-mcp': { command: CODEBASE_MEMORY_BINARY, args: [] },
1398
+ playwright: { command: 'playwright-mcp' },
1399
+ context7: { command: 'context7-mcp' }
1400
+ };
1401
+ if (data.permissions && Array.isArray(data.permissions.allow)) {
1402
+ const requiredPermissions = [
1403
+ "mcp__context7__resolve-library-id",
1404
+ "mcp__context7__query-docs"
1405
+ ];
1406
+ for (const perm of requiredPermissions) {
1407
+ if (!data.permissions.allow.includes(perm)) {
1408
+ data.permissions.allow.push(perm);
1409
+ }
1410
+ }
1411
+ }
1412
+ for (const [name, server] of Object.entries(baseServers)) {
1413
+ if (!data.mcpServers[name]) {
1414
+ data.mcpServers[name] = withType ? { type: 'stdio', ...server } : server;
1415
+ }
1416
+ }
1417
+ writeJsonFile(filePath, data);
1418
+ const tools = Object.keys(baseServers).join(', ');
1419
+ return { ok: true, message: `configured ${tools} MCP in ${filePath}` };
1420
+ }
1421
+
1422
+ function appendMcpTomlIfMissing(filePath) {
1423
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
1424
+ const existing = fs.existsSync(filePath) ? fs.readFileSync(filePath, 'utf8') : '';
1425
+ const blocks = [];
1426
+ if (!/\[mcp_servers\.context7\]/.test(existing)) {
1427
+ blocks.push(`[mcp_servers.context7]
1428
+ command = "context7-mcp"
1429
+ startup_timeout_sec = 20
1430
+ tool_timeout_sec = 120
1431
+ `);
1432
+ }
1433
+ if (!/\[mcp_servers\.codebase-memory-mcp\]/.test(existing)) {
1434
+ blocks.push(`[mcp_servers.codebase-memory-mcp]
1435
+ command = "${CODEBASE_MEMORY_BINARY}"
1436
+ startup_timeout_sec = 20
1437
+ tool_timeout_sec = 120
1438
+ `);
1439
+ }
1440
+ if (!/\[mcp_servers\.playwright\]/.test(existing)) {
1441
+ blocks.push(`[mcp_servers.playwright]
1442
+ command = "playwright-mcp"
1443
+ startup_timeout_sec = 20
1444
+ tool_timeout_sec = 120
1445
+ `);
1446
+ }
1447
+ if (blocks.length === 0) { return { ok: true, message: `already configured ${filePath}` };
1448
+ }
1449
+ const separator = existing && !existing.endsWith('\n') ? '\n\n' : existing ? '\n' : '';
1450
+ fs.writeFileSync(filePath, existing + separator + blocks.join('\n'), 'utf8');
1451
+ return { ok: true, message: `configured codebase-memory-mcp, context7, playwright MCP in ${filePath}` };
1452
+ }
1453
+
1454
+ const DOD_HOOK_MARKER = 'feature/refactor Definition-of-Done gate';
1455
+
1456
+ function generateDodStopHookPrompt() {
1457
+ return `Input JSON (Stop hook payload): $ARGUMENTS\n\nCheck this project's ${DOD_HOOK_MARKER}. This kit does not define its own workflow steps or require any specific workflow skill; it only verifies that evidence of the gate exists, however that evidence was produced (a documented planning/review skill, a manual process, or anything equivalent). Use the \`last_assistant_message\` field as the authoritative final response. Use \`transcript_path\` only for additional context because the transcript may not contain the final response yet.\n\nVerification is never optional: every change, fast path included, requires semantic evidence in the final response or visible transcript that focused tests or equivalent verification actually ran and passed (for example test/build output, or a described manual verification). Do not require a specific command; judge the evidence on its meaning.\n\nFor any change that is not a verified fast path (see below), completion additionally requires evidence of:\n- Plan: a plan was written and explicitly approved by the human before implementation started.\n- Review: the change was reviewed (correctness, simplicity, architecture, security, performance) and blocking issues were resolved.\n\nFast-path eligibility is based on risk and contract surface, not file count. A change is fast path only when it is localized, mechanically obvious, low-risk, and has no meaningful public API, schema, auth/security, data-integrity, payment, concurrency, dependency, CI/release, or architectural risk, with focused verification available. A source file plus its own focused test can still be fast path; a one-file auth, payment, schema, security, or concurrency change is not fast path no matter how small. A verified fast-path change is exempt from the Plan and Review evidence only when the final response states FAST_PATH: simple-fix, its reason, and its narrow scope — Verification evidence is still mandatory in that case, not optional.\n\nSteps:\n1. If the input JSON has \`stop_hook_active: true\`, allow stopping immediately (already checked once this cycle; never block twice in a row).\n2. Inspect the changed files and the \`last_assistant_message\` field.\n3. If no source files changed or the final response does not claim the task is done/complete, allow stopping.\n4. If the final response explicitly contains FAST_PATH: simple-fix, first check the claim against the risk criteria above, not file count. If it actually touches one of those risk areas regardless of file count, the FAST_PATH claim is invalid; treat it as a full feature/refactor change and continue to step 5. Otherwise, look for semantic evidence that focused verification actually ran and passed. If that evidence is present, allow stopping. If it is missing, block with {"ok": false, "reason": "Missing Verification evidence: ..."}.\n5. If the final response claims completion for a change that is not a verified fast path, check all three gates: (a) a plan was written and explicitly approved by the human before implementation started; (b) the change was reviewed and blocking issues were resolved; (c) tests or equivalent verification ran and passed. If any one of the three is missing, block naming exactly that gate; a generic statement that the task was simple, or that it touched only one file, is not enough on its own.\n6. Return exactly JSON: {"ok": true} to allow stopping, or {"ok": false, "reason": "..."} to block.\n\nWhen blocking, name exactly which gate is missing without prescribing which skill or workflow must produce it.`;
1458
+ }
1459
+
1460
+ function isDefinitionOfDoneHook(hook) {
1461
+ const prompt = hook && typeof hook.prompt === 'string' ? hook.prompt : '';
1462
+ return prompt.includes(DOD_HOOK_MARKER);
1463
+ }
1464
+
1465
+ function mergeClaudeProjectHooks(filePath) {
1466
+ const data = readJsonFile(filePath);
1467
+ if (data === null) {
1468
+ return { ok: false, message: `${filePath} is not valid JSON; skipped hook setup` };
1469
+ }
1470
+ if (!data.hooks || typeof data.hooks !== 'object') data.hooks = {};
1471
+ if (!Array.isArray(data.hooks.Stop)) data.hooks.Stop = [];
1472
+
1473
+ let primaryHook = null;
1474
+ let definitionHookCount = 0;
1475
+ for (const entry of data.hooks.Stop) {
1476
+ for (const hook of Array.isArray(entry?.hooks) ? entry.hooks : []) {
1477
+ if (isDefinitionOfDoneHook(hook)) {
1478
+ definitionHookCount++;
1479
+ if (!primaryHook) primaryHook = hook;
1480
+ }
1481
+ }
1482
+ }
1483
+
1484
+ if (primaryHook) {
1485
+ primaryHook.prompt = generateDodStopHookPrompt();
1486
+ primaryHook.timeout = 60;
1487
+ primaryHook.statusMessage = 'Checking feature/refactor Definition-of-Done gate...';
1488
+
1489
+ if (definitionHookCount > 1) {
1490
+ data.hooks.Stop = data.hooks.Stop
1491
+ .map(entry => ({
1492
+ ...entry,
1493
+ hooks: (Array.isArray(entry?.hooks) ? entry.hooks : [])
1494
+ .filter(hook => !isDefinitionOfDoneHook(hook) || hook === primaryHook)
1495
+ }))
1496
+ .filter(entry => entry.hooks.length > 0);
1497
+ }
1498
+
1499
+ writeJsonFile(filePath, data);
1500
+ return {
1501
+ ok: true,
1502
+ message: definitionHookCount > 1
1503
+ ? `consolidated Definition-of-Done Stop hooks in ${filePath}`
1504
+ : `updated Definition-of-Done Stop hook in ${filePath}`
1505
+ };
1506
+ }
1507
+
1508
+ data.hooks.Stop.push({
1509
+ hooks: [
1510
+ {
1511
+ type: 'agent',
1512
+ prompt: generateDodStopHookPrompt(),
1513
+ timeout: 60,
1514
+ statusMessage: 'Checking feature/refactor Definition-of-Done gate...'
1515
+ }
1516
+ ]
1517
+ });
1518
+ writeJsonFile(filePath, data);
1519
+ return { ok: true, message: `configured Definition-of-Done Stop hook in ${filePath}` };
1520
+ }
1521
+
1522
+ function setupMcpServers(options = {}) {
1523
+ const install = options.install === true;
1524
+ const results = [];
1525
+ results.push(ensureCodebaseMemoryCli(install));
1526
+ results.push(ensureGlobalNpmPackage('context7-mcp', '@upstash/context7-mcp', install));
1527
+ results.push(ensureGlobalNpmPackage('playwright-mcp', '@playwright/mcp', install));
1528
+
1529
+ const codexConfig = homePath('.codex', 'config.toml');
1530
+ if (codexConfig) results.push(appendMcpTomlIfMissing(codexConfig));
1531
+
1532
+ const geminiConfig = homePath('.gemini', 'config', 'mcp_config.json');
1533
+ if (geminiConfig) results.push(mergeMcpJson(geminiConfig, false));
1534
+
1535
+ const antigravityIdeConfig = homePath('.gemini', 'antigravity-ide', 'mcp_config.json');
1536
+ if (antigravityIdeConfig) results.push(mergeMcpJson(antigravityIdeConfig, false));
1537
+
1538
+ const antigravityConfig = homePath('.gemini', 'antigravity', 'mcp_config.json');
1539
+ if (antigravityConfig && fs.existsSync(path.dirname(antigravityConfig))) {
1540
+ results.push(mergeMcpJson(antigravityConfig, false));
1541
+ }
1542
+
1543
+ const claudeConfig = homePath('.claude.json');
1544
+ if (claudeConfig) results.push(mergeMcpJson(claudeConfig, true));
1545
+
1546
+ const claudeSettings = homePath('.claude', 'settings.json');
1547
+ if (claudeSettings) results.push(mergeMcpJson(claudeSettings, true));
1548
+
1549
+ return results;
1550
+ }
1551
+
1552
+ function runSkillsCommand(args) {
1553
+ const subcommand = args[0];
1554
+ const dryRun = args.includes('--dry-run');
1555
+
1556
+ if (subcommand === 'install') {
1557
+ console.log(pc.bold(pc.cyan('thachvd-kit skills install')));
1558
+ const mattResult = mattSkills.installSkills(targetDir, { dryRun });
1559
+ console.log(`\n${pc.bold('Matt Pocock skills')}`);
1560
+ console.log(` ${mattResult.ok ? pc.green('OK') : pc.yellow('WARN')} ${mattResult.message}`);
1561
+ const nativeResult = nativeSkills.installNativeSkills(targetDir, { dryRun });
1562
+ console.log(`\n${pc.bold('thachvd-kit native skills')}`);
1563
+ if (dryRun) for (const item of nativeResult.operations) console.log(` ${pc.dim('would copy')} ${item.relativePath} -> ${item.surface}`);
1564
+ console.log(` ${nativeResult.ok ? pc.green('OK') : pc.yellow('WARN')} ${nativeResult.message}`);
1565
+ if (!dryRun && mattResult.ok) printMattSkillsSetupReminder();
1566
+ if (!mattResult.ok || !nativeResult.ok) process.exitCode = 1;
1567
+ return;
1568
+ }
1569
+
1570
+ if (subcommand === 'check') {
1571
+ console.log(pc.bold(pc.cyan('thachvd-kit skills check')));
1572
+ const status = mattSkills.checkSkills(targetDir);
1573
+ console.log(`\n${pc.bold('Matt Pocock skills')}`);
1574
+ for (const name of mattSkills.PROMOTED_SKILLS) {
1575
+ const installed = status.installed.includes(name);
1576
+ console.log(` ${installed ? pc.green('OK') : pc.yellow('WARN')} ${name}`);
1577
+ }
1578
+ console.log(`\n ${status.installed.length}/${status.total} promoted skills installed in ${mattSkills.SKILLS_RELATIVE_DIR}`);
1579
+ if (status.missing.length > 0) console.log(` Run: ${pc.bold('thachvd-kit skills install')}`);
1580
+ console.log(`\n${pc.bold('thachvd-kit native skills')}`);
1581
+ const nativeStatus = nativeSkills.checkNativeSkills(targetDir);
1582
+ for (const name of nativeSkills.NATIVE_SKILLS) {
1583
+ const item = nativeStatus.skills[name];
1584
+ console.log(` ${item.agents && item.claude ? pc.green('OK') : pc.yellow('WARN')} ${name}`);
1585
+ console.log(` .agents: ${item.agents ? 'installed' : 'missing'}`);
1586
+ console.log(` .claude: ${item.claude ? 'installed' : 'missing'}`);
1587
+ }
1588
+ for (const surface of nativeSkills.NATIVE_SKILL_SURFACES) {
1589
+ const support = nativeStatus.surfaces[surface.id].support;
1590
+ const status = support.missing.length === 0 ? pc.green('OK') : pc.yellow('WARN');
1591
+ console.log(` ${status} support files (${surface.relativeDir}: ${support.missing.length === 0 ? 'installed' : 'missing'})`);
1592
+ }
1593
+ if (!nativeStatus.allInstalled) console.log(` Run: ${pc.bold('thachvd-kit skills install')}`);
1594
+ return;
1595
+ }
1596
+
1597
+ if (subcommand === 'update') {
1598
+ console.log(pc.bold(pc.cyan('thachvd-kit skills update')));
1599
+ const result = mattSkills.updateSkills(targetDir, { dryRun });
1600
+ console.log(`\n${pc.bold('Matt Pocock skills')}`);
1601
+ if (dryRun) {
1602
+ for (const item of result.results) console.log(` ${pc.dim('would run')} ${item.command}`);
1603
+ } else {
1604
+ for (const item of result.results) console.log(` ${item.ok ? pc.green('OK') : pc.yellow('WARN')} ${item.name}`);
1605
+ }
1606
+ console.log(`\n ${result.ok ? pc.green('OK') : pc.yellow('WARN')} ${result.message}`);
1607
+ const nativeResult = nativeSkills.updateNativeSkills(targetDir, { dryRun });
1608
+ console.log(`\n${pc.bold('thachvd-kit native skills')}`);
1609
+ if (dryRun) for (const item of nativeResult.operations) console.log(` ${pc.dim('would copy')} ${item.relativePath} -> ${item.surface}`);
1610
+ console.log(` ${nativeResult.ok ? pc.green('OK') : pc.yellow('WARN')} ${nativeResult.message}`);
1611
+ if (!result.ok || !nativeResult.ok) process.exitCode = 1;
1612
+ return;
1613
+ }
1614
+
1615
+ console.error(pc.red(`Unknown skills subcommand: ${subcommand || '(none)'}`));
1616
+ console.error('Usage: thachvd-kit skills install|check|update [--dry-run]');
1617
+ process.exit(1);
1618
+ }
1619
+
1620
+ // --- Main CLI ---
1621
+
1622
+ async function main() {
1623
+ // Note: 'upgrade' and 'global' are handled directly by bin/entry.js
1624
+ // (bin/upgrade.js / bin/global.js) before this module is even required,
1625
+ // so they never reach here.
1626
+ const rawArgs = process.argv.slice(2);
1627
+ const command = rawArgs[0] && !rawArgs[0].startsWith('-') ? rawArgs[0] : 'init';
1628
+ const args = command === 'init' ? rawArgs.slice(rawArgs[0] === 'init' ? 1 : 0) : rawArgs.slice(1);
1629
+
1630
+ if (rawArgs.includes('--version') || rawArgs.includes('-v')) {
1631
+ console.log(packageJson.version);
1632
+ return;
1633
+ }
1634
+
1635
+ if (rawArgs.includes('--help') || rawArgs.includes('-h')) {
1636
+ printHelp();
1637
+ return;
1638
+ }
1639
+
1640
+ if (command === 'doctor') {
1641
+ runDoctor();
1642
+ return;
1643
+ }
1644
+
1645
+ if (command === 'prompt') {
1646
+ printProjectPrompt();
1647
+ return;
1648
+ }
1649
+
1650
+ if (command === 'setup') {
1651
+ await runSetup(rawArgs.slice(1));
1652
+ return;
1653
+ }
1654
+
1655
+ if (command === 'skills') {
1656
+ runSkillsCommand(rawArgs.slice(1));
1657
+ return;
1658
+ }
1659
+
1660
+ if (command !== 'init') {
1661
+ console.error(pc.red(`Unknown command: ${command}`));
1662
+ printHelp();
1663
+ process.exit(1);
1664
+ }
1665
+
1666
+ const assumeYes = args.includes('--yes') || args.includes('-y');
1667
+ const skipMcpSetup = args.includes('--no-setup-mcp') || process.env.THACHVD_KIT_SKIP_MCP_SETUP === '1';
1668
+
1669
+ console.log(pc.bold(pc.cyan('='.repeat(50))));
1670
+ console.log(' ' + pc.bold('thachvd-kit') + ' - AI Project Bootstrap');
1671
+ console.log(' Generates shared agent rules and configures MCP tooling');
1672
+ console.log(pc.bold(pc.cyan('='.repeat(50))));
1673
+
1674
+ console.log(pc.dim(' Scanning current project files for a baseline...\n'));
1675
+ const scanned = scanProject();
1676
+
1677
+ console.log(` ${pc.bold('Detected context:')}`);
1678
+ formatScanPreviewLines(scanned).forEach(([label, value]) => {
1679
+ console.log(` - ${label}: ${value}`);
1680
+ });
1681
+ console.log('');
1682
+ if ((scanned.scan_evidence || []).length > 0) {
1683
+ console.log(` ${pc.bold('Evidence:')}`);
1684
+ scanned.scan_evidence.forEach(line => console.log(` - ${line}`));
1685
+ console.log('');
1686
+ }
1687
+
1688
+ // Combine data
1689
+ const data = {
1690
+ project_name: scanned?.project_name || path.basename(targetDir),
1691
+ description: scanned?.description || 'A software project',
1692
+ project_type: scanned?.project_type || 'web-app',
1693
+ primary_language: scanned?.primary_language || 'typescript',
1694
+ frameworks: scanned?.frameworks || [],
1695
+ database: scanned?.database || 'none',
1696
+ uses_docker: scanned?.uses_docker ?? true,
1697
+ cloud: scanned?.cloud || 'none',
1698
+ package_manager: scanned?.package_manager || 'auto-detect',
1699
+ test_framework: scanned?.test_framework || 'auto-detect'
1700
+ };
1701
+
1702
+ data.database = data.database || scanned?.database || 'none';
1703
+ data.uses_docker = typeof data.uses_docker === 'boolean' ? data.uses_docker : (scanned?.uses_docker ?? true);
1704
+ data.cloud = data.cloud || scanned?.cloud || 'none';
1705
+ data.package_manager = data.package_manager || scanned?.package_manager || 'auto-detect';
1706
+ data.test_framework = data.test_framework || scanned?.test_framework || 'auto-detect';
1707
+ data.framework_details = scanned?.framework_details || [];
1708
+ data.scan_evidence = scanned?.scan_evidence || [];
1709
+ data.app_root = scanned?.app_root || '.';
1710
+
1711
+ data.infrastructure = [];
1712
+ if (data.uses_docker) data.infrastructure.push('docker');
1713
+ if (data.cloud !== 'none') data.infrastructure.push(data.cloud);
1714
+
1715
+ const shouldSetupMcp = args.includes('--setup-mcp') && !skipMcpSetup;
1716
+
1717
+ console.log(`\n${pc.bold(pc.cyan('Generating Files'))}`);
1718
+
1719
+ // Shared entry files are written after the .agent folder is available.
1720
+
1721
+ console.log(` ${pc.green('OK')} Project context generation enabled; run thachvd-kit setup to install the promoted Matt Pocock skill set`);
1722
+
1723
+
1724
+ const generatedFiles = [
1725
+ ['AGENTS.md', generateSharedAgentsMd(data)],
1726
+ ['CLAUDE.md', generateSharedClaudeMd()],
1727
+ ['GEMINI.md', generateSharedGeminiMd()],
1728
+ ['.cursorrules', generateSharedCursorrules(data)],
1729
+ [path.join('.agent', 'docs', 'project.md'), generateProjectDoc(data)],
1730
+ [path.join('.agent', 'docs', 'architecture.md'), generateArchitectureDoc(data)],
1731
+ [path.join('.agent', 'docs', 'conventions.md'), generateConventionsDoc()],
1732
+ [path.join('.agent', 'docs', 'workflow.md'), generateWorkflowDoc()],
1733
+ [path.join('.agent', 'docs', 'tooling.md'), generateToolingDoc(data)],
1734
+ [path.join('.agent', 'docs', 'getting-started.md'), generateGettingStartedDoc()],
1735
+ [path.join('.agent', 'docs', 'index-project-prompt.md'), generateIndexProjectPrompt()]
1736
+ ];
1737
+
1738
+ for (const [relativePath, content] of generatedFiles) {
1739
+ const wrote = await writeGeneratedFile(relativePath, content, assumeYes);
1740
+ if (wrote) console.log(` ${pc.green('OK')} Generated ${pc.bold(relativePath)}`);
1741
+ else console.log(` ${pc.yellow('~')} Skipped ${pc.bold(relativePath)}`);
1742
+ }
1743
+
1744
+ if (shouldSetupMcp) {
1745
+ console.log(`\n${pc.bold(pc.cyan('Configuring MCP'))}`);
1746
+ for (const result of setupMcpServers()) {
1747
+ const marker = result.ok ? pc.green('OK') : pc.yellow('~');
1748
+ console.log(` ${marker} ${result.message}`);
1749
+ }
1750
+ }
1751
+
1752
+ console.log(`\n${pc.bold(pc.green('Done!'))}
1753
+
1754
+ ${pc.bold('Project:')} ${data.project_name}
1755
+ ${pc.bold('Stack:')} ${data.primary_language} | ${(data.frameworks || []).join(', ') || 'no framework'}
1756
+
1757
+ ${pc.bold('Generated:')}
1758
+ - ${pc.bold('AGENTS.md')} shared entry for Codex, Antigravity, Claude Code, and Cursor
1759
+ - ${pc.bold('CLAUDE.md')} Claude Code entry that imports AGENTS.md
1760
+ - ${pc.bold('GEMINI.md')} Antigravity entry
1761
+ - ${pc.bold('.cursorrules')} Cursor entry
1762
+ - ${pc.bold('.agent/docs/')} scan-based project rules and local operating notes
1763
+ - ${pc.bold('.agent/docs/index-project-prompt.md')} durable onboarding prompt
1764
+
1765
+ ${pc.bold('MCP/tooling setup:')}
1766
+ - Matt Pocock's promoted and thachvd-kit native skills are the workflow layer
1767
+ - Run ${pc.bold('thachvd-kit setup')} to install both groups and configure codebase-memory MCP and recommended RTK
1768
+
1769
+ ${pc.bold('Next steps:')}
1770
+ 1. Run ${pc.bold('thachvd-kit prompt')} and paste the saved prompt into your AI editor
1771
+ 2. Run ${pc.bold('thachvd-kit setup')} to install promoted and native skill sets and RTK
1772
+ 3. Run ${pc.bold('thachvd-kit doctor')} to verify the machine
1773
+ 4. Re-run ${pc.bold('thachvd-kit prompt')} whenever you need the onboarding prompt again
1774
+
1775
+ ${pc.bold('Onboarding prompt saved to .agent/docs/index-project-prompt.md:')}
1776
+ ${pc.dim('---')}
1777
+ ${generateIndexProjectPrompt().trim()}
1778
+ ${pc.dim('---')}
1779
+ `);
1780
+ }
1781
+
1782
+ main().catch(err => {
1783
+ console.error(pc.red('\nError during init:'), err);
1784
+ process.exit(1);
1785
+ });