scrumrun 2.6.5 → 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.5` · **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,11 +26,15 @@
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$/;
32
34
  const SPRINT_FILE = /^SPRINT-\d{3,}\.md$/;
33
35
  const MEMORY_FILE = /^(K|DEC|INS|DOS)-\d{3,}\.md$/;
36
+ const TASK_REF = /^TASK-\d{3,}$/;
37
+ const ISO_DATE = /^\d{4}-\d{2}-\d{2}$/;
34
38
  const FEAT_REF = /^FEAT-\d{3,}$/;
35
39
  const SLUG_REF = /^[a-z0-9][a-z0-9-]*$/;
36
40
 
@@ -47,6 +51,8 @@ const RUN_STATUS_ALIAS = {
47
51
  done: "completed"
48
52
  };
49
53
 
54
+ const GUARDRAIL_SCOPES = new Set(["all", "intake", "execution", "mutation", "canonical", "memory", "migration", "validation", "learning", "completion", "commit", "release", "logs"]);
55
+
50
56
  function readIf(file) {
51
57
  try {
52
58
  return fs.readFileSync(file, "utf8");
@@ -105,6 +111,19 @@ function planFile(file, kind, ctx) {
105
111
  let header = split.header;
106
112
  const changes = [];
107
113
 
114
+ const updated = extractField(header, "updated");
115
+ if (updated !== undefined && updated !== "null" && !ISO_DATE.test(updated)) {
116
+ const parsed = new Date(updated);
117
+ if (!isNaN(parsed.getTime())) {
118
+ const iso = parsed.toISOString().slice(0, 10);
119
+ const next = replaceField(header, "updated", iso);
120
+ if (next !== header) {
121
+ changes.push({ field: "updated", from: updated, to: iso });
122
+ header = next;
123
+ }
124
+ }
125
+ }
126
+
108
127
  const method = extractField(header, "method");
109
128
  if (method === undefined) {
110
129
  if (hasField(header, "id")) {
@@ -145,6 +164,22 @@ function planFile(file, kind, ctx) {
145
164
  }
146
165
 
147
166
  if (kind === "run") {
167
+ const taskRef = extractField(header, "task");
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)) {
177
+ const next = replaceField(header, "task", "null");
178
+ if (next !== header) {
179
+ changes.push({ field: "task", from: taskRef, to: "null" });
180
+ header = next;
181
+ }
182
+ }
148
183
  const status = extractField(header, "status");
149
184
  if (status && RUN_STATUS_ALIAS[status]) {
150
185
  const next = replaceField(header, "status", RUN_STATUS_ALIAS[status]);
@@ -168,11 +203,32 @@ function planFile(file, kind, ctx) {
168
203
  return { file, kind, changes, nextText, originalText: original };
169
204
  }
170
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
+
171
224
  function analyze(scrumDir) {
172
225
  const featureIds = scanFeatureIds(scrumDir);
173
226
  const ctx = { featureIds };
174
227
  const entries = [];
175
228
 
229
+ const guardrailPlan = planGuardrails(scrumDir);
230
+ if (guardrailPlan) entries.push(guardrailPlan);
231
+
176
232
  for (const file of listDir(path.join(scrumDir, "tasks"), TASK_FILE)) {
177
233
  const plan = planFile(file, "task", ctx);
178
234
  if (plan && plan.changes.length) entries.push(plan);
@@ -217,13 +273,21 @@ function apply(scrumDir, plan) {
217
273
  fs.writeFileSync(entry.file, entry.nextText);
218
274
  applied.push({ file: relative, backup: path.relative(scrumDir, backupPath), changes: entry.changes });
219
275
  }
220
- 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 };
221
284
  }
222
285
 
223
286
  function renderReport(plan, applied) {
224
287
  const lines = [];
225
288
  lines.push("## Repair plan\n");
226
- 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}`);
227
291
  const byField = plan.totals.byField;
228
292
  if (Object.keys(byField).length) {
229
293
  lines.push("\nBy field:");
@@ -241,7 +305,15 @@ function renderReport(plan, applied) {
241
305
  }
242
306
  if (applied) {
243
307
  lines.push("");
244
- 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
+ }
245
317
  } else {
246
318
  lines.push("");
247
319
  lines.push("The project remains unchanged. Apply with: scrumrun repair --apply");
@@ -254,8 +326,16 @@ function repair(scrumDir, { apply: doApply = false } = {}) {
254
326
  throw new Error(".scrumrun/ not found; run this inside a ScrumRun project.");
255
327
  }
256
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;
257
336
  if (!doApply) return { plan, applied: null, report: renderReport(plan, null) };
258
337
  const applied = apply(scrumDir, plan);
338
+ plan.ledgerNormalized = applied.ledger && applied.ledger.applied ? applied.ledger.applied.length : 0;
259
339
  return { plan, applied, report: renderReport(plan, applied) };
260
340
  }
261
341
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "scrumrun",
3
- "version": "2.6.5",
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",