scrumrun 2.6.7 → 2.6.8

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.7` · **Method target:** `2.0.0` · **Runtime:** Node.js `>=22.13.0` · **License:** MIT
7
+ **Package:** `2.6.8` · **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
 
@@ -83,12 +83,13 @@ function needsNormalization(record, body) {
83
83
  function buildNormalizedBody({ record, originalBody, backupRelative, reason, errors }) {
84
84
  const title = extractTitle(originalBody, `Run ${record.id}`);
85
85
  const sha = crypto.createHash("sha256").update(originalBody).digest("hex");
86
+ const snapshotDate = record.created || new Date().toISOString().slice(0, 10);
86
87
  const normalizedRecord = {
87
88
  id: record.id,
88
89
  kind: "run",
89
90
  status: record.status || "partial",
90
91
  created: record.created,
91
- updated: new Date().toISOString().slice(0, 10),
92
+ updated: snapshotDate,
92
93
  method: "2.0.0",
93
94
  task: TASK_REF_PATTERN.test(record.task || "") ? record.task : null,
94
95
  sprint: record.sprint || null,
@@ -108,7 +109,7 @@ function buildNormalizedBody({ record, originalBody, backupRelative, reason, err
108
109
  id: `${record.id}-EVT-001`,
109
110
  sequence: 1,
110
111
  type: "snapshot",
111
- occurred_at: `${normalizedRecord.created || normalizedRecord.updated}T00:00:00.000Z`,
112
+ occurred_at: `${snapshotDate}T00:00:00.000Z`,
112
113
  timestamp_precision: "date",
113
114
  actor: "migration",
114
115
  from: null,
@@ -102,6 +102,29 @@ function scanFeatureIds(scrumDir) {
102
102
  return ids;
103
103
  }
104
104
 
105
+ function scanTaskIds(scrumDir) {
106
+ const dir = path.join(scrumDir, "tasks");
107
+ const ids = new Set();
108
+ for (const file of listDir(dir, TASK_FILE)) {
109
+ ids.add(path.basename(file, ".md"));
110
+ }
111
+ return ids;
112
+ }
113
+
114
+ function planOrphanRuns(scrumDir, taskIds) {
115
+ const orphans = [];
116
+ for (const file of listDir(path.join(scrumDir, "runs"), RUN_FILE)) {
117
+ const raw = readIf(file);
118
+ if (raw === null) continue;
119
+ const split = splitFrontmatter(raw);
120
+ if (!split) continue;
121
+ const taskRef = extractField(split.header, "task");
122
+ if (taskRef && taskRef !== "null" && TASK_REF.test(taskRef) && taskIds.has(taskRef)) continue;
123
+ orphans.push({ file, reason: taskRef ? `task ${taskRef} is null or missing from .scrumrun/tasks/` : "task field missing", originalText: raw });
124
+ }
125
+ return orphans;
126
+ }
127
+
105
128
  function planFile(file, kind, ctx) {
106
129
  const original = readIf(file);
107
130
  if (original === null) return null;
@@ -223,8 +246,10 @@ function planGuardrails(scrumDir) {
223
246
 
224
247
  function analyze(scrumDir) {
225
248
  const featureIds = scanFeatureIds(scrumDir);
249
+ const taskIds = scanTaskIds(scrumDir);
226
250
  const ctx = { featureIds };
227
251
  const entries = [];
252
+ const orphanRuns = planOrphanRuns(scrumDir, taskIds);
228
253
 
229
254
  const guardrailPlan = planGuardrails(scrumDir);
230
255
  if (guardrailPlan) entries.push(guardrailPlan);
@@ -258,7 +283,7 @@ function analyze(scrumDir) {
258
283
  totals.byField[change.field] = (totals.byField[change.field] || 0) + 1;
259
284
  }
260
285
  }
261
- return { entries, totals };
286
+ return { entries, totals, orphanRuns };
262
287
  }
263
288
 
264
289
  function apply(scrumDir, plan) {
@@ -280,7 +305,18 @@ function apply(scrumDir, plan) {
280
305
  } catch {
281
306
  ledgerResult = { plan: { malformed: 0 }, applied: [] };
282
307
  }
283
- return { applied, ledger: ledgerResult };
308
+ // Quarantine orphan Runs (Task ref missing/invalid — schema requires TASK-NNN). Move byte-exact
309
+ // to backup so nothing is deleted; the orphan can be manually re-linked or discarded later.
310
+ const orphanBackup = path.join(scrumDir, ".migration-backup", "repair", "orphan-runs");
311
+ const quarantined = [];
312
+ for (const orphan of plan.orphanRuns || []) {
313
+ fs.mkdirSync(orphanBackup, { recursive: true });
314
+ const target = path.join(orphanBackup, path.basename(orphan.file));
315
+ if (!fs.existsSync(target)) fs.writeFileSync(target, orphan.originalText);
316
+ fs.rmSync(orphan.file, { force: true });
317
+ quarantined.push({ id: path.basename(orphan.file, ".md"), reason: orphan.reason, backup: path.relative(scrumDir, target) });
318
+ }
319
+ return { applied, ledger: ledgerResult, quarantined };
284
320
  }
285
321
 
286
322
  function renderReport(plan, applied) {
@@ -288,6 +324,7 @@ function renderReport(plan, applied) {
288
324
  lines.push("## Repair plan\n");
289
325
  lines.push(`Files needing frontmatter repair: ${plan.entries.length}`);
290
326
  if (plan.ledgerMalformed) lines.push(`Run ledgers to normalize: ${plan.ledgerMalformed}`);
327
+ if (plan.orphanRuns && plan.orphanRuns.length) lines.push(`Orphan Runs to quarantine: ${plan.orphanRuns.length}`);
291
328
  const byField = plan.totals.byField;
292
329
  if (Object.keys(byField).length) {
293
330
  lines.push("\nBy field:");
@@ -307,9 +344,11 @@ function renderReport(plan, applied) {
307
344
  lines.push("");
308
345
  const frontmatterCount = applied.applied ? applied.applied.length : 0;
309
346
  const ledgerCount = applied.ledger && applied.ledger.applied ? applied.ledger.applied.length : 0;
310
- if (frontmatterCount || ledgerCount) {
347
+ const quarantinedCount = applied.quarantined ? applied.quarantined.length : 0;
348
+ if (frontmatterCount || ledgerCount || quarantinedCount) {
311
349
  if (frontmatterCount) lines.push(`Applied: ${frontmatterCount} frontmatter file(s) rewritten.`);
312
350
  if (ledgerCount) lines.push(`Normalized: ${ledgerCount} Run ledger(s) collapsed to snapshot events.`);
351
+ if (quarantinedCount) lines.push(`Quarantined: ${quarantinedCount} orphan Run(s) moved to .scrumrun/.migration-backup/repair/orphan-runs/.`);
313
352
  lines.push("Backups under .scrumrun/.migration-backup/repair/ and .scrumrun/.migration-backup/runs/.");
314
353
  } else {
315
354
  lines.push("Applied: no changes.");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "scrumrun",
3
- "version": "2.6.7",
3
+ "version": "2.6.8",
4
4
  "description": "Evidence-driven Agile runtime and semantic project memory for AI coding agents.",
5
5
  "bin": {
6
6
  "scrumrun": "bin/scrumrun.js",