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 +6 -0
- package/README.md +2 -2
- package/bin/scrumrun.js +10 -3
- package/lib/commands/manifest.js +1 -0
- package/lib/commands/pretty-intake.js +9 -2
- package/lib/runtime/briefing.js +43 -4
- package/lib/runtime/canonical-snapshot.js +1 -0
- package/lib/runtime/context.js +93 -0
- package/lib/runtime/orchestrator.js +75 -12
- package/lib/runtime/request-engine.js +27 -1
- package/lib/v2/conformance.js +1 -1
- package/package.json +1 -1
- package/types/index.d.ts +1 -0
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.
|
|
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
|
|
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
|
|
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);
|
package/lib/commands/manifest.js
CHANGED
|
@@ -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 === "
|
|
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
|
-
|
|
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}`);
|
package/lib/runtime/briefing.js
CHANGED
|
@@ -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) =>
|
|
25
|
-
|
|
26
|
-
|
|
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
|
-
|
|
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,
|
package/lib/runtime/context.js
CHANGED
|
@@ -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:
|
|
119
|
-
sprint:
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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:
|
|
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
|
};
|
package/lib/v2/conformance.js
CHANGED
|
@@ -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
|
|
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
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;
|