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 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.9` · **Method target:** `2.0.0` · **Runtime:** Node.js `>=22.13.0` · **License:** MIT
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
@@ -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.push(...planAcceptanceCriteria(scrumDir));
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)) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "scrumrun",
3
- "version": "2.7.9",
3
+ "version": "2.7.10",
4
4
  "description": "Evidence-driven Agile runtime and semantic project memory for AI coding agents.",
5
5
  "bin": {
6
6
  "scrumrun": "bin/scrumrun.js",