scrumrun 2.7.8 → 2.7.9

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.8` · **Method target:** `2.0.0` · **Runtime:** Node.js `>=22.13.0` · **License:** MIT
7
+ **Package:** `2.7.9` · **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
 
@@ -23,6 +23,8 @@ const { RUN_LEDGER_VERSION } = require("../v2/schema");
23
23
 
24
24
  const RUN_FILENAME_PATTERN = /^RUN-\d{3,}\.md$/;
25
25
  const TASK_REF_PATTERN = /^TASK-\d{3,}$/;
26
+ const RUN_REF_PATTERN = /^RUN-\d{3,}$/;
27
+ const DATE_PATTERN = /^\d{4}-\d{2}-\d{2}$/;
26
28
 
27
29
  function readFrontmatterAndBody(text) {
28
30
  const match = String(text || "").match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/);
@@ -46,6 +48,11 @@ function extractTitle(body, fallback) {
46
48
  return match ? match[1] : fallback;
47
49
  }
48
50
 
51
+ function extractTaskReference(body) {
52
+ const match = String(body || "").match(/\b(TASK-\d{3,})\b/);
53
+ return match ? match[1] : null;
54
+ }
55
+
49
56
  function serializeFrontmatter(frontmatter) {
50
57
  const lines = Object.entries(frontmatter).map(([key, value]) => {
51
58
  if (value === null || value === undefined) return `${key}: null`;
@@ -71,27 +78,41 @@ function reserveBackupPath(backupDir, runId) {
71
78
 
72
79
  function needsNormalization(record, body) {
73
80
  if (!record) return { needs: true, reason: "frontmatter is malformed or missing" };
81
+ const identityErrors = [];
82
+ if (!RUN_REF_PATTERN.test(record.id || "")) identityErrors.push("id is missing or does not match RUN-NNN");
83
+ if (!DATE_PATTERN.test(record.created || "")) identityErrors.push("created is missing or is not YYYY-MM-DD");
74
84
  if (typeof record.task === "string" && record.task && !TASK_REF_PATTERN.test(record.task)) {
75
- return { needs: true, reason: `task field does not match TASK-NNN (was ${JSON.stringify(record.task)})` };
85
+ identityErrors.push(`task field does not match TASK-NNN (was ${JSON.stringify(record.task)})`);
86
+ }
87
+ if ((record.task === null || record.task === undefined || record.task === "") && !extractTaskReference(body)) {
88
+ identityErrors.push("task is missing and cannot be inferred from the legacy body");
76
89
  }
77
90
  const result = validateRunLedger(record, body);
78
91
  const errors = result.errors || [];
79
- if (errors.length) return { needs: true, reason: `ledger validation failed (${errors.length} error${errors.length === 1 ? "" : "s"})`, errors };
92
+ if (errors.length || identityErrors.length) {
93
+ const allErrors = [...identityErrors, ...errors];
94
+ const primaryIdentityError = identityErrors.find((error) => error.startsWith("task field")) || identityErrors[0];
95
+ return { needs: true, reason: primaryIdentityError || `ledger validation failed (${errors.length} error${errors.length === 1 ? "" : "s"})`, errors: allErrors };
96
+ }
80
97
  return { needs: false };
81
98
  }
82
99
 
83
- function buildNormalizedBody({ record, originalBody, backupRelative, reason, errors }) {
84
- const title = extractTitle(originalBody, `Run ${record.id}`);
100
+ function buildNormalizedBody({ record, runId, originalBody, backupRelative, reason, errors }) {
101
+ const normalizedId = RUN_REF_PATTERN.test(record.id || "") ? record.id : runId;
102
+ const normalizedTask = TASK_REF_PATTERN.test(record.task || "") ? record.task : extractTaskReference(originalBody);
103
+ const snapshotDate = DATE_PATTERN.test(record.created || "")
104
+ ? record.created
105
+ : DATE_PATTERN.test(record.updated || "") ? record.updated : new Date().toISOString().slice(0, 10);
106
+ const title = extractTitle(originalBody, `Run ${normalizedId}`);
85
107
  const sha = crypto.createHash("sha256").update(originalBody).digest("hex");
86
- const snapshotDate = record.created || new Date().toISOString().slice(0, 10);
87
108
  const normalizedRecord = {
88
- id: record.id,
109
+ id: normalizedId,
89
110
  kind: "run",
90
111
  status: record.status || "partial",
91
- created: record.created,
112
+ created: snapshotDate,
92
113
  updated: snapshotDate,
93
114
  method: "2.0.0",
94
- task: TASK_REF_PATTERN.test(record.task || "") ? record.task : null,
115
+ task: normalizedTask,
95
116
  sprint: record.sprint || null,
96
117
  attempt: record.attempt || 1,
97
118
  ledger: RUN_LEDGER_VERSION
@@ -157,6 +178,7 @@ function apply(scrumDir, plan) {
157
178
  const backupRelative = path.relative(scrumDir, backupPath);
158
179
  const normalized = buildNormalizedBody({
159
180
  record: frontmatter,
181
+ runId: entry.id,
160
182
  originalBody: raw,
161
183
  backupRelative,
162
184
  reason: entry.reason,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "scrumrun",
3
- "version": "2.7.8",
3
+ "version": "2.7.9",
4
4
  "description": "Evidence-driven Agile runtime and semantic project memory for AI coding agents.",
5
5
  "bin": {
6
6
  "scrumrun": "bin/scrumrun.js",