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.
- package/CHANGELOG.md +64 -0
- package/CORE.md +231 -308
- package/DECISIONS.md +151 -11
- package/LICENSE +21 -0
- package/MIGRATION-1-to-2.md +120 -0
- package/README.md +147 -78
- package/SPEC.md +259 -251
- package/bin/claude-install.js +18 -132
- package/bin/scrumrun.js +1367 -174
- package/docs/COMMANDS.md +71 -0
- package/docs/ENTITY-MODEL.md +38 -0
- package/docs/RELEASE-SCORECARD.md +43 -0
- package/docs/RELEASE.md +51 -0
- package/docs/SCHEMA.md +89 -0
- package/docs/SEMANTIC-MEMORY.md +68 -0
- package/docs/TROUBLESHOOTING.md +78 -0
- package/lib/code-intel/adapter.js +20 -0
- package/lib/code-intel/javascript.js +199 -0
- package/lib/code-intel/learning.js +66 -0
- package/lib/code-intel/scanner.js +126 -0
- package/lib/commands/manifest.js +118 -0
- package/lib/commands/render.js +78 -0
- package/lib/memory/index.js +659 -0
- package/lib/memory/markdown.js +51 -0
- package/lib/memory/service.js +300 -0
- package/lib/runtime/budgets.js +23 -0
- package/lib/runtime/canonical-snapshot.js +110 -0
- package/lib/runtime/context.js +101 -0
- package/lib/runtime/orchestrator.js +303 -0
- package/lib/runtime/policy-engine.js +184 -0
- package/lib/runtime/request-engine.js +132 -0
- package/lib/runtime/run-ledger.js +324 -0
- package/lib/security/secrets.js +23 -0
- package/lib/v2/artifacts.js +363 -0
- package/lib/v2/conformance.js +214 -0
- package/lib/v2/migration.js +1221 -0
- package/lib/v2/project-store.js +44 -0
- package/lib/v2/run-ledger-migration.js +240 -0
- package/lib/v2/schema.js +148 -0
- package/lib/v2/transaction.js +254 -0
- package/package.json +16 -6
- package/scripts/generate-contract-docs.js +124 -0
- package/templates/project/.scrumrun/config.md +4 -7
- package/templates/project/.scrumrun/guardrails.md +31 -0
- package/templates/project/.scrumrun/map.md +5 -16
- package/templates/project/.scrumrun/memory/decisions/.gitkeep +1 -0
- package/templates/project/.scrumrun/memory/dossiers/.gitkeep +1 -0
- package/templates/project/.scrumrun/memory/insights/.gitkeep +1 -0
- package/templates/project/.scrumrun/memory/knowledge/.gitkeep +1 -0
- package/templates/project/.scrumrun/method.json +7 -0
- package/templates/project/.scrumrun/project.md +6 -12
- package/templates/project/.scrumrun/runs/.gitkeep +1 -0
- package/templates/project/.scrumrun/sprints/.gitkeep +1 -0
- package/templates/project/.scrumrun/state.md +14 -0
- package/templates/project/.scrumrun/tasks/.gitkeep +1 -0
- package/templates/project/AGENTS.md +16 -49
- package/templates/project-lean/AGENTS.md +18 -0
- package/templates/shared/skills/scrumrun/SKILL.md +211 -0
- package/templates/codex/prompts/sc-agent.md +0 -14
- package/templates/codex/prompts/sc-backlog.md +0 -13
- package/templates/codex/prompts/sc-challenge.md +0 -57
- package/templates/codex/prompts/sc-config.md +0 -18
- package/templates/codex/prompts/sc-context.md +0 -24
- package/templates/codex/prompts/sc-decisions.md +0 -8
- package/templates/codex/prompts/sc-feature.md +0 -16
- package/templates/codex/prompts/sc-fix.md +0 -21
- package/templates/codex/prompts/sc-goal.md +0 -14
- package/templates/codex/prompts/sc-golden.md +0 -14
- package/templates/codex/prompts/sc-help.md +0 -12
- package/templates/codex/prompts/sc-init.md +0 -14
- package/templates/codex/prompts/sc-intake.md +0 -22
- package/templates/codex/prompts/sc-know.md +0 -75
- package/templates/codex/prompts/sc-map.md +0 -13
- package/templates/codex/prompts/sc-review.md +0 -13
- package/templates/codex/prompts/sc-sprint.md +0 -28
- package/templates/codex/prompts/sc-study.md +0 -23
- package/templates/codex/prompts/sc-uninstall.md +0 -14
- package/templates/codex/prompts/sc-update.md +0 -8
- package/templates/codex/prompts/sc-vault.md +0 -27
- package/templates/codex/skills/scrumrun/SKILL.md +0 -412
- package/templates/opencode/commands/sc-agent.md +0 -14
- package/templates/opencode/commands/sc-backlog.md +0 -13
- package/templates/opencode/commands/sc-challenge.md +0 -57
- package/templates/opencode/commands/sc-config.md +0 -18
- package/templates/opencode/commands/sc-context.md +0 -24
- package/templates/opencode/commands/sc-decisions.md +0 -8
- package/templates/opencode/commands/sc-feature.md +0 -16
- package/templates/opencode/commands/sc-fix.md +0 -21
- package/templates/opencode/commands/sc-goal.md +0 -14
- package/templates/opencode/commands/sc-golden.md +0 -14
- package/templates/opencode/commands/sc-help.md +0 -12
- package/templates/opencode/commands/sc-init.md +0 -14
- package/templates/opencode/commands/sc-intake.md +0 -22
- package/templates/opencode/commands/sc-know.md +0 -75
- package/templates/opencode/commands/sc-map.md +0 -13
- package/templates/opencode/commands/sc-review.md +0 -13
- package/templates/opencode/commands/sc-sprint.md +0 -28
- package/templates/opencode/commands/sc-study.md +0 -23
- package/templates/opencode/commands/sc-uninstall.md +0 -14
- package/templates/opencode/commands/sc-update.md +0 -8
- package/templates/opencode/commands/sc-vault.md +0 -27
- package/templates/opencode/skills/scrumrun/SKILL.md +0 -412
- package/templates/project/.scrumrun/agents.md +0 -36
- package/templates/project/.scrumrun/backlog.md +0 -7
- package/templates/project/.scrumrun/context.md +0 -61
- package/templates/project/.scrumrun/goals/main/decisions.md +0 -9
- package/templates/project/.scrumrun/goals/main/history.md +0 -51
- package/templates/project/.scrumrun/goals/main/sprint.md +0 -54
- package/templates/project/.scrumrun/golden-rules.md +0 -9
- package/templates/project/.scrumrun/knowledge.md +0 -15
- package/templates/project/.scrumrun/runbook.md +0 -101
- 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
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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
|
|
52
|
-
scrumrun
|
|
53
|
-
scrumrun
|
|
54
|
-
scrumrun
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
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
|
-
|
|
202
|
-
...copyDir(
|
|
207
|
+
writeCommandAsset(path.join(commandsDir, "sc.md"), renderRootPrompt(), force),
|
|
208
|
+
...copyDir(skillTemplateDir, skillsDir, { force })
|
|
203
209
|
];
|
|
204
|
-
|
|
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
|
|
221
|
+
function installClaude(force, options = {}) {
|
|
208
222
|
const home = os.homedir();
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
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
|
|
233
|
+
function installCodex(force, options = {}) {
|
|
219
234
|
const home = os.homedir();
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
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(
|
|
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/
|
|
824
|
-
Follow ScrumRun
|
|
825
|
-
If
|
|
826
|
-
|
|
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
|
|
931
|
-
study: `/sc
|
|
932
|
-
challenge: `/sc
|
|
933
|
-
know: `/sc
|
|
934
|
-
"context-build": `/sc
|
|
935
|
-
"context-update": `/sc
|
|
936
|
-
"context-show": `/sc
|
|
937
|
-
context: `/sc
|
|
938
|
-
vault: `/sc
|
|
939
|
-
"goal-set": `/sc
|
|
940
|
-
"goal-new": `/sc
|
|
941
|
-
goal: `/sc
|
|
942
|
-
"sprint-add": `/sc
|
|
943
|
-
"sprint-new": `/sc
|
|
944
|
-
"sprint-run": `/sc
|
|
945
|
-
"backlog-add": `/sc
|
|
946
|
-
backlog: `/sc
|
|
947
|
-
status: `/sc
|
|
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(
|
|
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 =
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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;
|