ai-runtime-kit 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +307 -0
- package/bin/cli.js +52 -0
- package/package.json +40 -0
- package/runtime/BOOTSTRAP.md +230 -0
- package/runtime/CAPABILITIES.md +166 -0
- package/runtime/INDEX.md +397 -0
- package/runtime/PRIORITIES.md +84 -0
- package/runtime/RUNTIME_HEALTH.md +87 -0
- package/runtime/RUNTIME_MODE.md +109 -0
- package/runtime/RUNTIME_TRANSITIONS.md +141 -0
- package/runtime/RUNTIME_VERSION.md +17 -0
- package/runtime/SAFETY.md +156 -0
- package/runtime/adr/0000-template.md +55 -0
- package/runtime/agents/executor.md +19 -0
- package/runtime/agents/verifier.md +83 -0
- package/runtime/hooks/README.md +163 -0
- package/runtime/hooks/_template/HOOK.md +87 -0
- package/runtime/hooks/pre-executor/runtime-scoped-preflight/HOOK.md +189 -0
- package/runtime/memory/architecture/principles.md +107 -0
- package/runtime/memory/engineering/principles.md +102 -0
- package/runtime/memory/runtime/context-loading.md +107 -0
- package/runtime/plans/_template.md +81 -0
- package/runtime/prds/_template.md +73 -0
- package/runtime/reviews/_template.md +37 -0
- package/runtime/rules/README.md +101 -0
- package/runtime/rules/_template/RULE.md +75 -0
- package/runtime/skills/README.md +96 -0
- package/runtime/skills/_template/SKILL.md +61 -0
- package/runtime/specs/_template/spec.md +50 -0
- package/runtime/specs/_template-bug-fix/spec.md +120 -0
- package/runtime/tasks/TASK_STATUS.md +58 -0
- package/runtime/tasks/_template.md +73 -0
- package/runtime/workflows/branching.md +128 -0
- package/runtime/workflows/bug-fix.md +169 -0
- package/runtime/workflows/feature-development.md +238 -0
- package/src/diff.js +81 -0
- package/src/git.js +38 -0
- package/src/init.js +166 -0
- package/src/prompt.js +17 -0
- package/src/snapshot.js +84 -0
- package/src/templates.js +96 -0
- package/src/upgrade.js +179 -0
- package/src/version.js +42 -0
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
# Feature Development Workflow
|
|
2
|
+
|
|
3
|
+
## Purpose
|
|
4
|
+
|
|
5
|
+
Use this workflow to ship one small feature safely with AI assistance.
|
|
6
|
+
|
|
7
|
+
> If the change is corrective (a bug fix), use
|
|
8
|
+
> `.ai/runtime/workflows/bug-fix.md` instead. That workflow is a
|
|
9
|
+
> strict superset of this one with three additional required spec
|
|
10
|
+
> sections (Root Cause, Reproduction, Regression Test) and a
|
|
11
|
+
> regression-test-first executor order.
|
|
12
|
+
|
|
13
|
+
## Roles
|
|
14
|
+
|
|
15
|
+
- ChatGPT: define, design, review, summarize
|
|
16
|
+
- Claude Code: implement, refactor, verify
|
|
17
|
+
|
|
18
|
+
## Spec Lifecycle
|
|
19
|
+
|
|
20
|
+
Specs should use one of:
|
|
21
|
+
|
|
22
|
+
```txt
|
|
23
|
+
DRAFT
|
|
24
|
+
APPROVED
|
|
25
|
+
REJECTED
|
|
26
|
+
SUPERSEDED
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Rules:
|
|
30
|
+
|
|
31
|
+
- New specs start as DRAFT.
|
|
32
|
+
- Approved specs may generate plans/tasks.
|
|
33
|
+
- Rejected specs must not generate executable tasks.
|
|
34
|
+
- Superseded specs should reference the replacement spec.
|
|
35
|
+
|
|
36
|
+
## Workflow
|
|
37
|
+
|
|
38
|
+
### 0. Define PRD (optional — product-driven features)
|
|
39
|
+
|
|
40
|
+
For features driven by product intent (new capabilities, UX
|
|
41
|
+
changes, user-visible behavior shifts), draft a PRD first under:
|
|
42
|
+
|
|
43
|
+
```txt
|
|
44
|
+
.ai/project/prds/YYYY-MM-DD-<slug>/prd.md
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Use `.ai/runtime/prds/_template.md` as the starting point. The
|
|
48
|
+
PRD answers **what & why** — Problem, Target Users, Success
|
|
49
|
+
Metrics, User Stories, Out of Scope, Open Questions,
|
|
50
|
+
Stakeholders. Engineering details (architecture, data flow, code
|
|
51
|
+
contracts) belong in the downstream spec, not the PRD.
|
|
52
|
+
|
|
53
|
+
Skip this step when:
|
|
54
|
+
|
|
55
|
+
- the change is corrective (use `bug-fix.md` instead — bug fixes
|
|
56
|
+
don't need PRDs),
|
|
57
|
+
- the change is engineering-only (refactor, dependency bump, test
|
|
58
|
+
coverage, governance maintenance),
|
|
59
|
+
- the scope is small enough that the spec alone communicates
|
|
60
|
+
intent.
|
|
61
|
+
|
|
62
|
+
PRD lifecycle mirrors specs: `DRAFT → APPROVED → REJECTED →
|
|
63
|
+
SUPERSEDED`. An APPROVED PRD authorizes spec drafting. The
|
|
64
|
+
downstream spec MUST reference its PRD by path in §1 Goal so
|
|
65
|
+
review can verify the spec satisfies the PRD.
|
|
66
|
+
|
|
67
|
+
### 1. Define Spec
|
|
68
|
+
|
|
69
|
+
Create a feature spec under:
|
|
70
|
+
|
|
71
|
+
```txt
|
|
72
|
+
.ai/project/specs/YYYY-MM-DD-feature-name/spec.md
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
If the spec is downstream of a PRD (Step 0), §1 Goal must cite
|
|
76
|
+
the PRD path so reviewers can check that the spec covers the
|
|
77
|
+
PRD's requirements without quietly expanding scope.
|
|
78
|
+
|
|
79
|
+
Spec must include:
|
|
80
|
+
|
|
81
|
+
- Goal
|
|
82
|
+
- Scope
|
|
83
|
+
- Requirements
|
|
84
|
+
- Acceptance Criteria
|
|
85
|
+
- Test Checklist
|
|
86
|
+
- Verification Commands
|
|
87
|
+
- Rollback Plan
|
|
88
|
+
|
|
89
|
+
### 2. Execute with Claude Code
|
|
90
|
+
|
|
91
|
+
Claude Code must read:
|
|
92
|
+
|
|
93
|
+
- `.ai/runtime/agents/executor.md`
|
|
94
|
+
- `.ai/project/memory/core/tech-stack.md`
|
|
95
|
+
- `.ai/runtime/workflows/feature-development.md`
|
|
96
|
+
- `.ai/runtime/memory/engineering/principles.md`
|
|
97
|
+
- related `.ai/project/contracts/**` files if the feature touches public APIs
|
|
98
|
+
- relevant skill files from **both** `.ai/runtime/skills/**`
|
|
99
|
+
(kit-shipped, framework-generic) **and** `.ai/project/skills/**`
|
|
100
|
+
(project-shipped, project-specific). Project-side files take
|
|
101
|
+
precedence on path collision.
|
|
102
|
+
- feature spec
|
|
103
|
+
|
|
104
|
+
If the spec itself authors or modifies a kit-shipped skill under
|
|
105
|
+
`.ai/runtime/skills/**`, follow `.ai/runtime/skills/README.md` and
|
|
106
|
+
treat the spec as runtime-scoped governance per
|
|
107
|
+
`.ai/runtime/SAFETY.md` § Runtime Tree Protection. Authoring a
|
|
108
|
+
project-side skill at `.ai/project/skills/**` is not governance-
|
|
109
|
+
protected (project owns its tree); it still follows the structural
|
|
110
|
+
convention from `.ai/runtime/skills/README.md`.
|
|
111
|
+
|
|
112
|
+
If the spec authors or modifies a kit-shipped rule under
|
|
113
|
+
`.ai/runtime/rules/**`, follow `.ai/runtime/rules/README.md` and
|
|
114
|
+
treat the spec as runtime-scoped governance per `.ai/runtime/SAFETY.md`
|
|
115
|
+
§ Runtime Tree Protection. Authoring a project-side rule at
|
|
116
|
+
`.ai/project/rules/**` is not governance-protected.
|
|
117
|
+
|
|
118
|
+
When the executor touches files in a rule's scope (e.g. a `.ts`
|
|
119
|
+
file when a TypeScript rule exists), it must load the relevant
|
|
120
|
+
rules from **both** `.ai/runtime/rules/<language>/*.md` (kit) and
|
|
121
|
+
`.ai/project/rules/<language>/*.md` (project) before writing code.
|
|
122
|
+
Project-side rules take precedence on path collision.
|
|
123
|
+
|
|
124
|
+
If the spec authors or modifies a kit-shipped hook under
|
|
125
|
+
`.ai/runtime/hooks/**`, follow `.ai/runtime/hooks/README.md` and
|
|
126
|
+
treat the spec as runtime-scoped governance per `.ai/runtime/SAFETY.md`
|
|
127
|
+
§ Runtime Tree Protection. Authoring a project-side hook at
|
|
128
|
+
`.ai/project/hooks/**` is not governance-protected.
|
|
129
|
+
|
|
130
|
+
At each agent-pipeline transition (Architect → Planner → Executor
|
|
131
|
+
→ Verifier → Reviewer), the active agent must load hooks attached
|
|
132
|
+
to its phase from **both** `.ai/runtime/hooks/<phase>-<agent>/*/HOOK.md`
|
|
133
|
+
(kit) and `.ai/project/hooks/<phase>-<agent>/*/HOOK.md` (project)
|
|
134
|
+
whose `appliesWhen` matches the current spec, diff, or runtime
|
|
135
|
+
mode. Project-side hooks take precedence on path collision. GATE
|
|
136
|
+
and MUTATION hooks block the transition until their action
|
|
137
|
+
completes; ADVISORY hooks log into the review file.
|
|
138
|
+
|
|
139
|
+
Claude Code must return:
|
|
140
|
+
|
|
141
|
+
- Changed files
|
|
142
|
+
- Implementation summary
|
|
143
|
+
- Test result
|
|
144
|
+
- Build result
|
|
145
|
+
- Unresolved risks
|
|
146
|
+
|
|
147
|
+
### 3. Verify
|
|
148
|
+
|
|
149
|
+
Verification should include:
|
|
150
|
+
|
|
151
|
+
- `.ai/runtime/agents/verifier.md`
|
|
152
|
+
- related `.ai/project/contracts/**`
|
|
153
|
+
- existing tests
|
|
154
|
+
- build integrity
|
|
155
|
+
- backward compatibility
|
|
156
|
+
|
|
157
|
+
Verification results should be saved under `.ai/project/verifications/` when:
|
|
158
|
+
|
|
159
|
+
- verification fails
|
|
160
|
+
- a contract violation is detected
|
|
161
|
+
- a breaking change is proposed
|
|
162
|
+
- the feature is a complex refactor
|
|
163
|
+
- explicit audit trail is required
|
|
164
|
+
|
|
165
|
+
For simple successful features, verification results may be recorded in the review file instead.
|
|
166
|
+
|
|
167
|
+
At minimum, run:
|
|
168
|
+
|
|
169
|
+
```bash
|
|
170
|
+
npm run verify
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
Local verification must use `npm run verify` as the canonical verification command.
|
|
174
|
+
If the project has lint/typecheck commands, run them too.
|
|
175
|
+
|
|
176
|
+
### 4. Review
|
|
177
|
+
|
|
178
|
+
Create review file under:
|
|
179
|
+
|
|
180
|
+
```txt
|
|
181
|
+
.ai/project/reviews/YYYY-MM-DD-feature-name-review.md
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
Review must include:
|
|
185
|
+
|
|
186
|
+
- Summary
|
|
187
|
+
- Blocking issues
|
|
188
|
+
- Non-blocking issues
|
|
189
|
+
- Suggested fixes
|
|
190
|
+
- Verdict
|
|
191
|
+
|
|
192
|
+
### 5. Fix
|
|
193
|
+
|
|
194
|
+
If review finds issues, Claude Code must:
|
|
195
|
+
|
|
196
|
+
- Read the review
|
|
197
|
+
- Fix blocking issues
|
|
198
|
+
- Re-run verification
|
|
199
|
+
- Report final result
|
|
200
|
+
|
|
201
|
+
### 6. Commit
|
|
202
|
+
|
|
203
|
+
Use conventional commits:
|
|
204
|
+
|
|
205
|
+
```txt
|
|
206
|
+
feat: ...
|
|
207
|
+
fix: ...
|
|
208
|
+
test: ...
|
|
209
|
+
refactor: ...
|
|
210
|
+
docs: ...
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
## Definition of Done
|
|
214
|
+
|
|
215
|
+
A feature is done only when:
|
|
216
|
+
|
|
217
|
+
- Spec exists
|
|
218
|
+
- Code is implemented
|
|
219
|
+
- Tests pass
|
|
220
|
+
- Build passes
|
|
221
|
+
- Review exists
|
|
222
|
+
- Changes are committed
|
|
223
|
+
|
|
224
|
+
## Task Status Lifecycle
|
|
225
|
+
|
|
226
|
+
Tasks should follow:
|
|
227
|
+
|
|
228
|
+
```txt
|
|
229
|
+
TODO → IN_PROGRESS → IN_REVIEW → DONE
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
If blocked:
|
|
233
|
+
|
|
234
|
+
```txt
|
|
235
|
+
TODO or IN_PROGRESS → BLOCKED
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
Task status must be updated when execution state changes.
|
package/src/diff.js
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('node:fs');
|
|
4
|
+
const path = require('node:path');
|
|
5
|
+
const { spawnSync } = require('node:child_process');
|
|
6
|
+
|
|
7
|
+
const { listFilesUnder, KIT_RUNTIME_DIR } = require('./snapshot');
|
|
8
|
+
|
|
9
|
+
function computeRuntimeDiff(projectRuntimeDir) {
|
|
10
|
+
const kitFiles = new Set(listFilesUnder(KIT_RUNTIME_DIR));
|
|
11
|
+
const projectFiles = new Set(listFilesUnder(projectRuntimeDir).filter((f) => f !== 'KIT_VERSION'));
|
|
12
|
+
|
|
13
|
+
const added = [];
|
|
14
|
+
const removed = [];
|
|
15
|
+
const replaced = [];
|
|
16
|
+
const unchanged = [];
|
|
17
|
+
|
|
18
|
+
for (const f of kitFiles) {
|
|
19
|
+
if (!projectFiles.has(f)) {
|
|
20
|
+
added.push(f);
|
|
21
|
+
} else {
|
|
22
|
+
const a = fs.readFileSync(path.join(KIT_RUNTIME_DIR, f));
|
|
23
|
+
const b = fs.readFileSync(path.join(projectRuntimeDir, f));
|
|
24
|
+
if (a.equals(b)) unchanged.push(f);
|
|
25
|
+
else replaced.push(f);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
for (const f of projectFiles) {
|
|
29
|
+
if (!kitFiles.has(f)) removed.push(f);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
added.sort();
|
|
33
|
+
removed.sort();
|
|
34
|
+
replaced.sort();
|
|
35
|
+
unchanged.sort();
|
|
36
|
+
|
|
37
|
+
return { added, removed, replaced, unchanged };
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function printDiffSummary(diff) {
|
|
41
|
+
const { added, removed, replaced, unchanged } = diff;
|
|
42
|
+
console.log('Upgrade preview:');
|
|
43
|
+
console.log(` ${added.length} file(s) to ADD`);
|
|
44
|
+
console.log(` ${replaced.length} file(s) to REPLACE`);
|
|
45
|
+
console.log(` ${removed.length} file(s) to DELETE`);
|
|
46
|
+
console.log(` ${unchanged.length} file(s) UNCHANGED`);
|
|
47
|
+
console.log('');
|
|
48
|
+
if (added.length) {
|
|
49
|
+
console.log('ADD:');
|
|
50
|
+
for (const f of added) console.log(` + ${f}`);
|
|
51
|
+
console.log('');
|
|
52
|
+
}
|
|
53
|
+
if (replaced.length) {
|
|
54
|
+
console.log('REPLACE:');
|
|
55
|
+
for (const f of replaced) console.log(` ~ ${f}`);
|
|
56
|
+
console.log('');
|
|
57
|
+
}
|
|
58
|
+
if (removed.length) {
|
|
59
|
+
console.log('DELETE:');
|
|
60
|
+
for (const f of removed) console.log(` - ${f}`);
|
|
61
|
+
console.log('');
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function printPerFileDiff(diff, projectRuntimeDir, out = process.stdout) {
|
|
66
|
+
const writeLine = (line) => out.write(`${line}\n`);
|
|
67
|
+
for (const f of diff.replaced) {
|
|
68
|
+
const a = path.join(projectRuntimeDir, f);
|
|
69
|
+
const b = path.join(KIT_RUNTIME_DIR, f);
|
|
70
|
+
writeLine(`--- a/.ai/runtime/${f}`);
|
|
71
|
+
writeLine(`+++ b/.ai/runtime/${f} (kit)`);
|
|
72
|
+
const r = spawnSync('diff', ['-u', a, b], { encoding: 'utf8' });
|
|
73
|
+
if (r.stdout) {
|
|
74
|
+
const body = r.stdout.split('\n').slice(2).join('\n');
|
|
75
|
+
out.write(body);
|
|
76
|
+
}
|
|
77
|
+
writeLine('');
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
module.exports = { computeRuntimeDiff, printDiffSummary, printPerFileDiff };
|
package/src/git.js
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { spawnSync } = require('node:child_process');
|
|
4
|
+
|
|
5
|
+
function gitStatusPorcelain(targetPath, cwd) {
|
|
6
|
+
const r = spawnSync('git', ['status', '--porcelain', '--', targetPath], {
|
|
7
|
+
cwd,
|
|
8
|
+
encoding: 'utf8',
|
|
9
|
+
});
|
|
10
|
+
if (r.status === 128) {
|
|
11
|
+
return { ok: false, error: 'not a git repository' };
|
|
12
|
+
}
|
|
13
|
+
if (r.status !== 0) {
|
|
14
|
+
return { ok: false, error: r.stderr.trim() || `git exited ${r.status}` };
|
|
15
|
+
}
|
|
16
|
+
const lines = r.stdout.split('\n').filter(Boolean);
|
|
17
|
+
return { ok: true, lines };
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function isGitRepo(cwd) {
|
|
21
|
+
const r = spawnSync('git', ['rev-parse', '--git-dir'], { cwd, encoding: 'utf8' });
|
|
22
|
+
return r.status === 0;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// `git check-ignore` returns 0 if path is ignored, 1 if not, 128 if not a
|
|
26
|
+
// git repo. Null on any non-deterministic result so callers stay silent
|
|
27
|
+
// rather than misreporting.
|
|
28
|
+
function isPathGitignored(targetPath, cwd) {
|
|
29
|
+
const r = spawnSync('git', ['check-ignore', '-q', '--', targetPath], {
|
|
30
|
+
cwd,
|
|
31
|
+
encoding: 'utf8',
|
|
32
|
+
});
|
|
33
|
+
if (r.status === 0) return true;
|
|
34
|
+
if (r.status === 1) return false;
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
module.exports = { gitStatusPorcelain, isGitRepo, isPathGitignored };
|
package/src/init.js
ADDED
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('node:fs');
|
|
4
|
+
const path = require('node:path');
|
|
5
|
+
const { parseArgs } = require('node:util');
|
|
6
|
+
|
|
7
|
+
const { copyKitRuntimeTo, dirHasFiles, removeDir } = require('./snapshot');
|
|
8
|
+
const { writeProjectKitVersion, KIT_VERSION } = require('./version');
|
|
9
|
+
const { projectStateMd, projectTaskStatusMd, agentEntryClaudeMd } = require('./templates');
|
|
10
|
+
const { isPathGitignored } = require('./git');
|
|
11
|
+
|
|
12
|
+
const PROJECT_SKELETON_DIRS = [
|
|
13
|
+
'prds',
|
|
14
|
+
'specs',
|
|
15
|
+
'plans',
|
|
16
|
+
'tasks',
|
|
17
|
+
'reviews',
|
|
18
|
+
'verifications',
|
|
19
|
+
'adr',
|
|
20
|
+
'contracts',
|
|
21
|
+
'memory',
|
|
22
|
+
'rules',
|
|
23
|
+
'skills',
|
|
24
|
+
'hooks',
|
|
25
|
+
];
|
|
26
|
+
|
|
27
|
+
function run(argv) {
|
|
28
|
+
let parsed;
|
|
29
|
+
try {
|
|
30
|
+
parsed = parseArgs({
|
|
31
|
+
args: argv,
|
|
32
|
+
options: {
|
|
33
|
+
cwd: { type: 'string' },
|
|
34
|
+
migrate: { type: 'boolean', default: false },
|
|
35
|
+
'no-agent-entry': { type: 'boolean', default: false },
|
|
36
|
+
help: { type: 'boolean', short: 'h', default: false },
|
|
37
|
+
},
|
|
38
|
+
strict: true,
|
|
39
|
+
allowPositionals: false,
|
|
40
|
+
});
|
|
41
|
+
} catch (e) {
|
|
42
|
+
console.error(`init: ${e.message}`);
|
|
43
|
+
console.error('Try: ai-runtime-kit init --help');
|
|
44
|
+
process.exit(1);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (parsed.values.help) {
|
|
48
|
+
printHelp();
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const cwd = path.resolve(parsed.values.cwd ?? process.cwd());
|
|
53
|
+
const aiDir = path.join(cwd, '.ai');
|
|
54
|
+
const runtimeDir = path.join(aiDir, 'runtime');
|
|
55
|
+
const projectDir = path.join(aiDir, 'project');
|
|
56
|
+
const claudeMdPath = path.join(cwd, 'CLAUDE.md');
|
|
57
|
+
|
|
58
|
+
// Detect "real" presence — empty-only directory tree counts as
|
|
59
|
+
// absent so `init --migrate` post `git rm` doesn't have to be
|
|
60
|
+
// chased with a manual `rm -rf` (kit v0.3.0 fix).
|
|
61
|
+
const runtimeHasFiles = dirHasFiles(runtimeDir);
|
|
62
|
+
const projectHasFiles = dirHasFiles(projectDir);
|
|
63
|
+
const runtimeEmptyButPresent = fs.existsSync(runtimeDir) && !runtimeHasFiles;
|
|
64
|
+
const projectEmptyButPresent = fs.existsSync(projectDir) && !projectHasFiles;
|
|
65
|
+
const claudeMdExists = fs.existsSync(claudeMdPath);
|
|
66
|
+
const writeAgentEntry = !parsed.values['no-agent-entry'];
|
|
67
|
+
|
|
68
|
+
if (!parsed.values.migrate) {
|
|
69
|
+
if (runtimeHasFiles || projectHasFiles) {
|
|
70
|
+
console.error('init: .ai/runtime/ or .ai/project/ already exists.');
|
|
71
|
+
console.error('If you intended to bootstrap an existing-.ai/project/ repo, pass --migrate.');
|
|
72
|
+
console.error('To upgrade an installed runtime, use: ai-runtime-kit upgrade');
|
|
73
|
+
process.exit(1);
|
|
74
|
+
}
|
|
75
|
+
if (writeAgentEntry && claudeMdExists) {
|
|
76
|
+
console.error('init: CLAUDE.md already exists at the project root.');
|
|
77
|
+
console.error('Pass --no-agent-entry to skip CLAUDE.md generation, or --migrate to keep the existing file.');
|
|
78
|
+
process.exit(1);
|
|
79
|
+
}
|
|
80
|
+
} else {
|
|
81
|
+
if (runtimeHasFiles) {
|
|
82
|
+
console.error('init --migrate: .ai/runtime/ already has content; refusing to overwrite.');
|
|
83
|
+
console.error('Move it aside or use: ai-runtime-kit upgrade');
|
|
84
|
+
process.exit(1);
|
|
85
|
+
}
|
|
86
|
+
// In --migrate mode .ai/project/ MAY already exist with content;
|
|
87
|
+
// we only lay down .ai/runtime/ and skip any pre-existing
|
|
88
|
+
// project skeleton files.
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Empty parent dirs (e.g. left by `git rm -r .ai/runtime/`) are
|
|
92
|
+
// removed here so copyKitRuntimeTo can re-create the tree cleanly.
|
|
93
|
+
if (runtimeEmptyButPresent) {
|
|
94
|
+
removeDir(runtimeDir);
|
|
95
|
+
}
|
|
96
|
+
// For --migrate (or fresh init), only remove an empty .ai/project/
|
|
97
|
+
// if it has no content — otherwise it stays untouched.
|
|
98
|
+
if (!parsed.values.migrate && projectEmptyButPresent) {
|
|
99
|
+
removeDir(projectDir);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
fs.mkdirSync(aiDir, { recursive: true });
|
|
103
|
+
copyKitRuntimeTo(runtimeDir);
|
|
104
|
+
writeProjectKitVersion(cwd, KIT_VERSION);
|
|
105
|
+
|
|
106
|
+
if (!projectHasFiles) {
|
|
107
|
+
fs.mkdirSync(projectDir, { recursive: true });
|
|
108
|
+
for (const d of PROJECT_SKELETON_DIRS) {
|
|
109
|
+
fs.mkdirSync(path.join(projectDir, d), { recursive: true });
|
|
110
|
+
}
|
|
111
|
+
fs.writeFileSync(path.join(projectDir, 'STATE.md'), projectStateMd());
|
|
112
|
+
fs.writeFileSync(path.join(projectDir, 'tasks', 'TASK_STATUS.md'), projectTaskStatusMd());
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
let agentEntryWritten = false;
|
|
116
|
+
if (writeAgentEntry && !claudeMdExists) {
|
|
117
|
+
fs.writeFileSync(claudeMdPath, agentEntryClaudeMd());
|
|
118
|
+
agentEntryWritten = true;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
console.log(`ai-runtime-kit ${KIT_VERSION}: initialized .ai/ at ${cwd}`);
|
|
122
|
+
console.log(' - .ai/runtime/ (kit-managed; do not hand-edit)');
|
|
123
|
+
if (!projectHasFiles) {
|
|
124
|
+
console.log(' - .ai/project/ (project-owned; STATE.md and tasks/TASK_STATUS.md scaffolded)');
|
|
125
|
+
} else {
|
|
126
|
+
console.log(' - .ai/project/ (pre-existing; not modified)');
|
|
127
|
+
}
|
|
128
|
+
console.log(` - .ai/runtime/KIT_VERSION = ${KIT_VERSION}`);
|
|
129
|
+
if (agentEntryWritten) {
|
|
130
|
+
console.log(' - CLAUDE.md (agent entry; project-owned, never touched by upgrade)');
|
|
131
|
+
} else if (claudeMdExists) {
|
|
132
|
+
console.log(' - CLAUDE.md (pre-existing; not modified)');
|
|
133
|
+
} else {
|
|
134
|
+
console.log(' - CLAUDE.md (skipped via --no-agent-entry)');
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (isPathGitignored('.ai/runtime', cwd) === true) {
|
|
138
|
+
console.log('');
|
|
139
|
+
console.log('Note: .ai/runtime/ is gitignored. Clones of this repo will not');
|
|
140
|
+
console.log(' contain the runtime tree; they will need to run');
|
|
141
|
+
console.log(' `ai-runtime-kit init` or `upgrade` to regenerate it.');
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function printHelp() {
|
|
146
|
+
console.log(`ai-runtime-kit init [options]
|
|
147
|
+
|
|
148
|
+
Lay down .ai/runtime/, a .ai/project/ skeleton (if absent), and a
|
|
149
|
+
project-root CLAUDE.md (if absent) in the current directory.
|
|
150
|
+
|
|
151
|
+
Options:
|
|
152
|
+
--cwd <dir> Target directory (default: process.cwd())
|
|
153
|
+
--migrate Allow pre-existing .ai/project/ and/or
|
|
154
|
+
CLAUDE.md (for bootstrapping an existing repo
|
|
155
|
+
into kit-consumer mode). Still refuses if
|
|
156
|
+
.ai/runtime/ already exists.
|
|
157
|
+
--no-agent-entry Skip CLAUDE.md generation entirely.
|
|
158
|
+
-h, --help Show this help.
|
|
159
|
+
|
|
160
|
+
Refuses (without --migrate) if .ai/runtime/, .ai/project/, or
|
|
161
|
+
CLAUDE.md already exists. To upgrade an installed runtime, use the
|
|
162
|
+
upgrade command instead. CLAUDE.md is project-owned and never
|
|
163
|
+
modified by upgrade.`);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
module.exports = { run, printHelp };
|
package/src/prompt.js
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const readline = require('node:readline/promises');
|
|
4
|
+
|
|
5
|
+
async function confirm(question, { defaultYes = false } = {}) {
|
|
6
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
7
|
+
try {
|
|
8
|
+
const suffix = defaultYes ? '(Y/n)' : '(y/N)';
|
|
9
|
+
const answer = (await rl.question(`${question} ${suffix} `)).trim().toLowerCase();
|
|
10
|
+
if (!answer) return defaultYes;
|
|
11
|
+
return answer === 'y' || answer === 'yes';
|
|
12
|
+
} finally {
|
|
13
|
+
rl.close();
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
module.exports = { confirm };
|
package/src/snapshot.js
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('node:fs');
|
|
4
|
+
const path = require('node:path');
|
|
5
|
+
|
|
6
|
+
const KIT_RUNTIME_DIR = path.resolve(__dirname, '..', 'runtime');
|
|
7
|
+
|
|
8
|
+
function copyKitRuntimeTo(targetRuntimeDir) {
|
|
9
|
+
if (fs.existsSync(targetRuntimeDir)) {
|
|
10
|
+
throw new Error(`copyKitRuntimeTo: target already exists: ${targetRuntimeDir}`);
|
|
11
|
+
}
|
|
12
|
+
fs.cpSync(KIT_RUNTIME_DIR, targetRuntimeDir, { recursive: true });
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function listKitRuntimeFiles() {
|
|
16
|
+
const out = [];
|
|
17
|
+
walk(KIT_RUNTIME_DIR, '');
|
|
18
|
+
function walk(absDir, relDir) {
|
|
19
|
+
const entries = fs.readdirSync(absDir, { withFileTypes: true });
|
|
20
|
+
for (const entry of entries) {
|
|
21
|
+
const rel = relDir ? `${relDir}/${entry.name}` : entry.name;
|
|
22
|
+
if (entry.isDirectory()) {
|
|
23
|
+
walk(path.join(absDir, entry.name), rel);
|
|
24
|
+
} else {
|
|
25
|
+
out.push(rel);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
return out.sort();
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function listFilesUnder(absDir) {
|
|
33
|
+
if (!fs.existsSync(absDir)) return [];
|
|
34
|
+
const out = [];
|
|
35
|
+
walk(absDir, '');
|
|
36
|
+
function walk(dir, relDir) {
|
|
37
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
38
|
+
for (const entry of entries) {
|
|
39
|
+
const rel = relDir ? `${relDir}/${entry.name}` : entry.name;
|
|
40
|
+
if (entry.isDirectory()) {
|
|
41
|
+
walk(path.join(dir, entry.name), rel);
|
|
42
|
+
} else {
|
|
43
|
+
out.push(rel);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return out.sort();
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function removeDir(absDir) {
|
|
51
|
+
if (fs.existsSync(absDir)) {
|
|
52
|
+
fs.rmSync(absDir, { recursive: true, force: true });
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// True iff absDir exists AND contains at least one regular file
|
|
57
|
+
// anywhere in its subtree. Empty parent directories (e.g. left over
|
|
58
|
+
// from `git rm -r` which doesn't remove the parent dirs) return
|
|
59
|
+
// false — they're not real content.
|
|
60
|
+
function dirHasFiles(absDir) {
|
|
61
|
+
if (!fs.existsSync(absDir)) return false;
|
|
62
|
+
const stack = [absDir];
|
|
63
|
+
while (stack.length) {
|
|
64
|
+
const current = stack.pop();
|
|
65
|
+
const entries = fs.readdirSync(current, { withFileTypes: true });
|
|
66
|
+
for (const entry of entries) {
|
|
67
|
+
if (entry.isDirectory()) {
|
|
68
|
+
stack.push(path.join(current, entry.name));
|
|
69
|
+
} else {
|
|
70
|
+
return true;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return false;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
module.exports = {
|
|
78
|
+
KIT_RUNTIME_DIR,
|
|
79
|
+
copyKitRuntimeTo,
|
|
80
|
+
listKitRuntimeFiles,
|
|
81
|
+
listFilesUnder,
|
|
82
|
+
removeDir,
|
|
83
|
+
dirHasFiles,
|
|
84
|
+
};
|