scrumrun 1.5.2 → 2.0.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 (106) hide show
  1. package/CHANGELOG.md +42 -0
  2. package/CORE.md +224 -311
  3. package/DECISIONS.md +95 -11
  4. package/LICENSE +21 -0
  5. package/MIGRATION-1-to-2.md +109 -0
  6. package/README.md +137 -78
  7. package/SPEC.md +248 -257
  8. package/bin/claude-install.js +18 -132
  9. package/bin/scrumrun.js +1259 -174
  10. package/docs/COMMANDS.md +69 -0
  11. package/docs/ENTITY-MODEL.md +38 -0
  12. package/docs/RELEASE.md +44 -0
  13. package/docs/SCHEMA.md +80 -0
  14. package/docs/SEMANTIC-MEMORY.md +68 -0
  15. package/docs/TROUBLESHOOTING.md +66 -0
  16. package/lib/code-intel/adapter.js +20 -0
  17. package/lib/code-intel/javascript.js +199 -0
  18. package/lib/code-intel/learning.js +66 -0
  19. package/lib/code-intel/scanner.js +126 -0
  20. package/lib/commands/manifest.js +109 -0
  21. package/lib/commands/render.js +75 -0
  22. package/lib/memory/index.js +600 -0
  23. package/lib/memory/markdown.js +51 -0
  24. package/lib/memory/service.js +300 -0
  25. package/lib/runtime/budgets.js +19 -0
  26. package/lib/runtime/context.js +141 -0
  27. package/lib/runtime/orchestrator.js +270 -0
  28. package/lib/runtime/request-engine.js +128 -0
  29. package/lib/security/secrets.js +23 -0
  30. package/lib/v2/artifacts.js +343 -0
  31. package/lib/v2/conformance.js +167 -0
  32. package/lib/v2/migration.js +1161 -0
  33. package/lib/v2/project-store.js +44 -0
  34. package/lib/v2/schema.js +128 -0
  35. package/package.json +16 -6
  36. package/scripts/generate-contract-docs.js +113 -0
  37. package/templates/project/.scrumrun/config.md +4 -7
  38. package/templates/project/.scrumrun/guardrails.md +23 -0
  39. package/templates/project/.scrumrun/map.md +4 -16
  40. package/templates/project/.scrumrun/memory/decisions/.gitkeep +1 -0
  41. package/templates/project/.scrumrun/memory/dossiers/.gitkeep +1 -0
  42. package/templates/project/.scrumrun/memory/insights/.gitkeep +1 -0
  43. package/templates/project/.scrumrun/memory/knowledge/.gitkeep +1 -0
  44. package/templates/project/.scrumrun/method.json +4 -0
  45. package/templates/project/.scrumrun/project.md +6 -12
  46. package/templates/project/.scrumrun/runs/.gitkeep +1 -0
  47. package/templates/project/.scrumrun/sprints/.gitkeep +1 -0
  48. package/templates/project/.scrumrun/state.md +21 -0
  49. package/templates/project/.scrumrun/tasks/.gitkeep +1 -0
  50. package/templates/project/AGENTS.md +16 -49
  51. package/templates/project-lean/AGENTS.md +18 -0
  52. package/templates/shared/skills/scrumrun/SKILL.md +201 -0
  53. package/templates/codex/prompts/sc-agent.md +0 -14
  54. package/templates/codex/prompts/sc-backlog.md +0 -13
  55. package/templates/codex/prompts/sc-challenge.md +0 -57
  56. package/templates/codex/prompts/sc-config.md +0 -18
  57. package/templates/codex/prompts/sc-context.md +0 -24
  58. package/templates/codex/prompts/sc-decisions.md +0 -8
  59. package/templates/codex/prompts/sc-feature.md +0 -16
  60. package/templates/codex/prompts/sc-fix.md +0 -21
  61. package/templates/codex/prompts/sc-goal.md +0 -14
  62. package/templates/codex/prompts/sc-golden.md +0 -14
  63. package/templates/codex/prompts/sc-help.md +0 -12
  64. package/templates/codex/prompts/sc-init.md +0 -14
  65. package/templates/codex/prompts/sc-intake.md +0 -22
  66. package/templates/codex/prompts/sc-know.md +0 -75
  67. package/templates/codex/prompts/sc-map.md +0 -13
  68. package/templates/codex/prompts/sc-review.md +0 -13
  69. package/templates/codex/prompts/sc-sprint.md +0 -28
  70. package/templates/codex/prompts/sc-study.md +0 -23
  71. package/templates/codex/prompts/sc-uninstall.md +0 -14
  72. package/templates/codex/prompts/sc-update.md +0 -8
  73. package/templates/codex/prompts/sc-vault.md +0 -27
  74. package/templates/codex/skills/scrumrun/SKILL.md +0 -412
  75. package/templates/opencode/commands/sc-agent.md +0 -14
  76. package/templates/opencode/commands/sc-backlog.md +0 -13
  77. package/templates/opencode/commands/sc-challenge.md +0 -57
  78. package/templates/opencode/commands/sc-config.md +0 -18
  79. package/templates/opencode/commands/sc-context.md +0 -24
  80. package/templates/opencode/commands/sc-decisions.md +0 -8
  81. package/templates/opencode/commands/sc-feature.md +0 -16
  82. package/templates/opencode/commands/sc-fix.md +0 -21
  83. package/templates/opencode/commands/sc-goal.md +0 -14
  84. package/templates/opencode/commands/sc-golden.md +0 -14
  85. package/templates/opencode/commands/sc-help.md +0 -12
  86. package/templates/opencode/commands/sc-init.md +0 -14
  87. package/templates/opencode/commands/sc-intake.md +0 -22
  88. package/templates/opencode/commands/sc-know.md +0 -75
  89. package/templates/opencode/commands/sc-map.md +0 -13
  90. package/templates/opencode/commands/sc-review.md +0 -13
  91. package/templates/opencode/commands/sc-sprint.md +0 -28
  92. package/templates/opencode/commands/sc-study.md +0 -23
  93. package/templates/opencode/commands/sc-uninstall.md +0 -14
  94. package/templates/opencode/commands/sc-update.md +0 -8
  95. package/templates/opencode/commands/sc-vault.md +0 -27
  96. package/templates/opencode/skills/scrumrun/SKILL.md +0 -412
  97. package/templates/project/.scrumrun/agents.md +0 -36
  98. package/templates/project/.scrumrun/backlog.md +0 -7
  99. package/templates/project/.scrumrun/context.md +0 -61
  100. package/templates/project/.scrumrun/goals/main/decisions.md +0 -9
  101. package/templates/project/.scrumrun/goals/main/history.md +0 -51
  102. package/templates/project/.scrumrun/goals/main/sprint.md +0 -54
  103. package/templates/project/.scrumrun/golden-rules.md +0 -9
  104. package/templates/project/.scrumrun/knowledge.md +0 -15
  105. package/templates/project/.scrumrun/runbook.md +0 -101
  106. package/templates/project/.scrumrun/token-policy.md +0 -43
package/bin/scrumrun.js CHANGED
@@ -1,5 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
 
3
+ const crypto = require("crypto");
3
4
  const fs = require("fs");
4
5
  const path = require("path");
5
6
  const os = require("os");
@@ -7,82 +8,46 @@ const os = require("os");
7
8
  const root = path.resolve(__dirname, "..");
8
9
  const templates = path.join(root, "templates");
9
10
  const { version } = require(path.join(root, "package.json"));
10
-
11
- const COMMANDS = [
12
- "sc-help",
13
- "sc-intake",
14
- "sc-study",
15
- "sc-init",
16
- "sc-uninstall",
17
- "sc-update",
18
- "sc-vault",
19
- "sc-know",
20
- "sc-context",
21
- "sc-config",
22
- "sc-golden",
23
- "sc-map",
24
- "sc-challenge",
25
- "sc-goal",
26
- "sc-backlog",
27
- "sc-feature",
28
- "sc-sprint",
29
- "sc-fix",
30
- "sc-agent",
31
- "sc-review",
32
- "sc-decisions"
33
- ];
11
+ const { applyMigration, dryRunMigration, rollbackMigration } = require(path.join(root, "lib", "v2", "migration"));
12
+ const { ARTIFACT_TYPES, ArtifactRepository } = require(path.join(root, "lib", "v2", "artifacts"));
13
+ const { aliases: COMMAND_ALIASES, resolveAlias, resolveRoute } = require(path.join(root, "lib", "commands", "manifest"));
14
+ const { renderCommandHelp, renderCompatibilityPrompt, renderRootPrompt } = require(path.join(root, "lib", "commands", "render"));
15
+ const { planRequest } = require(path.join(root, "lib", "runtime", "request-engine"));
16
+ const { approveRequest, refreshState, retryTask, transitionRun } = require(path.join(root, "lib", "runtime", "orchestrator"));
17
+ const { createMemory, listMemory, showMemory, transitionMemory } = require(path.join(root, "lib", "memory", "service"));
18
+ const { indexPath, indexStatus, queryIndex, rebuildIndex, writeMap } = require(path.join(root, "lib", "memory", "index"));
19
+ const { auditProject } = require(path.join(root, "lib", "v2", "conformance"));
20
+ const { containsSecret } = require(path.join(root, "lib", "security", "secrets"));
21
+
22
+ const COMMANDS = ["sc"];
23
+ const COMPATIBILITY_COMMANDS = Object.keys(COMMAND_ALIASES);
24
+ const MANAGED_COMMANDS = [...new Set(["sc", "sc-plan", "sc-knowledge", "sc-rules", ...COMPATIBILITY_COMMANDS])];
34
25
 
35
26
  function usage() {
36
27
  console.log(`ScrumRun
37
28
 
38
29
  Usage:
39
30
  scrumrun --version
31
+ scrumrun sc
32
+ scrumrun sc <noun> <subject> <action> [args]
40
33
  scrumrun install [all|codex|opencode|claude] [--force]
41
- scrumrun update [all|codex|opencode|claude]
42
- scrumrun init [--local|--shared] [--no-agent-hint] [--force]
34
+ scrumrun update [all|codex|opencode|claude] [--migrate]
35
+ scrumrun init [--local|--shared] [--lean] [--no-agent-hint] [--force]
43
36
  scrumrun status
44
37
  scrumrun core [--path|--prompt]
45
- scrumrun context build
46
- scrumrun context update [reason]
47
- scrumrun context show
48
- scrumrun context clear
49
- scrumrun context policy
50
38
  scrumrun commands
51
- scrumrun vault add <name> <value>
52
- scrumrun vault add <name:value>
53
- scrumrun vault list
54
- scrumrun vault show <V-NNN|name>
55
- scrumrun vault remove <V-NNN|name>
56
- scrumrun backlog add <sprint>
57
- scrumrun backlog list
58
- scrumrun know add <topic>
59
- scrumrun know list
60
- scrumrun know approve <K-NNN>
61
- scrumrun know reject <K-NNN> [reason]
62
- scrumrun know insight <K-NNN> <text>
63
- scrumrun know remove <K-NNN>
64
- scrumrun know resume [K-NNN]
65
- scrumrun prompt <intake|study|challenge|know|context-build|context-update|vault|goal-set|sprint-add|sprint-run|backlog-add> [text]
39
+ scrumrun migrate --to 2 --dry-run
40
+ scrumrun migrate --to 2 --apply
41
+ scrumrun migrate --to 2 --rollback
42
+ scrumrun doctor [all|codex|opencode|claude] [--strict]
66
43
  scrumrun uninstall [--force]
67
- scrumrun migrate
68
- scrumrun doctor [all|codex|opencode|claude]
69
44
 
70
45
  Examples:
71
46
  npx scrumrun@latest install
72
- npx scrumrun@latest install claude
47
+ npx scrumrun@latest sc knowledge decision --list
73
48
  npx scrumrun@latest init
74
- npx scrumrun@latest core
75
49
  npx scrumrun@latest status
76
- npx scrumrun@latest context show
77
- npx scrumrun@latest vault add OPENAI_API_KEY sk-local-dev
78
- npx scrumrun@latest backlog add "Sprint 01"
79
- npx scrumrun@latest know add "Tenant permissions for report exports"
80
- npx scrumrun@latest prompt intake "Checkout payments duplicate after refresh"
81
- npx scrumrun@latest prompt challenge "Exports need tenant permission checks"
82
- npx scrumrun@latest uninstall --force
83
- npx scrumrun@latest init --shared
84
- npx scrumrun@latest migrate
85
- npx scrumrun@latest doctor
50
+ npx scrumrun@latest migrate --to 2 --dry-run
86
51
  `);
87
52
  }
88
53
 
@@ -110,9 +75,37 @@ function readIfExists(file) {
110
75
  return fs.existsSync(file) ? fs.readFileSync(file, "utf8") : "";
111
76
  }
112
77
 
113
- function writeFile(file, content) {
78
+ function writeFile(file, content, { backup = false } = {}) {
114
79
  ensureDir(path.dirname(file));
115
- fs.writeFileSync(file, content.endsWith("\n") ? content : `${content}\n`);
80
+ const normalized = content.endsWith("\n") ? content : `${content}\n`;
81
+ const current = readIfExists(file);
82
+ if (current === normalized) return { changed: false, backup: null };
83
+
84
+ let backupFile = null;
85
+ if (backup && current) {
86
+ const scrumDir = path.join(process.cwd(), ".scrumrun");
87
+ const relative = path.relative(scrumDir, file);
88
+ const safeName = relative.replace(/[^a-zA-Z0-9._-]+/g, "__");
89
+ const digest = crypto.createHash("sha256").update(current).digest("hex").slice(0, 16);
90
+ backupFile = path.join(scrumDir, ".backup", `${safeName}.${digest}.bak`);
91
+ if (!fs.existsSync(backupFile)) {
92
+ ensureDir(path.dirname(backupFile));
93
+ fs.writeFileSync(backupFile, current);
94
+ }
95
+ }
96
+
97
+ const tempFile = path.join(
98
+ path.dirname(file),
99
+ `.${path.basename(file)}.${process.pid}.${crypto.randomBytes(6).toString("hex")}.tmp`
100
+ );
101
+ try {
102
+ fs.writeFileSync(tempFile, normalized);
103
+ fs.renameSync(tempFile, file);
104
+ } catch (error) {
105
+ if (fs.existsSync(tempFile)) fs.unlinkSync(tempFile);
106
+ throw error;
107
+ }
108
+ return { changed: true, backup: backupFile };
116
109
  }
117
110
 
118
111
  function projectFile(...parts) {
@@ -180,7 +173,8 @@ function cleanupLegacy(commandsDir, skillsDir) {
180
173
  const name = entry.name;
181
174
  const legacyPrefix = name.startsWith("srun-") || name.startsWith("asm-") || name.startsWith("sr-") || name.startsWith("scr-") || name.startsWith("src-") || name.startsWith("run-");
182
175
  const oldConsolidated = /^scr-[a-z]+-[a-z]+\.md$/.test(name);
183
- if (legacyPrefix || oldConsolidated) {
176
+ const managed = MANAGED_COMMANDS.some((command) => name === `${command}.md`);
177
+ if (legacyPrefix || oldConsolidated || managed) {
184
178
  fs.rmSync(path.join(commandsDir, name), { force: true });
185
179
  console.log(` rm legacy ${path.join(commandsDir, name)}`);
186
180
  }
@@ -193,53 +187,124 @@ function cleanupLegacy(commandsDir, skillsDir) {
193
187
  }
194
188
  }
195
189
 
196
- function installClaude(force) {
197
- const home = os.homedir();
198
- const commandsDir = path.join(home, ".claude", "commands");
199
- cleanupLegacy(commandsDir, path.join(home, ".claude", "skills"));
190
+ function writeCommandAsset(destination, content, force) {
191
+ if (fs.existsSync(destination) && !force) return { status: "skipped", dest: destination };
192
+ ensureDir(path.dirname(destination));
193
+ fs.writeFileSync(destination, content);
194
+ return { status: "written", dest: destination };
195
+ }
196
+
197
+ function installClient(label, commandsDir, skillsDir, skillTemplateDir, force, { compatibility = false } = {}) {
198
+ cleanupLegacy(commandsDir, skillsDir);
200
199
  const results = [
201
- ...copyDir(path.join(templates, "codex", "prompts"), commandsDir, { force }),
202
- ...copyDir(path.join(templates, "codex", "skills"), path.join(home, ".claude", "skills"), { force })
200
+ writeCommandAsset(path.join(commandsDir, "sc.md"), renderRootPrompt(), force),
201
+ ...copyDir(skillTemplateDir, skillsDir, { force })
203
202
  ];
204
- printResults("Installed claude", results);
203
+ if (compatibility) {
204
+ for (const alias of COMPATIBILITY_COMMANDS) {
205
+ results.push(writeCommandAsset(path.join(commandsDir, `${alias}.md`), renderCompatibilityPrompt(alias), force));
206
+ }
207
+ }
208
+ printResults(`Installed ${label}`, results);
209
+ console.log(compatibility
210
+ ? " compatibility adapters installed for this v1 upgrade cycle"
211
+ : " canonical surface: /sc only");
205
212
  }
206
213
 
207
- function installCodex(force) {
214
+ function installClaude(force, options = {}) {
208
215
  const home = os.homedir();
209
- const promptsDir = path.join(home, ".codex", "prompts");
210
- cleanupLegacy(promptsDir, path.join(home, ".codex", "skills"));
211
- const results = [
212
- ...copyDir(path.join(templates, "codex", "prompts"), promptsDir, { force }),
213
- ...copyDir(path.join(templates, "codex", "skills"), path.join(home, ".codex", "skills"), { force })
214
- ];
215
- printResults("Installed codex", results);
216
+ installClient(
217
+ "claude",
218
+ path.join(home, ".claude", "commands"),
219
+ path.join(home, ".claude", "skills"),
220
+ path.join(templates, "shared", "skills"),
221
+ force,
222
+ options
223
+ );
216
224
  }
217
225
 
218
- function installOpenCode(force) {
226
+ function installCodex(force, options = {}) {
219
227
  const home = os.homedir();
220
- const commandsDir = path.join(home, ".config", "opencode", "commands");
221
- cleanupLegacy(commandsDir, path.join(home, ".config", "opencode", "skills"));
222
- const results = [
223
- ...copyDir(path.join(templates, "opencode", "commands"), commandsDir, { force }),
224
- ...copyDir(path.join(templates, "opencode", "skills"), path.join(home, ".config", "opencode", "skills"), { force })
225
- ];
226
- printResults("Installed opencode", results);
228
+ installClient(
229
+ "codex",
230
+ path.join(home, ".codex", "prompts"),
231
+ path.join(home, ".codex", "skills"),
232
+ path.join(templates, "shared", "skills"),
233
+ force,
234
+ options
235
+ );
227
236
  }
228
237
 
229
- function install(target, force) {
238
+ function installOpenCode(force, options = {}) {
239
+ const home = os.homedir();
240
+ installClient(
241
+ "opencode",
242
+ path.join(home, ".config", "opencode", "commands"),
243
+ path.join(home, ".config", "opencode", "skills"),
244
+ path.join(templates, "shared", "skills"),
245
+ force,
246
+ options
247
+ );
248
+ }
249
+
250
+ function install(target, force, options = {}) {
230
251
  if (target === "all") {
231
- installCodex(force);
232
- installClaude(force);
233
- installOpenCode(force);
252
+ installCodex(force, options);
253
+ installClaude(force, options);
254
+ installOpenCode(force, options);
234
255
  } else if (target === "codex") {
235
- installCodex(force);
256
+ installCodex(force, options);
236
257
  } else if (target === "claude") {
237
- installClaude(force);
258
+ installClaude(force, options);
238
259
  } else if (target === "opencode") {
239
- installOpenCode(force);
260
+ installOpenCode(force, options);
261
+ }
262
+ }
263
+
264
+ function migrationPreflightOnUpdate({ apply = false } = {}) {
265
+ const scrumDir = path.join(process.cwd(), ".scrumrun");
266
+ if (!fs.existsSync(scrumDir)) return { status: "not-a-project" };
267
+ const marker = path.join(scrumDir, "method.json");
268
+ if (fs.existsSync(marker)) {
269
+ try {
270
+ if (JSON.parse(fs.readFileSync(marker, "utf8")).method === "2.0.0") return { status: "already-v2" };
271
+ } catch {
272
+ console.error("Project migration preflight failed: .scrumrun/method.json is malformed.");
273
+ process.exitCode = 1;
274
+ return { status: "blocked" };
275
+ }
276
+ }
277
+ try {
278
+ const preview = dryRunMigration(process.cwd());
279
+ console.log("\n## Project migration preflight\n");
280
+ console.log(preview.output.trimEnd());
281
+ if (preview.status === "blocked") {
282
+ for (const error of preview.plan.errors) console.error(`BLOCKED: ${error}`);
283
+ process.exitCode = 1;
284
+ return { status: "blocked" };
285
+ }
286
+ if (!apply) {
287
+ console.log("\nThe project remains unchanged. Apply the verified plan with: npx scrumrun@latest update --migrate");
288
+ return { status: "ready" };
289
+ }
290
+ const result = applyMigration(process.cwd(), { plan: preview.plan });
291
+ console.log("\nApplied the verified ScrumRun v1 → v2 project migration.");
292
+ console.log(`Source fingerprint: ${result.manifest.sourceInventory.rootSha256}`);
293
+ console.log("Rollback remains available with: npx scrumrun@latest migrate --to 2 --rollback");
294
+ return { status: "applied", result };
295
+ } catch (error) {
296
+ console.error(`Project migration preflight failed: ${error.message}`);
297
+ process.exitCode = 1;
298
+ return { status: "blocked" };
240
299
  }
241
300
  }
242
301
 
302
+ function updateInstallation(target, { migrate = false } = {}) {
303
+ const migration = migrationPreflightOnUpdate({ apply: migrate });
304
+ install(target, true, { compatibility: true });
305
+ return migration;
306
+ }
307
+
243
308
  function hasGitRepo(cwd) {
244
309
  return fs.existsSync(path.join(cwd, ".git"));
245
310
  }
@@ -260,6 +325,22 @@ function addLocalExclude(cwd, pattern) {
260
325
  return { status: "written", dest: `${excludeFile} (${pattern})` };
261
326
  }
262
327
 
328
+ function ensureProjectIgnore(cwd) {
329
+ const file = path.join(cwd, ".scrumrun", ".gitignore");
330
+ if (fs.existsSync(file) && fs.lstatSync(file).isSymbolicLink()) {
331
+ throw new Error("Refusing to write a symbolic .scrumrun/.gitignore.");
332
+ }
333
+ const current = readIfExists(file);
334
+ const lines = current.split(/\r?\n/).filter(Boolean);
335
+ for (const pattern of [".cache/", ".backup/", ".migration/", "vault.local.md"]) {
336
+ if (!lines.includes(pattern)) lines.push(pattern);
337
+ }
338
+ const next = `${lines.join("\n")}\n`;
339
+ if (next === current) return { status: "skipped", dest: file };
340
+ writeFile(file, next);
341
+ return { status: "written", dest: file };
342
+ }
343
+
263
344
  function removeLocalExclude(cwd, pattern) {
264
345
  const excludeFile = path.join(cwd, ".git", "info", "exclude");
265
346
  if (!fs.existsSync(excludeFile)) {
@@ -427,6 +508,218 @@ This policy reduces token waste without weakening ScrumRun safety.
427
508
  return policyFile;
428
509
  }
429
510
 
511
+ function renderGuardrails(cwd) {
512
+ const name = path.basename(cwd);
513
+ const golden = readIfExists(path.join(cwd, ".scrumrun", "golden-rules.md")).trim();
514
+ const config = readIfExists(path.join(cwd, ".scrumrun", "config.md")).trim();
515
+ const tokenPolicy = readIfExists(path.join(cwd, ".scrumrun", "token-policy.md")).trim();
516
+ const knowledge = readIfExists(path.join(cwd, ".scrumrun", "knowledge.md"));
517
+ const approvedMatch = knowledge.match(/## Approved Knowledge\n+([\s\S]*?)(?=\n## |$)/);
518
+ const approved = approvedMatch ? approvedMatch[1].trim() : "";
519
+ const approvedEntries = approved
520
+ .split(/(?=^### K-\d{3})/m)
521
+ .map((s) => s.trim())
522
+ .filter((s) => s.startsWith("### K-"));
523
+
524
+ const goldenSection = golden
525
+ ? golden.replace(/^# ScrumRun Golden Rules.*\n/m, "").trim()
526
+ : "Pending: add rules with `/sc-rules golden --add <rule>`.";
527
+ const configSection = config
528
+ ? config
529
+ .split("\n")
530
+ .filter((line) => line.match(/^[A-Z][\w -]*:/))
531
+ .map((line) => `- ${line}`)
532
+ .join("\n") || "Pending: configure with `/sc-config config --lang|--interaction|--approval|--quick-tasks`."
533
+ : "Pending: configure with `/sc-config config --lang|--interaction|--approval|--quick-tasks`.";
534
+ const tokenSection = tokenPolicy
535
+ ? tokenPolicy.replace(/^# ScrumRun Token Policy.*\n/m, "").trim()
536
+ : "Pending: token policy not initialized.";
537
+ const approvedSection = approvedEntries.length
538
+ ? approvedEntries.map((entry) => {
539
+ const id = (entry.match(/^### (K-\d{3})/) || [])[1];
540
+ const title = (entry.match(/^### K-\d{3} - (.+)$/m) || [])[1] || "untitled";
541
+ const fact = (entry.match(/#### Verified Facts\n+([\s\S]*?)(?=\n#### |$)/m) || [])[1];
542
+ const oneLine = fact ? fact.trim().split(/\r?\n/)[0].replace(/^[-*]\s*/, "") : "no verified fact summary";
543
+ return `- **${id}** — ${title}: ${oneLine}`;
544
+ }).join("\n")
545
+ : "Pending: add with `/sc-knowledge know --add <topic>`, then `--approve <K-NNN>`.";
546
+
547
+ return `# ScrumRun Guardrails - ${name}
548
+
549
+ Single-file view of rules, invariants, and configuration. Generated by \`scrumrun guardrails --build\`. The source files listed under \`Sources\` are canonical; this file is a pre-digested snapshot for token economy.
550
+
551
+ Last built: ${today()}
552
+ Built by: CLI
553
+
554
+ ## Precedence
555
+
556
+ When guidance conflicts, apply in this strict order (SPEC §5):
557
+
558
+ 1. Golden rules (active)
559
+ 2. Open decisions linked to current work
560
+ 3. Approved knowledge facts
561
+ 4. Current sprint / feature plan
562
+ 5. Backlog priority
563
+ 6. Map and dossiers
564
+ 7. \`state.md\` (never authoritative)
565
+
566
+ Higher precedence always wins. Lower precedence never silently overrides.
567
+
568
+ ## Golden Rules
569
+
570
+ ${goldenSection}
571
+
572
+ ## Active Invariants
573
+
574
+ Numbered for stable reference. Full list in \`SPEC.md\` §6.
575
+
576
+ - **I-01** No sprint moves from \`proposed\` to \`running\` without explicit owner approval.
577
+ - **I-02** No code changes outside a \`running\` sprint, \`running\` fix, or explicitly authorized quick task.
578
+ - **I-03** Active golden rule cannot be bypassed; retirement is the only path.
579
+ - **I-04** Intake is read-only.
580
+ - **I-05** Main goal history and feature lane history are physically separate.
581
+ - **I-06** Approval is synchronous and explicit; ambiguous acknowledgements are not approval.
582
+ - **I-07** Every state transition writes exactly one history entry. History is append-only.
583
+ - **I-08** Golden rules are checked before planning/execution; a fire moves a running sprint to \`blocked\`.
584
+ - **I-09** \`state.md\` and \`context.md\` are never authoritative.
585
+ - **I-10** Only \`approved\` knowledge informs planning.
586
+ - **I-11** Vault values never appear in history, knowledge, backlog, reviews, commits, or logs.
587
+ - **I-12** Every artifact carries \`id\`, \`kind\`, \`status\`, \`created\`, \`updated\`, \`method\` frontmatter.
588
+ - **I-13** Dossier updates record the source commit hash for drift detection.
589
+ - **I-14** Quick tasks may execute with \`Quick Tasks: allow\`, never bypassing golden rules.
590
+ - **I-15** Rejected knowledge is preserved in \`rejected\` state to prevent re-proposing.
591
+
592
+ ## Configuration
593
+
594
+ ${configSection}
595
+
596
+ ## Token Policy
597
+
598
+ ${tokenSection}
599
+
600
+ ## Approved Knowledge
601
+
602
+ ${approvedSection}
603
+
604
+ ## Sources
605
+
606
+ - Golden rules: \`.scrumrun/golden-rules.md\`
607
+ - Config: \`.scrumrun/config.md\`
608
+ - Token policy: \`.scrumrun/token-policy.md\`
609
+ - Knowledge: \`.scrumrun/knowledge.md\` (Approved section only)
610
+ - State: \`.scrumrun/state.md\`
611
+ `;
612
+ }
613
+
614
+ function renderState(cwd) {
615
+ const name = path.basename(cwd);
616
+ const project = readIfExists(path.join(cwd, ".scrumrun", "project.md")).trim();
617
+ const sprint = readIfExists(path.join(cwd, ".scrumrun", "goals", "main", "sprint.md"));
618
+ const history = readIfExists(path.join(cwd, ".scrumrun", "goals", "main", "history.md"));
619
+ const decisions = readIfExists(path.join(cwd, ".scrumrun", "goals", "main", "decisions.md"));
620
+ const backlog = readIfExists(path.join(cwd, ".scrumrun", "backlog.md")).trim();
621
+
622
+ const projectSection = project
623
+ ? project.replace(/^# .*\n/m, "").trim()
624
+ : "Pending: set with `/sc-plan goal --set <goal>`.";
625
+ const goalSection = sprint
626
+ ? sprint.split("\n").slice(0, 8).join("\n").trim() || "Pending: define with `/sc-plan goal --set <goal>`."
627
+ : "Pending: define with `/sc-plan goal --set <goal>`.";
628
+ const historyEntries = [
629
+ ...[...history.matchAll(/^## (\d{4}-\d{2}-\d{2}[^\n]* · [^\n]+)$/gm)].map((m) => ({ index: m.index, label: m[1] })),
630
+ ...[...history.matchAll(/^### ([^\n]+)\n(?:(?!^### |^## )[\s\S])*?^Status:\s*[a-z]+/gim)].map((m) => ({ index: m.index, label: m[1] }))
631
+ ].sort((a, b) => a.index - b.index);
632
+ const recentHistory = historyEntries
633
+ .slice(-5)
634
+ .reverse()
635
+ .map((entry) => `- ${entry.label}`)
636
+ .join("\n") || "- None.";
637
+ const parsedDecisions = parseDecisions(cwd);
638
+ const openList = parsedDecisions.open.length
639
+ ? parsedDecisions.open
640
+ .map((decision) => `- ${decision.id ? `${decision.id} — ` : ""}${decision.title}`)
641
+ .join("\n")
642
+ : "- None.";
643
+ const backlogSection = backlog && backlog.includes("- [")
644
+ ? backlog
645
+ .split("\n")
646
+ .filter((line) => line.match(/^- \[[ xX]\]/))
647
+ .map((line) => `- ${line.replace(/^- \[[ xX]\]\s*/, "")}`)
648
+ .join("\n") || "- None."
649
+ : "- None.";
650
+
651
+ return `# ScrumRun State - ${name}
652
+
653
+ Single-file view of project state. Generated by \`scrumrun guardrails --build\`. The source files listed under \`Sources\` are canonical; this file is a pre-digested snapshot for token economy.
654
+
655
+ Last built: ${today()}
656
+ Built by: CLI
657
+
658
+ ## Project
659
+
660
+ ${projectSection}
661
+
662
+ ## Main Goal
663
+
664
+ ${goalSection}
665
+
666
+ ## Active Sprint
667
+
668
+ - Pending: confirm with /sc-plan sprint --status.
669
+
670
+ ## Open Decisions
671
+
672
+ ${openList}
673
+
674
+ ## Backlog
675
+
676
+ ${backlogSection}
677
+
678
+ ## Recent History
679
+
680
+ ${recentHistory}
681
+
682
+ ## Sources
683
+
684
+ - Project: \`.scrumrun/project.md\`
685
+ - Sprint: \`.scrumrun/goals/main/sprint.md\`
686
+ - History: \`.scrumrun/goals/main/history.md\`
687
+ - Decisions: \`.scrumrun/goals/main/decisions.md\`
688
+ - Backlog: \`.scrumrun/backlog.md\`
689
+ - Knowledge: \`.scrumrun/knowledge.md\`
690
+ `;
691
+ }
692
+
693
+ function runGuardrails(parts) {
694
+ const cwd = process.cwd();
695
+ const action = parts[0];
696
+ if (!action || action === "build" || action === "--build" || action === "-b") {
697
+ const guardrailsFile = path.join(cwd, ".scrumrun", "guardrails.md");
698
+ const stateFile = path.join(cwd, ".scrumrun", "state.md");
699
+ if (!fs.existsSync(path.join(cwd, ".scrumrun"))) {
700
+ console.error("Not a ScrumRun project. Run `scrumrun init` first.");
701
+ process.exitCode = 1;
702
+ return;
703
+ }
704
+ const existingGuardrails = readIfExists(guardrailsFile);
705
+ const canonicalGuardrails = /^# ScrumRun Project Guardrails\b/m.test(existingGuardrails);
706
+ if (!canonicalGuardrails) writeFile(guardrailsFile, renderGuardrails(cwd));
707
+ writeFile(stateFile, renderState(cwd));
708
+ if (canonicalGuardrails) {
709
+ console.log(`Preserved canonical ${path.relative(cwd, guardrailsFile)} and built ${path.relative(cwd, stateFile)}.`);
710
+ } else {
711
+ console.log(`Built ${path.relative(cwd, guardrailsFile)} and ${path.relative(cwd, stateFile)}.`);
712
+ console.log("Sources remain canonical. Re-run after edits to golden-rules, config, knowledge, or sprint/history.");
713
+ }
714
+ } else if (action === "show" || action === "--show" || action === "-s") {
715
+ const file = path.join(cwd, ".scrumrun", "guardrails.md");
716
+ console.log(readIfExists(file) || "guardrails.md not built. Run `scrumrun guardrails --build`.");
717
+ } else {
718
+ console.error("Usage: scrumrun guardrails [--build|--show]");
719
+ process.exitCode = 1;
720
+ }
721
+ }
722
+
430
723
  function removePendingNone(content) {
431
724
  return content.replace(/^- Pending: none\.\n?/gm, "").replace(/\n{3,}/g, "\n\n");
432
725
  }
@@ -551,6 +844,11 @@ function runVault(parts) {
551
844
  }
552
845
 
553
846
  function addBacklogItem(label) {
847
+ if (containsSecret(label)) {
848
+ console.error("Secret-like content is forbidden outside the local vault.");
849
+ process.exitCode = 1;
850
+ return;
851
+ }
554
852
  if (!label) {
555
853
  console.error("Usage: scrumrun backlog add <sprint number or name>");
556
854
  process.exitCode = 1;
@@ -634,6 +932,11 @@ function addKnowledge(topic) {
634
932
  process.exitCode = 1;
635
933
  return;
636
934
  }
935
+ if (containsSecret(topic)) {
936
+ console.error("Secret-like content is forbidden outside the local vault.");
937
+ process.exitCode = 1;
938
+ return;
939
+ }
637
940
  const file = ensureKnowledgeFile(process.cwd());
638
941
  const current = readIfExists(file);
639
942
  const id = nextKnowledgeId(current);
@@ -772,38 +1075,12 @@ function runBacklog(parts) {
772
1075
  }
773
1076
 
774
1077
  function commandList() {
775
- console.log(`Slash commands installed by ScrumRun:
776
-
777
- Project:
778
- /sc-help
779
- /sc-init
780
- /sc-uninstall
781
- /sc-update
782
- /sc-study
783
- /sc-intake
784
- /sc-challenge
785
-
786
- Knowledge and planning:
787
- /sc-vault
788
- /sc-know
789
- /sc-context
790
- /sc-goal
791
- /sc-feature
792
- /sc-sprint
793
- /sc-backlog
794
-
795
- Project controls:
796
- /sc-config
797
- /sc-golden
798
- /sc-context
799
- /sc-map
800
- /sc-agent
801
- /sc-review
802
- /sc-decisions
1078
+ console.log(`${renderCommandHelp()}
803
1079
 
804
1080
  CLI helpers:
805
1081
  scrumrun status
806
1082
  scrumrun commands
1083
+ scrumrun sc knowledge decision --list
807
1084
  scrumrun vault add "NAME" "local-dev-value"
808
1085
  scrumrun context show
809
1086
  scrumrun backlog add "Sprint 01"
@@ -813,6 +1090,300 @@ CLI helpers:
813
1090
  `);
814
1091
  }
815
1092
 
1093
+ function printRouteHelp(route) {
1094
+ if (route.level === "root") {
1095
+ commandList();
1096
+ } else if (route.level === "noun") {
1097
+ console.log(`# /sc ${route.noun}`);
1098
+ console.log(route.spec.description);
1099
+ for (const [subject, actions] of Object.entries(route.spec.subjects)) {
1100
+ console.log(`- ${subject}: ${actions.join(", ")}`);
1101
+ }
1102
+ } else {
1103
+ console.log(`# /sc ${route.noun} ${route.subject}`);
1104
+ console.log(route.actions.join("\n"));
1105
+ }
1106
+ }
1107
+
1108
+ function agentWorkflowRequired(noun, subject, args) {
1109
+ const suffix = args.length ? ` ${args.join(" ")}` : "";
1110
+ console.log(`Agent workflow: /sc ${noun} ${subject}${suffix}`);
1111
+ console.log("Run this through an installed ScrumRun AI client; the CLI parser validated the route but this workflow requires repository reasoning.");
1112
+ }
1113
+
1114
+ function v2Project(cwd = process.cwd()) {
1115
+ const marker = path.join(cwd, ".scrumrun", "method.json");
1116
+ if (!fs.existsSync(marker)) return false;
1117
+ try {
1118
+ return JSON.parse(fs.readFileSync(marker, "utf8")).method === "2.0.0";
1119
+ } catch {
1120
+ return false;
1121
+ }
1122
+ }
1123
+
1124
+ function optionValues(args, flag) {
1125
+ const values = [];
1126
+ for (let index = 0; index < args.length; index++) {
1127
+ if (args[index] === flag && args[index + 1] && !args[index + 1].startsWith("--")) values.push(args[index + 1]);
1128
+ }
1129
+ return values;
1130
+ }
1131
+
1132
+ function optionValue(args, flag, fallback = null) {
1133
+ const values = optionValues(args, flag);
1134
+ return values.length ? values[values.length - 1] : fallback;
1135
+ }
1136
+
1137
+ function positionalValues(args, start = 1) {
1138
+ const values = [];
1139
+ const valued = new Set(["--title", "--content", "--evidence", "--relation", "--subject", "--source", "--source-id", "--confidence", "--valid-from", "--valid-until", "--review-trigger", "--note", "--limit"]);
1140
+ for (let index = start; index < args.length; index++) {
1141
+ if (valued.has(args[index])) {
1142
+ index++;
1143
+ continue;
1144
+ }
1145
+ if (!args[index].startsWith("--")) values.push(args[index]);
1146
+ }
1147
+ return values;
1148
+ }
1149
+
1150
+ function memoryOptions(args) {
1151
+ const positional = positionalValues(args).join(" ").trim();
1152
+ const subject = optionValue(args, "--subject");
1153
+ const separator = subject ? subject.indexOf(":") : -1;
1154
+ return {
1155
+ title: optionValue(args, "--title") || positional,
1156
+ content: optionValue(args, "--content") || positional || optionValue(args, "--title"),
1157
+ evidence: optionValues(args, "--evidence"),
1158
+ relations: optionValues(args, "--relation"),
1159
+ subjectType: separator > 0 ? subject.slice(0, separator) : null,
1160
+ subjectId: separator > 0 ? subject.slice(separator + 1) : subject,
1161
+ sourceType: optionValue(args, "--source", "human"),
1162
+ sourceId: optionValue(args, "--source-id"),
1163
+ confidence: optionValue(args, "--confidence"),
1164
+ validFrom: optionValue(args, "--valid-from"),
1165
+ validUntil: optionValue(args, "--valid-until"),
1166
+ reviewTrigger: optionValue(args, "--review-trigger")
1167
+ };
1168
+ }
1169
+
1170
+ function printMemoryArtifact(artifact) {
1171
+ if (!artifact) return false;
1172
+ console.log(readIfExists(artifact.file));
1173
+ return true;
1174
+ }
1175
+
1176
+ function runV2Memory(subject, args) {
1177
+ const kind = subject === "fact" ? "knowledge" : subject;
1178
+ const action = args[0];
1179
+ const createAction = kind === "insight" ? "--propose" : "--add";
1180
+ if (action === createAction) {
1181
+ const artifact = createMemory(process.cwd(), kind, memoryOptions(args));
1182
+ console.log(`Created ${artifact.record.status} ${artifact.record.id}: ${path.relative(process.cwd(), artifact.file)}`);
1183
+ return;
1184
+ }
1185
+ if (action === "--list") {
1186
+ const artifacts = listMemory(process.cwd(), kind, { includeInactive: args.includes("--include-inactive") });
1187
+ console.log(artifacts.length
1188
+ ? artifacts.map((artifact) => `${artifact.record.id} | ${artifact.record.status} | ${((artifact.body || "").match(/^# ([^\r\n]+)/m) || [])[1] || artifact.record.id}`).join("\n")
1189
+ : `No ${kind} artifacts.`);
1190
+ return;
1191
+ }
1192
+ if (action === "--show") {
1193
+ if (!printMemoryArtifact(showMemory(process.cwd(), kind, args[1]))) {
1194
+ console.error(`${kind} not found: ${args[1]}`);
1195
+ process.exitCode = 1;
1196
+ }
1197
+ return;
1198
+ }
1199
+ const actionMap = {
1200
+ "--approve": "approve",
1201
+ "--reject": "reject",
1202
+ "--invalidate": "invalidate",
1203
+ "--resolve": "resolve",
1204
+ "--confirm": "confirm",
1205
+ "--refresh": "refresh",
1206
+ "--archive": "archive",
1207
+ "--stale": "stale",
1208
+ "--deprecate": "deprecate"
1209
+ };
1210
+ if (actionMap[action]) {
1211
+ const artifact = transitionMemory(process.cwd(), kind, args[1], actionMap[action], {
1212
+ evidence: optionValues(args.slice(2), "--evidence"),
1213
+ note: optionValue(args.slice(2), "--note")
1214
+ });
1215
+ console.log(`${artifact.record.id}: ${artifact.record.status}.`);
1216
+ return;
1217
+ }
1218
+ throw new Error(`Unsupported ${kind} command: ${action || "missing action"}`);
1219
+ }
1220
+
1221
+ function runSemanticContext(subject, args) {
1222
+ const action = args[0];
1223
+ if (subject === "study") {
1224
+ const result = queryIndex(process.cwd(), args.join(" "));
1225
+ console.log(JSON.stringify(result, null, 2));
1226
+ return;
1227
+ }
1228
+ if (subject === "map") {
1229
+ if (action === "--build") {
1230
+ const built = rebuildIndex(process.cwd());
1231
+ const map = writeMap(process.cwd());
1232
+ console.log(`Built ${path.relative(process.cwd(), built.file)} and ${path.relative(process.cwd(), map.file)}: ${map.nodes} nodes, ${map.edges} edges.`);
1233
+ return;
1234
+ }
1235
+ const mapFile = path.join(process.cwd(), ".scrumrun", "map.md");
1236
+ if (!fs.existsSync(mapFile)) throw new Error("Generated map.md is missing; run /sc knowledge map --build.");
1237
+ const status = indexStatus(process.cwd());
1238
+ if (status.stale) console.warn("WARNING: map.md is stale; rebuild it before relying on the view.");
1239
+ console.log(fs.readFileSync(mapFile, "utf8"));
1240
+ return;
1241
+ }
1242
+ if (action === "--build" || action === "--update") {
1243
+ const built = rebuildIndex(process.cwd());
1244
+ console.log(`Built ${path.relative(process.cwd(), built.file)} from ${built.artifacts} canonical artifacts.`);
1245
+ return;
1246
+ }
1247
+ if (action === "--clear") {
1248
+ const file = indexPath(process.cwd());
1249
+ if (fs.existsSync(file)) fs.rmSync(file, { force: true });
1250
+ console.log("Cleared the disposable semantic index; canonical Markdown was not changed.");
1251
+ return;
1252
+ }
1253
+ if (action === "--show") {
1254
+ const query = args.slice(1).join(" ").trim();
1255
+ console.log(JSON.stringify(query ? queryIndex(process.cwd(), query) : indexStatus(process.cwd()), null, 2));
1256
+ return;
1257
+ }
1258
+ throw new Error(`Unsupported knowledge ${subject} command: ${action || "missing action"}`);
1259
+ }
1260
+
1261
+ function executeRootRoute(route) {
1262
+ const { noun, subject, args: routeArgs } = route;
1263
+ if (noun === "plan" && subject === "intake") {
1264
+ if (routeArgs[0] === "--approve") {
1265
+ const result = approveRequest(process.cwd(), routeArgs[1]);
1266
+ console.log(`${result.status === "already-approved" ? "Already approved" : "Approved"}: ${result.task.id} → ${result.run.id}`);
1267
+ return;
1268
+ }
1269
+ const request = routeArgs[0] === "--request" ? routeArgs.slice(1).join(" ") : routeArgs.join(" ");
1270
+ const plan = planRequest(process.cwd(), request);
1271
+ console.log(`# ScrumRun Intake`);
1272
+ console.log(`State: ${plan.state}`);
1273
+ console.log(`Classification: ${plan.classification.type} (${plan.classification.reason})`);
1274
+ console.log(`Risk: ${plan.risk.level} — ${plan.risk.reasons.join("; ")}`);
1275
+ console.log(`Policy: ${plan.policy.status}`);
1276
+ for (const violation of plan.policy.violations) console.log(`BLOCKED: ${violation}`);
1277
+ for (const warning of plan.context.warnings) console.log(`WARNING: ${warning}`);
1278
+ if (plan.approvalToken) {
1279
+ console.log(`Approval: scrumrun sc plan intake --approve ${plan.approvalToken}`);
1280
+ }
1281
+ return;
1282
+ }
1283
+ if (noun === "plan" && subject === "task" && routeArgs[0] === "--retry") {
1284
+ const result = retryTask(process.cwd(), routeArgs[1]);
1285
+ console.log(`Created retry ${result.run.id} for ${result.task.id} (attempt ${result.run.attempt}).`);
1286
+ return;
1287
+ }
1288
+ if (noun === "plan" && ["task", "run"].includes(subject) && ["--list", "--show"].includes(routeArgs[0])) {
1289
+ const repository = new ArtifactRepository(projectFile());
1290
+ if (routeArgs[0] === "--list") {
1291
+ const artifacts = repository.list(subject).map((artifact) => `${artifact.record.id} | ${artifact.record.status} | ${((artifact.body || "").match(/^# ([^\r\n]+)/m) || [])[1] || artifact.record.id}`);
1292
+ console.log(artifacts.length ? artifacts.join("\n") : `No ${subject} artifacts.`);
1293
+ } else {
1294
+ const artifact = repository.read(subject, routeArgs[1]);
1295
+ if (!artifact) {
1296
+ console.error(`${subject} not found: ${routeArgs[1]}`);
1297
+ process.exitCode = 1;
1298
+ } else {
1299
+ console.log(readIfExists(artifact.file));
1300
+ }
1301
+ }
1302
+ return;
1303
+ }
1304
+ if (noun === "plan" && subject === "run") {
1305
+ const transitions = {
1306
+ "--validate": "validating",
1307
+ "--learn": "learning",
1308
+ "--complete": "completed",
1309
+ "--resume": "executing",
1310
+ "--fail": "failed",
1311
+ "--block": "blocked"
1312
+ };
1313
+ if (transitions[routeArgs[0]]) {
1314
+ const result = transitionRun(process.cwd(), routeArgs[1], transitions[routeArgs[0]], { note: routeArgs.slice(2).join(" ") || null });
1315
+ console.log(`${result.run.id}: ${result.run.status}; ${result.task.id}: ${result.task.status}.`);
1316
+ if (result.learning) {
1317
+ if (result.learning.created.length) console.log(`Learning candidates: ${result.learning.created.join(", ")}.`);
1318
+ for (const warning of result.learning.warnings) console.warn(`WARNING: ${warning}`);
1319
+ }
1320
+ return;
1321
+ }
1322
+ }
1323
+ if (noun === "knowledge" && ["fact", "decision", "insight", "dossier"].includes(subject) && v2Project()) return runV2Memory(subject, routeArgs);
1324
+ if (noun === "knowledge" && ["context", "map", "study"].includes(subject) && v2Project()) return runSemanticContext(subject, routeArgs);
1325
+ if (noun === "knowledge" && subject === "decision") return runDecisions(routeArgs);
1326
+ if (noun === "knowledge" && subject === "fact") return runKnow(routeArgs);
1327
+ if (noun === "knowledge" && subject === "vault") return runVault(routeArgs);
1328
+ if (noun === "knowledge" && subject === "context") return runContext(routeArgs);
1329
+ if (noun === "review" && subject === "artifact" && routeArgs[0] === "--run") {
1330
+ const audit = auditProject(process.cwd());
1331
+ console.log(JSON.stringify(audit, null, 2));
1332
+ if (!audit.passed) process.exitCode = 1;
1333
+ return;
1334
+ }
1335
+ if (noun === "config" && subject === "migrate") return runMigration(routeArgs);
1336
+ if (noun === "config" && subject === "doctor") {
1337
+ const target = ["all", "codex", "opencode", "claude"].includes(routeArgs[0]) ? routeArgs[0] : "all";
1338
+ return doctor(target);
1339
+ }
1340
+ if (noun === "config" && subject === "update") {
1341
+ const target = ["all", "codex", "opencode", "claude"].includes(routeArgs[0]) ? routeArgs[0] : "all";
1342
+ return updateInstallation(target, { migrate: routeArgs.includes("--migrate") });
1343
+ }
1344
+ if (noun === "config" && subject === "init") {
1345
+ const localMode = routeArgs.includes("--local");
1346
+ const sharedMode = routeArgs.includes("--shared");
1347
+ if (localMode && sharedMode) {
1348
+ console.error("Use either --local or --shared, not both.");
1349
+ process.exitCode = 1;
1350
+ return;
1351
+ }
1352
+ return initProject({
1353
+ force: routeArgs.includes("--force"),
1354
+ mode: sharedMode ? "shared" : "local",
1355
+ agentHint: sharedMode || !routeArgs.includes("--no-agent-hint"),
1356
+ lean: routeArgs.includes("--lean")
1357
+ });
1358
+ }
1359
+ if (noun === "config" && subject === "uninstall") return uninstallProject(routeArgs.includes("--force"));
1360
+ if (noun === "config" && subject === "help") return commandList();
1361
+ if (noun === "config" && subject === "project" && routeArgs[0] === "--show") {
1362
+ console.log(readIfExists(projectFile("config.md")) || "Project config is missing.");
1363
+ return;
1364
+ }
1365
+ agentWorkflowRequired(noun, subject, routeArgs);
1366
+ }
1367
+
1368
+ function runRoot(parts) {
1369
+ const route = resolveRoute(parts);
1370
+ if (route.type === "help") return printRouteHelp(route);
1371
+ if (route.type === "error") {
1372
+ console.error(route.message);
1373
+ process.exitCode = 1;
1374
+ return;
1375
+ }
1376
+ return executeRootRoute(route);
1377
+ }
1378
+
1379
+ function runCompatibilityAlias(alias, parts) {
1380
+ const route = resolveAlias(alias, parts);
1381
+ console.warn(`Deprecated: ${alias} now executes /sc ${route.noun} ${route.subject}.`);
1382
+ if (route.note) console.warn(route.note);
1383
+ if (alias === "sc-backlog") return runBacklog(parts);
1384
+ return executeRootRoute({ type: "route", noun: route.noun, subject: route.subject, args: route.args });
1385
+ }
1386
+
816
1387
  function printCore({ pathOnly = false, promptOnly = false } = {}) {
817
1388
  const coreFile = path.join(root, "CORE.md");
818
1389
  if (pathOnly) {
@@ -820,10 +1391,10 @@ function printCore({ pathOnly = false, promptOnly = false } = {}) {
820
1391
  return;
821
1392
  }
822
1393
  if (promptOnly) {
823
- console.log(`Read AGENTS.md and .scrumrun/core.md before doing anything else.
824
- Follow ScrumRun exactly.
825
- If slash commands are unavailable, execute the equivalent workflow from .scrumrun/core.md manually.
826
- 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.`);
1394
+ console.log(`Read AGENTS.md, .scrumrun/guardrails.md, and .scrumrun/state.md first.
1395
+ Follow ScrumRun 2.0 and load only the canonical ids/evidence relevant to the request.
1396
+ If /sc is unavailable, execute the equivalent /sc <noun> <subject> <action> workflow from .scrumrun/core.md manually.
1397
+ Keep intake read-only; do not create a Task or Run until explicit approval.`);
827
1398
  return;
828
1399
  }
829
1400
  console.log(readIfExists(coreFile));
@@ -871,9 +1442,435 @@ function sprintStatusCounts(history) {
871
1442
  };
872
1443
  }
873
1444
 
1445
+ function decisionsFile(cwd) {
1446
+ return path.join(cwd, ".scrumrun", "goals", "main", "decisions.md");
1447
+ }
1448
+
1449
+ function extractFieldBody(body, label) {
1450
+ const re = new RegExp(`^${label}\\s*\\n([\\s\\S]*?)(?=^\\w[\\w ]*:|^### |^## |$)`, "m");
1451
+ const m = body.match(re);
1452
+ return m ? m[1] : "";
1453
+ }
1454
+
1455
+ function parseListField(text) {
1456
+ if (!text) return [];
1457
+ return text
1458
+ .split(/\n/)
1459
+ .map((line) => line.trim())
1460
+ .filter((line) => line.startsWith("- "))
1461
+ .map((line) => line.slice(2).trim());
1462
+ }
1463
+
1464
+ function escapeRegExp(value) {
1465
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1466
+ }
1467
+
1468
+ function markdownSection(content, title) {
1469
+ const heading = new RegExp(`^## ${escapeRegExp(title)}[ \\t]*$`, "m");
1470
+ const match = heading.exec(content);
1471
+ if (!match) return null;
1472
+ const bodyStart = match.index + match[0].length;
1473
+ const tail = content.slice(bodyStart);
1474
+ const next = /^## [^\n]+$/m.exec(tail);
1475
+ const end = next ? bodyStart + next.index : content.length;
1476
+ return {
1477
+ headingStart: match.index,
1478
+ bodyStart,
1479
+ end,
1480
+ body: content.slice(bodyStart, end)
1481
+ };
1482
+ }
1483
+
1484
+ function decisionBlocks(section) {
1485
+ const headings = [...section.body.matchAll(/^### (D-\d{3}) - ([^\r\n]+)[ \t]*$/gm)];
1486
+ return headings.map((heading, index) => {
1487
+ const localStart = heading.index;
1488
+ const localEnd = index + 1 < headings.length ? headings[index + 1].index : section.body.length;
1489
+ const rawWithSpacing = section.body.slice(localStart, localEnd);
1490
+ const raw = rawWithSpacing.trimEnd();
1491
+ return {
1492
+ id: heading[1],
1493
+ title: heading[2].trim(),
1494
+ body: raw.slice(heading[0].length),
1495
+ raw,
1496
+ start: section.bodyStart + localStart,
1497
+ end: section.bodyStart + localStart + raw.length,
1498
+ maskStart: localStart,
1499
+ maskEnd: localEnd
1500
+ };
1501
+ });
1502
+ }
1503
+
1504
+ function parseDecisionsContent(content, file = null) {
1505
+ const result = { open: [], resolved: [], file };
1506
+ for (const [title, target] of [["Open Decisions", "open"], ["Resolved Decisions", "resolved"]]) {
1507
+ const section = markdownSection(content, title);
1508
+ if (!section) continue;
1509
+ const blocks = decisionBlocks(section);
1510
+ for (const block of blocks) {
1511
+ const { id, title: decisionTitle, body } = block;
1512
+ const statusMatch = body.match(/^Status:\s*(.+)$/m);
1513
+ const dateMatch = body.match(/^Date:\s*(.+)$/m);
1514
+ const sourceMatch = body.match(/^Source:\s*(.+)$/m);
1515
+ const extendsMatch = body.match(/^Extends:\s*(D-\d{3})/m);
1516
+ const supersedesMatch = body.match(/^Supersedes:\s*(D-\d{3})/m);
1517
+ const validWhileText = extractFieldBody(body, "Valid while:");
1518
+ const reviewWhenText = extractFieldBody(body, "Review when:");
1519
+ const expiredMatch = body.match(/^Expired:\s*(.+)$/m);
1520
+ result[target].push({
1521
+ id,
1522
+ title: decisionTitle,
1523
+ status: (statusMatch ? statusMatch[1].trim() : "open").toLowerCase(),
1524
+ date: dateMatch ? dateMatch[1].trim() : null,
1525
+ source: sourceMatch ? sourceMatch[1].trim() : null,
1526
+ extendsId: extendsMatch ? extendsMatch[1] : null,
1527
+ supersedesId: supersedesMatch ? supersedesMatch[1] : null,
1528
+ validWhile: parseListField(validWhileText),
1529
+ reviewWhen: parseListField(reviewWhenText),
1530
+ expired: expiredMatch ? expiredMatch[1].trim() : null,
1531
+ raw: block.raw,
1532
+ start: block.start,
1533
+ end: block.end
1534
+ });
1535
+ }
1536
+
1537
+ const withoutStructuredBlocks = [...section.body];
1538
+ for (const block of blocks) {
1539
+ for (let i = block.maskStart; i < block.maskEnd; i++) {
1540
+ if (withoutStructuredBlocks[i] !== "\n" && withoutStructuredBlocks[i] !== "\r") {
1541
+ withoutStructuredBlocks[i] = " ";
1542
+ }
1543
+ }
1544
+ }
1545
+ const bullets = [...withoutStructuredBlocks.join("").matchAll(/^- (.+)$/gm)]
1546
+ .map((match) => match[1].trim())
1547
+ .filter((text) => text !== "Pending: none.");
1548
+ for (const bullet of bullets) {
1549
+ result[target].push({
1550
+ id: null,
1551
+ title: bullet,
1552
+ status: target === "resolved" ? "resolved" : "open",
1553
+ date: null,
1554
+ source: null,
1555
+ extendsId: null,
1556
+ supersedesId: null,
1557
+ validWhile: [],
1558
+ reviewWhen: [],
1559
+ expired: null,
1560
+ raw: null,
1561
+ start: null,
1562
+ end: null
1563
+ });
1564
+ }
1565
+ }
1566
+ return result;
1567
+ }
1568
+
1569
+ function parseDecisions(cwd) {
1570
+ const file = decisionsFile(cwd);
1571
+ return parseDecisionsContent(readIfExists(file), file);
1572
+ }
1573
+
1574
+ function nextDecisionId(cwd) {
1575
+ const { open, resolved } = parseDecisions(cwd);
1576
+ const all = [...open, ...resolved];
1577
+ const max = all.reduce((acc, d) => {
1578
+ if (!d.id) return acc;
1579
+ const n = Number(d.id.slice(2));
1580
+ return Number.isFinite(n) && n > acc ? n : acc;
1581
+ }, 0);
1582
+ return `D-${String(max + 1).padStart(3, "0")}`;
1583
+ }
1584
+
1585
+ function tokenize(text) {
1586
+ return text
1587
+ .toLowerCase()
1588
+ .replace(/[^a-z0-9\s-]/g, " ")
1589
+ .split(/\s+/)
1590
+ .filter((t) => t.length >= 3);
1591
+ }
1592
+
1593
+ function decisionSimilarity(a, b) {
1594
+ const aTokens = new Set(tokenize(a));
1595
+ const bTokens = new Set(tokenize(b));
1596
+ if (!aTokens.size || !bTokens.size) return 0;
1597
+ let inter = 0;
1598
+ for (const t of aTokens) if (bTokens.has(t)) inter++;
1599
+ const union = new Set([...aTokens, ...bTokens]).size;
1600
+ return union ? inter / union : 0;
1601
+ }
1602
+
1603
+ function findSimilarDecisions(text, decisions, { threshold = 0.3, limit = 5 } = {}) {
1604
+ const matches = [];
1605
+ for (const list of [decisions.open, decisions.resolved]) {
1606
+ for (const d of list) {
1607
+ const score = decisionSimilarity(text, d.title);
1608
+ if (score >= threshold) {
1609
+ matches.push({ ...d, score });
1610
+ }
1611
+ }
1612
+ }
1613
+ matches.sort((a, b) => b.score - a.score);
1614
+ return matches.slice(0, limit);
1615
+ }
1616
+
1617
+ function serializeDecision(d) {
1618
+ const lines = [`### ${d.id} - ${d.title}`, `Status: ${d.status}`];
1619
+ if (d.date) lines.push(`Date: ${d.date}`);
1620
+ if (d.source) lines.push(`Source: ${d.source}`);
1621
+ if (d.extendsId) lines.push(`Extends: ${d.extendsId}`);
1622
+ if (d.supersedesId) lines.push(`Supersedes: ${d.supersedesId}`);
1623
+ if (d.validWhile && d.validWhile.length) {
1624
+ lines.push("Valid while:", ...d.validWhile.map((v) => `- ${v}`));
1625
+ }
1626
+ if (d.reviewWhen && d.reviewWhen.length) {
1627
+ lines.push("Review when:", ...d.reviewWhen.map((v) => `- ${v}`));
1628
+ }
1629
+ if (d.expired) lines.push(`Expired: ${d.expired}`);
1630
+ return lines.join("\n");
1631
+ }
1632
+
1633
+ function decisionDocument(content) {
1634
+ if (content.trim()) return content;
1635
+ return "# Main Goal Decisions\n\n## Open Decisions\n\n- Pending: none.\n\n## Resolved Decisions\n\n- Pending: none.\n";
1636
+ }
1637
+
1638
+ function prependDecisionBlock(content, sectionTitle, block) {
1639
+ const section = markdownSection(content, sectionTitle);
1640
+ if (!section) {
1641
+ return `${content.trimEnd()}\n\n## ${sectionTitle}\n\n${block.trim()}\n`;
1642
+ }
1643
+ const withoutPlaceholder = section.body.replace(/^[ \t]*- Pending: none\.[ \t]*(?:\r?\n)?/m, "");
1644
+ const remainder = withoutPlaceholder.replace(/^(?:\r?\n)+/, "");
1645
+ const body = `\n\n${block.trim()}\n${remainder.trim() ? `\n${remainder}` : ""}`;
1646
+ return `${content.slice(0, section.bodyStart)}${body}${content.slice(section.end)}`;
1647
+ }
1648
+
1649
+ function ensureDecisionSectionPlaceholder(content, sectionTitle) {
1650
+ const section = markdownSection(content, sectionTitle);
1651
+ if (!section || section.body.trim()) return content;
1652
+ return `${content.slice(0, section.bodyStart)}\n\n- Pending: none.\n\n${content.slice(section.end)}`;
1653
+ }
1654
+
1655
+ function updateDecisionStatus(block, status) {
1656
+ if (/^Status:\s*.+$/m.test(block)) {
1657
+ return block.replace(/^Status:\s*.+$/m, `Status: ${status}`);
1658
+ }
1659
+ const newline = block.indexOf("\n");
1660
+ if (newline === -1) return `${block}\nStatus: ${status}`;
1661
+ return `${block.slice(0, newline + 1)}Status: ${status}\n${block.slice(newline + 1)}`;
1662
+ }
1663
+
1664
+ function appendDecisionHistory(cwd, id) {
1665
+ const file = path.join(cwd, ".scrumrun", "goals", "main", "history.md");
1666
+ const marker = `${id} · open-to-resolved`;
1667
+ const current = readIfExists(file) || "# Main Goal History\n";
1668
+ if (current.includes(marker)) return;
1669
+ const event = `## ${today()} · ${marker}\n\nOwner: ScrumRun CLI\nReason: Decision resolved through \`scrumrun decisions --resolve\`.\nStatus: completed.\n`;
1670
+ writeFile(file, `${current.trimEnd()}\n\n${event}`, { backup: true });
1671
+ }
1672
+
1673
+ function addDecision(cwd, title, { date = today(), source = null, validWhile = [], reviewWhen = [], extendsId = null, supersedesId = null } = {}) {
1674
+ if (containsSecret([title, source, ...validWhile, ...reviewWhen].filter(Boolean).join("\n"))) {
1675
+ throw new Error("Secret-like content is forbidden outside the local vault.");
1676
+ }
1677
+ const file = decisionsFile(cwd);
1678
+ const id = nextDecisionId(cwd);
1679
+ const block = serializeDecision({
1680
+ id,
1681
+ title: title.trim(),
1682
+ status: "open",
1683
+ date,
1684
+ source,
1685
+ extendsId,
1686
+ supersedesId,
1687
+ validWhile,
1688
+ reviewWhen,
1689
+ expired: null
1690
+ });
1691
+ const current = decisionDocument(readIfExists(file));
1692
+ const next = prependDecisionBlock(current, "Open Decisions", block);
1693
+ const parsed = parseDecisionsContent(next, file);
1694
+ if (parsed.open.filter((decision) => decision.id === id).length !== 1) {
1695
+ throw new Error(`Decision write validation failed for ${id}.`);
1696
+ }
1697
+ writeFile(file, next, { backup: true });
1698
+ return id;
1699
+ }
1700
+
1701
+ function resolveDecision(cwd, id) {
1702
+ const file = decisionsFile(cwd);
1703
+ const current = readIfExists(file);
1704
+ const decisions = parseDecisionsContent(current, file);
1705
+ const moved = decisions.open.find((decision) => decision.id === id && decision.raw);
1706
+ if (!moved) return false;
1707
+
1708
+ let next = `${current.slice(0, moved.start)}${current.slice(moved.end)}`;
1709
+ next = ensureDecisionSectionPlaceholder(next, "Open Decisions");
1710
+ next = prependDecisionBlock(next, "Resolved Decisions", updateDecisionStatus(moved.raw, "resolved"));
1711
+
1712
+ const parsed = parseDecisionsContent(next, file);
1713
+ const stillOpen = parsed.open.some((decision) => decision.id === id);
1714
+ const resolvedCount = parsed.resolved.filter((decision) => decision.id === id).length;
1715
+ if (stillOpen || resolvedCount !== 1) {
1716
+ throw new Error(`Decision transition validation failed for ${id}.`);
1717
+ }
1718
+ writeFile(file, next, { backup: true });
1719
+ appendDecisionHistory(cwd, id);
1720
+ return true;
1721
+ }
1722
+
1723
+ function runDecisions(parts) {
1724
+ const cwd = process.cwd();
1725
+ const action = parts[0];
1726
+
1727
+ if (!action || action === "list" || action === "--list" || action === "-l") {
1728
+ const decisions = parseDecisions(cwd);
1729
+ const format = (d) => `${d.id || "no-id"} | ${d.status} | ${d.title}${d.expired ? ` (expired: ${d.expired})` : ""}`;
1730
+ console.log("# Open");
1731
+ console.log(decisions.open.length ? decisions.open.map(format).join("\n") : " - none");
1732
+ console.log("\n# Resolved");
1733
+ console.log(decisions.resolved.length ? decisions.resolved.map(format).join("\n") : " - none");
1734
+ return;
1735
+ }
1736
+
1737
+ if (action === "similar" || action === "--similar" || action === "-s") {
1738
+ const text = joinArgs(parts.slice(1));
1739
+ if (!text) {
1740
+ console.error("Usage: scrumrun decisions --similar <text>");
1741
+ process.exitCode = 1;
1742
+ return;
1743
+ }
1744
+ const decisions = parseDecisions(cwd);
1745
+ const matches = findSimilarDecisions(text, decisions);
1746
+ if (!matches.length) {
1747
+ console.log("No similar decisions found.");
1748
+ return;
1749
+ }
1750
+ for (const m of matches) {
1751
+ const pct = Math.round(m.score * 100);
1752
+ const valid = m.expired ? `expired (${m.expired})` : m.status;
1753
+ console.log(`${m.id || "no-id"} | ${valid} | ${pct}% similar | ${m.title}`);
1754
+ }
1755
+ return;
1756
+ }
1757
+
1758
+ if (action === "add" || action === "--add" || action === "-a") {
1759
+ const sourceIdx = parts.findIndex((p) => p === "--source" || p === "-s");
1760
+ const source = sourceIdx > 0 ? parts[sourceIdx + 1] : null;
1761
+ const title = joinArgs(parts.slice(1).filter((p, i, arr) => {
1762
+ if (p === "--source" || p === "-s") return false;
1763
+ if (i > 0 && (arr[i - 1] === "--source" || arr[i - 1] === "-s")) return false;
1764
+ return true;
1765
+ }));
1766
+ if (!title) {
1767
+ console.error("Usage: scrumrun decisions --add <title> [--source <ref>]");
1768
+ process.exitCode = 1;
1769
+ return;
1770
+ }
1771
+ const decisions = parseDecisions(cwd);
1772
+ const similar = findSimilarDecisions(title, decisions);
1773
+ const id = addDecision(cwd, title, source ? { source } : {});
1774
+ console.log(`Added decision: ${id}`);
1775
+ if (similar.length) {
1776
+ console.log("\nSimilar decisions found — review before relying on this new one:");
1777
+ for (const m of similar) {
1778
+ const pct = Math.round(m.score * 100);
1779
+ console.log(` ${m.id || "no-id"} | ${m.status} | ${pct}% similar | ${m.title}`);
1780
+ }
1781
+ console.log("\nIf the new decision reuses an existing one, edit and add Extends: D-NNN.");
1782
+ console.log("If it supersedes, add Supersedes: D-NNN.");
1783
+ console.log("If it's genuinely new, ignore this list.");
1784
+ }
1785
+ return;
1786
+ }
1787
+
1788
+ if (action === "resolve" || action === "--resolve" || action === "-r") {
1789
+ const id = parts[1];
1790
+ if (!id) {
1791
+ console.error("Usage: scrumrun decisions --resolve <D-NNN>");
1792
+ process.exitCode = 1;
1793
+ return;
1794
+ }
1795
+ if (resolveDecision(cwd, id)) {
1796
+ console.log(`Resolved decision: ${id}`);
1797
+ } else {
1798
+ console.error(`Open decision not found: ${id}`);
1799
+ process.exitCode = 1;
1800
+ }
1801
+ return;
1802
+ }
1803
+
1804
+ if (action === "check" || action === "--check" || action === "-c") {
1805
+ const id = parts[1];
1806
+ if (!id) {
1807
+ console.error("Usage: scrumrun decisions --check <D-NNN>");
1808
+ process.exitCode = 1;
1809
+ return;
1810
+ }
1811
+ const decisions = parseDecisions(cwd);
1812
+ const all = [...decisions.open, ...decisions.resolved];
1813
+ const d = all.find((x) => x.id === id);
1814
+ if (!d) {
1815
+ console.error(`Decision not found: ${id}`);
1816
+ process.exitCode = 1;
1817
+ return;
1818
+ }
1819
+ console.log(`# ${d.id} - ${d.title}`);
1820
+ console.log(`Status: ${d.status}${d.expired ? ` (expired: ${d.expired})` : ""}`);
1821
+ if (d.date) console.log(`Date: ${d.date}`);
1822
+ if (d.source) console.log(`Source: ${d.source}`);
1823
+ if (d.extendsId) console.log(`Extends: ${d.extendsId}`);
1824
+ if (d.supersedesId) console.log(`Supersedes: ${d.supersedesId}`);
1825
+ if (d.validWhile.length) {
1826
+ console.log(`\nValid while:`);
1827
+ for (const v of d.validWhile) console.log(` - ${v}`);
1828
+ }
1829
+ if (d.reviewWhen.length) {
1830
+ console.log(`\nReview when:`);
1831
+ for (const v of d.reviewWhen) console.log(` - ${v}`);
1832
+ }
1833
+ if (d.expired) {
1834
+ console.log(`\nThis decision is marked as expired. Review and either:`);
1835
+ console.log(` - Add Extends: D-NNN to a new decision that supersedes it, or`);
1836
+ console.log(` - Edit and remove the Expired line if it is still valid.`);
1837
+ }
1838
+ return;
1839
+ }
1840
+
1841
+ console.error("Usage: scrumrun decisions [--list|--similar|--add|--resolve|--check]");
1842
+ process.exitCode = 1;
1843
+ }
1844
+
874
1845
  function statusProject() {
875
1846
  const cwd = process.cwd();
876
1847
  const scrumDir = path.join(cwd, ".scrumrun");
1848
+ const methodMarker = readIfExists(path.join(scrumDir, "method.json"));
1849
+ let method = null;
1850
+ try {
1851
+ method = methodMarker ? JSON.parse(methodMarker).method : null;
1852
+ } catch {
1853
+ method = "invalid";
1854
+ }
1855
+ if (method === "2.0.0") {
1856
+ const repository = new ArtifactRepository(scrumDir);
1857
+ console.log(`# ScrumRun Status - ${path.basename(cwd)}`);
1858
+ console.log("");
1859
+ console.log("Method: 2.0.0");
1860
+ console.log(`Guardrails: ${fs.existsSync(path.join(scrumDir, "guardrails.md")) ? "present" : "missing"}`);
1861
+ console.log(`Generated state: ${fs.existsSync(path.join(scrumDir, "state.md")) ? "present" : "missing"}`);
1862
+ console.log("");
1863
+ console.log("## Artifacts");
1864
+ for (const kind of Object.keys(ARTIFACT_TYPES)) {
1865
+ const artifacts = repository.list(kind);
1866
+ const active = artifacts.filter((artifact) => artifact.record && !["completed", "resolved", "rejected", "deprecated", "invalidated", "archived", "cancelled", "passed"].includes(artifact.record.status));
1867
+ console.log(`- ${kind}: ${artifacts.length} total, ${active.length} active`);
1868
+ }
1869
+ console.log("");
1870
+ console.log(`Migration record: ${fs.existsSync(path.join(scrumDir, ".migration", "v1-to-v2", "manifest.json")) ? "v1-to-v2 applied" : "not applicable / native v2"}`);
1871
+ console.log(`Canonical command: /sc`);
1872
+ return;
1873
+ }
877
1874
  const required = [
878
1875
  "core.md",
879
1876
  "config.md",
@@ -927,24 +1924,24 @@ function promptCommand(parts) {
927
1924
  const kind = parts[0];
928
1925
  const text = joinText(parts.slice(1));
929
1926
  const prompts = {
930
- intake: `/sc-intake ${text}`.trim(),
931
- study: `/sc-study${text ? ` ${text}` : ""}`,
932
- challenge: `/sc-challenge ${text}`.trim(),
933
- know: `/sc-know --add ${text}`.trim(),
934
- "context-build": `/sc-context --build${text ? ` ${text}` : ""}`,
935
- "context-update": `/sc-context --update${text ? ` ${text}` : ""}`,
936
- "context-show": `/sc-context --show${text ? ` ${text}` : ""}`,
937
- context: `/sc-context ${text}`.trim(),
938
- vault: `/sc-vault ${text}`.trim(),
939
- "goal-set": `/sc-goal --set ${text}`.trim(),
940
- "goal-new": `/sc-goal --set ${text}`.trim(),
941
- goal: `/sc-goal --set ${text}`.trim(),
942
- "sprint-add": `/sc-sprint --add ${text}`.trim(),
943
- "sprint-new": `/sc-sprint --add ${text}`.trim(),
944
- "sprint-run": `/sc-sprint --run ${text}`.trim(),
945
- "backlog-add": `/sc-backlog --add ${text}`.trim(),
946
- backlog: `/sc-backlog --add ${text}`.trim(),
947
- status: `/sc-sprint --status${text ? ` ${text}` : ""}`
1927
+ intake: `/sc plan intake ${text}`.trim(),
1928
+ study: `/sc knowledge study${text ? ` ${text}` : ""}`,
1929
+ challenge: `/sc plan challenge ${text}`.trim(),
1930
+ know: `/sc knowledge fact --add ${text}`.trim(),
1931
+ "context-build": `/sc knowledge context --build${text ? ` ${text}` : ""}`,
1932
+ "context-update": `/sc knowledge context --update${text ? ` ${text}` : ""}`,
1933
+ "context-show": `/sc knowledge context --show${text ? ` ${text}` : ""}`,
1934
+ context: `/sc knowledge context ${text}`.trim(),
1935
+ vault: `/sc knowledge vault ${text}`.trim(),
1936
+ "goal-set": `/sc plan feature --add ${text}`.trim(),
1937
+ "goal-new": `/sc plan feature --add ${text}`.trim(),
1938
+ goal: `/sc plan feature --add ${text}`.trim(),
1939
+ "sprint-add": `/sc plan sprint --add ${text}`.trim(),
1940
+ "sprint-new": `/sc plan sprint --add ${text}`.trim(),
1941
+ "sprint-run": `/sc plan task --run ${text}`.trim(),
1942
+ "backlog-add": `/sc plan task --add ${text} --status backlog`.trim(),
1943
+ backlog: `/sc plan task --add ${text} --status backlog`.trim(),
1944
+ status: `/sc plan sprint --show${text ? ` ${text}` : ""}`
948
1945
  };
949
1946
  if (!kind || !prompts[kind]) {
950
1947
  console.error("Usage: scrumrun prompt <intake|study|challenge|know|context-build|context-update|vault|goal-set|sprint-add|sprint-run|backlog-add|status> [text]");
@@ -954,20 +1951,22 @@ function promptCommand(parts) {
954
1951
  console.log(prompts[kind]);
955
1952
  }
956
1953
 
957
- function initProject({ force, mode, agentHint }) {
1954
+ function initProject({ force, mode, agentHint, lean }) {
958
1955
  const cwd = process.cwd();
959
1956
  const vars = {
960
1957
  PROJECT_NAME: path.basename(cwd),
961
1958
  DATE: new Date().toISOString().slice(0, 10)
962
1959
  };
963
1960
  const projectTemplate = path.join(templates, "project");
1961
+ const agentTemplate = path.join(templates, lean ? "project-lean" : "project", "AGENTS.md");
964
1962
  const results = [];
965
1963
 
966
1964
  results.push(...copyDir(path.join(projectTemplate, ".scrumrun"), path.join(cwd, ".scrumrun"), { force, vars }));
967
1965
  results.push(copyFile(path.join(root, "CORE.md"), path.join(cwd, ".scrumrun", "core.md"), { force, vars }));
1966
+ results.push(ensureProjectIgnore(cwd));
968
1967
 
969
1968
  if (mode === "shared" || agentHint) {
970
- results.push(copyFile(path.join(projectTemplate, "AGENTS.md"), path.join(cwd, "AGENTS.md"), { force, vars }));
1969
+ results.push(copyFile(agentTemplate, path.join(cwd, "AGENTS.md"), { force, vars }));
971
1970
  } else {
972
1971
  results.push({ status: "skipped", dest: path.join(cwd, "AGENTS.md") });
973
1972
  }
@@ -979,14 +1978,20 @@ function initProject({ force, mode, agentHint }) {
979
1978
  }
980
1979
  }
981
1980
  results.push(addLocalExclude(cwd, ".scrumrun/vault.local.md"));
1981
+ if (v2Project(cwd)) refreshState(path.join(cwd, ".scrumrun"));
982
1982
 
983
- const title = mode === "local"
1983
+ const title = lean
1984
+ ? `Initialized ScrumRun 2.0 project (lean read policy)`
1985
+ : mode === "local"
984
1986
  ? "Initialized local ScrumRun project"
985
1987
  : "Initialized shared ScrumRun project";
986
1988
  printResults(title, results);
987
1989
  if (mode === "local" && hasGitRepo(cwd)) {
988
1990
  console.log("\nLocal mode keeps ScrumRun out of commits by writing .scrumrun/ to .git/info/exclude.");
989
1991
  }
1992
+ if (lean) {
1993
+ console.log("\nLean mode stores complete v2 truth but tells agents to read only guardrails, state, and relevant ids by default.");
1994
+ }
990
1995
  }
991
1996
 
992
1997
  function uninstallProject(force) {
@@ -1015,7 +2020,7 @@ function moveIfPossible(cwd, from, to) {
1015
2020
  return { status: "moved", dest: `${src} -> ${dest}` };
1016
2021
  }
1017
2022
 
1018
- function migrateProject() {
2023
+ function migrateLegacyProject() {
1019
2024
  const cwd = process.cwd();
1020
2025
  const moves = [
1021
2026
  ["asm-config.md", ".scrumrun/config.md"],
@@ -1077,36 +2082,106 @@ function migrateProject() {
1077
2082
  printResults("Migrated project", results.filter((result) => result.status !== "missing"));
1078
2083
  }
1079
2084
 
1080
- function doctor(target = "all") {
2085
+ function runMigration(parts) {
2086
+ if (parts.includes("--legacy-layout")) {
2087
+ console.warn("Deprecated: --legacy-layout only migrates pre-.scrumrun files into the v1 layout.");
2088
+ migrateLegacyProject();
2089
+ return;
2090
+ }
2091
+ const toIndex = parts.indexOf("--to");
2092
+ const target = toIndex >= 0 ? parts[toIndex + 1] : null;
2093
+ const actions = ["--dry-run", "--apply", "--rollback"].filter((action) => parts.includes(action));
2094
+ if (target !== "2" || actions.length !== 1) {
2095
+ console.error("Usage: scrumrun migrate --to 2 [--dry-run|--apply|--rollback]");
2096
+ process.exitCode = 1;
2097
+ return;
2098
+ }
2099
+ try {
2100
+ if (actions[0] === "--dry-run") {
2101
+ const result = dryRunMigration(process.cwd());
2102
+ console.log(result.output.trimEnd());
2103
+ if (result.status === "blocked") {
2104
+ for (const error of result.plan.errors) console.error(`BLOCKED: ${error}`);
2105
+ process.exitCode = 1;
2106
+ }
2107
+ } else if (actions[0] === "--apply") {
2108
+ const result = applyMigration(process.cwd());
2109
+ if (result.status === "already-applied") {
2110
+ console.log("ScrumRun project is already migrated to method 2.0.0; no files changed.");
2111
+ } else {
2112
+ console.log("Applied ScrumRun v1 to v2 migration.");
2113
+ console.log(`Source fingerprint: ${result.manifest.sourceInventory.rootSha256}`);
2114
+ console.log(`Generated artifacts: ${result.manifest.generated.length}`);
2115
+ console.log("Rollback remains available with: scrumrun migrate --to 2 --rollback");
2116
+ }
2117
+ } else {
2118
+ const result = rollbackMigration(process.cwd());
2119
+ console.log("Rolled back ScrumRun v2 migration and restored the exact v1 fingerprint.");
2120
+ console.log(`Restored fingerprint: ${result.rootSha256}`);
2121
+ }
2122
+ } catch (error) {
2123
+ console.error(error.message);
2124
+ process.exitCode = 1;
2125
+ }
2126
+ }
2127
+
2128
+ function doctor(target = "all", { compatibility = false, strict = false } = {}) {
1081
2129
  const home = os.homedir();
1082
2130
  const checks = [];
2131
+ const commands = compatibility ? [...COMMANDS, ...COMPATIBILITY_COMMANDS] : COMMANDS;
2132
+ const skillContent = fs.readFileSync(path.join(templates, "shared", "skills", "scrumrun", "SKILL.md"), "utf8");
2133
+
2134
+ function commandContent(command) {
2135
+ return command === "sc" ? renderRootPrompt() : renderCompatibilityPrompt(command);
2136
+ }
1083
2137
 
1084
2138
  if (target === "all" || target === "codex") {
1085
- for (const command of COMMANDS) {
1086
- checks.push([`Codex prompt ${command}`, path.join(home, ".codex", "prompts", `${command}.md`)]);
2139
+ for (const command of commands) {
2140
+ checks.push([`Codex prompt ${command}`, path.join(home, ".codex", "prompts", `${command}.md`), commandContent(command)]);
1087
2141
  }
1088
- checks.push(["Codex skill scrumrun", path.join(home, ".codex", "skills", "scrumrun", "SKILL.md")]);
2142
+ checks.push(["Codex skill scrumrun", path.join(home, ".codex", "skills", "scrumrun", "SKILL.md"), skillContent]);
1089
2143
  }
1090
2144
 
1091
2145
  if (target === "all" || target === "claude") {
1092
- for (const command of COMMANDS) {
1093
- checks.push([`Claude command ${command}`, path.join(home, ".claude", "commands", `${command}.md`)]);
2146
+ for (const command of commands) {
2147
+ checks.push([`Claude command ${command}`, path.join(home, ".claude", "commands", `${command}.md`), commandContent(command)]);
1094
2148
  }
1095
- checks.push(["Claude skill scrumrun", path.join(home, ".claude", "skills", "scrumrun", "SKILL.md")]);
2149
+ checks.push(["Claude skill scrumrun", path.join(home, ".claude", "skills", "scrumrun", "SKILL.md"), skillContent]);
1096
2150
  }
1097
2151
 
1098
2152
  if (target === "all" || target === "opencode") {
1099
- for (const command of COMMANDS) {
1100
- checks.push([`OpenCode command ${command}`, path.join(home, ".config", "opencode", "commands", `${command}.md`)]);
2153
+ for (const command of commands) {
2154
+ checks.push([`OpenCode command ${command}`, path.join(home, ".config", "opencode", "commands", `${command}.md`), commandContent(command)]);
2155
+ }
2156
+ checks.push(["OpenCode skill scrumrun", path.join(home, ".config", "opencode", "skills", "scrumrun", "SKILL.md"), skillContent]);
2157
+ }
2158
+
2159
+ const [nodeMajor, nodeMinor] = process.versions.node.split(".").map(Number);
2160
+ const nodeOk = nodeMajor > 22 || (nodeMajor === 22 && nodeMinor >= 13);
2161
+ console.log(`${nodeOk ? "ok " : "miss"} Node.js >=22.13.0: ${process.versions.node}`);
2162
+ let ok = nodeOk;
2163
+ for (const [label, file, expected] of checks) {
2164
+ const safeFile = fs.existsSync(file) && fs.lstatSync(file).isFile() && !fs.lstatSync(file).isSymbolicLink();
2165
+ const current = safeFile ? fs.readFileSync(file, "utf8") : null;
2166
+ const matches = safeFile && current === expected;
2167
+ ok = ok && matches;
2168
+ const state = !safeFile ? "miss" : matches ? "ok " : "stale";
2169
+ const hashes = safeFile && !matches
2170
+ ? ` (expected ${crypto.createHash("sha256").update(expected).digest("hex").slice(0, 12)}, found ${crypto.createHash("sha256").update(current).digest("hex").slice(0, 12)})`
2171
+ : "";
2172
+ console.log(`${state} ${label}: ${file}${hashes}`);
2173
+ }
2174
+ if (strict) {
2175
+ const scrumDir = path.join(process.cwd(), ".scrumrun");
2176
+ if (!fs.existsSync(scrumDir)) {
2177
+ ok = false;
2178
+ console.log(`miss ScrumRun project audit: ${scrumDir}`);
2179
+ } else {
2180
+ const audit = auditProject(process.cwd());
2181
+ ok = ok && audit.passed && audit.findings.length === 0;
2182
+ console.log(`${audit.passed && audit.findings.length === 0 ? "ok " : "fail"} ScrumRun project audit: ${audit.findings.length} finding(s)`);
2183
+ for (const item of audit.findings) console.log(` ${item.severity} ${item.code}: ${item.message}`);
1101
2184
  }
1102
- checks.push(["OpenCode skill scrumrun", path.join(home, ".config", "opencode", "skills", "scrumrun", "SKILL.md")]);
1103
- }
1104
-
1105
- let ok = true;
1106
- for (const [label, file] of checks) {
1107
- const exists = fs.existsSync(file);
1108
- ok = ok && exists;
1109
- console.log(`${exists ? "ok " : "miss"} ${label}: ${file}`);
1110
2185
  }
1111
2186
  process.exitCode = ok ? 0 : 1;
1112
2187
  }
@@ -1117,6 +2192,7 @@ const force = args.includes("--force") || args.includes("-f");
1117
2192
  const local = args.includes("--local") || args.includes("-l");
1118
2193
  const shared = args.includes("--shared") || args.includes("-s");
1119
2194
  const noAgentHint = args.includes("--no-agent-hint") || args.includes("-n");
2195
+ const lean = args.includes("--lean");
1120
2196
 
1121
2197
  if (!command || command === "--help" || command === "-h") {
1122
2198
  usage();
@@ -1124,13 +2200,18 @@ if (!command || command === "--help" || command === "-h") {
1124
2200
  console.log(`ScrumRun ${version}`);
1125
2201
  } else if (command === "install" || command === "update") {
1126
2202
  const target = ["all", "codex", "opencode", "claude"].includes(args[1]) ? args[1] : "all";
1127
- install(target, true);
2203
+ if (command === "update") updateInstallation(target, { migrate: args.includes("--migrate") });
2204
+ else install(target, true, { compatibility: false });
2205
+ } else if (command === "sc") {
2206
+ runRoot(args.slice(1));
2207
+ } else if (COMMAND_ALIASES[command]) {
2208
+ runCompatibilityAlias(command, args.slice(1));
1128
2209
  } else if (command === "init") {
1129
2210
  if (local && shared) {
1130
2211
  console.error("Use either --local or --shared, not both.");
1131
2212
  process.exitCode = 1;
1132
2213
  } else {
1133
- initProject({ force, mode: shared ? "shared" : "local", agentHint: shared || !noAgentHint });
2214
+ initProject({ force, mode: shared ? "shared" : "local", agentHint: shared || !noAgentHint, lean });
1134
2215
  }
1135
2216
  } else if (command === "uninstall") {
1136
2217
  uninstallProject(force);
@@ -1140,6 +2221,10 @@ if (!command || command === "--help" || command === "-h") {
1140
2221
  printCore({ pathOnly: args.includes("--path"), promptOnly: args.includes("--prompt") });
1141
2222
  } else if (command === "commands") {
1142
2223
  commandList();
2224
+ } else if (command === "guardrails") {
2225
+ runGuardrails(args.slice(1));
2226
+ } else if (command === "decisions") {
2227
+ runDecisions(args.slice(1));
1143
2228
  } else if (command === "context") {
1144
2229
  runContext(args.slice(1));
1145
2230
  } else if (command === "vault") {
@@ -1157,16 +2242,16 @@ if (!command || command === "--help" || command === "-h") {
1157
2242
  } else if (command === "prompt") {
1158
2243
  promptCommand(args.slice(1));
1159
2244
  } else if (command === "migrate") {
1160
- migrateProject();
2245
+ runMigration(args.slice(1));
1161
2246
  } else if (command === "doctor") {
1162
2247
  const target = ["all", "codex", "opencode", "claude"].includes(args[1]) ? args[1] : "all";
1163
- doctor(target);
2248
+ doctor(target, { compatibility: args.includes("--compat"), strict: args.includes("--strict") });
1164
2249
  } else if (command === "claude") {
1165
2250
  const sub = args[1];
1166
2251
  if (sub === "install" || sub === "update") {
1167
- installClaude(true);
2252
+ installClaude(true, { compatibility: sub === "update" });
1168
2253
  } else if (sub === "doctor") {
1169
- doctor("claude");
2254
+ doctor("claude", { compatibility: args.includes("--compat"), strict: args.includes("--strict") });
1170
2255
  } else {
1171
2256
  console.error("Usage: scrumrun claude [install|update|doctor]");
1172
2257
  process.exitCode = 1;
@@ -1174,9 +2259,9 @@ if (!command || command === "--help" || command === "-h") {
1174
2259
  } else if (command === "codex") {
1175
2260
  const sub = args[1];
1176
2261
  if (sub === "install" || sub === "update") {
1177
- installCodex(true);
2262
+ installCodex(true, { compatibility: sub === "update" });
1178
2263
  } else if (sub === "doctor") {
1179
- doctor("codex");
2264
+ doctor("codex", { compatibility: args.includes("--compat"), strict: args.includes("--strict") });
1180
2265
  } else {
1181
2266
  console.error("Usage: scrumrun codex [install|update|doctor]");
1182
2267
  process.exitCode = 1;
@@ -1184,9 +2269,9 @@ if (!command || command === "--help" || command === "-h") {
1184
2269
  } else if (command === "opencode") {
1185
2270
  const sub = args[1];
1186
2271
  if (sub === "install" || sub === "update") {
1187
- installOpenCode(true);
2272
+ installOpenCode(true, { compatibility: sub === "update" });
1188
2273
  } else if (sub === "doctor") {
1189
- doctor("opencode");
2274
+ doctor("opencode", { compatibility: args.includes("--compat"), strict: args.includes("--strict") });
1190
2275
  } else {
1191
2276
  console.error("Usage: scrumrun opencode [install|update|doctor]");
1192
2277
  process.exitCode = 1;