scrumrun 2.7.10 → 2.7.11

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/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:** `2.7.10` · **Method target:** `2.0.0` · **Runtime:** Node.js `>=22.13.0` · **License:** MIT
7
+ **Package:** `2.7.11` · **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
 
@@ -12,6 +12,8 @@
12
12
  // complete→completed, in_progress→executing.
13
13
  // 3. frontmatter method field: any value that isn't "2.0.0" → "2.0.0".
14
14
  // 4. run.attempt missing → 1 (documented default).
15
+ // 5. Sprint task projections: add Task.sprint references absent from the
16
+ // Sprint's `## Tasks` list. Extra entries remain for human review.
15
17
  //
16
18
  // Categories intentionally NOT touched (need human judgment):
17
19
  // - Secret-like content (SECRET_CANONICAL)
@@ -132,6 +134,48 @@ function scanTaskIds(scrumDir) {
132
134
  return ids;
133
135
  }
134
136
 
137
+ function planSprintMembership(scrumDir) {
138
+ const tasksBySprint = new Map();
139
+ for (const file of listDir(path.join(scrumDir, "tasks"), TASK_FILE)) {
140
+ const parsed = splitFrontmatter(readIf(file));
141
+ if (!parsed) continue;
142
+ const sprint = extractField(parsed.header, "sprint");
143
+ const id = extractField(parsed.header, "id") || path.basename(file, ".md");
144
+ if (!/^SPRINT-\d{3,}$/.test(sprint || "") || !TASK_REF.test(id)) continue;
145
+ if (!tasksBySprint.has(sprint)) tasksBySprint.set(sprint, []);
146
+ tasksBySprint.get(sprint).push(id);
147
+ }
148
+ const entries = [];
149
+ for (const file of listDir(path.join(scrumDir, "sprints"), SPRINT_FILE)) {
150
+ const original = readIf(file);
151
+ const parsed = splitFrontmatter(original);
152
+ if (!parsed) continue;
153
+ const sprintId = extractField(parsed.header, "id") || path.basename(file, ".md");
154
+ const expected = tasksBySprint.get(sprintId) || [];
155
+ if (!expected.length) continue;
156
+ const heading = /^## Tasks[ \t]*$/m.exec(parsed.body);
157
+ if (!heading) continue;
158
+ const tail = parsed.body.slice(heading.index + heading[0].length);
159
+ const nextHeading = /^## [^\r\n]+$/m.exec(tail);
160
+ const sectionEnd = nextHeading ? heading.index + heading[0].length + nextHeading.index : parsed.body.length;
161
+ const section = parsed.body.slice(heading.index + heading[0].length, sectionEnd);
162
+ const listed = new Set([...section.matchAll(/^-\s+(TASK-\d{3,})\s*$/gm)].map((match) => match[1]));
163
+ const missing = expected.filter((id) => !listed.has(id));
164
+ if (!missing.length) continue;
165
+ const before = parsed.body.slice(0, sectionEnd).replace(/\s*$/, "\n");
166
+ const after = parsed.body.slice(sectionEnd).replace(/^\n/, "");
167
+ const body = `${before}${missing.map((id) => `- ${id}`).join("\n")}\n${after}`;
168
+ entries.push({
169
+ file,
170
+ kind: "sprint-membership",
171
+ changes: [{ field: "sprint.tasks", from: "missing projection", to: missing.join(", ") }],
172
+ nextText: `---\n${parsed.header}\n---${parsed.sep}${body}`,
173
+ originalText: original
174
+ });
175
+ }
176
+ return entries;
177
+ }
178
+
135
179
  function legacyMetadata(body) {
136
180
  const metadata = {};
137
181
  const section = String(body || "").match(/^## Metadata\s*$([\s\S]*?)(?=^##\s+|(?![\s\S]))/im);
@@ -629,6 +673,7 @@ function analyze(scrumDir) {
629
673
  const missingFrontmatter = new Set(entries.filter((entry) => entry.kind === "legacy-frontmatter").map((entry) => entry.file));
630
674
  entries.push(...planAcceptanceCriteria(scrumDir).filter((entry) => !missingFrontmatter.has(entry.file)));
631
675
  entries.push(...planSecretRedaction(scrumDir));
676
+ entries.push(...planSprintMembership(scrumDir));
632
677
 
633
678
  for (const file of listDir(path.join(scrumDir, "tasks"), TASK_FILE)) {
634
679
  const plan = planFile(file, "task", ctx);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "scrumrun",
3
- "version": "2.7.10",
3
+ "version": "2.7.11",
4
4
  "description": "Evidence-driven Agile runtime and semantic project memory for AI coding agents.",
5
5
  "bin": {
6
6
  "scrumrun": "bin/scrumrun.js",