scrumrun 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.
Files changed (67) hide show
  1. package/CORE.md +381 -0
  2. package/DECISIONS.md +212 -0
  3. package/README.md +123 -0
  4. package/SPEC.md +324 -0
  5. package/bin/claude-install.js +147 -0
  6. package/bin/scrumrun.js +1194 -0
  7. package/package.json +48 -0
  8. package/templates/codex/prompts/sc-agent.md +14 -0
  9. package/templates/codex/prompts/sc-backlog.md +13 -0
  10. package/templates/codex/prompts/sc-challenge.md +57 -0
  11. package/templates/codex/prompts/sc-config.md +18 -0
  12. package/templates/codex/prompts/sc-context.md +24 -0
  13. package/templates/codex/prompts/sc-decisions.md +8 -0
  14. package/templates/codex/prompts/sc-feature.md +16 -0
  15. package/templates/codex/prompts/sc-fix.md +21 -0
  16. package/templates/codex/prompts/sc-goal.md +14 -0
  17. package/templates/codex/prompts/sc-golden.md +14 -0
  18. package/templates/codex/prompts/sc-help.md +12 -0
  19. package/templates/codex/prompts/sc-init.md +14 -0
  20. package/templates/codex/prompts/sc-intake.md +22 -0
  21. package/templates/codex/prompts/sc-know.md +75 -0
  22. package/templates/codex/prompts/sc-map.md +13 -0
  23. package/templates/codex/prompts/sc-review.md +13 -0
  24. package/templates/codex/prompts/sc-sprint.md +28 -0
  25. package/templates/codex/prompts/sc-study.md +23 -0
  26. package/templates/codex/prompts/sc-uninstall.md +14 -0
  27. package/templates/codex/prompts/sc-update.md +6 -0
  28. package/templates/codex/prompts/sc-vault.md +27 -0
  29. package/templates/codex/skills/scrumrun/SKILL.md +412 -0
  30. package/templates/opencode/commands/sc-agent.md +14 -0
  31. package/templates/opencode/commands/sc-backlog.md +13 -0
  32. package/templates/opencode/commands/sc-challenge.md +57 -0
  33. package/templates/opencode/commands/sc-config.md +18 -0
  34. package/templates/opencode/commands/sc-context.md +24 -0
  35. package/templates/opencode/commands/sc-decisions.md +8 -0
  36. package/templates/opencode/commands/sc-feature.md +16 -0
  37. package/templates/opencode/commands/sc-fix.md +21 -0
  38. package/templates/opencode/commands/sc-goal.md +14 -0
  39. package/templates/opencode/commands/sc-golden.md +14 -0
  40. package/templates/opencode/commands/sc-help.md +12 -0
  41. package/templates/opencode/commands/sc-init.md +14 -0
  42. package/templates/opencode/commands/sc-intake.md +22 -0
  43. package/templates/opencode/commands/sc-know.md +75 -0
  44. package/templates/opencode/commands/sc-map.md +13 -0
  45. package/templates/opencode/commands/sc-review.md +13 -0
  46. package/templates/opencode/commands/sc-sprint.md +28 -0
  47. package/templates/opencode/commands/sc-study.md +23 -0
  48. package/templates/opencode/commands/sc-uninstall.md +14 -0
  49. package/templates/opencode/commands/sc-update.md +6 -0
  50. package/templates/opencode/commands/sc-vault.md +27 -0
  51. package/templates/opencode/skills/scrumrun/SKILL.md +412 -0
  52. package/templates/project/.scrumrun/agents.md +36 -0
  53. package/templates/project/.scrumrun/backlog.md +7 -0
  54. package/templates/project/.scrumrun/config.md +13 -0
  55. package/templates/project/.scrumrun/context.md +61 -0
  56. package/templates/project/.scrumrun/features/.gitkeep +1 -0
  57. package/templates/project/.scrumrun/goals/main/decisions.md +9 -0
  58. package/templates/project/.scrumrun/goals/main/history.md +51 -0
  59. package/templates/project/.scrumrun/goals/main/sprint.md +54 -0
  60. package/templates/project/.scrumrun/golden-rules.md +9 -0
  61. package/templates/project/.scrumrun/knowledge.md +15 -0
  62. package/templates/project/.scrumrun/map.md +20 -0
  63. package/templates/project/.scrumrun/project.md +26 -0
  64. package/templates/project/.scrumrun/reviews/.gitkeep +1 -0
  65. package/templates/project/.scrumrun/runbook.md +101 -0
  66. package/templates/project/.scrumrun/token-policy.md +43 -0
  67. package/templates/project/AGENTS.md +59 -0
@@ -0,0 +1,1194 @@
1
+ #!/usr/bin/env node
2
+
3
+ const fs = require("fs");
4
+ const path = require("path");
5
+ const os = require("os");
6
+
7
+ const root = path.resolve(__dirname, "..");
8
+ const templates = path.join(root, "templates");
9
+
10
+ const COMMANDS = [
11
+ "sc-help",
12
+ "sc-intake",
13
+ "sc-study",
14
+ "sc-init",
15
+ "sc-uninstall",
16
+ "sc-update",
17
+ "sc-vault",
18
+ "sc-know",
19
+ "sc-context",
20
+ "sc-config",
21
+ "sc-golden",
22
+ "sc-map",
23
+ "sc-challenge",
24
+ "sc-goal",
25
+ "sc-backlog",
26
+ "sc-feature",
27
+ "sc-sprint",
28
+ "sc-fix",
29
+ "sc-agent",
30
+ "sc-review",
31
+ "sc-decisions"
32
+ ];
33
+
34
+ function usage() {
35
+ console.log(`ScrumRun
36
+
37
+ Usage:
38
+ scrumrun install [all|codex|opencode|claude] [--force]
39
+ scrumrun update [all|codex|opencode|claude]
40
+ scrumrun init [--local|--shared] [--no-agent-hint] [--force]
41
+ scrumrun status
42
+ scrumrun core [--path|--prompt]
43
+ scrumrun context build
44
+ scrumrun context update [reason]
45
+ scrumrun context show
46
+ scrumrun context clear
47
+ scrumrun context policy
48
+ scrumrun commands
49
+ scrumrun vault add <name> <value>
50
+ scrumrun vault add <name:value>
51
+ scrumrun vault list
52
+ scrumrun vault show <V-NNN|name>
53
+ scrumrun vault remove <V-NNN|name>
54
+ scrumrun backlog add <sprint>
55
+ scrumrun backlog list
56
+ scrumrun know add <topic>
57
+ scrumrun know list
58
+ scrumrun know approve <K-NNN>
59
+ scrumrun know reject <K-NNN> [reason]
60
+ scrumrun know insight <K-NNN> <text>
61
+ scrumrun know remove <K-NNN>
62
+ scrumrun know resume [K-NNN]
63
+ scrumrun prompt <intake|study|challenge|know|context-build|context-update|vault|goal-set|sprint-add|sprint-run|backlog-add> [text]
64
+ scrumrun uninstall [--force]
65
+ scrumrun migrate
66
+ scrumrun doctor [all|codex|opencode|claude]
67
+
68
+ Examples:
69
+ npx scrumrun@latest install
70
+ npx scrumrun@latest install claude
71
+ npx scrumrun@latest init
72
+ npx scrumrun@latest core
73
+ npx scrumrun@latest status
74
+ npx scrumrun@latest context show
75
+ npx scrumrun@latest vault add OPENAI_API_KEY sk-local-dev
76
+ npx scrumrun@latest backlog add "Sprint 01"
77
+ npx scrumrun@latest know add "Tenant permissions for report exports"
78
+ npx scrumrun@latest prompt intake "Checkout payments duplicate after refresh"
79
+ npx scrumrun@latest prompt challenge "Exports need tenant permission checks"
80
+ npx scrumrun@latest uninstall --force
81
+ npx scrumrun@latest init --shared
82
+ npx scrumrun@latest migrate
83
+ npx scrumrun@latest doctor
84
+ `);
85
+ }
86
+
87
+ function ensureDir(dir) {
88
+ fs.mkdirSync(dir, { recursive: true });
89
+ }
90
+
91
+ function today() {
92
+ return new Date().toISOString().slice(0, 10);
93
+ }
94
+
95
+ function joinArgs(parts) {
96
+ return parts.filter((part) => part && !part.startsWith("--")).join(" ").trim();
97
+ }
98
+
99
+ function joinText(parts) {
100
+ return parts.filter(Boolean).join(" ").trim();
101
+ }
102
+
103
+ function stripQuotes(value) {
104
+ return value.replace(/^["']|["']$/g, "");
105
+ }
106
+
107
+ function readIfExists(file) {
108
+ return fs.existsSync(file) ? fs.readFileSync(file, "utf8") : "";
109
+ }
110
+
111
+ function writeFile(file, content) {
112
+ ensureDir(path.dirname(file));
113
+ fs.writeFileSync(file, content.endsWith("\n") ? content : `${content}\n`);
114
+ }
115
+
116
+ function projectFile(...parts) {
117
+ return path.join(process.cwd(), ".scrumrun", ...parts);
118
+ }
119
+
120
+ function ensureInteractionPreferences(cwd) {
121
+ const configFile = path.join(cwd, ".scrumrun", "config.md");
122
+ if (!fs.existsSync(configFile)) return null;
123
+
124
+ let content = fs.readFileSync(configFile, "utf8").trimEnd();
125
+ const defaults = [
126
+ ["Interaction Mode:", "Interaction Mode: guided"],
127
+ ["Execution Approval:", "Execution Approval: always"],
128
+ ["Quick Tasks:", "Quick Tasks: ask"]
129
+ ];
130
+ const added = defaults.filter(([prefix]) => !content.split("\n").some((line) => line.trim().startsWith(prefix)));
131
+ if (!added.length) return null;
132
+
133
+ const section = content.includes("\n## Intake") ? "\n" : "\n\n## Intake\n\n";
134
+ content += `${section}${added.map(([, line]) => line).join("\n")}\n`;
135
+ fs.writeFileSync(configFile, content);
136
+ return { status: "updated", dest: configFile };
137
+ }
138
+
139
+ function copyFile(src, dest, { force = true, vars = {} } = {}) {
140
+ if (fs.existsSync(dest) && !force) {
141
+ return { status: "skipped", dest };
142
+ }
143
+ ensureDir(path.dirname(dest));
144
+ let content = fs.readFileSync(src, "utf8");
145
+ for (const [key, value] of Object.entries(vars)) {
146
+ content = content.split(`{{${key}}}`).join(value);
147
+ }
148
+ fs.writeFileSync(dest, content);
149
+ return { status: "written", dest };
150
+ }
151
+
152
+ function copyDir(srcDir, destDir, options = {}) {
153
+ const results = [];
154
+ for (const entry of fs.readdirSync(srcDir, { withFileTypes: true })) {
155
+ const src = path.join(srcDir, entry.name);
156
+ const dest = path.join(destDir, entry.name);
157
+ if (entry.isDirectory()) {
158
+ results.push(...copyDir(src, dest, options));
159
+ } else {
160
+ results.push(copyFile(src, dest, options));
161
+ }
162
+ }
163
+ return results;
164
+ }
165
+
166
+ function printResults(title, results) {
167
+ console.log(`\n${title}`);
168
+ for (const result of results) {
169
+ const prefix = result.status === "skipped" ? "skip" : result.status;
170
+ console.log(` ${prefix} ${result.dest}`);
171
+ }
172
+ }
173
+
174
+ function cleanupLegacy(commandsDir, skillsDir) {
175
+ if (fs.existsSync(commandsDir)) {
176
+ for (const entry of fs.readdirSync(commandsDir, { withFileTypes: true })) {
177
+ if (!entry.isFile()) continue;
178
+ const name = entry.name;
179
+ const legacyPrefix = name.startsWith("srun-") || name.startsWith("asm-") || name.startsWith("sr-") || name.startsWith("scr-") || name.startsWith("src-") || name.startsWith("run-");
180
+ const oldConsolidated = /^scr-[a-z]+-[a-z]+\.md$/.test(name);
181
+ if (legacyPrefix || oldConsolidated) {
182
+ fs.rmSync(path.join(commandsDir, name), { force: true });
183
+ console.log(` rm legacy ${path.join(commandsDir, name)}`);
184
+ }
185
+ }
186
+ }
187
+ const oldSkill = path.join(skillsDir, "ai-scrum");
188
+ if (fs.existsSync(oldSkill)) {
189
+ fs.rmSync(oldSkill, { recursive: true, force: true });
190
+ console.log(` rm legacy ${oldSkill}`);
191
+ }
192
+ }
193
+
194
+ function installClaude(force) {
195
+ const home = os.homedir();
196
+ const commandsDir = path.join(home, ".claude", "commands");
197
+ cleanupLegacy(commandsDir, path.join(home, ".claude", "skills"));
198
+ const results = [
199
+ ...copyDir(path.join(templates, "codex", "prompts"), commandsDir, { force }),
200
+ ...copyDir(path.join(templates, "codex", "skills"), path.join(home, ".claude", "skills"), { force })
201
+ ];
202
+ printResults("Installed claude", results);
203
+ }
204
+
205
+ function installCodex(force) {
206
+ const home = os.homedir();
207
+ const promptsDir = path.join(home, ".codex", "prompts");
208
+ cleanupLegacy(promptsDir, path.join(home, ".codex", "skills"));
209
+ const results = [
210
+ ...copyDir(path.join(templates, "codex", "prompts"), promptsDir, { force }),
211
+ ...copyDir(path.join(templates, "codex", "skills"), path.join(home, ".codex", "skills"), { force })
212
+ ];
213
+ printResults("Installed codex", results);
214
+ }
215
+
216
+ function installOpenCode(force) {
217
+ const home = os.homedir();
218
+ const commandsDir = path.join(home, ".config", "opencode", "commands");
219
+ cleanupLegacy(commandsDir, path.join(home, ".config", "opencode", "skills"));
220
+ const results = [
221
+ ...copyDir(path.join(templates, "opencode", "commands"), commandsDir, { force }),
222
+ ...copyDir(path.join(templates, "opencode", "skills"), path.join(home, ".config", "opencode", "skills"), { force })
223
+ ];
224
+ printResults("Installed opencode", results);
225
+ }
226
+
227
+ function install(target, force) {
228
+ if (target === "all") {
229
+ installCodex(force);
230
+ installClaude(force);
231
+ installOpenCode(force);
232
+ } else if (target === "codex") {
233
+ installCodex(force);
234
+ } else if (target === "claude") {
235
+ installClaude(force);
236
+ } else if (target === "opencode") {
237
+ installOpenCode(force);
238
+ }
239
+ }
240
+
241
+ function hasGitRepo(cwd) {
242
+ return fs.existsSync(path.join(cwd, ".git"));
243
+ }
244
+
245
+ function addLocalExclude(cwd, pattern) {
246
+ const excludeFile = path.join(cwd, ".git", "info", "exclude");
247
+ if (!fs.existsSync(path.join(cwd, ".git"))) {
248
+ return { status: "skipped", dest: `${pattern} (not a git repo)` };
249
+ }
250
+ ensureDir(path.dirname(excludeFile));
251
+ const existing = fs.existsSync(excludeFile) ? fs.readFileSync(excludeFile, "utf8") : "";
252
+ const lines = existing.split(/\r?\n/).map((line) => line.trim());
253
+ if (lines.includes(pattern)) {
254
+ return { status: "skipped", dest: excludeFile };
255
+ }
256
+ const prefix = existing && !existing.endsWith("\n") ? "\n" : "";
257
+ fs.appendFileSync(excludeFile, `${prefix}${pattern}\n`);
258
+ return { status: "written", dest: `${excludeFile} (${pattern})` };
259
+ }
260
+
261
+ function removeLocalExclude(cwd, pattern) {
262
+ const excludeFile = path.join(cwd, ".git", "info", "exclude");
263
+ if (!fs.existsSync(excludeFile)) {
264
+ return { status: "skipped", dest: `${pattern} (no .git/info/exclude)` };
265
+ }
266
+ const existing = fs.readFileSync(excludeFile, "utf8");
267
+ const next = existing
268
+ .split(/\r?\n/)
269
+ .filter((line) => line.trim() !== pattern)
270
+ .join("\n")
271
+ .replace(/\n{3,}/g, "\n\n");
272
+ if (next === existing) {
273
+ return { status: "skipped", dest: `${excludeFile} (${pattern})` };
274
+ }
275
+ fs.writeFileSync(excludeFile, next.endsWith("\n") || next === "" ? next : `${next}\n`);
276
+ return { status: "removed", dest: `${excludeFile} (${pattern})` };
277
+ }
278
+
279
+ function looksLikeScrumRunAgents(file) {
280
+ if (!fs.existsSync(file)) return false;
281
+ const content = fs.readFileSync(file, "utf8");
282
+ return content.includes("This project uses ScrumRun.")
283
+ || content.includes("## ScrumRun")
284
+ || content.includes(".scrumrun/golden-rules.md");
285
+ }
286
+
287
+ function removePathIfExists(target, { force = false, onlyIfScrumRunAgents = false } = {}) {
288
+ if (!fs.existsSync(target)) return { status: "skipped", dest: target };
289
+ if (!force) return { status: "skipped", dest: `${target} (use --force to remove)` };
290
+ if (onlyIfScrumRunAgents && !looksLikeScrumRunAgents(target)) {
291
+ return { status: "skipped", dest: `${target} (not recognized as ScrumRun-generated)` };
292
+ }
293
+ fs.rmSync(target, { recursive: true, force: true });
294
+ return { status: "removed", dest: target };
295
+ }
296
+
297
+ function ensureBacklogFile(cwd) {
298
+ const backlogFile = path.join(cwd, ".scrumrun", "backlog.md");
299
+ if (!fs.existsSync(backlogFile)) {
300
+ ensureDir(path.dirname(backlogFile));
301
+ fs.writeFileSync(
302
+ backlogFile,
303
+ `# Sprint Backlog - ${path.basename(cwd)}\n\nBacklog items are sprint candidates. They are not active work until the user explicitly runs \`/sc-sprint --run\`.\n\n## Items\n\n- Pending: none.\n`
304
+ );
305
+ }
306
+ return backlogFile;
307
+ }
308
+
309
+ function ensureKnowledgeFile(cwd) {
310
+ const knowledgeFile = path.join(cwd, ".scrumrun", "knowledge.md");
311
+ if (!fs.existsSync(knowledgeFile)) {
312
+ writeFile(
313
+ knowledgeFile,
314
+ `# Knowledge Base - ${path.basename(cwd)}\n\nOnly approved knowledge should be used as planning context for /sc-challenge, /sc-sprint --add, /sc-sprint --run, and feature planning.\n\n## Approved Knowledge\n\n- Pending: none.\n\n## Pending Proposals\n\n- Pending: none.\n\n## Rejected Proposals\n\n- Pending: none.\n`
315
+ );
316
+ }
317
+ return knowledgeFile;
318
+ }
319
+
320
+ function ensureVaultFile(cwd) {
321
+ const vaultFile = path.join(cwd, ".scrumrun", "vault.local.md");
322
+ if (!fs.existsSync(vaultFile)) {
323
+ writeFile(
324
+ vaultFile,
325
+ `# Local Development Vault - ${path.basename(cwd)}\n\nThis file is for local development secrets only. It is plaintext, local-only, and must never be committed.\n\n## Entries\n\n- Pending: none.\n`
326
+ );
327
+ }
328
+ return vaultFile;
329
+ }
330
+
331
+ function contextTemplate(cwd, { stale = false, reason = "CLI" } = {}) {
332
+ const name = path.basename(cwd);
333
+ const status = stale ? "stale - rebuild required" : "cli placeholder - agent verification required";
334
+ return `# ScrumRun Context Snapshot - ${name}
335
+
336
+ This file is a token-safe snapshot. It helps agents decide what to read next, but it is not a source of truth.
337
+
338
+ Canonical sources remain \`golden-rules.md\`, \`config.md\`, \`project.md\`, \`knowledge.md\`, \`runbook.md\`, sprint/feature plans, history, decisions, and source code.
339
+
340
+ Last updated: ${today()}
341
+ Updated by: ${reason}
342
+ Confidence: ${status}
343
+
344
+ ## Current Focus
345
+
346
+ - Main goal: pending.
347
+ - Active sprint: pending.
348
+ - Active feature lane: none.
349
+ - Current challenge: none.
350
+
351
+ ## Must Read For Current Work
352
+
353
+ - \`AGENTS.md\`
354
+ - \`.scrumrun/core.md\`
355
+ - \`.scrumrun/golden-rules.md\`
356
+ - \`.scrumrun/config.md\`
357
+ - \`.scrumrun/token-policy.md\`
358
+ - \`.scrumrun/map.md\`
359
+ - \`.scrumrun/project.md\`
360
+ - \`.scrumrun/knowledge.md\`
361
+ - \`.scrumrun/runbook.md\`
362
+
363
+ ## Read Only If Needed
364
+
365
+ - Full historical reviews under \`.scrumrun/reviews/\`
366
+ - Full old sprint history unrelated to the current lane
367
+ - Large generated files, build outputs, dependency folders, and logs
368
+
369
+ ## Current Decisions
370
+
371
+ - Pending: none.
372
+
373
+ ## Current Risks
374
+
375
+ - Do not treat this snapshot as canonical truth.
376
+ - Do not use pending knowledge as approved planning truth.
377
+ - Never print or copy \`.scrumrun/vault.local.md\` values.
378
+
379
+ ## Current Map Pointers
380
+
381
+ - Pending: run \`/sc-map --build\` or \`/sc-context --build\`.
382
+
383
+ ## Staleness Triggers
384
+
385
+ Refresh this snapshot after:
386
+
387
+ - \`/sc-study\`
388
+ - \`/sc-goal --set\`
389
+ - \`/sc-feature --add\`
390
+ - \`/sc-sprint --add\`
391
+ - \`/sc-sprint --run\`
392
+ - \`/sc-sprint --fix\`
393
+ - \`/sc-review --code\`
394
+ - major changes to \`knowledge.md\`, \`project.md\`, \`map.md\`, \`history.md\`, or \`decisions.md\`
395
+ `;
396
+ }
397
+
398
+ function ensureContextFile(cwd) {
399
+ const contextFile = path.join(cwd, ".scrumrun", "context.md");
400
+ if (!fs.existsSync(contextFile)) {
401
+ writeFile(contextFile, contextTemplate(cwd));
402
+ }
403
+ return contextFile;
404
+ }
405
+
406
+ function ensureTokenPolicyFile(cwd) {
407
+ const policyFile = path.join(cwd, ".scrumrun", "token-policy.md");
408
+ if (!fs.existsSync(policyFile)) {
409
+ writeFile(
410
+ policyFile,
411
+ `# ScrumRun Token Policy - ${path.basename(cwd)}
412
+
413
+ This policy reduces token waste without weakening ScrumRun safety.
414
+
415
+ ## Rules
416
+
417
+ 1. Safety beats token economy. Never skip core, golden rules, config, or relevant history to save tokens.
418
+ 2. Context is a snapshot, not truth. Use it to choose what to read, then verify against canonical files.
419
+ 3. Prefer targeted reads over broad dumps.
420
+ 4. Do not read or print vault values unless explicitly required for a local development credential workflow.
421
+ 5. If uncertainty affects correctness or safety, read the source even if it costs more tokens.
422
+ `
423
+ );
424
+ }
425
+ return policyFile;
426
+ }
427
+
428
+ function removePendingNone(content) {
429
+ return content.replace(/^- Pending: none\.\n?/gm, "").replace(/\n{3,}/g, "\n\n");
430
+ }
431
+
432
+ function nextVaultId(content) {
433
+ const ids = [...content.matchAll(/\bV-(\d{3})\b/g)].map((match) => Number(match[1]));
434
+ const next = ids.length ? Math.max(...ids) + 1 : 1;
435
+ return `V-${String(next).padStart(3, "0")}`;
436
+ }
437
+
438
+ function findVaultBlock(content, selector) {
439
+ const escaped = (selector || "").replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
440
+ const byId = /^V-\d{3}$/.test(selector || "")
441
+ ? new RegExp(`^### ${escaped}\\b[\\s\\S]*?(?=^### V-\\d{3}\\b|^## |(?![\\s\\S]))`, "m")
442
+ : null;
443
+ const byName = selector
444
+ ? new RegExp(`^### V-\\d{3} - ${escaped}$[\\s\\S]*?(?=^### V-\\d{3}\\b|^## |(?![\\s\\S]))`, "m")
445
+ : null;
446
+ const match = (byId && content.match(byId)) || (byName && content.match(byName));
447
+ return match ? match[0].trim() : "";
448
+ }
449
+
450
+ function removeVaultBlock(content, selector) {
451
+ const block = findVaultBlock(content, selector);
452
+ return block ? content.replace(block, "").replace(/\n{3,}/g, "\n\n") : content;
453
+ }
454
+
455
+ function parseVaultInput(parts) {
456
+ const values = parts.filter((part) => part && !part.startsWith("--"));
457
+ if (values.length === 1 && values[0].includes(":")) {
458
+ const [name, ...rest] = values[0].split(":");
459
+ return { name: name.trim(), value: rest.join(":").trim() };
460
+ }
461
+ if (values.length === 1 && values[0].includes("=")) {
462
+ const [name, ...rest] = values[0].split("=");
463
+ return { name: name.trim(), value: rest.join("=").trim() };
464
+ }
465
+ return { name: values[0] || "", value: values.slice(1).join(" ").trim() };
466
+ }
467
+
468
+ function addVaultEntry(parts) {
469
+ const { name, value } = parseVaultInput(parts);
470
+ if (!name || !value) {
471
+ console.error("Usage: scrumrun vault add <name> <value> OR scrumrun vault add <name:value>");
472
+ process.exitCode = 1;
473
+ return;
474
+ }
475
+ const file = ensureVaultFile(process.cwd());
476
+ const current = readIfExists(file);
477
+ const id = nextVaultId(current);
478
+ let next = removePendingNone(current);
479
+ if (!next.endsWith("\n")) next += "\n";
480
+ next += `### ${id} - ${name}\nCreated: ${today()}\nSource: CLI\nValue: ${value}\n\n`;
481
+ writeFile(file, next);
482
+ console.log(`Stored local vault entry: ${id} - ${name}`);
483
+ console.log("Value hidden. Use `scrumrun vault show <id|name>` to reveal it locally.");
484
+ }
485
+
486
+ function listVaultEntries() {
487
+ const file = ensureVaultFile(process.cwd());
488
+ const content = readIfExists(file);
489
+ const entries = [...content.matchAll(/^### (V-\d{3}) - (.+)$/gm)];
490
+ if (!entries.length) {
491
+ console.log("No local vault entries.");
492
+ return;
493
+ }
494
+ for (const [, id, name] of entries) {
495
+ console.log(`- ${id} | ${name} | value: ******`);
496
+ }
497
+ }
498
+
499
+ function showVaultEntry(selector) {
500
+ if (!selector) {
501
+ console.error("Usage: scrumrun vault show <V-NNN|name>");
502
+ process.exitCode = 1;
503
+ return;
504
+ }
505
+ const file = ensureVaultFile(process.cwd());
506
+ const block = findVaultBlock(readIfExists(file), selector);
507
+ if (!block) {
508
+ console.error(`Vault entry not found: ${selector}`);
509
+ process.exitCode = 1;
510
+ return;
511
+ }
512
+ console.log(block);
513
+ }
514
+
515
+ function removeVaultEntry(selector) {
516
+ if (!selector) {
517
+ console.error("Usage: scrumrun vault remove <V-NNN|name>");
518
+ process.exitCode = 1;
519
+ return;
520
+ }
521
+ const file = ensureVaultFile(process.cwd());
522
+ const current = readIfExists(file);
523
+ if (!findVaultBlock(current, selector)) {
524
+ console.error(`Vault entry not found: ${selector}`);
525
+ process.exitCode = 1;
526
+ return;
527
+ }
528
+ writeFile(file, removeVaultBlock(current, selector));
529
+ console.log(`Removed local vault entry: ${selector}`);
530
+ }
531
+
532
+ function runVault(parts) {
533
+ const action = parts[0];
534
+ if (!action || action === "list" || action === "--list" || action === "-l") {
535
+ listVaultEntries();
536
+ } else if (action === "add" || action === "--add" || action === "-a") {
537
+ addVaultEntry(parts.slice(1));
538
+ } else if (action === "show" || action === "--show" || action === "-s") {
539
+ showVaultEntry(parts[1]);
540
+ } else if (action === "remove" || action === "delete" || action === "--remove" || action === "-r") {
541
+ removeVaultEntry(parts[1]);
542
+ } else if (action === "path") {
543
+ console.log(ensureVaultFile(process.cwd()));
544
+ } else if (parts.includes("--add") || parts.includes("-a")) {
545
+ addVaultEntry(parts);
546
+ } else {
547
+ showVaultEntry(action);
548
+ }
549
+ }
550
+
551
+ function addBacklogItem(label) {
552
+ if (!label) {
553
+ console.error("Usage: scrumrun backlog add <sprint number or name>");
554
+ process.exitCode = 1;
555
+ return;
556
+ }
557
+ const file = ensureBacklogFile(process.cwd());
558
+ const current = readIfExists(file);
559
+ const escaped = label.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
560
+ if (new RegExp(`^- \\[[ xX]\\] .*${escaped}(?: |$)`, "m").test(current)) {
561
+ console.log(`Already in backlog: ${label}`);
562
+ return;
563
+ }
564
+ let next = removePendingNone(current);
565
+ if (!next.endsWith("\n")) next += "\n";
566
+ next += `- [ ] ${label} - added ${today()}. Source: CLI.\n`;
567
+ writeFile(file, next);
568
+ console.log(`Added to backlog: ${label}`);
569
+ }
570
+
571
+ function listBacklog(filter = "") {
572
+ const file = ensureBacklogFile(process.cwd());
573
+ const content = readIfExists(file);
574
+ if (!filter) {
575
+ console.log(content);
576
+ return;
577
+ }
578
+ const lines = content.split(/\r?\n/).filter((line) => line.toLowerCase().includes(filter.toLowerCase()));
579
+ console.log(lines.length ? lines.join("\n") : `No backlog items matched: ${filter}`);
580
+ }
581
+
582
+ function sectionRegex(section) {
583
+ return new RegExp(`(^## ${section}\\n)([\\s\\S]*?)(?=^## |(?![\\s\\S]))`, "m");
584
+ }
585
+
586
+ function replaceSection(content, section, body) {
587
+ const regex = sectionRegex(section);
588
+ const cleanBody = body.trim() ? `${body.trim()}\n\n` : "- Pending: none.\n\n";
589
+ if (regex.test(content)) {
590
+ return content.replace(regex, `$1\n${cleanBody}`);
591
+ }
592
+ return `${content.trim()}\n\n## ${section}\n\n${cleanBody}`;
593
+ }
594
+
595
+ function getSection(content, section) {
596
+ const match = content.match(sectionRegex(section));
597
+ return match ? match[2].trim() : "";
598
+ }
599
+
600
+ function nextKnowledgeId(content) {
601
+ const ids = [...content.matchAll(/\bK-(\d{3})\b/g)].map((match) => Number(match[1]));
602
+ const next = ids.length ? Math.max(...ids) + 1 : 1;
603
+ return `K-${String(next).padStart(3, "0")}`;
604
+ }
605
+
606
+ function findKnowledgeBlock(content, id) {
607
+ const regex = new RegExp(`^### ${id}\\b[\\s\\S]*?(?=^### K-\\d{3}\\b|^## |(?![\\s\\S]))`, "m");
608
+ const match = content.match(regex);
609
+ return match ? match[0].trim() : "";
610
+ }
611
+
612
+ function removeKnowledgeBlock(content, id) {
613
+ const regex = new RegExp(`\\n?^### ${id}\\b[\\s\\S]*?(?=^### K-\\d{3}\\b|^## |(?![\\s\\S]))`, "m");
614
+ return content.replace(regex, "\n").replace(/\n{3,}/g, "\n\n");
615
+ }
616
+
617
+ function updateKnowledgeStatus(block, status, extra = "") {
618
+ const lines = block.split(/\r?\n/);
619
+ const next = lines.map((line) => line.startsWith("Status: ") ? `Status: ${status}` : line);
620
+ if (!lines.some((line) => line.startsWith("Status: "))) {
621
+ next.splice(1, 0, `Status: ${status}`);
622
+ }
623
+ if (extra) {
624
+ next.push("", extra);
625
+ }
626
+ return next.join("\n").trim();
627
+ }
628
+
629
+ function addKnowledge(topic) {
630
+ if (!topic) {
631
+ console.error("Usage: scrumrun know add <topic>");
632
+ process.exitCode = 1;
633
+ return;
634
+ }
635
+ const file = ensureKnowledgeFile(process.cwd());
636
+ const current = readIfExists(file);
637
+ const id = nextKnowledgeId(current);
638
+ const block = `### ${id} - ${stripQuotes(topic)}\nStatus: pending\nDate: ${today()}\nSource: CLI\nApprover: pending\n\n#### User Insight\n\n${stripQuotes(topic)}\n\n#### Verified Facts\n\n- Pending: requires agent verification with /sc-know.\n\n#### Assumptions and Uncertainty\n\n- This entry was created from CLI input and has not been verified by an agent.\n\n#### Risks If Wrong\n\n- Future sprint planning could be based on an incorrect assumption if approved without review.\n\n#### Affected Modules\n\n- Pending: unknown.\n\n#### Suggested Future Use\n\n- Use as context only after approval.\n`;
639
+ const pending = getSection(current, "Pending Proposals");
640
+ const nextPending = `${pending.replace(/^- Pending: none\.\n?/gm, "").trim()}\n\n${block}`.trim();
641
+ writeFile(file, replaceSection(current, "Pending Proposals", nextPending));
642
+ console.log(`Created pending knowledge: ${id}`);
643
+ console.log(`Approve later with: scrumrun know approve ${id}`);
644
+ }
645
+
646
+ function moveKnowledge(id, targetSection, status, reason = "") {
647
+ if (!/^K-\d{3}$/.test(id || "")) {
648
+ console.error(`Invalid knowledge id: ${id || "(missing)"}`);
649
+ process.exitCode = 1;
650
+ return;
651
+ }
652
+ const file = ensureKnowledgeFile(process.cwd());
653
+ const current = readIfExists(file);
654
+ const block = findKnowledgeBlock(current, id);
655
+ if (!block) {
656
+ console.error(`Knowledge entry not found: ${id}`);
657
+ process.exitCode = 1;
658
+ return;
659
+ }
660
+ let next = removeKnowledgeBlock(current, id);
661
+ const extra = status === "approved"
662
+ ? `Approved: ${today()}`
663
+ : reason ? `Rejected: ${today()}\nReason: ${reason}` : `Rejected: ${today()}`;
664
+ const moved = updateKnowledgeStatus(block, status, extra);
665
+ const target = getSection(next, targetSection).replace(/^- Pending: none\.\n?/gm, "").trim();
666
+ next = replaceSection(next, targetSection, `${target}\n\n${moved}`.trim());
667
+ writeFile(file, next);
668
+ console.log(`${status === "approved" ? "Approved" : "Rejected"} knowledge: ${id}`);
669
+ }
670
+
671
+ function removeKnowledge(id) {
672
+ const file = ensureKnowledgeFile(process.cwd());
673
+ const current = readIfExists(file);
674
+ if (!findKnowledgeBlock(current, id)) {
675
+ console.error(`Knowledge entry not found: ${id}`);
676
+ process.exitCode = 1;
677
+ return;
678
+ }
679
+ writeFile(file, removeKnowledgeBlock(current, id));
680
+ console.log(`Removed knowledge: ${id}`);
681
+ }
682
+
683
+ function addKnowledgeInsight(id, text) {
684
+ if (!text) {
685
+ console.error("Usage: scrumrun know insight <K-NNN> <text>");
686
+ process.exitCode = 1;
687
+ return;
688
+ }
689
+ const file = ensureKnowledgeFile(process.cwd());
690
+ const current = readIfExists(file);
691
+ const block = findKnowledgeBlock(current, id);
692
+ if (!block) {
693
+ console.error(`Knowledge entry not found: ${id}`);
694
+ process.exitCode = 1;
695
+ return;
696
+ }
697
+ const insight = `- ${today()}: ${text}`;
698
+ const updated = block.includes("#### Insights")
699
+ ? block.replace(/(#### Insights\n\n)/, `$1${insight}\n`)
700
+ : `${block}\n\n#### Insights\n\n${insight}`;
701
+ writeFile(file, current.replace(block, updated));
702
+ console.log(`Added insight to ${id}`);
703
+ }
704
+
705
+ function listKnowledge(id = "") {
706
+ const file = ensureKnowledgeFile(process.cwd());
707
+ const content = readIfExists(file);
708
+ if (id) {
709
+ const block = findKnowledgeBlock(content, id);
710
+ console.log(block || `Knowledge entry not found: ${id}`);
711
+ return;
712
+ }
713
+ console.log(content);
714
+ }
715
+
716
+ function resumeKnowledge(id = "") {
717
+ const file = ensureKnowledgeFile(process.cwd());
718
+ const content = readIfExists(file);
719
+ const source = id ? findKnowledgeBlock(content, id) : content;
720
+ if (!source) {
721
+ console.log(`Knowledge entry not found: ${id}`);
722
+ return;
723
+ }
724
+ const entries = [...source.matchAll(/^### (K-\d{3}) - (.+)$/gm)];
725
+ if (!entries.length) {
726
+ console.log(source);
727
+ return;
728
+ }
729
+ for (const [, entryId, title] of entries) {
730
+ const block = findKnowledgeBlock(content, entryId);
731
+ const status = (block.match(/^Status: (.+)$/m) || [])[1] || "unknown";
732
+ const fact = (block.match(/#### Verified Facts\n\n([\s\S]*?)(?=\n#### |$)/) || [])[1] || "";
733
+ console.log(`- ${entryId} | ${status} | ${title}`);
734
+ console.log(` ${fact.trim().split(/\r?\n/)[0] || "No verified fact summary."}`);
735
+ }
736
+ }
737
+
738
+ function runKnow(parts) {
739
+ const action = parts[0];
740
+ if (!action || action === "list" || action === "--list" || action === "-l") {
741
+ listKnowledge(parts[1]);
742
+ } else if (action === "add" || action === "--add" || action === "-a") {
743
+ addKnowledge(joinArgs(parts.slice(1)));
744
+ } else if (action === "approve") {
745
+ moveKnowledge(parts[1], "Approved Knowledge", "approved");
746
+ } else if (action === "reject") {
747
+ moveKnowledge(parts[1], "Rejected Proposals", "rejected", joinArgs(parts.slice(2)));
748
+ } else if (action === "remove" || action === "delete" || action === "--remove" || action === "-r") {
749
+ removeKnowledge(parts[1]);
750
+ } else if (action === "insight" || action === "--insight" || action === "-i") {
751
+ addKnowledgeInsight(parts[1], joinArgs(parts.slice(2)));
752
+ } else if (action === "resume" || action === "--resume" || action === "-s") {
753
+ resumeKnowledge(parts[1]);
754
+ } else if (/^K-\d{3}$/.test(action) && (parts[1] === "resume" || parts[1] === "--resume" || parts[1] === "-s")) {
755
+ resumeKnowledge(action);
756
+ } else {
757
+ addKnowledge(joinArgs(parts));
758
+ }
759
+ }
760
+
761
+ function runBacklog(parts) {
762
+ const action = parts[0];
763
+ if (!action || action === "list" || action === "--list" || action === "-l") {
764
+ listBacklog(joinArgs(parts.slice(1)));
765
+ } else if (action === "add" || action === "--add" || action === "-a") {
766
+ addBacklogItem(joinArgs(parts.slice(1)));
767
+ } else {
768
+ addBacklogItem(joinArgs(parts));
769
+ }
770
+ }
771
+
772
+ function commandList() {
773
+ console.log(`Slash commands installed by ScrumRun:
774
+
775
+ Project:
776
+ /sc-help
777
+ /sc-init
778
+ /sc-uninstall
779
+ /sc-update
780
+ /sc-study
781
+ /sc-intake
782
+ /sc-challenge
783
+
784
+ Knowledge and planning:
785
+ /sc-vault
786
+ /sc-know
787
+ /sc-context
788
+ /sc-goal
789
+ /sc-feature
790
+ /sc-sprint
791
+ /sc-backlog
792
+
793
+ Project controls:
794
+ /sc-config
795
+ /sc-golden
796
+ /sc-context
797
+ /sc-map
798
+ /sc-agent
799
+ /sc-review
800
+ /sc-decisions
801
+
802
+ CLI helpers:
803
+ scrumrun status
804
+ scrumrun commands
805
+ scrumrun vault add "NAME" "local-dev-value"
806
+ scrumrun context show
807
+ scrumrun backlog add "Sprint 01"
808
+ scrumrun know add "Topic"
809
+ scrumrun prompt challenge "..."
810
+ scrumrun core
811
+ `);
812
+ }
813
+
814
+ function printCore({ pathOnly = false, promptOnly = false } = {}) {
815
+ const coreFile = path.join(root, "CORE.md");
816
+ if (pathOnly) {
817
+ console.log(coreFile);
818
+ return;
819
+ }
820
+ if (promptOnly) {
821
+ console.log(`Read AGENTS.md and .scrumrun/core.md before doing anything else.
822
+ Follow ScrumRun exactly.
823
+ If slash commands are unavailable, execute the equivalent workflow from .scrumrun/core.md manually.
824
+ Do not plan, edit, run, or review work until you have applied the ScrumRun safety rules, project files, approved knowledge, sprint history, and decision history.`);
825
+ return;
826
+ }
827
+ console.log(readIfExists(coreFile));
828
+ }
829
+
830
+ function runContext(parts) {
831
+ const action = parts[0];
832
+ const cwd = process.cwd();
833
+ if (!action || action === "show" || action === "--show" || action === "-s") {
834
+ console.log(readIfExists(ensureContextFile(cwd)));
835
+ } else if (action === "build" || action === "--build" || action === "-b") {
836
+ ensureTokenPolicyFile(cwd);
837
+ writeFile(projectFile("context.md"), contextTemplate(cwd, { reason: "CLI build" }));
838
+ console.log("Built .scrumrun/context.md placeholder.");
839
+ console.log("For a verified snapshot, run `/sc-context --build` in your AI client.");
840
+ } else if (action === "update" || action === "--update" || action === "-u") {
841
+ const reason = joinArgs(parts.slice(1)) || "CLI update";
842
+ const file = ensureContextFile(cwd);
843
+ const current = readIfExists(file);
844
+ const note = `\n## CLI Update Note - ${today()}\n\n- Reason: ${reason}\n- This note is not verified project truth. Run \`/sc-context --update ${reason}\` for an agent-verified snapshot update.\n`;
845
+ writeFile(file, `${current.trim()}\n${note}`);
846
+ console.log("Added context update note to .scrumrun/context.md.");
847
+ } else if (action === "clear" || action === "--clear" || action === "-c") {
848
+ writeFile(projectFile("context.md"), contextTemplate(cwd, { stale: true, reason: "CLI clear" }));
849
+ console.log("Reset .scrumrun/context.md to a stale placeholder.");
850
+ } else if (action === "policy" || action === "--policy" || action === "-p") {
851
+ console.log(readIfExists(ensureTokenPolicyFile(cwd)));
852
+ } else if (action === "path") {
853
+ console.log(ensureContextFile(cwd));
854
+ } else {
855
+ console.error("Usage: scrumrun context [build|update|show|clear|policy|path]");
856
+ process.exitCode = 1;
857
+ }
858
+ }
859
+
860
+ function countMatches(content, regex) {
861
+ return (content.match(regex) || []).length;
862
+ }
863
+
864
+ function sprintStatusCounts(history) {
865
+ return {
866
+ completed: countMatches(history, /Status:\s*completed\.?/gi),
867
+ partial: countMatches(history, /Status:\s*partial\.?/gi),
868
+ blocked: countMatches(history, /Status:\s*blocked\.?/gi)
869
+ };
870
+ }
871
+
872
+ function statusProject() {
873
+ const cwd = process.cwd();
874
+ const scrumDir = path.join(cwd, ".scrumrun");
875
+ const required = [
876
+ "core.md",
877
+ "config.md",
878
+ "golden-rules.md",
879
+ "token-policy.md",
880
+ "context.md",
881
+ "map.md",
882
+ "agents.md",
883
+ "runbook.md",
884
+ "project.md",
885
+ "backlog.md",
886
+ "knowledge.md",
887
+ path.join("goals", "main", "sprint.md"),
888
+ path.join("goals", "main", "history.md"),
889
+ path.join("goals", "main", "decisions.md")
890
+ ];
891
+ console.log(`# ScrumRun Status - ${path.basename(cwd)}`);
892
+ console.log("");
893
+ console.log(`Project files: ${fs.existsSync(scrumDir) ? "present" : "missing"}`);
894
+ console.log(`AGENTS.md: ${fs.existsSync(path.join(cwd, "AGENTS.md")) ? "present" : "missing"}`);
895
+ if (hasGitRepo(cwd)) {
896
+ const exclude = readIfExists(path.join(cwd, ".git", "info", "exclude"));
897
+ console.log(`Local git exclude: ${exclude.includes(".scrumrun/") ? ".scrumrun/ ignored" : ".scrumrun/ not ignored"}`);
898
+ }
899
+ console.log("");
900
+ console.log("## Files");
901
+ for (const file of required) {
902
+ const full = path.join(scrumDir, file);
903
+ console.log(`- ${fs.existsSync(full) ? "ok" : "missing"} ${path.join(".scrumrun", file)}`);
904
+ }
905
+ const backlog = readIfExists(projectFile("backlog.md"));
906
+ const knowledge = readIfExists(projectFile("knowledge.md"));
907
+ const sprint = readIfExists(projectFile("goals", "main", "sprint.md"));
908
+ const history = readIfExists(projectFile("goals", "main", "history.md"));
909
+ const counts = sprintStatusCounts(history);
910
+ const vaultFile = projectFile("vault.local.md");
911
+ console.log("");
912
+ console.log("## Counts");
913
+ console.log(`- Backlog candidates: ${countMatches(backlog, /^- \[[ xX]\] /gm)}`);
914
+ console.log(`- Knowledge approved: ${countMatches(getSection(knowledge, "Approved Knowledge"), /^### K-\d{3}/gm)}`);
915
+ console.log(`- Knowledge pending: ${countMatches(getSection(knowledge, "Pending Proposals"), /^### K-\d{3}/gm)}`);
916
+ console.log(`- Knowledge rejected: ${countMatches(getSection(knowledge, "Rejected Proposals"), /^### K-\d{3}/gm)}`);
917
+ console.log(`- Vault entries: ${countMatches(readIfExists(vaultFile), /^### V-\d{3}/gm)} (${fs.existsSync(vaultFile) ? "local vault present" : "local vault not created"})`);
918
+ console.log(`- Planned sprints: ${countMatches(sprint, /^##+ .*Sprint/gi)}`);
919
+ console.log(`- Completed: ${counts.completed}`);
920
+ console.log(`- Partial: ${counts.partial}`);
921
+ console.log(`- Blocked: ${counts.blocked}`);
922
+ }
923
+
924
+ function promptCommand(parts) {
925
+ const kind = parts[0];
926
+ const text = joinText(parts.slice(1));
927
+ const prompts = {
928
+ intake: `/sc-intake ${text}`.trim(),
929
+ study: `/sc-study${text ? ` ${text}` : ""}`,
930
+ challenge: `/sc-challenge ${text}`.trim(),
931
+ know: `/sc-know --add ${text}`.trim(),
932
+ "context-build": `/sc-context --build${text ? ` ${text}` : ""}`,
933
+ "context-update": `/sc-context --update${text ? ` ${text}` : ""}`,
934
+ "context-show": `/sc-context --show${text ? ` ${text}` : ""}`,
935
+ context: `/sc-context ${text}`.trim(),
936
+ vault: `/sc-vault ${text}`.trim(),
937
+ "goal-set": `/sc-goal --set ${text}`.trim(),
938
+ "goal-new": `/sc-goal --set ${text}`.trim(),
939
+ goal: `/sc-goal --set ${text}`.trim(),
940
+ "sprint-add": `/sc-sprint --add ${text}`.trim(),
941
+ "sprint-new": `/sc-sprint --add ${text}`.trim(),
942
+ "sprint-run": `/sc-sprint --run ${text}`.trim(),
943
+ "backlog-add": `/sc-backlog --add ${text}`.trim(),
944
+ backlog: `/sc-backlog --add ${text}`.trim(),
945
+ status: `/sc-sprint --status${text ? ` ${text}` : ""}`
946
+ };
947
+ if (!kind || !prompts[kind]) {
948
+ console.error("Usage: scrumrun prompt <intake|study|challenge|know|context-build|context-update|vault|goal-set|sprint-add|sprint-run|backlog-add|status> [text]");
949
+ process.exitCode = 1;
950
+ return;
951
+ }
952
+ console.log(prompts[kind]);
953
+ }
954
+
955
+ function initProject({ force, mode, agentHint }) {
956
+ const cwd = process.cwd();
957
+ const vars = {
958
+ PROJECT_NAME: path.basename(cwd),
959
+ DATE: new Date().toISOString().slice(0, 10)
960
+ };
961
+ const projectTemplate = path.join(templates, "project");
962
+ const results = [];
963
+
964
+ results.push(...copyDir(path.join(projectTemplate, ".scrumrun"), path.join(cwd, ".scrumrun"), { force, vars }));
965
+ results.push(copyFile(path.join(root, "CORE.md"), path.join(cwd, ".scrumrun", "core.md"), { force, vars }));
966
+
967
+ if (mode === "shared" || agentHint) {
968
+ results.push(copyFile(path.join(projectTemplate, "AGENTS.md"), path.join(cwd, "AGENTS.md"), { force, vars }));
969
+ } else {
970
+ results.push({ status: "skipped", dest: path.join(cwd, "AGENTS.md") });
971
+ }
972
+
973
+ if (mode === "local") {
974
+ results.push(addLocalExclude(cwd, ".scrumrun/"));
975
+ if (agentHint) {
976
+ results.push(addLocalExclude(cwd, "AGENTS.md"));
977
+ }
978
+ }
979
+ results.push(addLocalExclude(cwd, ".scrumrun/vault.local.md"));
980
+
981
+ const title = mode === "local"
982
+ ? "Initialized local ScrumRun project"
983
+ : "Initialized shared ScrumRun project";
984
+ printResults(title, results);
985
+ if (mode === "local" && hasGitRepo(cwd)) {
986
+ console.log("\nLocal mode keeps ScrumRun out of commits by writing .scrumrun/ to .git/info/exclude.");
987
+ }
988
+ }
989
+
990
+ function uninstallProject(force) {
991
+ const cwd = process.cwd();
992
+ const results = [
993
+ removeLocalExclude(cwd, ".scrumrun/"),
994
+ removeLocalExclude(cwd, ".scrumrun/vault.local.md"),
995
+ removeLocalExclude(cwd, "AGENTS.md"),
996
+ removePathIfExists(path.join(cwd, ".scrumrun"), { force }),
997
+ removePathIfExists(path.join(cwd, "AGENTS.md"), { force, onlyIfScrumRunAgents: true })
998
+ ];
999
+
1000
+ printResults(force ? "Uninstalled ScrumRun project files" : "Prepared ScrumRun uninstall", results);
1001
+ if (!force) {
1002
+ console.log("\nRun `scrumrun uninstall --force` to remove .scrumrun/ and a ScrumRun-generated AGENTS.md.");
1003
+ }
1004
+ }
1005
+
1006
+ function moveIfPossible(cwd, from, to) {
1007
+ const src = path.join(cwd, from);
1008
+ const dest = path.join(cwd, to);
1009
+ if (!fs.existsSync(src)) return { status: "missing", dest: src };
1010
+ if (fs.existsSync(dest)) return { status: "skipped", dest };
1011
+ ensureDir(path.dirname(dest));
1012
+ fs.renameSync(src, dest);
1013
+ return { status: "moved", dest: `${src} -> ${dest}` };
1014
+ }
1015
+
1016
+ function migrateProject() {
1017
+ const cwd = process.cwd();
1018
+ const moves = [
1019
+ ["asm-config.md", ".scrumrun/config.md"],
1020
+ ["golden-rules.md", ".scrumrun/golden-rules.md"],
1021
+ ["map.md", ".scrumrun/map.md"],
1022
+ ["review-agents.md", ".scrumrun/agents.md"],
1023
+ ["ai-sprint-runbook.md", ".scrumrun/runbook.md"],
1024
+ ["sprint.md", ".scrumrun/goals/main/sprint.md"],
1025
+ ["history.md", ".scrumrun/goals/main/history.md"]
1026
+ ];
1027
+ ensureDir(path.join(cwd, ".scrumrun", "features"));
1028
+ ensureDir(path.join(cwd, ".scrumrun", "reviews"));
1029
+ ensureDir(path.join(cwd, ".scrumrun", "goals", "main"));
1030
+ const results = moves.map(([from, to]) => moveIfPossible(cwd, from, to));
1031
+ if (!fs.existsSync(path.join(cwd, ".scrumrun", "config.md"))) {
1032
+ results.push(copyFile(path.join(templates, "project", ".scrumrun", "config.md"), path.join(cwd, ".scrumrun", "config.md"), {
1033
+ vars: { PROJECT_NAME: path.basename(cwd) }
1034
+ }));
1035
+ }
1036
+ const interactionConfig = ensureInteractionPreferences(cwd);
1037
+ if (interactionConfig) results.push(interactionConfig);
1038
+ results.push(copyFile(path.join(root, "CORE.md"), path.join(cwd, ".scrumrun", "core.md"), {
1039
+ force: true,
1040
+ vars: { PROJECT_NAME: path.basename(cwd), DATE: today() }
1041
+ }));
1042
+ if (!fs.existsSync(path.join(cwd, ".scrumrun", "goals", "main", "decisions.md"))) {
1043
+ fs.writeFileSync(
1044
+ path.join(cwd, ".scrumrun", "goals", "main", "decisions.md"),
1045
+ "# Main Goal Decisions\n\n## Open Decisions\n\n- Pending: none.\n\n## Resolved Decisions\n\n- Pending: none.\n"
1046
+ );
1047
+ results.push({ status: "written", dest: path.join(cwd, ".scrumrun", "goals", "main", "decisions.md") });
1048
+ }
1049
+ if (!fs.existsSync(path.join(cwd, ".scrumrun", "project.md"))) {
1050
+ fs.writeFileSync(
1051
+ path.join(cwd, ".scrumrun", "project.md"),
1052
+ `# Project - ${path.basename(cwd)}\n\n## Purpose\n\nPending: review migrated ScrumRun files and update with /sc-goal --set.\n\n## Active Lanes\n\n- Main goal: .scrumrun/goals/main/\n- Features: .scrumrun/features/\n`
1053
+ );
1054
+ results.push({ status: "written", dest: path.join(cwd, ".scrumrun", "project.md") });
1055
+ }
1056
+ if (!fs.existsSync(path.join(cwd, ".scrumrun", "context.md"))) {
1057
+ ensureContextFile(cwd);
1058
+ results.push({ status: "written", dest: path.join(cwd, ".scrumrun", "context.md") });
1059
+ }
1060
+ if (!fs.existsSync(path.join(cwd, ".scrumrun", "token-policy.md"))) {
1061
+ ensureTokenPolicyFile(cwd);
1062
+ results.push({ status: "written", dest: path.join(cwd, ".scrumrun", "token-policy.md") });
1063
+ }
1064
+ if (!fs.existsSync(path.join(cwd, ".scrumrun", "backlog.md"))) {
1065
+ ensureBacklogFile(cwd);
1066
+ results.push({ status: "written", dest: path.join(cwd, ".scrumrun", "backlog.md") });
1067
+ }
1068
+ if (!fs.existsSync(path.join(cwd, ".scrumrun", "knowledge.md"))) {
1069
+ fs.writeFileSync(
1070
+ path.join(cwd, ".scrumrun", "knowledge.md"),
1071
+ `# Knowledge Base - ${path.basename(cwd)}\n\nOnly approved knowledge should be used as planning context for /sc-challenge, /sc-sprint --add, /sc-sprint --run, and feature planning.\n\n## Approved Knowledge\n\n- Pending: none.\n\n## Pending Proposals\n\n- Pending: none.\n\n## Rejected Proposals\n\n- Pending: none.\n`
1072
+ );
1073
+ results.push({ status: "written", dest: path.join(cwd, ".scrumrun", "knowledge.md") });
1074
+ }
1075
+ printResults("Migrated project", results.filter((result) => result.status !== "missing"));
1076
+ }
1077
+
1078
+ function doctor(target = "all") {
1079
+ const home = os.homedir();
1080
+ const checks = [];
1081
+
1082
+ if (target === "all" || target === "codex") {
1083
+ for (const command of COMMANDS) {
1084
+ checks.push([`Codex prompt ${command}`, path.join(home, ".codex", "prompts", `${command}.md`)]);
1085
+ }
1086
+ checks.push(["Codex skill scrumrun", path.join(home, ".codex", "skills", "scrumrun", "SKILL.md")]);
1087
+ }
1088
+
1089
+ if (target === "all" || target === "claude") {
1090
+ for (const command of COMMANDS) {
1091
+ checks.push([`Claude command ${command}`, path.join(home, ".claude", "commands", `${command}.md`)]);
1092
+ }
1093
+ checks.push(["Claude skill scrumrun", path.join(home, ".claude", "skills", "scrumrun", "SKILL.md")]);
1094
+ }
1095
+
1096
+ if (target === "all" || target === "opencode") {
1097
+ for (const command of COMMANDS) {
1098
+ checks.push([`OpenCode command ${command}`, path.join(home, ".config", "opencode", "commands", `${command}.md`)]);
1099
+ }
1100
+ checks.push(["OpenCode skill scrumrun", path.join(home, ".config", "opencode", "skills", "scrumrun", "SKILL.md")]);
1101
+ }
1102
+
1103
+ let ok = true;
1104
+ for (const [label, file] of checks) {
1105
+ const exists = fs.existsSync(file);
1106
+ ok = ok && exists;
1107
+ console.log(`${exists ? "ok " : "miss"} ${label}: ${file}`);
1108
+ }
1109
+ process.exitCode = ok ? 0 : 1;
1110
+ }
1111
+
1112
+ const args = process.argv.slice(2);
1113
+ const command = args[0];
1114
+ const force = args.includes("--force") || args.includes("-f");
1115
+ const local = args.includes("--local") || args.includes("-l");
1116
+ const shared = args.includes("--shared") || args.includes("-s");
1117
+ const noAgentHint = args.includes("--no-agent-hint") || args.includes("-n");
1118
+
1119
+ if (!command || command === "--help" || command === "-h") {
1120
+ usage();
1121
+ } else if (command === "install" || command === "update") {
1122
+ const target = ["all", "codex", "opencode", "claude"].includes(args[1]) ? args[1] : "all";
1123
+ install(target, true);
1124
+ } else if (command === "init") {
1125
+ if (local && shared) {
1126
+ console.error("Use either --local or --shared, not both.");
1127
+ process.exitCode = 1;
1128
+ } else {
1129
+ initProject({ force, mode: shared ? "shared" : "local", agentHint: shared || !noAgentHint });
1130
+ }
1131
+ } else if (command === "uninstall") {
1132
+ uninstallProject(force);
1133
+ } else if (command === "status") {
1134
+ statusProject();
1135
+ } else if (command === "core") {
1136
+ printCore({ pathOnly: args.includes("--path"), promptOnly: args.includes("--prompt") });
1137
+ } else if (command === "commands") {
1138
+ commandList();
1139
+ } else if (command === "context") {
1140
+ runContext(args.slice(1));
1141
+ } else if (command === "vault") {
1142
+ runVault(args.slice(1));
1143
+ } else if (command === "backlog" || command === "backlog-add" || command === "backlog-list") {
1144
+ if (command === "backlog-add") {
1145
+ runBacklog(["add", ...args.slice(1)]);
1146
+ } else if (command === "backlog-list") {
1147
+ runBacklog(["list", ...args.slice(1)]);
1148
+ } else {
1149
+ runBacklog(args.slice(1));
1150
+ }
1151
+ } else if (command === "know") {
1152
+ runKnow(args.slice(1));
1153
+ } else if (command === "prompt") {
1154
+ promptCommand(args.slice(1));
1155
+ } else if (command === "migrate") {
1156
+ migrateProject();
1157
+ } else if (command === "doctor") {
1158
+ const target = ["all", "codex", "opencode", "claude"].includes(args[1]) ? args[1] : "all";
1159
+ doctor(target);
1160
+ } else if (command === "claude") {
1161
+ const sub = args[1];
1162
+ if (sub === "install" || sub === "update") {
1163
+ installClaude(true);
1164
+ } else if (sub === "doctor") {
1165
+ doctor("claude");
1166
+ } else {
1167
+ console.error("Usage: scrumrun claude [install|update|doctor]");
1168
+ process.exitCode = 1;
1169
+ }
1170
+ } else if (command === "codex") {
1171
+ const sub = args[1];
1172
+ if (sub === "install" || sub === "update") {
1173
+ installCodex(true);
1174
+ } else if (sub === "doctor") {
1175
+ doctor("codex");
1176
+ } else {
1177
+ console.error("Usage: scrumrun codex [install|update|doctor]");
1178
+ process.exitCode = 1;
1179
+ }
1180
+ } else if (command === "opencode") {
1181
+ const sub = args[1];
1182
+ if (sub === "install" || sub === "update") {
1183
+ installOpenCode(true);
1184
+ } else if (sub === "doctor") {
1185
+ doctor("opencode");
1186
+ } else {
1187
+ console.error("Usage: scrumrun opencode [install|update|doctor]");
1188
+ process.exitCode = 1;
1189
+ }
1190
+ } else {
1191
+ console.error(`Unknown command: ${command}`);
1192
+ usage();
1193
+ process.exitCode = 1;
1194
+ }