scrumrun 2.6.6 → 2.6.7

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.6.6` · **Method target:** `2.0.0` · **Runtime:** Node.js `>=22.13.0` · **License:** MIT
7
+ **Package:** `2.6.7` · **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
 
@@ -26,6 +26,8 @@
26
26
  const fs = require("node:fs");
27
27
  const path = require("node:path");
28
28
 
29
+ const { normalizeLegacyRuns } = require("./normalize-legacy");
30
+
29
31
  const TASK_FILE = /^TASK-\d{3,}\.md$/;
30
32
  const RUN_FILE = /^RUN-\d{3,}\.md$/;
31
33
  const FEAT_FILE = /^FEAT-\d{3,}\.md$/;
@@ -49,6 +51,8 @@ const RUN_STATUS_ALIAS = {
49
51
  done: "completed"
50
52
  };
51
53
 
54
+ const GUARDRAIL_SCOPES = new Set(["all", "intake", "execution", "mutation", "canonical", "memory", "migration", "validation", "learning", "completion", "commit", "release", "logs"]);
55
+
52
56
  function readIf(file) {
53
57
  try {
54
58
  return fs.readFileSync(file, "utf8");
@@ -161,7 +165,15 @@ function planFile(file, kind, ctx) {
161
165
 
162
166
  if (kind === "run") {
163
167
  const taskRef = extractField(header, "task");
164
- if (taskRef !== undefined && taskRef !== "null" && taskRef !== null && taskRef !== "" && !TASK_REF.test(taskRef)) {
168
+ if (taskRef === undefined) {
169
+ if (hasField(header, "id")) {
170
+ const next = insertFieldAfter(header, "id", "task", "null");
171
+ if (next !== header) {
172
+ changes.push({ field: "task", from: "(missing)", to: "null" });
173
+ header = next;
174
+ }
175
+ }
176
+ } else if (taskRef !== "null" && taskRef !== null && taskRef !== "" && !TASK_REF.test(taskRef)) {
165
177
  const next = replaceField(header, "task", "null");
166
178
  if (next !== header) {
167
179
  changes.push({ field: "task", from: taskRef, to: "null" });
@@ -191,11 +203,32 @@ function planFile(file, kind, ctx) {
191
203
  return { file, kind, changes, nextText, originalText: original };
192
204
  }
193
205
 
206
+ function planGuardrails(scrumDir) {
207
+ const file = path.join(scrumDir, "guardrails.md");
208
+ const original = readIf(file);
209
+ if (original === null) return null;
210
+ const changes = [];
211
+ let text = original;
212
+ const scopeRe = /^(\s*scope:\s*)([^\n\r]+)$/gim;
213
+ text = text.replace(scopeRe, (match, prefix, value) => {
214
+ const trimmed = value.trim();
215
+ if (!trimmed || trimmed === "null") return match;
216
+ if (GUARDRAIL_SCOPES.has(trimmed)) return match;
217
+ changes.push({ field: "scope", from: trimmed, to: "all" });
218
+ return `${prefix}all`;
219
+ });
220
+ if (!changes.length) return null;
221
+ return { file, kind: "guardrails", changes, nextText: text, originalText: original };
222
+ }
223
+
194
224
  function analyze(scrumDir) {
195
225
  const featureIds = scanFeatureIds(scrumDir);
196
226
  const ctx = { featureIds };
197
227
  const entries = [];
198
228
 
229
+ const guardrailPlan = planGuardrails(scrumDir);
230
+ if (guardrailPlan) entries.push(guardrailPlan);
231
+
199
232
  for (const file of listDir(path.join(scrumDir, "tasks"), TASK_FILE)) {
200
233
  const plan = planFile(file, "task", ctx);
201
234
  if (plan && plan.changes.length) entries.push(plan);
@@ -240,13 +273,21 @@ function apply(scrumDir, plan) {
240
273
  fs.writeFileSync(entry.file, entry.nextText);
241
274
  applied.push({ file: relative, backup: path.relative(scrumDir, backupPath), changes: entry.changes });
242
275
  }
243
- return applied;
276
+ // Also normalize legacy Run ledgers (empty/broken) — same safety guarantee: byte-exact backup.
277
+ let ledgerResult = null;
278
+ try {
279
+ ledgerResult = normalizeLegacyRuns(scrumDir, { dryRun: false });
280
+ } catch {
281
+ ledgerResult = { plan: { malformed: 0 }, applied: [] };
282
+ }
283
+ return { applied, ledger: ledgerResult };
244
284
  }
245
285
 
246
286
  function renderReport(plan, applied) {
247
287
  const lines = [];
248
288
  lines.push("## Repair plan\n");
249
- lines.push(`Files needing repair: ${plan.entries.length}`);
289
+ lines.push(`Files needing frontmatter repair: ${plan.entries.length}`);
290
+ if (plan.ledgerMalformed) lines.push(`Run ledgers to normalize: ${plan.ledgerMalformed}`);
250
291
  const byField = plan.totals.byField;
251
292
  if (Object.keys(byField).length) {
252
293
  lines.push("\nBy field:");
@@ -264,7 +305,15 @@ function renderReport(plan, applied) {
264
305
  }
265
306
  if (applied) {
266
307
  lines.push("");
267
- lines.push(applied.length ? `Applied: ${applied.length} file(s) rewritten. Backups under .scrumrun/.migration-backup/repair/.` : "Applied: no changes.");
308
+ const frontmatterCount = applied.applied ? applied.applied.length : 0;
309
+ const ledgerCount = applied.ledger && applied.ledger.applied ? applied.ledger.applied.length : 0;
310
+ if (frontmatterCount || ledgerCount) {
311
+ if (frontmatterCount) lines.push(`Applied: ${frontmatterCount} frontmatter file(s) rewritten.`);
312
+ if (ledgerCount) lines.push(`Normalized: ${ledgerCount} Run ledger(s) collapsed to snapshot events.`);
313
+ lines.push("Backups under .scrumrun/.migration-backup/repair/ and .scrumrun/.migration-backup/runs/.");
314
+ } else {
315
+ lines.push("Applied: no changes.");
316
+ }
268
317
  } else {
269
318
  lines.push("");
270
319
  lines.push("The project remains unchanged. Apply with: scrumrun repair --apply");
@@ -277,8 +326,16 @@ function repair(scrumDir, { apply: doApply = false } = {}) {
277
326
  throw new Error(".scrumrun/ not found; run this inside a ScrumRun project.");
278
327
  }
279
328
  const plan = analyze(scrumDir);
329
+ let ledgerPreview = null;
330
+ try {
331
+ ledgerPreview = normalizeLegacyRuns(scrumDir, { dryRun: true }).plan;
332
+ } catch {
333
+ ledgerPreview = null;
334
+ }
335
+ plan.ledgerMalformed = ledgerPreview ? ledgerPreview.malformed : 0;
280
336
  if (!doApply) return { plan, applied: null, report: renderReport(plan, null) };
281
337
  const applied = apply(scrumDir, plan);
338
+ plan.ledgerNormalized = applied.ledger && applied.ledger.applied ? applied.ledger.applied.length : 0;
282
339
  return { plan, applied, report: renderReport(plan, applied) };
283
340
  }
284
341
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "scrumrun",
3
- "version": "2.6.6",
3
+ "version": "2.6.7",
4
4
  "description": "Evidence-driven Agile runtime and semantic project memory for AI coding agents.",
5
5
  "bin": {
6
6
  "scrumrun": "bin/scrumrun.js",