scrumrun 1.5.1 → 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 +143 -68
  7. package/SPEC.md +248 -257
  8. package/bin/claude-install.js +18 -132
  9. package/bin/scrumrun.js +1263 -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 -6
  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 -6
  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,86 +1,53 @@
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");
6
7
 
7
8
  const root = path.resolve(__dirname, "..");
8
9
  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
- ];
10
+ const { version } = require(path.join(root, "package.json"));
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])];
33
25
 
34
26
  function usage() {
35
27
  console.log(`ScrumRun
36
28
 
37
29
  Usage:
30
+ scrumrun --version
31
+ scrumrun sc
32
+ scrumrun sc <noun> <subject> <action> [args]
38
33
  scrumrun install [all|codex|opencode|claude] [--force]
39
- scrumrun update [all|codex|opencode|claude]
40
- 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]
41
36
  scrumrun status
42
37
  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
38
  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]
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]
64
43
  scrumrun uninstall [--force]
65
- scrumrun migrate
66
- scrumrun doctor [all|codex|opencode|claude]
67
44
 
68
45
  Examples:
69
46
  npx scrumrun@latest install
70
- npx scrumrun@latest install claude
47
+ npx scrumrun@latest sc knowledge decision --list
71
48
  npx scrumrun@latest init
72
- npx scrumrun@latest core
73
49
  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
50
+ npx scrumrun@latest migrate --to 2 --dry-run
84
51
  `);
85
52
  }
86
53
 
@@ -108,9 +75,37 @@ function readIfExists(file) {
108
75
  return fs.existsSync(file) ? fs.readFileSync(file, "utf8") : "";
109
76
  }
110
77
 
111
- function writeFile(file, content) {
78
+ function writeFile(file, content, { backup = false } = {}) {
112
79
  ensureDir(path.dirname(file));
113
- 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 };
114
109
  }
115
110
 
116
111
  function projectFile(...parts) {
@@ -178,7 +173,8 @@ function cleanupLegacy(commandsDir, skillsDir) {
178
173
  const name = entry.name;
179
174
  const legacyPrefix = name.startsWith("srun-") || name.startsWith("asm-") || name.startsWith("sr-") || name.startsWith("scr-") || name.startsWith("src-") || name.startsWith("run-");
180
175
  const oldConsolidated = /^scr-[a-z]+-[a-z]+\.md$/.test(name);
181
- if (legacyPrefix || oldConsolidated) {
176
+ const managed = MANAGED_COMMANDS.some((command) => name === `${command}.md`);
177
+ if (legacyPrefix || oldConsolidated || managed) {
182
178
  fs.rmSync(path.join(commandsDir, name), { force: true });
183
179
  console.log(` rm legacy ${path.join(commandsDir, name)}`);
184
180
  }
@@ -191,53 +187,124 @@ function cleanupLegacy(commandsDir, skillsDir) {
191
187
  }
192
188
  }
193
189
 
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"));
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);
198
199
  const results = [
199
- ...copyDir(path.join(templates, "codex", "prompts"), commandsDir, { force }),
200
- ...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 })
201
202
  ];
202
- 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");
203
212
  }
204
213
 
205
- function installCodex(force) {
214
+ function installClaude(force, options = {}) {
206
215
  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);
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
+ );
214
224
  }
215
225
 
216
- function installOpenCode(force) {
226
+ function installCodex(force, options = {}) {
217
227
  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);
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
+ );
225
236
  }
226
237
 
227
- 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 = {}) {
228
251
  if (target === "all") {
229
- installCodex(force);
230
- installClaude(force);
231
- installOpenCode(force);
252
+ installCodex(force, options);
253
+ installClaude(force, options);
254
+ installOpenCode(force, options);
232
255
  } else if (target === "codex") {
233
- installCodex(force);
256
+ installCodex(force, options);
234
257
  } else if (target === "claude") {
235
- installClaude(force);
258
+ installClaude(force, options);
236
259
  } else if (target === "opencode") {
237
- 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" };
238
299
  }
239
300
  }
240
301
 
302
+ function updateInstallation(target, { migrate = false } = {}) {
303
+ const migration = migrationPreflightOnUpdate({ apply: migrate });
304
+ install(target, true, { compatibility: true });
305
+ return migration;
306
+ }
307
+
241
308
  function hasGitRepo(cwd) {
242
309
  return fs.existsSync(path.join(cwd, ".git"));
243
310
  }
@@ -258,6 +325,22 @@ function addLocalExclude(cwd, pattern) {
258
325
  return { status: "written", dest: `${excludeFile} (${pattern})` };
259
326
  }
260
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
+
261
344
  function removeLocalExclude(cwd, pattern) {
262
345
  const excludeFile = path.join(cwd, ".git", "info", "exclude");
263
346
  if (!fs.existsSync(excludeFile)) {
@@ -425,6 +508,218 @@ This policy reduces token waste without weakening ScrumRun safety.
425
508
  return policyFile;
426
509
  }
427
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
+
428
723
  function removePendingNone(content) {
429
724
  return content.replace(/^- Pending: none\.\n?/gm, "").replace(/\n{3,}/g, "\n\n");
430
725
  }
@@ -549,6 +844,11 @@ function runVault(parts) {
549
844
  }
550
845
 
551
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
+ }
552
852
  if (!label) {
553
853
  console.error("Usage: scrumrun backlog add <sprint number or name>");
554
854
  process.exitCode = 1;
@@ -632,6 +932,11 @@ function addKnowledge(topic) {
632
932
  process.exitCode = 1;
633
933
  return;
634
934
  }
935
+ if (containsSecret(topic)) {
936
+ console.error("Secret-like content is forbidden outside the local vault.");
937
+ process.exitCode = 1;
938
+ return;
939
+ }
635
940
  const file = ensureKnowledgeFile(process.cwd());
636
941
  const current = readIfExists(file);
637
942
  const id = nextKnowledgeId(current);
@@ -770,38 +1075,12 @@ function runBacklog(parts) {
770
1075
  }
771
1076
 
772
1077
  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
1078
+ console.log(`${renderCommandHelp()}
801
1079
 
802
1080
  CLI helpers:
803
1081
  scrumrun status
804
1082
  scrumrun commands
1083
+ scrumrun sc knowledge decision --list
805
1084
  scrumrun vault add "NAME" "local-dev-value"
806
1085
  scrumrun context show
807
1086
  scrumrun backlog add "Sprint 01"
@@ -811,6 +1090,300 @@ CLI helpers:
811
1090
  `);
812
1091
  }
813
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
+
814
1387
  function printCore({ pathOnly = false, promptOnly = false } = {}) {
815
1388
  const coreFile = path.join(root, "CORE.md");
816
1389
  if (pathOnly) {
@@ -818,10 +1391,10 @@ function printCore({ pathOnly = false, promptOnly = false } = {}) {
818
1391
  return;
819
1392
  }
820
1393
  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.`);
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.`);
825
1398
  return;
826
1399
  }
827
1400
  console.log(readIfExists(coreFile));
@@ -869,9 +1442,435 @@ function sprintStatusCounts(history) {
869
1442
  };
870
1443
  }
871
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
+
872
1845
  function statusProject() {
873
1846
  const cwd = process.cwd();
874
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
+ }
875
1874
  const required = [
876
1875
  "core.md",
877
1876
  "config.md",
@@ -925,24 +1924,24 @@ function promptCommand(parts) {
925
1924
  const kind = parts[0];
926
1925
  const text = joinText(parts.slice(1));
927
1926
  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}` : ""}`
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}` : ""}`
946
1945
  };
947
1946
  if (!kind || !prompts[kind]) {
948
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]");
@@ -952,20 +1951,22 @@ function promptCommand(parts) {
952
1951
  console.log(prompts[kind]);
953
1952
  }
954
1953
 
955
- function initProject({ force, mode, agentHint }) {
1954
+ function initProject({ force, mode, agentHint, lean }) {
956
1955
  const cwd = process.cwd();
957
1956
  const vars = {
958
1957
  PROJECT_NAME: path.basename(cwd),
959
1958
  DATE: new Date().toISOString().slice(0, 10)
960
1959
  };
961
1960
  const projectTemplate = path.join(templates, "project");
1961
+ const agentTemplate = path.join(templates, lean ? "project-lean" : "project", "AGENTS.md");
962
1962
  const results = [];
963
1963
 
964
1964
  results.push(...copyDir(path.join(projectTemplate, ".scrumrun"), path.join(cwd, ".scrumrun"), { force, vars }));
965
1965
  results.push(copyFile(path.join(root, "CORE.md"), path.join(cwd, ".scrumrun", "core.md"), { force, vars }));
1966
+ results.push(ensureProjectIgnore(cwd));
966
1967
 
967
1968
  if (mode === "shared" || agentHint) {
968
- 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 }));
969
1970
  } else {
970
1971
  results.push({ status: "skipped", dest: path.join(cwd, "AGENTS.md") });
971
1972
  }
@@ -977,14 +1978,20 @@ function initProject({ force, mode, agentHint }) {
977
1978
  }
978
1979
  }
979
1980
  results.push(addLocalExclude(cwd, ".scrumrun/vault.local.md"));
1981
+ if (v2Project(cwd)) refreshState(path.join(cwd, ".scrumrun"));
980
1982
 
981
- const title = mode === "local"
1983
+ const title = lean
1984
+ ? `Initialized ScrumRun 2.0 project (lean read policy)`
1985
+ : mode === "local"
982
1986
  ? "Initialized local ScrumRun project"
983
1987
  : "Initialized shared ScrumRun project";
984
1988
  printResults(title, results);
985
1989
  if (mode === "local" && hasGitRepo(cwd)) {
986
1990
  console.log("\nLocal mode keeps ScrumRun out of commits by writing .scrumrun/ to .git/info/exclude.");
987
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
+ }
988
1995
  }
989
1996
 
990
1997
  function uninstallProject(force) {
@@ -1013,7 +2020,7 @@ function moveIfPossible(cwd, from, to) {
1013
2020
  return { status: "moved", dest: `${src} -> ${dest}` };
1014
2021
  }
1015
2022
 
1016
- function migrateProject() {
2023
+ function migrateLegacyProject() {
1017
2024
  const cwd = process.cwd();
1018
2025
  const moves = [
1019
2026
  ["asm-config.md", ".scrumrun/config.md"],
@@ -1075,36 +2082,106 @@ function migrateProject() {
1075
2082
  printResults("Migrated project", results.filter((result) => result.status !== "missing"));
1076
2083
  }
1077
2084
 
1078
- 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 } = {}) {
1079
2129
  const home = os.homedir();
1080
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
+ }
1081
2137
 
1082
2138
  if (target === "all" || target === "codex") {
1083
- for (const command of COMMANDS) {
1084
- 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)]);
1085
2141
  }
1086
- 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]);
1087
2143
  }
1088
2144
 
1089
2145
  if (target === "all" || target === "claude") {
1090
- for (const command of COMMANDS) {
1091
- 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)]);
1092
2148
  }
1093
- 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]);
1094
2150
  }
1095
2151
 
1096
2152
  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`)]);
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}`);
1099
2184
  }
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
2185
  }
1109
2186
  process.exitCode = ok ? 0 : 1;
1110
2187
  }
@@ -1115,18 +2192,26 @@ const force = args.includes("--force") || args.includes("-f");
1115
2192
  const local = args.includes("--local") || args.includes("-l");
1116
2193
  const shared = args.includes("--shared") || args.includes("-s");
1117
2194
  const noAgentHint = args.includes("--no-agent-hint") || args.includes("-n");
2195
+ const lean = args.includes("--lean");
1118
2196
 
1119
2197
  if (!command || command === "--help" || command === "-h") {
1120
2198
  usage();
2199
+ } else if (command === "--version" || command === "-v") {
2200
+ console.log(`ScrumRun ${version}`);
1121
2201
  } else if (command === "install" || command === "update") {
1122
2202
  const target = ["all", "codex", "opencode", "claude"].includes(args[1]) ? args[1] : "all";
1123
- 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));
1124
2209
  } else if (command === "init") {
1125
2210
  if (local && shared) {
1126
2211
  console.error("Use either --local or --shared, not both.");
1127
2212
  process.exitCode = 1;
1128
2213
  } else {
1129
- initProject({ force, mode: shared ? "shared" : "local", agentHint: shared || !noAgentHint });
2214
+ initProject({ force, mode: shared ? "shared" : "local", agentHint: shared || !noAgentHint, lean });
1130
2215
  }
1131
2216
  } else if (command === "uninstall") {
1132
2217
  uninstallProject(force);
@@ -1136,6 +2221,10 @@ if (!command || command === "--help" || command === "-h") {
1136
2221
  printCore({ pathOnly: args.includes("--path"), promptOnly: args.includes("--prompt") });
1137
2222
  } else if (command === "commands") {
1138
2223
  commandList();
2224
+ } else if (command === "guardrails") {
2225
+ runGuardrails(args.slice(1));
2226
+ } else if (command === "decisions") {
2227
+ runDecisions(args.slice(1));
1139
2228
  } else if (command === "context") {
1140
2229
  runContext(args.slice(1));
1141
2230
  } else if (command === "vault") {
@@ -1153,16 +2242,16 @@ if (!command || command === "--help" || command === "-h") {
1153
2242
  } else if (command === "prompt") {
1154
2243
  promptCommand(args.slice(1));
1155
2244
  } else if (command === "migrate") {
1156
- migrateProject();
2245
+ runMigration(args.slice(1));
1157
2246
  } else if (command === "doctor") {
1158
2247
  const target = ["all", "codex", "opencode", "claude"].includes(args[1]) ? args[1] : "all";
1159
- doctor(target);
2248
+ doctor(target, { compatibility: args.includes("--compat"), strict: args.includes("--strict") });
1160
2249
  } else if (command === "claude") {
1161
2250
  const sub = args[1];
1162
2251
  if (sub === "install" || sub === "update") {
1163
- installClaude(true);
2252
+ installClaude(true, { compatibility: sub === "update" });
1164
2253
  } else if (sub === "doctor") {
1165
- doctor("claude");
2254
+ doctor("claude", { compatibility: args.includes("--compat"), strict: args.includes("--strict") });
1166
2255
  } else {
1167
2256
  console.error("Usage: scrumrun claude [install|update|doctor]");
1168
2257
  process.exitCode = 1;
@@ -1170,9 +2259,9 @@ if (!command || command === "--help" || command === "-h") {
1170
2259
  } else if (command === "codex") {
1171
2260
  const sub = args[1];
1172
2261
  if (sub === "install" || sub === "update") {
1173
- installCodex(true);
2262
+ installCodex(true, { compatibility: sub === "update" });
1174
2263
  } else if (sub === "doctor") {
1175
- doctor("codex");
2264
+ doctor("codex", { compatibility: args.includes("--compat"), strict: args.includes("--strict") });
1176
2265
  } else {
1177
2266
  console.error("Usage: scrumrun codex [install|update|doctor]");
1178
2267
  process.exitCode = 1;
@@ -1180,9 +2269,9 @@ if (!command || command === "--help" || command === "-h") {
1180
2269
  } else if (command === "opencode") {
1181
2270
  const sub = args[1];
1182
2271
  if (sub === "install" || sub === "update") {
1183
- installOpenCode(true);
2272
+ installOpenCode(true, { compatibility: sub === "update" });
1184
2273
  } else if (sub === "doctor") {
1185
- doctor("opencode");
2274
+ doctor("opencode", { compatibility: args.includes("--compat"), strict: args.includes("--strict") });
1186
2275
  } else {
1187
2276
  console.error("Usage: scrumrun opencode [install|update|doctor]");
1188
2277
  process.exitCode = 1;