thachvd-kit 1.0.8 โ†’ 1.0.11

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,112 @@
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
+ - `AGENTS.md`
44
+ - `AI_CONTEXT_REFINEMENT.md` when first-session AI refinement is enabled
45
+
46
+ ## Scan Behavior
47
+
48
+ The scanner is hybrid and practical:
49
+
50
+ - App stack detection uses an inferred `app_root` so nested apps like `src/` can still resolve Laravel, CakePHP, Next.js, and similar stacks correctly.
51
+ - Infra detection stays repo-wide, so folders like `dev/`, `stg/`, `prod/` can still contribute Docker and CI evidence.
52
+ - Noise folders such as `vendor`, `node_modules`, `.git`, `dist`, and `build` are ignored during recursive scan.
53
+
54
+ The scan preview surfaces:
55
+
56
+ - app root
57
+ - project type
58
+ - primary language
59
+ - frameworks
60
+ - database
61
+ - package manager
62
+ - test framework
63
+ - cloud
64
+ - infrastructure
65
+ - CI/CD
66
+ - evidence for each inferred framework
67
+
68
+ ## AI Refinement Flow
69
+
70
+ `init` creates a baseline `PROJECT_CONTEXT.md`.
71
+
72
+ If you enable first-session AI refinement, the generated context is marked with:
73
+
74
+ - `context.status: baseline`
75
+ - `context.needs_ai_refinement: true`
76
+
77
+ Then, when your agent opens the project, `GEMINI.md` / `CLAUDE.md` / `AGENTS.md` instruct it to:
78
+
79
+ 1. Read `PROJECT_CONTEXT.md`
80
+ 2. Refine the context once if `needs_ai_refinement` is `true`
81
+ 3. Update the file to `context.status: refined`
82
+ 4. Set `context.needs_ai_refinement: false`
83
+
84
+ This keeps `init` simple while still allowing a deeper AI pass without requiring API-key integration inside the CLI.
85
+
86
+ ## Skill Resolution
87
+
88
+ The kit auto-loads generic and framework-specific skills based on detected context.
89
+
90
+ Examples:
91
+
92
+ - React -> `react-frontend`, `frontend-design`
93
+ - Next.js -> `nextjs-react-expert`, `frontend-design`
94
+ - Laravel -> `laravel-patterns`, `laravel-security`, `laravel-tdd`
95
+ - Tauri -> `rust-pro`, `desktop-design`
96
+
97
+ Generic quality skills such as `clean-code` and `systematic-debugging` are always included.
98
+
99
+ ## Directory Layout
100
+
101
+ ```text
102
+ .agent/
103
+ โ”œโ”€โ”€ agents/
104
+ โ”œโ”€โ”€ rules/
105
+ โ”œโ”€โ”€ skills/
106
+ โ””โ”€โ”€ workflows/
107
+ ```
108
+
109
+ ## Notes
110
+
111
+ - `thachvd-kit` generates both `CLAUDE.md` for Claude Code and `AGENTS.md` for Codex/AGENTS-compatible editors.
112
+ - 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(`
@@ -128,6 +129,7 @@ ${pc.bold('Options:')}
128
129
 
129
130
  function formatScanPreviewLines(scanned) {
130
131
  return [
132
+ ['App root', scanned.app_root || '.'],
131
133
  ['Project type', scanned.project_type || 'unknown'],
132
134
  ['Primary language', scanned.primary_language || 'unknown'],
133
135
  ['Frameworks', (scanned.frameworks || []).join(', ') || 'no framework'],
@@ -153,6 +155,10 @@ function fileExists(...parts) {
153
155
  return fs.existsSync(path.join(targetDir, ...parts));
154
156
  }
155
157
 
158
+ function fileExistsAt(baseDir, ...parts) {
159
+ return fs.existsSync(path.join(baseDir, ...parts));
160
+ }
161
+
156
162
  function readJsonIfExists(...parts) {
157
163
  const fullPath = path.join(targetDir, ...parts);
158
164
  if (!fs.existsSync(fullPath)) return null;
@@ -163,6 +169,16 @@ function readJsonIfExists(...parts) {
163
169
  }
164
170
  }
165
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
+
166
182
  function readTextIfExists(...parts) {
167
183
  const fullPath = path.join(targetDir, ...parts);
168
184
  if (!fs.existsSync(fullPath)) return '';
@@ -173,17 +189,27 @@ function readTextIfExists(...parts) {
173
189
  }
174
190
  }
175
191
 
176
- function detectPackageManager() {
177
- if (fileExists('pnpm-lock.yaml')) return 'pnpm';
178
- if (fileExists('yarn.lock')) return 'yarn';
179
- if (fileExists('bun.lockb') || fileExists('bun.lock')) return 'bun';
180
- if (fileExists('package-lock.json')) return 'npm';
181
- if (fileExists('uv.lock')) return 'uv';
182
- if (fileExists('poetry.lock')) return 'pip';
183
- if (fileExists('requirements.txt') || fileExists('pyproject.toml')) return 'pip';
184
- if (fileExists('go.mod')) return 'go mod';
185
- if (fileExists('Cargo.toml')) return 'cargo';
186
- 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';
187
213
  return 'auto-detect';
188
214
  }
189
215
 
@@ -191,6 +217,76 @@ function uniq(values) {
191
217
  return Array.from(new Set(values.filter(Boolean)));
192
218
  }
193
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
+
194
290
  function evidenceIf(condition, label, weight = 1) {
195
291
  return condition ? { label, weight } : null;
196
292
  }
@@ -552,9 +648,10 @@ const FRAMEWORK_DETECTORS = [
552
648
  ];
553
649
 
554
650
  function scanProject() {
555
- const pkg = readJsonIfExists('package.json') || {};
556
- const composer = readJsonIfExists('composer.json') || {};
557
- 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) : [];
558
655
  const rootCsproj = rootEntries.find(name => name.endsWith('.csproj')) || '';
559
656
  const deps = {
560
657
  ...(pkg.dependencies || {}),
@@ -573,19 +670,19 @@ function scanProject() {
573
670
  depNames,
574
671
  composerDepNames,
575
672
  rootEntries,
576
- fileExists,
577
- requirementsText: readTextIfExists('requirements.txt'),
578
- pyprojectText: readTextIfExists('pyproject.toml'),
579
- goModText: readTextIfExists('go.mod'),
580
- pubspecText: readTextIfExists('pubspec.yaml'),
581
- gemfileText: readTextIfExists('Gemfile'),
582
- pomText: readTextIfExists('pom.xml'),
583
- gradleText: `${readTextIfExists('build.gradle')}\n${readTextIfExists('build.gradle.kts')}`,
584
- cargoText: `${readTextIfExists('Cargo.toml')}\n${readTextIfExists('src-tauri', 'Cargo.toml')}`,
585
- csprojText: rootCsproj ? readTextIfExists(rootCsproj) : '',
586
- programCsText: readTextIfExists('Program.cs'),
587
- viewFilesText: fs.existsSync(path.join(targetDir, 'app', 'View'))
588
- ? fs.readdirSync(path.join(targetDir, 'app', 'View'), { recursive: true }).join('\n')
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')
589
686
  : ''
590
687
  };
591
688
 
@@ -631,7 +728,7 @@ function scanProject() {
631
728
  else if (frameworkDetails.some(item => item.projectType === 'web-app')) projectType = 'web-app';
632
729
  else if (frameworkDetails.some(item => item.kind === 'backend')) projectType = 'api';
633
730
  else if (frameworkDetails.some(item => item.kind === 'frontend')) projectType = 'web-app';
634
- else if (fileExists('package.json') && pkg.bin) projectType = 'cli';
731
+ else if (fileExistsAt(appRoot, 'package.json') && pkg.bin) projectType = 'cli';
635
732
  else if (['php', 'python', 'go', 'java', 'ruby', 'csharp', 'rust'].includes(primaryLanguage)) projectType = 'api';
636
733
 
637
734
  if (hasDependency(depNames, 'pg', 'postgres', '@prisma/client') || /postgres/i.test(JSON.stringify(pkg))) database = 'postgres';
@@ -646,28 +743,30 @@ function scanProject() {
646
743
  else if (hasDependency(depNames, 'pytest') || textMatches(ctx.requirementsText, /\bpytest\b/i) || textMatches(ctx.pyprojectText, /\bpytest\b/i)) testFramework = 'pytest';
647
744
  else if (hasComposerDependency(composerDepNames, 'phpunit/phpunit')) testFramework = 'phpunit';
648
745
  else if (hasComposerDependency(composerDepNames, 'pestphp/pest')) testFramework = 'pest';
649
- else if (fileExists('go.mod')) testFramework = 'go test';
746
+ else if (fileExistsAt(appRoot, 'go.mod')) testFramework = 'go test';
650
747
  else if (textMatches(ctx.gemfileText, /^\s*gem\s+["']rspec["']/im)) testFramework = 'rspec';
651
748
 
652
- if (fileExists('Dockerfile') || fileExists('docker-compose.yml') || fileExists('docker-compose.yaml')) {
749
+ if (repoHasAny(targetDir, [['Dockerfile'], ['docker-compose.yml'], ['docker-compose.yaml']])) {
653
750
  infrastructure.push('docker');
654
751
  }
655
752
 
656
- if (fileExists('vercel.json')) {
753
+ if (repoHasAny(targetDir, [['vercel.json']])) {
657
754
  cloud = 'vercel';
658
755
  infrastructure.push('vercel');
659
- } else if (fileExists('fly.toml')) {
756
+ } else if (repoHasAny(targetDir, [['fly.toml']])) {
660
757
  cloud = 'fly';
661
758
  infrastructure.push('fly');
662
- } else if (fileExists('railway.json') || fileExists('railway.toml')) {
759
+ } else if (repoHasAny(targetDir, [['railway.json'], ['railway.toml']])) {
663
760
  cloud = 'railway';
664
761
  infrastructure.push('railway');
665
762
  }
666
763
 
667
- if (fileExists('.github', 'workflows')) ciCd = 'github-actions';
668
- else if (fileExists('.gitlab-ci.yml')) ciCd = 'gitlab-ci';
669
- else if (fileExists('.circleci', 'config.yml')) ciCd = 'circleci';
670
- 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
+ }
671
770
 
672
771
  const scanEvidence = [
673
772
  ...(languageDetections[0] ? [`language:${languageDetections[0].id} => ${languageDetections[0].evidence.join('; ')}`] : []),
@@ -676,6 +775,7 @@ function scanProject() {
676
775
 
677
776
  return {
678
777
  project_name: path.basename(targetDir),
778
+ app_root: path.relative(targetDir, appRoot) || '.',
679
779
  description: pkg.description || composer.description || 'A software project',
680
780
  project_type: PROJECT_TYPE_CHOICES.includes(projectType) ? projectType : 'web-app',
681
781
  primary_language: LANGUAGE_CHOICES.includes(primaryLanguage) ? primaryLanguage : 'typescript',
@@ -683,7 +783,7 @@ function scanProject() {
683
783
  framework_details: frameworkDetails,
684
784
  scan_evidence: scanEvidence,
685
785
  database: DATABASE_CHOICES.includes(database) ? database : 'none',
686
- package_manager: PACKAGE_MANAGER_CHOICES.includes(detectPackageManager()) ? detectPackageManager() : 'auto-detect',
786
+ package_manager: PACKAGE_MANAGER_CHOICES.includes(detectPackageManager(appRoot)) ? detectPackageManager(appRoot) : 'auto-detect',
687
787
  uses_docker: infrastructure.includes('docker'),
688
788
  cloud,
689
789
  infrastructure: uniq(infrastructure),
@@ -791,10 +891,11 @@ context:
791
891
  ## Stack
792
892
 
793
893
  \`\`\`yaml
794
- stack:
795
- primary_language: ${data.primary_language}
796
- frameworks: [${(data.frameworks || []).join(', ')}]
797
- 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'}
798
899
  infrastructure: [${(data.infrastructure || []).join(', ')}]
799
900
  package_manager: ${data.package_manager || 'auto-detect'}
800
901
  test_framework: ${data.test_framework || 'auto-detect'}
@@ -852,11 +953,11 @@ ${autoSkillsList}
852
953
  function generateClaudeMd(data) {
853
954
  return `# CLAUDE.md โ€” ${data.project_name}
854
955
  # Generated by thachvd-kit. See PROJECT_CONTEXT.md for full stack info.
855
- # Cross-platform: Claude Code ยท Codex ยท OpenCode
856
-
857
- ## Project
858
- ${data.description}
859
- Stack: ${data.primary_language} | ${(data.frameworks || []).join(', ')}
956
+ # Platform: Claude Code
957
+
958
+ ## Project
959
+ ${data.description}
960
+ Stack: ${data.primary_language} | ${(data.frameworks || []).join(', ')}
860
961
 
861
962
  ## Rules
862
963
  - Read PROJECT_CONTEXT.md first for stack details and agent routing.
@@ -876,11 +977,52 @@ Stack: ${data.primary_language} | ${(data.frameworks || []).join(', ')}
876
977
  - \`/debug\` โ€” Systematic debugging
877
978
  - \`/test\` โ€” Run test suite
878
979
  - \`/deploy\` โ€” Deploy checklist
879
-
980
+
880
981
  See PROJECT_CONTEXT.md for full agent routing table.
881
982
  `;
882
983
  }
883
984
 
985
+ function generateAgentsMd(data) {
986
+ return `# AGENTS.md โ€” ${data.project_name}
987
+ # Generated by thachvd-kit. See PROJECT_CONTEXT.md for full stack info.
988
+ # Platform: Codex / AGENTS-compatible editors
989
+
990
+ ## Project
991
+ ${data.description}
992
+ Stack: ${data.primary_language} | ${(data.frameworks || []).join(', ') || 'no framework'}
993
+
994
+ ## Mandatory First Action
995
+ - Read \`PROJECT_CONTEXT.md\` before making plans or code changes.
996
+ - If \`PROJECT_CONTEXT.md\` is missing or stale, create/update it before major work.
997
+ - If \`context.needs_ai_refinement: true\`, refine \`PROJECT_CONTEXT.md\` first, then set:
998
+ - \`context.status: refined\`
999
+ - \`context.needs_ai_refinement: false\`
1000
+ - \`context.last_refined_at: <today>\`
1001
+
1002
+ ## Working Rules
1003
+ - Use \`.agent/agents\`, \`.agent/skills\`, \`.agent/rules\`, and \`.agent/workflows\` as the local project kit.
1004
+ - Match the task against the Agent Routing table in \`PROJECT_CONTEXT.md\`.
1005
+ - Load relevant skills before editing code.
1006
+ - Tests are mandatory. Verify before claiming done.
1007
+ - Keep files under ${data.max_file_lines || '300'} lines when practical, then split into modules.
1008
+ - Update dependent files together when a change crosses boundaries.
1009
+
1010
+ ## Preferred Workflow
1011
+ - Plan work from \`PROJECT_CONTEXT.md\` first.
1012
+ - For feature work, use: \`plan-writing\` -> \`executing-plans\` -> \`verification-before-completion\`.
1013
+ - For reviews, use: \`requesting-code-review\` / \`receiving-code-review\`.
1014
+ - For debugging, use: \`systematic-debugging\`.
1015
+
1016
+ ## Key Paths
1017
+ - Project context: \`PROJECT_CONTEXT.md\`
1018
+ - Agent kit: \`.agent/\`
1019
+ - Skills: \`.agent/skills/\`
1020
+ - Workflows: \`.agent/workflows/\`
1021
+
1022
+ See \`PROJECT_CONTEXT.md\` for stack details, routing, and auto-resolved skills.
1023
+ `;
1024
+ }
1025
+
884
1026
  function generateAiContextRefinementPrompt(data) {
885
1027
  const evidence = (data.scan_evidence || []).length > 0
886
1028
  ? (data.scan_evidence || []).map(line => `- ${line}`).join('\n')
@@ -1121,6 +1263,7 @@ async function main() {
1121
1263
  data.max_file_lines = data.max_file_lines || scanned?.max_file_lines || '300';
1122
1264
  data.framework_details = scanned?.framework_details || [];
1123
1265
  data.scan_evidence = scanned?.scan_evidence || [];
1266
+ data.app_root = scanned?.app_root || '.';
1124
1267
 
1125
1268
  if (!assumeYes) {
1126
1269
  const aiRefineAnswer = await askQuestions({
@@ -1170,6 +1313,10 @@ async function main() {
1170
1313
  fs.writeFileSync(claudeMdPath, generateClaudeMd(data), 'utf8');
1171
1314
  console.log(` ${pc.green('โœ“')} Generated ${pc.bold('CLAUDE.md')}`);
1172
1315
 
1316
+ const agentsMdPath = path.join(targetDir, 'AGENTS.md');
1317
+ fs.writeFileSync(agentsMdPath, generateAgentsMd(data), 'utf8');
1318
+ console.log(` ${pc.green('โœ“')} Generated ${pc.bold('AGENTS.md')}`);
1319
+
1173
1320
  let aiRefinementPath = null;
1174
1321
  if (requestAiRefinement) {
1175
1322
  aiRefinementPath = path.join(targetDir, 'AI_CONTEXT_REFINEMENT.md');
@@ -1191,13 +1338,14 @@ ${autoSkills.map(s => ' ยท ' + s).join('\n')}
1191
1338
  ${pc.bold('Generated files:')}
1192
1339
  ยท ${pc.bold(projectContextPath)}
1193
1340
  ยท ${pc.bold(claudeMdPath)}
1341
+ ยท ${pc.bold(agentsMdPath)}
1194
1342
  ${aiRefinementPath ? ` ยท ${pc.bold(aiRefinementPath)}` : ''}
1195
1343
 
1196
1344
  ${pc.bold('Next steps:')}
1197
1345
  1. Your kit is installed in ${pc.bold('./.agent')}
1198
1346
  2. Review ${pc.bold('PROJECT_CONTEXT.md')} first and add architecture notes
1199
1347
  3. ${aiRefinementPath ? `Open ${pc.bold('AI_CONTEXT_REFINEMENT.md')} in your AI editor to refine project context` : `Instruct the agent to read/update ${pc.bold('PROJECT_CONTEXT.md')} before major tasks`}
1200
- 4. Open project in Antigravity or Claude Code โ€” the agent will read context on start
1348
+ 4. Open project in Claude Code or Codex โ€” the agent will read context on start
1201
1349
  `);
1202
1350
  }
1203
1351
 
package/kit/README.md CHANGED
@@ -1,6 +1,6 @@
1
- # ๐Ÿ›ก๏ธ Antigravity Project Kit
2
-
3
- This toolkit helps you build high-quality, modular, and performance-optimized applications by leveraging the full power of AI Agents with a Context-First approach.
1
+ # ๐Ÿ›ก๏ธ Cross-Platform Project Kit
2
+
3
+ This toolkit helps you build high-quality, modular, and performance-optimized applications by leveraging AI agents with a context-first approach across Antigravity, Claude Code, and Codex-compatible editors.
4
4
 
5
5
  ---
6
6
 
@@ -11,10 +11,11 @@ This toolkit helps you build high-quality, modular, and performance-optimized ap
11
11
  - Update project metadata (e.g., `package.json`).
12
12
  - Rename and fill out `PROJECT_CONTEXT.template.md` as `PROJECT_CONTEXT.md` in the root directory.
13
13
 
14
- ### 2. Working with AI Agents
15
- - Always use the templates provided in `kit/PROMPT_RECIPE.md`.
16
- - Ensure the Agent reads and updates `PROJECT_CONTEXT.md`.
17
- - Enforce file splitting rules (e.g., Max 250 lines) to maintain a clean codebase.
14
+ ### 2. Working with AI Agents
15
+ - Always use the templates provided in `kit/PROMPT_RECIPE.md`.
16
+ - Ensure the Agent reads and updates `PROJECT_CONTEXT.md`.
17
+ - Use `CLAUDE.md` for Claude Code and `AGENTS.md` for Codex when generating project-specific entry files.
18
+ - Enforce file splitting rules (e.g., Max 250 lines) to maintain a clean codebase.
18
19
 
19
20
  ### 3. Proposed Structure
20
21
  - `.agent/rules/GEMINI.md`: Core behavioral rules and protocols.
@@ -29,4 +30,4 @@ This toolkit helps you build high-quality, modular, and performance-optimized ap
29
30
 
30
31
  ---
31
32
 
32
- *Powered by Antigravity Framework.*
33
+ *Powered by thachvd-kit.*
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "thachvd-kit",
3
- "version": "1.0.8",
4
- "description": "Universal Agent Kit for Antigravity, Claude Code, and OpenCode",
3
+ "version": "1.0.11",
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
@@ -21,7 +21,7 @@ trigger: always_on
21
21
  ```
22
22
 
23
23
  > All routing decisions below depend on PROJECT_CONTEXT.md.
24
- > Without it, ask user to run: `python scripts/init.py`
24
+ > Without it, ask user to run: `thachvd-kit init`
25
25
  > Baseline context must be refined once when `context.needs_ai_refinement = true`.
26
26
 
27
27
  ---
@@ -203,7 +203,7 @@ scripts/ โ†’ scripts/init.py, checklist.py, verify_all.py
203
203
  | Platform | Rules File | Skills Dir | Workflows |
204
204
  |----------|-----------|-----------|-----------|
205
205
  | **Antigravity** | `rules/GEMINI.md` (this file) | `skills/` | `workflows/` |
206
- | **Claude Code** | `CLAUDE.md` at project root | `.claude/skills/` | `.claude/commands/` |
207
- | **Codex / OpenCode** | `CLAUDE.md` at project root | `.claude/skills/` | โ€” |
208
-
209
- > Run `python scripts/init.py --platform all` to generate platform-specific config files automatically.
206
+ | **Claude Code** | `CLAUDE.md` at project root | `.agent/skills/` | `.agent/workflows/` |
207
+ | **Codex / OpenCode** | `AGENTS.md` at project root | `.agent/skills/` | `.agent/workflows/` |
208
+
209
+ > Run `thachvd-kit init` to generate project context and platform entry files automatically.
package/scripts/init.py CHANGED
@@ -3,10 +3,11 @@
3
3
  thachvd-kit โ€” Project Init Wizard
4
4
  Generates PROJECT_CONTEXT.md and optional platform config files.
5
5
 
6
- Usage:
7
- python scripts/init.py # Interactive wizard
8
- python scripts/init.py --platform all # Also generate CLAUDE.md
9
- python scripts/init.py --platform claude # Only CLAUDE.md
6
+ Usage:
7
+ python scripts/init.py # Interactive wizard
8
+ python scripts/init.py --platform all # Also generate CLAUDE.md + AGENTS.md
9
+ python scripts/init.py --platform claude # Only CLAUDE.md
10
+ python scripts/init.py --platform codex # Only AGENTS.md
10
11
  """
11
12
 
12
13
  import sys
@@ -156,6 +157,7 @@ CLOUDS_ALL = ["aws", "gcp", "azure", "vercel", "fly", "railway", "digitalocean",
156
157
  FRONTEND_FRAMEWORKS = ["vue", "react", "next", "nuxt", "svelte", "angular"]
157
158
  BACKEND_FRAMEWORKS = ["laravel", "cakephp", "symfony", "codeigniter", "express", "fastapi", "django", "flask", "gin", "fiber", "echo", "nestjs", "rails", "spring", "aspnet", "axum", "actix"]
158
159
  MOBILE_DESKTOP_FRAMEWORKS = ["tauri", "flutter", "react-native"]
160
+ IGNORED_SCAN_DIRS = {".git", "node_modules", "vendor", "dist", "build", "coverage", ".next", ".nuxt", ".turbo", ".cache", "storage", "tmp"}
159
161
 
160
162
  def read_text_if_exists(*parts):
161
163
  path = Path.cwd().joinpath(*parts)
@@ -171,23 +173,26 @@ def read_json_if_exists(*parts):
171
173
  except Exception:
172
174
  return None
173
175
 
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"
176
+ def detect_package_manager(base_dir=None):
177
+ base_dir = base_dir or Path.cwd()
178
+ if path_exists(base_dir, "pnpm-lock.yaml"): return "pnpm"
179
+ if path_exists(base_dir, "yarn.lock"): return "yarn"
180
+ if path_exists(base_dir, "bun.lockb") or path_exists(base_dir, "bun.lock"): return "bun"
181
+ if path_exists(base_dir, "package-lock.json"): return "npm"
182
+ if path_exists(base_dir, "uv.lock"): return "uv"
183
+ if path_exists(base_dir, "poetry.lock"): return "poetry"
184
+ if path_exists(base_dir, "requirements.txt") or path_exists(base_dir, "pyproject.toml"): return "pip"
185
+ if path_exists(base_dir, "go.mod"): return "go mod"
186
+ if path_exists(base_dir, "Cargo.toml"): return "cargo"
187
+ if path_exists(base_dir, "composer.json"): return "composer"
186
188
  return "auto-detect"
187
189
 
188
190
  def uniq(values):
189
191
  return list(dict.fromkeys(v for v in values if v))
190
192
 
193
+ def path_exists(base_dir, *parts):
194
+ return base_dir.joinpath(*parts).exists()
195
+
191
196
  def evidence_if(condition, label, weight=1):
192
197
  return {"label": label, "weight": weight} if condition else None
193
198
 
@@ -215,10 +220,73 @@ def has_composer_dependency(composer_dep_names, *names):
215
220
  def text_matches(text, pattern):
216
221
  return bool(text and re.search(pattern, text, re.IGNORECASE | re.MULTILINE))
217
222
 
223
+ def detect_app_root(base_dir):
224
+ candidates = [base_dir]
225
+ queue = [(base_dir, 0)]
226
+ while queue:
227
+ current, depth = queue.pop(0)
228
+ if depth >= 2:
229
+ continue
230
+ try:
231
+ entries = list(current.iterdir())
232
+ except Exception:
233
+ continue
234
+ for entry in entries:
235
+ if not entry.is_dir():
236
+ continue
237
+ if entry.name in IGNORED_SCAN_DIRS:
238
+ continue
239
+ candidates.append(entry)
240
+ queue.append((entry, depth + 1))
241
+
242
+ def score_candidate(path):
243
+ score = 0
244
+ if path_exists(path, "composer.json"): score += 5
245
+ if path_exists(path, "package.json"): score += 4
246
+ if path_exists(path, "Cargo.toml"): score += 4
247
+ if path_exists(path, "go.mod"): score += 4
248
+ if path_exists(path, "pyproject.toml") or path_exists(path, "requirements.txt"): score += 4
249
+ if path_exists(path, "artisan"): score += 5
250
+ if path_exists(path, "bin", "cake") or path_exists(path, "app", "Config", "core.php"): score += 5
251
+ if path_exists(path, "src-tauri", "tauri.conf.json") or path_exists(path, "src-tauri", "tauri.conf.json5"): score += 5
252
+ if path_exists(path, "manage.py"): score += 4
253
+ if path_exists(path, "Gemfile"): score += 4
254
+ if path_exists(path, "pom.xml") or path_exists(path, "build.gradle") or path_exists(path, "build.gradle.kts"): score += 4
255
+ 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
256
+ return score
257
+
258
+ ranked = [{"path": path, "score": score_candidate(path)} for path in candidates]
259
+ ranked = [item for item in ranked if item["score"] > 0]
260
+ ranked.sort(key=lambda item: (-item["score"], len(str(item["path"]))))
261
+ return ranked[0]["path"] if ranked else base_dir
262
+
263
+ def repo_has_any(base_dir, relative_paths, max_depth=3):
264
+ queue = [(base_dir, 0)]
265
+ while queue:
266
+ current, depth = queue.pop(0)
267
+ for rel_parts in relative_paths:
268
+ if path_exists(current, *rel_parts):
269
+ return True
270
+ if depth >= max_depth:
271
+ continue
272
+ try:
273
+ entries = list(current.iterdir())
274
+ except Exception:
275
+ continue
276
+ for entry in entries:
277
+ if not entry.is_dir():
278
+ continue
279
+ if entry.name in IGNORED_SCAN_DIRS:
280
+ continue
281
+ queue.append((entry, depth + 1))
282
+ return False
283
+
218
284
  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 []
285
+ repo_root = Path.cwd()
286
+ app_root = detect_app_root(repo_root)
287
+ pkg = read_json_if_exists(app_root, "package.json") or {}
288
+ composer = read_json_if_exists(app_root, "composer.json") or {}
289
+ root_entries = [p.name for p in app_root.iterdir()] if app_root.exists() else []
222
290
  root_csproj = next((name for name in root_entries if name.endswith(".csproj")), "")
223
291
  deps = {}
224
292
  for key in ("dependencies", "devDependencies", "peerDependencies"):
@@ -228,53 +296,53 @@ def scan_project():
228
296
  for key in ("require", "require-dev"):
229
297
  composer_deps.update(composer.get(key, {}) or {})
230
298
  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")
299
+ requirements = read_text_if_exists(app_root, "requirements.txt")
300
+ pyproject = read_text_if_exists(app_root, "pyproject.toml")
301
+ go_mod = read_text_if_exists(app_root, "go.mod")
302
+ pubspec = read_text_if_exists(app_root, "pubspec.yaml")
303
+ gemfile = read_text_if_exists(app_root, "Gemfile")
304
+ pom = read_text_if_exists(app_root, "pom.xml")
305
+ gradle = f"{read_text_if_exists(app_root, 'build.gradle')}\n{read_text_if_exists(app_root, 'build.gradle.kts')}"
306
+ cargo = f"{read_text_if_exists(app_root, 'Cargo.toml')}\n{read_text_if_exists(app_root, 'src-tauri/Cargo.toml')}"
307
+ csproj = read_text_if_exists(app_root, root_csproj) if root_csproj else ""
308
+ program_cs = read_text_if_exists(app_root, "Program.cs")
241
309
 
242
310
  language_detections = [
243
311
  build_detection("typescript", [
244
- evidence_if(Path("tsconfig.json").exists(), "found tsconfig.json", 4),
312
+ evidence_if(path_exists(app_root, "tsconfig.json"), "found tsconfig.json", 4),
245
313
  evidence_if(has_dependency(dep_names, "typescript", "ts-node", "tsx"), "package.json -> typescript toolchain", 2),
246
314
  ]),
247
315
  build_detection("javascript", [
248
- evidence_if(Path("package.json").exists(), "found package.json", 2),
316
+ evidence_if(path_exists(app_root, "package.json"), "found package.json", 2),
249
317
  evidence_if(bool(dep_names), "package.json -> JavaScript dependency graph", 1),
250
318
  ]),
251
319
  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),
320
+ evidence_if(path_exists(app_root, "requirements.txt"), "found requirements.txt", 3),
321
+ evidence_if(path_exists(app_root, "pyproject.toml"), "found pyproject.toml", 2),
254
322
  evidence_if(text_matches(pyproject, r"\[tool\.(poetry|uv|hatch|pdm)\]"), "pyproject.toml -> Python tool section", 2),
255
323
  ]),
256
324
  build_detection("go", [
257
- evidence_if(Path("go.mod").exists(), "found go.mod", 4),
325
+ evidence_if(path_exists(app_root, "go.mod"), "found go.mod", 4),
258
326
  ]),
259
327
  build_detection("php", [
260
- evidence_if(Path("composer.json").exists(), "found composer.json", 4),
328
+ evidence_if(path_exists(app_root, "composer.json"), "found composer.json", 4),
261
329
  evidence_if(bool(composer_dep_names), "composer.json -> PHP dependency graph", 1),
262
330
  ]),
263
331
  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),
332
+ evidence_if(path_exists(app_root, "Cargo.toml"), "found Cargo.toml", 4),
333
+ evidence_if(path_exists(app_root, "src-tauri", "Cargo.toml"), "found src-tauri/Cargo.toml", 5),
266
334
  ]),
267
335
  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),
336
+ evidence_if(path_exists(app_root, "pom.xml"), "found pom.xml", 4),
337
+ evidence_if(path_exists(app_root, "build.gradle") or path_exists(app_root, "build.gradle.kts"), "found Gradle build file", 3),
270
338
  ]),
271
339
  build_detection("ruby", [
272
- evidence_if(Path("Gemfile").exists(), "found Gemfile", 4),
273
- evidence_if(Path("config.ru").exists(), "found config.ru", 1),
340
+ evidence_if(path_exists(app_root, "Gemfile"), "found Gemfile", 4),
341
+ evidence_if(path_exists(app_root, "config.ru"), "found config.ru", 1),
274
342
  ]),
275
343
  build_detection("csharp", [
276
344
  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),
345
+ evidence_if(path_exists(app_root, "Program.cs"), "found Program.cs", 1),
278
346
  ]),
279
347
  ]
280
348
  language_detections = [item for item in language_detections if item["score"] >= 2]
@@ -283,8 +351,8 @@ def scan_project():
283
351
  framework_details = [
284
352
  {**build_detection("next", [
285
353
  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),
354
+ 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),
355
+ evidence_if(path_exists(app_root, "app") or path_exists(app_root, "pages"), "found app/ or pages/", 1),
288
356
  ]), "kind": "frontend", "project_type": "web-app"},
289
357
  {**build_detection("react", [
290
358
  evidence_if(has_dependency(dep_names, "react"), "package.json -> react", 4),
@@ -295,14 +363,14 @@ def scan_project():
295
363
  ]), "kind": "frontend", "project_type": "web-app"},
296
364
  {**build_detection("nuxt", [
297
365
  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),
366
+ evidence_if(path_exists(app_root, "nuxt.config.ts") or path_exists(app_root, "nuxt.config.js"), "found nuxt.config.*", 3),
299
367
  ]), "kind": "frontend", "project_type": "web-app"},
300
368
  {**build_detection("svelte", [
301
369
  evidence_if(has_dependency(dep_names, "svelte", "@sveltejs/kit"), "package.json -> svelte", 4),
302
370
  ]), "kind": "frontend", "project_type": "web-app"},
303
371
  {**build_detection("angular", [
304
372
  evidence_if(has_dependency(dep_names, "@angular/core"), "package.json -> @angular/core", 4),
305
- evidence_if(Path("angular.json").exists(), "found angular.json", 3),
373
+ evidence_if(path_exists(app_root, "angular.json"), "found angular.json", 3),
306
374
  ]), "kind": "frontend", "project_type": "web-app"},
307
375
  {**build_detection("express", [
308
376
  evidence_if(has_dependency(dep_names, "express"), "package.json -> express", 4),
@@ -315,39 +383,39 @@ def scan_project():
315
383
  ]), "kind": "backend", "project_type": "api", "language": "python"},
316
384
  {**build_detection("django", [
317
385
  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
- evidence_if(Path("templates").exists(), "found templates/", 1),
386
+ evidence_if(path_exists(app_root, "manage.py"), "found manage.py", 2),
387
+ evidence_if(path_exists(app_root, "templates"), "found templates/", 1),
320
388
  ]), "kind": "backend", "project_type": "web-app", "language": "python"},
321
389
  {**build_detection("flask", [
322
390
  evidence_if(text_matches(requirements, r"\bflask\b") or text_matches(pyproject, r"\bflask\b"), "Python deps -> flask", 4),
323
391
  ]), "kind": "backend", "project_type": "api", "language": "python"},
324
392
  {**build_detection("laravel", [
325
393
  evidence_if(has_composer_dependency(composer_dep_names, "laravel/framework"), "composer.json -> laravel/framework", 4),
326
- evidence_if(Path("artisan").exists(), "found artisan", 3),
327
- evidence_if(Path("resources/views").exists(), "found resources/views", 1),
394
+ evidence_if(path_exists(app_root, "artisan"), "found artisan", 3),
395
+ evidence_if(path_exists(app_root, "resources", "views"), "found resources/views", 1),
328
396
  ]), "kind": "backend", "project_type": "web-app", "language": "php"},
329
397
  {**build_detection("cakephp", [
330
398
  evidence_if(has_composer_dependency(composer_dep_names, "cakephp/cakephp"), "composer.json -> cakephp/cakephp", 4),
331
- evidence_if(Path("bin/cake").exists(), "found bin/cake", 3),
332
- evidence_if(Path("config/app.php").exists(), "found config/app.php", 2),
333
- evidence_if(Path("app/Config/core.php").exists(), "found app/Config/core.php (CakePHP legacy)", 3),
334
- evidence_if(Path("app/Controller/AppController.php").exists(), "found app/Controller/AppController.php", 2),
335
- evidence_if(Path("app/Model/AppModel.php").exists(), "found app/Model/AppModel.php", 2),
336
- evidence_if(Path("lib/Cake").exists(), "found lib/Cake", 3),
337
- evidence_if(Path("cake/console/cake").exists(), "found cake/console/cake", 3),
338
- evidence_if(Path("app/View").exists(), "found app/View", 2),
339
- evidence_if(any(p.suffix.lower() == ".ctp" for p in Path("app/View").rglob("*.ctp")) if Path("app/View").exists() else False, "found .ctp view templates", 2),
399
+ evidence_if(path_exists(app_root, "bin", "cake"), "found bin/cake", 3),
400
+ evidence_if(path_exists(app_root, "config", "app.php"), "found config/app.php", 2),
401
+ evidence_if(path_exists(app_root, "app", "Config", "core.php"), "found app/Config/core.php (CakePHP legacy)", 3),
402
+ evidence_if(path_exists(app_root, "app", "Controller", "AppController.php"), "found app/Controller/AppController.php", 2),
403
+ evidence_if(path_exists(app_root, "app", "Model", "AppModel.php"), "found app/Model/AppModel.php", 2),
404
+ evidence_if(path_exists(app_root, "lib", "Cake"), "found lib/Cake", 3),
405
+ evidence_if(path_exists(app_root, "cake", "console", "cake"), "found cake/console/cake", 3),
406
+ evidence_if(path_exists(app_root, "app", "View"), "found app/View", 2),
407
+ 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),
340
408
  ]), "kind": "backend", "project_type": "web-app", "language": "php"},
341
409
  {**build_detection("symfony", [
342
410
  evidence_if(has_composer_dependency(composer_dep_names, "symfony/framework-bundle"), "composer.json -> symfony/framework-bundle", 4),
343
- evidence_if(Path("bin/console").exists(), "found bin/console", 2),
344
- evidence_if(Path("config/bundles.php").exists(), "found config/bundles.php", 1),
345
- evidence_if(Path("templates").exists(), "found templates/", 1),
411
+ evidence_if(path_exists(app_root, "bin", "console"), "found bin/console", 2),
412
+ evidence_if(path_exists(app_root, "config", "bundles.php"), "found config/bundles.php", 1),
413
+ evidence_if(path_exists(app_root, "templates"), "found templates/", 1),
346
414
  ]), "kind": "backend", "project_type": "web-app", "language": "php"},
347
415
  {**build_detection("codeigniter", [
348
416
  evidence_if(has_composer_dependency(composer_dep_names, "codeigniter4/framework"), "composer.json -> codeigniter4/framework", 4),
349
- evidence_if(Path("spark").exists(), "found spark", 2),
350
- evidence_if(Path("app/Config/App.php").exists(), "found app/Config/App.php", 1),
417
+ evidence_if(path_exists(app_root, "spark"), "found spark", 2),
418
+ evidence_if(path_exists(app_root, "app", "Config", "App.php"), "found app/Config/App.php", 1),
351
419
  ]), "kind": "backend", "project_type": "api", "language": "php"},
352
420
  {**build_detection("gin", [
353
421
  evidence_if(text_matches(go_mod, r"\bgithub\.com\/gin-gonic\/gin\b"), "go.mod -> github.com/gin-gonic/gin", 4),
@@ -360,8 +428,8 @@ def scan_project():
360
428
  ]), "kind": "backend", "project_type": "api", "language": "go"},
361
429
  {**build_detection("rails", [
362
430
  evidence_if(text_matches(gemfile, r'^\s*gem\s+["\']rails["\']'), "Gemfile -> rails", 4),
363
- evidence_if(Path("bin/rails").exists(), "found bin/rails", 2),
364
- evidence_if(Path("app/views").exists(), "found app/views", 1),
431
+ evidence_if(path_exists(app_root, "bin", "rails"), "found bin/rails", 2),
432
+ evidence_if(path_exists(app_root, "app", "views"), "found app/views", 1),
365
433
  ]), "kind": "backend", "project_type": "web-app", "language": "ruby"},
366
434
  {**build_detection("spring", [
367
435
  evidence_if(text_matches(pom, r"spring-boot") or text_matches(gradle, r"spring-boot"), "Java build -> spring-boot", 4),
@@ -372,8 +440,8 @@ def scan_project():
372
440
  ]), "kind": "backend", "project_type": "api", "language": "csharp"},
373
441
  {**build_detection("tauri", [
374
442
  evidence_if(has_dependency(dep_names, "@tauri-apps/api"), "package.json -> @tauri-apps/api", 4),
375
- evidence_if(Path("src-tauri/tauri.conf.json").exists() or Path("src-tauri/tauri.conf.json5").exists(), "found src-tauri/tauri.conf.*", 4),
376
- evidence_if(Path("src-tauri/Cargo.toml").exists(), "found src-tauri/Cargo.toml", 2),
443
+ 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),
444
+ evidence_if(path_exists(app_root, "src-tauri", "Cargo.toml"), "found src-tauri/Cargo.toml", 2),
377
445
  ]), "kind": "desktop", "project_type": "desktop", "language": "rust"},
378
446
  {**build_detection("axum", [
379
447
  evidence_if(text_matches(cargo, r"\baxum\b"), "Cargo.toml -> axum", 4),
@@ -413,7 +481,7 @@ def scan_project():
413
481
  elif any(item["project_type"] == "web-app" for item in framework_details): project_type = "web-app"
414
482
  elif any(item["kind"] == "backend" for item in framework_details): project_type = "api"
415
483
  elif any(item["kind"] == "frontend" for item in framework_details): project_type = "web-app"
416
- elif Path("package.json").exists() and pkg.get("bin"): project_type = "cli"
484
+ elif path_exists(app_root, "package.json") and pkg.get("bin"): project_type = "cli"
417
485
  elif primary_language in ["php", "python", "go", "java", "ruby", "csharp", "rust"]: project_type = "api"
418
486
 
419
487
  if has_dependency(dep_names, "pg", "postgres", "@prisma/client") or "postgres" in json.dumps(pkg).lower(): database = "postgres"
@@ -428,26 +496,26 @@ def scan_project():
428
496
  elif has_dependency(dep_names, "pytest") or text_matches(requirements, r"\bpytest\b") or text_matches(pyproject, r"\bpytest\b"): test_framework = "pytest"
429
497
  elif has_composer_dependency(composer_dep_names, "phpunit/phpunit"): test_framework = "phpunit"
430
498
  elif has_composer_dependency(composer_dep_names, "pestphp/pest"): test_framework = "pest"
431
- elif Path("go.mod").exists(): test_framework = "go test"
499
+ elif path_exists(app_root, "go.mod"): test_framework = "go test"
432
500
  elif text_matches(gemfile, r'^\s*gem\s+["\']rspec["\']'): test_framework = "rspec"
433
501
 
434
- if Path("Dockerfile").exists() or Path("docker-compose.yml").exists() or Path("docker-compose.yaml").exists():
502
+ if repo_has_any(repo_root, [("Dockerfile",), ("docker-compose.yml",), ("docker-compose.yaml",)]):
435
503
  infrastructure.append("docker")
436
504
 
437
- if Path("vercel.json").exists():
505
+ if repo_has_any(repo_root, [("vercel.json",)]):
438
506
  cloud = "vercel"
439
507
  infrastructure.append("vercel")
440
- elif Path("fly.toml").exists():
508
+ elif repo_has_any(repo_root, [("fly.toml",)]):
441
509
  cloud = "fly"
442
510
  infrastructure.append("fly")
443
- elif Path("railway.json").exists() or Path("railway.toml").exists():
511
+ elif repo_has_any(repo_root, [("railway.json",), ("railway.toml",)]):
444
512
  cloud = "railway"
445
513
  infrastructure.append("railway")
446
514
 
447
- if Path(".github/workflows").exists(): ci_cd = "github-actions"
448
- elif Path(".gitlab-ci.yml").exists(): ci_cd = "gitlab-ci"
449
- elif Path(".circleci/config.yml").exists(): ci_cd = "circleci"
450
- elif Path("Jenkinsfile").exists(): ci_cd = "jenkins"
515
+ if repo_has_any(repo_root, [(".github", "workflows")], max_depth=2): ci_cd = "github-actions"
516
+ elif repo_has_any(repo_root, [(".gitlab-ci.yml",)], max_depth=2): ci_cd = "gitlab-ci"
517
+ elif repo_has_any(repo_root, [(".circleci", "config.yml")], max_depth=2): ci_cd = "circleci"
518
+ elif repo_has_any(repo_root, [("Jenkinsfile",)], max_depth=2): ci_cd = "jenkins"
451
519
 
452
520
  scan_evidence = []
453
521
  if language_detections:
@@ -456,6 +524,7 @@ def scan_project():
456
524
 
457
525
  return {
458
526
  "project_name": Path.cwd().name,
527
+ "app_root": str(app_root.relative_to(repo_root)) if app_root != repo_root else ".",
459
528
  "description": pkg.get("description") or composer.get("description", "A software project"),
460
529
  "project_type": project_type if project_type in PROJECT_TYPES_ALL else "web-app",
461
530
  "primary_language": primary_language if primary_language in LANGUAGES_ALL else "typescript",
@@ -463,7 +532,7 @@ def scan_project():
463
532
  "framework_details": framework_details,
464
533
  "scan_evidence": scan_evidence,
465
534
  "database": database if database in DATABASES_ALL else "none",
466
- "package_manager": detect_package_manager(),
535
+ "package_manager": detect_package_manager(app_root),
467
536
  "uses_docker": "docker" in infrastructure,
468
537
  "cloud": cloud,
469
538
  "infrastructure": uniq(infrastructure),
@@ -568,10 +637,11 @@ context:
568
637
  ## Stack
569
638
 
570
639
  ```yaml
571
- stack:
572
- primary_language: {data['primary_language']}
573
- frameworks: [{', '.join(frameworks)}]
574
- database: {data.get('database', 'none')}
640
+ stack:
641
+ app_root: {data.get('app_root', '.')}
642
+ primary_language: {data['primary_language']}
643
+ frameworks: [{', '.join(frameworks)}]
644
+ database: {data.get('database', 'none')}
575
645
  infrastructure: [{', '.join(infrastructure)}]
576
646
  package_manager: {data.get('package_manager', 'auto-detect')}
577
647
  test_framework: {data.get('test_framework', 'auto-detect')}
@@ -626,14 +696,14 @@ Based on your stack, these skills will be loaded automatically:
626
696
  """
627
697
 
628
698
  def generate_claude_md(data):
629
- """Generate CLAUDE.md for Claude Code / Codex compatibility."""
630
- return f"""# CLAUDE.md โ€” {data['project_name']}
631
- # Generated by thachvd-kit. See PROJECT_CONTEXT.md for full stack info.
632
- # Cross-platform: Claude Code ยท Codex ยท OpenCode
633
-
634
- ## Project
635
- {data['description']}
636
- Stack: {data['primary_language']} | {', '.join(data.get('frameworks', []))}
699
+ """Generate CLAUDE.md for Claude Code."""
700
+ return f"""# CLAUDE.md โ€” {data['project_name']}
701
+ # Generated by thachvd-kit. See PROJECT_CONTEXT.md for full stack info.
702
+ # Platform: Claude Code
703
+
704
+ ## Project
705
+ {data['description']}
706
+ Stack: {data['primary_language']} | {', '.join(data.get('frameworks', []))}
637
707
 
638
708
  ## Rules
639
709
  - Read PROJECT_CONTEXT.md first for stack details and agent routing.
@@ -657,6 +727,47 @@ Stack: {data['primary_language']} | {', '.join(data.get('frameworks', []))}
657
727
  See PROJECT_CONTEXT.md for full agent routing table.
658
728
  """
659
729
 
730
+ def generate_agents_md(data):
731
+ """Generate AGENTS.md for Codex and AGENTS-compatible editors."""
732
+ return f"""# AGENTS.md โ€” {data['project_name']}
733
+ # Generated by thachvd-kit. See PROJECT_CONTEXT.md for full stack info.
734
+ # Platform: Codex / AGENTS-compatible editors
735
+
736
+ ## Project
737
+ {data['description']}
738
+ Stack: {data['primary_language']} | {', '.join(data.get('frameworks', [])) or 'no framework'}
739
+
740
+ ## Mandatory First Action
741
+ - Read `PROJECT_CONTEXT.md` before making plans or code changes.
742
+ - If `PROJECT_CONTEXT.md` is missing or stale, create/update it before major work.
743
+ - If `context.needs_ai_refinement: true`, refine `PROJECT_CONTEXT.md` first, then set:
744
+ - `context.status: refined`
745
+ - `context.needs_ai_refinement: false`
746
+ - `context.last_refined_at: <today>`
747
+
748
+ ## Working Rules
749
+ - Use `.agent/agents`, `.agent/skills`, `.agent/rules`, and `.agent/workflows` as the local project kit.
750
+ - Match the task against the Agent Routing table in `PROJECT_CONTEXT.md`.
751
+ - Load relevant skills before editing code.
752
+ - Tests are mandatory. Verify before claiming done.
753
+ - Keep files under {data.get('max_file_lines', '300')} lines when practical, then split into modules.
754
+ - Update dependent files together when a change crosses boundaries.
755
+
756
+ ## Preferred Workflow
757
+ - Plan work from `PROJECT_CONTEXT.md` first.
758
+ - For feature work, use: `plan-writing` -> `executing-plans` -> `verification-before-completion`.
759
+ - For reviews, use: `requesting-code-review` / `receiving-code-review`.
760
+ - For debugging, use: `systematic-debugging`.
761
+
762
+ ## Key Paths
763
+ - Project context: `PROJECT_CONTEXT.md`
764
+ - Agent kit: `.agent/`
765
+ - Skills: `.agent/skills/`
766
+ - Workflows: `.agent/workflows/`
767
+
768
+ See `PROJECT_CONTEXT.md` for stack details, routing, and auto-resolved skills.
769
+ """
770
+
660
771
  def generate_ai_context_refinement_prompt(data):
661
772
  scan_evidence = data.get("scan_evidence", [])
662
773
  evidence_block = "\n".join("- " + line for line in scan_evidence) if scan_evidence else "- No scan evidence was captured during init."
@@ -720,6 +831,7 @@ When done, summarize:
720
831
 
721
832
  def format_scan_preview_lines(scanned):
722
833
  return [
834
+ ("App root", scanned.get("app_root", ".")),
723
835
  ("Project type", scanned.get("project_type", "unknown")),
724
836
  ("Primary language", scanned.get("primary_language", "unknown")),
725
837
  ("Frameworks", ", ".join(scanned.get("frameworks", [])) or "no framework"),
@@ -735,7 +847,7 @@ def format_scan_preview_lines(scanned):
735
847
 
736
848
  def main():
737
849
  parser = argparse.ArgumentParser(description="thachvd-kit project init wizard")
738
- parser.add_argument("--platform", choices=["antigravity", "claude", "all"], default="antigravity",
850
+ parser.add_argument("--platform", choices=["antigravity", "claude", "codex", "all"], default="antigravity",
739
851
  help="Which platform config to generate (default: antigravity)")
740
852
  parser.add_argument("--yes", "-y", action="store_true", help="Use defaults without prompting")
741
853
  parser.add_argument("--scan", action="store_true", help="Scan the current project to prefill or auto-generate PROJECT_CONTEXT.md")
@@ -863,6 +975,7 @@ def main():
863
975
  data["code_style"] = data.get("code_style", (scanned or {}).get("code_style", "project-standard"))
864
976
  data["framework_details"] = data.get("framework_details", (scanned or {}).get("framework_details", []))
865
977
  data["scan_evidence"] = data.get("scan_evidence", (scanned or {}).get("scan_evidence", []))
978
+ data["app_root"] = data.get("app_root", (scanned or {}).get("app_root", "."))
866
979
  data["context_status"] = data.get("context_status", "baseline")
867
980
  data["needs_ai_refinement"] = data.get("needs_ai_refinement", False)
868
981
  data["last_refined_at"] = data.get("last_refined_at", "not-yet-refined")
@@ -885,11 +998,17 @@ def main():
885
998
  output_path.write_text(context_content, encoding="utf-8")
886
999
  print(f" {green('โœ“')} {bold('PROJECT_CONTEXT.md')} generated")
887
1000
 
888
- # CLAUDE.md (for Claude Code / Codex)
1001
+ # CLAUDE.md (for Claude Code)
889
1002
  if args.platform in ("claude", "all"):
890
1003
  claude_path = Path("CLAUDE.md")
891
1004
  claude_path.write_text(generate_claude_md(data), encoding="utf-8")
892
- print(f" {green('โœ“')} {bold('CLAUDE.md')} generated (Claude Code / Codex)")
1005
+ print(f" {green('โœ“')} {bold('CLAUDE.md')} generated (Claude Code)")
1006
+
1007
+ # AGENTS.md (for Codex / AGENTS-compatible editors)
1008
+ if args.platform in ("codex", "all"):
1009
+ agents_path = Path("AGENTS.md")
1010
+ agents_path.write_text(generate_agents_md(data), encoding="utf-8")
1011
+ print(f" {green('โœ“')} {bold('AGENTS.md')} generated (Codex)")
893
1012
 
894
1013
  ai_refine_path = None
895
1014
  if data.get("needs_ai_refinement"):
@@ -912,6 +1031,7 @@ def main():
912
1031
  {bold('Generated files:')}
913
1032
  ยท {bold(str(output_path.resolve()))}
914
1033
  ยท {bold(str(Path("CLAUDE.md").resolve())) if args.platform in ("claude", "all") else 'CLAUDE.md not generated for this platform'}
1034
+ ยท {bold(str(Path("AGENTS.md").resolve())) if args.platform in ("codex", "all") else 'AGENTS.md not generated for this platform'}
915
1035
  ยท {bold(str(ai_refine_path.resolve())) if ai_refine_path else 'AI_CONTEXT_REFINEMENT.md not generated'}
916
1036
 
917
1037
  {bold('Next steps:')}
@@ -920,7 +1040,7 @@ def main():
920
1040
  3. Open project in your AI editor (Antigravity / Claude Code / Codex)
921
1041
  4. The agent will read PROJECT_CONTEXT.md on session start
922
1042
 
923
- {dim(' Tip: Run with --platform all to also generate CLAUDE.md')}
1043
+ {dim(' Tip: Run with --platform all to also generate CLAUDE.md and AGENTS.md')}
924
1044
  """)
925
1045
 
926
1046
  if __name__ == "__main__":