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 +1 -1
- package/lib/commands/repair.js +61 -4
- 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,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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|