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