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 +101 -71
- package/bin/cli.js +197 -49
- package/kit/README.md +9 -8
- package/package.json +3 -3
- package/rules/GEMINI.md +5 -5
- package/scripts/init.py +215 -95
package/README.md
CHANGED
|
@@ -1,82 +1,112 @@
|
|
|
1
|
-
# thachvd-kit
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
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
|
-
|
|
14
|
-
# or
|
|
15
|
-
npx github:thachvd/thachvd-kit init
|
|
12
|
+
npm install -g thachvd-kit
|
|
16
13
|
```
|
|
17
14
|
|
|
18
|
-
|
|
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
|
|
18
|
+
thachvd-kit init
|
|
25
19
|
```
|
|
26
20
|
|
|
27
|
-
|
|
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
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
-
|
|
60
|
-
-
|
|
61
|
-
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
-
|
|
66
|
-
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
.
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
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
|
|
177
|
-
|
|
178
|
-
if (
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
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
|
|
556
|
-
const
|
|
557
|
-
const
|
|
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:
|
|
578
|
-
pyprojectText:
|
|
579
|
-
goModText:
|
|
580
|
-
pubspecText:
|
|
581
|
-
gemfileText:
|
|
582
|
-
pomText:
|
|
583
|
-
gradleText: `${
|
|
584
|
-
cargoText: `${
|
|
585
|
-
csprojText: rootCsproj ?
|
|
586
|
-
programCsText:
|
|
587
|
-
viewFilesText: fs.existsSync(path.join(
|
|
588
|
-
? fs.readdirSync(path.join(
|
|
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 (
|
|
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 (
|
|
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 (
|
|
749
|
+
if (repoHasAny(targetDir, [['Dockerfile'], ['docker-compose.yml'], ['docker-compose.yaml']])) {
|
|
653
750
|
infrastructure.push('docker');
|
|
654
751
|
}
|
|
655
752
|
|
|
656
|
-
if (
|
|
753
|
+
if (repoHasAny(targetDir, [['vercel.json']])) {
|
|
657
754
|
cloud = 'vercel';
|
|
658
755
|
infrastructure.push('vercel');
|
|
659
|
-
} else if (
|
|
756
|
+
} else if (repoHasAny(targetDir, [['fly.toml']])) {
|
|
660
757
|
cloud = 'fly';
|
|
661
758
|
infrastructure.push('fly');
|
|
662
|
-
} else if (
|
|
759
|
+
} else if (repoHasAny(targetDir, [['railway.json'], ['railway.toml']])) {
|
|
663
760
|
cloud = 'railway';
|
|
664
761
|
infrastructure.push('railway');
|
|
665
762
|
}
|
|
666
763
|
|
|
667
|
-
if (
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
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
|
-
|
|
796
|
-
|
|
797
|
-
|
|
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
|
-
#
|
|
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
|
|
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
|
-
# ๐ก๏ธ
|
|
2
|
-
|
|
3
|
-
This toolkit helps you build high-quality, modular, and performance-optimized applications by leveraging
|
|
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
|
-
-
|
|
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
|
|
33
|
+
*Powered by thachvd-kit.*
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "thachvd-kit",
|
|
3
|
-
"version": "1.0.
|
|
4
|
-
"description": "
|
|
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": "
|
|
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: `
|
|
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 | `.
|
|
207
|
-
| **Codex / OpenCode** | `
|
|
208
|
-
|
|
209
|
-
> Run `
|
|
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
|
-
|
|
176
|
-
if (
|
|
177
|
-
if (
|
|
178
|
-
if (
|
|
179
|
-
if (
|
|
180
|
-
if (
|
|
181
|
-
if (
|
|
182
|
-
if (
|
|
183
|
-
if (
|
|
184
|
-
if (
|
|
185
|
-
if (
|
|
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
|
-
|
|
220
|
-
|
|
221
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
253
|
-
evidence_if(
|
|
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(
|
|
325
|
+
evidence_if(path_exists(app_root, "go.mod"), "found go.mod", 4),
|
|
258
326
|
]),
|
|
259
327
|
build_detection("php", [
|
|
260
|
-
evidence_if(
|
|
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(
|
|
265
|
-
evidence_if(
|
|
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(
|
|
269
|
-
evidence_if(
|
|
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(
|
|
273
|
-
evidence_if(
|
|
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(
|
|
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(
|
|
287
|
-
evidence_if(
|
|
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(
|
|
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(
|
|
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(
|
|
319
|
-
evidence_if(
|
|
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(
|
|
327
|
-
evidence_if(
|
|
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(
|
|
332
|
-
evidence_if(
|
|
333
|
-
evidence_if(
|
|
334
|
-
evidence_if(
|
|
335
|
-
evidence_if(
|
|
336
|
-
evidence_if(
|
|
337
|
-
evidence_if(
|
|
338
|
-
evidence_if(
|
|
339
|
-
evidence_if(any(p.suffix.lower() == ".ctp" for p in
|
|
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(
|
|
344
|
-
evidence_if(
|
|
345
|
-
evidence_if(
|
|
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(
|
|
350
|
-
evidence_if(
|
|
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(
|
|
364
|
-
evidence_if(
|
|
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(
|
|
376
|
-
evidence_if(
|
|
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
|
|
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
|
|
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
|
|
502
|
+
if repo_has_any(repo_root, [("Dockerfile",), ("docker-compose.yml",), ("docker-compose.yaml",)]):
|
|
435
503
|
infrastructure.append("docker")
|
|
436
504
|
|
|
437
|
-
if
|
|
505
|
+
if repo_has_any(repo_root, [("vercel.json",)]):
|
|
438
506
|
cloud = "vercel"
|
|
439
507
|
infrastructure.append("vercel")
|
|
440
|
-
elif
|
|
508
|
+
elif repo_has_any(repo_root, [("fly.toml",)]):
|
|
441
509
|
cloud = "fly"
|
|
442
510
|
infrastructure.append("fly")
|
|
443
|
-
elif
|
|
511
|
+
elif repo_has_any(repo_root, [("railway.json",), ("railway.toml",)]):
|
|
444
512
|
cloud = "railway"
|
|
445
513
|
infrastructure.append("railway")
|
|
446
514
|
|
|
447
|
-
if
|
|
448
|
-
elif
|
|
449
|
-
elif
|
|
450
|
-
elif
|
|
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
|
-
|
|
573
|
-
|
|
574
|
-
|
|
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
|
|
630
|
-
return f"""# CLAUDE.md โ {data['project_name']}
|
|
631
|
-
# Generated by thachvd-kit. See PROJECT_CONTEXT.md for full stack info.
|
|
632
|
-
#
|
|
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
|
|
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
|
|
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__":
|