scrumrun 2.7.7 → 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/README.md +2 -2
- package/lib/commands/pretty-intake.js +9 -2
- package/lib/runtime/briefing.js +6 -3
- package/lib/runtime/canonical-snapshot.js +1 -0
- package/lib/runtime/context.js +93 -0
- package/lib/runtime/orchestrator.js +66 -10
- package/lib/runtime/request-engine.js +27 -1
- package/lib/v2/conformance.js +1 -1
- 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.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
|
|
@@ -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 || [])
|
|
@@ -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,7 +17,7 @@ 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");
|
|
@@ -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
|
}
|
|
@@ -112,6 +126,11 @@ function approveRequestUnlocked(projectRoot, token, { failurePoint = null, inter
|
|
|
112
126
|
const enforceablePolicy = policyState(projectRoot);
|
|
113
127
|
const baseline = publicWorkspace(workspace);
|
|
114
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;
|
|
115
134
|
const taskId = nextId(repository, "task");
|
|
116
135
|
const runId = nextId(repository, "run");
|
|
117
136
|
const task = {
|
|
@@ -122,8 +141,8 @@ function approveRequestUnlocked(projectRoot, token, { failurePoint = null, inter
|
|
|
122
141
|
created,
|
|
123
142
|
updated: created,
|
|
124
143
|
method: METHOD_VERSION,
|
|
125
|
-
feature:
|
|
126
|
-
sprint:
|
|
144
|
+
feature: featureId,
|
|
145
|
+
sprint: sprintId,
|
|
127
146
|
branch,
|
|
128
147
|
assignee: agentIdentity(scrumDir) || "agent",
|
|
129
148
|
approval_id: approvalId,
|
|
@@ -138,7 +157,8 @@ function approveRequestUnlocked(projectRoot, token, { failurePoint = null, inter
|
|
|
138
157
|
updated: created,
|
|
139
158
|
method: METHOD_VERSION,
|
|
140
159
|
task: taskId,
|
|
141
|
-
|
|
160
|
+
feature: featureId,
|
|
161
|
+
sprint: sprintId,
|
|
142
162
|
branch,
|
|
143
163
|
attempt: 1,
|
|
144
164
|
ledger: 1,
|
|
@@ -146,20 +166,56 @@ function approveRequestUnlocked(projectRoot, token, { failurePoint = null, inter
|
|
|
146
166
|
workspace: 1,
|
|
147
167
|
approval_id: approvalId
|
|
148
168
|
};
|
|
169
|
+
const taskLinks = { feature: featureId, sprint: sprintId };
|
|
149
170
|
const previewSection = payload.preview ? `\n\n## Preview\n\n${payload.preview}\n` : "";
|
|
150
|
-
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;
|
|
151
195
|
const runBody = createRunBody(run, {
|
|
152
196
|
approvalId,
|
|
153
197
|
obligations: payload.policy.obligations || [],
|
|
154
198
|
policyFingerprint: enforceablePolicy.fingerprint,
|
|
155
199
|
workspaceBaseline: baseline
|
|
156
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
|
+
};
|
|
157
216
|
try {
|
|
158
|
-
runKernelTransaction(scrumDir, "approve-task-run",
|
|
159
|
-
|
|
160
|
-
{ file: repository.pathFor(run), previous: null, next: serializeArtifact(run, runBody) }
|
|
161
|
-
], {
|
|
162
|
-
failurePoint: failurePoint === "after-task" ? "after-1" : failurePoint === "after-run" ? "after-2" : null,
|
|
217
|
+
runKernelTransaction(scrumDir, "approve-task-run", writes, {
|
|
218
|
+
failurePoint: failureTarget[failurePoint] || failurePoint,
|
|
163
219
|
interruptPoint
|
|
164
220
|
});
|
|
165
221
|
} catch (error) {
|
|
@@ -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"] },
|