scrumrun 4.1.1 → 4.1.3
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 +12 -0
- package/README.md +1 -1
- package/lib/security/secrets.js +34 -3
- package/lib/v2/conformance.js +4 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,18 @@ All notable changes follow Semantic Versioning.
|
|
|
4
4
|
|
|
5
5
|
## Unreleased
|
|
6
6
|
|
|
7
|
+
## 4.1.3 - 2026-09-22
|
|
8
|
+
|
|
9
|
+
### Fixed
|
|
10
|
+
|
|
11
|
+
- **`parseFrontmatter` handles multi-line arrays and YAML block lists.** The inline `allow_secrets_in: [a,\nb,\nc]` and the block form `allow_secrets_in:\n - a\n - b` used to be parsed as a single truncated string, silently disabling the whitelist. The parser now (a) keeps reading until the closing `]` for multi-line inline arrays, and (b) recognizes `- item` block lists as an array value. Existing single-line inline arrays continue to work.
|
|
12
|
+
|
|
13
|
+
## 4.1.2 - 2026-09-22
|
|
14
|
+
|
|
15
|
+
### Fixed
|
|
16
|
+
|
|
17
|
+
- **`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).
|
|
18
|
+
|
|
7
19
|
## 4.1.1 - 2026-09-22
|
|
8
20
|
|
|
9
21
|
### 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.3` · **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/lib/security/secrets.js
CHANGED
|
@@ -41,12 +41,42 @@ function parseFrontmatter(text) {
|
|
|
41
41
|
if (!match) return { explicit: {}, raw: "" };
|
|
42
42
|
const body = match[1];
|
|
43
43
|
const explicit = {};
|
|
44
|
-
|
|
45
|
-
|
|
44
|
+
const rawLines = body.split(/\r?\n/);
|
|
45
|
+
let index = 0;
|
|
46
|
+
while (index < rawLines.length) {
|
|
47
|
+
const line = rawLines[index];
|
|
48
|
+
if (!line.trim() || line.trim().startsWith("#")) { index += 1; continue; }
|
|
46
49
|
const kv = line.match(/^([A-Za-z0-9_.-]+)\s*:\s*(.*)$/);
|
|
47
|
-
if (!kv) continue;
|
|
50
|
+
if (!kv) { index += 1; continue; }
|
|
48
51
|
const key = kv[1].trim();
|
|
49
52
|
let value = kv[2].trim();
|
|
53
|
+
|
|
54
|
+
// Inline array that spans multiple lines: keep reading until we see the closing `]`.
|
|
55
|
+
if (value.startsWith("[") && !value.endsWith("]")) {
|
|
56
|
+
let acc = value;
|
|
57
|
+
let cursor = index + 1;
|
|
58
|
+
while (cursor < rawLines.length) {
|
|
59
|
+
acc += " " + rawLines[cursor].trim();
|
|
60
|
+
if (rawLines[cursor].trim().endsWith("]")) { break; }
|
|
61
|
+
cursor += 1;
|
|
62
|
+
}
|
|
63
|
+
value = acc;
|
|
64
|
+
index = cursor;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// YAML block list: subsequent lines start with `- item`. Consume while indented under this key.
|
|
68
|
+
if (value === "" && index + 1 < rawLines.length && /^\s*-\s+/.test(rawLines[index + 1])) {
|
|
69
|
+
const items = [];
|
|
70
|
+
let cursor = index + 1;
|
|
71
|
+
while (cursor < rawLines.length && /^\s*-\s+/.test(rawLines[cursor])) {
|
|
72
|
+
items.push(rawLines[cursor].replace(/^\s*-\s+/, "").trim().replace(/^['"]|['"]$/g, ""));
|
|
73
|
+
cursor += 1;
|
|
74
|
+
}
|
|
75
|
+
explicit[key] = items.filter(Boolean);
|
|
76
|
+
index = cursor;
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
|
|
50
80
|
if (value.startsWith("[") && value.endsWith("]")) {
|
|
51
81
|
value = value.slice(1, -1).split(",").map((s) => s.trim().replace(/^['"]|['"]$/g, "")).filter(Boolean);
|
|
52
82
|
} else if (/^['"].*['"]$/.test(value)) {
|
|
@@ -55,6 +85,7 @@ function parseFrontmatter(text) {
|
|
|
55
85
|
value = value === "true";
|
|
56
86
|
}
|
|
57
87
|
explicit[key] = value;
|
|
88
|
+
index += 1;
|
|
58
89
|
}
|
|
59
90
|
return { explicit, raw: body };
|
|
60
91
|
}
|
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
|
}
|