thachvd-kit 1.0.7 โ†’ 1.0.10

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/README.md CHANGED
@@ -1,82 +1,111 @@
1
- # thachvd-kit โ€” Universal Agent Kit
2
-
3
- A universally compatible, high-power agent toolkit designed for **Antigravity**, **Claude Code**, and **Codex/OpenCode**.
4
- It dynamically sets up rules and intelligently routes agent skills based on your project's technology stack using a CLI wizard.
5
-
6
- ## ๐Ÿš€ Quick Start
7
-
8
- You can initialize `thachvd-kit` in **any** project directory without manually copying files!
9
-
10
- Simply run the wizard:
1
+ # thachvd-kit
2
+
3
+ `thachvd-kit` bootstraps project context for AI coding agents such as Antigravity, Claude Code, and Codex.
4
+
5
+ It installs a local `.agent/` kit, generates `PROJECT_CONTEXT.md`, and can scan an existing codebase to infer stack, framework, database, and infra signals before your agent starts working.
6
+
7
+ ## Quick Start
8
+
9
+ Install globally:
11
10
 
12
11
  ```bash
13
- npx thachvd-kit init
14
- # or
15
- npx github:thachvd/thachvd-kit init
12
+ npm install -g thachvd-kit
16
13
  ```
17
14
 
18
- The interactive wizard will first ask whether this is a new project or an existing one.
19
- If it is an existing project, it can scan the current codebase to prefill `PROJECT_CONTEXT.md`, or let you enter everything manually.
20
- The scan mode uses evidence-based detectors and shows why a framework was inferred, so you can quickly confirm or correct it.
21
- After init, you can also generate an AI refinement prompt to let your editor/agent deepen the project context.
15
+ Then run inside any project folder:
22
16
 
23
17
  ```bash
24
- thachvd-kit init --scan
18
+ thachvd-kit init
25
19
  ```
26
20
 
27
- Use `--scan` when you want to skip straight to codebase detection for an existing project.
28
-
29
- The wizard will then ask you a few questions about your project (frontend, backend, database, infrastructure, etc.).
30
- Based on your answers, it will:
31
- 1. **Copy** the highly optimized agents, skills, and rules to a `.agent` folder inside your project.
32
- 2. **Generate** a smart `PROJECT_CONTEXT.md` file tailored specifically to your chosen stack.
33
- 3. **Generate** a `CLAUDE.md` to ensure maximum cross-platform compatibility.
34
-
35
- ### Global Installation (Like `agkit-init`)
36
-
37
- If you want to install it globally on your machine:
38
-
39
- ```bash
40
- npm install -g github:thachvd/thachvd-kit
41
- ```
42
-
43
- Then, whenever you start a new project, just type:
21
+ You can also skip directly to scan mode:
44
22
 
45
23
  ```bash
46
- thachvd-kit init
24
+ thachvd-kit init --scan
47
25
  ```
48
-
49
- ---
50
-
51
- ## ๐Ÿง  How It Works
52
-
53
- This kit uses a **Universal Protocol Engine** (`GEMINI.md` / `CLAUDE.md`) that is completely stack-agnostic.
54
-
55
- Instead of hardcoding rules, it relies on the `PROJECT_CONTEXT.md` file (generated by the CLI wizard) to automatically detect the environment and load the **right** skills:
56
-
57
- - Are you using **Laravel**? It loads `laravel-patterns`, `laravel-security`, and `laravel-tdd`.
58
- - Are you using **Go**? It brings in `golang-patterns` and `golang-testing`.
59
- - Are you using **React**? It primes the agent with `react-frontend` and `frontend-design`.
60
- - Are you using **Next.js**? It primes the agent with `nextjs-react-expert` and `frontend-design`.
61
- - Are you using **Tauri**? It primes the agent with `rust-pro` and `desktop-design`.
62
-
63
- ## ๐Ÿ› ๏ธ Features
64
-
65
- - ๐ŸŽฏ **50+ Production-Grade Skills:** From architecture patterns to multi-agent parallel dispatching.
66
- - โšก๏ธ **Subagent-Driven Development:** Agents act as your implementer, spec-verifier, and code-reviewer without human bottlenecks.
67
- - ๐Ÿ›ก๏ธ **Zero Hallucination Completion:** Built-in `verification-before-completion` forces the agent to run tests before claiming "done".
68
- - ๐Ÿš€ **Isolated Worktrees:** Automatically manages hidden `git worktrees` so you never pollute your main branch with broken iterations.
69
-
70
- ## Directory Structure (Inside your `.agent` folder)
71
-
72
- ```text
73
- .agent/
74
- โ”œโ”€โ”€ rules/GEMINI.md โ† The Universal Rules Engine
75
- โ”œโ”€โ”€ agents/ โ† 20+ specialized agents (frontend, backend, db, ...)
76
- โ”œโ”€โ”€ skills/ โ† 50+ domain & workflow skills
77
- โ””โ”€โ”€ workflows/ โ† Executable workflows
78
- ```
79
-
80
- ---
81
-
82
- **Made with โค๏ธ for AI-native software engineering.**
26
+
27
+ ## What `init` Does
28
+
29
+ `thachvd-kit init` now supports both new and existing projects:
30
+
31
+ - New project: enter context manually.
32
+ - Existing project: choose scan mode or manual mode.
33
+ - Scan mode: detect stack from the codebase, show evidence, then either accept detected values or review/edit them.
34
+
35
+ Generated files:
36
+
37
+ - `.agent/agents`
38
+ - `.agent/rules`
39
+ - `.agent/skills`
40
+ - `.agent/workflows`
41
+ - `PROJECT_CONTEXT.md`
42
+ - `CLAUDE.md`
43
+ - `AI_CONTEXT_REFINEMENT.md` when first-session AI refinement is enabled
44
+
45
+ ## Scan Behavior
46
+
47
+ The scanner is hybrid and practical:
48
+
49
+ - App stack detection uses an inferred `app_root` so nested apps like `src/` can still resolve Laravel, CakePHP, Next.js, and similar stacks correctly.
50
+ - Infra detection stays repo-wide, so folders like `dev/`, `stg/`, `prod/` can still contribute Docker and CI evidence.
51
+ - Noise folders such as `vendor`, `node_modules`, `.git`, `dist`, and `build` are ignored during recursive scan.
52
+
53
+ The scan preview surfaces:
54
+
55
+ - app root
56
+ - project type
57
+ - primary language
58
+ - frameworks
59
+ - database
60
+ - package manager
61
+ - test framework
62
+ - cloud
63
+ - infrastructure
64
+ - CI/CD
65
+ - evidence for each inferred framework
66
+
67
+ ## AI Refinement Flow
68
+
69
+ `init` creates a baseline `PROJECT_CONTEXT.md`.
70
+
71
+ If you enable first-session AI refinement, the generated context is marked with:
72
+
73
+ - `context.status: baseline`
74
+ - `context.needs_ai_refinement: true`
75
+
76
+ Then, when your agent opens the project, `GEMINI.md` / `CLAUDE.md` instruct it to:
77
+
78
+ 1. Read `PROJECT_CONTEXT.md`
79
+ 2. Refine the context once if `needs_ai_refinement` is `true`
80
+ 3. Update the file to `context.status: refined`
81
+ 4. Set `context.needs_ai_refinement: false`
82
+
83
+ This keeps `init` simple while still allowing a deeper AI pass without requiring API-key integration inside the CLI.
84
+
85
+ ## Skill Resolution
86
+
87
+ The kit auto-loads generic and framework-specific skills based on detected context.
88
+
89
+ Examples:
90
+
91
+ - React -> `react-frontend`, `frontend-design`
92
+ - Next.js -> `nextjs-react-expert`, `frontend-design`
93
+ - Laravel -> `laravel-patterns`, `laravel-security`, `laravel-tdd`
94
+ - Tauri -> `rust-pro`, `desktop-design`
95
+
96
+ Generic quality skills such as `clean-code` and `systematic-debugging` are always included.
97
+
98
+ ## Directory Layout
99
+
100
+ ```text
101
+ .agent/
102
+ โ”œโ”€โ”€ agents/
103
+ โ”œโ”€โ”€ rules/
104
+ โ”œโ”€โ”€ skills/
105
+ โ””โ”€โ”€ workflows/
106
+ ```
107
+
108
+ ## Notes
109
+
110
+ - `thachvd-kit` is compatible with Codex workflows through `PROJECT_CONTEXT.md` and `CLAUDE.md`; it does not directly control Codex sessions.
111
+ - The npm package page updates only after a new `npm publish`, not just after pushing to GitHub.
package/bin/cli.js CHANGED
@@ -107,6 +107,7 @@ const CLOUD_CHOICES = ['aws', 'gcp', 'azure', 'vercel', 'fly', 'railway', 'none'
107
107
  const FRONTEND_FRAMEWORKS = ['vue', 'react', 'next', 'nuxt', 'svelte', 'angular'];
108
108
  const BACKEND_FRAMEWORKS = ['laravel', 'cakephp', 'symfony', 'codeigniter', 'express', 'fastapi', 'django', 'flask', 'gin', 'fiber', 'echo', 'nestjs', 'rails', 'spring', 'aspnet', 'axum', 'actix'];
109
109
  const MOBILE_DESKTOP_FRAMEWORKS = ['tauri', 'flutter', 'react-native'];
110
+ const IGNORED_SCAN_DIRS = new Set(['.git', 'node_modules', 'vendor', 'dist', 'build', 'coverage', '.next', '.nuxt', '.turbo', '.cache', 'storage', 'tmp']);
110
111
 
111
112
  function printHelp() {
112
113
  console.log(`
@@ -126,6 +127,21 @@ ${pc.bold('Options:')}
126
127
  `);
127
128
  }
128
129
 
130
+ function formatScanPreviewLines(scanned) {
131
+ return [
132
+ ['App root', scanned.app_root || '.'],
133
+ ['Project type', scanned.project_type || 'unknown'],
134
+ ['Primary language', scanned.primary_language || 'unknown'],
135
+ ['Frameworks', (scanned.frameworks || []).join(', ') || 'no framework'],
136
+ ['Database', scanned.database || 'none'],
137
+ ['Package manager', scanned.package_manager || 'auto-detect'],
138
+ ['Test framework', scanned.test_framework || 'auto-detect'],
139
+ ['Cloud', scanned.cloud || 'none'],
140
+ ['Infrastructure', (scanned.infrastructure || []).join(', ') || 'none'],
141
+ ['CI/CD', scanned.ci_cd || 'none']
142
+ ];
143
+ }
144
+
129
145
  async function askQuestions(questions) {
130
146
  return prompts(questions, {
131
147
  onCancel: () => {
@@ -139,6 +155,10 @@ function fileExists(...parts) {
139
155
  return fs.existsSync(path.join(targetDir, ...parts));
140
156
  }
141
157
 
158
+ function fileExistsAt(baseDir, ...parts) {
159
+ return fs.existsSync(path.join(baseDir, ...parts));
160
+ }
161
+
142
162
  function readJsonIfExists(...parts) {
143
163
  const fullPath = path.join(targetDir, ...parts);
144
164
  if (!fs.existsSync(fullPath)) return null;
@@ -149,6 +169,16 @@ function readJsonIfExists(...parts) {
149
169
  }
150
170
  }
151
171
 
172
+ function readJsonIfExistsAt(baseDir, ...parts) {
173
+ const fullPath = path.join(baseDir, ...parts);
174
+ if (!fs.existsSync(fullPath)) return null;
175
+ try {
176
+ return JSON.parse(fs.readFileSync(fullPath, 'utf8'));
177
+ } catch {
178
+ return null;
179
+ }
180
+ }
181
+
152
182
  function readTextIfExists(...parts) {
153
183
  const fullPath = path.join(targetDir, ...parts);
154
184
  if (!fs.existsSync(fullPath)) return '';
@@ -159,17 +189,27 @@ function readTextIfExists(...parts) {
159
189
  }
160
190
  }
161
191
 
162
- function detectPackageManager() {
163
- if (fileExists('pnpm-lock.yaml')) return 'pnpm';
164
- if (fileExists('yarn.lock')) return 'yarn';
165
- if (fileExists('bun.lockb') || fileExists('bun.lock')) return 'bun';
166
- if (fileExists('package-lock.json')) return 'npm';
167
- if (fileExists('uv.lock')) return 'uv';
168
- if (fileExists('poetry.lock')) return 'pip';
169
- if (fileExists('requirements.txt') || fileExists('pyproject.toml')) return 'pip';
170
- if (fileExists('go.mod')) return 'go mod';
171
- if (fileExists('Cargo.toml')) return 'cargo';
172
- if (fileExists('composer.json')) return 'composer';
192
+ function readTextIfExistsAt(baseDir, ...parts) {
193
+ const fullPath = path.join(baseDir, ...parts);
194
+ if (!fs.existsSync(fullPath)) return '';
195
+ try {
196
+ return fs.readFileSync(fullPath, 'utf8');
197
+ } catch {
198
+ return '';
199
+ }
200
+ }
201
+
202
+ function detectPackageManager(baseDir = targetDir) {
203
+ if (fileExistsAt(baseDir, 'pnpm-lock.yaml')) return 'pnpm';
204
+ if (fileExistsAt(baseDir, 'yarn.lock')) return 'yarn';
205
+ if (fileExistsAt(baseDir, 'bun.lockb') || fileExistsAt(baseDir, 'bun.lock')) return 'bun';
206
+ if (fileExistsAt(baseDir, 'package-lock.json')) return 'npm';
207
+ if (fileExistsAt(baseDir, 'uv.lock')) return 'uv';
208
+ if (fileExistsAt(baseDir, 'poetry.lock')) return 'pip';
209
+ if (fileExistsAt(baseDir, 'requirements.txt') || fileExistsAt(baseDir, 'pyproject.toml')) return 'pip';
210
+ if (fileExistsAt(baseDir, 'go.mod')) return 'go mod';
211
+ if (fileExistsAt(baseDir, 'Cargo.toml')) return 'cargo';
212
+ if (fileExistsAt(baseDir, 'composer.json')) return 'composer';
173
213
  return 'auto-detect';
174
214
  }
175
215
 
@@ -177,6 +217,76 @@ function uniq(values) {
177
217
  return Array.from(new Set(values.filter(Boolean)));
178
218
  }
179
219
 
220
+ function detectAppRoot(baseDir) {
221
+ const candidates = [baseDir];
222
+ const queue = [{ dir: baseDir, depth: 0 }];
223
+
224
+ while (queue.length > 0) {
225
+ const { dir, depth } = queue.shift();
226
+ if (depth >= 2) continue;
227
+ let entries = [];
228
+ try {
229
+ entries = fs.readdirSync(dir, { withFileTypes: true });
230
+ } catch {
231
+ continue;
232
+ }
233
+ for (const entry of entries) {
234
+ if (!entry.isDirectory()) continue;
235
+ if (IGNORED_SCAN_DIRS.has(entry.name)) continue;
236
+ const nextDir = path.join(dir, entry.name);
237
+ candidates.push(nextDir);
238
+ queue.push({ dir: nextDir, depth: depth + 1 });
239
+ }
240
+ }
241
+
242
+ const scoreCandidate = dir => {
243
+ let score = 0;
244
+ if (fileExistsAt(dir, 'composer.json')) score += 5;
245
+ if (fileExistsAt(dir, 'package.json')) score += 4;
246
+ if (fileExistsAt(dir, 'Cargo.toml')) score += 4;
247
+ if (fileExistsAt(dir, 'go.mod')) score += 4;
248
+ if (fileExistsAt(dir, 'pyproject.toml') || fileExistsAt(dir, 'requirements.txt')) score += 4;
249
+ if (fileExistsAt(dir, 'artisan')) score += 5;
250
+ if (fileExistsAt(dir, 'bin', 'cake') || fileExistsAt(dir, 'app', 'Config', 'core.php')) score += 5;
251
+ if (fileExistsAt(dir, 'src-tauri', 'tauri.conf.json') || fileExistsAt(dir, 'src-tauri', 'tauri.conf.json5')) score += 5;
252
+ if (fileExistsAt(dir, 'manage.py')) score += 4;
253
+ if (fileExistsAt(dir, 'Gemfile')) score += 4;
254
+ if (fileExistsAt(dir, 'pom.xml') || fileExistsAt(dir, 'build.gradle') || fileExistsAt(dir, 'build.gradle.kts')) score += 4;
255
+ if (fileExistsAt(dir, 'app', 'View') || fileExistsAt(dir, 'resources', 'views') || fileExistsAt(dir, 'templates') || fileExistsAt(dir, 'app', 'views')) score += 2;
256
+ return score;
257
+ };
258
+
259
+ const ranked = candidates
260
+ .map(dir => ({ dir, score: scoreCandidate(dir) }))
261
+ .filter(item => item.score > 0)
262
+ .sort((a, b) => (b.score - a.score) || (a.dir.length - b.dir.length));
263
+
264
+ return ranked[0]?.dir || baseDir;
265
+ }
266
+
267
+ function repoHasAny(baseDir, relativePaths, maxDepth = 3) {
268
+ const queue = [{ dir: baseDir, depth: 0 }];
269
+ while (queue.length > 0) {
270
+ const { dir, depth } = queue.shift();
271
+ for (const relativePath of relativePaths) {
272
+ if (fileExistsAt(dir, ...relativePath)) return true;
273
+ }
274
+ if (depth >= maxDepth) continue;
275
+ let entries = [];
276
+ try {
277
+ entries = fs.readdirSync(dir, { withFileTypes: true });
278
+ } catch {
279
+ continue;
280
+ }
281
+ for (const entry of entries) {
282
+ if (!entry.isDirectory()) continue;
283
+ if (IGNORED_SCAN_DIRS.has(entry.name)) continue;
284
+ queue.push({ dir: path.join(dir, entry.name), depth: depth + 1 });
285
+ }
286
+ }
287
+ return false;
288
+ }
289
+
180
290
  function evidenceIf(condition, label, weight = 1) {
181
291
  return condition ? { label, weight } : null;
182
292
  }
@@ -365,10 +475,11 @@ const FRAMEWORK_DETECTORS = [
365
475
  id: 'django',
366
476
  kind: 'backend',
367
477
  language: 'python',
368
- projectType: 'api',
478
+ projectType: 'web-app',
369
479
  detect: ctx => buildDetection('django', [
370
480
  evidenceIf(textMatches(ctx.requirementsText, /\bdjango\b/i) || textMatches(ctx.pyprojectText, /\bdjango\b/i), 'Python deps -> django', 4),
371
- evidenceIf(ctx.fileExists('manage.py'), 'found manage.py', 2)
481
+ evidenceIf(ctx.fileExists('manage.py'), 'found manage.py', 2),
482
+ evidenceIf(ctx.fileExists('templates'), 'found templates/', 1)
372
483
  ])
373
484
  },
374
485
  {
@@ -384,17 +495,18 @@ const FRAMEWORK_DETECTORS = [
384
495
  id: 'laravel',
385
496
  kind: 'backend',
386
497
  language: 'php',
387
- projectType: 'api',
498
+ projectType: 'web-app',
388
499
  detect: ctx => buildDetection('laravel', [
389
500
  evidenceIf(hasComposerDependency(ctx.composerDepNames, 'laravel/framework'), 'composer.json -> laravel/framework', 4),
390
- evidenceIf(ctx.fileExists('artisan'), 'found artisan', 3)
501
+ evidenceIf(ctx.fileExists('artisan'), 'found artisan', 3),
502
+ evidenceIf(ctx.fileExists('resources', 'views'), 'found resources/views', 1)
391
503
  ])
392
504
  },
393
505
  {
394
506
  id: 'cakephp',
395
507
  kind: 'backend',
396
508
  language: 'php',
397
- projectType: 'api',
509
+ projectType: 'web-app',
398
510
  detect: ctx => buildDetection('cakephp', [
399
511
  evidenceIf(hasComposerDependency(ctx.composerDepNames, 'cakephp/cakephp'), 'composer.json -> cakephp/cakephp', 4),
400
512
  evidenceIf(ctx.fileExists('bin', 'cake'), 'found bin/cake', 3),
@@ -403,18 +515,21 @@ const FRAMEWORK_DETECTORS = [
403
515
  evidenceIf(ctx.fileExists('app', 'Controller', 'AppController.php'), 'found app/Controller/AppController.php', 2),
404
516
  evidenceIf(ctx.fileExists('app', 'Model', 'AppModel.php'), 'found app/Model/AppModel.php', 2),
405
517
  evidenceIf(ctx.fileExists('lib', 'Cake'), 'found lib/Cake', 3),
406
- evidenceIf(ctx.fileExists('cake', 'console', 'cake'), 'found cake/console/cake', 3)
518
+ evidenceIf(ctx.fileExists('cake', 'console', 'cake'), 'found cake/console/cake', 3),
519
+ evidenceIf(ctx.fileExists('app', 'View'), 'found app/View', 2),
520
+ evidenceIf(textMatches(ctx.viewFilesText, /\.ctp$/im), 'found .ctp view templates', 2)
407
521
  ])
408
522
  },
409
523
  {
410
524
  id: 'symfony',
411
525
  kind: 'backend',
412
526
  language: 'php',
413
- projectType: 'api',
527
+ projectType: 'web-app',
414
528
  detect: ctx => buildDetection('symfony', [
415
529
  evidenceIf(hasComposerDependency(ctx.composerDepNames, 'symfony/framework-bundle'), 'composer.json -> symfony/framework-bundle', 4),
416
530
  evidenceIf(ctx.fileExists('bin', 'console'), 'found bin/console', 2),
417
- evidenceIf(ctx.fileExists('config', 'bundles.php'), 'found config/bundles.php', 1)
531
+ evidenceIf(ctx.fileExists('config', 'bundles.php'), 'found config/bundles.php', 1),
532
+ evidenceIf(ctx.fileExists('templates'), 'found templates/', 1)
418
533
  ])
419
534
  },
420
535
  {
@@ -459,10 +574,11 @@ const FRAMEWORK_DETECTORS = [
459
574
  id: 'rails',
460
575
  kind: 'backend',
461
576
  language: 'ruby',
462
- projectType: 'api',
577
+ projectType: 'web-app',
463
578
  detect: ctx => buildDetection('rails', [
464
579
  evidenceIf(textMatches(ctx.gemfileText, /^\s*gem\s+["']rails["']/im), 'Gemfile -> rails', 4),
465
- evidenceIf(ctx.fileExists('bin', 'rails'), 'found bin/rails', 2)
580
+ evidenceIf(ctx.fileExists('bin', 'rails'), 'found bin/rails', 2),
581
+ evidenceIf(ctx.fileExists('app', 'views'), 'found app/views', 1)
466
582
  ])
467
583
  },
468
584
  {
@@ -532,9 +648,10 @@ const FRAMEWORK_DETECTORS = [
532
648
  ];
533
649
 
534
650
  function scanProject() {
535
- const pkg = readJsonIfExists('package.json') || {};
536
- const composer = readJsonIfExists('composer.json') || {};
537
- const rootEntries = fs.existsSync(targetDir) ? fs.readdirSync(targetDir) : [];
651
+ const appRoot = detectAppRoot(targetDir);
652
+ const pkg = readJsonIfExistsAt(appRoot, 'package.json') || {};
653
+ const composer = readJsonIfExistsAt(appRoot, 'composer.json') || {};
654
+ const rootEntries = fs.existsSync(appRoot) ? fs.readdirSync(appRoot) : [];
538
655
  const rootCsproj = rootEntries.find(name => name.endsWith('.csproj')) || '';
539
656
  const deps = {
540
657
  ...(pkg.dependencies || {}),
@@ -553,17 +670,20 @@ function scanProject() {
553
670
  depNames,
554
671
  composerDepNames,
555
672
  rootEntries,
556
- fileExists,
557
- requirementsText: readTextIfExists('requirements.txt'),
558
- pyprojectText: readTextIfExists('pyproject.toml'),
559
- goModText: readTextIfExists('go.mod'),
560
- pubspecText: readTextIfExists('pubspec.yaml'),
561
- gemfileText: readTextIfExists('Gemfile'),
562
- pomText: readTextIfExists('pom.xml'),
563
- gradleText: `${readTextIfExists('build.gradle')}\n${readTextIfExists('build.gradle.kts')}`,
564
- cargoText: `${readTextIfExists('Cargo.toml')}\n${readTextIfExists('src-tauri', 'Cargo.toml')}`,
565
- csprojText: rootCsproj ? readTextIfExists(rootCsproj) : '',
566
- programCsText: readTextIfExists('Program.cs')
673
+ fileExists: (...parts) => fileExistsAt(appRoot, ...parts),
674
+ requirementsText: readTextIfExistsAt(appRoot, 'requirements.txt'),
675
+ pyprojectText: readTextIfExistsAt(appRoot, 'pyproject.toml'),
676
+ goModText: readTextIfExistsAt(appRoot, 'go.mod'),
677
+ pubspecText: readTextIfExistsAt(appRoot, 'pubspec.yaml'),
678
+ gemfileText: readTextIfExistsAt(appRoot, 'Gemfile'),
679
+ pomText: readTextIfExistsAt(appRoot, 'pom.xml'),
680
+ gradleText: `${readTextIfExistsAt(appRoot, 'build.gradle')}\n${readTextIfExistsAt(appRoot, 'build.gradle.kts')}`,
681
+ cargoText: `${readTextIfExistsAt(appRoot, 'Cargo.toml')}\n${readTextIfExistsAt(appRoot, 'src-tauri', 'Cargo.toml')}`,
682
+ csprojText: rootCsproj ? readTextIfExistsAt(appRoot, rootCsproj) : '',
683
+ programCsText: readTextIfExistsAt(appRoot, 'Program.cs'),
684
+ viewFilesText: fs.existsSync(path.join(appRoot, 'app', 'View'))
685
+ ? fs.readdirSync(path.join(appRoot, 'app', 'View'), { recursive: true }).join('\n')
686
+ : ''
567
687
  };
568
688
 
569
689
  const languageDetections = LANGUAGE_DETECTORS
@@ -605,9 +725,10 @@ function scanProject() {
605
725
  if (frameworkDetails.some(item => item.projectType === 'desktop')) projectType = 'desktop';
606
726
  else if (frameworkDetails.some(item => item.projectType === 'mobile')) projectType = 'mobile';
607
727
  else if (frameworkDetails.some(item => item.kind === 'frontend') && frameworkDetails.some(item => item.kind === 'backend')) projectType = 'fullstack';
728
+ else if (frameworkDetails.some(item => item.projectType === 'web-app')) projectType = 'web-app';
608
729
  else if (frameworkDetails.some(item => item.kind === 'backend')) projectType = 'api';
609
730
  else if (frameworkDetails.some(item => item.kind === 'frontend')) projectType = 'web-app';
610
- else if (fileExists('package.json') && pkg.bin) projectType = 'cli';
731
+ else if (fileExistsAt(appRoot, 'package.json') && pkg.bin) projectType = 'cli';
611
732
  else if (['php', 'python', 'go', 'java', 'ruby', 'csharp', 'rust'].includes(primaryLanguage)) projectType = 'api';
612
733
 
613
734
  if (hasDependency(depNames, 'pg', 'postgres', '@prisma/client') || /postgres/i.test(JSON.stringify(pkg))) database = 'postgres';
@@ -622,28 +743,30 @@ function scanProject() {
622
743
  else if (hasDependency(depNames, 'pytest') || textMatches(ctx.requirementsText, /\bpytest\b/i) || textMatches(ctx.pyprojectText, /\bpytest\b/i)) testFramework = 'pytest';
623
744
  else if (hasComposerDependency(composerDepNames, 'phpunit/phpunit')) testFramework = 'phpunit';
624
745
  else if (hasComposerDependency(composerDepNames, 'pestphp/pest')) testFramework = 'pest';
625
- else if (fileExists('go.mod')) testFramework = 'go test';
746
+ else if (fileExistsAt(appRoot, 'go.mod')) testFramework = 'go test';
626
747
  else if (textMatches(ctx.gemfileText, /^\s*gem\s+["']rspec["']/im)) testFramework = 'rspec';
627
748
 
628
- if (fileExists('Dockerfile') || fileExists('docker-compose.yml') || fileExists('docker-compose.yaml')) {
749
+ if (repoHasAny(targetDir, [['Dockerfile'], ['docker-compose.yml'], ['docker-compose.yaml']])) {
629
750
  infrastructure.push('docker');
630
751
  }
631
752
 
632
- if (fileExists('vercel.json')) {
753
+ if (repoHasAny(targetDir, [['vercel.json']])) {
633
754
  cloud = 'vercel';
634
755
  infrastructure.push('vercel');
635
- } else if (fileExists('fly.toml')) {
756
+ } else if (repoHasAny(targetDir, [['fly.toml']])) {
636
757
  cloud = 'fly';
637
758
  infrastructure.push('fly');
638
- } else if (fileExists('railway.json') || fileExists('railway.toml')) {
759
+ } else if (repoHasAny(targetDir, [['railway.json'], ['railway.toml']])) {
639
760
  cloud = 'railway';
640
761
  infrastructure.push('railway');
641
762
  }
642
763
 
643
- if (fileExists('.github', 'workflows')) ciCd = 'github-actions';
644
- else if (fileExists('.gitlab-ci.yml')) ciCd = 'gitlab-ci';
645
- else if (fileExists('.circleci', 'config.yml')) ciCd = 'circleci';
646
- else if (fileExists('Jenkinsfile')) ciCd = 'jenkins';
764
+ if (repoHasAny(targetDir, [['.github', 'workflows'], ['.gitlab-ci.yml'], ['.circleci', 'config.yml'], ['Jenkinsfile']], 2)) {
765
+ if (repoHasAny(targetDir, [['.github', 'workflows']], 2)) ciCd = 'github-actions';
766
+ else if (repoHasAny(targetDir, [['.gitlab-ci.yml']], 2)) ciCd = 'gitlab-ci';
767
+ else if (repoHasAny(targetDir, [['.circleci', 'config.yml']], 2)) ciCd = 'circleci';
768
+ else if (repoHasAny(targetDir, [['Jenkinsfile']], 2)) ciCd = 'jenkins';
769
+ }
647
770
 
648
771
  const scanEvidence = [
649
772
  ...(languageDetections[0] ? [`language:${languageDetections[0].id} => ${languageDetections[0].evidence.join('; ')}`] : []),
@@ -652,6 +775,7 @@ function scanProject() {
652
775
 
653
776
  return {
654
777
  project_name: path.basename(targetDir),
778
+ app_root: path.relative(targetDir, appRoot) || '.',
655
779
  description: pkg.description || composer.description || 'A software project',
656
780
  project_type: PROJECT_TYPE_CHOICES.includes(projectType) ? projectType : 'web-app',
657
781
  primary_language: LANGUAGE_CHOICES.includes(primaryLanguage) ? primaryLanguage : 'typescript',
@@ -659,7 +783,7 @@ function scanProject() {
659
783
  framework_details: frameworkDetails,
660
784
  scan_evidence: scanEvidence,
661
785
  database: DATABASE_CHOICES.includes(database) ? database : 'none',
662
- package_manager: PACKAGE_MANAGER_CHOICES.includes(detectPackageManager()) ? detectPackageManager() : 'auto-detect',
786
+ package_manager: PACKAGE_MANAGER_CHOICES.includes(detectPackageManager(appRoot)) ? detectPackageManager(appRoot) : 'auto-detect',
663
787
  uses_docker: infrastructure.includes('docker'),
664
788
  cloud,
665
789
  infrastructure: uniq(infrastructure),
@@ -745,21 +869,33 @@ ${(data.scan_evidence || []).map(line => '- ' + line).join('\n')}
745
869
  # Generated by thachvd-kit init wizard on ${new Date().toISOString().split('T')[0]}
746
870
  # Edit freely โ€” this is your project's brain.
747
871
 
748
- ## Project
749
-
750
- - **Name:** ${data.project_name}
751
- - **Description:** ${data.description}
752
- - **Type:** ${data.project_type}
753
-
754
- ---
872
+ ## Project
873
+
874
+ - **Name:** ${data.project_name}
875
+ - **Description:** ${data.description}
876
+ - **Type:** ${data.project_type}
877
+
878
+ ---
879
+
880
+ ## Context Status
881
+
882
+ \`\`\`yaml
883
+ context:
884
+ status: ${data.context_status || 'baseline'}
885
+ needs_ai_refinement: ${data.needs_ai_refinement ? 'true' : 'false'}
886
+ last_refined_at: ${data.last_refined_at || 'not-yet-refined'}
887
+ \`\`\`
888
+
889
+ ---
755
890
 
756
891
  ## Stack
757
892
 
758
893
  \`\`\`yaml
759
- stack:
760
- primary_language: ${data.primary_language}
761
- frameworks: [${(data.frameworks || []).join(', ')}]
762
- database: ${data.database || 'none'}
894
+ stack:
895
+ app_root: ${data.app_root || '.'}
896
+ primary_language: ${data.primary_language}
897
+ frameworks: [${(data.frameworks || []).join(', ')}]
898
+ database: ${data.database || 'none'}
763
899
  infrastructure: [${(data.infrastructure || []).join(', ')}]
764
900
  package_manager: ${data.package_manager || 'auto-detect'}
765
901
  test_framework: ${data.test_framework || 'auto-detect'}
@@ -992,8 +1128,11 @@ async function main() {
992
1128
  let requestAiRefinement = false;
993
1129
 
994
1130
  if (useScan) {
995
- console.log(`${pc.bold(' Scan result:')} ${scanned.primary_language} | ${scanned.frameworks.join(', ') || 'no framework'}`);
996
- console.log(` ${pc.bold('Detected infra:')} ${scanned.infrastructure.join(', ') || 'none'}\n`);
1131
+ console.log(` ${pc.bold('Detected context:')}`);
1132
+ formatScanPreviewLines(scanned).forEach(([label, value]) => {
1133
+ console.log(` ยท ${label}: ${value}`);
1134
+ });
1135
+ console.log('');
997
1136
  if ((scanned.scan_evidence || []).length > 0) {
998
1137
  console.log(` ${pc.bold('Evidence:')}`);
999
1138
  scanned.scan_evidence.forEach(line => console.log(` ยท ${line}`));
@@ -1083,6 +1222,21 @@ async function main() {
1083
1222
  data.max_file_lines = data.max_file_lines || scanned?.max_file_lines || '300';
1084
1223
  data.framework_details = scanned?.framework_details || [];
1085
1224
  data.scan_evidence = scanned?.scan_evidence || [];
1225
+ data.app_root = scanned?.app_root || '.';
1226
+
1227
+ if (!assumeYes) {
1228
+ const aiRefineAnswer = await askQuestions({
1229
+ type: 'confirm',
1230
+ name: 'ai_refine',
1231
+ message: 'Should the agent refine PROJECT_CONTEXT.md on first session start?',
1232
+ initial: useScan
1233
+ });
1234
+ requestAiRefinement = Boolean(aiRefineAnswer.ai_refine);
1235
+ }
1236
+
1237
+ data.context_status = 'baseline';
1238
+ data.needs_ai_refinement = requestAiRefinement;
1239
+ data.last_refined_at = 'not-yet-refined';
1086
1240
  data.infrastructure = [];
1087
1241
  if (data.uses_docker) data.infrastructure.push('docker');
1088
1242
  if (data.cloud !== 'none') data.infrastructure.push(data.cloud);
@@ -1118,16 +1272,6 @@ async function main() {
1118
1272
  fs.writeFileSync(claudeMdPath, generateClaudeMd(data), 'utf8');
1119
1273
  console.log(` ${pc.green('โœ“')} Generated ${pc.bold('CLAUDE.md')}`);
1120
1274
 
1121
- if (!assumeYes) {
1122
- const aiRefineAnswer = await askQuestions({
1123
- type: 'confirm',
1124
- name: 'ai_refine',
1125
- message: 'Do you want an AI refinement prompt for PROJECT_CONTEXT.md?',
1126
- initial: useScan
1127
- });
1128
- requestAiRefinement = Boolean(aiRefineAnswer.ai_refine);
1129
- }
1130
-
1131
1275
  let aiRefinementPath = null;
1132
1276
  if (requestAiRefinement) {
1133
1277
  aiRefinementPath = path.join(targetDir, 'AI_CONTEXT_REFINEMENT.md');
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "thachvd-kit",
3
- "version": "1.0.7",
4
- "description": "Universal Agent Kit for Antigravity, Claude Code, and OpenCode",
3
+ "version": "1.0.10",
4
+ "description": "Project context bootstrap kit for Antigravity, Claude Code, and Codex",
5
5
  "bin": {
6
6
  "thachvd-kit": "./bin/cli.js"
7
7
  },
@@ -38,7 +38,7 @@
38
38
  "license": "MIT",
39
39
  "repository": {
40
40
  "type": "git",
41
- "url": "git@github_home:holdon1996/thachvd-kit.git"
41
+ "url": "https://github.com/holdon1996/thachvd-kit.git"
42
42
  },
43
43
  "homepage": "https://github.com/holdon1996/thachvd-kit",
44
44
  "bugs": {
package/rules/GEMINI.md CHANGED
@@ -10,16 +10,19 @@ trigger: always_on
10
10
 
11
11
  ## MANDATORY FIRST ACTION
12
12
 
13
- **At session start โ€” before anything else:**
14
-
15
- ```
16
- 1. Read PROJECT_CONTEXT.md (if exists) โ†’ load stack, agents, constraints
17
- 2. If missing โ†’ run @[skills/project-onboarding] to create it
18
- 3. Confirm: "Context loaded: [Project Name] | Stack: [Stack] | Ready."
19
- ```
20
-
21
- > All routing decisions below depend on PROJECT_CONTEXT.md.
22
- > Without it, ask user to run: `python scripts/init.py`
13
+ **At session start โ€” before anything else:**
14
+
15
+ ```
16
+ 1. Read PROJECT_CONTEXT.md (if exists) โ†’ load stack, agents, constraints
17
+ 2. If missing โ†’ run @[skills/project-onboarding] to create it
18
+ 3. Read PROJECT_CONTEXT context.status and context.needs_ai_refinement
19
+ 4. If needs_ai_refinement = true โ†’ scan project with AI, update PROJECT_CONTEXT.md, set status=refined and needs_ai_refinement=false
20
+ 5. Confirm: "Context loaded: [Project Name] | Stack: [Stack] | Ready."
21
+ ```
22
+
23
+ > All routing decisions below depend on PROJECT_CONTEXT.md.
24
+ > Without it, ask user to run: `python scripts/init.py`
25
+ > Baseline context must be refined once when `context.needs_ai_refinement = true`.
23
26
 
24
27
  ---
25
28
 
package/scripts/init.py CHANGED
@@ -156,6 +156,7 @@ CLOUDS_ALL = ["aws", "gcp", "azure", "vercel", "fly", "railway", "digitalocean",
156
156
  FRONTEND_FRAMEWORKS = ["vue", "react", "next", "nuxt", "svelte", "angular"]
157
157
  BACKEND_FRAMEWORKS = ["laravel", "cakephp", "symfony", "codeigniter", "express", "fastapi", "django", "flask", "gin", "fiber", "echo", "nestjs", "rails", "spring", "aspnet", "axum", "actix"]
158
158
  MOBILE_DESKTOP_FRAMEWORKS = ["tauri", "flutter", "react-native"]
159
+ IGNORED_SCAN_DIRS = {".git", "node_modules", "vendor", "dist", "build", "coverage", ".next", ".nuxt", ".turbo", ".cache", "storage", "tmp"}
159
160
 
160
161
  def read_text_if_exists(*parts):
161
162
  path = Path.cwd().joinpath(*parts)
@@ -171,23 +172,26 @@ def read_json_if_exists(*parts):
171
172
  except Exception:
172
173
  return None
173
174
 
174
- def detect_package_manager():
175
- cwd = Path.cwd()
176
- if (cwd / "pnpm-lock.yaml").exists(): return "pnpm"
177
- if (cwd / "yarn.lock").exists(): return "yarn"
178
- if (cwd / "bun.lockb").exists() or (cwd / "bun.lock").exists(): return "bun"
179
- if (cwd / "package-lock.json").exists(): return "npm"
180
- if (cwd / "uv.lock").exists(): return "uv"
181
- if (cwd / "poetry.lock").exists(): return "poetry"
182
- if (cwd / "requirements.txt").exists() or (cwd / "pyproject.toml").exists(): return "pip"
183
- if (cwd / "go.mod").exists(): return "go mod"
184
- if (cwd / "Cargo.toml").exists(): return "cargo"
185
- if (cwd / "composer.json").exists(): return "composer"
175
+ def detect_package_manager(base_dir=None):
176
+ base_dir = base_dir or Path.cwd()
177
+ if path_exists(base_dir, "pnpm-lock.yaml"): return "pnpm"
178
+ if path_exists(base_dir, "yarn.lock"): return "yarn"
179
+ if path_exists(base_dir, "bun.lockb") or path_exists(base_dir, "bun.lock"): return "bun"
180
+ if path_exists(base_dir, "package-lock.json"): return "npm"
181
+ if path_exists(base_dir, "uv.lock"): return "uv"
182
+ if path_exists(base_dir, "poetry.lock"): return "poetry"
183
+ if path_exists(base_dir, "requirements.txt") or path_exists(base_dir, "pyproject.toml"): return "pip"
184
+ if path_exists(base_dir, "go.mod"): return "go mod"
185
+ if path_exists(base_dir, "Cargo.toml"): return "cargo"
186
+ if path_exists(base_dir, "composer.json"): return "composer"
186
187
  return "auto-detect"
187
188
 
188
189
  def uniq(values):
189
190
  return list(dict.fromkeys(v for v in values if v))
190
191
 
192
+ def path_exists(base_dir, *parts):
193
+ return base_dir.joinpath(*parts).exists()
194
+
191
195
  def evidence_if(condition, label, weight=1):
192
196
  return {"label": label, "weight": weight} if condition else None
193
197
 
@@ -215,10 +219,73 @@ def has_composer_dependency(composer_dep_names, *names):
215
219
  def text_matches(text, pattern):
216
220
  return bool(text and re.search(pattern, text, re.IGNORECASE | re.MULTILINE))
217
221
 
222
+ def detect_app_root(base_dir):
223
+ candidates = [base_dir]
224
+ queue = [(base_dir, 0)]
225
+ while queue:
226
+ current, depth = queue.pop(0)
227
+ if depth >= 2:
228
+ continue
229
+ try:
230
+ entries = list(current.iterdir())
231
+ except Exception:
232
+ continue
233
+ for entry in entries:
234
+ if not entry.is_dir():
235
+ continue
236
+ if entry.name in IGNORED_SCAN_DIRS:
237
+ continue
238
+ candidates.append(entry)
239
+ queue.append((entry, depth + 1))
240
+
241
+ def score_candidate(path):
242
+ score = 0
243
+ if path_exists(path, "composer.json"): score += 5
244
+ if path_exists(path, "package.json"): score += 4
245
+ if path_exists(path, "Cargo.toml"): score += 4
246
+ if path_exists(path, "go.mod"): score += 4
247
+ if path_exists(path, "pyproject.toml") or path_exists(path, "requirements.txt"): score += 4
248
+ if path_exists(path, "artisan"): score += 5
249
+ if path_exists(path, "bin", "cake") or path_exists(path, "app", "Config", "core.php"): score += 5
250
+ if path_exists(path, "src-tauri", "tauri.conf.json") or path_exists(path, "src-tauri", "tauri.conf.json5"): score += 5
251
+ if path_exists(path, "manage.py"): score += 4
252
+ if path_exists(path, "Gemfile"): score += 4
253
+ if path_exists(path, "pom.xml") or path_exists(path, "build.gradle") or path_exists(path, "build.gradle.kts"): score += 4
254
+ if path_exists(path, "app", "View") or path_exists(path, "resources", "views") or path_exists(path, "templates") or path_exists(path, "app", "views"): score += 2
255
+ return score
256
+
257
+ ranked = [{"path": path, "score": score_candidate(path)} for path in candidates]
258
+ ranked = [item for item in ranked if item["score"] > 0]
259
+ ranked.sort(key=lambda item: (-item["score"], len(str(item["path"]))))
260
+ return ranked[0]["path"] if ranked else base_dir
261
+
262
+ def repo_has_any(base_dir, relative_paths, max_depth=3):
263
+ queue = [(base_dir, 0)]
264
+ while queue:
265
+ current, depth = queue.pop(0)
266
+ for rel_parts in relative_paths:
267
+ if path_exists(current, *rel_parts):
268
+ return True
269
+ if depth >= max_depth:
270
+ continue
271
+ try:
272
+ entries = list(current.iterdir())
273
+ except Exception:
274
+ continue
275
+ for entry in entries:
276
+ if not entry.is_dir():
277
+ continue
278
+ if entry.name in IGNORED_SCAN_DIRS:
279
+ continue
280
+ queue.append((entry, depth + 1))
281
+ return False
282
+
218
283
  def scan_project():
219
- pkg = read_json_if_exists("package.json") or {}
220
- composer = read_json_if_exists("composer.json") or {}
221
- root_entries = [p.name for p in Path.cwd().iterdir()] if Path.cwd().exists() else []
284
+ repo_root = Path.cwd()
285
+ app_root = detect_app_root(repo_root)
286
+ pkg = read_json_if_exists(app_root, "package.json") or {}
287
+ composer = read_json_if_exists(app_root, "composer.json") or {}
288
+ root_entries = [p.name for p in app_root.iterdir()] if app_root.exists() else []
222
289
  root_csproj = next((name for name in root_entries if name.endswith(".csproj")), "")
223
290
  deps = {}
224
291
  for key in ("dependencies", "devDependencies", "peerDependencies"):
@@ -228,53 +295,53 @@ def scan_project():
228
295
  for key in ("require", "require-dev"):
229
296
  composer_deps.update(composer.get(key, {}) or {})
230
297
  composer_dep_names = set(composer_deps.keys())
231
- requirements = read_text_if_exists("requirements.txt")
232
- pyproject = read_text_if_exists("pyproject.toml")
233
- go_mod = read_text_if_exists("go.mod")
234
- pubspec = read_text_if_exists("pubspec.yaml")
235
- gemfile = read_text_if_exists("Gemfile")
236
- pom = read_text_if_exists("pom.xml")
237
- gradle = f"{read_text_if_exists('build.gradle')}\n{read_text_if_exists('build.gradle.kts')}"
238
- cargo = f"{read_text_if_exists('Cargo.toml')}\n{read_text_if_exists('src-tauri/Cargo.toml')}"
239
- csproj = read_text_if_exists(root_csproj) if root_csproj else ""
240
- program_cs = read_text_if_exists("Program.cs")
298
+ requirements = read_text_if_exists(app_root, "requirements.txt")
299
+ pyproject = read_text_if_exists(app_root, "pyproject.toml")
300
+ go_mod = read_text_if_exists(app_root, "go.mod")
301
+ pubspec = read_text_if_exists(app_root, "pubspec.yaml")
302
+ gemfile = read_text_if_exists(app_root, "Gemfile")
303
+ pom = read_text_if_exists(app_root, "pom.xml")
304
+ gradle = f"{read_text_if_exists(app_root, 'build.gradle')}\n{read_text_if_exists(app_root, 'build.gradle.kts')}"
305
+ cargo = f"{read_text_if_exists(app_root, 'Cargo.toml')}\n{read_text_if_exists(app_root, 'src-tauri/Cargo.toml')}"
306
+ csproj = read_text_if_exists(app_root, root_csproj) if root_csproj else ""
307
+ program_cs = read_text_if_exists(app_root, "Program.cs")
241
308
 
242
309
  language_detections = [
243
310
  build_detection("typescript", [
244
- evidence_if(Path("tsconfig.json").exists(), "found tsconfig.json", 4),
311
+ evidence_if(path_exists(app_root, "tsconfig.json"), "found tsconfig.json", 4),
245
312
  evidence_if(has_dependency(dep_names, "typescript", "ts-node", "tsx"), "package.json -> typescript toolchain", 2),
246
313
  ]),
247
314
  build_detection("javascript", [
248
- evidence_if(Path("package.json").exists(), "found package.json", 2),
315
+ evidence_if(path_exists(app_root, "package.json"), "found package.json", 2),
249
316
  evidence_if(bool(dep_names), "package.json -> JavaScript dependency graph", 1),
250
317
  ]),
251
318
  build_detection("python", [
252
- evidence_if(Path("requirements.txt").exists(), "found requirements.txt", 3),
253
- evidence_if(Path("pyproject.toml").exists(), "found pyproject.toml", 2),
319
+ evidence_if(path_exists(app_root, "requirements.txt"), "found requirements.txt", 3),
320
+ evidence_if(path_exists(app_root, "pyproject.toml"), "found pyproject.toml", 2),
254
321
  evidence_if(text_matches(pyproject, r"\[tool\.(poetry|uv|hatch|pdm)\]"), "pyproject.toml -> Python tool section", 2),
255
322
  ]),
256
323
  build_detection("go", [
257
- evidence_if(Path("go.mod").exists(), "found go.mod", 4),
324
+ evidence_if(path_exists(app_root, "go.mod"), "found go.mod", 4),
258
325
  ]),
259
326
  build_detection("php", [
260
- evidence_if(Path("composer.json").exists(), "found composer.json", 4),
327
+ evidence_if(path_exists(app_root, "composer.json"), "found composer.json", 4),
261
328
  evidence_if(bool(composer_dep_names), "composer.json -> PHP dependency graph", 1),
262
329
  ]),
263
330
  build_detection("rust", [
264
- evidence_if(Path("Cargo.toml").exists(), "found Cargo.toml", 4),
265
- evidence_if(Path("src-tauri/Cargo.toml").exists(), "found src-tauri/Cargo.toml", 5),
331
+ evidence_if(path_exists(app_root, "Cargo.toml"), "found Cargo.toml", 4),
332
+ evidence_if(path_exists(app_root, "src-tauri", "Cargo.toml"), "found src-tauri/Cargo.toml", 5),
266
333
  ]),
267
334
  build_detection("java", [
268
- evidence_if(Path("pom.xml").exists(), "found pom.xml", 4),
269
- evidence_if(Path("build.gradle").exists() or Path("build.gradle.kts").exists(), "found Gradle build file", 3),
335
+ evidence_if(path_exists(app_root, "pom.xml"), "found pom.xml", 4),
336
+ evidence_if(path_exists(app_root, "build.gradle") or path_exists(app_root, "build.gradle.kts"), "found Gradle build file", 3),
270
337
  ]),
271
338
  build_detection("ruby", [
272
- evidence_if(Path("Gemfile").exists(), "found Gemfile", 4),
273
- evidence_if(Path("config.ru").exists(), "found config.ru", 1),
339
+ evidence_if(path_exists(app_root, "Gemfile"), "found Gemfile", 4),
340
+ evidence_if(path_exists(app_root, "config.ru"), "found config.ru", 1),
274
341
  ]),
275
342
  build_detection("csharp", [
276
343
  evidence_if(any(name.endswith(".sln") or name.endswith(".csproj") for name in root_entries), "found .sln/.csproj", 4),
277
- evidence_if(Path("Program.cs").exists(), "found Program.cs", 1),
344
+ evidence_if(path_exists(app_root, "Program.cs"), "found Program.cs", 1),
278
345
  ]),
279
346
  ]
280
347
  language_detections = [item for item in language_detections if item["score"] >= 2]
@@ -283,8 +350,8 @@ def scan_project():
283
350
  framework_details = [
284
351
  {**build_detection("next", [
285
352
  evidence_if(has_dependency(dep_names, "next"), "package.json -> next", 4),
286
- evidence_if(Path("next.config.js").exists() or Path("next.config.mjs").exists() or Path("next.config.ts").exists(), "found next.config.*", 3),
287
- evidence_if(Path("app").exists() or Path("pages").exists(), "found app/ or pages/", 1),
353
+ evidence_if(path_exists(app_root, "next.config.js") or path_exists(app_root, "next.config.mjs") or path_exists(app_root, "next.config.ts"), "found next.config.*", 3),
354
+ evidence_if(path_exists(app_root, "app") or path_exists(app_root, "pages"), "found app/ or pages/", 1),
288
355
  ]), "kind": "frontend", "project_type": "web-app"},
289
356
  {**build_detection("react", [
290
357
  evidence_if(has_dependency(dep_names, "react"), "package.json -> react", 4),
@@ -295,14 +362,14 @@ def scan_project():
295
362
  ]), "kind": "frontend", "project_type": "web-app"},
296
363
  {**build_detection("nuxt", [
297
364
  evidence_if(has_dependency(dep_names, "nuxt"), "package.json -> nuxt", 4),
298
- evidence_if(Path("nuxt.config.ts").exists() or Path("nuxt.config.js").exists(), "found nuxt.config.*", 3),
365
+ evidence_if(path_exists(app_root, "nuxt.config.ts") or path_exists(app_root, "nuxt.config.js"), "found nuxt.config.*", 3),
299
366
  ]), "kind": "frontend", "project_type": "web-app"},
300
367
  {**build_detection("svelte", [
301
368
  evidence_if(has_dependency(dep_names, "svelte", "@sveltejs/kit"), "package.json -> svelte", 4),
302
369
  ]), "kind": "frontend", "project_type": "web-app"},
303
370
  {**build_detection("angular", [
304
371
  evidence_if(has_dependency(dep_names, "@angular/core"), "package.json -> @angular/core", 4),
305
- evidence_if(Path("angular.json").exists(), "found angular.json", 3),
372
+ evidence_if(path_exists(app_root, "angular.json"), "found angular.json", 3),
306
373
  ]), "kind": "frontend", "project_type": "web-app"},
307
374
  {**build_detection("express", [
308
375
  evidence_if(has_dependency(dep_names, "express"), "package.json -> express", 4),
@@ -315,34 +382,39 @@ def scan_project():
315
382
  ]), "kind": "backend", "project_type": "api", "language": "python"},
316
383
  {**build_detection("django", [
317
384
  evidence_if(text_matches(requirements, r"\bdjango\b") or text_matches(pyproject, r"\bdjango\b"), "Python deps -> django", 4),
318
- evidence_if(Path("manage.py").exists(), "found manage.py", 2),
319
- ]), "kind": "backend", "project_type": "api", "language": "python"},
385
+ evidence_if(path_exists(app_root, "manage.py"), "found manage.py", 2),
386
+ evidence_if(path_exists(app_root, "templates"), "found templates/", 1),
387
+ ]), "kind": "backend", "project_type": "web-app", "language": "python"},
320
388
  {**build_detection("flask", [
321
389
  evidence_if(text_matches(requirements, r"\bflask\b") or text_matches(pyproject, r"\bflask\b"), "Python deps -> flask", 4),
322
390
  ]), "kind": "backend", "project_type": "api", "language": "python"},
323
391
  {**build_detection("laravel", [
324
392
  evidence_if(has_composer_dependency(composer_dep_names, "laravel/framework"), "composer.json -> laravel/framework", 4),
325
- evidence_if(Path("artisan").exists(), "found artisan", 3),
326
- ]), "kind": "backend", "project_type": "api", "language": "php"},
393
+ evidence_if(path_exists(app_root, "artisan"), "found artisan", 3),
394
+ evidence_if(path_exists(app_root, "resources", "views"), "found resources/views", 1),
395
+ ]), "kind": "backend", "project_type": "web-app", "language": "php"},
327
396
  {**build_detection("cakephp", [
328
397
  evidence_if(has_composer_dependency(composer_dep_names, "cakephp/cakephp"), "composer.json -> cakephp/cakephp", 4),
329
- evidence_if(Path("bin/cake").exists(), "found bin/cake", 3),
330
- evidence_if(Path("config/app.php").exists(), "found config/app.php", 2),
331
- evidence_if(Path("app/Config/core.php").exists(), "found app/Config/core.php (CakePHP legacy)", 3),
332
- evidence_if(Path("app/Controller/AppController.php").exists(), "found app/Controller/AppController.php", 2),
333
- evidence_if(Path("app/Model/AppModel.php").exists(), "found app/Model/AppModel.php", 2),
334
- evidence_if(Path("lib/Cake").exists(), "found lib/Cake", 3),
335
- evidence_if(Path("cake/console/cake").exists(), "found cake/console/cake", 3),
336
- ]), "kind": "backend", "project_type": "api", "language": "php"},
398
+ evidence_if(path_exists(app_root, "bin", "cake"), "found bin/cake", 3),
399
+ evidence_if(path_exists(app_root, "config", "app.php"), "found config/app.php", 2),
400
+ evidence_if(path_exists(app_root, "app", "Config", "core.php"), "found app/Config/core.php (CakePHP legacy)", 3),
401
+ evidence_if(path_exists(app_root, "app", "Controller", "AppController.php"), "found app/Controller/AppController.php", 2),
402
+ evidence_if(path_exists(app_root, "app", "Model", "AppModel.php"), "found app/Model/AppModel.php", 2),
403
+ evidence_if(path_exists(app_root, "lib", "Cake"), "found lib/Cake", 3),
404
+ evidence_if(path_exists(app_root, "cake", "console", "cake"), "found cake/console/cake", 3),
405
+ evidence_if(path_exists(app_root, "app", "View"), "found app/View", 2),
406
+ evidence_if(any(p.suffix.lower() == ".ctp" for p in app_root.joinpath("app", "View").rglob("*.ctp")) if path_exists(app_root, "app", "View") else False, "found .ctp view templates", 2),
407
+ ]), "kind": "backend", "project_type": "web-app", "language": "php"},
337
408
  {**build_detection("symfony", [
338
409
  evidence_if(has_composer_dependency(composer_dep_names, "symfony/framework-bundle"), "composer.json -> symfony/framework-bundle", 4),
339
- evidence_if(Path("bin/console").exists(), "found bin/console", 2),
340
- evidence_if(Path("config/bundles.php").exists(), "found config/bundles.php", 1),
341
- ]), "kind": "backend", "project_type": "api", "language": "php"},
410
+ evidence_if(path_exists(app_root, "bin", "console"), "found bin/console", 2),
411
+ evidence_if(path_exists(app_root, "config", "bundles.php"), "found config/bundles.php", 1),
412
+ evidence_if(path_exists(app_root, "templates"), "found templates/", 1),
413
+ ]), "kind": "backend", "project_type": "web-app", "language": "php"},
342
414
  {**build_detection("codeigniter", [
343
415
  evidence_if(has_composer_dependency(composer_dep_names, "codeigniter4/framework"), "composer.json -> codeigniter4/framework", 4),
344
- evidence_if(Path("spark").exists(), "found spark", 2),
345
- evidence_if(Path("app/Config/App.php").exists(), "found app/Config/App.php", 1),
416
+ evidence_if(path_exists(app_root, "spark"), "found spark", 2),
417
+ evidence_if(path_exists(app_root, "app", "Config", "App.php"), "found app/Config/App.php", 1),
346
418
  ]), "kind": "backend", "project_type": "api", "language": "php"},
347
419
  {**build_detection("gin", [
348
420
  evidence_if(text_matches(go_mod, r"\bgithub\.com\/gin-gonic\/gin\b"), "go.mod -> github.com/gin-gonic/gin", 4),
@@ -355,8 +427,9 @@ def scan_project():
355
427
  ]), "kind": "backend", "project_type": "api", "language": "go"},
356
428
  {**build_detection("rails", [
357
429
  evidence_if(text_matches(gemfile, r'^\s*gem\s+["\']rails["\']'), "Gemfile -> rails", 4),
358
- evidence_if(Path("bin/rails").exists(), "found bin/rails", 2),
359
- ]), "kind": "backend", "project_type": "api", "language": "ruby"},
430
+ evidence_if(path_exists(app_root, "bin", "rails"), "found bin/rails", 2),
431
+ evidence_if(path_exists(app_root, "app", "views"), "found app/views", 1),
432
+ ]), "kind": "backend", "project_type": "web-app", "language": "ruby"},
360
433
  {**build_detection("spring", [
361
434
  evidence_if(text_matches(pom, r"spring-boot") or text_matches(gradle, r"spring-boot"), "Java build -> spring-boot", 4),
362
435
  ]), "kind": "backend", "project_type": "api", "language": "java"},
@@ -366,8 +439,8 @@ def scan_project():
366
439
  ]), "kind": "backend", "project_type": "api", "language": "csharp"},
367
440
  {**build_detection("tauri", [
368
441
  evidence_if(has_dependency(dep_names, "@tauri-apps/api"), "package.json -> @tauri-apps/api", 4),
369
- evidence_if(Path("src-tauri/tauri.conf.json").exists() or Path("src-tauri/tauri.conf.json5").exists(), "found src-tauri/tauri.conf.*", 4),
370
- evidence_if(Path("src-tauri/Cargo.toml").exists(), "found src-tauri/Cargo.toml", 2),
442
+ evidence_if(path_exists(app_root, "src-tauri", "tauri.conf.json") or path_exists(app_root, "src-tauri", "tauri.conf.json5"), "found src-tauri/tauri.conf.*", 4),
443
+ evidence_if(path_exists(app_root, "src-tauri", "Cargo.toml"), "found src-tauri/Cargo.toml", 2),
371
444
  ]), "kind": "desktop", "project_type": "desktop", "language": "rust"},
372
445
  {**build_detection("axum", [
373
446
  evidence_if(text_matches(cargo, r"\baxum\b"), "Cargo.toml -> axum", 4),
@@ -404,9 +477,10 @@ def scan_project():
404
477
  if any(item["project_type"] == "desktop" for item in framework_details): project_type = "desktop"
405
478
  elif any(item["project_type"] == "mobile" for item in framework_details): project_type = "mobile"
406
479
  elif any(item["kind"] == "frontend" for item in framework_details) and any(item["kind"] == "backend" for item in framework_details): project_type = "fullstack"
480
+ elif any(item["project_type"] == "web-app" for item in framework_details): project_type = "web-app"
407
481
  elif any(item["kind"] == "backend" for item in framework_details): project_type = "api"
408
482
  elif any(item["kind"] == "frontend" for item in framework_details): project_type = "web-app"
409
- elif Path("package.json").exists() and pkg.get("bin"): project_type = "cli"
483
+ elif path_exists(app_root, "package.json") and pkg.get("bin"): project_type = "cli"
410
484
  elif primary_language in ["php", "python", "go", "java", "ruby", "csharp", "rust"]: project_type = "api"
411
485
 
412
486
  if has_dependency(dep_names, "pg", "postgres", "@prisma/client") or "postgres" in json.dumps(pkg).lower(): database = "postgres"
@@ -421,26 +495,26 @@ def scan_project():
421
495
  elif has_dependency(dep_names, "pytest") or text_matches(requirements, r"\bpytest\b") or text_matches(pyproject, r"\bpytest\b"): test_framework = "pytest"
422
496
  elif has_composer_dependency(composer_dep_names, "phpunit/phpunit"): test_framework = "phpunit"
423
497
  elif has_composer_dependency(composer_dep_names, "pestphp/pest"): test_framework = "pest"
424
- elif Path("go.mod").exists(): test_framework = "go test"
498
+ elif path_exists(app_root, "go.mod"): test_framework = "go test"
425
499
  elif text_matches(gemfile, r'^\s*gem\s+["\']rspec["\']'): test_framework = "rspec"
426
500
 
427
- if Path("Dockerfile").exists() or Path("docker-compose.yml").exists() or Path("docker-compose.yaml").exists():
501
+ if repo_has_any(repo_root, [("Dockerfile",), ("docker-compose.yml",), ("docker-compose.yaml",)]):
428
502
  infrastructure.append("docker")
429
503
 
430
- if Path("vercel.json").exists():
504
+ if repo_has_any(repo_root, [("vercel.json",)]):
431
505
  cloud = "vercel"
432
506
  infrastructure.append("vercel")
433
- elif Path("fly.toml").exists():
507
+ elif repo_has_any(repo_root, [("fly.toml",)]):
434
508
  cloud = "fly"
435
509
  infrastructure.append("fly")
436
- elif Path("railway.json").exists() or Path("railway.toml").exists():
510
+ elif repo_has_any(repo_root, [("railway.json",), ("railway.toml",)]):
437
511
  cloud = "railway"
438
512
  infrastructure.append("railway")
439
513
 
440
- if Path(".github/workflows").exists(): ci_cd = "github-actions"
441
- elif Path(".gitlab-ci.yml").exists(): ci_cd = "gitlab-ci"
442
- elif Path(".circleci/config.yml").exists(): ci_cd = "circleci"
443
- elif Path("Jenkinsfile").exists(): ci_cd = "jenkins"
514
+ if repo_has_any(repo_root, [(".github", "workflows")], max_depth=2): ci_cd = "github-actions"
515
+ elif repo_has_any(repo_root, [(".gitlab-ci.yml",)], max_depth=2): ci_cd = "gitlab-ci"
516
+ elif repo_has_any(repo_root, [(".circleci", "config.yml")], max_depth=2): ci_cd = "circleci"
517
+ elif repo_has_any(repo_root, [("Jenkinsfile",)], max_depth=2): ci_cd = "jenkins"
444
518
 
445
519
  scan_evidence = []
446
520
  if language_detections:
@@ -449,6 +523,7 @@ def scan_project():
449
523
 
450
524
  return {
451
525
  "project_name": Path.cwd().name,
526
+ "app_root": str(app_root.relative_to(repo_root)) if app_root != repo_root else ".",
452
527
  "description": pkg.get("description") or composer.get("description", "A software project"),
453
528
  "project_type": project_type if project_type in PROJECT_TYPES_ALL else "web-app",
454
529
  "primary_language": primary_language if primary_language in LANGUAGES_ALL else "typescript",
@@ -456,7 +531,7 @@ def scan_project():
456
531
  "framework_details": framework_details,
457
532
  "scan_evidence": scan_evidence,
458
533
  "database": database if database in DATABASES_ALL else "none",
459
- "package_manager": detect_package_manager(),
534
+ "package_manager": detect_package_manager(app_root),
460
535
  "uses_docker": "docker" in infrastructure,
461
536
  "cloud": cloud,
462
537
  "infrastructure": uniq(infrastructure),
@@ -539,21 +614,33 @@ These detections were inferred from the current codebase during `init --scan`:
539
614
  # Generated by thachvd-kit init wizard on {datetime.now().strftime('%Y-%m-%d')}
540
615
  # Edit freely โ€” this is your project's brain.
541
616
 
542
- ## Project
543
-
544
- - **Name:** {data['project_name']}
545
- - **Description:** {data['description']}
546
- - **Type:** {data['project_type']}
547
-
548
- ---
617
+ ## Project
618
+
619
+ - **Name:** {data['project_name']}
620
+ - **Description:** {data['description']}
621
+ - **Type:** {data['project_type']}
622
+
623
+ ---
624
+
625
+ ## Context Status
626
+
627
+ ```yaml
628
+ context:
629
+ status: {data.get('context_status', 'baseline')}
630
+ needs_ai_refinement: {'true' if data.get('needs_ai_refinement') else 'false'}
631
+ last_refined_at: {data.get('last_refined_at', 'not-yet-refined')}
632
+ ```
633
+
634
+ ---
549
635
 
550
636
  ## Stack
551
637
 
552
638
  ```yaml
553
- stack:
554
- primary_language: {data['primary_language']}
555
- frameworks: [{', '.join(frameworks)}]
556
- database: {data.get('database', 'none')}
639
+ stack:
640
+ app_root: {data.get('app_root', '.')}
641
+ primary_language: {data['primary_language']}
642
+ frameworks: [{', '.join(frameworks)}]
643
+ database: {data.get('database', 'none')}
557
644
  infrastructure: [{', '.join(infrastructure)}]
558
645
  package_manager: {data.get('package_manager', 'auto-detect')}
559
646
  test_framework: {data.get('test_framework', 'auto-detect')}
@@ -699,6 +786,20 @@ When done, summarize:
699
786
  - what is still inferred
700
787
  - which files provided the strongest evidence
701
788
  """
789
+
790
+ def format_scan_preview_lines(scanned):
791
+ return [
792
+ ("App root", scanned.get("app_root", ".")),
793
+ ("Project type", scanned.get("project_type", "unknown")),
794
+ ("Primary language", scanned.get("primary_language", "unknown")),
795
+ ("Frameworks", ", ".join(scanned.get("frameworks", [])) or "no framework"),
796
+ ("Database", scanned.get("database", "none")),
797
+ ("Package manager", scanned.get("package_manager", "auto-detect")),
798
+ ("Test framework", scanned.get("test_framework", "auto-detect")),
799
+ ("Cloud", scanned.get("cloud", "none")),
800
+ ("Infrastructure", ", ".join(scanned.get("infrastructure", [])) or "none"),
801
+ ("CI/CD", scanned.get("ci_cd", "none")),
802
+ ]
702
803
 
703
804
  # โ”€โ”€โ”€ Main wizard โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
704
805
 
@@ -739,8 +840,10 @@ def main():
739
840
  review_scanned_fields = not use_scan
740
841
  if use_scan:
741
842
  print(f" {dim('Scan mode: detecting stack from current project files...')}")
742
- print(f" {bold('Scan result:')} {scanned['primary_language']} | {', '.join(scanned['frameworks']) or 'no framework'}")
743
- print(f" {bold('Detected infra:')} {', '.join(scanned['infrastructure']) or 'none'}\n")
843
+ print(f" {bold('Detected context:')}")
844
+ for label, value in format_scan_preview_lines(scanned):
845
+ print(f" ยท {label}: {value}")
846
+ print()
744
847
  if scanned.get("scan_evidence"):
745
848
  print(f" {bold('Evidence:')}")
746
849
  for line in scanned["scan_evidence"]:
@@ -830,6 +933,10 @@ def main():
830
933
  data["code_style"] = data.get("code_style", (scanned or {}).get("code_style", "project-standard"))
831
934
  data["framework_details"] = data.get("framework_details", (scanned or {}).get("framework_details", []))
832
935
  data["scan_evidence"] = data.get("scan_evidence", (scanned or {}).get("scan_evidence", []))
936
+ data["app_root"] = data.get("app_root", (scanned or {}).get("app_root", "."))
937
+ data["context_status"] = data.get("context_status", "baseline")
938
+ data["needs_ai_refinement"] = data.get("needs_ai_refinement", False)
939
+ data["last_refined_at"] = data.get("last_refined_at", "not-yet-refined")
833
940
  data["architecture_notes"] = data.get("architecture_notes", (scanned or {}).get("architecture_notes", "<!-- Add notes -->"))
834
941
  data["current_phase"] = data.get("current_phase", (scanned or {}).get("current_phase", "Initial setup"))
835
942
  data["next_phase"] = data.get("next_phase", (scanned or {}).get("next_phase", "โ€”"))
@@ -838,6 +945,9 @@ def main():
838
945
  if data["uses_docker"]: data["infrastructure"].append("docker")
839
946
  if data["cloud"] != "none": data["infrastructure"].append(data["cloud"])
840
947
 
948
+ if not args.yes:
949
+ data["needs_ai_refinement"] = ask_bool("Should the agent refine PROJECT_CONTEXT.md on first session start?", default=use_scan)
950
+
841
951
  # โ”€โ”€ Generate files โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
842
952
  section("Generating Files")
843
953
 
@@ -853,7 +963,7 @@ def main():
853
963
  print(f" {green('โœ“')} {bold('CLAUDE.md')} generated (Claude Code / Codex)")
854
964
 
855
965
  ai_refine_path = None
856
- if not args.yes and ask_bool("Generate AI context refinement prompt?", default=use_scan):
966
+ if data.get("needs_ai_refinement"):
857
967
  ai_refine_path = Path("AI_CONTEXT_REFINEMENT.md")
858
968
  ai_refine_path.write_text(generate_ai_context_refinement_prompt(data), encoding="utf-8")
859
969
  print(f" {green('โœ“')} {bold('AI_CONTEXT_REFINEMENT.md')} generated")