kracked-core 1.1.0 → 1.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/README.md +73 -2
- package/bin/kracked-core.mjs +36 -2
- package/package.json +1 -1
- package/src/detect.mjs +34 -12
- package/src/prompt.mjs +110 -6
- package/src/scaffold.mjs +113 -19
- package/src/status.mjs +125 -0
- package/src/uninstall.mjs +214 -0
- package/src/update.mjs +151 -0
- package/src/wizard.mjs +89 -4
- package/templates/loaders/AGENTS.md +1 -0
- package/templates/loaders/CLAUDE.md +1 -0
- package/templates/loaders/antigravity-rules.md +1 -0
- package/templates/project/sdd/README.md +46 -0
- package/templates/project/sdd/architecture/.gitkeep +0 -0
- package/templates/project/sdd/architecture/_TEMPLATE.md +51 -0
- package/templates/project/sdd/architecture/decisions/_TEMPLATE.md +40 -0
- package/templates/project/sdd/epics/.gitkeep +0 -0
- package/templates/project/sdd/epics/_TEMPLATE.md +34 -0
- package/templates/project/sdd/specs/_TEMPLATE.md +44 -0
- package/templates/project/sdd/stories/.gitkeep +0 -0
- package/templates/project/sdd/stories/_TEMPLATE.md +33 -0
- package/templates/skills/kracked-boot/SKILL.md +1 -0
- package/templates/skills/kracked-sdd/SKILL.md +36 -1
package/README.md
CHANGED
|
@@ -49,7 +49,12 @@ npx kracked-core init
|
|
|
49
49
|
|
|
50
50
|
The wizard asks a few questions — name your agent, is this a new or existing project, which editor you use — and writes the files. Hold Enter through it and you'll get a sane setup.
|
|
51
51
|
|
|
52
|
-
**
|
|
52
|
+
**Full support** (memory + all five commands):
|
|
53
|
+
**Antigravity** · **Claude Code** · **Kilo Code** (VS Code) · **Roo Code** (VS Code)
|
|
54
|
+
|
|
55
|
+
**Memory only:** Cursor, Windsurf, and anything else that reads `AGENTS.md` — your agent loads its
|
|
56
|
+
memory, but the `/kracked-*` commands aren't installed, because those editors use a different
|
|
57
|
+
skill format. You can still run the flow by asking in plain language.
|
|
53
58
|
|
|
54
59
|
## Use
|
|
55
60
|
|
|
@@ -82,7 +87,13 @@ your-project/.kracked/ PROJECT — this codebase only
|
|
|
82
87
|
project.md stack, conventions, how to run it
|
|
83
88
|
session.md where we are right now
|
|
84
89
|
decisions.md why things are the way they are
|
|
85
|
-
sdd/
|
|
90
|
+
sdd/
|
|
91
|
+
tracker.md story status + evidence
|
|
92
|
+
specs/ what to build and why
|
|
93
|
+
epics/ groups of related stories
|
|
94
|
+
stories/ one shippable slice each
|
|
95
|
+
architecture/ how it's built (large work only)
|
|
96
|
+
decisions/ ADRs, numbered, never renumbered
|
|
86
97
|
```
|
|
87
98
|
|
|
88
99
|
### The boundary rule
|
|
@@ -123,6 +134,66 @@ Then it tracks the work:
|
|
|
123
134
|
|
|
124
135
|
**A story cannot move to `done` without evidence.** Not "the tests pass" — what did you actually verify, and what did you *not*? Green tests are not a working feature. This one rule catches more bugs than any other part of the system.
|
|
125
136
|
|
|
137
|
+
## Terminal commands
|
|
138
|
+
|
|
139
|
+
```bash
|
|
140
|
+
npx kracked-core init # set up memory in this project
|
|
141
|
+
npx kracked-core status # what's installed, and is it current?
|
|
142
|
+
npx kracked-core update # refresh skills + loaders, keep your memory
|
|
143
|
+
npx kracked-core uninstall # remove it (asks before deleting anything)
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
## Am I on the latest version?
|
|
147
|
+
|
|
148
|
+
```bash
|
|
149
|
+
npx kracked-core@latest status
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
Shows the version that installed your files, the latest on npm, and tells you plainly whether
|
|
153
|
+
you need to update:
|
|
154
|
+
|
|
155
|
+
```
|
|
156
|
+
This project ~/code/my-app
|
|
157
|
+
installed version: 1.4.0
|
|
158
|
+
.agents/skills: 5/5 skills
|
|
159
|
+
|
|
160
|
+
Global memory ~/.kracked
|
|
161
|
+
4/4 files present
|
|
162
|
+
lessons learned: 12
|
|
163
|
+
|
|
164
|
+
Version
|
|
165
|
+
running now: 1.5.0
|
|
166
|
+
latest on npm: 1.5.0
|
|
167
|
+
|
|
168
|
+
Up to date.
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
> The `@latest` matters — plain `npx kracked-core` can serve a cached older copy.
|
|
172
|
+
|
|
173
|
+
## Updating
|
|
174
|
+
|
|
175
|
+
When a new version ships:
|
|
176
|
+
|
|
177
|
+
```bash
|
|
178
|
+
npx kracked-core@latest update
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
Refreshes the skills and loaders. **Your memory is never touched** — identity, preferences,
|
|
182
|
+
lessons, and everything under `.kracked/` stay exactly as they are. It keeps your agent's name too.
|
|
183
|
+
|
|
184
|
+
Restart your editor afterwards so it picks up the new skills.
|
|
185
|
+
|
|
186
|
+
## Uninstalling
|
|
187
|
+
|
|
188
|
+
```bash
|
|
189
|
+
npx kracked-core uninstall
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
Shows what it will remove and asks before deleting — project files and global memory are separate
|
|
193
|
+
questions. It never touches a `CLAUDE.md` or `AGENTS.md` another tool wrote.
|
|
194
|
+
|
|
195
|
+
Full guide, including manual removal and how to keep your docs: [docs/UNINSTALL.md](https://github.com/Krackeddevs-Org/kracked-core/blob/main/docs/UNINSTALL.md)
|
|
196
|
+
|
|
126
197
|
## FAQ
|
|
127
198
|
|
|
128
199
|
**Does this send my code anywhere?**
|
package/bin/kracked-core.mjs
CHANGED
|
@@ -1,24 +1,58 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
import { runInit } from '../src/wizard.mjs';
|
|
4
|
+
import { runUninstall } from '../src/uninstall.mjs';
|
|
5
|
+
import { runUpdate } from '../src/update.mjs';
|
|
6
|
+
import { runStatus } from '../src/status.mjs';
|
|
7
|
+
import { beginTypeAhead } from '../src/prompt.mjs';
|
|
4
8
|
|
|
5
9
|
const USAGE = `kracked-core — memory & workflow installer for AI coding agents
|
|
6
10
|
|
|
7
11
|
Usage:
|
|
8
|
-
npx kracked-core init
|
|
12
|
+
npx kracked-core init Set up global + project memory
|
|
13
|
+
npx kracked-core status Show what's installed and whether it's current
|
|
14
|
+
npx kracked-core update Refresh skills + loaders, keeping your memory
|
|
15
|
+
npx kracked-core uninstall Remove kracked-core files (asks before deleting)
|
|
9
16
|
|
|
10
17
|
Options:
|
|
11
|
-
-h, --help
|
|
18
|
+
-h, --help Show this help
|
|
19
|
+
-v, --version Show the installed version
|
|
12
20
|
`;
|
|
13
21
|
|
|
14
22
|
async function main() {
|
|
15
23
|
const [, , subcommand] = process.argv;
|
|
16
24
|
|
|
25
|
+
// Capture keystrokes from the very first millisecond — a student holding
|
|
26
|
+
// Enter starts before Node has finished booting.
|
|
27
|
+
if (['init', 'update', 'uninstall'].includes(subcommand)) beginTypeAhead();
|
|
28
|
+
|
|
17
29
|
if (subcommand === 'init') {
|
|
18
30
|
await runInit();
|
|
19
31
|
return;
|
|
20
32
|
}
|
|
21
33
|
|
|
34
|
+
if (subcommand === 'status') {
|
|
35
|
+
await runStatus();
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (subcommand === 'update') {
|
|
40
|
+
await runUpdate();
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (subcommand === 'uninstall') {
|
|
45
|
+
await runUninstall();
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (subcommand === '--version' || subcommand === '-v') {
|
|
50
|
+
const { createRequire } = await import('node:module');
|
|
51
|
+
const pkg = createRequire(import.meta.url)('../package.json');
|
|
52
|
+
process.stdout.write(`${pkg.version}\n`);
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
|
|
22
56
|
if (subcommand === '--help' || subcommand === '-h' || !subcommand) {
|
|
23
57
|
process.stdout.write(USAGE);
|
|
24
58
|
process.exit(subcommand ? 0 : 1);
|
package/package.json
CHANGED
package/src/detect.mjs
CHANGED
|
@@ -5,6 +5,30 @@ import fs from 'node:fs';
|
|
|
5
5
|
import path from 'node:path';
|
|
6
6
|
import os from 'node:os';
|
|
7
7
|
|
|
8
|
+
/**
|
|
9
|
+
* Scrub a value read from an untrusted repo before it can reach a template.
|
|
10
|
+
*
|
|
11
|
+
* Everything scanned from a project (package.json name, script names) gets
|
|
12
|
+
* interpolated into AGENTS.md and ~/.kracked/projects.md — files an AI agent
|
|
13
|
+
* READS AS INSTRUCTIONS on every boot. A crafted `name` containing newlines and
|
|
14
|
+
* a markdown heading can therefore inject text that reads as package-authored
|
|
15
|
+
* instruction, and via the global registry it persists into every OTHER project
|
|
16
|
+
* the user opens. Sanitize here, at the boundary, so no caller can forget.
|
|
17
|
+
*/
|
|
18
|
+
export function sanitizeScanned(value, maxLength = 64) {
|
|
19
|
+
if (typeof value !== 'string') return '';
|
|
20
|
+
return value
|
|
21
|
+
.replace(/[\r\n\t]+/g, ' ') // no line breaks — kills injected blocks
|
|
22
|
+
// eslint-disable-next-line no-control-regex
|
|
23
|
+
.replace(/[\u0000-\u001f\u007f]/g, '') // strip remaining control chars
|
|
24
|
+
.replace(/[|`<>]/g, '') // markdown table + code + html structure
|
|
25
|
+
.replace(/^[\s#>*\-=+]+/, '') // no leading heading/quote/list markers
|
|
26
|
+
.replace(/\s+/g, ' ')
|
|
27
|
+
.trim()
|
|
28
|
+
.slice(0, maxLength)
|
|
29
|
+
.trim();
|
|
30
|
+
}
|
|
31
|
+
|
|
8
32
|
/** Does ~/.kracked/ already exist? */
|
|
9
33
|
export function globalMemoryExists() {
|
|
10
34
|
const dir = path.join(os.homedir(), '.kracked');
|
|
@@ -99,9 +123,17 @@ export function scanExistingProject(targetDir) {
|
|
|
99
123
|
if (fs.existsSync(pkgPath)) {
|
|
100
124
|
try {
|
|
101
125
|
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
|
|
102
|
-
|
|
126
|
+
|
|
127
|
+
// Everything below comes from an untrusted repo and ends up inside files
|
|
128
|
+
// the agent reads as instructions — sanitize before it goes anywhere.
|
|
129
|
+
const cleanName = sanitizeScanned(pkg.name);
|
|
130
|
+
if (cleanName) summary.name = cleanName;
|
|
131
|
+
|
|
103
132
|
if (pkg.scripts && typeof pkg.scripts === 'object') {
|
|
104
|
-
|
|
133
|
+
// Keep only plausible script names; a hostile key is dropped, not rendered.
|
|
134
|
+
for (const key of Object.keys(pkg.scripts)) {
|
|
135
|
+
if (/^[a-zA-Z0-9:_-]{1,40}$/.test(key)) summary.scripts[key] = true;
|
|
136
|
+
}
|
|
105
137
|
}
|
|
106
138
|
const deps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
107
139
|
const frameworkHints = [
|
|
@@ -127,13 +159,3 @@ export function stackSummaryLine(summary) {
|
|
|
127
159
|
return parts.length ? parts.join(' ') : 'unknown — not auto-detected';
|
|
128
160
|
}
|
|
129
161
|
|
|
130
|
-
/** Which harnesses already have a footprint in this project dir? */
|
|
131
|
-
export function detectHarnesses(targetDir) {
|
|
132
|
-
return {
|
|
133
|
-
claudeCode: fs.existsSync(path.join(targetDir, '.claude')) ||
|
|
134
|
-
fs.existsSync(path.join(targetDir, 'CLAUDE.md')),
|
|
135
|
-
antigravity: fs.existsSync(path.join(targetDir, '.agents')) ||
|
|
136
|
-
fs.existsSync(path.join(targetDir, 'AGENTS.md')),
|
|
137
|
-
cursor: fs.existsSync(path.join(targetDir, '.cursor')),
|
|
138
|
-
};
|
|
139
|
-
}
|
package/src/prompt.mjs
CHANGED
|
@@ -27,32 +27,112 @@ function setCursor(visible) {
|
|
|
27
27
|
stdout.write(visible ? `${ESC}[?25h` : `${ESC}[?25l`);
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
+
// A throw anywhere outside the keypress handler would otherwise exit with the
|
|
31
|
+
// terminal still in raw mode and the cursor hidden — leaving the user's shell
|
|
32
|
+
// with invisible, unechoed input. One idempotent restore covers every exit path,
|
|
33
|
+
// including process.exit().
|
|
34
|
+
let restoreRegistered = false;
|
|
35
|
+
function registerTerminalRestore() {
|
|
36
|
+
if (restoreRegistered) return;
|
|
37
|
+
restoreRegistered = true;
|
|
38
|
+
process.on('exit', () => {
|
|
39
|
+
try {
|
|
40
|
+
if (stdin.isTTY && stdin.isRaw) stdin.setRawMode(false);
|
|
41
|
+
stdout.write(`${ESC}[?25h`);
|
|
42
|
+
} catch {
|
|
43
|
+
// Nothing useful to do while the process is already exiting.
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
|
|
30
48
|
/** Move up n lines and clear from there down — used to redraw the menu in place. */
|
|
31
49
|
function clearLines(n) {
|
|
32
50
|
if (n > 0) stdout.write(`${ESC}[${n}A`);
|
|
33
51
|
stdout.write(`${ESC}[0J`);
|
|
34
52
|
}
|
|
35
53
|
|
|
54
|
+
/**
|
|
55
|
+
* Split one stdin chunk into individual keys.
|
|
56
|
+
*
|
|
57
|
+
* stdin delivers DATA, not keystrokes: holding Enter (key auto-repeat), typing
|
|
58
|
+
* fast, pasting, or running over SSH/tmux routinely packs several keys into one
|
|
59
|
+
* chunk. Comparing a whole chunk against '\r' then matches nothing, so the key
|
|
60
|
+
* is silently dropped and the prompt hangs — the worst kind of failure, because
|
|
61
|
+
* there is no error to see. Escape sequences (arrows) must stay intact as one
|
|
62
|
+
* key; everything else is per-character.
|
|
63
|
+
*/
|
|
64
|
+
function splitKeys(chunk) {
|
|
65
|
+
const keys = [];
|
|
66
|
+
let i = 0;
|
|
67
|
+
|
|
68
|
+
while (i < chunk.length) {
|
|
69
|
+
if (chunk[i] === ESC) {
|
|
70
|
+
// CSI sequence: ESC [ ... final-byte (@ through ~). Arrows are ESC[A..D.
|
|
71
|
+
const match = /^\x1b\[[0-9;?]*[ -/]*[@-~]/.exec(chunk.slice(i));
|
|
72
|
+
if (match) {
|
|
73
|
+
keys.push(match[0]);
|
|
74
|
+
i += match[0].length;
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
// ESC alone, or an unrecognised sequence — take just the ESC.
|
|
78
|
+
keys.push(chunk[i]);
|
|
79
|
+
i += 1;
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
keys.push(chunk[i]);
|
|
83
|
+
i += 1;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return keys;
|
|
87
|
+
}
|
|
88
|
+
|
|
36
89
|
/**
|
|
37
90
|
* Read single keypresses until `onKey` signals completion.
|
|
38
91
|
* onKey(key) returns { done: true, value } to finish, or falsy to keep listening.
|
|
39
92
|
*/
|
|
93
|
+
// Keys that arrive between prompts — while one question has resolved and the
|
|
94
|
+
// next hasn't attached its listener yet — would otherwise be dropped on the
|
|
95
|
+
// floor. Holding Enter produces exactly that. Buffer them and replay into the
|
|
96
|
+
// next prompt so no keystroke is ever silently lost.
|
|
97
|
+
const pendingKeys = [];
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Start capturing keystrokes immediately, before the first prompt renders.
|
|
101
|
+
* Node takes a moment to boot and draw; anything typed in that window is
|
|
102
|
+
* otherwise lost. Call once at CLI start.
|
|
103
|
+
*/
|
|
104
|
+
export function beginTypeAhead() {
|
|
105
|
+
if (!stdin.isTTY) return;
|
|
106
|
+
registerTerminalRestore();
|
|
107
|
+
stdin.setRawMode(true);
|
|
108
|
+
stdin.resume();
|
|
109
|
+
stdin.setEncoding('utf8');
|
|
110
|
+
stdin.on('data', bufferEarlyKeys);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function bufferEarlyKeys(chunk) {
|
|
114
|
+
pendingKeys.push(...splitKeys(chunk));
|
|
115
|
+
}
|
|
116
|
+
|
|
40
117
|
function readKeys(onKey) {
|
|
41
118
|
return new Promise((resolve, reject) => {
|
|
119
|
+
registerTerminalRestore();
|
|
42
120
|
const wasRaw = stdin.isRaw;
|
|
43
121
|
stdin.setRawMode(true);
|
|
44
122
|
stdin.resume();
|
|
45
123
|
stdin.setEncoding('utf8');
|
|
124
|
+
stdin.removeListener('data', bufferEarlyKeys);
|
|
46
125
|
|
|
47
126
|
const cleanup = () => {
|
|
48
127
|
stdin.setRawMode(wasRaw ?? false);
|
|
49
|
-
stdin.pause();
|
|
50
128
|
stdin.removeListener('data', handler);
|
|
129
|
+
// Resume early-capture so keys typed between prompts aren't lost.
|
|
130
|
+
stdin.on('data', bufferEarlyKeys);
|
|
51
131
|
setCursor(true);
|
|
52
132
|
};
|
|
53
133
|
|
|
54
|
-
|
|
55
|
-
|
|
134
|
+
/** Feed one key to onKey. Returns true when the prompt is finished. */
|
|
135
|
+
const dispatch = (key) => {
|
|
56
136
|
if (key === KEY.ctrlC || key === KEY.ctrlD) {
|
|
57
137
|
cleanup();
|
|
58
138
|
stdout.write('\n\nCancelled — nothing was written.\n');
|
|
@@ -65,12 +145,32 @@ function readKeys(onKey) {
|
|
|
65
145
|
} catch (err) {
|
|
66
146
|
cleanup();
|
|
67
147
|
reject(err);
|
|
68
|
-
return;
|
|
148
|
+
return true;
|
|
69
149
|
}
|
|
70
150
|
|
|
71
151
|
if (result && result.done) {
|
|
72
152
|
cleanup();
|
|
73
153
|
resolve(result.value);
|
|
154
|
+
return true;
|
|
155
|
+
}
|
|
156
|
+
return false;
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
// Replay anything typed ahead of this prompt before listening for more.
|
|
160
|
+
while (pendingKeys.length) {
|
|
161
|
+
if (dispatch(pendingKeys.shift())) return;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function handler(chunk) {
|
|
165
|
+
// One chunk can carry several keys. Dispatch them one at a time; once a
|
|
166
|
+
// key completes this prompt, park the rest for the next one rather than
|
|
167
|
+
// discarding them.
|
|
168
|
+
const keys = splitKeys(chunk);
|
|
169
|
+
for (let i = 0; i < keys.length; i++) {
|
|
170
|
+
if (dispatch(keys[i])) {
|
|
171
|
+
pendingKeys.push(...keys.slice(i + 1));
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
74
174
|
}
|
|
75
175
|
}
|
|
76
176
|
|
|
@@ -210,8 +310,12 @@ export async function input(question, defaultValue = '') {
|
|
|
210
310
|
render();
|
|
211
311
|
return null;
|
|
212
312
|
}
|
|
213
|
-
// Ignore escape sequences (arrows etc.) and
|
|
214
|
-
|
|
313
|
+
// Ignore escape sequences (arrows etc.) and any control character. Compare
|
|
314
|
+
// the code point, not the string — `key < ' '` on a multi-char value tests
|
|
315
|
+
// lexicographic order, not "is this a control char".
|
|
316
|
+
if (key.startsWith(ESC)) return null;
|
|
317
|
+
const code = key.codePointAt(0);
|
|
318
|
+
if (code < 0x20 || code === 0x7f) return null;
|
|
215
319
|
|
|
216
320
|
buffer += key;
|
|
217
321
|
render();
|
package/src/scaffold.mjs
CHANGED
|
@@ -9,7 +9,12 @@ import { fileURLToPath } from 'node:url';
|
|
|
9
9
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
10
10
|
const TEMPLATES_ROOT = path.join(__dirname, '..', 'templates');
|
|
11
11
|
|
|
12
|
-
|
|
12
|
+
// Written into every loader kracked-core generates. `uninstall` requires this
|
|
13
|
+
// exact string before deleting a shared-name file (AGENTS.md / CLAUDE.md), so
|
|
14
|
+
// another tool's loader is never removed. Never change it without a migration.
|
|
15
|
+
export const OWNED_MARKER = '<!-- kracked-core:owned -->';
|
|
16
|
+
|
|
17
|
+
export const SKILL_NAMES = [
|
|
13
18
|
'kracked-boot',
|
|
14
19
|
'kracked-sdd',
|
|
15
20
|
'kracked-wrap',
|
|
@@ -95,8 +100,10 @@ export async function writeGlobalMemory({ tokens, ask, report }) {
|
|
|
95
100
|
export function registerProject({ tokens }) {
|
|
96
101
|
const globalDir = path.join(os.homedir(), '.kracked');
|
|
97
102
|
const projectsPath = path.join(globalDir, 'projects.md');
|
|
103
|
+
// Escape pipes — a path or name containing `|` would add phantom columns.
|
|
104
|
+
const cell = (v) => String(v ?? '').replace(/\|/g, '\\|');
|
|
98
105
|
const stack = tokens.STACK && tokens.STACK.trim() ? tokens.STACK : '—';
|
|
99
|
-
const row = `| ${tokens.PROJECT_NAME} | ${tokens.PROJECT_PATH} | ${stack} | active |`;
|
|
106
|
+
const row = `| ${cell(tokens.PROJECT_NAME)} | ${cell(tokens.PROJECT_PATH)} | ${cell(stack)} | active |`;
|
|
100
107
|
|
|
101
108
|
fs.mkdirSync(globalDir, { recursive: true });
|
|
102
109
|
|
|
@@ -148,34 +155,83 @@ export async function writeProjectMemory({ projectDir, tokens, ask, report }) {
|
|
|
148
155
|
await writeFileWithConflictCheck(destPath, content, ask, report);
|
|
149
156
|
}
|
|
150
157
|
|
|
151
|
-
//
|
|
152
|
-
|
|
153
|
-
const
|
|
154
|
-
|
|
158
|
+
// SDD docs: tracker + the artifact folders, each with a template that
|
|
159
|
+
// explains what belongs there. An empty folder teaches nothing.
|
|
160
|
+
const sddDocs = [
|
|
161
|
+
'sdd/tracker.md',
|
|
162
|
+
'sdd/README.md',
|
|
163
|
+
'sdd/specs/_TEMPLATE.md',
|
|
164
|
+
'sdd/epics/_TEMPLATE.md',
|
|
165
|
+
'sdd/stories/_TEMPLATE.md',
|
|
166
|
+
'sdd/architecture/_TEMPLATE.md',
|
|
167
|
+
'sdd/architecture/decisions/_TEMPLATE.md',
|
|
168
|
+
];
|
|
169
|
+
|
|
170
|
+
for (const rel of sddDocs) {
|
|
171
|
+
const destPath = path.join(krackedDir, rel);
|
|
172
|
+
const content = applyTokens(readTemplate(`project/${rel}`), tokens);
|
|
173
|
+
await writeFileWithConflictCheck(destPath, content, ask, report);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Record which version wrote these files. Without this there is no way to
|
|
179
|
+
* answer "am I on the latest?" — you can see what npm serves, but not what
|
|
180
|
+
* you actually installed.
|
|
181
|
+
*/
|
|
182
|
+
export function writeVersionStamp({ projectDir, version, report }) {
|
|
183
|
+
const dest = path.join(projectDir, '.kracked', '.version');
|
|
184
|
+
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
|
185
|
+
fs.writeFileSync(dest, `${version}\n`, 'utf8');
|
|
186
|
+
report({ path: dest, action: 'written' });
|
|
187
|
+
}
|
|
155
188
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
fs.
|
|
160
|
-
|
|
189
|
+
/** Read the version that last wrote this project's files, or null. */
|
|
190
|
+
export function readVersionStamp(projectDir) {
|
|
191
|
+
try {
|
|
192
|
+
return fs.readFileSync(path.join(projectDir, '.kracked', '.version'), 'utf8').trim();
|
|
193
|
+
} catch {
|
|
194
|
+
return null; // pre-1.5.0 install, or not installed
|
|
161
195
|
}
|
|
162
196
|
}
|
|
163
197
|
|
|
164
198
|
/** Write AGENTS.md, CLAUDE.md (shim), and .agents/rules/kracked.md. */
|
|
165
|
-
export async function writeLoaders({ projectDir, tokens, ask, report }) {
|
|
199
|
+
export async function writeLoaders({ projectDir, tokens, ask, report, editors }) {
|
|
166
200
|
const agentsContent = applyTokens(readTemplate('loaders/AGENTS.md'), tokens);
|
|
167
201
|
await writeFileWithConflictCheck(path.join(projectDir, 'AGENTS.md'), agentsContent, ask, report);
|
|
168
202
|
|
|
169
203
|
const claudeContent = applyTokens(readTemplate('loaders/CLAUDE.md'), tokens);
|
|
170
204
|
await writeFileWithConflictCheck(path.join(projectDir, 'CLAUDE.md'), claudeContent, ask, report);
|
|
171
205
|
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
206
|
+
// Roo reads .roo/rules/ only — it does NOT read .agents/rules/, so without
|
|
207
|
+
// this mirror the pointer is invisible to Roo.
|
|
208
|
+
if (editors && editors.includes('roo')) {
|
|
209
|
+
const rooRules = applyTokens(readTemplate('loaders/antigravity-rules.md'), tokens);
|
|
210
|
+
await writeFileWithConflictCheck(
|
|
211
|
+
path.join(projectDir, '.roo', 'rules', 'kracked.md'),
|
|
212
|
+
rooRules,
|
|
213
|
+
ask,
|
|
214
|
+
report
|
|
215
|
+
);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// Kilo only loads a rules file if it's declared in kilo.jsonc's `instructions`
|
|
219
|
+
// array. Merge into an existing config rather than clobbering the user's.
|
|
220
|
+
if (editors && editors.includes('kilo')) {
|
|
221
|
+
await writeKiloConfig({ projectDir, report });
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// Only write the Antigravity pointer when Antigravity is actually in use —
|
|
225
|
+
// `update` was creating .agents/ in Claude-only projects.
|
|
226
|
+
if (!editors || editors.includes('antigravity')) {
|
|
227
|
+
const krackedRulesContent = applyTokens(readTemplate('loaders/antigravity-rules.md'), tokens);
|
|
228
|
+
await writeFileWithConflictCheck(
|
|
229
|
+
path.join(projectDir, '.agents', 'rules', 'kracked.md'),
|
|
230
|
+
krackedRulesContent,
|
|
231
|
+
ask,
|
|
232
|
+
report
|
|
233
|
+
);
|
|
234
|
+
}
|
|
179
235
|
}
|
|
180
236
|
|
|
181
237
|
/**
|
|
@@ -196,3 +252,41 @@ export async function writeSkills({ projectDir, tokens, editors, ask, report })
|
|
|
196
252
|
}
|
|
197
253
|
}
|
|
198
254
|
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Add our rules file to kilo.jsonc's `instructions` array. Kilo does not
|
|
258
|
+
* auto-load `.kilo/rules/` — a file is only read if it's listed here, so
|
|
259
|
+
* without this the pointer is silently ignored.
|
|
260
|
+
* Merges into an existing config; never overwrites the user's settings.
|
|
261
|
+
*/
|
|
262
|
+
async function writeKiloConfig({ projectDir, report }) {
|
|
263
|
+
const configPath = path.join(projectDir, 'kilo.jsonc');
|
|
264
|
+
const entry = '.agents/rules/kracked.md';
|
|
265
|
+
|
|
266
|
+
if (fs.existsSync(configPath)) {
|
|
267
|
+
const body = fs.readFileSync(configPath, 'utf8');
|
|
268
|
+
if (body.includes(entry)) {
|
|
269
|
+
report({ path: configPath, action: 'already configured' });
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
272
|
+
// Append to the existing instructions array if there is one, else add it.
|
|
273
|
+
let updated;
|
|
274
|
+
if (/"instructions"\s*:\s*\[/.test(body)) {
|
|
275
|
+
updated = body.replace(/("instructions"\s*:\s*\[)/, `$1\n "${entry}",`);
|
|
276
|
+
} else {
|
|
277
|
+
updated = body.replace(/^\s*\{/, `{\n "instructions": ["${entry}"],`);
|
|
278
|
+
}
|
|
279
|
+
fs.writeFileSync(configPath, updated, 'utf8');
|
|
280
|
+
report({ path: configPath, action: 'updated (added instructions entry)' });
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
const fresh = `{
|
|
285
|
+
// Kilo reads AGENTS.md automatically. This entry additionally loads the
|
|
286
|
+
// kracked-core rules pointer. Add your own settings alongside it.
|
|
287
|
+
"instructions": ["${entry}"]
|
|
288
|
+
}
|
|
289
|
+
`;
|
|
290
|
+
fs.writeFileSync(configPath, fresh, 'utf8');
|
|
291
|
+
report({ path: configPath, action: 'written' });
|
|
292
|
+
}
|