kracked-core 1.0.0 → 1.1.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 CHANGED
@@ -53,12 +53,15 @@ The wizard asks a few questions — name your agent, is this a new or existing p
53
53
 
54
54
  ## Use
55
55
 
56
- Three commands. That's the whole thing.
56
+ Five commands. Three of them are the whole daily loop.
57
57
 
58
58
  ```
59
59
  /kracked-boot Start of session — agent loads its memory and tells you where you left off
60
60
  /kracked-sdd Build something — idea → spec → docs → build → review
61
61
  /kracked-wrap End of session — agent writes down what it learned
62
+
63
+ /kracked-identity Change your agent's name, tone, or how you like to work
64
+ /kracked-explain "What is all this?" — walks you through what got installed
62
65
  ```
63
66
 
64
67
  > **`/kracked-wrap` is not optional.** Boot without wrap is a memory system that just forgets more slowly. The wrap is where memory actually gets written.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kracked-core",
3
- "version": "1.0.0",
3
+ "version": "1.1.0",
4
4
  "description": "Zero-dependency memory & workflow installer for AI coding agents — global identity, project memory, and skills that survive session restarts.",
5
5
  "type": "module",
6
6
  "bin": {
package/src/detect.mjs CHANGED
@@ -15,6 +15,39 @@ export function globalMemoryDir() {
15
15
  return path.join(os.homedir(), '.kracked');
16
16
  }
17
17
 
18
+ /**
19
+ * Find agent setups already governing this directory, so we never silently
20
+ * overwrite someone's existing system. A CLAUDE.md/AGENTS.md that kracked-core
21
+ * didn't write is another system's loader — installing over it would replace
22
+ * that agent's identity, which is exactly the failure this check prevents.
23
+ *
24
+ * Returns [{ file, path, scope, ours }] — `ours` marks files kracked-core
25
+ * itself wrote (safe to refresh), everything else is someone else's.
26
+ */
27
+ export function detectExistingAgentSetup(projectDir) {
28
+ const found = [];
29
+
30
+ const inspect = (filePath, label, scope) => {
31
+ if (!fs.existsSync(filePath)) return;
32
+ let body = '';
33
+ try {
34
+ body = fs.readFileSync(filePath, 'utf8');
35
+ } catch {
36
+ return; // unreadable is not our problem to solve here
37
+ }
38
+ // Our own loaders always reference kracked-core by name.
39
+ const ours = /kracked-core|kracked\.md|\.kracked\//.test(body);
40
+ found.push({ file: label, path: filePath, scope, ours });
41
+ };
42
+
43
+ inspect(path.join(projectDir, 'CLAUDE.md'), 'CLAUDE.md', 'project');
44
+ inspect(path.join(projectDir, 'AGENTS.md'), 'AGENTS.md', 'project');
45
+ inspect(path.join(projectDir, '.agents', 'rules', 'kracked.md'), '.agents/rules/kracked.md', 'project');
46
+ inspect(path.join(os.homedir(), '.claude', 'CLAUDE.md'), '~/.claude/CLAUDE.md', 'global');
47
+
48
+ return found;
49
+ }
50
+
18
51
  /**
19
52
  * Is the target dir an existing codebase, empty, or new (doesn't exist yet)?
20
53
  * Returns one of: "new" (doesn't exist), "empty" (exists, no entries),
package/src/prompt.mjs ADDED
@@ -0,0 +1,235 @@
1
+ // Keyboard-driven prompts: arrow-key single select and spacebar checkboxes.
2
+ // Zero dependencies — raw TTY keypress handling, so the installer never fails
3
+ // in a classroom because of a missing package.
4
+
5
+ import { stdin, stdout } from 'node:process';
6
+
7
+ const ESC = '';
8
+ const KEY = {
9
+ up: `${ESC}[A`,
10
+ down: `${ESC}[B`,
11
+ ctrlC: '',
12
+ ctrlD: '',
13
+ enter: '\r',
14
+ newline: '\n',
15
+ space: ' ',
16
+ };
17
+
18
+ const c = {
19
+ dim: (s) => `${s}`,
20
+ cyan: (s) => `${s}`,
21
+ green: (s) => `${s}`,
22
+ bold: (s) => `${s}`,
23
+ };
24
+
25
+ /** Hide/show the cursor so the redrawing menu doesn't flicker a caret around. */
26
+ function setCursor(visible) {
27
+ stdout.write(visible ? `${ESC}[?25h` : `${ESC}[?25l`);
28
+ }
29
+
30
+ /** Move up n lines and clear from there down — used to redraw the menu in place. */
31
+ function clearLines(n) {
32
+ if (n > 0) stdout.write(`${ESC}[${n}A`);
33
+ stdout.write(`${ESC}[0J`);
34
+ }
35
+
36
+ /**
37
+ * Read single keypresses until `onKey` signals completion.
38
+ * onKey(key) returns { done: true, value } to finish, or falsy to keep listening.
39
+ */
40
+ function readKeys(onKey) {
41
+ return new Promise((resolve, reject) => {
42
+ const wasRaw = stdin.isRaw;
43
+ stdin.setRawMode(true);
44
+ stdin.resume();
45
+ stdin.setEncoding('utf8');
46
+
47
+ const cleanup = () => {
48
+ stdin.setRawMode(wasRaw ?? false);
49
+ stdin.pause();
50
+ stdin.removeListener('data', handler);
51
+ setCursor(true);
52
+ };
53
+
54
+ function handler(key) {
55
+ // Ctrl+C / Ctrl+D must always escape, whatever the prompt is doing.
56
+ if (key === KEY.ctrlC || key === KEY.ctrlD) {
57
+ cleanup();
58
+ stdout.write('\n\nCancelled — nothing was written.\n');
59
+ process.exit(130);
60
+ }
61
+
62
+ let result;
63
+ try {
64
+ result = onKey(key);
65
+ } catch (err) {
66
+ cleanup();
67
+ reject(err);
68
+ return;
69
+ }
70
+
71
+ if (result && result.done) {
72
+ cleanup();
73
+ resolve(result.value);
74
+ }
75
+ }
76
+
77
+ stdin.on('data', handler);
78
+ });
79
+ }
80
+
81
+ /**
82
+ * Arrow-key single select. `choices` is [{ label, value, hint }].
83
+ * Returns the chosen `value`.
84
+ */
85
+ export async function select(question, choices, defaultIndex = 0) {
86
+ let index = defaultIndex;
87
+ let drawn = 0;
88
+
89
+ const render = () => {
90
+ clearLines(drawn);
91
+ let out = `${c.bold('?')} ${question} ${c.dim('↑↓ to move, Enter to pick')}\n`;
92
+ for (let i = 0; i < choices.length; i++) {
93
+ const active = i === index;
94
+ const pointer = active ? c.cyan('❯') : ' ';
95
+ const label = active ? c.cyan(choices[i].label) : choices[i].label;
96
+ const hint = choices[i].hint ? ` ${c.dim(choices[i].hint)}` : '';
97
+ out += `${pointer} ${label}${hint}\n`;
98
+ }
99
+ stdout.write(out);
100
+ drawn = choices.length + 1;
101
+ };
102
+
103
+ setCursor(false);
104
+ render();
105
+
106
+ const value = await readKeys((key) => {
107
+ if (key === KEY.up) {
108
+ index = (index - 1 + choices.length) % choices.length;
109
+ render();
110
+ } else if (key === KEY.down) {
111
+ index = (index + 1) % choices.length;
112
+ render();
113
+ } else if (key === KEY.enter || key === KEY.newline) {
114
+ return { done: true, value: choices[index].value };
115
+ }
116
+ return null;
117
+ });
118
+
119
+ // Collapse the menu to a single answered line.
120
+ clearLines(drawn);
121
+ const chosen = choices.find((ch) => ch.value === value);
122
+ stdout.write(`${c.green('✔')} ${question} ${c.cyan(chosen.label)}\n`);
123
+ return value;
124
+ }
125
+
126
+ /**
127
+ * Spacebar checkbox multi-select. `choices` is [{ label, value, hint, checked }].
128
+ * Returns an array of checked `value`s. Enter with nothing checked is refused,
129
+ * since every multi-select in this wizard needs at least one answer.
130
+ */
131
+ export async function checkbox(question, choices) {
132
+ let index = 0;
133
+ let drawn = 0;
134
+ const state = choices.map((ch) => Boolean(ch.checked));
135
+ let warning = '';
136
+
137
+ const render = () => {
138
+ clearLines(drawn);
139
+ let out = `${c.bold('?')} ${question} ${c.dim('↑↓ move, space to toggle, Enter to confirm')}\n`;
140
+ for (let i = 0; i < choices.length; i++) {
141
+ const active = i === index;
142
+ const pointer = active ? c.cyan('❯') : ' ';
143
+ const box = state[i] ? c.green('◉') : '◯';
144
+ const label = active ? c.cyan(choices[i].label) : choices[i].label;
145
+ const hint = choices[i].hint ? ` ${c.dim(choices[i].hint)}` : '';
146
+ out += `${pointer} ${box} ${label}${hint}\n`;
147
+ }
148
+ if (warning) out += `${c.dim(warning)}\n`;
149
+ stdout.write(out);
150
+ drawn = choices.length + 1 + (warning ? 1 : 0);
151
+ };
152
+
153
+ setCursor(false);
154
+ render();
155
+
156
+ const values = await readKeys((key) => {
157
+ if (key === KEY.up) {
158
+ index = (index - 1 + choices.length) % choices.length;
159
+ warning = '';
160
+ render();
161
+ } else if (key === KEY.down) {
162
+ index = (index + 1) % choices.length;
163
+ warning = '';
164
+ render();
165
+ } else if (key === KEY.space) {
166
+ state[index] = !state[index];
167
+ warning = '';
168
+ render();
169
+ } else if (key === KEY.enter || key === KEY.newline) {
170
+ const picked = choices.filter((_, i) => state[i]).map((ch) => ch.value);
171
+ if (picked.length === 0) {
172
+ warning = ' Pick at least one (space to toggle).';
173
+ render();
174
+ return null;
175
+ }
176
+ return { done: true, value: picked };
177
+ }
178
+ return null;
179
+ });
180
+
181
+ clearLines(drawn);
182
+ const labels = choices.filter((_, i) => state[i]).map((ch) => ch.label).join(', ');
183
+ stdout.write(`${c.green('✔')} ${question} ${c.cyan(labels)}\n`);
184
+ return values;
185
+ }
186
+
187
+ /**
188
+ * Free-text input with a default. Implemented on the same raw-mode reader as
189
+ * the menus: readline and raw mode cannot share stdin — if both are listening,
190
+ * keystrokes go to the wrong reader and the wizard hangs. One owner, always.
191
+ */
192
+ export async function input(question, defaultValue = '') {
193
+ let buffer = '';
194
+ const hint = defaultValue ? c.dim(` [${defaultValue}]`) : '';
195
+
196
+ const render = () => {
197
+ stdout.write(`\r${ESC}[0K${c.bold('?')} ${question}${hint} ${buffer}`);
198
+ };
199
+
200
+ setCursor(true);
201
+ render();
202
+
203
+ const value = await readKeys((key) => {
204
+ if (key === KEY.enter || key === KEY.newline) {
205
+ return { done: true, value: buffer.trim() || defaultValue };
206
+ }
207
+ // Backspace (DEL or BS)
208
+ if (key === '' || key === '\b') {
209
+ buffer = buffer.slice(0, -1);
210
+ render();
211
+ return null;
212
+ }
213
+ // Ignore escape sequences (arrows etc.) and other control chars.
214
+ if (key.startsWith(ESC) || key < ' ') return null;
215
+
216
+ buffer += key;
217
+ render();
218
+ return null;
219
+ });
220
+
221
+ stdout.write(`\r${ESC}[0K${c.green('✔')} ${question} ${c.cyan(value)}\n`);
222
+ return value;
223
+ }
224
+
225
+ /** Yes/no as an arrow menu, so every prompt in the wizard behaves the same way. */
226
+ export async function confirm(question, defaultYes = true) {
227
+ return select(
228
+ question,
229
+ [
230
+ { label: 'Yes', value: true },
231
+ { label: 'No', value: false },
232
+ ],
233
+ defaultYes ? 0 : 1
234
+ );
235
+ }
package/src/scaffold.mjs CHANGED
@@ -9,7 +9,13 @@ 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
- const SKILL_NAMES = ['kracked-boot', 'kracked-sdd', 'kracked-wrap', 'kracked-explain'];
12
+ const SKILL_NAMES = [
13
+ 'kracked-boot',
14
+ 'kracked-sdd',
15
+ 'kracked-wrap',
16
+ 'kracked-explain',
17
+ 'kracked-identity',
18
+ ];
13
19
 
14
20
  /** Read a template file, throwing a clear error if Track A hasn't shipped it yet. */
15
21
  function readTemplate(relPath) {
@@ -173,7 +179,7 @@ export async function writeLoaders({ projectDir, tokens, ask, report }) {
173
179
  }
174
180
 
175
181
  /**
176
- * Write the 4 skills to BOTH .claude/skills/<name>/SKILL.md and
182
+ * Write every skill to BOTH .claude/skills/<name>/SKILL.md and
177
183
  * .agents/skills/<name>/SKILL.md — but only for the harnesses selected.
178
184
  * `editors` is an array that may contain 'antigravity' and/or 'claude'.
179
185
  */
package/src/wizard.mjs CHANGED
@@ -1,7 +1,6 @@
1
1
  // Interactive prompt flow for `npx kracked-core init`.
2
2
  // Follows the wizard flow in CONTRACT.md exactly — question order matters.
3
3
 
4
- import readline from 'node:readline/promises';
5
4
  import { stdin, stdout } from 'node:process';
6
5
  import fs from 'node:fs';
7
6
  import path from 'node:path';
@@ -13,7 +12,9 @@ import {
13
12
  classifyProjectDir,
14
13
  scanExistingProject,
15
14
  stackSummaryLine,
15
+ detectExistingAgentSetup,
16
16
  } from './detect.mjs';
17
+ import { select, checkbox, confirm, input } from './prompt.mjs';
17
18
  import {
18
19
  writeGlobalMemory,
19
20
  registerProject,
@@ -68,87 +69,91 @@ export async function runInit() {
68
69
  return;
69
70
  }
70
71
 
71
- const rl = readline.createInterface({ input: stdin, output: stdout });
72
-
73
- let interrupted = false;
72
+ // Ctrl+C / Ctrl+D are handled inside prompt.mjs, which owns stdin in raw
73
+ // mode. Keep a SIGINT guard for interrupts arriving between prompts.
74
74
  const onSigint = () => {
75
- interrupted = true;
76
75
  stdout.write('\n\nCancelled. Nothing after this point was written.\n');
77
- rl.close();
78
76
  process.exit(130);
79
77
  };
80
78
  process.on('SIGINT', onSigint);
81
79
 
82
80
  try {
83
- await wizardFlow(rl);
84
- } catch (err) {
85
- // Ctrl+D (EOF) mid-question surfaces as readline's AbortError. Treat it
86
- // the same as Ctrl+C — a clean cancel, not a crash with a scary stack.
87
- if (err.code === 'ABORT_ERR' || /ctrl\+d/i.test(err.message || '')) {
88
- stdout.write('\n\nCancelled. Nothing after this point was written.\n');
89
- process.exitCode = 130;
90
- return;
91
- }
92
- throw err;
81
+ await wizardFlow();
93
82
  } finally {
94
83
  process.off('SIGINT', onSigint);
95
- if (!interrupted) rl.close();
84
+ stdin.pause();
96
85
  }
97
86
  }
98
87
 
99
- /** Ask a question with a default; Enter alone accepts the default. */
100
- async function ask(rl, question, defaultValue) {
101
- const suffix = defaultValue ? ` [${defaultValue}]` : '';
102
- const answer = (await rl.question(`${question}${suffix} `)).trim();
103
- return answer === '' ? defaultValue : answer;
88
+ /** Prompt for how to resolve a file-already-exists conflict. */
89
+ async function askConflict(destPath) {
90
+ stdout.write(`\n Already exists: ${destPath}\n`);
91
+ return select(
92
+ ' What should we do?',
93
+ [
94
+ { label: 'Skip', value: 'skip', hint: 'keep the existing file' },
95
+ { label: 'Overwrite', value: 'overwrite', hint: 'replace it' },
96
+ { label: 'Write alongside', value: 'alongside', hint: 'save as .kracked-new' },
97
+ ],
98
+ 0
99
+ );
104
100
  }
105
101
 
106
- /** Ask a yes/no question. Returns boolean. Default shown as Y/n or y/N. */
107
- async function askYesNo(rl, question, defaultYes) {
108
- const suffix = defaultYes ? '[Y/n]' : '[y/N]';
109
- const answer = (await rl.question(`${question} ${suffix} `)).trim().toLowerCase();
110
- if (answer === '') return defaultYes;
111
- return answer === 'y' || answer === 'yes';
102
+ /**
103
+ * Warn before writing into a directory another agent system already governs.
104
+ * A CLAUDE.md we didn't write belongs to someone else's setup — overwriting it
105
+ * silently replaces that agent's identity. Ask first, every time.
106
+ */
107
+ async function warnOnExistingSetup(projectDir) {
108
+ const found = detectExistingAgentSetup(projectDir).filter((f) => !f.ours);
109
+ if (found.length === 0) return true;
110
+
111
+ stdout.write('\n Heads up — this looks like it already has an agent setup:\n');
112
+ for (const f of found) {
113
+ const where = f.scope === 'global' ? 'global' : 'this project';
114
+ stdout.write(` • ${f.file} ${c_dim(`(${where})`)}\n`);
115
+ }
116
+ stdout.write(
117
+ '\n Installing here would overwrite those loaders and change who your\n' +
118
+ ' agent is in this directory. Existing files are never replaced without\n' +
119
+ ' asking, but it is easy to end up with two systems giving different\n' +
120
+ ' instructions.\n\n'
121
+ );
122
+
123
+ return confirm('Continue installing here?', false);
112
124
  }
113
125
 
114
- /** Prompt for how to resolve a file-already-exists conflict. */
115
- async function askConflict(rl, destPath) {
116
- stdout.write(`\n Already exists: ${destPath}\n`);
117
- const answer = (
118
- await rl.question(' skip / overwrite / write alongside as .kracked-new? [skip] ')
119
- ).trim().toLowerCase();
120
-
121
- if (answer === 'o' || answer === 'overwrite') return 'overwrite';
122
- if (answer === 'a' || answer === 'alongside' || answer === 'write alongside') return 'alongside';
123
- return 'skip';
126
+ /** Minimal dim helper for wizard-level notices. */
127
+ function c_dim(s) {
128
+ return `${s}`;
124
129
  }
125
130
 
126
- async function wizardFlow(rl) {
131
+ async function wizardFlow() {
127
132
  const created = [];
128
133
  const reporter = (entry) => created.push(entry);
129
- const conflictAsk = (destPath) => askConflict(rl, destPath);
134
+ const conflictAsk = (destPath) => askConflict(destPath);
130
135
 
131
136
  stdout.write('kracked-core — set up memory for your AI coding agent\n\n');
132
137
 
133
138
  // 1. Agent name
134
- const agentName = (await ask(rl, 'What should we call your agent?', 'KC')) || 'KC';
139
+ const agentName = (await input('What should we call your agent?', 'KC')) || 'KC';
135
140
 
136
141
  // 2. User name
137
142
  const systemUser = os.userInfo().username || 'you';
138
- const userName = (await ask(rl, 'Your name?', systemUser)) || systemUser;
143
+ const userName = (await input('Your name?', systemUser)) || systemUser;
139
144
 
140
- // 3. Global memory
145
+ // 3. Global memory — always installed. It holds identity, preferences and
146
+ // cross-project lessons, and boot depends on it; making it optional only
147
+ // created a way to end up with a half-installed system that can't boot.
148
+ // Existing files are still never overwritten without asking.
141
149
  const hasGlobal = globalMemoryExists();
142
- let setUpGlobal;
143
- if (!hasGlobal) {
144
- setUpGlobal = await askYesNo(rl, 'Set up global memory now?', true);
145
- } else {
146
- const reuse = await askYesNo(rl, 'Global memory found. Reuse it?', true);
147
- setUpGlobal = !reuse; // if not reusing, we still (re)write it below
150
+ const setUpGlobal = true;
151
+ if (hasGlobal) {
152
+ stdout.write(`\n Global memory found at ${globalMemoryDir()} reusing it.\n`);
148
153
  }
149
154
 
150
155
  // 4. Project setup
151
- const setUpProject = await askYesNo(rl, 'Set up this project?', true);
156
+ const setUpProject = await confirm('Set up this project?', true);
152
157
 
153
158
  let projectDir = process.cwd();
154
159
  let projectMode = null; // 'new' | 'existing'
@@ -156,24 +161,29 @@ async function wizardFlow(rl) {
156
161
  let projectSummary = null;
157
162
 
158
163
  if (setUpProject) {
159
- const pathAnswer = await ask(rl, 'Project directory?', '.');
164
+ const pathAnswer = await input('Project directory?', '.');
160
165
  const expanded = expandTilde(pathAnswer);
161
166
  projectDir = path.resolve(expanded === '' ? '.' : expanded);
162
167
 
163
- const classification = classifyProjectDir(projectDir);
164
-
165
- if (classification === 'existing') {
166
- const modeAnswer = (
167
- await ask(rl, 'New project or existing codebase? (new/existing)', 'existing')
168
- ).toLowerCase();
169
- projectMode = modeAnswer.startsWith('n') ? 'new' : 'existing';
170
- } else {
171
- const modeAnswer = (
172
- await ask(rl, 'New project or existing codebase? (new/existing)', 'new')
173
- ).toLowerCase();
174
- projectMode = modeAnswer.startsWith('e') ? 'existing' : 'new';
168
+ // Another agent system already here? Ask before touching its loaders.
169
+ if (fs.existsSync(projectDir)) {
170
+ const proceed = await warnOnExistingSetup(projectDir);
171
+ if (!proceed) {
172
+ stdout.write('\nStopped nothing was written to this project.\n');
173
+ return;
174
+ }
175
175
  }
176
176
 
177
+ const classification = classifyProjectDir(projectDir);
178
+ projectMode = await select(
179
+ 'Is this a new project or an existing codebase?',
180
+ [
181
+ { label: 'Existing codebase', value: 'existing', hint: 'scan it and draft the memory' },
182
+ { label: 'New project', value: 'new', hint: 'start with a blank scaffold' },
183
+ ],
184
+ classification === 'existing' ? 0 : 1
185
+ );
186
+
177
187
  if (projectMode === 'existing') {
178
188
  if (!fs.existsSync(projectDir)) {
179
189
  stdout.write(`\n ${projectDir} doesn't exist yet — nothing to scan. Treating as new.\n`);
@@ -192,9 +202,9 @@ async function wizardFlow(rl) {
192
202
  }
193
203
  stdout.write('\n');
194
204
 
195
- const confirmed = await askYesNo(rl, 'Does this look right?', true);
205
+ const confirmed = await confirm('Does this look right?', true);
196
206
  if (!confirmed) {
197
- const manualStack = await ask(rl, 'Describe the stack yourself:', stackLine);
207
+ const manualStack = await input('Describe the stack yourself:', stackLine);
198
208
  stackLine = manualStack;
199
209
  }
200
210
  }
@@ -208,23 +218,25 @@ async function wizardFlow(rl) {
208
218
  }
209
219
  }
210
220
 
211
- // 5. Editors
212
- const editorAnswer = (
213
- await ask(rl, 'Which editor(s) do you use? (antigravity/claude/both/other)', 'both')
214
- ).toLowerCase();
215
-
216
- let editors;
217
- if (editorAnswer.startsWith('both')) {
218
- editors = ['antigravity', 'claude'];
219
- } else if (editorAnswer.startsWith('a')) {
220
- editors = ['antigravity'];
221
- } else if (editorAnswer.startsWith('c')) {
222
- editors = ['claude'];
223
- } else {
224
- // "other" or anything unrecognized — still write both loader formats,
225
- // since AGENTS.md/CLAUDE.md are cheap and cover most tools either way.
226
- editors = ['antigravity', 'claude'];
227
- }
221
+ // 5. Editors — a checkbox, so anyone already running another agent system in
222
+ // one harness can install for the other and leave that one alone.
223
+ const existing = setUpProject ? detectExistingAgentSetup(projectDir) : [];
224
+ const claudeTaken = existing.some((f) => f.file === 'CLAUDE.md' && !f.ours);
225
+
226
+ const editors = await checkbox('Which editor(s) do you use?', [
227
+ {
228
+ label: 'Antigravity',
229
+ value: 'antigravity',
230
+ checked: true,
231
+ hint: 'writes .agents/',
232
+ },
233
+ {
234
+ label: 'Claude Code',
235
+ value: 'claude',
236
+ checked: !claudeTaken,
237
+ hint: claudeTaken ? 'CLAUDE.md already in use here' : 'writes CLAUDE.md + .claude/',
238
+ },
239
+ ]);
228
240
 
229
241
  // 6. Write files
230
242
  const projectName = projectSummary?.name || (setUpProject ? path.basename(projectDir) : 'unnamed-project');
@@ -50,6 +50,7 @@ the signup flow, next step is wiring the email step. Ready."
50
50
  | `/kracked-sdd` | Runs the SDD flow: idea → spec → docs → build → review |
51
51
  | `/kracked-wrap` | Writes lessons + updates session state, closes the session cleanly |
52
52
  | `/kracked-explain` | Walks {{USER_NAME}} through what kracked-core installed and why |
53
+ | `/kracked-identity` | Change who you are, your tone, or how {{USER_NAME}} likes to work |
53
54
 
54
55
  ## Writing memory (wrap)
55
56
 
@@ -5,14 +5,21 @@ description: Use at the start of every session, before any other work, to load g
5
5
 
6
6
  Run this before doing anything else in the session. Do not skip steps or merge reads together.
7
7
 
8
- ## 1. Check that memory exists
8
+ ## 1. Check what memory exists
9
9
 
10
- Look for `~/.kracked/identity.md`. If it is missing, global memory has not been set up. Tell the
11
- user:
10
+ Look for `~/.kracked/identity.md` (global) and `.kracked/` in the current directory (project).
11
+ At least one must exist. Boot works with either layer — never halt just because one is missing.
12
12
 
13
- > Global memory isn't set up yet. Run `npx kracked-core init` to create it, then re-run this.
13
+ - **Both present** normal boot. Continue to step 2.
14
+ - **Project only** (no `~/.kracked/`) — boot from project memory alone. Behave as a careful,
15
+ direct coding agent, and say in your orientation line that global memory isn't set up, so
16
+ cross-project lessons and preferences aren't loaded. Mention `npx kracked-core init` once.
17
+ Do NOT invent preferences or lessons you haven't read.
18
+ - **Global only** (no `.kracked/` here) — see step 3.
19
+ - **Neither** — nothing is installed. Say so and point at `npx kracked-core init`. This is the
20
+ only case where there's nothing to load.
14
21
 
15
- Stop here if memory doesn't exist. Do not fabricate identity, preferences, or project state.
22
+ Never fabricate identity, preferences, project state, or lessons. Missing is missing say it.
16
23
 
17
24
  ## 2. Load global memory, in this exact order
18
25
 
@@ -0,0 +1,70 @@
1
+ ---
2
+ name: kracked-identity
3
+ description: Use when the user wants to change their agent's name, personality, or communication style, or to record how they like to work — anything about who the agent is or what it should remember about them.
4
+ ---
5
+
6
+ Help the user shape who you are and how you work with them. Two files, two purposes — keep them
7
+ straight, because mixing them up is the most common mistake.
8
+
9
+ | File | Holds | Example |
10
+ |---|---|---|
11
+ | `~/.kracked/identity.md` | Who the AGENT is | name, tone, how direct to be |
12
+ | `~/.kracked/preferences.md` | How the USER works | their stack, their conventions, what to never do |
13
+
14
+ If they say "be less formal" → identity. If they say "I use Tailwind, stop suggesting CSS
15
+ modules" → preferences. When unsure, ask which one they mean rather than guessing.
16
+
17
+ ## 1. Work out what they actually want to change
18
+
19
+ Read both files first, so you're editing from what's there rather than overwriting blind.
20
+
21
+ Then figure out which of these they're asking for:
22
+
23
+ - **Rename** — just the agent's name
24
+ - **Personality / tone** — how you talk to them
25
+ - **Preferences** — their stack, style, or a standing rule for you to follow
26
+ - **"I don't know, it just feels off"** — go to step 2
27
+
28
+ ## 2. If they can't name it, ask about behaviour, not settings
29
+
30
+ Don't hand them a config form. Ask what's been annoying:
31
+
32
+ - "Am I too wordy, or not detailed enough?"
33
+ - "Do I explain too much before doing the work?"
34
+ - "Anything I keep suggesting that you never want?"
35
+ - "Anything I keep getting wrong about your setup?"
36
+
37
+ Turn each answer into a concrete line. "Be less annoying" is not actionable; "answer first, then
38
+ explain, and don't restate my question back to me" is.
39
+
40
+ ## 3. Make the edit
41
+
42
+ Edit the file directly — surgical changes, not a rewrite. Keep the existing structure and only
43
+ touch the lines that need to change.
44
+
45
+ - Renaming: update the name in `identity.md`. Mention that `AGENTS.md` in their projects may
46
+ also name the agent, and offer to update those too.
47
+ - Tone/personality: edit the "How I work" or "Communication style" section.
48
+ - Preferences: add to the right section of `preferences.md`. One line per rule, specific enough
49
+ to act on.
50
+
51
+ **Never invent preferences they didn't state.** If you observed something during the session
52
+ ("you corrected me twice about using Postgres, not MySQL"), say so and ask whether to write it
53
+ down — don't just write it.
54
+
55
+ ## 4. Read the change back
56
+
57
+ Show them the lines you changed, exactly as written. Then say plainly:
58
+
59
+ > This takes effect at your next boot. Run `/kracked-boot` in a fresh session to load it.
60
+
61
+ Do not claim the change is already active in the current session — memory is read at boot.
62
+
63
+ ## Two things worth telling them once
64
+
65
+ - **These are plain markdown files they own.** They can open `~/.kracked/identity.md` in any
66
+ editor and change it directly. Doing that is often faster than asking you.
67
+ - **Global vs project.** Identity and preferences are global — they apply everywhere. Anything
68
+ true only for one codebase (its stack, conventions, structure) belongs in that project's
69
+ `.kracked/project.md` instead. Putting project detail in global memory makes you apply it to
70
+ every other project, which is the most common way this system goes wrong.