scrumrun 2.7.9 → 2.7.10
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 +10 -1
- package/lib/commands/repair.js +83 -2
- 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.7.
|
|
7
|
+
**Package:** `2.7.10` · **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
|
|
|
@@ -167,6 +167,15 @@ npx scrumrun@latest migrate --to 2 --rollback
|
|
|
167
167
|
|
|
168
168
|
Migration uses content hashes, a byte-exact ignored backup, staging validation, an atomic directory switch, a source-to-destination report, idempotent replay, and rollback protection. It also recognizes incomplete hybrid v1/v2 trees, reuses already-linked canonical work, and normalizes only deterministic schema aliases. Early v2 Run prose is preflighted and upgraded to the structured ledger by the same explicit `update --migrate` gate; ambiguous paths become evidenced snapshots instead of invented transitions. Legacy-only files leave the active tree but remain byte-exact in the ignored backup; vault contents remain local. Ambiguous history remains an explicit warning, and the migrator never invents a Sprint or Run.
|
|
169
169
|
|
|
170
|
+
For projects that already have v2 directories but contain pre-v2 Markdown records without YAML frontmatter, use the explicit repair gate:
|
|
171
|
+
|
|
172
|
+
```bash
|
|
173
|
+
npx scrumrun@latest repair
|
|
174
|
+
npx scrumrun@latest repair --apply
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
Repair converts deterministic `## Metadata` fields into v2 frontmatter, preserves the authored body, recovers IDs from canonical filenames, and stores byte-exact backups under `.scrumrun/.migration-backup/repair/`. It does not guess ambiguous relationships; validate afterward with `doctor --strict`.
|
|
178
|
+
|
|
170
179
|
After migration or an integration update, verify both installed assets and project state:
|
|
171
180
|
|
|
172
181
|
```bash
|
package/lib/commands/repair.js
CHANGED
|
@@ -72,6 +72,7 @@ const RUN_TO_TASK_STATUS = {
|
|
|
72
72
|
};
|
|
73
73
|
|
|
74
74
|
const GUARDRAIL_SCOPES = new Set(["all", "intake", "execution", "mutation", "canonical", "memory", "migration", "validation", "learning", "completion", "commit", "release", "logs"]);
|
|
75
|
+
const CANONICAL_DIRS = ["tasks", "runs", "features", "sprints", "reviews", "memory/knowledge", "memory/decisions", "memory/insights", "memory/dossiers"];
|
|
75
76
|
|
|
76
77
|
function readIf(file) {
|
|
77
78
|
try {
|
|
@@ -131,6 +132,85 @@ function scanTaskIds(scrumDir) {
|
|
|
131
132
|
return ids;
|
|
132
133
|
}
|
|
133
134
|
|
|
135
|
+
function legacyMetadata(body) {
|
|
136
|
+
const metadata = {};
|
|
137
|
+
const section = String(body || "").match(/^## Metadata\s*$([\s\S]*?)(?=^##\s+|(?![\s\S]))/im);
|
|
138
|
+
if (!section) return metadata;
|
|
139
|
+
for (const line of section[1].split(/\r?\n/)) {
|
|
140
|
+
const match = line.match(/^\s*-\s*([a-z][a-z0-9_]*)\s*:\s*(.*?)\s*$/i);
|
|
141
|
+
if (match) metadata[match[1].toLowerCase()] = match[2];
|
|
142
|
+
}
|
|
143
|
+
return metadata;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function canonicalKind(relative) {
|
|
147
|
+
if (/^tasks\/TASK-\d{3,}\.md$/.test(relative)) return "task";
|
|
148
|
+
if (/^runs\/RUN-\d{3,}\.md$/.test(relative)) return "run";
|
|
149
|
+
if (/^features\/FEAT-\d{3,}\.md$/.test(relative)) return "feature";
|
|
150
|
+
if (/^sprints\/SPRINT-\d{3,}\.md$/.test(relative)) return "sprint";
|
|
151
|
+
if (/^reviews\/REV-\d{3,}\.md$/.test(relative)) return "review";
|
|
152
|
+
if (/^memory\/knowledge\/K-\d{3,}\.md$/.test(relative)) return "knowledge";
|
|
153
|
+
if (/^memory\/decisions\/DEC-\d{3,}\.md$/.test(relative)) return "decision";
|
|
154
|
+
if (/^memory\/insights\/INS-\d{3,}\.md$/.test(relative)) return "insight";
|
|
155
|
+
if (/^memory\/dossiers\/DOS-\d{3,}\.md$/.test(relative)) return "dossier";
|
|
156
|
+
return null;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function legacyStatus(kind, value) {
|
|
160
|
+
const raw = String(value || "").trim().toLowerCase();
|
|
161
|
+
const aliases = kind === "task" ? TASK_STATUS_ALIAS : kind === "run" ? RUN_STATUS_ALIAS : {};
|
|
162
|
+
const normalized = aliases[raw] || raw;
|
|
163
|
+
const valid = {
|
|
164
|
+
feature: ["backlog", "proposed", "active", "completed", "paused", "cancelled"],
|
|
165
|
+
task: [...VALID_TASK_STATUS],
|
|
166
|
+
sprint: ["proposed", "running", "partial", "completed", "blocked", "cancelled"],
|
|
167
|
+
run: [...VALID_RUN_STATUS],
|
|
168
|
+
review: ["proposed", "running", "passed", "failed", "archived"],
|
|
169
|
+
knowledge: ["candidate", "approved", "rejected", "deprecated", "invalidated"],
|
|
170
|
+
decision: ["open", "resolved", "deprecated", "invalidated"],
|
|
171
|
+
insight: ["candidate", "confirmed", "stale", "deprecated", "invalidated"],
|
|
172
|
+
dossier: ["active", "stale", "deprecated", "archived"]
|
|
173
|
+
}[kind] || [];
|
|
174
|
+
if (valid.includes(normalized)) return normalized;
|
|
175
|
+
return { feature: "proposed", task: "proposed", sprint: "proposed", run: "partial", review: "proposed", knowledge: "candidate", decision: "open", insight: "candidate", dossier: "active" }[kind];
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function planMissingFrontmatter(scrumDir) {
|
|
179
|
+
const entries = [];
|
|
180
|
+
for (const directory of CANONICAL_DIRS) {
|
|
181
|
+
const absolute = path.join(scrumDir, directory);
|
|
182
|
+
if (!fs.existsSync(absolute) || !fs.lstatSync(absolute).isDirectory()) continue;
|
|
183
|
+
for (const file of fs.readdirSync(absolute).sort().map((name) => path.join(absolute, name))) {
|
|
184
|
+
if (!fs.lstatSync(file).isFile()) continue;
|
|
185
|
+
const relative = path.relative(scrumDir, file).split(path.sep).join("/");
|
|
186
|
+
const kind = canonicalKind(relative);
|
|
187
|
+
if (!kind) continue;
|
|
188
|
+
const original = readIf(file);
|
|
189
|
+
if (original === null || splitFrontmatter(original)) continue;
|
|
190
|
+
const metadata = legacyMetadata(original);
|
|
191
|
+
const id = path.basename(file, ".md");
|
|
192
|
+
const statDate = new Date(fs.statSync(file).mtimeMs).toISOString().slice(0, 10);
|
|
193
|
+
const created = ISO_DATE.test(metadata.created || "") ? metadata.created : statDate;
|
|
194
|
+
const updated = ISO_DATE.test(metadata.updated || "") ? metadata.updated : created;
|
|
195
|
+
const task = kind === "run" ? (TASK_REF.test(metadata.task || "") ? metadata.task : ((original.match(/\b(TASK-\d{3,})\b/) || [])[1] || null)) : null;
|
|
196
|
+
const featureValue = metadata.feature || metadata.scope || null;
|
|
197
|
+
const feature = FEAT_REF.test(featureValue || "") ? featureValue : null;
|
|
198
|
+
const sprint = /^SPRINT-\d{3,}$/.test(metadata.sprint || "") ? metadata.sprint : null;
|
|
199
|
+
const record = { id, kind, status: legacyStatus(kind, metadata.status), created, updated, method: "2.0.0" };
|
|
200
|
+
if (kind === "task") Object.assign(record, { type: metadata.type || "task", feature, sprint });
|
|
201
|
+
if (kind === "run") Object.assign(record, { task, sprint, attempt: Number(metadata.attempt) > 0 ? Number(metadata.attempt) : 1, ledger: 1 });
|
|
202
|
+
if (kind === "feature") record.type = "initiative";
|
|
203
|
+
if (kind === "decision" && feature) record.feature = feature;
|
|
204
|
+
const frontmatter = Object.entries(record).map(([key, value]) => `${key}: ${value === null ? "null" : value}`).join("\n");
|
|
205
|
+
const acceptance = kind === "task" && !/^## Acceptance Criteria\b/m.test(original)
|
|
206
|
+
? "\n\n## Acceptance Criteria\n\n- Preserved from legacy migration. Define what done means before re-approaching this work.\n"
|
|
207
|
+
: "";
|
|
208
|
+
entries.push({ file, kind: "legacy-frontmatter", changes: [{ field: "frontmatter", from: "missing", to: `${kind} schema` }], nextText: `---\n${frontmatter}\n---\n\n${original}${acceptance}`, originalText: original });
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
return entries;
|
|
212
|
+
}
|
|
213
|
+
|
|
134
214
|
function planStatusSync(scrumDir) {
|
|
135
215
|
// For each Task with Runs, align Task.status with the latest Run's status
|
|
136
216
|
// (by highest attempt number, then created date). Legacy Tasks hand-marked
|
|
@@ -537,7 +617,7 @@ function analyze(scrumDir) {
|
|
|
537
617
|
const featureIds = scanFeatureIds(scrumDir);
|
|
538
618
|
const taskIds = scanTaskIds(scrumDir);
|
|
539
619
|
const ctx = { featureIds };
|
|
540
|
-
const entries =
|
|
620
|
+
const entries = planMissingFrontmatter(scrumDir);
|
|
541
621
|
const orphanRuns = planOrphanRuns(scrumDir, taskIds);
|
|
542
622
|
|
|
543
623
|
const guardrailPlan = planGuardrails(scrumDir);
|
|
@@ -546,7 +626,8 @@ function analyze(scrumDir) {
|
|
|
546
626
|
// Extra passes: status sync, attempt resequence, acceptance criteria, secret redaction.
|
|
547
627
|
entries.push(...planStatusSync(scrumDir));
|
|
548
628
|
entries.push(...planAttemptResequence(scrumDir));
|
|
549
|
-
entries.
|
|
629
|
+
const missingFrontmatter = new Set(entries.filter((entry) => entry.kind === "legacy-frontmatter").map((entry) => entry.file));
|
|
630
|
+
entries.push(...planAcceptanceCriteria(scrumDir).filter((entry) => !missingFrontmatter.has(entry.file)));
|
|
550
631
|
entries.push(...planSecretRedaction(scrumDir));
|
|
551
632
|
|
|
552
633
|
for (const file of listDir(path.join(scrumDir, "tasks"), TASK_FILE)) {
|