scrumrun 1.5.2 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (112) hide show
  1. package/CHANGELOG.md +64 -0
  2. package/CORE.md +231 -308
  3. package/DECISIONS.md +151 -11
  4. package/LICENSE +21 -0
  5. package/MIGRATION-1-to-2.md +120 -0
  6. package/README.md +147 -78
  7. package/SPEC.md +259 -251
  8. package/bin/claude-install.js +18 -132
  9. package/bin/scrumrun.js +1367 -174
  10. package/docs/COMMANDS.md +71 -0
  11. package/docs/ENTITY-MODEL.md +38 -0
  12. package/docs/RELEASE-SCORECARD.md +43 -0
  13. package/docs/RELEASE.md +51 -0
  14. package/docs/SCHEMA.md +89 -0
  15. package/docs/SEMANTIC-MEMORY.md +68 -0
  16. package/docs/TROUBLESHOOTING.md +78 -0
  17. package/lib/code-intel/adapter.js +20 -0
  18. package/lib/code-intel/javascript.js +199 -0
  19. package/lib/code-intel/learning.js +66 -0
  20. package/lib/code-intel/scanner.js +126 -0
  21. package/lib/commands/manifest.js +118 -0
  22. package/lib/commands/render.js +78 -0
  23. package/lib/memory/index.js +659 -0
  24. package/lib/memory/markdown.js +51 -0
  25. package/lib/memory/service.js +300 -0
  26. package/lib/runtime/budgets.js +23 -0
  27. package/lib/runtime/canonical-snapshot.js +110 -0
  28. package/lib/runtime/context.js +101 -0
  29. package/lib/runtime/orchestrator.js +303 -0
  30. package/lib/runtime/policy-engine.js +184 -0
  31. package/lib/runtime/request-engine.js +132 -0
  32. package/lib/runtime/run-ledger.js +324 -0
  33. package/lib/security/secrets.js +23 -0
  34. package/lib/v2/artifacts.js +363 -0
  35. package/lib/v2/conformance.js +214 -0
  36. package/lib/v2/migration.js +1221 -0
  37. package/lib/v2/project-store.js +44 -0
  38. package/lib/v2/run-ledger-migration.js +240 -0
  39. package/lib/v2/schema.js +148 -0
  40. package/lib/v2/transaction.js +254 -0
  41. package/package.json +16 -6
  42. package/scripts/generate-contract-docs.js +124 -0
  43. package/templates/project/.scrumrun/config.md +4 -7
  44. package/templates/project/.scrumrun/guardrails.md +31 -0
  45. package/templates/project/.scrumrun/map.md +5 -16
  46. package/templates/project/.scrumrun/memory/decisions/.gitkeep +1 -0
  47. package/templates/project/.scrumrun/memory/dossiers/.gitkeep +1 -0
  48. package/templates/project/.scrumrun/memory/insights/.gitkeep +1 -0
  49. package/templates/project/.scrumrun/memory/knowledge/.gitkeep +1 -0
  50. package/templates/project/.scrumrun/method.json +7 -0
  51. package/templates/project/.scrumrun/project.md +6 -12
  52. package/templates/project/.scrumrun/runs/.gitkeep +1 -0
  53. package/templates/project/.scrumrun/sprints/.gitkeep +1 -0
  54. package/templates/project/.scrumrun/state.md +14 -0
  55. package/templates/project/.scrumrun/tasks/.gitkeep +1 -0
  56. package/templates/project/AGENTS.md +16 -49
  57. package/templates/project-lean/AGENTS.md +18 -0
  58. package/templates/shared/skills/scrumrun/SKILL.md +211 -0
  59. package/templates/codex/prompts/sc-agent.md +0 -14
  60. package/templates/codex/prompts/sc-backlog.md +0 -13
  61. package/templates/codex/prompts/sc-challenge.md +0 -57
  62. package/templates/codex/prompts/sc-config.md +0 -18
  63. package/templates/codex/prompts/sc-context.md +0 -24
  64. package/templates/codex/prompts/sc-decisions.md +0 -8
  65. package/templates/codex/prompts/sc-feature.md +0 -16
  66. package/templates/codex/prompts/sc-fix.md +0 -21
  67. package/templates/codex/prompts/sc-goal.md +0 -14
  68. package/templates/codex/prompts/sc-golden.md +0 -14
  69. package/templates/codex/prompts/sc-help.md +0 -12
  70. package/templates/codex/prompts/sc-init.md +0 -14
  71. package/templates/codex/prompts/sc-intake.md +0 -22
  72. package/templates/codex/prompts/sc-know.md +0 -75
  73. package/templates/codex/prompts/sc-map.md +0 -13
  74. package/templates/codex/prompts/sc-review.md +0 -13
  75. package/templates/codex/prompts/sc-sprint.md +0 -28
  76. package/templates/codex/prompts/sc-study.md +0 -23
  77. package/templates/codex/prompts/sc-uninstall.md +0 -14
  78. package/templates/codex/prompts/sc-update.md +0 -8
  79. package/templates/codex/prompts/sc-vault.md +0 -27
  80. package/templates/codex/skills/scrumrun/SKILL.md +0 -412
  81. package/templates/opencode/commands/sc-agent.md +0 -14
  82. package/templates/opencode/commands/sc-backlog.md +0 -13
  83. package/templates/opencode/commands/sc-challenge.md +0 -57
  84. package/templates/opencode/commands/sc-config.md +0 -18
  85. package/templates/opencode/commands/sc-context.md +0 -24
  86. package/templates/opencode/commands/sc-decisions.md +0 -8
  87. package/templates/opencode/commands/sc-feature.md +0 -16
  88. package/templates/opencode/commands/sc-fix.md +0 -21
  89. package/templates/opencode/commands/sc-goal.md +0 -14
  90. package/templates/opencode/commands/sc-golden.md +0 -14
  91. package/templates/opencode/commands/sc-help.md +0 -12
  92. package/templates/opencode/commands/sc-init.md +0 -14
  93. package/templates/opencode/commands/sc-intake.md +0 -22
  94. package/templates/opencode/commands/sc-know.md +0 -75
  95. package/templates/opencode/commands/sc-map.md +0 -13
  96. package/templates/opencode/commands/sc-review.md +0 -13
  97. package/templates/opencode/commands/sc-sprint.md +0 -28
  98. package/templates/opencode/commands/sc-study.md +0 -23
  99. package/templates/opencode/commands/sc-uninstall.md +0 -14
  100. package/templates/opencode/commands/sc-update.md +0 -8
  101. package/templates/opencode/commands/sc-vault.md +0 -27
  102. package/templates/opencode/skills/scrumrun/SKILL.md +0 -412
  103. package/templates/project/.scrumrun/agents.md +0 -36
  104. package/templates/project/.scrumrun/backlog.md +0 -7
  105. package/templates/project/.scrumrun/context.md +0 -61
  106. package/templates/project/.scrumrun/goals/main/decisions.md +0 -9
  107. package/templates/project/.scrumrun/goals/main/history.md +0 -51
  108. package/templates/project/.scrumrun/goals/main/sprint.md +0 -54
  109. package/templates/project/.scrumrun/golden-rules.md +0 -9
  110. package/templates/project/.scrumrun/knowledge.md +0 -15
  111. package/templates/project/.scrumrun/runbook.md +0 -101
  112. 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,53 @@ 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 {
13
+ applyRunLedgerMigration,
14
+ planRunLedgerMigration,
15
+ reportRunLedgerMigration,
16
+ rollbackRunLedgerMigration
17
+ } = require(path.join(root, "lib", "v2", "run-ledger-migration"));
18
+ const { ARTIFACT_TYPES, ArtifactRepository } = require(path.join(root, "lib", "v2", "artifacts"));
19
+ const { aliases: COMMAND_ALIASES, resolveAlias, resolveRoute } = require(path.join(root, "lib", "commands", "manifest"));
20
+ const { renderCommandHelp, renderCompatibilityPrompt, renderRootPrompt } = require(path.join(root, "lib", "commands", "render"));
21
+ const { planRequest } = require(path.join(root, "lib", "runtime", "request-engine"));
22
+ const { approveRequest, refreshState, retryTask, transitionRun } = require(path.join(root, "lib", "runtime", "orchestrator"));
23
+ const { createMemory, listMemory, showMemory, transitionMemory } = require(path.join(root, "lib", "memory", "service"));
24
+ const { indexPath, indexStatus, mapStatus, queryIndex, rebuildIndex, writeMap } = require(path.join(root, "lib", "memory", "index"));
25
+ const { auditProject } = require(path.join(root, "lib", "v2", "conformance"));
26
+ const { recoverPendingTransactions } = require(path.join(root, "lib", "v2", "transaction"));
27
+ const { containsSecret } = require(path.join(root, "lib", "security", "secrets"));
28
+
29
+ const COMMANDS = ["sc"];
30
+ const COMPATIBILITY_COMMANDS = Object.keys(COMMAND_ALIASES);
31
+ const MANAGED_COMMANDS = [...new Set(["sc", "sc-plan", "sc-knowledge", "sc-rules", ...COMPATIBILITY_COMMANDS])];
34
32
 
35
33
  function usage() {
36
34
  console.log(`ScrumRun
37
35
 
38
36
  Usage:
39
37
  scrumrun --version
38
+ scrumrun sc
39
+ scrumrun sc <noun> <subject> <action> [args]
40
40
  scrumrun install [all|codex|opencode|claude] [--force]
41
- scrumrun update [all|codex|opencode|claude]
42
- scrumrun init [--local|--shared] [--no-agent-hint] [--force]
41
+ scrumrun update [all|codex|opencode|claude] [--migrate]
42
+ scrumrun init [--local|--shared] [--lean] [--no-agent-hint] [--force]
43
43
  scrumrun status
44
44
  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
45
  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]
46
+ scrumrun migrate --to 2 --dry-run
47
+ scrumrun migrate --to 2 --apply
48
+ scrumrun migrate --to 2 --rollback
49
+ scrumrun doctor [all|codex|opencode|claude] [--strict] [--recover]
66
50
  scrumrun uninstall [--force]
67
- scrumrun migrate
68
- scrumrun doctor [all|codex|opencode|claude]
69
51
 
70
52
  Examples:
71
53
  npx scrumrun@latest install
72
- npx scrumrun@latest install claude
54
+ npx scrumrun@latest sc knowledge decision --list
73
55
  npx scrumrun@latest init
74
- npx scrumrun@latest core
75
56
  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
57
+ npx scrumrun@latest migrate --to 2 --dry-run
86
58
  `);
87
59
  }
88
60
 
@@ -110,9 +82,37 @@ function readIfExists(file) {
110
82
  return fs.existsSync(file) ? fs.readFileSync(file, "utf8") : "";
111
83
  }
112
84
 
113
- function writeFile(file, content) {
85
+ function writeFile(file, content, { backup = false } = {}) {
114
86
  ensureDir(path.dirname(file));
115
- fs.writeFileSync(file, content.endsWith("\n") ? content : `${content}\n`);
87
+ const normalized = content.endsWith("\n") ? content : `${content}\n`;
88
+ const current = readIfExists(file);
89
+ if (current === normalized) return { changed: false, backup: null };
90
+
91
+ let backupFile = null;
92
+ if (backup && current) {
93
+ const scrumDir = path.join(process.cwd(), ".scrumrun");
94
+ const relative = path.relative(scrumDir, file);
95
+ const safeName = relative.replace(/[^a-zA-Z0-9._-]+/g, "__");
96
+ const digest = crypto.createHash("sha256").update(current).digest("hex").slice(0, 16);
97
+ backupFile = path.join(scrumDir, ".backup", `${safeName}.${digest}.bak`);
98
+ if (!fs.existsSync(backupFile)) {
99
+ ensureDir(path.dirname(backupFile));
100
+ fs.writeFileSync(backupFile, current);
101
+ }
102
+ }
103
+
104
+ const tempFile = path.join(
105
+ path.dirname(file),
106
+ `.${path.basename(file)}.${process.pid}.${crypto.randomBytes(6).toString("hex")}.tmp`
107
+ );
108
+ try {
109
+ fs.writeFileSync(tempFile, normalized);
110
+ fs.renameSync(tempFile, file);
111
+ } catch (error) {
112
+ if (fs.existsSync(tempFile)) fs.unlinkSync(tempFile);
113
+ throw error;
114
+ }
115
+ return { changed: true, backup: backupFile };
116
116
  }
117
117
 
118
118
  function projectFile(...parts) {
@@ -180,7 +180,8 @@ function cleanupLegacy(commandsDir, skillsDir) {
180
180
  const name = entry.name;
181
181
  const legacyPrefix = name.startsWith("srun-") || name.startsWith("asm-") || name.startsWith("sr-") || name.startsWith("scr-") || name.startsWith("src-") || name.startsWith("run-");
182
182
  const oldConsolidated = /^scr-[a-z]+-[a-z]+\.md$/.test(name);
183
- if (legacyPrefix || oldConsolidated) {
183
+ const managed = MANAGED_COMMANDS.some((command) => name === `${command}.md`);
184
+ if (legacyPrefix || oldConsolidated || managed) {
184
185
  fs.rmSync(path.join(commandsDir, name), { force: true });
185
186
  console.log(` rm legacy ${path.join(commandsDir, name)}`);
186
187
  }
@@ -193,51 +194,142 @@ function cleanupLegacy(commandsDir, skillsDir) {
193
194
  }
194
195
  }
195
196
 
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"));
197
+ function writeCommandAsset(destination, content, force) {
198
+ if (fs.existsSync(destination) && !force) return { status: "skipped", dest: destination };
199
+ ensureDir(path.dirname(destination));
200
+ fs.writeFileSync(destination, content);
201
+ return { status: "written", dest: destination };
202
+ }
203
+
204
+ function installClient(label, commandsDir, skillsDir, skillTemplateDir, force, { compatibility = false } = {}) {
205
+ cleanupLegacy(commandsDir, skillsDir);
200
206
  const results = [
201
- ...copyDir(path.join(templates, "codex", "prompts"), commandsDir, { force }),
202
- ...copyDir(path.join(templates, "codex", "skills"), path.join(home, ".claude", "skills"), { force })
207
+ writeCommandAsset(path.join(commandsDir, "sc.md"), renderRootPrompt(), force),
208
+ ...copyDir(skillTemplateDir, skillsDir, { force })
203
209
  ];
204
- printResults("Installed claude", results);
210
+ if (compatibility) {
211
+ for (const alias of COMPATIBILITY_COMMANDS) {
212
+ results.push(writeCommandAsset(path.join(commandsDir, `${alias}.md`), renderCompatibilityPrompt(alias), force));
213
+ }
214
+ }
215
+ printResults(`Installed ${label}`, results);
216
+ console.log(compatibility
217
+ ? " compatibility adapters installed for this v1 upgrade cycle"
218
+ : " canonical surface: /sc only");
205
219
  }
206
220
 
207
- function installCodex(force) {
221
+ function installClaude(force, options = {}) {
208
222
  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);
223
+ installClient(
224
+ "claude",
225
+ path.join(home, ".claude", "commands"),
226
+ path.join(home, ".claude", "skills"),
227
+ path.join(templates, "shared", "skills"),
228
+ force,
229
+ options
230
+ );
216
231
  }
217
232
 
218
- function installOpenCode(force) {
233
+ function installCodex(force, options = {}) {
219
234
  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);
235
+ installClient(
236
+ "codex",
237
+ path.join(home, ".codex", "prompts"),
238
+ path.join(home, ".codex", "skills"),
239
+ path.join(templates, "shared", "skills"),
240
+ force,
241
+ options
242
+ );
243
+ }
244
+
245
+ function installOpenCode(force, options = {}) {
246
+ const home = os.homedir();
247
+ installClient(
248
+ "opencode",
249
+ path.join(home, ".config", "opencode", "commands"),
250
+ path.join(home, ".config", "opencode", "skills"),
251
+ path.join(templates, "shared", "skills"),
252
+ force,
253
+ options
254
+ );
227
255
  }
228
256
 
229
- function install(target, force) {
257
+ function install(target, force, options = {}) {
230
258
  if (target === "all") {
231
- installCodex(force);
232
- installClaude(force);
233
- installOpenCode(force);
259
+ installCodex(force, options);
260
+ installClaude(force, options);
261
+ installOpenCode(force, options);
234
262
  } else if (target === "codex") {
235
- installCodex(force);
263
+ installCodex(force, options);
236
264
  } else if (target === "claude") {
237
- installClaude(force);
265
+ installClaude(force, options);
238
266
  } else if (target === "opencode") {
239
- installOpenCode(force);
267
+ installOpenCode(force, options);
268
+ }
269
+ }
270
+
271
+ function migrationPreflightOnUpdate({ apply = false } = {}) {
272
+ const scrumDir = path.join(process.cwd(), ".scrumrun");
273
+ if (!fs.existsSync(scrumDir)) return { status: "not-a-project" };
274
+ const marker = path.join(scrumDir, "method.json");
275
+ if (fs.existsSync(marker)) {
276
+ try {
277
+ if (JSON.parse(fs.readFileSync(marker, "utf8")).method === "2.0.0") {
278
+ const preview = planRunLedgerMigration(process.cwd());
279
+ if (preview.status === "current") return { status: "already-v2" };
280
+ console.log("\n## Project schema migration preflight\n");
281
+ console.log(reportRunLedgerMigration(preview).trimEnd());
282
+ if (preview.status === "blocked") {
283
+ for (const error of preview.errors) console.error(`BLOCKED: ${error}`);
284
+ process.exitCode = 1;
285
+ return { status: "blocked" };
286
+ }
287
+ if (!apply) {
288
+ console.log("\nThe project remains unchanged. Apply the verified plan with: npx scrumrun@latest update --migrate");
289
+ return { status: "ready" };
290
+ }
291
+ const result = applyRunLedgerMigration(process.cwd());
292
+ refreshState(path.join(process.cwd(), ".scrumrun"));
293
+ console.log("\nApplied the verified ScrumRun Run ledger schema migration.");
294
+ console.log(`Source fingerprint: ${result.plan.fingerprint}`);
295
+ console.log("Rollback remains available with: npx scrumrun@latest migrate --to 2 --rollback");
296
+ return { status: result.status, result };
297
+ }
298
+ } catch {
299
+ console.error("Project migration preflight failed: .scrumrun/method.json is malformed.");
300
+ process.exitCode = 1;
301
+ return { status: "blocked" };
302
+ }
240
303
  }
304
+ try {
305
+ const preview = dryRunMigration(process.cwd());
306
+ console.log("\n## Project migration preflight\n");
307
+ console.log(preview.output.trimEnd());
308
+ if (preview.status === "blocked") {
309
+ for (const error of preview.plan.errors) console.error(`BLOCKED: ${error}`);
310
+ process.exitCode = 1;
311
+ return { status: "blocked" };
312
+ }
313
+ if (!apply) {
314
+ console.log("\nThe project remains unchanged. Apply the verified plan with: npx scrumrun@latest update --migrate");
315
+ return { status: "ready" };
316
+ }
317
+ const result = applyMigration(process.cwd(), { plan: preview.plan });
318
+ console.log("\nApplied the verified ScrumRun v1 → v2 project migration.");
319
+ console.log(`Source fingerprint: ${result.manifest.sourceInventory.rootSha256}`);
320
+ console.log("Rollback remains available with: npx scrumrun@latest migrate --to 2 --rollback");
321
+ return { status: "applied", result };
322
+ } catch (error) {
323
+ console.error(`Project migration preflight failed: ${error.message}`);
324
+ process.exitCode = 1;
325
+ return { status: "blocked" };
326
+ }
327
+ }
328
+
329
+ function updateInstallation(target, { migrate = false } = {}) {
330
+ const migration = migrationPreflightOnUpdate({ apply: migrate });
331
+ install(target, true, { compatibility: true });
332
+ return migration;
241
333
  }
242
334
 
243
335
  function hasGitRepo(cwd) {
@@ -260,6 +352,22 @@ function addLocalExclude(cwd, pattern) {
260
352
  return { status: "written", dest: `${excludeFile} (${pattern})` };
261
353
  }
262
354
 
355
+ function ensureProjectIgnore(cwd) {
356
+ const file = path.join(cwd, ".scrumrun", ".gitignore");
357
+ if (fs.existsSync(file) && fs.lstatSync(file).isSymbolicLink()) {
358
+ throw new Error("Refusing to write a symbolic .scrumrun/.gitignore.");
359
+ }
360
+ const current = readIfExists(file);
361
+ const lines = current.split(/\r?\n/).filter(Boolean);
362
+ for (const pattern of [".cache/", ".backup/", ".migration/", "vault.local.md"]) {
363
+ if (!lines.includes(pattern)) lines.push(pattern);
364
+ }
365
+ const next = `${lines.join("\n")}\n`;
366
+ if (next === current) return { status: "skipped", dest: file };
367
+ writeFile(file, next);
368
+ return { status: "written", dest: file };
369
+ }
370
+
263
371
  function removeLocalExclude(cwd, pattern) {
264
372
  const excludeFile = path.join(cwd, ".git", "info", "exclude");
265
373
  if (!fs.existsSync(excludeFile)) {
@@ -427,6 +535,218 @@ This policy reduces token waste without weakening ScrumRun safety.
427
535
  return policyFile;
428
536
  }
429
537
 
538
+ function renderGuardrails(cwd) {
539
+ const name = path.basename(cwd);
540
+ const golden = readIfExists(path.join(cwd, ".scrumrun", "golden-rules.md")).trim();
541
+ const config = readIfExists(path.join(cwd, ".scrumrun", "config.md")).trim();
542
+ const tokenPolicy = readIfExists(path.join(cwd, ".scrumrun", "token-policy.md")).trim();
543
+ const knowledge = readIfExists(path.join(cwd, ".scrumrun", "knowledge.md"));
544
+ const approvedMatch = knowledge.match(/## Approved Knowledge\n+([\s\S]*?)(?=\n## |$)/);
545
+ const approved = approvedMatch ? approvedMatch[1].trim() : "";
546
+ const approvedEntries = approved
547
+ .split(/(?=^### K-\d{3})/m)
548
+ .map((s) => s.trim())
549
+ .filter((s) => s.startsWith("### K-"));
550
+
551
+ const goldenSection = golden
552
+ ? golden.replace(/^# ScrumRun Golden Rules.*\n/m, "").trim()
553
+ : "Pending: add rules with `/sc-rules golden --add <rule>`.";
554
+ const configSection = config
555
+ ? config
556
+ .split("\n")
557
+ .filter((line) => line.match(/^[A-Z][\w -]*:/))
558
+ .map((line) => `- ${line}`)
559
+ .join("\n") || "Pending: configure with `/sc-config config --lang|--interaction|--approval|--quick-tasks`."
560
+ : "Pending: configure with `/sc-config config --lang|--interaction|--approval|--quick-tasks`.";
561
+ const tokenSection = tokenPolicy
562
+ ? tokenPolicy.replace(/^# ScrumRun Token Policy.*\n/m, "").trim()
563
+ : "Pending: token policy not initialized.";
564
+ const approvedSection = approvedEntries.length
565
+ ? approvedEntries.map((entry) => {
566
+ const id = (entry.match(/^### (K-\d{3})/) || [])[1];
567
+ const title = (entry.match(/^### K-\d{3} - (.+)$/m) || [])[1] || "untitled";
568
+ const fact = (entry.match(/#### Verified Facts\n+([\s\S]*?)(?=\n#### |$)/m) || [])[1];
569
+ const oneLine = fact ? fact.trim().split(/\r?\n/)[0].replace(/^[-*]\s*/, "") : "no verified fact summary";
570
+ return `- **${id}** — ${title}: ${oneLine}`;
571
+ }).join("\n")
572
+ : "Pending: add with `/sc-knowledge know --add <topic>`, then `--approve <K-NNN>`.";
573
+
574
+ return `# ScrumRun Guardrails - ${name}
575
+
576
+ 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.
577
+
578
+ Last built: ${today()}
579
+ Built by: CLI
580
+
581
+ ## Precedence
582
+
583
+ When guidance conflicts, apply in this strict order (SPEC §5):
584
+
585
+ 1. Golden rules (active)
586
+ 2. Open decisions linked to current work
587
+ 3. Approved knowledge facts
588
+ 4. Current sprint / feature plan
589
+ 5. Backlog priority
590
+ 6. Map and dossiers
591
+ 7. \`state.md\` (never authoritative)
592
+
593
+ Higher precedence always wins. Lower precedence never silently overrides.
594
+
595
+ ## Golden Rules
596
+
597
+ ${goldenSection}
598
+
599
+ ## Active Invariants
600
+
601
+ Numbered for stable reference. Full list in \`SPEC.md\` §6.
602
+
603
+ - **I-01** No sprint moves from \`proposed\` to \`running\` without explicit owner approval.
604
+ - **I-02** No code changes outside a \`running\` sprint, \`running\` fix, or explicitly authorized quick task.
605
+ - **I-03** Active golden rule cannot be bypassed; retirement is the only path.
606
+ - **I-04** Intake is read-only.
607
+ - **I-05** Main goal history and feature lane history are physically separate.
608
+ - **I-06** Approval is synchronous and explicit; ambiguous acknowledgements are not approval.
609
+ - **I-07** Every state transition writes exactly one history entry. History is append-only.
610
+ - **I-08** Golden rules are checked before planning/execution; a fire moves a running sprint to \`blocked\`.
611
+ - **I-09** \`state.md\` and \`context.md\` are never authoritative.
612
+ - **I-10** Only \`approved\` knowledge informs planning.
613
+ - **I-11** Vault values never appear in history, knowledge, backlog, reviews, commits, or logs.
614
+ - **I-12** Every artifact carries \`id\`, \`kind\`, \`status\`, \`created\`, \`updated\`, \`method\` frontmatter.
615
+ - **I-13** Dossier updates record the source commit hash for drift detection.
616
+ - **I-14** Quick tasks may execute with \`Quick Tasks: allow\`, never bypassing golden rules.
617
+ - **I-15** Rejected knowledge is preserved in \`rejected\` state to prevent re-proposing.
618
+
619
+ ## Configuration
620
+
621
+ ${configSection}
622
+
623
+ ## Token Policy
624
+
625
+ ${tokenSection}
626
+
627
+ ## Approved Knowledge
628
+
629
+ ${approvedSection}
630
+
631
+ ## Sources
632
+
633
+ - Golden rules: \`.scrumrun/golden-rules.md\`
634
+ - Config: \`.scrumrun/config.md\`
635
+ - Token policy: \`.scrumrun/token-policy.md\`
636
+ - Knowledge: \`.scrumrun/knowledge.md\` (Approved section only)
637
+ - State: \`.scrumrun/state.md\`
638
+ `;
639
+ }
640
+
641
+ function renderState(cwd) {
642
+ const name = path.basename(cwd);
643
+ const project = readIfExists(path.join(cwd, ".scrumrun", "project.md")).trim();
644
+ const sprint = readIfExists(path.join(cwd, ".scrumrun", "goals", "main", "sprint.md"));
645
+ const history = readIfExists(path.join(cwd, ".scrumrun", "goals", "main", "history.md"));
646
+ const decisions = readIfExists(path.join(cwd, ".scrumrun", "goals", "main", "decisions.md"));
647
+ const backlog = readIfExists(path.join(cwd, ".scrumrun", "backlog.md")).trim();
648
+
649
+ const projectSection = project
650
+ ? project.replace(/^# .*\n/m, "").trim()
651
+ : "Pending: set with `/sc-plan goal --set <goal>`.";
652
+ const goalSection = sprint
653
+ ? sprint.split("\n").slice(0, 8).join("\n").trim() || "Pending: define with `/sc-plan goal --set <goal>`."
654
+ : "Pending: define with `/sc-plan goal --set <goal>`.";
655
+ const historyEntries = [
656
+ ...[...history.matchAll(/^## (\d{4}-\d{2}-\d{2}[^\n]* · [^\n]+)$/gm)].map((m) => ({ index: m.index, label: m[1] })),
657
+ ...[...history.matchAll(/^### ([^\n]+)\n(?:(?!^### |^## )[\s\S])*?^Status:\s*[a-z]+/gim)].map((m) => ({ index: m.index, label: m[1] }))
658
+ ].sort((a, b) => a.index - b.index);
659
+ const recentHistory = historyEntries
660
+ .slice(-5)
661
+ .reverse()
662
+ .map((entry) => `- ${entry.label}`)
663
+ .join("\n") || "- None.";
664
+ const parsedDecisions = parseDecisions(cwd);
665
+ const openList = parsedDecisions.open.length
666
+ ? parsedDecisions.open
667
+ .map((decision) => `- ${decision.id ? `${decision.id} — ` : ""}${decision.title}`)
668
+ .join("\n")
669
+ : "- None.";
670
+ const backlogSection = backlog && backlog.includes("- [")
671
+ ? backlog
672
+ .split("\n")
673
+ .filter((line) => line.match(/^- \[[ xX]\]/))
674
+ .map((line) => `- ${line.replace(/^- \[[ xX]\]\s*/, "")}`)
675
+ .join("\n") || "- None."
676
+ : "- None.";
677
+
678
+ return `# ScrumRun State - ${name}
679
+
680
+ 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.
681
+
682
+ Last built: ${today()}
683
+ Built by: CLI
684
+
685
+ ## Project
686
+
687
+ ${projectSection}
688
+
689
+ ## Main Goal
690
+
691
+ ${goalSection}
692
+
693
+ ## Active Sprint
694
+
695
+ - Pending: confirm with /sc-plan sprint --status.
696
+
697
+ ## Open Decisions
698
+
699
+ ${openList}
700
+
701
+ ## Backlog
702
+
703
+ ${backlogSection}
704
+
705
+ ## Recent History
706
+
707
+ ${recentHistory}
708
+
709
+ ## Sources
710
+
711
+ - Project: \`.scrumrun/project.md\`
712
+ - Sprint: \`.scrumrun/goals/main/sprint.md\`
713
+ - History: \`.scrumrun/goals/main/history.md\`
714
+ - Decisions: \`.scrumrun/goals/main/decisions.md\`
715
+ - Backlog: \`.scrumrun/backlog.md\`
716
+ - Knowledge: \`.scrumrun/knowledge.md\`
717
+ `;
718
+ }
719
+
720
+ function runGuardrails(parts) {
721
+ const cwd = process.cwd();
722
+ const action = parts[0];
723
+ if (!action || action === "build" || action === "--build" || action === "-b") {
724
+ const guardrailsFile = path.join(cwd, ".scrumrun", "guardrails.md");
725
+ const stateFile = path.join(cwd, ".scrumrun", "state.md");
726
+ if (!fs.existsSync(path.join(cwd, ".scrumrun"))) {
727
+ console.error("Not a ScrumRun project. Run `scrumrun init` first.");
728
+ process.exitCode = 1;
729
+ return;
730
+ }
731
+ const existingGuardrails = readIfExists(guardrailsFile);
732
+ const canonicalGuardrails = /^# ScrumRun Project Guardrails\b/m.test(existingGuardrails);
733
+ if (!canonicalGuardrails) writeFile(guardrailsFile, renderGuardrails(cwd));
734
+ writeFile(stateFile, renderState(cwd));
735
+ if (canonicalGuardrails) {
736
+ console.log(`Preserved canonical ${path.relative(cwd, guardrailsFile)} and built ${path.relative(cwd, stateFile)}.`);
737
+ } else {
738
+ console.log(`Built ${path.relative(cwd, guardrailsFile)} and ${path.relative(cwd, stateFile)}.`);
739
+ console.log("Sources remain canonical. Re-run after edits to golden-rules, config, knowledge, or sprint/history.");
740
+ }
741
+ } else if (action === "show" || action === "--show" || action === "-s") {
742
+ const file = path.join(cwd, ".scrumrun", "guardrails.md");
743
+ console.log(readIfExists(file) || "guardrails.md not built. Run `scrumrun guardrails --build`.");
744
+ } else {
745
+ console.error("Usage: scrumrun guardrails [--build|--show]");
746
+ process.exitCode = 1;
747
+ }
748
+ }
749
+
430
750
  function removePendingNone(content) {
431
751
  return content.replace(/^- Pending: none\.\n?/gm, "").replace(/\n{3,}/g, "\n\n");
432
752
  }
@@ -551,6 +871,11 @@ function runVault(parts) {
551
871
  }
552
872
 
553
873
  function addBacklogItem(label) {
874
+ if (containsSecret(label)) {
875
+ console.error("Secret-like content is forbidden outside the local vault.");
876
+ process.exitCode = 1;
877
+ return;
878
+ }
554
879
  if (!label) {
555
880
  console.error("Usage: scrumrun backlog add <sprint number or name>");
556
881
  process.exitCode = 1;
@@ -634,6 +959,11 @@ function addKnowledge(topic) {
634
959
  process.exitCode = 1;
635
960
  return;
636
961
  }
962
+ if (containsSecret(topic)) {
963
+ console.error("Secret-like content is forbidden outside the local vault.");
964
+ process.exitCode = 1;
965
+ return;
966
+ }
637
967
  const file = ensureKnowledgeFile(process.cwd());
638
968
  const current = readIfExists(file);
639
969
  const id = nextKnowledgeId(current);
@@ -772,38 +1102,12 @@ function runBacklog(parts) {
772
1102
  }
773
1103
 
774
1104
  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
1105
+ console.log(`${renderCommandHelp()}
803
1106
 
804
1107
  CLI helpers:
805
1108
  scrumrun status
806
1109
  scrumrun commands
1110
+ scrumrun sc knowledge decision --list
807
1111
  scrumrun vault add "NAME" "local-dev-value"
808
1112
  scrumrun context show
809
1113
  scrumrun backlog add "Sprint 01"
@@ -813,6 +1117,344 @@ CLI helpers:
813
1117
  `);
814
1118
  }
815
1119
 
1120
+ function printRouteHelp(route) {
1121
+ if (route.level === "root") {
1122
+ commandList();
1123
+ } else if (route.level === "noun") {
1124
+ console.log(`# /sc ${route.noun}`);
1125
+ console.log(route.spec.description);
1126
+ for (const [subject, actions] of Object.entries(route.spec.subjects)) {
1127
+ console.log(`- ${subject}: ${actions.join(", ")}`);
1128
+ }
1129
+ } else {
1130
+ console.log(`# /sc ${route.noun} ${route.subject}`);
1131
+ console.log(route.actions.join("\n"));
1132
+ }
1133
+ }
1134
+
1135
+ function agentWorkflowRequired(noun, subject, args) {
1136
+ const suffix = args.length ? ` ${args.join(" ")}` : "";
1137
+ console.log(`Agent workflow: /sc ${noun} ${subject}${suffix}`);
1138
+ console.log("Run this through an installed ScrumRun AI client; the CLI parser validated the route but this workflow requires repository reasoning.");
1139
+ }
1140
+
1141
+ function v2Project(cwd = process.cwd()) {
1142
+ const marker = path.join(cwd, ".scrumrun", "method.json");
1143
+ if (!fs.existsSync(marker)) return false;
1144
+ try {
1145
+ return JSON.parse(fs.readFileSync(marker, "utf8")).method === "2.0.0";
1146
+ } catch {
1147
+ return false;
1148
+ }
1149
+ }
1150
+
1151
+ function optionValues(args, flag) {
1152
+ const values = [];
1153
+ for (let index = 0; index < args.length; index++) {
1154
+ if (args[index] === flag && args[index + 1] && !args[index + 1].startsWith("--")) values.push(args[index + 1]);
1155
+ }
1156
+ return values;
1157
+ }
1158
+
1159
+ function optionValue(args, flag, fallback = null) {
1160
+ const values = optionValues(args, flag);
1161
+ return values.length ? values[values.length - 1] : fallback;
1162
+ }
1163
+
1164
+ function positionalValues(args, start = 1) {
1165
+ const values = [];
1166
+ const valued = new Set(["--title", "--content", "--evidence", "--relation", "--subject", "--source", "--source-id", "--confidence", "--valid-from", "--valid-until", "--review-trigger", "--note", "--limit"]);
1167
+ for (let index = start; index < args.length; index++) {
1168
+ if (valued.has(args[index])) {
1169
+ index++;
1170
+ continue;
1171
+ }
1172
+ if (!args[index].startsWith("--")) values.push(args[index]);
1173
+ }
1174
+ return values;
1175
+ }
1176
+
1177
+ function memoryOptions(args) {
1178
+ const positional = positionalValues(args).join(" ").trim();
1179
+ const subject = optionValue(args, "--subject");
1180
+ const separator = subject ? subject.indexOf(":") : -1;
1181
+ return {
1182
+ title: optionValue(args, "--title") || positional,
1183
+ content: optionValue(args, "--content") || positional || optionValue(args, "--title"),
1184
+ evidence: optionValues(args, "--evidence"),
1185
+ relations: optionValues(args, "--relation"),
1186
+ subjectType: separator > 0 ? subject.slice(0, separator) : null,
1187
+ subjectId: separator > 0 ? subject.slice(separator + 1) : subject,
1188
+ sourceType: optionValue(args, "--source", "human"),
1189
+ sourceId: optionValue(args, "--source-id"),
1190
+ confidence: optionValue(args, "--confidence"),
1191
+ validFrom: optionValue(args, "--valid-from"),
1192
+ validUntil: optionValue(args, "--valid-until"),
1193
+ reviewTrigger: optionValue(args, "--review-trigger")
1194
+ };
1195
+ }
1196
+
1197
+ function runTransitionOptions(args) {
1198
+ const evidenceFlags = new Map([
1199
+ ["--command", "command"],
1200
+ ["--test", "test"],
1201
+ ["--file", "file"],
1202
+ ["--review", "review"],
1203
+ ["--decision", "decision"],
1204
+ ["--insight", "insight"],
1205
+ ["--risk", "risk"]
1206
+ ]);
1207
+ const referenceKinds = new Set(["file", "review", "decision", "insight"]);
1208
+ const evidence = [];
1209
+ const noteParts = [];
1210
+ let note = null;
1211
+ let actor = "agent";
1212
+ let occurredAt = null;
1213
+ for (let index = 2; index < args.length; index++) {
1214
+ const token = args[index];
1215
+ const value = args[index + 1] && !args[index + 1].startsWith("--") ? args[index + 1] : null;
1216
+ if (token === "--note" || token === "--actor" || token === "--at" || token === "--evidence" || evidenceFlags.has(token)) {
1217
+ if (!value) throw new Error(`${token} requires a value.`);
1218
+ index++;
1219
+ if (token === "--note") note = value;
1220
+ else if (token === "--actor") actor = value;
1221
+ else if (token === "--at") occurredAt = value;
1222
+ else if (token === "--evidence") {
1223
+ const separator = value.indexOf(":");
1224
+ const kind = separator > 0 ? value.slice(0, separator) : "note";
1225
+ const content = separator > 0 ? value.slice(separator + 1) : value;
1226
+ evidence.push(referenceKinds.has(kind) ? { kind, ref: content } : { kind, summary: content });
1227
+ } else {
1228
+ const kind = evidenceFlags.get(token);
1229
+ evidence.push(referenceKinds.has(kind) ? { kind, ref: value } : { kind, summary: value });
1230
+ }
1231
+ continue;
1232
+ }
1233
+ if (token.startsWith("--")) throw new Error(`Unknown Run evidence option: ${token}`);
1234
+ noteParts.push(token);
1235
+ }
1236
+ return { note: note || noteParts.join(" ").trim() || null, evidence, actor, occurredAt };
1237
+ }
1238
+
1239
+ function printMemoryArtifact(artifact) {
1240
+ if (!artifact) return false;
1241
+ console.log(readIfExists(artifact.file));
1242
+ return true;
1243
+ }
1244
+
1245
+ function runV2Memory(subject, args) {
1246
+ const kind = subject === "fact" ? "knowledge" : subject;
1247
+ const action = args[0];
1248
+ const createAction = kind === "insight" ? "--propose" : "--add";
1249
+ if (action === createAction) {
1250
+ const artifact = createMemory(process.cwd(), kind, memoryOptions(args));
1251
+ console.log(`Created ${artifact.record.status} ${artifact.record.id}: ${path.relative(process.cwd(), artifact.file)}`);
1252
+ return;
1253
+ }
1254
+ if (action === "--list") {
1255
+ const artifacts = listMemory(process.cwd(), kind, { includeInactive: args.includes("--include-inactive") });
1256
+ console.log(artifacts.length
1257
+ ? artifacts.map((artifact) => `${artifact.record.id} | ${artifact.record.status} | ${((artifact.body || "").match(/^# ([^\r\n]+)/m) || [])[1] || artifact.record.id}`).join("\n")
1258
+ : `No ${kind} artifacts.`);
1259
+ return;
1260
+ }
1261
+ if (action === "--show") {
1262
+ if (!printMemoryArtifact(showMemory(process.cwd(), kind, args[1]))) {
1263
+ console.error(`${kind} not found: ${args[1]}`);
1264
+ process.exitCode = 1;
1265
+ }
1266
+ return;
1267
+ }
1268
+ const actionMap = {
1269
+ "--approve": "approve",
1270
+ "--reject": "reject",
1271
+ "--invalidate": "invalidate",
1272
+ "--resolve": "resolve",
1273
+ "--confirm": "confirm",
1274
+ "--refresh": "refresh",
1275
+ "--archive": "archive",
1276
+ "--stale": "stale",
1277
+ "--deprecate": "deprecate"
1278
+ };
1279
+ if (actionMap[action]) {
1280
+ const artifact = transitionMemory(process.cwd(), kind, args[1], actionMap[action], {
1281
+ evidence: optionValues(args.slice(2), "--evidence"),
1282
+ note: optionValue(args.slice(2), "--note")
1283
+ });
1284
+ console.log(`${artifact.record.id}: ${artifact.record.status}.`);
1285
+ return;
1286
+ }
1287
+ throw new Error(`Unsupported ${kind} command: ${action || "missing action"}`);
1288
+ }
1289
+
1290
+ function runSemanticContext(subject, args) {
1291
+ const action = args[0];
1292
+ if (subject === "study") {
1293
+ const result = queryIndex(process.cwd(), args.join(" "));
1294
+ console.log(JSON.stringify(result, null, 2));
1295
+ return;
1296
+ }
1297
+ if (subject === "map") {
1298
+ if (action === "--build") {
1299
+ const built = rebuildIndex(process.cwd());
1300
+ const map = writeMap(process.cwd());
1301
+ console.log(`Built ${path.relative(process.cwd(), built.file)} and ${path.relative(process.cwd(), map.file)}: ${map.nodes} nodes, ${map.edges} edges.`);
1302
+ return;
1303
+ }
1304
+ const mapFile = path.join(process.cwd(), ".scrumrun", "map.md");
1305
+ const status = mapStatus(process.cwd());
1306
+ if (status.stale) throw new Error(`Generated map.md is stale (${status.reason || status.error || "unknown reason"}); run /sc knowledge map --build.`);
1307
+ console.log(fs.readFileSync(mapFile, "utf8"));
1308
+ return;
1309
+ }
1310
+ if (action === "--build" || action === "--update") {
1311
+ const built = rebuildIndex(process.cwd());
1312
+ console.log(`Built ${path.relative(process.cwd(), built.file)} from ${built.artifacts} canonical artifacts.`);
1313
+ return;
1314
+ }
1315
+ if (action === "--clear") {
1316
+ const file = indexPath(process.cwd());
1317
+ if (fs.existsSync(file)) fs.rmSync(file, { force: true });
1318
+ console.log("Cleared the disposable semantic index; canonical Markdown was not changed.");
1319
+ return;
1320
+ }
1321
+ if (action === "--show") {
1322
+ const query = args.slice(1).join(" ").trim();
1323
+ console.log(JSON.stringify(query ? queryIndex(process.cwd(), query) : indexStatus(process.cwd()), null, 2));
1324
+ return;
1325
+ }
1326
+ throw new Error(`Unsupported knowledge ${subject} command: ${action || "missing action"}`);
1327
+ }
1328
+
1329
+ function executeRootRoute(route) {
1330
+ const { noun, subject, args: routeArgs } = route;
1331
+ if (noun === "plan" && subject === "intake") {
1332
+ if (routeArgs[0] === "--approve") {
1333
+ const result = approveRequest(process.cwd(), routeArgs[1]);
1334
+ console.log(`${result.status === "already-approved" ? "Already approved" : "Approved"}: ${result.task.id} → ${result.run.id}`);
1335
+ return;
1336
+ }
1337
+ const request = routeArgs[0] === "--request" ? routeArgs.slice(1).join(" ") : routeArgs.join(" ");
1338
+ const plan = planRequest(process.cwd(), request);
1339
+ console.log(`# ScrumRun Intake`);
1340
+ console.log(`State: ${plan.state}`);
1341
+ console.log(`Classification: ${plan.classification.type} (${plan.classification.reason})`);
1342
+ console.log(`Risk: ${plan.risk.level} — ${plan.risk.reasons.join("; ")}`);
1343
+ console.log(`Policy: ${plan.policy.status} (${plan.policy.checked.length} checked; ${plan.policy.deferred.length} deferred)`);
1344
+ for (const violation of plan.policy.violations) console.log(`BLOCKED: ${violation}`);
1345
+ for (const result of plan.policy.evaluations.filter((item) => item.status === "deferred")) {
1346
+ console.log(`DEFERRED: ${result.guardrail} ${result.code}: ${result.message}`);
1347
+ }
1348
+ for (const warning of plan.context.warnings) console.log(`WARNING: ${warning}`);
1349
+ if (plan.approvalToken) {
1350
+ console.log(`Approval: scrumrun sc plan intake --approve ${plan.approvalToken}`);
1351
+ }
1352
+ return;
1353
+ }
1354
+ if (noun === "plan" && subject === "task" && routeArgs[0] === "--retry") {
1355
+ const result = retryTask(process.cwd(), routeArgs[1]);
1356
+ console.log(`Created retry ${result.run.id} for ${result.task.id} (attempt ${result.run.attempt}).`);
1357
+ return;
1358
+ }
1359
+ if (noun === "plan" && ["task", "run"].includes(subject) && ["--list", "--show"].includes(routeArgs[0])) {
1360
+ const repository = new ArtifactRepository(projectFile());
1361
+ if (routeArgs[0] === "--list") {
1362
+ const artifacts = repository.list(subject).map((artifact) => `${artifact.record.id} | ${artifact.record.status} | ${((artifact.body || "").match(/^# ([^\r\n]+)/m) || [])[1] || artifact.record.id}`);
1363
+ console.log(artifacts.length ? artifacts.join("\n") : `No ${subject} artifacts.`);
1364
+ } else {
1365
+ const artifact = repository.read(subject, routeArgs[1]);
1366
+ if (!artifact) {
1367
+ console.error(`${subject} not found: ${routeArgs[1]}`);
1368
+ process.exitCode = 1;
1369
+ } else {
1370
+ console.log(readIfExists(artifact.file));
1371
+ }
1372
+ }
1373
+ return;
1374
+ }
1375
+ if (noun === "plan" && subject === "run") {
1376
+ const transitions = {
1377
+ "--validate": "validating",
1378
+ "--learn": "learning",
1379
+ "--complete": "completed",
1380
+ "--resume": "executing",
1381
+ "--fail": "failed",
1382
+ "--block": "blocked"
1383
+ };
1384
+ if (transitions[routeArgs[0]]) {
1385
+ const result = transitionRun(process.cwd(), routeArgs[1], transitions[routeArgs[0]], runTransitionOptions(routeArgs));
1386
+ console.log(`${result.run.id}: ${result.run.status}; ${result.task.id}: ${result.task.status}.`);
1387
+ if (result.learning) {
1388
+ if (result.learning.created.length) console.log(`Learning candidates: ${result.learning.created.join(", ")}.`);
1389
+ for (const warning of result.learning.warnings) console.warn(`WARNING: ${warning}`);
1390
+ }
1391
+ return;
1392
+ }
1393
+ }
1394
+ if (noun === "knowledge" && ["fact", "decision", "insight", "dossier"].includes(subject) && v2Project()) return runV2Memory(subject, routeArgs);
1395
+ if (noun === "knowledge" && ["context", "map", "study"].includes(subject) && v2Project()) return runSemanticContext(subject, routeArgs);
1396
+ if (noun === "knowledge" && subject === "decision") return runDecisions(routeArgs);
1397
+ if (noun === "knowledge" && subject === "fact") return runKnow(routeArgs);
1398
+ if (noun === "knowledge" && subject === "vault") return runVault(routeArgs);
1399
+ if (noun === "knowledge" && subject === "context") return runContext(routeArgs);
1400
+ if (noun === "review" && subject === "artifact" && routeArgs[0] === "--run") {
1401
+ const audit = auditProject(process.cwd());
1402
+ console.log(JSON.stringify(audit, null, 2));
1403
+ if (!audit.passed) process.exitCode = 1;
1404
+ return;
1405
+ }
1406
+ if (noun === "config" && subject === "migrate") return runMigration(routeArgs);
1407
+ if (noun === "config" && subject === "doctor") {
1408
+ const target = ["all", "codex", "opencode", "claude"].includes(routeArgs[0]) ? routeArgs[0] : "all";
1409
+ return doctor(target, { strict: routeArgs.includes("--strict"), recover: routeArgs.includes("--recover") });
1410
+ }
1411
+ if (noun === "config" && subject === "update") {
1412
+ const target = ["all", "codex", "opencode", "claude"].includes(routeArgs[0]) ? routeArgs[0] : "all";
1413
+ return updateInstallation(target, { migrate: routeArgs.includes("--migrate") });
1414
+ }
1415
+ if (noun === "config" && subject === "init") {
1416
+ const localMode = routeArgs.includes("--local");
1417
+ const sharedMode = routeArgs.includes("--shared");
1418
+ if (localMode && sharedMode) {
1419
+ console.error("Use either --local or --shared, not both.");
1420
+ process.exitCode = 1;
1421
+ return;
1422
+ }
1423
+ return initProject({
1424
+ force: routeArgs.includes("--force"),
1425
+ mode: sharedMode ? "shared" : "local",
1426
+ agentHint: sharedMode || !routeArgs.includes("--no-agent-hint"),
1427
+ lean: routeArgs.includes("--lean")
1428
+ });
1429
+ }
1430
+ if (noun === "config" && subject === "uninstall") return uninstallProject(routeArgs.includes("--force"));
1431
+ if (noun === "config" && subject === "help") return commandList();
1432
+ if (noun === "config" && subject === "project" && routeArgs[0] === "--show") {
1433
+ console.log(readIfExists(projectFile("config.md")) || "Project config is missing.");
1434
+ return;
1435
+ }
1436
+ agentWorkflowRequired(noun, subject, routeArgs);
1437
+ }
1438
+
1439
+ function runRoot(parts) {
1440
+ const route = resolveRoute(parts);
1441
+ if (route.type === "help") return printRouteHelp(route);
1442
+ if (route.type === "error") {
1443
+ console.error(route.message);
1444
+ process.exitCode = 1;
1445
+ return;
1446
+ }
1447
+ return executeRootRoute(route);
1448
+ }
1449
+
1450
+ function runCompatibilityAlias(alias, parts) {
1451
+ const route = resolveAlias(alias, parts);
1452
+ console.warn(`Deprecated: ${alias} now executes /sc ${route.noun} ${route.subject}.`);
1453
+ if (route.note) console.warn(route.note);
1454
+ if (alias === "sc-backlog") return runBacklog(parts);
1455
+ return executeRootRoute({ type: "route", noun: route.noun, subject: route.subject, args: route.args });
1456
+ }
1457
+
816
1458
  function printCore({ pathOnly = false, promptOnly = false } = {}) {
817
1459
  const coreFile = path.join(root, "CORE.md");
818
1460
  if (pathOnly) {
@@ -820,10 +1462,10 @@ function printCore({ pathOnly = false, promptOnly = false } = {}) {
820
1462
  return;
821
1463
  }
822
1464
  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.`);
1465
+ console.log(`Read AGENTS.md, .scrumrun/guardrails.md, and .scrumrun/state.md first.
1466
+ Follow ScrumRun 2.0 and load only the canonical ids/evidence relevant to the request.
1467
+ If /sc is unavailable, execute the equivalent /sc <noun> <subject> <action> workflow from .scrumrun/core.md manually.
1468
+ Keep intake read-only; do not create a Task or Run until explicit approval.`);
827
1469
  return;
828
1470
  }
829
1471
  console.log(readIfExists(coreFile));
@@ -871,9 +1513,435 @@ function sprintStatusCounts(history) {
871
1513
  };
872
1514
  }
873
1515
 
1516
+ function decisionsFile(cwd) {
1517
+ return path.join(cwd, ".scrumrun", "goals", "main", "decisions.md");
1518
+ }
1519
+
1520
+ function extractFieldBody(body, label) {
1521
+ const re = new RegExp(`^${label}\\s*\\n([\\s\\S]*?)(?=^\\w[\\w ]*:|^### |^## |$)`, "m");
1522
+ const m = body.match(re);
1523
+ return m ? m[1] : "";
1524
+ }
1525
+
1526
+ function parseListField(text) {
1527
+ if (!text) return [];
1528
+ return text
1529
+ .split(/\n/)
1530
+ .map((line) => line.trim())
1531
+ .filter((line) => line.startsWith("- "))
1532
+ .map((line) => line.slice(2).trim());
1533
+ }
1534
+
1535
+ function escapeRegExp(value) {
1536
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1537
+ }
1538
+
1539
+ function markdownSection(content, title) {
1540
+ const heading = new RegExp(`^## ${escapeRegExp(title)}[ \\t]*$`, "m");
1541
+ const match = heading.exec(content);
1542
+ if (!match) return null;
1543
+ const bodyStart = match.index + match[0].length;
1544
+ const tail = content.slice(bodyStart);
1545
+ const next = /^## [^\n]+$/m.exec(tail);
1546
+ const end = next ? bodyStart + next.index : content.length;
1547
+ return {
1548
+ headingStart: match.index,
1549
+ bodyStart,
1550
+ end,
1551
+ body: content.slice(bodyStart, end)
1552
+ };
1553
+ }
1554
+
1555
+ function decisionBlocks(section) {
1556
+ const headings = [...section.body.matchAll(/^### (D-\d{3}) - ([^\r\n]+)[ \t]*$/gm)];
1557
+ return headings.map((heading, index) => {
1558
+ const localStart = heading.index;
1559
+ const localEnd = index + 1 < headings.length ? headings[index + 1].index : section.body.length;
1560
+ const rawWithSpacing = section.body.slice(localStart, localEnd);
1561
+ const raw = rawWithSpacing.trimEnd();
1562
+ return {
1563
+ id: heading[1],
1564
+ title: heading[2].trim(),
1565
+ body: raw.slice(heading[0].length),
1566
+ raw,
1567
+ start: section.bodyStart + localStart,
1568
+ end: section.bodyStart + localStart + raw.length,
1569
+ maskStart: localStart,
1570
+ maskEnd: localEnd
1571
+ };
1572
+ });
1573
+ }
1574
+
1575
+ function parseDecisionsContent(content, file = null) {
1576
+ const result = { open: [], resolved: [], file };
1577
+ for (const [title, target] of [["Open Decisions", "open"], ["Resolved Decisions", "resolved"]]) {
1578
+ const section = markdownSection(content, title);
1579
+ if (!section) continue;
1580
+ const blocks = decisionBlocks(section);
1581
+ for (const block of blocks) {
1582
+ const { id, title: decisionTitle, body } = block;
1583
+ const statusMatch = body.match(/^Status:\s*(.+)$/m);
1584
+ const dateMatch = body.match(/^Date:\s*(.+)$/m);
1585
+ const sourceMatch = body.match(/^Source:\s*(.+)$/m);
1586
+ const extendsMatch = body.match(/^Extends:\s*(D-\d{3})/m);
1587
+ const supersedesMatch = body.match(/^Supersedes:\s*(D-\d{3})/m);
1588
+ const validWhileText = extractFieldBody(body, "Valid while:");
1589
+ const reviewWhenText = extractFieldBody(body, "Review when:");
1590
+ const expiredMatch = body.match(/^Expired:\s*(.+)$/m);
1591
+ result[target].push({
1592
+ id,
1593
+ title: decisionTitle,
1594
+ status: (statusMatch ? statusMatch[1].trim() : "open").toLowerCase(),
1595
+ date: dateMatch ? dateMatch[1].trim() : null,
1596
+ source: sourceMatch ? sourceMatch[1].trim() : null,
1597
+ extendsId: extendsMatch ? extendsMatch[1] : null,
1598
+ supersedesId: supersedesMatch ? supersedesMatch[1] : null,
1599
+ validWhile: parseListField(validWhileText),
1600
+ reviewWhen: parseListField(reviewWhenText),
1601
+ expired: expiredMatch ? expiredMatch[1].trim() : null,
1602
+ raw: block.raw,
1603
+ start: block.start,
1604
+ end: block.end
1605
+ });
1606
+ }
1607
+
1608
+ const withoutStructuredBlocks = [...section.body];
1609
+ for (const block of blocks) {
1610
+ for (let i = block.maskStart; i < block.maskEnd; i++) {
1611
+ if (withoutStructuredBlocks[i] !== "\n" && withoutStructuredBlocks[i] !== "\r") {
1612
+ withoutStructuredBlocks[i] = " ";
1613
+ }
1614
+ }
1615
+ }
1616
+ const bullets = [...withoutStructuredBlocks.join("").matchAll(/^- (.+)$/gm)]
1617
+ .map((match) => match[1].trim())
1618
+ .filter((text) => text !== "Pending: none.");
1619
+ for (const bullet of bullets) {
1620
+ result[target].push({
1621
+ id: null,
1622
+ title: bullet,
1623
+ status: target === "resolved" ? "resolved" : "open",
1624
+ date: null,
1625
+ source: null,
1626
+ extendsId: null,
1627
+ supersedesId: null,
1628
+ validWhile: [],
1629
+ reviewWhen: [],
1630
+ expired: null,
1631
+ raw: null,
1632
+ start: null,
1633
+ end: null
1634
+ });
1635
+ }
1636
+ }
1637
+ return result;
1638
+ }
1639
+
1640
+ function parseDecisions(cwd) {
1641
+ const file = decisionsFile(cwd);
1642
+ return parseDecisionsContent(readIfExists(file), file);
1643
+ }
1644
+
1645
+ function nextDecisionId(cwd) {
1646
+ const { open, resolved } = parseDecisions(cwd);
1647
+ const all = [...open, ...resolved];
1648
+ const max = all.reduce((acc, d) => {
1649
+ if (!d.id) return acc;
1650
+ const n = Number(d.id.slice(2));
1651
+ return Number.isFinite(n) && n > acc ? n : acc;
1652
+ }, 0);
1653
+ return `D-${String(max + 1).padStart(3, "0")}`;
1654
+ }
1655
+
1656
+ function tokenize(text) {
1657
+ return text
1658
+ .toLowerCase()
1659
+ .replace(/[^a-z0-9\s-]/g, " ")
1660
+ .split(/\s+/)
1661
+ .filter((t) => t.length >= 3);
1662
+ }
1663
+
1664
+ function decisionSimilarity(a, b) {
1665
+ const aTokens = new Set(tokenize(a));
1666
+ const bTokens = new Set(tokenize(b));
1667
+ if (!aTokens.size || !bTokens.size) return 0;
1668
+ let inter = 0;
1669
+ for (const t of aTokens) if (bTokens.has(t)) inter++;
1670
+ const union = new Set([...aTokens, ...bTokens]).size;
1671
+ return union ? inter / union : 0;
1672
+ }
1673
+
1674
+ function findSimilarDecisions(text, decisions, { threshold = 0.3, limit = 5 } = {}) {
1675
+ const matches = [];
1676
+ for (const list of [decisions.open, decisions.resolved]) {
1677
+ for (const d of list) {
1678
+ const score = decisionSimilarity(text, d.title);
1679
+ if (score >= threshold) {
1680
+ matches.push({ ...d, score });
1681
+ }
1682
+ }
1683
+ }
1684
+ matches.sort((a, b) => b.score - a.score);
1685
+ return matches.slice(0, limit);
1686
+ }
1687
+
1688
+ function serializeDecision(d) {
1689
+ const lines = [`### ${d.id} - ${d.title}`, `Status: ${d.status}`];
1690
+ if (d.date) lines.push(`Date: ${d.date}`);
1691
+ if (d.source) lines.push(`Source: ${d.source}`);
1692
+ if (d.extendsId) lines.push(`Extends: ${d.extendsId}`);
1693
+ if (d.supersedesId) lines.push(`Supersedes: ${d.supersedesId}`);
1694
+ if (d.validWhile && d.validWhile.length) {
1695
+ lines.push("Valid while:", ...d.validWhile.map((v) => `- ${v}`));
1696
+ }
1697
+ if (d.reviewWhen && d.reviewWhen.length) {
1698
+ lines.push("Review when:", ...d.reviewWhen.map((v) => `- ${v}`));
1699
+ }
1700
+ if (d.expired) lines.push(`Expired: ${d.expired}`);
1701
+ return lines.join("\n");
1702
+ }
1703
+
1704
+ function decisionDocument(content) {
1705
+ if (content.trim()) return content;
1706
+ return "# Main Goal Decisions\n\n## Open Decisions\n\n- Pending: none.\n\n## Resolved Decisions\n\n- Pending: none.\n";
1707
+ }
1708
+
1709
+ function prependDecisionBlock(content, sectionTitle, block) {
1710
+ const section = markdownSection(content, sectionTitle);
1711
+ if (!section) {
1712
+ return `${content.trimEnd()}\n\n## ${sectionTitle}\n\n${block.trim()}\n`;
1713
+ }
1714
+ const withoutPlaceholder = section.body.replace(/^[ \t]*- Pending: none\.[ \t]*(?:\r?\n)?/m, "");
1715
+ const remainder = withoutPlaceholder.replace(/^(?:\r?\n)+/, "");
1716
+ const body = `\n\n${block.trim()}\n${remainder.trim() ? `\n${remainder}` : ""}`;
1717
+ return `${content.slice(0, section.bodyStart)}${body}${content.slice(section.end)}`;
1718
+ }
1719
+
1720
+ function ensureDecisionSectionPlaceholder(content, sectionTitle) {
1721
+ const section = markdownSection(content, sectionTitle);
1722
+ if (!section || section.body.trim()) return content;
1723
+ return `${content.slice(0, section.bodyStart)}\n\n- Pending: none.\n\n${content.slice(section.end)}`;
1724
+ }
1725
+
1726
+ function updateDecisionStatus(block, status) {
1727
+ if (/^Status:\s*.+$/m.test(block)) {
1728
+ return block.replace(/^Status:\s*.+$/m, `Status: ${status}`);
1729
+ }
1730
+ const newline = block.indexOf("\n");
1731
+ if (newline === -1) return `${block}\nStatus: ${status}`;
1732
+ return `${block.slice(0, newline + 1)}Status: ${status}\n${block.slice(newline + 1)}`;
1733
+ }
1734
+
1735
+ function appendDecisionHistory(cwd, id) {
1736
+ const file = path.join(cwd, ".scrumrun", "goals", "main", "history.md");
1737
+ const marker = `${id} · open-to-resolved`;
1738
+ const current = readIfExists(file) || "# Main Goal History\n";
1739
+ if (current.includes(marker)) return;
1740
+ const event = `## ${today()} · ${marker}\n\nOwner: ScrumRun CLI\nReason: Decision resolved through \`scrumrun decisions --resolve\`.\nStatus: completed.\n`;
1741
+ writeFile(file, `${current.trimEnd()}\n\n${event}`, { backup: true });
1742
+ }
1743
+
1744
+ function addDecision(cwd, title, { date = today(), source = null, validWhile = [], reviewWhen = [], extendsId = null, supersedesId = null } = {}) {
1745
+ if (containsSecret([title, source, ...validWhile, ...reviewWhen].filter(Boolean).join("\n"))) {
1746
+ throw new Error("Secret-like content is forbidden outside the local vault.");
1747
+ }
1748
+ const file = decisionsFile(cwd);
1749
+ const id = nextDecisionId(cwd);
1750
+ const block = serializeDecision({
1751
+ id,
1752
+ title: title.trim(),
1753
+ status: "open",
1754
+ date,
1755
+ source,
1756
+ extendsId,
1757
+ supersedesId,
1758
+ validWhile,
1759
+ reviewWhen,
1760
+ expired: null
1761
+ });
1762
+ const current = decisionDocument(readIfExists(file));
1763
+ const next = prependDecisionBlock(current, "Open Decisions", block);
1764
+ const parsed = parseDecisionsContent(next, file);
1765
+ if (parsed.open.filter((decision) => decision.id === id).length !== 1) {
1766
+ throw new Error(`Decision write validation failed for ${id}.`);
1767
+ }
1768
+ writeFile(file, next, { backup: true });
1769
+ return id;
1770
+ }
1771
+
1772
+ function resolveDecision(cwd, id) {
1773
+ const file = decisionsFile(cwd);
1774
+ const current = readIfExists(file);
1775
+ const decisions = parseDecisionsContent(current, file);
1776
+ const moved = decisions.open.find((decision) => decision.id === id && decision.raw);
1777
+ if (!moved) return false;
1778
+
1779
+ let next = `${current.slice(0, moved.start)}${current.slice(moved.end)}`;
1780
+ next = ensureDecisionSectionPlaceholder(next, "Open Decisions");
1781
+ next = prependDecisionBlock(next, "Resolved Decisions", updateDecisionStatus(moved.raw, "resolved"));
1782
+
1783
+ const parsed = parseDecisionsContent(next, file);
1784
+ const stillOpen = parsed.open.some((decision) => decision.id === id);
1785
+ const resolvedCount = parsed.resolved.filter((decision) => decision.id === id).length;
1786
+ if (stillOpen || resolvedCount !== 1) {
1787
+ throw new Error(`Decision transition validation failed for ${id}.`);
1788
+ }
1789
+ writeFile(file, next, { backup: true });
1790
+ appendDecisionHistory(cwd, id);
1791
+ return true;
1792
+ }
1793
+
1794
+ function runDecisions(parts) {
1795
+ const cwd = process.cwd();
1796
+ const action = parts[0];
1797
+
1798
+ if (!action || action === "list" || action === "--list" || action === "-l") {
1799
+ const decisions = parseDecisions(cwd);
1800
+ const format = (d) => `${d.id || "no-id"} | ${d.status} | ${d.title}${d.expired ? ` (expired: ${d.expired})` : ""}`;
1801
+ console.log("# Open");
1802
+ console.log(decisions.open.length ? decisions.open.map(format).join("\n") : " - none");
1803
+ console.log("\n# Resolved");
1804
+ console.log(decisions.resolved.length ? decisions.resolved.map(format).join("\n") : " - none");
1805
+ return;
1806
+ }
1807
+
1808
+ if (action === "similar" || action === "--similar" || action === "-s") {
1809
+ const text = joinArgs(parts.slice(1));
1810
+ if (!text) {
1811
+ console.error("Usage: scrumrun decisions --similar <text>");
1812
+ process.exitCode = 1;
1813
+ return;
1814
+ }
1815
+ const decisions = parseDecisions(cwd);
1816
+ const matches = findSimilarDecisions(text, decisions);
1817
+ if (!matches.length) {
1818
+ console.log("No similar decisions found.");
1819
+ return;
1820
+ }
1821
+ for (const m of matches) {
1822
+ const pct = Math.round(m.score * 100);
1823
+ const valid = m.expired ? `expired (${m.expired})` : m.status;
1824
+ console.log(`${m.id || "no-id"} | ${valid} | ${pct}% similar | ${m.title}`);
1825
+ }
1826
+ return;
1827
+ }
1828
+
1829
+ if (action === "add" || action === "--add" || action === "-a") {
1830
+ const sourceIdx = parts.findIndex((p) => p === "--source" || p === "-s");
1831
+ const source = sourceIdx > 0 ? parts[sourceIdx + 1] : null;
1832
+ const title = joinArgs(parts.slice(1).filter((p, i, arr) => {
1833
+ if (p === "--source" || p === "-s") return false;
1834
+ if (i > 0 && (arr[i - 1] === "--source" || arr[i - 1] === "-s")) return false;
1835
+ return true;
1836
+ }));
1837
+ if (!title) {
1838
+ console.error("Usage: scrumrun decisions --add <title> [--source <ref>]");
1839
+ process.exitCode = 1;
1840
+ return;
1841
+ }
1842
+ const decisions = parseDecisions(cwd);
1843
+ const similar = findSimilarDecisions(title, decisions);
1844
+ const id = addDecision(cwd, title, source ? { source } : {});
1845
+ console.log(`Added decision: ${id}`);
1846
+ if (similar.length) {
1847
+ console.log("\nSimilar decisions found — review before relying on this new one:");
1848
+ for (const m of similar) {
1849
+ const pct = Math.round(m.score * 100);
1850
+ console.log(` ${m.id || "no-id"} | ${m.status} | ${pct}% similar | ${m.title}`);
1851
+ }
1852
+ console.log("\nIf the new decision reuses an existing one, edit and add Extends: D-NNN.");
1853
+ console.log("If it supersedes, add Supersedes: D-NNN.");
1854
+ console.log("If it's genuinely new, ignore this list.");
1855
+ }
1856
+ return;
1857
+ }
1858
+
1859
+ if (action === "resolve" || action === "--resolve" || action === "-r") {
1860
+ const id = parts[1];
1861
+ if (!id) {
1862
+ console.error("Usage: scrumrun decisions --resolve <D-NNN>");
1863
+ process.exitCode = 1;
1864
+ return;
1865
+ }
1866
+ if (resolveDecision(cwd, id)) {
1867
+ console.log(`Resolved decision: ${id}`);
1868
+ } else {
1869
+ console.error(`Open decision not found: ${id}`);
1870
+ process.exitCode = 1;
1871
+ }
1872
+ return;
1873
+ }
1874
+
1875
+ if (action === "check" || action === "--check" || action === "-c") {
1876
+ const id = parts[1];
1877
+ if (!id) {
1878
+ console.error("Usage: scrumrun decisions --check <D-NNN>");
1879
+ process.exitCode = 1;
1880
+ return;
1881
+ }
1882
+ const decisions = parseDecisions(cwd);
1883
+ const all = [...decisions.open, ...decisions.resolved];
1884
+ const d = all.find((x) => x.id === id);
1885
+ if (!d) {
1886
+ console.error(`Decision not found: ${id}`);
1887
+ process.exitCode = 1;
1888
+ return;
1889
+ }
1890
+ console.log(`# ${d.id} - ${d.title}`);
1891
+ console.log(`Status: ${d.status}${d.expired ? ` (expired: ${d.expired})` : ""}`);
1892
+ if (d.date) console.log(`Date: ${d.date}`);
1893
+ if (d.source) console.log(`Source: ${d.source}`);
1894
+ if (d.extendsId) console.log(`Extends: ${d.extendsId}`);
1895
+ if (d.supersedesId) console.log(`Supersedes: ${d.supersedesId}`);
1896
+ if (d.validWhile.length) {
1897
+ console.log(`\nValid while:`);
1898
+ for (const v of d.validWhile) console.log(` - ${v}`);
1899
+ }
1900
+ if (d.reviewWhen.length) {
1901
+ console.log(`\nReview when:`);
1902
+ for (const v of d.reviewWhen) console.log(` - ${v}`);
1903
+ }
1904
+ if (d.expired) {
1905
+ console.log(`\nThis decision is marked as expired. Review and either:`);
1906
+ console.log(` - Add Extends: D-NNN to a new decision that supersedes it, or`);
1907
+ console.log(` - Edit and remove the Expired line if it is still valid.`);
1908
+ }
1909
+ return;
1910
+ }
1911
+
1912
+ console.error("Usage: scrumrun decisions [--list|--similar|--add|--resolve|--check]");
1913
+ process.exitCode = 1;
1914
+ }
1915
+
874
1916
  function statusProject() {
875
1917
  const cwd = process.cwd();
876
1918
  const scrumDir = path.join(cwd, ".scrumrun");
1919
+ const methodMarker = readIfExists(path.join(scrumDir, "method.json"));
1920
+ let method = null;
1921
+ try {
1922
+ method = methodMarker ? JSON.parse(methodMarker).method : null;
1923
+ } catch {
1924
+ method = "invalid";
1925
+ }
1926
+ if (method === "2.0.0") {
1927
+ const repository = new ArtifactRepository(scrumDir);
1928
+ console.log(`# ScrumRun Status - ${path.basename(cwd)}`);
1929
+ console.log("");
1930
+ console.log("Method: 2.0.0");
1931
+ console.log(`Guardrails: ${fs.existsSync(path.join(scrumDir, "guardrails.md")) ? "present" : "missing"}`);
1932
+ console.log(`Generated state: ${fs.existsSync(path.join(scrumDir, "state.md")) ? "present" : "missing"}`);
1933
+ console.log("");
1934
+ console.log("## Artifacts");
1935
+ for (const kind of Object.keys(ARTIFACT_TYPES)) {
1936
+ const artifacts = repository.list(kind);
1937
+ const active = artifacts.filter((artifact) => artifact.record && !["completed", "resolved", "rejected", "deprecated", "invalidated", "archived", "cancelled", "passed"].includes(artifact.record.status));
1938
+ console.log(`- ${kind}: ${artifacts.length} total, ${active.length} active`);
1939
+ }
1940
+ console.log("");
1941
+ console.log(`Migration record: ${fs.existsSync(path.join(scrumDir, ".migration", "v1-to-v2", "manifest.json")) ? "v1-to-v2 applied" : "not applicable / native v2"}`);
1942
+ console.log(`Canonical command: /sc`);
1943
+ return;
1944
+ }
877
1945
  const required = [
878
1946
  "core.md",
879
1947
  "config.md",
@@ -927,24 +1995,24 @@ function promptCommand(parts) {
927
1995
  const kind = parts[0];
928
1996
  const text = joinText(parts.slice(1));
929
1997
  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}` : ""}`
1998
+ intake: `/sc plan intake ${text}`.trim(),
1999
+ study: `/sc knowledge study${text ? ` ${text}` : ""}`,
2000
+ challenge: `/sc plan challenge ${text}`.trim(),
2001
+ know: `/sc knowledge fact --add ${text}`.trim(),
2002
+ "context-build": `/sc knowledge context --build${text ? ` ${text}` : ""}`,
2003
+ "context-update": `/sc knowledge context --update${text ? ` ${text}` : ""}`,
2004
+ "context-show": `/sc knowledge context --show${text ? ` ${text}` : ""}`,
2005
+ context: `/sc knowledge context ${text}`.trim(),
2006
+ vault: `/sc knowledge vault ${text}`.trim(),
2007
+ "goal-set": `/sc plan feature --add ${text}`.trim(),
2008
+ "goal-new": `/sc plan feature --add ${text}`.trim(),
2009
+ goal: `/sc plan feature --add ${text}`.trim(),
2010
+ "sprint-add": `/sc plan sprint --add ${text}`.trim(),
2011
+ "sprint-new": `/sc plan sprint --add ${text}`.trim(),
2012
+ "sprint-run": `/sc plan task --run ${text}`.trim(),
2013
+ "backlog-add": `/sc plan task --add ${text} --status backlog`.trim(),
2014
+ backlog: `/sc plan task --add ${text} --status backlog`.trim(),
2015
+ status: `/sc plan sprint --show${text ? ` ${text}` : ""}`
948
2016
  };
949
2017
  if (!kind || !prompts[kind]) {
950
2018
  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 +2022,22 @@ function promptCommand(parts) {
954
2022
  console.log(prompts[kind]);
955
2023
  }
956
2024
 
957
- function initProject({ force, mode, agentHint }) {
2025
+ function initProject({ force, mode, agentHint, lean }) {
958
2026
  const cwd = process.cwd();
959
2027
  const vars = {
960
2028
  PROJECT_NAME: path.basename(cwd),
961
2029
  DATE: new Date().toISOString().slice(0, 10)
962
2030
  };
963
2031
  const projectTemplate = path.join(templates, "project");
2032
+ const agentTemplate = path.join(templates, lean ? "project-lean" : "project", "AGENTS.md");
964
2033
  const results = [];
965
2034
 
966
2035
  results.push(...copyDir(path.join(projectTemplate, ".scrumrun"), path.join(cwd, ".scrumrun"), { force, vars }));
967
2036
  results.push(copyFile(path.join(root, "CORE.md"), path.join(cwd, ".scrumrun", "core.md"), { force, vars }));
2037
+ results.push(ensureProjectIgnore(cwd));
968
2038
 
969
2039
  if (mode === "shared" || agentHint) {
970
- results.push(copyFile(path.join(projectTemplate, "AGENTS.md"), path.join(cwd, "AGENTS.md"), { force, vars }));
2040
+ results.push(copyFile(agentTemplate, path.join(cwd, "AGENTS.md"), { force, vars }));
971
2041
  } else {
972
2042
  results.push({ status: "skipped", dest: path.join(cwd, "AGENTS.md") });
973
2043
  }
@@ -979,14 +2049,20 @@ function initProject({ force, mode, agentHint }) {
979
2049
  }
980
2050
  }
981
2051
  results.push(addLocalExclude(cwd, ".scrumrun/vault.local.md"));
2052
+ if (v2Project(cwd)) refreshState(path.join(cwd, ".scrumrun"));
982
2053
 
983
- const title = mode === "local"
2054
+ const title = lean
2055
+ ? `Initialized ScrumRun 2.0 project (lean read policy)`
2056
+ : mode === "local"
984
2057
  ? "Initialized local ScrumRun project"
985
2058
  : "Initialized shared ScrumRun project";
986
2059
  printResults(title, results);
987
2060
  if (mode === "local" && hasGitRepo(cwd)) {
988
2061
  console.log("\nLocal mode keeps ScrumRun out of commits by writing .scrumrun/ to .git/info/exclude.");
989
2062
  }
2063
+ if (lean) {
2064
+ console.log("\nLean mode stores complete v2 truth but tells agents to read only guardrails, state, and relevant ids by default.");
2065
+ }
990
2066
  }
991
2067
 
992
2068
  function uninstallProject(force) {
@@ -1015,7 +2091,7 @@ function moveIfPossible(cwd, from, to) {
1015
2091
  return { status: "moved", dest: `${src} -> ${dest}` };
1016
2092
  }
1017
2093
 
1018
- function migrateProject() {
2094
+ function migrateLegacyProject() {
1019
2095
  const cwd = process.cwd();
1020
2096
  const moves = [
1021
2097
  ["asm-config.md", ".scrumrun/config.md"],
@@ -1077,36 +2153,143 @@ function migrateProject() {
1077
2153
  printResults("Migrated project", results.filter((result) => result.status !== "missing"));
1078
2154
  }
1079
2155
 
1080
- function doctor(target = "all") {
2156
+ function runMigration(parts) {
2157
+ if (parts.includes("--legacy-layout")) {
2158
+ console.warn("Deprecated: --legacy-layout only migrates pre-.scrumrun files into the v1 layout.");
2159
+ migrateLegacyProject();
2160
+ return;
2161
+ }
2162
+ const toIndex = parts.indexOf("--to");
2163
+ const target = toIndex >= 0 ? parts[toIndex + 1] : null;
2164
+ const actions = ["--dry-run", "--apply", "--rollback"].filter((action) => parts.includes(action));
2165
+ if (target !== "2" || actions.length !== 1) {
2166
+ console.error("Usage: scrumrun migrate --to 2 [--dry-run|--apply|--rollback]");
2167
+ process.exitCode = 1;
2168
+ return;
2169
+ }
2170
+ try {
2171
+ const ledgerManifest = path.join(process.cwd(), ".scrumrun", ".migration", "run-ledger-v1", "manifest.json");
2172
+ if (v2Project() && actions[0] !== "--rollback") {
2173
+ if (actions[0] === "--dry-run") {
2174
+ const plan = planRunLedgerMigration(process.cwd());
2175
+ console.log(reportRunLedgerMigration(plan).trimEnd());
2176
+ if (plan.status === "blocked") process.exitCode = 1;
2177
+ } else {
2178
+ const result = applyRunLedgerMigration(process.cwd());
2179
+ if (result.status === "current") {
2180
+ console.log("ScrumRun project is already migrated to method 2.0.0 and the current Run ledger; no files changed.");
2181
+ } else {
2182
+ refreshState(path.join(process.cwd(), ".scrumrun"));
2183
+ console.log(result.output.trimEnd());
2184
+ console.log("Rollback remains available with: scrumrun migrate --to 2 --rollback");
2185
+ }
2186
+ }
2187
+ return;
2188
+ }
2189
+ if (actions[0] === "--rollback" && fs.existsSync(ledgerManifest)) {
2190
+ const manifest = JSON.parse(fs.readFileSync(ledgerManifest, "utf8"));
2191
+ if (["applied", "prepared"].includes(manifest.status)) {
2192
+ const result = rollbackRunLedgerMigration(process.cwd());
2193
+ refreshState(path.join(process.cwd(), ".scrumrun"));
2194
+ console.log(`Rolled back ScrumRun Run ledger migration: ${result.status}.`);
2195
+ return;
2196
+ }
2197
+ }
2198
+ if (actions[0] === "--dry-run") {
2199
+ const result = dryRunMigration(process.cwd());
2200
+ console.log(result.output.trimEnd());
2201
+ if (result.status === "blocked") {
2202
+ for (const error of result.plan.errors) console.error(`BLOCKED: ${error}`);
2203
+ process.exitCode = 1;
2204
+ }
2205
+ } else if (actions[0] === "--apply") {
2206
+ const result = applyMigration(process.cwd());
2207
+ if (result.status === "already-applied") {
2208
+ console.log("ScrumRun project is already migrated to method 2.0.0; no files changed.");
2209
+ } else {
2210
+ console.log("Applied ScrumRun v1 to v2 migration.");
2211
+ console.log(`Source fingerprint: ${result.manifest.sourceInventory.rootSha256}`);
2212
+ console.log(`Generated artifacts: ${result.manifest.generated.length}`);
2213
+ console.log("Rollback remains available with: scrumrun migrate --to 2 --rollback");
2214
+ }
2215
+ } else {
2216
+ const result = rollbackMigration(process.cwd());
2217
+ console.log("Rolled back ScrumRun v2 migration and restored the exact v1 fingerprint.");
2218
+ console.log(`Restored fingerprint: ${result.rootSha256}`);
2219
+ }
2220
+ } catch (error) {
2221
+ console.error(error.message);
2222
+ process.exitCode = 1;
2223
+ }
2224
+ }
2225
+
2226
+ function doctor(target = "all", { compatibility = false, strict = false, recover = false } = {}) {
1081
2227
  const home = os.homedir();
1082
2228
  const checks = [];
2229
+ const commands = compatibility ? [...COMMANDS, ...COMPATIBILITY_COMMANDS] : COMMANDS;
2230
+ const skillContent = fs.readFileSync(path.join(templates, "shared", "skills", "scrumrun", "SKILL.md"), "utf8");
2231
+
2232
+ if (recover) {
2233
+ const scrumDir = path.join(process.cwd(), ".scrumrun");
2234
+ if (!fs.existsSync(scrumDir)) throw new Error("Cannot recover transactions outside a ScrumRun project.");
2235
+ const recovered = recoverPendingTransactions(scrumDir);
2236
+ console.log(recovered.length
2237
+ ? `recovered ${recovered.map((item) => `${item.id}:${item.action}`).join(", ")}`
2238
+ : "recovered no pending kernel transactions");
2239
+ refreshState(scrumDir);
2240
+ }
2241
+
2242
+ function commandContent(command) {
2243
+ return command === "sc" ? renderRootPrompt() : renderCompatibilityPrompt(command);
2244
+ }
1083
2245
 
1084
2246
  if (target === "all" || target === "codex") {
1085
- for (const command of COMMANDS) {
1086
- checks.push([`Codex prompt ${command}`, path.join(home, ".codex", "prompts", `${command}.md`)]);
2247
+ for (const command of commands) {
2248
+ checks.push([`Codex prompt ${command}`, path.join(home, ".codex", "prompts", `${command}.md`), commandContent(command)]);
1087
2249
  }
1088
- checks.push(["Codex skill scrumrun", path.join(home, ".codex", "skills", "scrumrun", "SKILL.md")]);
2250
+ checks.push(["Codex skill scrumrun", path.join(home, ".codex", "skills", "scrumrun", "SKILL.md"), skillContent]);
1089
2251
  }
1090
2252
 
1091
2253
  if (target === "all" || target === "claude") {
1092
- for (const command of COMMANDS) {
1093
- checks.push([`Claude command ${command}`, path.join(home, ".claude", "commands", `${command}.md`)]);
2254
+ for (const command of commands) {
2255
+ checks.push([`Claude command ${command}`, path.join(home, ".claude", "commands", `${command}.md`), commandContent(command)]);
1094
2256
  }
1095
- checks.push(["Claude skill scrumrun", path.join(home, ".claude", "skills", "scrumrun", "SKILL.md")]);
2257
+ checks.push(["Claude skill scrumrun", path.join(home, ".claude", "skills", "scrumrun", "SKILL.md"), skillContent]);
1096
2258
  }
1097
2259
 
1098
2260
  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`)]);
2261
+ for (const command of commands) {
2262
+ checks.push([`OpenCode command ${command}`, path.join(home, ".config", "opencode", "commands", `${command}.md`), commandContent(command)]);
2263
+ }
2264
+ checks.push(["OpenCode skill scrumrun", path.join(home, ".config", "opencode", "skills", "scrumrun", "SKILL.md"), skillContent]);
2265
+ }
2266
+
2267
+ const [nodeMajor, nodeMinor] = process.versions.node.split(".").map(Number);
2268
+ const nodeOk = nodeMajor > 22 || (nodeMajor === 22 && nodeMinor >= 13);
2269
+ console.log(`${nodeOk ? "ok " : "miss"} Node.js >=22.13.0: ${process.versions.node}`);
2270
+ let ok = nodeOk;
2271
+ for (const [label, file, expected] of checks) {
2272
+ const safeFile = fs.existsSync(file) && fs.lstatSync(file).isFile() && !fs.lstatSync(file).isSymbolicLink();
2273
+ const current = safeFile ? fs.readFileSync(file, "utf8") : null;
2274
+ const matches = safeFile && current === expected;
2275
+ ok = ok && matches;
2276
+ const state = !safeFile ? "miss" : matches ? "ok " : "stale";
2277
+ const hashes = safeFile && !matches
2278
+ ? ` (expected ${crypto.createHash("sha256").update(expected).digest("hex").slice(0, 12)}, found ${crypto.createHash("sha256").update(current).digest("hex").slice(0, 12)})`
2279
+ : "";
2280
+ console.log(`${state} ${label}: ${file}${hashes}`);
2281
+ }
2282
+ if (strict) {
2283
+ const scrumDir = path.join(process.cwd(), ".scrumrun");
2284
+ if (!fs.existsSync(scrumDir)) {
2285
+ ok = false;
2286
+ console.log(`miss ScrumRun project audit: ${scrumDir}`);
2287
+ } else {
2288
+ const audit = auditProject(process.cwd());
2289
+ ok = ok && audit.passed && audit.findings.length === 0;
2290
+ console.log(`${audit.passed && audit.findings.length === 0 ? "ok " : "fail"} ScrumRun project audit: ${audit.findings.length} finding(s)`);
2291
+ for (const item of audit.findings) console.log(` ${item.severity} ${item.code}: ${item.message}`);
1101
2292
  }
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
2293
  }
1111
2294
  process.exitCode = ok ? 0 : 1;
1112
2295
  }
@@ -1117,6 +2300,7 @@ const force = args.includes("--force") || args.includes("-f");
1117
2300
  const local = args.includes("--local") || args.includes("-l");
1118
2301
  const shared = args.includes("--shared") || args.includes("-s");
1119
2302
  const noAgentHint = args.includes("--no-agent-hint") || args.includes("-n");
2303
+ const lean = args.includes("--lean");
1120
2304
 
1121
2305
  if (!command || command === "--help" || command === "-h") {
1122
2306
  usage();
@@ -1124,13 +2308,18 @@ if (!command || command === "--help" || command === "-h") {
1124
2308
  console.log(`ScrumRun ${version}`);
1125
2309
  } else if (command === "install" || command === "update") {
1126
2310
  const target = ["all", "codex", "opencode", "claude"].includes(args[1]) ? args[1] : "all";
1127
- install(target, true);
2311
+ if (command === "update") updateInstallation(target, { migrate: args.includes("--migrate") });
2312
+ else install(target, true, { compatibility: false });
2313
+ } else if (command === "sc") {
2314
+ runRoot(args.slice(1));
2315
+ } else if (COMMAND_ALIASES[command]) {
2316
+ runCompatibilityAlias(command, args.slice(1));
1128
2317
  } else if (command === "init") {
1129
2318
  if (local && shared) {
1130
2319
  console.error("Use either --local or --shared, not both.");
1131
2320
  process.exitCode = 1;
1132
2321
  } else {
1133
- initProject({ force, mode: shared ? "shared" : "local", agentHint: shared || !noAgentHint });
2322
+ initProject({ force, mode: shared ? "shared" : "local", agentHint: shared || !noAgentHint, lean });
1134
2323
  }
1135
2324
  } else if (command === "uninstall") {
1136
2325
  uninstallProject(force);
@@ -1140,6 +2329,10 @@ if (!command || command === "--help" || command === "-h") {
1140
2329
  printCore({ pathOnly: args.includes("--path"), promptOnly: args.includes("--prompt") });
1141
2330
  } else if (command === "commands") {
1142
2331
  commandList();
2332
+ } else if (command === "guardrails") {
2333
+ runGuardrails(args.slice(1));
2334
+ } else if (command === "decisions") {
2335
+ runDecisions(args.slice(1));
1143
2336
  } else if (command === "context") {
1144
2337
  runContext(args.slice(1));
1145
2338
  } else if (command === "vault") {
@@ -1157,16 +2350,16 @@ if (!command || command === "--help" || command === "-h") {
1157
2350
  } else if (command === "prompt") {
1158
2351
  promptCommand(args.slice(1));
1159
2352
  } else if (command === "migrate") {
1160
- migrateProject();
2353
+ runMigration(args.slice(1));
1161
2354
  } else if (command === "doctor") {
1162
2355
  const target = ["all", "codex", "opencode", "claude"].includes(args[1]) ? args[1] : "all";
1163
- doctor(target);
2356
+ doctor(target, { compatibility: args.includes("--compat"), strict: args.includes("--strict"), recover: args.includes("--recover") });
1164
2357
  } else if (command === "claude") {
1165
2358
  const sub = args[1];
1166
2359
  if (sub === "install" || sub === "update") {
1167
- installClaude(true);
2360
+ installClaude(true, { compatibility: sub === "update" });
1168
2361
  } else if (sub === "doctor") {
1169
- doctor("claude");
2362
+ doctor("claude", { compatibility: args.includes("--compat"), strict: args.includes("--strict") });
1170
2363
  } else {
1171
2364
  console.error("Usage: scrumrun claude [install|update|doctor]");
1172
2365
  process.exitCode = 1;
@@ -1174,9 +2367,9 @@ if (!command || command === "--help" || command === "-h") {
1174
2367
  } else if (command === "codex") {
1175
2368
  const sub = args[1];
1176
2369
  if (sub === "install" || sub === "update") {
1177
- installCodex(true);
2370
+ installCodex(true, { compatibility: sub === "update" });
1178
2371
  } else if (sub === "doctor") {
1179
- doctor("codex");
2372
+ doctor("codex", { compatibility: args.includes("--compat"), strict: args.includes("--strict") });
1180
2373
  } else {
1181
2374
  console.error("Usage: scrumrun codex [install|update|doctor]");
1182
2375
  process.exitCode = 1;
@@ -1184,9 +2377,9 @@ if (!command || command === "--help" || command === "-h") {
1184
2377
  } else if (command === "opencode") {
1185
2378
  const sub = args[1];
1186
2379
  if (sub === "install" || sub === "update") {
1187
- installOpenCode(true);
2380
+ installOpenCode(true, { compatibility: sub === "update" });
1188
2381
  } else if (sub === "doctor") {
1189
- doctor("opencode");
2382
+ doctor("opencode", { compatibility: args.includes("--compat"), strict: args.includes("--strict") });
1190
2383
  } else {
1191
2384
  console.error("Usage: scrumrun opencode [install|update|doctor]");
1192
2385
  process.exitCode = 1;