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.
@@ -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
 
@@ -1,3 +1,4 @@
1
+ <!-- kracked-core:owned -->
1
2
  # AGENTS.md — {{PROJECT_NAME}}
2
3
 
3
4
  This is the canonical loader. Every harness (Antigravity, Claude Code, others) reads this file
@@ -1,4 +1,5 @@
1
1
  @AGENTS.md
2
+ <!-- kracked-core:owned -->
2
3
 
3
4
  This file is a shim. The canonical instructions live in `AGENTS.md` — duplicating them here would
4
5
  let the two drift apart, which is the exact bug kracked-core exists to prevent. If you need to
@@ -1,3 +1,4 @@
1
+ <!-- kracked-core:owned -->
1
2
  # Antigravity — {{PROJECT_NAME}}
2
3
 
3
4
  This file is a pointer, not the source of truth. The full instructions — who the agent is, the
@@ -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