@abelxiaoxing/cadence 1.4.0 → 1.4.1
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 +58 -4
- package/agents/design-explorer.md +17 -11
- package/agents/diagnosis-worker.md +9 -1
- package/agents/implementation-worker.md +3 -1
- package/config/plan-draft.example.json +122 -0
- package/config/plan-draft.multiple-tasks.example.json +345 -0
- package/package.json +5 -3
- package/prompts/abel-design.md +93 -44
- package/src/activity-contracts.ts +82 -0
- package/src/candidate-artifact.ts +143 -0
- package/src/candidate-patch.ts +31 -11
- package/src/candidate-workspace.ts +74 -0
- package/src/change-contract-schema.ts +191 -0
- package/src/change-contract.ts +5 -0
- package/src/change-verification.ts +896 -0
- package/src/child-model.ts +129 -0
- package/src/child-session.ts +293 -326
- package/src/contracts.ts +184 -76
- package/src/delivery-compiler.ts +103 -120
- package/src/delivery-revision.ts +42 -0
- package/src/design-control.ts +113 -4
- package/src/design-diagnostics.ts +150 -0
- package/src/durable-contracts.ts +210 -0
- package/src/durable-workflow.ts +148 -3570
- package/src/evidence-draft.ts +122 -0
- package/src/implement-graph.ts +3 -0
- package/src/implement-plan.ts +70 -0
- package/src/index.ts +59 -522
- package/src/model-source.ts +22 -0
- package/src/package-candidate.ts +224 -0
- package/src/package-workflow.ts +253 -0
- package/src/packet-runtime.ts +27 -48
- package/src/parent-provider.ts +86 -198
- package/src/phase-execution.ts +2248 -0
- package/src/pi-adapter.ts +19 -0
- package/src/plan-draft-summary.ts +146 -0
- package/src/plan-draft.ts +493 -0
- package/src/subagent-activity.ts +32 -66
- package/src/submit-tool.ts +20 -59
- package/src/transport-budget.ts +32 -0
- package/src/worker-broker.ts +37 -57
- package/src/workflow-policy.ts +14 -53
- package/src/workflow-recovery-policy.ts +95 -0
- package/src/workflow-scheduling.ts +105 -0
- package/src/workflow-state-machine.ts +105 -497
- package/src/workflow-status.ts +384 -0
- package/src/workspace-io-worker.mjs +7 -0
- package/src/empty-resource-loader.ts +0 -38
- package/src/parent-payload-bridge.ts +0 -523
package/src/delivery-compiler.ts
CHANGED
|
@@ -2,14 +2,12 @@ import { createHash } from "node:crypto";
|
|
|
2
2
|
import { compareCanonicalStrings } from "./canonical.ts";
|
|
3
3
|
import {
|
|
4
4
|
assertPlanWithinChangeContract,
|
|
5
|
-
type ChangeContract,
|
|
6
5
|
normalizeChangeContract,
|
|
7
6
|
} from "./change-contract.ts";
|
|
8
7
|
import {
|
|
9
8
|
type AgentsImpact,
|
|
10
9
|
type ImplementGraphBoundary,
|
|
11
10
|
type ImplementGraphOutput,
|
|
12
|
-
type ImplementTaskBoundary,
|
|
13
11
|
isAgentsPath,
|
|
14
12
|
isValidRelativePath,
|
|
15
13
|
type PhaseBoundary,
|
|
@@ -17,12 +15,24 @@ import {
|
|
|
17
15
|
validateImplementGraphBoundary,
|
|
18
16
|
validateVerificationContract,
|
|
19
17
|
} from "./contracts.ts";
|
|
18
|
+
import {
|
|
19
|
+
type DesignPlanDiagnostic,
|
|
20
|
+
DesignPlanValidationError,
|
|
21
|
+
} from "./design-diagnostics.ts";
|
|
20
22
|
import {
|
|
21
23
|
assessImplementGraphReadiness,
|
|
22
24
|
canonicalJson,
|
|
23
25
|
hashCanonicalValue,
|
|
24
26
|
type ImplementGraphReadiness,
|
|
25
27
|
} from "./implement-graph.ts";
|
|
28
|
+
import type {
|
|
29
|
+
ImplementPlan,
|
|
30
|
+
PlanAgentsCheckpointOperation,
|
|
31
|
+
PlanTaskDraft,
|
|
32
|
+
PlanTracking,
|
|
33
|
+
PlanVerification,
|
|
34
|
+
} from "./implement-plan.ts";
|
|
35
|
+
import { expandPlanDraft, preparePlanDraft } from "./plan-draft.ts";
|
|
26
36
|
import {
|
|
27
37
|
bindDraftVerificationInputs,
|
|
28
38
|
validateVerificationAdapterCapability,
|
|
@@ -103,40 +113,6 @@ export class DeliveryValidationError extends Error {
|
|
|
103
113
|
}
|
|
104
114
|
}
|
|
105
115
|
|
|
106
|
-
export interface DesignPlanDiagnostic {
|
|
107
|
-
code: string;
|
|
108
|
-
taskId?: string;
|
|
109
|
-
phase?: string;
|
|
110
|
-
field?: string;
|
|
111
|
-
category?: string;
|
|
112
|
-
owner?: string;
|
|
113
|
-
verificationId?: string;
|
|
114
|
-
outputId?: string;
|
|
115
|
-
dependencyTaskId?: string;
|
|
116
|
-
producerTaskId?: string;
|
|
117
|
-
producerPhase?: string;
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
export class DesignPlanValidationError extends Error {
|
|
121
|
-
readonly diagnostics: readonly DesignPlanDiagnostic[];
|
|
122
|
-
|
|
123
|
-
constructor(message: string, diagnostics: readonly DesignPlanDiagnostic[]) {
|
|
124
|
-
const normalized = [
|
|
125
|
-
...new Map(
|
|
126
|
-
diagnostics
|
|
127
|
-
.map((diagnostic) => structuredClone(diagnostic))
|
|
128
|
-
.sort((left, right) =>
|
|
129
|
-
compareCanonicalStrings(canonicalJson(left), canonicalJson(right)),
|
|
130
|
-
)
|
|
131
|
-
.map((diagnostic) => [canonicalJson(diagnostic), diagnostic]),
|
|
132
|
-
).values(),
|
|
133
|
-
];
|
|
134
|
-
super(message);
|
|
135
|
-
this.name = "DesignPlanValidationError";
|
|
136
|
-
this.diagnostics = Object.freeze(normalized);
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
|
|
140
116
|
const SAFE_PLAN_ERROR_CODE = /^[a-z0-9](?:[a-z0-9._-]{0,126}[a-z0-9])?$/u;
|
|
141
117
|
|
|
142
118
|
function planErrorCode(error: unknown): string {
|
|
@@ -182,6 +158,22 @@ export function assessDeliveryTraceability(input: {
|
|
|
182
158
|
plan: ImplementPlan;
|
|
183
159
|
}): DeliveryTraceabilityAssessment {
|
|
184
160
|
const diagnostics = new Set<string>();
|
|
161
|
+
let authorMarkdown = input.tasksMarkdown;
|
|
162
|
+
let bindings: string | undefined;
|
|
163
|
+
try {
|
|
164
|
+
const region = taskVerificationRegion(input.tasksMarkdown);
|
|
165
|
+
if (region) {
|
|
166
|
+
authorMarkdown =
|
|
167
|
+
input.tasksMarkdown.slice(0, region.start) +
|
|
168
|
+
input.tasksMarkdown.slice(region.end);
|
|
169
|
+
bindings = region.text;
|
|
170
|
+
if (bindings !== renderTasks(input.plan)) {
|
|
171
|
+
diagnostics.add("traceability-managed-region-stale");
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
} catch {
|
|
175
|
+
return { ok: false, diagnostics: ["traceability-managed-region-invalid"] };
|
|
176
|
+
}
|
|
185
177
|
const headings = new Set<string>();
|
|
186
178
|
for (const spec of input.specs) {
|
|
187
179
|
if (!isValidRelativePath(spec.path) || !spec.path.endsWith("/spec.md")) {
|
|
@@ -206,7 +198,7 @@ export function assessDeliveryTraceability(input: {
|
|
|
206
198
|
}
|
|
207
199
|
}
|
|
208
200
|
const references = [
|
|
209
|
-
...
|
|
201
|
+
...authorMarkdown.matchAll(/`(specs\/[^`\n]+\/spec\.md#[^`\n]+)`/gu),
|
|
210
202
|
].flatMap((match) => (match[1] ? [match[1].trim()] : []));
|
|
211
203
|
if (references.length === 0) diagnostics.add("traceability-reference-empty");
|
|
212
204
|
const referenceSet = new Set(references);
|
|
@@ -220,9 +212,7 @@ export function assessDeliveryTraceability(input: {
|
|
|
220
212
|
diagnostics.add("traceability-scenario-unowned");
|
|
221
213
|
}
|
|
222
214
|
const taskIds = input.plan.tasks.map((task) => task.taskId).sort();
|
|
223
|
-
if (
|
|
224
|
-
taskIds.some((taskId) => !input.tasksMarkdown.includes(`\`${taskId}\``))
|
|
225
|
-
) {
|
|
215
|
+
if (taskIds.some((taskId) => !authorMarkdown.includes(`\`${taskId}\``))) {
|
|
226
216
|
diagnostics.add("traceability-task-unmapped");
|
|
227
217
|
}
|
|
228
218
|
const verificationIds = input.plan.tasks
|
|
@@ -236,7 +226,8 @@ export function assessDeliveryTraceability(input: {
|
|
|
236
226
|
.sort();
|
|
237
227
|
if (
|
|
238
228
|
verificationIds.some(
|
|
239
|
-
(verificationId) =>
|
|
229
|
+
(verificationId) =>
|
|
230
|
+
!(bindings ?? authorMarkdown).includes(verificationId),
|
|
240
231
|
)
|
|
241
232
|
) {
|
|
242
233
|
diagnostics.add("traceability-verification-unmapped");
|
|
@@ -260,70 +251,16 @@ export function assessDeliveryTraceability(input: {
|
|
|
260
251
|
};
|
|
261
252
|
}
|
|
262
253
|
|
|
263
|
-
export type
|
|
264
|
-
|
|
265
|
-
export
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
export interface PlanVerification {
|
|
276
|
-
baseline: {
|
|
277
|
-
target: "task-red-contracts";
|
|
278
|
-
affected: "task-affected-contracts";
|
|
279
|
-
fullSuite: StructuredVerificationContract;
|
|
280
|
-
failureIdentity: "normalized";
|
|
281
|
-
};
|
|
282
|
-
change: {
|
|
283
|
-
affected: "task-affected-contracts";
|
|
284
|
-
fullSuite: StructuredVerificationContract;
|
|
285
|
-
postApply: StructuredVerificationContract;
|
|
286
|
-
};
|
|
287
|
-
artifactCorrection: {
|
|
288
|
-
maxAttempts: number;
|
|
289
|
-
};
|
|
290
|
-
repair: {
|
|
291
|
-
maxAttempts: number;
|
|
292
|
-
inBoundaryOnly: true;
|
|
293
|
-
approvalOnBoundaryExpansion: true;
|
|
294
|
-
attribution: ["pre-existing", "introduced", "unresolved", "environment"];
|
|
295
|
-
};
|
|
296
|
-
agentsCheckpoint: {
|
|
297
|
-
required: boolean;
|
|
298
|
-
verification: StructuredVerificationContract | null;
|
|
299
|
-
operations: PlanAgentsCheckpointOperation[];
|
|
300
|
-
};
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
export interface PlanAgentsCheckpointOperation {
|
|
304
|
-
target: string;
|
|
305
|
-
impact: Exclude<AgentsImpact, "none">;
|
|
306
|
-
taskIds: string[];
|
|
307
|
-
managedBlock: string | null;
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
export interface PlanTracking {
|
|
311
|
-
path: "tasks.md";
|
|
312
|
-
format: "markdown-checkbox";
|
|
313
|
-
taskIds: string[];
|
|
314
|
-
completionOwner: "parent";
|
|
315
|
-
}
|
|
316
|
-
|
|
317
|
-
export interface PlanDraft {
|
|
318
|
-
changeContract?: ChangeContract;
|
|
319
|
-
changeId: string;
|
|
320
|
-
tasks: PlanTaskDraft[];
|
|
321
|
-
outputs: ImplementGraphOutput[];
|
|
322
|
-
verification: PlanVerification;
|
|
323
|
-
tracking: PlanTracking;
|
|
324
|
-
}
|
|
325
|
-
|
|
326
|
-
export type ImplementPlan = PlanDraft;
|
|
254
|
+
export type { DesignPlanDiagnostic } from "./design-diagnostics.ts";
|
|
255
|
+
export { DesignPlanValidationError } from "./design-diagnostics.ts";
|
|
256
|
+
export type {
|
|
257
|
+
ImplementPlan,
|
|
258
|
+
PlanAgentsCheckpointOperation,
|
|
259
|
+
PlanTaskDraft,
|
|
260
|
+
PlanTracking,
|
|
261
|
+
PlanVerification,
|
|
262
|
+
} from "./implement-plan.ts";
|
|
263
|
+
export type { PlanDraft, PlanPhaseDraft, PlanTaskInput } from "./plan-draft.ts";
|
|
327
264
|
|
|
328
265
|
export interface CompiledDelivery {
|
|
329
266
|
plan: ImplementPlan;
|
|
@@ -690,7 +627,7 @@ function normalizeVerification(
|
|
|
690
627
|
return cloned;
|
|
691
628
|
}
|
|
692
629
|
|
|
693
|
-
function normalizePhase(value: unknown, readOnly = false):
|
|
630
|
+
function normalizePhase(value: unknown, readOnly = false): PhaseBoundary {
|
|
694
631
|
if (
|
|
695
632
|
!isRecord(value) ||
|
|
696
633
|
!exactKeys(value, [
|
|
@@ -837,7 +774,7 @@ function normalizeTask(value: unknown): PlanTaskDraft {
|
|
|
837
774
|
}
|
|
838
775
|
|
|
839
776
|
function graphFromPlan(plan: ImplementPlan): ImplementGraphBoundary {
|
|
840
|
-
const stripPhase = (phase:
|
|
777
|
+
const stripPhase = (phase: PhaseBoundary): PhaseBoundary => ({
|
|
841
778
|
read: phase.read,
|
|
842
779
|
write: phase.write,
|
|
843
780
|
delete: phase.delete,
|
|
@@ -1177,7 +1114,7 @@ function normalizeDraft(value: unknown): ImplementPlan {
|
|
|
1177
1114
|
if (!draftGraph.ok) {
|
|
1178
1115
|
throw new DesignPlanValidationError(
|
|
1179
1116
|
`delivery-plan-invalid:${draftGraph.reason}`,
|
|
1180
|
-
[draftGraph.diagnostic],
|
|
1117
|
+
draftGraph.diagnostics ?? [draftGraph.diagnostic],
|
|
1181
1118
|
);
|
|
1182
1119
|
}
|
|
1183
1120
|
const plan: ImplementPlan = {
|
|
@@ -1247,32 +1184,78 @@ function normalizeDraft(value: unknown): ImplementPlan {
|
|
|
1247
1184
|
return plan;
|
|
1248
1185
|
}
|
|
1249
1186
|
|
|
1187
|
+
const TASK_BINDINGS_START = "<!-- ABEL:VERIFICATION-BINDINGS:START -->";
|
|
1188
|
+
const TASK_BINDINGS_END = "<!-- ABEL:VERIFICATION-BINDINGS:END -->";
|
|
1189
|
+
|
|
1190
|
+
function taskVerificationRegion(markdown: string) {
|
|
1191
|
+
const markers = markdown.match(/<!-- ABEL:VERIFICATION-BINDINGS/gu) ?? [];
|
|
1192
|
+
if (markers.length === 0) return undefined;
|
|
1193
|
+
const match =
|
|
1194
|
+
/^<!-- ABEL:VERIFICATION-BINDINGS:START -->\r?\n[\s\S]*?^<!-- ABEL:VERIFICATION-BINDINGS:END -->(?=\r?$)/mu.exec(
|
|
1195
|
+
markdown,
|
|
1196
|
+
);
|
|
1197
|
+
if (markers.length !== 2 || !match) {
|
|
1198
|
+
throw new DesignPlanValidationError("traceability-managed-region-invalid", [
|
|
1199
|
+
{ code: "traceability-managed-region-invalid", path: "tasks.md" },
|
|
1200
|
+
]);
|
|
1201
|
+
}
|
|
1202
|
+
return {
|
|
1203
|
+
start: match.index,
|
|
1204
|
+
end: match.index + match[0].length,
|
|
1205
|
+
text: match[0],
|
|
1206
|
+
};
|
|
1207
|
+
}
|
|
1208
|
+
|
|
1209
|
+
// Only identity bindings are generated. Author task checkboxes and Scenario
|
|
1210
|
+
// ownership remain outside this region and are validated independently.
|
|
1250
1211
|
function renderTasks(plan: ImplementPlan): string {
|
|
1251
|
-
const lines = ["
|
|
1252
|
-
for (const task of plan.tasks)
|
|
1253
|
-
|
|
1212
|
+
const lines = [TASK_BINDINGS_START, "## Compiled verification bindings", ""];
|
|
1213
|
+
for (const task of [...plan.tasks].sort((a, b) =>
|
|
1214
|
+
compareCanonicalStrings(a.taskId, b.taskId),
|
|
1215
|
+
)) {
|
|
1254
1216
|
lines.push(
|
|
1255
|
-
|
|
1217
|
+
`- Task \`${task.taskId}\` (${task.verificationMode ?? "behavior"})`,
|
|
1256
1218
|
);
|
|
1257
|
-
if (!task.verificationMode || task.verificationMode === "behavior")
|
|
1258
|
-
lines.push(` - Red
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
);
|
|
1263
|
-
|
|
1219
|
+
if (!task.verificationMode || task.verificationMode === "behavior") {
|
|
1220
|
+
lines.push(` - Red: \`${task.phases.red.verification.id}\``);
|
|
1221
|
+
}
|
|
1222
|
+
lines.push(` - Green: \`${task.phases.green.verification.id}\``);
|
|
1223
|
+
if (task.phases.refactor) {
|
|
1224
|
+
lines.push(` - Refactor: \`${task.phases.refactor.verification.id}\``);
|
|
1225
|
+
}
|
|
1264
1226
|
}
|
|
1265
|
-
lines.push(
|
|
1227
|
+
lines.push(TASK_BINDINGS_END);
|
|
1266
1228
|
return lines.join("\n");
|
|
1267
1229
|
}
|
|
1268
1230
|
|
|
1231
|
+
export function bindTaskVerifications(
|
|
1232
|
+
markdown: string,
|
|
1233
|
+
plan: ImplementPlan,
|
|
1234
|
+
): string {
|
|
1235
|
+
const region = taskVerificationRegion(markdown);
|
|
1236
|
+
const bindings = renderTasks(plan);
|
|
1237
|
+
if (region) {
|
|
1238
|
+
return (
|
|
1239
|
+
markdown.slice(0, region.start) + bindings + markdown.slice(region.end)
|
|
1240
|
+
);
|
|
1241
|
+
}
|
|
1242
|
+
return `${markdown}${markdown.endsWith("\n") ? "\n" : "\n\n"}${bindings}\n`;
|
|
1243
|
+
}
|
|
1244
|
+
|
|
1269
1245
|
export function compileImplementPlan(
|
|
1270
1246
|
draft: unknown,
|
|
1271
1247
|
options: { consumerRoot: string; bindExecutionInputs?: boolean },
|
|
1272
1248
|
): CompiledDelivery {
|
|
1273
1249
|
try {
|
|
1250
|
+
// Resolve omitted manifest command bytes before named definitions are
|
|
1251
|
+
// validated and purpose identities are hashed. The second pass below
|
|
1252
|
+
// binds execution inputs on the expanded contracts with their final IDs.
|
|
1253
|
+
if (options.bindExecutionInputs)
|
|
1254
|
+
draft = bindDraftVerificationInputs(options.consumerRoot, draft);
|
|
1255
|
+
draft = expandPlanDraft(draft);
|
|
1274
1256
|
if (options.bindExecutionInputs)
|
|
1275
1257
|
draft = bindDraftVerificationInputs(options.consumerRoot, draft);
|
|
1258
|
+
draft = preparePlanDraft(draft);
|
|
1276
1259
|
const normalized = normalizeDraft(draft);
|
|
1277
1260
|
const plan =
|
|
1278
1261
|
isRecord(draft) && legacyPlans.get(draft) === JSON.stringify(draft)
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type { ImplementPlan } from "./implement-plan.ts";
|
|
2
|
+
import {
|
|
3
|
+
deliveryBoundPaths,
|
|
4
|
+
type EngineTaskRow,
|
|
5
|
+
taskApprovalBoundary,
|
|
6
|
+
} from "./workflow-policy.ts";
|
|
7
|
+
/** Compare authority only; retained bytes still require executor currentness and replay. */
|
|
8
|
+
export function compareDeliveryRevision(
|
|
9
|
+
priorPlan: ImplementPlan | undefined,
|
|
10
|
+
nextPlan: ImplementPlan,
|
|
11
|
+
priorRows: readonly Pick<EngineTaskRow, "task_id">[],
|
|
12
|
+
) {
|
|
13
|
+
const priorTasks = new Map(
|
|
14
|
+
(priorPlan?.tasks ?? []).map((task) => [task.taskId, task]),
|
|
15
|
+
);
|
|
16
|
+
const nextTasks = new Map(nextPlan.tasks.map((task) => [task.taskId, task]));
|
|
17
|
+
const priorBoundPaths = new Set(
|
|
18
|
+
priorPlan ? deliveryBoundPaths(priorPlan) : [],
|
|
19
|
+
);
|
|
20
|
+
const boundaryExpanded = deliveryBoundPaths(nextPlan).some(
|
|
21
|
+
(relative) => !priorBoundPaths.has(relative),
|
|
22
|
+
);
|
|
23
|
+
const compatible = new Set<string>();
|
|
24
|
+
if (priorPlan) {
|
|
25
|
+
for (const [taskId, priorTask] of priorTasks) {
|
|
26
|
+
const nextTask = nextTasks.get(taskId);
|
|
27
|
+
if (
|
|
28
|
+
nextTask &&
|
|
29
|
+
taskApprovalBoundary(priorPlan, priorTask) ===
|
|
30
|
+
taskApprovalBoundary(nextPlan, nextTask)
|
|
31
|
+
) {
|
|
32
|
+
compatible.add(taskId);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
const invalidated = new Set(
|
|
37
|
+
priorRows
|
|
38
|
+
.filter((row) => !compatible.has(row.task_id))
|
|
39
|
+
.map((row) => row.task_id),
|
|
40
|
+
);
|
|
41
|
+
return { compatible, invalidated, boundaryExpanded };
|
|
42
|
+
}
|
package/src/design-control.ts
CHANGED
|
@@ -13,9 +13,11 @@ import {
|
|
|
13
13
|
type ChangeContract,
|
|
14
14
|
normalizeChangeContract,
|
|
15
15
|
} from "./change-contract.ts";
|
|
16
|
+
import { changeContractDiagnostics } from "./change-contract-schema.ts";
|
|
16
17
|
import { isValidRelativePath } from "./contracts.ts";
|
|
17
18
|
import {
|
|
18
19
|
assessDeliveryTraceability,
|
|
20
|
+
bindTaskVerifications,
|
|
19
21
|
compileGateAReceipt,
|
|
20
22
|
compileImplementPlan,
|
|
21
23
|
compileReadyReceipt,
|
|
@@ -26,6 +28,7 @@ import {
|
|
|
26
28
|
parseImplementPlan,
|
|
27
29
|
parseReadyReceipt,
|
|
28
30
|
} from "./delivery-compiler.ts";
|
|
31
|
+
import type { DesignPlanDiagnostic } from "./design-diagnostics.ts";
|
|
29
32
|
import {
|
|
30
33
|
type DesignArtifactOperationFacts,
|
|
31
34
|
type DesignArtifactOperationOutcome,
|
|
@@ -33,6 +36,7 @@ import {
|
|
|
33
36
|
type DesignStatusProjection,
|
|
34
37
|
} from "./design-journal.ts";
|
|
35
38
|
import { OpenSpecCliError, type OpenSpecDiagnostic } from "./openspec-cli.ts";
|
|
39
|
+
import { planAuthoringSummary } from "./plan-draft-summary.ts";
|
|
36
40
|
import { canonicalJson } from "./run-state.ts";
|
|
37
41
|
import { RunStore } from "./run-store.ts";
|
|
38
42
|
import { observeSafePath } from "./safe-path.ts";
|
|
@@ -199,7 +203,53 @@ function lifecycleOperationId(kind: string, runId: string): string {
|
|
|
199
203
|
.slice(0, 32)}`;
|
|
200
204
|
}
|
|
201
205
|
|
|
202
|
-
export
|
|
206
|
+
export class DesignControlValidationError extends DesignPlanValidationError {
|
|
207
|
+
constructor(diagnostics: readonly DesignPlanDiagnostic[]) {
|
|
208
|
+
super("invalid-design-control-request", diagnostics);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
export function validateDesignControlRequest(value: unknown):
|
|
212
|
+
| { ok: true; value: DesignControlRequest }
|
|
213
|
+
| {
|
|
214
|
+
ok: false;
|
|
215
|
+
code: "invalid-design-control-request";
|
|
216
|
+
diagnostics: DesignPlanDiagnostic[];
|
|
217
|
+
} {
|
|
218
|
+
const parsed = parseDesignControlRequest(value);
|
|
219
|
+
if (parsed.ok) return parsed;
|
|
220
|
+
let diagnostics: DesignPlanDiagnostic[] = [];
|
|
221
|
+
if (
|
|
222
|
+
isRecord(value) &&
|
|
223
|
+
value.operation === "approve-gate" &&
|
|
224
|
+
value.gate === "gate-a" &&
|
|
225
|
+
typeof value.contract !== "string"
|
|
226
|
+
)
|
|
227
|
+
diagnostics = changeContractDiagnostics(value.contract);
|
|
228
|
+
if (!diagnostics.length) {
|
|
229
|
+
const field = !isRecord(value)
|
|
230
|
+
? "request"
|
|
231
|
+
: typeof value.operation !== "string"
|
|
232
|
+
? "operation"
|
|
233
|
+
: value.operation !== "start" &&
|
|
234
|
+
(typeof value.runId !== "string" || !IDENTIFIER.test(value.runId))
|
|
235
|
+
? "runId"
|
|
236
|
+
: !["status", "validate-plan-draft"].includes(
|
|
237
|
+
String(value.operation),
|
|
238
|
+
) &&
|
|
239
|
+
(typeof value.operationId !== "string" ||
|
|
240
|
+
!IDENTIFIER.test(value.operationId))
|
|
241
|
+
? "operationId"
|
|
242
|
+
: value.operation === "approve-gate"
|
|
243
|
+
? value.gate === "gate-a"
|
|
244
|
+
? "contract"
|
|
245
|
+
: "gate"
|
|
246
|
+
: "request";
|
|
247
|
+
diagnostics = [{ code: "design-control-field-invalid", field }];
|
|
248
|
+
}
|
|
249
|
+
return { ...parsed, diagnostics };
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function parseDesignControlRequest(
|
|
203
253
|
value: unknown,
|
|
204
254
|
):
|
|
205
255
|
| { ok: true; value: DesignControlRequest }
|
|
@@ -553,7 +603,8 @@ export class DesignController {
|
|
|
553
603
|
async execute(value: unknown): Promise<Record<string, unknown>> {
|
|
554
604
|
this.#assertOpen();
|
|
555
605
|
const validation = validateDesignControlRequest(value);
|
|
556
|
-
if (!validation.ok)
|
|
606
|
+
if (!validation.ok)
|
|
607
|
+
throw new DesignControlValidationError(validation.diagnostics);
|
|
557
608
|
const request = validation.value;
|
|
558
609
|
switch (request.operation) {
|
|
559
610
|
case "start":
|
|
@@ -631,6 +682,30 @@ export class DesignController {
|
|
|
631
682
|
completed: run.state === "completed",
|
|
632
683
|
...(run.pauseCode ? { pause: { code: run.pauseCode } } : {}),
|
|
633
684
|
legalOperations,
|
|
685
|
+
nextStep: terminal
|
|
686
|
+
? null
|
|
687
|
+
: !design.gates.gateA.current
|
|
688
|
+
? {
|
|
689
|
+
operation: "approve-gate",
|
|
690
|
+
instruction:
|
|
691
|
+
"Read applicable instructions, the affected source and existing tests, and the verification manifest together. Resolve only remaining substantive choices; use existing same-task approval. Submit the accepted structured ChangeContract, not a prose placeholder.",
|
|
692
|
+
}
|
|
693
|
+
: !design.change
|
|
694
|
+
? {
|
|
695
|
+
operation: "bind-change",
|
|
696
|
+
instruction: "Bind the approved change name to this runId.",
|
|
697
|
+
}
|
|
698
|
+
: !design.plan || !design.gates.gateB.current
|
|
699
|
+
? {
|
|
700
|
+
operation: "write-artifact",
|
|
701
|
+
instruction:
|
|
702
|
+
"Write proposal, design, specs, author task checkboxes with exact Scenario references, and plan-draft.json. Use the installed example named in the tool description; omit derived identities. Preflight then compile; compilation supplies verification bindings and Gate B.",
|
|
703
|
+
}
|
|
704
|
+
: {
|
|
705
|
+
operation: "finalize-delivery",
|
|
706
|
+
instruction:
|
|
707
|
+
"Finalize the prepared artifacts. If tasks or the draft changed since compilation, preflight and compile again first. No separate Gate B approval is needed.",
|
|
708
|
+
},
|
|
634
709
|
packetActions: terminal ? [] : ["finish"],
|
|
635
710
|
design,
|
|
636
711
|
};
|
|
@@ -866,6 +941,7 @@ export class DesignController {
|
|
|
866
941
|
{ code: "design-plan-draft-invalid", field: "plan-draft.json" },
|
|
867
942
|
]);
|
|
868
943
|
}
|
|
944
|
+
const authoredDraft = structuredClone(draft);
|
|
869
945
|
if (status.changeContract) {
|
|
870
946
|
if (!draft || typeof draft !== "object" || Array.isArray(draft))
|
|
871
947
|
throw new Error("change-contract-invalid");
|
|
@@ -888,7 +964,7 @@ export class DesignController {
|
|
|
888
964
|
{ code: "design-plan-change-mismatch", field: "changeId" },
|
|
889
965
|
]);
|
|
890
966
|
}
|
|
891
|
-
return { status, compiled };
|
|
967
|
+
return { status, compiled, authoredDraft };
|
|
892
968
|
}
|
|
893
969
|
|
|
894
970
|
#validatePlanDraft(
|
|
@@ -897,11 +973,12 @@ export class DesignController {
|
|
|
897
973
|
{ operation: "validate-plan-draft" }
|
|
898
974
|
>,
|
|
899
975
|
) {
|
|
900
|
-
const { compiled } = this.#readCompiledDraft(request.runId);
|
|
976
|
+
const { compiled, authoredDraft } = this.#readCompiledDraft(request.runId);
|
|
901
977
|
return {
|
|
902
978
|
operation: request.operation,
|
|
903
979
|
runId: request.runId,
|
|
904
980
|
valid: true,
|
|
981
|
+
summary: planAuthoringSummary(compiled.plan, authoredDraft),
|
|
905
982
|
plan: {
|
|
906
983
|
taskCount: compiled.plan.tasks.length,
|
|
907
984
|
outputCount: compiled.plan.outputs.length,
|
|
@@ -931,7 +1008,29 @@ export class DesignController {
|
|
|
931
1008
|
if (!change) throw new Error("design-change-required");
|
|
932
1009
|
const readyPath = safeRelative(change, "ready.yaml");
|
|
933
1010
|
const gateAPath = safeRelative(change, "gate-a.yaml");
|
|
1011
|
+
const tasksPath = safeRelative(change, "tasks.md");
|
|
1012
|
+
if (observeSafePath(this.consumerRoot, tasksPath).kind === "absent") {
|
|
1013
|
+
throw new DesignPlanValidationError(
|
|
1014
|
+
"design-traceability-input-unavailable",
|
|
1015
|
+
[{ code: "design-traceability-input-unavailable", path: "tasks.md" }],
|
|
1016
|
+
);
|
|
1017
|
+
}
|
|
1018
|
+
const priorTasks = boundedArtifactBytes(this.consumerRoot, tasksPath);
|
|
1019
|
+
const tasksBytes = Buffer.from(
|
|
1020
|
+
bindTaskVerifications(
|
|
1021
|
+
new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }).decode(
|
|
1022
|
+
priorTasks,
|
|
1023
|
+
),
|
|
1024
|
+
compiled.plan,
|
|
1025
|
+
),
|
|
1026
|
+
"utf8",
|
|
1027
|
+
);
|
|
1028
|
+
if (tasksBytes.length > MAX_DESIGN_FILE_BYTES) {
|
|
1029
|
+
throw new Error("design-artifact-content-too-large");
|
|
1030
|
+
}
|
|
1031
|
+
const tasksUnchanged = tasksBytes.equals(priorTasks);
|
|
934
1032
|
const unchanged =
|
|
1033
|
+
tasksUnchanged &&
|
|
935
1034
|
status.gates.gateB.current &&
|
|
936
1035
|
status.plan?.rawSha256 === compiled.rawSha256 &&
|
|
937
1036
|
status.plan.canonicalHash === compiled.planHash;
|
|
@@ -940,6 +1039,16 @@ export class DesignController {
|
|
|
940
1039
|
removeSafeFile(this.consumerRoot, gateAPath);
|
|
941
1040
|
}
|
|
942
1041
|
this.#assertFinalizationLease(lease);
|
|
1042
|
+
if (
|
|
1043
|
+
!boundedArtifactBytes(this.consumerRoot, tasksPath).equals(priorTasks)
|
|
1044
|
+
) {
|
|
1045
|
+
throw new Error("design-artifact-currentness-mismatch");
|
|
1046
|
+
}
|
|
1047
|
+
// The compilation journal commits only after both files are installed.
|
|
1048
|
+
// A crash before that point can replay this deterministic projection;
|
|
1049
|
+
// finalization always rechecks the plan and task bytes together.
|
|
1050
|
+
if (!tasksUnchanged)
|
|
1051
|
+
atomicWrite(this.consumerRoot, tasksPath, tasksBytes);
|
|
943
1052
|
atomicWrite(
|
|
944
1053
|
this.consumerRoot,
|
|
945
1054
|
safeRelative(change, "implement-plan.json"),
|