kracked-core 1.1.0 → 1.4.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 +37 -2
- package/bin/kracked-core.mjs +29 -2
- package/package.json +1 -1
- package/src/detect.mjs +34 -12
- package/src/prompt.mjs +110 -6
- package/src/scaffold.mjs +92 -19
- package/src/uninstall.mjs +214 -0
- package/src/update.mjs +150 -0
- package/src/wizard.mjs +77 -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
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
// `kracked-core uninstall` — remove what the installer wrote.
|
|
2
|
+
//
|
|
3
|
+
// Deleting a user's memory is unrecoverable, so this is deliberately cautious:
|
|
4
|
+
// it shows exactly what it will remove, asks per layer, defaults to No on the
|
|
5
|
+
// global layer, and never deletes a file it didn't recognise as its own.
|
|
6
|
+
|
|
7
|
+
import fs from 'node:fs';
|
|
8
|
+
import path from 'node:path';
|
|
9
|
+
import os from 'node:os';
|
|
10
|
+
import { stdout } from 'node:process';
|
|
11
|
+
|
|
12
|
+
import { select, confirm } from './prompt.mjs';
|
|
13
|
+
import { OWNED_MARKER, SKILL_NAMES } from './scaffold.mjs';
|
|
14
|
+
|
|
15
|
+
const dim = (s) => `\x1b[2m${s}\x1b[0m`;
|
|
16
|
+
const bold = (s) => `\x1b[1m${s}\x1b[0m`;
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Files/dirs the installer writes into a project.
|
|
20
|
+
*
|
|
21
|
+
* CRITICAL: when the "project" IS the home directory, `<project>/.kracked` and
|
|
22
|
+
* `~/.kracked` are the same folder — so listing it as a project target would
|
|
23
|
+
* delete the user's entire global memory behind the mild "Remove these project
|
|
24
|
+
* files?" prompt, never reaching the two-step global confirmation below.
|
|
25
|
+
* Global memory is only ever removable via the global layer.
|
|
26
|
+
*/
|
|
27
|
+
function projectTargets(projectDir) {
|
|
28
|
+
const globalDir = path.resolve(path.join(os.homedir(), '.kracked'));
|
|
29
|
+
|
|
30
|
+
const targets = [
|
|
31
|
+
{ path: path.join(projectDir, '.kracked'), label: '.kracked/', kind: 'dir' },
|
|
32
|
+
{ path: path.join(projectDir, 'AGENTS.md'), label: 'AGENTS.md', kind: 'file' },
|
|
33
|
+
{ path: path.join(projectDir, 'CLAUDE.md'), label: 'CLAUDE.md', kind: 'file' },
|
|
34
|
+
{
|
|
35
|
+
path: path.join(projectDir, '.agents', 'rules', 'kracked.md'),
|
|
36
|
+
label: '.agents/rules/kracked.md',
|
|
37
|
+
kind: 'file',
|
|
38
|
+
},
|
|
39
|
+
];
|
|
40
|
+
|
|
41
|
+
// Roo's rules mirror. kilo.jsonc is deliberately NOT listed — it may hold the
|
|
42
|
+
// user's own settings, so removing it wholesale would destroy their config.
|
|
43
|
+
targets.push({
|
|
44
|
+
path: path.join(projectDir, '.roo', 'rules', 'kracked.md'),
|
|
45
|
+
label: '.roo/rules/kracked.md',
|
|
46
|
+
kind: 'file',
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
// "Write alongside" during init creates these; nothing else knew about them,
|
|
50
|
+
// so they were orphaned forever.
|
|
51
|
+
for (const base of ['AGENTS.md', 'CLAUDE.md']) {
|
|
52
|
+
targets.push({
|
|
53
|
+
path: path.join(projectDir, `${base}.kracked-new`),
|
|
54
|
+
label: `${base}.kracked-new`,
|
|
55
|
+
kind: 'file',
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
for (const skill of SKILL_NAMES) {
|
|
60
|
+
targets.push({
|
|
61
|
+
path: path.join(projectDir, '.claude', 'skills', skill),
|
|
62
|
+
label: `.claude/skills/${skill}/`,
|
|
63
|
+
kind: 'dir',
|
|
64
|
+
});
|
|
65
|
+
targets.push({
|
|
66
|
+
path: path.join(projectDir, '.agents', 'skills', skill),
|
|
67
|
+
label: `.agents/skills/${skill}/`,
|
|
68
|
+
kind: 'dir',
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return targets.filter((t) => {
|
|
73
|
+
if (!fs.existsSync(t.path)) return false;
|
|
74
|
+
// Never let a project target resolve onto global memory.
|
|
75
|
+
if (path.resolve(t.path) === globalDir) return false;
|
|
76
|
+
return true;
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Only the two SHARED loader filenames can belong to another system —
|
|
82
|
+
* AGENTS.md and CLAUDE.md are conventions other tools use too, so their
|
|
83
|
+
* contents decide ownership. Everything else lives at a kracked-core-specific
|
|
84
|
+
* path (.kracked/, kracked.md, skills/kracked-*) and is ours by definition.
|
|
85
|
+
*/
|
|
86
|
+
function isOurs(filePath) {
|
|
87
|
+
const base = path.basename(filePath);
|
|
88
|
+
if (base !== 'AGENTS.md' && base !== 'CLAUDE.md') return true;
|
|
89
|
+
|
|
90
|
+
try {
|
|
91
|
+
// Require the sentinel the installer writes. Anything else — including a
|
|
92
|
+
// file that merely MENTIONS kracked-core, or another tool using the shared
|
|
93
|
+
// `@AGENTS.md` import convention — is someone else's and must survive.
|
|
94
|
+
// Fail closed: an orphaned file is recoverable, a deleted one is not.
|
|
95
|
+
const body = fs.readFileSync(filePath, 'utf8');
|
|
96
|
+
if (body.includes(OWNED_MARKER)) return true;
|
|
97
|
+
|
|
98
|
+
// Pre-sentinel installs (<=1.2.0) have no marker. Recognise their exact
|
|
99
|
+
// generated shape so an upgrade path doesn't orphan loaders forever.
|
|
100
|
+
// Deliberately narrow: a bare `@AGENTS.md` (a shared convention other tools
|
|
101
|
+
// use) is NOT enough on its own.
|
|
102
|
+
const legacyClaude = /^@AGENTS\.md\s*\n[\s\S]*canonical instructions live in `AGENTS\.md`/.test(body);
|
|
103
|
+
const legacyAgents = /^# AGENTS\.md — [\s\S]*This is the canonical loader[\s\S]*kracked-boot/.test(body);
|
|
104
|
+
return legacyClaude || legacyAgents;
|
|
105
|
+
} catch {
|
|
106
|
+
return false; // unreadable — leave it alone rather than risk deleting someone else's
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function remove(target) {
|
|
111
|
+
if (target.kind === 'dir') {
|
|
112
|
+
fs.rmSync(target.path, { recursive: true, force: true });
|
|
113
|
+
} else {
|
|
114
|
+
fs.rmSync(target.path, { force: true });
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** Drop now-empty .claude/.agents shells so uninstall leaves no litter. */
|
|
119
|
+
function pruneEmptyParents(projectDir) {
|
|
120
|
+
const candidates = [
|
|
121
|
+
path.join(projectDir, '.claude', 'skills'),
|
|
122
|
+
path.join(projectDir, '.claude'),
|
|
123
|
+
path.join(projectDir, '.agents', 'rules'),
|
|
124
|
+
path.join(projectDir, '.roo', 'rules'),
|
|
125
|
+
path.join(projectDir, '.roo'),
|
|
126
|
+
path.join(projectDir, '.agents', 'skills'),
|
|
127
|
+
path.join(projectDir, '.agents'),
|
|
128
|
+
];
|
|
129
|
+
for (const dir of candidates) {
|
|
130
|
+
try {
|
|
131
|
+
if (fs.existsSync(dir) && fs.readdirSync(dir).length === 0) fs.rmdirSync(dir);
|
|
132
|
+
} catch {
|
|
133
|
+
// Non-empty or not ours to remove — leaving it is the safe outcome.
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export async function runUninstall() {
|
|
139
|
+
if (!process.stdin.isTTY) {
|
|
140
|
+
process.stderr.write(
|
|
141
|
+
'kracked-core uninstall needs an interactive terminal.\n' +
|
|
142
|
+
'Run it directly in a terminal, or delete the files manually — see\n' +
|
|
143
|
+
'https://github.com/Krackeddevs-Org/kracked-core/blob/main/docs/UNINSTALL.md\n'
|
|
144
|
+
);
|
|
145
|
+
process.exitCode = 1;
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const projectDir = process.cwd();
|
|
150
|
+
const globalDir = path.join(os.homedir(), '.kracked');
|
|
151
|
+
|
|
152
|
+
stdout.write(`${bold('kracked-core uninstall')}\n\n`);
|
|
153
|
+
|
|
154
|
+
// ---- Project layer ----
|
|
155
|
+
const found = projectTargets(projectDir);
|
|
156
|
+
|
|
157
|
+
if (found.length === 0) {
|
|
158
|
+
stdout.write(`No kracked-core files found in ${projectDir}\n`);
|
|
159
|
+
} else {
|
|
160
|
+
// Loaders another system owns must never be deleted.
|
|
161
|
+
const foreign = found.filter((t) => t.kind === 'file' && !isOurs(t.path));
|
|
162
|
+
const ours = found.filter((t) => !foreign.includes(t));
|
|
163
|
+
|
|
164
|
+
stdout.write(`Found in ${dim(projectDir)}:\n`);
|
|
165
|
+
for (const t of ours) stdout.write(` ${t.label}\n`);
|
|
166
|
+
if (foreign.length) {
|
|
167
|
+
stdout.write('\n Not ours — will NOT be touched:\n');
|
|
168
|
+
for (const t of foreign) stdout.write(` ${t.label} ${dim('(another system wrote this)')}\n`);
|
|
169
|
+
}
|
|
170
|
+
stdout.write('\n');
|
|
171
|
+
|
|
172
|
+
const choice = await select('Remove these project files?', [
|
|
173
|
+
{ label: 'No, keep everything', value: 'no' },
|
|
174
|
+
{ label: 'Yes, remove them', value: 'yes', hint: 'this cannot be undone' },
|
|
175
|
+
], 0);
|
|
176
|
+
|
|
177
|
+
if (choice === 'yes') {
|
|
178
|
+
for (const t of ours) remove(t);
|
|
179
|
+
pruneEmptyParents(projectDir);
|
|
180
|
+
stdout.write(`\n Removed ${ours.length} item(s) from this project.\n`);
|
|
181
|
+
} else {
|
|
182
|
+
stdout.write('\n Kept project files.\n');
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// ---- Global layer ----
|
|
187
|
+
stdout.write('\n');
|
|
188
|
+
if (!fs.existsSync(globalDir)) {
|
|
189
|
+
stdout.write(`No global memory at ${globalDir}\n`);
|
|
190
|
+
} else {
|
|
191
|
+
stdout.write(`${bold('Global memory')} — ${dim(globalDir)}\n`);
|
|
192
|
+
stdout.write(
|
|
193
|
+
' This holds your agent\'s identity, your preferences, and every lesson it has\n' +
|
|
194
|
+
' learned across ALL projects. Removing it cannot be undone, and it is not\n' +
|
|
195
|
+
' recreated by reinstalling — the content is yours, not the package\'s.\n\n'
|
|
196
|
+
);
|
|
197
|
+
|
|
198
|
+
const wipe = await confirm('Remove global memory too?', false);
|
|
199
|
+
if (wipe) {
|
|
200
|
+
// Deleting months of accumulated lessons deserves a second, explicit yes.
|
|
201
|
+
const sure = await confirm('Are you sure? Your lessons and preferences will be gone.', false);
|
|
202
|
+
if (sure) {
|
|
203
|
+
fs.rmSync(globalDir, { recursive: true, force: true });
|
|
204
|
+
stdout.write('\n Removed global memory.\n');
|
|
205
|
+
} else {
|
|
206
|
+
stdout.write('\n Kept global memory.\n');
|
|
207
|
+
}
|
|
208
|
+
} else {
|
|
209
|
+
stdout.write('\n Kept global memory.\n');
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
stdout.write('\nDone.\n');
|
|
214
|
+
}
|
package/src/update.mjs
ADDED
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
// `kracked-core update` — refresh the package-owned files without touching
|
|
2
|
+
// anything the user has written.
|
|
3
|
+
//
|
|
4
|
+
// The distinction that makes this safe: SKILLS and LOADERS are package files
|
|
5
|
+
// (we wrote them, we can replace them). MEMORY is the user's (identity,
|
|
6
|
+
// preferences, lessons, project docs) — never overwritten here, ever.
|
|
7
|
+
|
|
8
|
+
import fs from 'node:fs';
|
|
9
|
+
import path from 'node:path';
|
|
10
|
+
import os from 'node:os';
|
|
11
|
+
import { stdout } from 'node:process';
|
|
12
|
+
import { createRequire } from 'node:module';
|
|
13
|
+
|
|
14
|
+
import { select } from './prompt.mjs';
|
|
15
|
+
import { writeLoaders, writeSkills, SKILL_NAMES } from './scaffold.mjs';
|
|
16
|
+
|
|
17
|
+
const require = createRequire(import.meta.url);
|
|
18
|
+
const dim = (s) => `\x1b[2m${s}\x1b[0m`;
|
|
19
|
+
const bold = (s) => `\x1b[1m${s}\x1b[0m`;
|
|
20
|
+
|
|
21
|
+
function installedVersion() {
|
|
22
|
+
try {
|
|
23
|
+
return require('../package.json').version;
|
|
24
|
+
} catch {
|
|
25
|
+
return 'unknown';
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Which harnesses this project already uses, so update doesn't add new ones. */
|
|
30
|
+
function detectInstalledHarnesses(projectDir) {
|
|
31
|
+
const editors = [];
|
|
32
|
+
if (fs.existsSync(path.join(projectDir, '.claude', 'skills'))) editors.push('claude');
|
|
33
|
+
if (fs.existsSync(path.join(projectDir, '.agents', 'skills'))) editors.push('antigravity');
|
|
34
|
+
// A CLAUDE.md with no skills dir still means Claude Code is in play.
|
|
35
|
+
if (!editors.includes('claude') && fs.existsSync(path.join(projectDir, 'CLAUDE.md'))) {
|
|
36
|
+
editors.push('claude');
|
|
37
|
+
}
|
|
38
|
+
return editors;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Read the agent + user name back out of an installed AGENTS.md. */
|
|
42
|
+
function recoverTokens(projectDir) {
|
|
43
|
+
const agentsPath = path.join(projectDir, 'AGENTS.md');
|
|
44
|
+
const tokens = { AGENT_NAME: 'KC', USER_NAME: os.userInfo().username || 'you' };
|
|
45
|
+
|
|
46
|
+
try {
|
|
47
|
+
const body = fs.readFileSync(agentsPath, 'utf8');
|
|
48
|
+
const agent = body.match(/You are \*\*([^*]+)\*\*/);
|
|
49
|
+
if (agent) tokens.AGENT_NAME = agent[1].trim();
|
|
50
|
+
const user = body.match(/how ([^\s]+) likes to work/);
|
|
51
|
+
if (user) tokens.USER_NAME = user[1].trim();
|
|
52
|
+
} catch {
|
|
53
|
+
// No AGENTS.md to read — fall back to defaults above.
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return tokens;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Which skills exist here but aren't in this version — i.e. new ones to add. */
|
|
60
|
+
function missingSkills(projectDir, editors) {
|
|
61
|
+
const missing = [];
|
|
62
|
+
for (const skill of SKILL_NAMES) {
|
|
63
|
+
const inClaude = editors.includes('claude')
|
|
64
|
+
&& fs.existsSync(path.join(projectDir, '.claude', 'skills', skill, 'SKILL.md'));
|
|
65
|
+
const inAgents = editors.includes('antigravity')
|
|
66
|
+
&& fs.existsSync(path.join(projectDir, '.agents', 'skills', skill, 'SKILL.md'));
|
|
67
|
+
const expected = (editors.includes('claude') ? 1 : 0) + (editors.includes('antigravity') ? 1 : 0);
|
|
68
|
+
const present = (inClaude ? 1 : 0) + (inAgents ? 1 : 0);
|
|
69
|
+
if (present < expected) missing.push(skill);
|
|
70
|
+
}
|
|
71
|
+
return missing;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export async function runUpdate() {
|
|
75
|
+
if (!process.stdin.isTTY) {
|
|
76
|
+
process.stderr.write(
|
|
77
|
+
'kracked-core update needs an interactive terminal.\n' +
|
|
78
|
+
'Run it directly in a terminal, not piped or in a script.\n'
|
|
79
|
+
);
|
|
80
|
+
process.exitCode = 1;
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const projectDir = process.cwd();
|
|
85
|
+
const version = installedVersion();
|
|
86
|
+
|
|
87
|
+
stdout.write(`${bold('kracked-core update')} ${dim(`v${version}`)}\n\n`);
|
|
88
|
+
|
|
89
|
+
if (!fs.existsSync(path.join(projectDir, '.kracked'))) {
|
|
90
|
+
stdout.write(
|
|
91
|
+
`No kracked-core install found in ${projectDir}\n\n` +
|
|
92
|
+
'Run `npx kracked-core init` to set it up first.\n'
|
|
93
|
+
);
|
|
94
|
+
process.exitCode = 1;
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const editors = detectInstalledHarnesses(projectDir);
|
|
99
|
+
if (editors.length === 0) {
|
|
100
|
+
stdout.write('Found .kracked/ but no editor files. Run `npx kracked-core init` instead.\n');
|
|
101
|
+
process.exitCode = 1;
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const tokens = {
|
|
106
|
+
...recoverTokens(projectDir),
|
|
107
|
+
PROJECT_NAME: path.basename(projectDir),
|
|
108
|
+
PROJECT_PATH: projectDir,
|
|
109
|
+
DATE: new Date().toISOString().slice(0, 10),
|
|
110
|
+
STACK: '',
|
|
111
|
+
RUN_COMMANDS: '',
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
const newSkills = missingSkills(projectDir, editors);
|
|
115
|
+
|
|
116
|
+
stdout.write(`Agent: ${tokens.AGENT_NAME}\n`);
|
|
117
|
+
stdout.write(`Editors: ${editors.join(', ')}\n`);
|
|
118
|
+
if (newSkills.length) {
|
|
119
|
+
stdout.write(`New in this version: ${newSkills.join(', ')}\n`);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
stdout.write(`\n${bold('Will be refreshed')} ${dim('(package files — safe to replace)')}\n`);
|
|
123
|
+
stdout.write(' the 5 skills, AGENTS.md, CLAUDE.md, .agents/rules/kracked.md\n');
|
|
124
|
+
stdout.write(`\n${bold('Will NOT be touched')} ${dim('(yours)')}\n`);
|
|
125
|
+
stdout.write(' ~/.kracked/ — identity, preferences, lessons\n');
|
|
126
|
+
stdout.write(' .kracked/ — project memory, specs, epics, stories, architecture\n\n');
|
|
127
|
+
|
|
128
|
+
const choice = await select('Refresh package files to this version?', [
|
|
129
|
+
{ label: 'Yes, update', value: 'yes' },
|
|
130
|
+
{ label: 'No, cancel', value: 'no' },
|
|
131
|
+
], 0);
|
|
132
|
+
|
|
133
|
+
if (choice === 'no') {
|
|
134
|
+
stdout.write('\nCancelled — nothing changed.\n');
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Package files are ours, so overwrite silently rather than prompting per
|
|
139
|
+
// file. User memory is never in this set.
|
|
140
|
+
const created = [];
|
|
141
|
+
const report = (entry) => created.push(entry);
|
|
142
|
+
const overwrite = async () => 'overwrite';
|
|
143
|
+
|
|
144
|
+
stdout.write('\nUpdating...\n');
|
|
145
|
+
await writeLoaders({ projectDir, tokens, ask: overwrite, report, editors });
|
|
146
|
+
await writeSkills({ projectDir, tokens, editors, ask: overwrite, report });
|
|
147
|
+
|
|
148
|
+
stdout.write(`\n Refreshed ${created.length} file(s).\n`);
|
|
149
|
+
stdout.write('\nRestart your editor so it picks up the updated skills.\n');
|
|
150
|
+
}
|
package/src/wizard.mjs
CHANGED
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
scanExistingProject,
|
|
14
14
|
stackSummaryLine,
|
|
15
15
|
detectExistingAgentSetup,
|
|
16
|
+
sanitizeScanned,
|
|
16
17
|
} from './detect.mjs';
|
|
17
18
|
import { select, checkbox, confirm, input } from './prompt.mjs';
|
|
18
19
|
import {
|
|
@@ -38,6 +39,31 @@ function isoDate() {
|
|
|
38
39
|
return new Date().toISOString().slice(0, 10);
|
|
39
40
|
}
|
|
40
41
|
|
|
42
|
+
// Installing into these scatters loader files across the filesystem, and
|
|
43
|
+
// installing into the home dir makes `<project>/.kracked` collide with global
|
|
44
|
+
// memory. A typo like `/` should not silently create a tree.
|
|
45
|
+
const FORBIDDEN_DIRS = ['/', '/etc', '/usr', '/bin', '/sbin', '/var', '/System', '/Library'];
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Refuse obviously wrong install targets before anything is written.
|
|
49
|
+
* Returns an error string, or null when the path is fine.
|
|
50
|
+
*/
|
|
51
|
+
function rejectUnsafeProjectDir(dir) {
|
|
52
|
+
const resolved = path.resolve(dir);
|
|
53
|
+
|
|
54
|
+
if (FORBIDDEN_DIRS.includes(resolved)) {
|
|
55
|
+
return `${resolved} is a system directory — pick a project folder instead.`;
|
|
56
|
+
}
|
|
57
|
+
if (resolved === path.resolve(os.homedir())) {
|
|
58
|
+
return (
|
|
59
|
+
'That\'s your home directory. Installing here would put loader files loose in\n' +
|
|
60
|
+
' your home folder and collide with global memory at ~/.kracked.\n' +
|
|
61
|
+
' Make a project folder first, then run this inside it.'
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
|
|
41
67
|
/**
|
|
42
68
|
* Turn detected package.json scripts into a ready-to-read "how to run it" block.
|
|
43
69
|
* The scan already knows these commands — writing them into project.md is the
|
|
@@ -136,11 +162,11 @@ async function wizardFlow() {
|
|
|
136
162
|
stdout.write('kracked-core — set up memory for your AI coding agent\n\n');
|
|
137
163
|
|
|
138
164
|
// 1. Agent name
|
|
139
|
-
const agentName = (await input('What should we call your agent?', 'KC')) || 'KC';
|
|
165
|
+
const agentName = sanitizeScanned(await input('What should we call your agent?', 'KC'), 40) || 'KC';
|
|
140
166
|
|
|
141
167
|
// 2. User name
|
|
142
168
|
const systemUser = os.userInfo().username || 'you';
|
|
143
|
-
const userName = (await input('Your name?', systemUser)) || systemUser;
|
|
169
|
+
const userName = sanitizeScanned(await input('Your name?', systemUser), 40) || systemUser;
|
|
144
170
|
|
|
145
171
|
// 3. Global memory — always installed. It holds identity, preferences and
|
|
146
172
|
// cross-project lessons, and boot depends on it; making it optional only
|
|
@@ -165,6 +191,35 @@ async function wizardFlow() {
|
|
|
165
191
|
const expanded = expandTilde(pathAnswer);
|
|
166
192
|
projectDir = path.resolve(expanded === '' ? '.' : expanded);
|
|
167
193
|
|
|
194
|
+
const unsafe = rejectUnsafeProjectDir(projectDir);
|
|
195
|
+
if (unsafe) {
|
|
196
|
+
stdout.write(`\n ${unsafe}\n\nStopped — nothing was written.\n`);
|
|
197
|
+
process.exitCode = 1;
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// Already installed here? 27 individual conflict prompts is not a sane
|
|
202
|
+
// default — offer the three real intents up front instead.
|
|
203
|
+
if (fs.existsSync(path.join(projectDir, '.kracked'))) {
|
|
204
|
+
stdout.write('\n kracked-core is already set up in this project.\n\n');
|
|
205
|
+
const action = await select('What would you like to do?', [
|
|
206
|
+
{ label: 'Refresh package files', value: 'refresh', hint: 'skills + loaders, keeps all your memory' },
|
|
207
|
+
{ label: 'Reinstall everything', value: 'reinstall', hint: 'asks per file before replacing' },
|
|
208
|
+
{ label: 'Cancel', value: 'cancel' },
|
|
209
|
+
], 0);
|
|
210
|
+
|
|
211
|
+
if (action === 'cancel') {
|
|
212
|
+
stdout.write('\nCancelled — nothing changed.\n');
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
if (action === 'refresh') {
|
|
216
|
+
const { runUpdate } = await import('./update.mjs');
|
|
217
|
+
process.chdir(projectDir);
|
|
218
|
+
await runUpdate();
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
168
223
|
// Another agent system already here? Ask before touching its loaders.
|
|
169
224
|
if (fs.existsSync(projectDir)) {
|
|
170
225
|
const proceed = await warnOnExistingSetup(projectDir);
|
|
@@ -205,7 +260,7 @@ async function wizardFlow() {
|
|
|
205
260
|
const confirmed = await confirm('Does this look right?', true);
|
|
206
261
|
if (!confirmed) {
|
|
207
262
|
const manualStack = await input('Describe the stack yourself:', stackLine);
|
|
208
|
-
stackLine = manualStack;
|
|
263
|
+
stackLine = sanitizeScanned(manualStack, 80) || stackLine;
|
|
209
264
|
}
|
|
210
265
|
}
|
|
211
266
|
}
|
|
@@ -236,8 +291,26 @@ async function wizardFlow() {
|
|
|
236
291
|
checked: !claudeTaken,
|
|
237
292
|
hint: claudeTaken ? 'CLAUDE.md already in use here' : 'writes CLAUDE.md + .claude/',
|
|
238
293
|
},
|
|
294
|
+
{
|
|
295
|
+
label: 'Kilo Code (VS Code)',
|
|
296
|
+
value: 'kilo',
|
|
297
|
+
checked: false,
|
|
298
|
+
hint: 'reads AGENTS.md + .agents/skills; adds kilo.jsonc',
|
|
299
|
+
},
|
|
300
|
+
{
|
|
301
|
+
label: 'Roo Code (VS Code)',
|
|
302
|
+
value: 'roo',
|
|
303
|
+
checked: false,
|
|
304
|
+
hint: 'reads AGENTS.md + .agents/skills; adds .roo/rules',
|
|
305
|
+
},
|
|
239
306
|
]);
|
|
240
307
|
|
|
308
|
+
// Kilo and Roo both read AGENTS.md and .agents/skills/ natively, so picking
|
|
309
|
+
// either implies the shared .agents/ footprint.
|
|
310
|
+
if ((editors.includes('kilo') || editors.includes('roo')) && !editors.includes('antigravity')) {
|
|
311
|
+
editors.push('antigravity');
|
|
312
|
+
}
|
|
313
|
+
|
|
241
314
|
// 6. Write files
|
|
242
315
|
const projectName = projectSummary?.name || (setUpProject ? path.basename(projectDir) : 'unnamed-project');
|
|
243
316
|
|
|
@@ -266,7 +339,7 @@ async function wizardFlow() {
|
|
|
266
339
|
|
|
267
340
|
if (setUpProject) {
|
|
268
341
|
await writeProjectMemory({ projectDir, tokens, ask: conflictAsk, report: reporter });
|
|
269
|
-
await writeLoaders({ projectDir, tokens, ask: conflictAsk, report: reporter });
|
|
342
|
+
await writeLoaders({ projectDir, tokens, ask: conflictAsk, report: reporter, editors });
|
|
270
343
|
await writeSkills({ projectDir, tokens, editors, ask: conflictAsk, report: reporter });
|
|
271
344
|
}
|
|
272
345
|
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# SDD — {{PROJECT_NAME}}
|
|
2
|
+
|
|
3
|
+
Spec-driven development artifacts. Run `/kracked-sdd` to work through the flow; these folders are
|
|
4
|
+
where it puts things.
|
|
5
|
+
|
|
6
|
+
```
|
|
7
|
+
idea → spec → epic → stories → build → review
|
|
8
|
+
```
|
|
9
|
+
|
|
10
|
+
| Folder | Holds | Written when |
|
|
11
|
+
|---|---|---|
|
|
12
|
+
| `specs/` | What to build and why | Start of any non-trivial job |
|
|
13
|
+
| `epics/` | A group of related stories | When a spec is too big for one story |
|
|
14
|
+
| `stories/` | One independently shippable slice | Broken out of a spec or epic |
|
|
15
|
+
| `architecture/` | How the system is put together | Large or risky work only |
|
|
16
|
+
| `tracker.md` | Status of every story + evidence | Continuously |
|
|
17
|
+
|
|
18
|
+
## Naming
|
|
19
|
+
|
|
20
|
+
Numbers first so files sort in the order you built them.
|
|
21
|
+
|
|
22
|
+
```
|
|
23
|
+
specs/login.md
|
|
24
|
+
epics/1-authentication.md
|
|
25
|
+
stories/1.1-email-login.md
|
|
26
|
+
stories/1.2-password-reset.md
|
|
27
|
+
architecture/data-model.md
|
|
28
|
+
architecture/decisions/0001-use-postgres.md
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Story IDs are `<epic>.<story>` and must match the ID in `tracker.md`. That's the link between a
|
|
32
|
+
story's detail and its status — if they drift, the tracker stops being trustworthy.
|
|
33
|
+
|
|
34
|
+
## The two rules that matter
|
|
35
|
+
|
|
36
|
+
**1. Not everything needs the full flow.** A typo fix needs no spec. `/kracked-sdd` sizes the job
|
|
37
|
+
first and skips straight to the build when ceremony would be waste. Over-documenting small work is
|
|
38
|
+
the fastest way to abandon the system entirely.
|
|
39
|
+
|
|
40
|
+
**2. A story isn't `done` without evidence.** The tracker's Evidence column must say what was
|
|
41
|
+
actually verified — and what wasn't. "Tests pass" is not evidence of a working feature.
|
|
42
|
+
|
|
43
|
+
## Commit these
|
|
44
|
+
|
|
45
|
+
These docs belong in git. They're how the next person — including you in three months, and your
|
|
46
|
+
agent on its next boot — finds out why the code looks the way it does.
|
|
File without changes
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# Architecture — <area>
|
|
2
|
+
|
|
3
|
+
**Date:** {{DATE}}
|
|
4
|
+
**Status:** current | superseded by `<file>`
|
|
5
|
+
|
|
6
|
+
Write one of these only for work that's **large or risky** — new services, data model changes,
|
|
7
|
+
auth, anything touching money or user data. A small feature does not need an architecture doc,
|
|
8
|
+
and writing one anyway is how the system becomes ceremony people skip.
|
|
9
|
+
|
|
10
|
+
## The shape
|
|
11
|
+
|
|
12
|
+
What the pieces are and how they talk to each other. A diagram in text is fine:
|
|
13
|
+
|
|
14
|
+
```
|
|
15
|
+
browser → api route → service → database
|
|
16
|
+
↘ queue → worker
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Data model
|
|
20
|
+
|
|
21
|
+
Tables/collections, key fields, and the relationships that matter. Not every column — the ones a
|
|
22
|
+
newcomer would get wrong.
|
|
23
|
+
|
|
24
|
+
| Entity | Key fields | Related to |
|
|
25
|
+
|---|---|---|
|
|
26
|
+
| | | |
|
|
27
|
+
|
|
28
|
+
## Key decisions
|
|
29
|
+
|
|
30
|
+
The choices someone would otherwise re-litigate. Link to an ADR in `decisions/` for the big ones.
|
|
31
|
+
|
|
32
|
+
| Decision | Why | Alternative rejected |
|
|
33
|
+
|---|---|---|
|
|
34
|
+
| | | |
|
|
35
|
+
|
|
36
|
+
## Failure modes
|
|
37
|
+
|
|
38
|
+
What breaks, and what happens when it does. Answer for each: does it fail loudly or silently?
|
|
39
|
+
|
|
40
|
+
| What fails | Effect | Handling |
|
|
41
|
+
|---|---|---|
|
|
42
|
+
| Database unreachable | | |
|
|
43
|
+
| Third-party API down | | |
|
|
44
|
+
| Duplicate/retried request | | |
|
|
45
|
+
|
|
46
|
+
## What this does NOT do
|
|
47
|
+
|
|
48
|
+
Scope boundaries and deliberate limitations. Prevents someone assuming a capability that isn't
|
|
49
|
+
there.
|
|
50
|
+
|
|
51
|
+
- ...
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# <NNNN> — <the decision, as a statement>
|
|
2
|
+
|
|
3
|
+
**Date:** {{DATE}}
|
|
4
|
+
**Status:** accepted | superseded by `<NNNN>` | reversed
|
|
5
|
+
|
|
6
|
+
> Example filename: `0001-use-postgres-not-mongo.md`
|
|
7
|
+
> Number them in order. Never renumber or delete one — a reversed decision is still history worth
|
|
8
|
+
> keeping, and the reasoning is often more valuable than the outcome.
|
|
9
|
+
|
|
10
|
+
## Context
|
|
11
|
+
|
|
12
|
+
What forced a choice here? The constraints, the pressures, what was true at the time. Someone
|
|
13
|
+
reading this in six months has none of that context — this section is the whole point.
|
|
14
|
+
|
|
15
|
+
## Decision
|
|
16
|
+
|
|
17
|
+
What was decided, stated plainly and in the present tense. "We use X." Not "we should" or
|
|
18
|
+
"we might."
|
|
19
|
+
|
|
20
|
+
## Alternatives considered
|
|
21
|
+
|
|
22
|
+
| Option | Why not |
|
|
23
|
+
|---|---|
|
|
24
|
+
| | |
|
|
25
|
+
|
|
26
|
+
## Consequences
|
|
27
|
+
|
|
28
|
+
Both directions — honest about the costs.
|
|
29
|
+
|
|
30
|
+
**Good:**
|
|
31
|
+
- ...
|
|
32
|
+
|
|
33
|
+
**Costs / things we now have to live with:**
|
|
34
|
+
- ...
|
|
35
|
+
|
|
36
|
+
## Revisit if
|
|
37
|
+
|
|
38
|
+
The conditions that would make this decision wrong. If none, say "no expected trigger."
|
|
39
|
+
|
|
40
|
+
- ...
|
|
File without changes
|