@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.
Files changed (49) hide show
  1. package/README.md +58 -4
  2. package/agents/design-explorer.md +17 -11
  3. package/agents/diagnosis-worker.md +9 -1
  4. package/agents/implementation-worker.md +3 -1
  5. package/config/plan-draft.example.json +122 -0
  6. package/config/plan-draft.multiple-tasks.example.json +345 -0
  7. package/package.json +5 -3
  8. package/prompts/abel-design.md +93 -44
  9. package/src/activity-contracts.ts +82 -0
  10. package/src/candidate-artifact.ts +143 -0
  11. package/src/candidate-patch.ts +31 -11
  12. package/src/candidate-workspace.ts +74 -0
  13. package/src/change-contract-schema.ts +191 -0
  14. package/src/change-contract.ts +5 -0
  15. package/src/change-verification.ts +896 -0
  16. package/src/child-model.ts +129 -0
  17. package/src/child-session.ts +293 -326
  18. package/src/contracts.ts +184 -76
  19. package/src/delivery-compiler.ts +103 -120
  20. package/src/delivery-revision.ts +42 -0
  21. package/src/design-control.ts +113 -4
  22. package/src/design-diagnostics.ts +150 -0
  23. package/src/durable-contracts.ts +210 -0
  24. package/src/durable-workflow.ts +148 -3570
  25. package/src/evidence-draft.ts +122 -0
  26. package/src/implement-graph.ts +3 -0
  27. package/src/implement-plan.ts +70 -0
  28. package/src/index.ts +59 -522
  29. package/src/model-source.ts +22 -0
  30. package/src/package-candidate.ts +224 -0
  31. package/src/package-workflow.ts +253 -0
  32. package/src/packet-runtime.ts +27 -48
  33. package/src/parent-provider.ts +86 -198
  34. package/src/phase-execution.ts +2248 -0
  35. package/src/pi-adapter.ts +19 -0
  36. package/src/plan-draft-summary.ts +146 -0
  37. package/src/plan-draft.ts +493 -0
  38. package/src/subagent-activity.ts +32 -66
  39. package/src/submit-tool.ts +20 -59
  40. package/src/transport-budget.ts +32 -0
  41. package/src/worker-broker.ts +37 -57
  42. package/src/workflow-policy.ts +14 -53
  43. package/src/workflow-recovery-policy.ts +95 -0
  44. package/src/workflow-scheduling.ts +105 -0
  45. package/src/workflow-state-machine.ts +105 -497
  46. package/src/workflow-status.ts +384 -0
  47. package/src/workspace-io-worker.mjs +7 -0
  48. package/src/empty-resource-loader.ts +0 -38
  49. package/src/parent-payload-bridge.ts +0 -523
@@ -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
- ...input.tasksMarkdown.matchAll(/`(specs\/[^`\n]+\/spec\.md#[^`\n]+)`/gu),
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) => !input.tasksMarkdown.includes(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 PlanPhaseDraft = PhaseBoundary;
264
-
265
- export interface PlanTaskDraft extends Omit<ImplementTaskBoundary, "phases"> {
266
- phases: {
267
- red: PlanPhaseDraft;
268
- green: PlanPhaseDraft;
269
- refactor?: PlanPhaseDraft;
270
- };
271
- affectedVerification: StructuredVerificationContract;
272
- repairVerification: StructuredVerificationContract;
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): PlanPhaseDraft {
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: PlanPhaseDraft): PhaseBoundary => ({
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 = ["# Implementation tasks", ""];
1252
- for (const task of plan.tasks) {
1253
- lines.push(`- [ ] ${task.taskId} — ${task.objective}`);
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
- ` - Depends on: ${task.dependsOn.length > 0 ? task.dependsOn.join(", ") : "[]"}`,
1217
+ `- Task \`${task.taskId}\` (${task.verificationMode ?? "behavior"})`,
1256
1218
  );
1257
- if (!task.verificationMode || task.verificationMode === "behavior")
1258
- lines.push(` - Red verification: ${task.phases.red.verification.id}`);
1259
- else
1260
- lines.push(
1261
- ` - Evidence mode: ${task.verificationMode}; baseline and postconditions required`,
1262
- );
1263
- lines.push(` - Green verification: ${task.phases.green.verification.id}`);
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
+ }
@@ -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 function validateDesignControlRequest(
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) throw new Error(validation.code);
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"),