scrumrun 2.7.6 → 2.7.8

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/CHANGELOG.md CHANGED
@@ -4,6 +4,12 @@ All notable changes follow Semantic Versioning.
4
4
 
5
5
  ## Unreleased
6
6
 
7
+ ## 2.7.7 - 2026-08-24
8
+
9
+ ### Added
10
+
11
+ - **Error index.** `sc knowledge errors --show` renders `.scrumrun/errors.md` — a derived index grouping `type: fix` Tasks into Open and Resolved (with branch), so reported bugs have a dedicated project log. `sc plan task --list --type <type>` now filters Tasks by type (e.g. `--type fix`).
12
+
7
13
  ## 2.7.6 - 2026-08-24
8
14
 
9
15
  ### Added
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.6` · **Method target:** `2.0.0` · **Runtime:** Node.js `>=22.13.0` · **License:** MIT
7
+ **Package:** `2.7.8` · **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
 
@@ -79,7 +79,7 @@ RECEIVED → CONTEXTUALIZING → POLICY → RISK → CLASSIFICATION
79
79
 
80
80
  The agent may assert the classification (`--type fix|task|feature|docs|discovery`) and attach a short technical preview (`--preview "…"`), rendered with color in the terminal before any Task exists. Nothing canonical is persisted before approval.
81
81
 
82
- Explicit approval atomically creates a Task and a Run. Execution then follows:
82
+ Explicit approval atomically materializes the required plan artifacts and always creates the linked Task and Run. Feature and Sprint artifacts are created automatically when the request calls for them. Execution then follows:
83
83
 
84
84
  ```text
85
85
  EXECUTING → VALIDATING → LEARNING → COMPLETED | FAILED | BLOCKED
package/bin/scrumrun.js CHANGED
@@ -31,7 +31,7 @@ const { ARTIFACT_TYPES, ArtifactRepository } = require(path.join(root, "lib", "v
31
31
  const { aliases: COMMAND_ALIASES, resolveAlias, resolveRoute } = require(path.join(root, "lib", "commands", "manifest"));
32
32
  const { renderCommandHelp, renderCompatibilityPrompt, renderRootPrompt } = require(path.join(root, "lib", "commands", "render"));
33
33
  const { planRequest } = require(path.join(root, "lib", "runtime", "request-engine"));
34
- const { addPlanArtifact, approveRequest, nextBacklogTask, refreshState, retryTask, startBacklogTask, transitionRun } = require(path.join(root, "lib", "runtime", "orchestrator"));
34
+ const { addPlanArtifact, approveRequest, nextBacklogTask, refreshErrors, refreshState, retryTask, startBacklogTask, transitionRun } = require(path.join(root, "lib", "runtime", "orchestrator"));
35
35
  const { authorizeMutation, recordMutation, satisfyGuardrail } = require(path.join(root, "lib", "runtime", "mutation-gateway"));
36
36
  const { recordArtifactReview } = require(path.join(root, "lib", "runtime", "review-service"));
37
37
  const { createMemory, listMemory, showMemory, transitionMemory } = require(path.join(root, "lib", "memory", "service"));
@@ -1378,6 +1378,11 @@ function runSemanticContext(subject, args) {
1378
1378
  console.log(JSON.stringify(result, null, 2));
1379
1379
  return;
1380
1380
  }
1381
+ if (subject === "errors") {
1382
+ const report = refreshErrors(path.join(process.cwd(), ".scrumrun"));
1383
+ console.log(report.content);
1384
+ return;
1385
+ }
1381
1386
  if (subject === "map") {
1382
1387
  if (action === "--build") {
1383
1388
  const built = rebuildIndex(process.cwd());
@@ -1537,7 +1542,9 @@ function executeRootRoute(route) {
1537
1542
  if (noun === "plan" && ["task", "run", "feature", "sprint"].includes(subject) && ["--list", "--show"].includes(routeArgs[0])) {
1538
1543
  const repository = new ArtifactRepository(projectFile());
1539
1544
  if (routeArgs[0] === "--list") {
1540
- const artifacts = repository.list(subject).map((artifact) => {
1545
+ const typeFilter = optionValue(routeArgs, "--type");
1546
+ const listed = repository.list(subject).filter((artifact) => !typeFilter || artifact.record.type === typeFilter);
1547
+ const artifacts = listed.map((artifact) => {
1541
1548
  const title = ((artifact.body || "").match(/^# ([^\r\n]+)/m) || [])[1] || artifact.record.id;
1542
1549
  const branch = artifact.record.branch ? ` [${artifact.record.branch}]` : "";
1543
1550
  return `${artifact.record.id} | ${artifact.record.status} | ${title}${branch}`;
@@ -1601,7 +1608,7 @@ function executeRootRoute(route) {
1601
1608
  }
1602
1609
  }
1603
1610
  if (noun === "knowledge" && ["fact", "decision", "insight", "dossier"].includes(subject) && v2Project()) return runV2Memory(subject, routeArgs);
1604
- if (noun === "knowledge" && ["context", "map", "study"].includes(subject) && v2Project()) return runSemanticContext(subject, routeArgs);
1611
+ if (noun === "knowledge" && ["context", "map", "study", "errors"].includes(subject) && v2Project()) return runSemanticContext(subject, routeArgs);
1605
1612
  if (noun === "knowledge" && subject === "decision") return runDecisions(routeArgs);
1606
1613
  if (noun === "knowledge" && subject === "fact") return runKnow(routeArgs);
1607
1614
  if (noun === "knowledge" && subject === "vault") return runVault(routeArgs);
@@ -38,6 +38,7 @@ const nouns = Object.freeze({
38
38
  dossier: ["--add [--title] [--content] [--evidence] [--relation] [--subject] [--source] [--source-id] [--valid-from] [--valid-until] [--review-trigger]", "--list [--include-inactive]", "--show", "--refresh [--evidence] [--note]", "--stale [--note]", "--deprecate [--note]", "--archive [--note]"],
39
39
  context: ["--build", "--update", "--show", "--clear"],
40
40
  map: ["--build", "--show"],
41
+ errors: ["--show"],
41
42
  study: ["<focus>"],
42
43
  vault: ["--add", "--list", "--show", "--remove", "--path"]
43
44
  }
@@ -173,7 +173,12 @@ function pipelineDetail(stage, plan) {
173
173
  if (stage === "CLASSIFICATION") {
174
174
  return classificationLabel(plan.classification).toLowerCase();
175
175
  }
176
- if (stage === "AWAITING_APPROVAL") return "";
176
+ if (stage === "PLANNING") {
177
+ return plan.proposal && plan.proposal.materialization ? `will materialize ${plan.proposal.materialization.summary}` : "";
178
+ }
179
+ if (stage === "AWAITING_APPROVAL") {
180
+ return plan.proposal && plan.proposal.materialization ? plan.proposal.materialization.summary : "";
181
+ }
177
182
  if (stage === "BLOCKED") return "policy denied — no token issued";
178
183
  return "";
179
184
  }
@@ -241,7 +246,8 @@ function renderIntake(plan) {
241
246
  } else if (plan.approvalToken) {
242
247
  const command = "scrumrun sc plan intake --approve";
243
248
  const tokenWidth = Math.max(16, width - 8);
244
- lines.push(boxLine(width, ` ${paint(FG.gray, "APPROVE · copy the command below to create Task + Run")}`));
249
+ const materialization = plan.proposal && plan.proposal.materialization ? plan.proposal.materialization.summary : "Task + Run";
250
+ lines.push(boxLine(width, ` ${paint(FG.gray, `APPROVE · copy the command below to create ${materialization}`)}`));
245
251
  lines.push(boxLine(width, ` ${paint(ACID_FG, "$")} ${paint(FG.white, command)}`));
246
252
  for (const chunk of hardWrap(plan.approvalToken, tokenWidth)) {
247
253
  lines.push(boxLine(width, ` ${paint(DIM_ACID_FG, chunk)}`));
@@ -260,6 +266,7 @@ function renderIntakePlain(plan) {
260
266
  lines.push(`State: ${plan.state}`);
261
267
  lines.push(`Classification: ${plan.classification.type} (${plan.classification.reason})`);
262
268
  if (plan.preview) lines.push(`Preview: ${plan.preview}`);
269
+ if (plan.proposal && plan.proposal.materialization) lines.push(`Materialization: ${plan.proposal.materialization.summary}`);
263
270
  lines.push(`Risk: ${plan.risk.level} — ${plan.risk.reasons.join("; ")}`);
264
271
  lines.push(`Policy: ${plan.policy.status} (${plan.policy.checked.length} checked; ${plan.policy.deferred.length} deferred)`);
265
272
  for (const violation of plan.policy.violations || []) lines.push(`BLOCKED: ${violation}`);
@@ -21,9 +21,12 @@ function generateBriefing(scrumDir, repository) {
21
21
  const activeWork = ["feature", "task", "sprint", "run", "review"]
22
22
  .flatMap((kind) => (snapshot.records[kind] || [])
23
23
  .filter((r) => r.id && !TERMINAL.has(r.status))
24
- .map((r) => r.assignee && r.assignee !== "agent"
25
- ? `- ${r.id} | ${r.status} | ${r.assignee} | ${r.title || r.id}`
26
- : `- ${r.id} | ${r.status} | ${r.title || r.id}`))
24
+ .map((r) => {
25
+ const owner = r.assignee && r.assignee !== "agent" ? ` | ${r.assignee}` : "";
26
+ const links = [r.feature ? `feature:${r.feature}` : null, r.sprint ? `sprint:${r.sprint}` : null].filter(Boolean);
27
+ const relation = links.length ? ` | ${links.join(" · ")}` : "";
28
+ return `- ${r.id} | ${r.status}${owner}${relation} | ${r.title || r.id}`;
29
+ }))
27
30
  .slice(0, 5);
28
31
 
29
32
  const completedRuns = (snapshot.records.run || [])
@@ -101,4 +104,40 @@ ${backlogTasks.length ? backlogTasks.join("\n") : "- No backlog Tasks."}
101
104
  return { content, sourceFingerprint, watchFingerprint: watch.fingerprint, sourceFiles: watch.files };
102
105
  }
103
106
 
104
- module.exports = { TERMINAL, generateBriefing };
107
+ function generateErrorsReport(scrumDir, repository) {
108
+ const repo = repository || new ArtifactRepository(scrumDir);
109
+ const snapshot = artifactSnapshot(scrumDir);
110
+ const sourceFingerprint = canonicalFingerprint(scrumDir, snapshot.hashes);
111
+ const fixes = repo.list("task")
112
+ .filter((artifact) => artifact.record && !artifact.errors.length && artifact.record.type === "fix")
113
+ .sort((left, right) => left.record.id.localeCompare(right.record.id));
114
+ const open = fixes.filter((artifact) => artifact.record.status !== "completed");
115
+ const resolved = fixes.filter((artifact) => artifact.record.status === "completed");
116
+ const line = (artifact) => {
117
+ const record = artifact.record;
118
+ const title = ((artifact.body || "").match(/^# ([^\r\n]+)/m) || [])[1] || record.id;
119
+ return `- ${record.id} | ${record.status} | ${title}${record.branch ? ` [${record.branch}]` : ""}`;
120
+ };
121
+ const content = `# ScrumRun Error Index
122
+
123
+ Projection schema: 1
124
+ Generated: ${new Date().toISOString()}
125
+ Source fingerprint: ${sourceFingerprint}
126
+ Authority: none; rebuild from canonical artifacts.
127
+
128
+ ## Open
129
+
130
+ ${open.length ? open.map(line).join("\n") : "- None."}
131
+
132
+ ## Resolved
133
+
134
+ ${resolved.length ? resolved.map(line).join("\n") : "- None."}
135
+
136
+ ## Summary
137
+
138
+ - Fix Tasks: ${fixes.length} (${open.length} open, ${resolved.length} resolved)
139
+ `;
140
+ return { content, sourceFingerprint };
141
+ }
142
+
143
+ module.exports = { TERMINAL, generateBriefing, generateErrorsReport };
@@ -31,6 +31,7 @@ function artifactSnapshot(scrumDir) {
31
31
  }
32
32
  return artifact.record ? {
33
33
  id: artifact.record.id,
34
+ kind: artifact.record.kind,
34
35
  status: artifact.record.status,
35
36
  title: ((artifact.body || "").match(/^# ([^\r\n]+)/m) || [])[1] || artifact.record.id,
36
37
  task: artifact.record.task || null,
@@ -58,6 +58,97 @@ function recentTechnicalSummaries(scrumDir, runRecords, limit = 5) {
58
58
  return summaries;
59
59
  }
60
60
 
61
+ function latestRunSummaries(scrumDir) {
62
+ const repository = new ArtifactRepository(scrumDir);
63
+ const latestByTask = new Map();
64
+ for (const artifact of repository.list("run")) {
65
+ const record = artifact.record;
66
+ if (!record || record.status !== "completed" || !record.task) continue;
67
+ const current = latestByTask.get(record.task);
68
+ const currentAttempt = current ? Number(current.record.attempt) || 0 : -1;
69
+ const attempt = Number(record.attempt) || 0;
70
+ if (current && (attempt < currentAttempt || (attempt === currentAttempt && (record.updated || "") <= (current.record.updated || "")))) continue;
71
+ latestByTask.set(record.task, artifact);
72
+ }
73
+ return latestByTask;
74
+ }
75
+
76
+ function latestRunSummaryForTask(runByTask, taskId) {
77
+ const artifact = runByTask.get(taskId);
78
+ if (!artifact) return null;
79
+ const summary = extractTechnicalSummary(artifact.body);
80
+ return summary ? { run: artifact.record.id, summary: capped(summary, 500) } : null;
81
+ }
82
+
83
+ function relatedWork(scrumDir, artifacts, activeWork) {
84
+ const runByTask = latestRunSummaries(scrumDir);
85
+ const featureById = new Map((artifacts.records.feature || []).map((record) => [record.id, record]));
86
+ const sprintById = new Map((artifacts.records.sprint || []).map((record) => [record.id, record]));
87
+ const tasksByFeature = new Map();
88
+ const tasksBySprint = new Map();
89
+ for (const task of artifacts.records.task || []) {
90
+ if (task.feature) {
91
+ const list = tasksByFeature.get(task.feature) || [];
92
+ list.push(task);
93
+ tasksByFeature.set(task.feature, list);
94
+ }
95
+ if (task.sprint) {
96
+ const list = tasksBySprint.get(task.sprint) || [];
97
+ list.push(task);
98
+ tasksBySprint.set(task.sprint, list);
99
+ }
100
+ }
101
+ return activeWork.slice(0, 8).map((item) => {
102
+ const related = [];
103
+ if (item.kind === "task") {
104
+ if (item.feature && featureById.has(item.feature)) related.push({ relation: "belongs_to", id: item.feature });
105
+ if (item.sprint && sprintById.has(item.sprint)) related.push({ relation: "included_in", id: item.sprint });
106
+ const latest = latestRunSummaryForTask(runByTask, item.id);
107
+ return {
108
+ id: item.id,
109
+ kind: item.kind,
110
+ status: item.status,
111
+ related,
112
+ latestRun: latest
113
+ };
114
+ }
115
+ if (item.kind === "feature") {
116
+ const linked = tasksByFeature.get(item.id) || [];
117
+ for (const task of linked.slice(0, 5)) {
118
+ related.push({ relation: "contains", id: task.id, status: task.status });
119
+ }
120
+ const summaries = linked.map((task) => latestRunSummaryForTask(runByTask, task.id)).filter(Boolean).slice(0, 3);
121
+ return {
122
+ id: item.id,
123
+ kind: item.kind,
124
+ status: item.status,
125
+ related,
126
+ recentSummaries: summaries
127
+ };
128
+ }
129
+ if (item.kind === "sprint") {
130
+ const linked = tasksBySprint.get(item.id) || [];
131
+ for (const task of linked.slice(0, 5)) {
132
+ related.push({ relation: "groups", id: task.id, status: task.status });
133
+ }
134
+ const summaries = linked.map((task) => latestRunSummaryForTask(runByTask, task.id)).filter(Boolean).slice(0, 3);
135
+ return {
136
+ id: item.id,
137
+ kind: item.kind,
138
+ status: item.status,
139
+ related,
140
+ recentSummaries: summaries
141
+ };
142
+ }
143
+ return {
144
+ id: item.id,
145
+ kind: item.kind,
146
+ status: item.status,
147
+ related
148
+ };
149
+ });
150
+ }
151
+
61
152
  function contextFingerprint(scrumDir, artifactHashes) {
62
153
  return canonicalFingerprint(scrumDir, artifactHashes);
63
154
  }
@@ -85,6 +176,7 @@ function buildContextPackage(projectRoot, request) {
85
176
  const activeWork = ["feature", "task", "sprint", "run"].flatMap((kind) =>
86
177
  (artifacts.records[kind] || []).filter((record) => !["completed", "failed", "cancelled"].includes(record.status))
87
178
  ).slice(0, 50);
179
+ const relatedWorkView = relatedWork(scrumDir, artifacts, activeWork);
88
180
  const missing = [];
89
181
  for (const relative of ["guardrails.md", "config.md", "project.md"]) {
90
182
  if (!fs.existsSync(path.join(scrumDir, relative))) missing.push(relative);
@@ -105,6 +197,7 @@ function buildContextPackage(projectRoot, request) {
105
197
  config,
106
198
  project: project || "[missing]",
107
199
  history: { recentRuns, recentSummaries },
200
+ relatedWork: relatedWorkView,
108
201
  decisions: { open: openDecisions },
109
202
  activeWork,
110
203
  graph: {
@@ -17,12 +17,12 @@ const {
17
17
  const { buildContextPackage } = require("./context");
18
18
  const { agentIdentity, evaluatePolicy } = require("./policy-engine");
19
19
  const { artifactSnapshot, canonicalFingerprint, canonicalWatchSnapshot } = require("./canonical-snapshot");
20
- const { decodeApproval } = require("./request-engine");
20
+ const { decodeApproval, materializeRequest } = require("./request-engine");
21
21
  const { containsSecret } = require("../security/secrets");
22
22
  const { currentBranch } = require("./workspace-state");
23
23
  const { extractLearningCandidates } = require("../code-intel/learning");
24
24
  const { appendRunEvent, appendTechnicalSummary, createRunBody, instant } = require("./run-ledger");
25
- const { generateBriefing } = require("./briefing");
25
+ const { generateBriefing, generateErrorsReport } = require("./briefing");
26
26
  const { assertCanonicalWrite, policyState, prepareCompletion, publicWorkspace, verifyWorkspaceIntegrity, workspaceState } = require("./mutation-gateway");
27
27
  const { recoverPendingTransactions, runKernelTransaction } = require("../v2/transaction");
28
28
 
@@ -46,6 +46,20 @@ function titleFor(request) {
46
46
  return firstLine.length > 100 ? `${firstLine.slice(0, 97)}...` : firstLine;
47
47
  }
48
48
 
49
+ function initiativeBody(title, request, created, taskId) {
50
+ return `# ${title}\n\n## Purpose\n\n${request}\n\n## Linked Tasks\n\n- ${taskId} — ${title}\n\n## Exit Criteria\n\n- [ ] Define the initiative success condition before completion.\n\n## Source\n\n- ${created}: materialized automatically from approved intake.\n`;
51
+ }
52
+
53
+ function sprintBody(title, request, created, taskId) {
54
+ return `# ${title}\n\n## Timebox\n\n${request}\n\n## Tasks\n\n- ${taskId}\n\n## Exit Gate\n\n- [ ] Define the batch completion condition before completion.\n\n## Source\n\n- ${created}: materialized automatically from approved intake. The linked Task represents Sprint planning until the requested work is decomposed into atomic Tasks.\n`;
55
+ }
56
+
57
+ function buildTaskBody(request, classification, approvalId, fingerprint, risk, previewSection, links = {}) {
58
+ const featureSection = links.feature ? `\n## Feature\n\n- ${links.feature}\n` : "";
59
+ const sprintSection = links.sprint ? `\n## Sprint\n\n- ${links.sprint}\n` : "";
60
+ return `# ${titleFor(request)}\n\n## Request\n\n${request}\n\n## Acceptance Criteria\n\n- [ ] _Define what "done" means before execution._\n${previewSection}${featureSection}${sprintSection}\n## Classification\n\n- Type: ${classification.type}\n- Reason: ${classification.reason}\n- Risk: ${risk.level}\n\n## Approval\n\n- Explicit approval token: ${approvalId}\n- Context fingerprint: ${fingerprint}`;
61
+ }
62
+
49
63
  function stateFingerprint(repository) {
50
64
  return canonicalFingerprint(repository.scrumDir, artifactSnapshot(repository.scrumDir).hashes);
51
65
  }
@@ -65,6 +79,13 @@ function refreshState(scrumDir) {
65
79
  return projection;
66
80
  }
67
81
 
82
+ function refreshErrors(scrumDir) {
83
+ const repository = new ArtifactRepository(scrumDir);
84
+ const report = generateErrorsReport(scrumDir, repository);
85
+ atomicWrite(path.join(scrumDir, "errors.md"), report.content);
86
+ return report;
87
+ }
88
+
68
89
  function stateIsStale(scrumDir) {
69
90
  const content = fs.existsSync(path.join(scrumDir, "state.md")) ? fs.readFileSync(path.join(scrumDir, "state.md"), "utf8") : "";
70
91
  const stored = (content.match(/^Source fingerprint:\s*([a-f0-9]{64})$/m) || [])[1];
@@ -105,6 +126,11 @@ function approveRequestUnlocked(projectRoot, token, { failurePoint = null, inter
105
126
  const enforceablePolicy = policyState(projectRoot);
106
127
  const baseline = publicWorkspace(workspace);
107
128
  const branch = currentBranch(projectRoot);
129
+ const materialization = materializeRequest(payload.classification);
130
+ const createFeature = materialization.links.feature;
131
+ const createSprint = materialization.links.sprint;
132
+ const featureId = createFeature ? nextId(repository, "feature") : null;
133
+ const sprintId = createSprint ? nextId(repository, "sprint") : null;
108
134
  const taskId = nextId(repository, "task");
109
135
  const runId = nextId(repository, "run");
110
136
  const task = {
@@ -115,8 +141,8 @@ function approveRequestUnlocked(projectRoot, token, { failurePoint = null, inter
115
141
  created,
116
142
  updated: created,
117
143
  method: METHOD_VERSION,
118
- feature: null,
119
- sprint: null,
144
+ feature: featureId,
145
+ sprint: sprintId,
120
146
  branch,
121
147
  assignee: agentIdentity(scrumDir) || "agent",
122
148
  approval_id: approvalId,
@@ -131,7 +157,8 @@ function approveRequestUnlocked(projectRoot, token, { failurePoint = null, inter
131
157
  updated: created,
132
158
  method: METHOD_VERSION,
133
159
  task: taskId,
134
- sprint: null,
160
+ feature: featureId,
161
+ sprint: sprintId,
135
162
  branch,
136
163
  attempt: 1,
137
164
  ledger: 1,
@@ -139,20 +166,56 @@ function approveRequestUnlocked(projectRoot, token, { failurePoint = null, inter
139
166
  workspace: 1,
140
167
  approval_id: approvalId
141
168
  };
169
+ const taskLinks = { feature: featureId, sprint: sprintId };
142
170
  const previewSection = payload.preview ? `\n\n## Preview\n\n${payload.preview}\n` : "";
143
- const taskBody = `# ${titleFor(payload.request)}\n\n## Request\n\n${payload.request}\n\n## Acceptance Criteria\n\n- [ ] _Define what "done" means before execution._\n${previewSection}\n\n## Classification\n\n- Type: ${payload.classification.type}\n- Reason: ${payload.classification.reason}\n- Risk: ${payload.risk.level}\n\n## Approval\n\n- Explicit approval token: ${approvalId}\n- Context fingerprint: ${payload.fingerprint}`;
171
+ const taskContent = buildTaskBody(payload.request, payload.classification, approvalId, payload.fingerprint, payload.risk, previewSection, taskLinks);
172
+ const featureRecord = featureId ? {
173
+ id: featureId,
174
+ kind: "feature",
175
+ status: "active",
176
+ created,
177
+ updated: created,
178
+ method: METHOD_VERSION,
179
+ branch,
180
+ type: "initiative"
181
+ } : null;
182
+ const featureBody = featureId ? initiativeBody(titleFor(payload.request), payload.request, created, taskId) : null;
183
+ const sprintRecord = sprintId ? {
184
+ id: sprintId,
185
+ kind: "sprint",
186
+ status: "running",
187
+ created,
188
+ updated: created,
189
+ method: METHOD_VERSION,
190
+ branch,
191
+ type: null,
192
+ feature: featureId
193
+ } : null;
194
+ const sprintBodyText = sprintId ? sprintBody(titleFor(payload.request), payload.request, created, taskId) : null;
144
195
  const runBody = createRunBody(run, {
145
196
  approvalId,
146
197
  obligations: payload.policy.obligations || [],
147
198
  policyFingerprint: enforceablePolicy.fingerprint,
148
199
  workspaceBaseline: baseline
149
200
  }).body;
201
+ const featurePath = featureRecord ? repository.pathFor(featureRecord) : null;
202
+ const sprintPath = sprintRecord ? repository.pathFor(sprintRecord) : null;
203
+ const taskPath = repository.pathFor(task);
204
+ const runPath = repository.pathFor(run);
205
+ const writes = [];
206
+ if (featureRecord) writes.push({ file: featurePath, previous: null, next: serializeArtifact(featureRecord, featureBody) });
207
+ if (sprintRecord) writes.push({ file: sprintPath, previous: null, next: serializeArtifact(sprintRecord, sprintBodyText) });
208
+ writes.push(
209
+ { file: taskPath, previous: null, next: serializeArtifact(task, taskContent) },
210
+ { file: runPath, previous: null, next: serializeArtifact(run, runBody) }
211
+ );
212
+ const failureTarget = {
213
+ "after-task": `after-${writes.findIndex((entry) => entry.file === taskPath) + 1}`,
214
+ "after-run": `after-${writes.findIndex((entry) => entry.file === runPath) + 1}`
215
+ };
150
216
  try {
151
- runKernelTransaction(scrumDir, "approve-task-run", [
152
- { file: repository.pathFor(task), previous: null, next: serializeArtifact(task, taskBody) },
153
- { file: repository.pathFor(run), previous: null, next: serializeArtifact(run, runBody) }
154
- ], {
155
- failurePoint: failurePoint === "after-task" ? "after-1" : failurePoint === "after-run" ? "after-2" : null,
217
+ runKernelTransaction(scrumDir, "approve-task-run", writes, {
218
+ failurePoint: failureTarget[failurePoint] || failurePoint,
156
219
  interruptPoint
157
220
  });
158
221
  } catch (error) {
@@ -535,4 +598,4 @@ function addPlanArtifact(projectRoot, kind, label, options = {}) {
535
598
  return withArtifactLock(scrumDir, "create", () => addPlanArtifactUnlocked(projectRoot, kind, label, options));
536
599
  }
537
600
 
538
- module.exports = { addPlanArtifact, approveRequest, nextBacklogTask, nextId, refreshState, renderState, retryTask, startBacklogTask, stateFingerprint, stateIsStale, transitionRun };
601
+ module.exports = { addPlanArtifact, approveRequest, nextBacklogTask, nextId, refreshErrors, refreshState, renderState, retryTask, startBacklogTask, stateFingerprint, stateIsStale, transitionRun };
@@ -51,6 +51,28 @@ function classifyRequest(request) {
51
51
  return { type: "task", taskType: "task", reason: "request is best represented as one atomic Task" };
52
52
  }
53
53
 
54
+ function materializeRequest(classification) {
55
+ const create = [];
56
+ const links = {
57
+ feature: false,
58
+ sprint: false
59
+ };
60
+ if (classification.type === "feature") {
61
+ create.push("Feature");
62
+ links.feature = true;
63
+ }
64
+ if (classification.type === "sprint") {
65
+ create.push("Sprint");
66
+ links.sprint = true;
67
+ }
68
+ create.push("Task", "Run");
69
+ return {
70
+ create,
71
+ links,
72
+ summary: create.join(" + ")
73
+ };
74
+ }
75
+
54
76
  function applyPolicy(context) {
55
77
  return evaluatePolicy(context);
56
78
  }
@@ -124,6 +146,7 @@ function planRequest(projectRoot, request, { typeOverride = null, preview = null
124
146
  if (containsSecret(preview)) throw new Error("Preview contains secret-like content.");
125
147
  safePreview = String(preview).trim().slice(0, 2000);
126
148
  }
149
+ const materialization = materializeRequest(classification);
127
150
  const plan = {
128
151
  state: policy.status === "passed" ? "awaiting_approval" : "blocked",
129
152
  pipeline: ["received", "contextualizing", "policy", "risk", "classification", "planning", policy.status === "passed" ? "awaiting_approval" : "blocked"],
@@ -135,7 +158,9 @@ function planRequest(projectRoot, request, { typeOverride = null, preview = null
135
158
  classification,
136
159
  preview: safePreview,
137
160
  proposal: {
138
- create: ["Task", "Run"],
161
+ create: materialization.create,
162
+ materialization,
163
+ relationships: materialization.links,
139
164
  sprint: classification.type === "sprint" ? "propose only after confirming real batch/timebox membership" : null,
140
165
  validation: risk.level === "high" ? "targeted tests plus configured reviewers" : "targeted tests"
141
166
  },
@@ -153,5 +178,6 @@ module.exports = {
153
178
  decodeApproval,
154
179
  encodeApproval,
155
180
  planRequest,
181
+ materializeRequest,
156
182
  stableJson
157
183
  };
@@ -17,7 +17,7 @@ const { canonicalPaths, PATHS_SCHEMA_VERSION } = require("./paths");
17
17
 
18
18
  const INVARIANTS = Object.freeze([
19
19
  { id: "I-01", summary: "pre-approval work is read-only", tests: ["intake builds bounded context without writing"] },
20
- { id: "I-02", summary: "approval creates Task and Run atomically", tests: ["explicit approval creates exactly one linked Task and Run", "approval failure injection rolls back"] },
20
+ { id: "I-02", summary: "approval materializes the requested plan shape atomically", tests: ["explicit approval creates exactly one linked Task and Run", "approval failure injection rolls back"] },
21
21
  { id: "I-03", summary: "Task is atomic and Sprint only groups evidenced batches", tests: ["migration apply creates linked v2 artifacts", "artifact repository builds explicit Feature Task Sprint Run graph"] },
22
22
  { id: "I-04", summary: "retry preserves prior Runs", tests: ["retry creates a new Run and preserves"] },
23
23
  { id: "I-05", summary: "state transitions are declared, evidenced, ordered, and recoverable", tests: ["run state machine follows", "Run ledger uses stable event ids", "paired transition failure restores"] },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "scrumrun",
3
- "version": "2.7.6",
3
+ "version": "2.7.8",
4
4
  "description": "Evidence-driven Agile runtime and semantic project memory for AI coding agents.",
5
5
  "bin": {
6
6
  "scrumrun": "bin/scrumrun.js",
package/types/index.d.ts CHANGED
@@ -392,6 +392,7 @@ export function retryTask(projectRoot: string, taskId: string, options?: { note?
392
392
  export function startBacklogTask(projectRoot: string, taskId: string, options?: { note?: string; failurePoint?: string | null; interruptPoint?: string | null }): { run: ArtifactRecord; task: ArtifactRecord; content: string; recovered?: unknown[] };
393
393
  export function nextBacklogTask(repository: ArtifactRepository): ArtifactRecord | null;
394
394
  export function refreshState(scrumDir: string): StateProjection;
395
+ export function refreshErrors(scrumDir: string): { content: string; sourceFingerprint: string };
395
396
  export function renderState(repository: ArtifactRepository): string;
396
397
  export function stateFingerprint(repository: ArtifactRepository): string;
397
398
  export function stateIsStale(scrumDir: string): boolean;