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 +1 -1
- package/lib/commands/repair.js +83 -3
- package/package.json +1 -1
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
|
+
**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
|
|
package/lib/commands/repair.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
|