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