scrumrun 4.1.0 → 4.1.2
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 +17 -0
- package/README.md +1 -1
- package/bin/scrumrun.js +16 -4
- package/lib/commands/repair.js +24 -3
- package/lib/runtime/policy-engine.js +2 -1
- package/lib/v2/conformance.js +4 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,23 @@ All notable changes follow Semantic Versioning.
|
|
|
4
4
|
|
|
5
5
|
## Unreleased
|
|
6
6
|
|
|
7
|
+
## 4.1.2 - 2026-09-22
|
|
8
|
+
|
|
9
|
+
### Fixed
|
|
10
|
+
|
|
11
|
+
- **`doctor --strict` now honors `allow_secrets_in`.** The audit pipeline was calling `containsSecret()` directly, ignoring the allowlist declared in `.scrumrun/config.md` frontmatter. Result: files listed under `allow_secrets_in` still triggered `SECRET_CANONICAL`. The audit now loads the allowlist once per run and calls `containsSecretWithAllowlist(text, relativePath, allowlist)` — a straight wiring bug from 4.0. Common false-positive triggers (`password: null`, `password:string`, `password` inside documentation) are now silenceable by whitelisting the specific file(s).
|
|
12
|
+
|
|
13
|
+
## 4.1.1 - 2026-09-22
|
|
14
|
+
|
|
15
|
+
### Added
|
|
16
|
+
|
|
17
|
+
- **`scrumrun update --repair-legacy`.** New flag runs `repair --apply` after refreshing project guidance, so a single `scrumrun update --project --repair-legacy` upgrades the CLI, refreshes packaged Markdown, and normalizes the local `.scrumrun/` tree in one shot. The repair pass covers status aliases, `feature:` free-text kebab labels, guardrail scopes, missing frontmatter, orphaned Runs/Tasks, and legacy Sprint projections — never touches secrets (they always require human review).
|
|
18
|
+
|
|
19
|
+
### Changed
|
|
20
|
+
|
|
21
|
+
- **Broader status aliases in `scrumrun repair`.** `TASK_STATUS_ALIAS` now maps `pending`, `planned`, `skipped`, `canceled`, `wontdo`/`won't do`, `wip`, `doing`, `in-progress`, `review`, `reviewing`, `finished`, `closed` alongside the existing `done`/`todo`/`complete`/`in_progress`/`executing`/`active`. `RUN_STATUS_ALIAS` gains `in-progress`, `wip`, `finished`, `closed`, `canceled`/`cancelled`, `skipped`, `running`. `doctor --strict` remains strict for fresh installs; the aliases only take effect when an owner explicitly runs `repair --apply` (or `update --repair-legacy`).
|
|
22
|
+
- **Guardrail scope parser tolerates parentheses and `+` separators.** `Scope: frontend + backend` and `Scope: frontend (vue templates, docs)` now parse to the intended token(s) instead of splitting on commas inside parens and producing bogus scope names. Unknown scopes still fail conformance in fresh installs — `repair` rewrites them to `all` when normalizing legacy projects.
|
|
23
|
+
|
|
7
24
|
## 4.1.0 - 2026-09-22
|
|
8
25
|
|
|
9
26
|
### Added
|
package/README.md
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
ScrumRun gives an agent a small command surface and a precise project memory: what should be done, how each attempt happened, which decisions constrain the code, and why the architecture exists in its current form.
|
|
6
6
|
|
|
7
|
-
**Package:** `4.1.
|
|
7
|
+
**Package:** `4.1.2` · **Method target:** `2.0.0` · **Runtime:** Node.js `>=22.13.0` · **License:** MIT
|
|
8
8
|
|
|
9
9
|
**New here?** Read the [Quickstart](docs/QUICKSTART.md) — first Run in under 10 minutes, no `SPEC.md` reading required. Full docs map in [`docs/INDEX.md`](docs/INDEX.md).
|
|
10
10
|
|
package/bin/scrumrun.js
CHANGED
|
@@ -61,7 +61,7 @@ Usage:
|
|
|
61
61
|
scrumrun <noun> <subject> <action> [args]
|
|
62
62
|
scrumrun sc <noun> <subject> <action> [args] # compatibility alias
|
|
63
63
|
scrumrun install [all|codex|opencode|claude] [--force]
|
|
64
|
-
scrumrun update [all|codex|opencode|claude] [--project] [--seal-policy] [--migrate] [--verbose]
|
|
64
|
+
scrumrun update [all|codex|opencode|claude] [--project] [--seal-policy] [--migrate] [--repair-legacy] [--verbose]
|
|
65
65
|
scrumrun init [--local|--shared] [--lean] [--no-agent-hint] [--force]
|
|
66
66
|
scrumrun status
|
|
67
67
|
scrumrun core [--path|--prompt]
|
|
@@ -398,7 +398,7 @@ function refreshProjectGuidance(cwd = process.cwd()) {
|
|
|
398
398
|
return results;
|
|
399
399
|
}
|
|
400
400
|
|
|
401
|
-
function updateInstallation(target, { migrate = false, project = false, sealPolicy = false, verbose = false } = {}) {
|
|
401
|
+
function updateInstallation(target, { migrate = false, project = false, sealPolicy = false, verbose = false, repairLegacy = false } = {}) {
|
|
402
402
|
installVerbose = verbose;
|
|
403
403
|
installSummary.cleaned = 0;
|
|
404
404
|
installSummary.written = 0;
|
|
@@ -414,6 +414,18 @@ function updateInstallation(target, { migrate = false, project = false, sealPoli
|
|
|
414
414
|
const sealed = writeFile(marker, sealPolicyIntegrity(scrumDir, { includeGuardrails: true }), { backup: true });
|
|
415
415
|
projectResults.push({ status: sealed.changed ? "updated" : "skipped", dest: marker, backup: sealed.backup });
|
|
416
416
|
}
|
|
417
|
+
let repairSummary = "";
|
|
418
|
+
if (repairLegacy && v2Project()) {
|
|
419
|
+
try {
|
|
420
|
+
const { repair } = require(path.join(root, "lib", "commands", "repair"));
|
|
421
|
+
const scrumDir = path.join(process.cwd(), ".scrumrun");
|
|
422
|
+
const result = repair(scrumDir, { apply: true, recoverOrphanTasks: true });
|
|
423
|
+
const applied = (result.plan && result.plan.entries && result.plan.entries.length) || 0;
|
|
424
|
+
repairSummary = ` Legacy repair: ${applied} entry(ies) normalized.`;
|
|
425
|
+
} catch (error) {
|
|
426
|
+
repairSummary = ` Legacy repair skipped: ${error.message}.`;
|
|
427
|
+
}
|
|
428
|
+
}
|
|
417
429
|
if (migrate && v2Project()) {
|
|
418
430
|
try {
|
|
419
431
|
refreshState(path.join(process.cwd(), ".scrumrun"));
|
|
@@ -425,7 +437,7 @@ function updateInstallation(target, { migrate = false, project = false, sealPoli
|
|
|
425
437
|
if (!verbose) {
|
|
426
438
|
const targetSummary = installSummary.targets.join(", ") || "no clients";
|
|
427
439
|
const projectSummary = projectResults.length ? ` Project guidance: ${projectResults.filter((item) => item.status === "updated").length} file(s) refreshed.` : "";
|
|
428
|
-
console.log(`Updated ${targetSummary} — ${installSummary.written} files written, ${installSummary.cleaned} legacy removed.${projectSummary} Run with --verbose to see file paths.`);
|
|
440
|
+
console.log(`Updated ${targetSummary} — ${installSummary.written} files written, ${installSummary.cleaned} legacy removed.${projectSummary}${repairSummary} Run with --verbose to see file paths.`);
|
|
429
441
|
}
|
|
430
442
|
return migration;
|
|
431
443
|
}
|
|
@@ -2696,7 +2708,7 @@ if (!command || command === "--help" || command === "-h") {
|
|
|
2696
2708
|
console.log(`ScrumRun ${version}`);
|
|
2697
2709
|
} else if (command === "install" || command === "update") {
|
|
2698
2710
|
const target = ["all", "codex", "opencode", "claude"].includes(args[1]) ? args[1] : "all";
|
|
2699
|
-
if (command === "update") updateInstallation(target, { migrate: args.includes("--migrate"), project: args.includes("--project"), sealPolicy: args.includes("--seal-policy"), verbose: args.includes("--verbose") });
|
|
2711
|
+
if (command === "update") updateInstallation(target, { migrate: args.includes("--migrate"), project: args.includes("--project"), sealPolicy: args.includes("--seal-policy"), verbose: args.includes("--verbose"), repairLegacy: args.includes("--repair-legacy") });
|
|
2700
2712
|
else install(target, true, { compatibility: false });
|
|
2701
2713
|
} else if (command === "sc") {
|
|
2702
2714
|
runRoot(args.slice(1));
|
package/lib/commands/repair.js
CHANGED
|
@@ -51,17 +51,38 @@ const VALID_RUN_STATUS = new Set(["executing", "validating", "learning", "partia
|
|
|
51
51
|
const TASK_STATUS_ALIAS = {
|
|
52
52
|
done: "completed",
|
|
53
53
|
todo: "backlog",
|
|
54
|
+
pending: "backlog",
|
|
55
|
+
planned: "backlog",
|
|
54
56
|
complete: "completed",
|
|
57
|
+
finished: "completed",
|
|
58
|
+
closed: "completed",
|
|
59
|
+
skipped: "cancelled",
|
|
60
|
+
canceled: "cancelled",
|
|
61
|
+
wontdo: "cancelled",
|
|
62
|
+
"won't do": "cancelled",
|
|
55
63
|
in_progress: "running",
|
|
56
|
-
|
|
57
|
-
|
|
64
|
+
"in-progress": "running",
|
|
65
|
+
wip: "running",
|
|
66
|
+
doing: "running",
|
|
67
|
+
executing: "running",
|
|
68
|
+
active: "running",
|
|
69
|
+
review: "validating",
|
|
70
|
+
reviewing: "validating"
|
|
58
71
|
};
|
|
59
72
|
|
|
60
73
|
const RUN_STATUS_ALIAS = {
|
|
61
74
|
complete: "completed",
|
|
62
75
|
in_progress: "executing",
|
|
76
|
+
"in-progress": "executing",
|
|
77
|
+
running: "executing",
|
|
78
|
+
wip: "executing",
|
|
63
79
|
done: "completed",
|
|
64
|
-
|
|
80
|
+
finished: "completed",
|
|
81
|
+
closed: "completed",
|
|
82
|
+
partial: "failed",
|
|
83
|
+
canceled: "blocked",
|
|
84
|
+
cancelled: "blocked",
|
|
85
|
+
skipped: "blocked"
|
|
65
86
|
};
|
|
66
87
|
|
|
67
88
|
// When syncing Task.status from latest Run.status, translate Run vocabulary
|
|
@@ -72,7 +72,8 @@ function parseGuardrails(content) {
|
|
|
72
72
|
const rule = capped(fields.rule || proseRule(block) || title);
|
|
73
73
|
const status = normalized(fields.status || "active").replace(/[._-]+$/, "");
|
|
74
74
|
const enforcement = normalized(fields.enforcement || inferEnforcement(title, rule));
|
|
75
|
-
const
|
|
75
|
+
const rawScope = String(fields.scope || "all").replace(/\([^)]*\)/g, "");
|
|
76
|
+
const scope = rawScope.split(/\s*[,+]\s*/).map((value) => normalized(value)).filter(Boolean);
|
|
76
77
|
const declarative = enforcementBlock(block, heading[1]);
|
|
77
78
|
return {
|
|
78
79
|
id: heading[1],
|
package/lib/v2/conformance.js
CHANGED
|
@@ -132,6 +132,7 @@ function auditProject(projectRoot, options = {}) {
|
|
|
132
132
|
const counts = {};
|
|
133
133
|
const ids = new Set();
|
|
134
134
|
const records = {};
|
|
135
|
+
const secretAllowlist = loadSecretAllowlist(scrumDir);
|
|
135
136
|
for (const kind of Object.keys(ARTIFACT_TYPES)) {
|
|
136
137
|
try {
|
|
137
138
|
const artifacts = repository.list(kind);
|
|
@@ -140,7 +141,9 @@ function auditProject(projectRoot, options = {}) {
|
|
|
140
141
|
for (const artifact of artifacts) {
|
|
141
142
|
for (const error of artifact.errors) findings.push(finding("high", "ARTIFACT_INVALID", `${path.basename(artifact.file)}: ${error}`, artifact.file));
|
|
142
143
|
const relativeArtifact = path.relative(scrumDir, artifact.file);
|
|
143
|
-
if (
|
|
144
|
+
if (containsSecretWithAllowlist(fs.readFileSync(artifact.file, "utf8"), relativeArtifact, secretAllowlist)) {
|
|
145
|
+
findings.push(finding("critical", "SECRET_CANONICAL", `Secret-like content detected in ${relativeArtifact}.`, artifact.file));
|
|
146
|
+
}
|
|
144
147
|
if (artifact.record && ids.has(artifact.record.id)) findings.push(finding("critical", "ID_DUPLICATE", `Duplicate artifact id: ${artifact.record.id}`));
|
|
145
148
|
if (artifact.record) ids.add(artifact.record.id);
|
|
146
149
|
}
|